mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +08:00
feat(skill): MCP-Skill bridge + system tool dedup + Lessons i18n
This commit is contained in:
parent
18c5c00797
commit
b759eb8719
@ -191,6 +191,14 @@ public class AgentBindingService {
|
||||
for (Long skillId : boundSkillIds) {
|
||||
ResolvedSkill resolved = findResolvedSkillById(skillId);
|
||||
if (resolved == null) continue;
|
||||
if (!vip.mate.skill.runtime.SkillRuntimeService.passesActiveGate(resolved)) {
|
||||
// §14.2 fix: a disabled / security-blocked / setup-needed
|
||||
// skill must not contribute tools to the LLM
|
||||
// advertisement even if it's still bound. Without this
|
||||
// guard, users see ghost tools for skills they thought
|
||||
// were off.
|
||||
continue;
|
||||
}
|
||||
Set<String> skillTools = resolved.getEffectiveAllowedTools();
|
||||
if (skillTools != null && !skillTools.isEmpty()) merged.addAll(skillTools);
|
||||
}
|
||||
@ -201,9 +209,46 @@ public class AgentBindingService {
|
||||
merged.addAll(directTools);
|
||||
}
|
||||
|
||||
// System-level tools that don't belong to any single skill but
|
||||
// are agent-wide capabilities. Without this carve-out, binding
|
||||
// any skill silently strips record_lesson / remember / structured-
|
||||
// memory tools, breaking the §11 self-evolution loop entirely
|
||||
// (the LLM stops being able to write to LESSONS.md / MEMORY.md).
|
||||
merged.addAll(SYSTEM_LEVEL_TOOLS);
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-090 §11 — tools that exist outside the skill scope and must
|
||||
* survive any agent-level skill binding restriction.
|
||||
*
|
||||
* <p>Add new entries here only after verifying the tool is genuinely
|
||||
* agent-wide, not skill-specific. Tools added here bypass the
|
||||
* {@link #getEffectiveToolNames} allowlist completely.
|
||||
*/
|
||||
private static final Set<String> SYSTEM_LEVEL_TOOLS = Set.of(
|
||||
// Memory write/read primitives — every agent needs these
|
||||
// regardless of skill bindings, otherwise the self-evolution
|
||||
// path collapses (§11.3 / §11.4).
|
||||
"record_lesson",
|
||||
"remember",
|
||||
"remember_structured",
|
||||
"recall_structured",
|
||||
"forget_structured",
|
||||
// Workspace memory file CRUD (PROFILE.md / MEMORY.md / SOUL.md)
|
||||
"read_workspace_file",
|
||||
"write_workspace_file",
|
||||
"list_workspace_files",
|
||||
// Skill discovery / dispatch — skills are docs, not callables;
|
||||
// these helpers let the LLM read SKILL.md / run scripts.
|
||||
"readSkillFile",
|
||||
"runSkillScript",
|
||||
// Date/time + delegate — fundamental cross-skill utilities
|
||||
"datetime",
|
||||
"delegate_agent"
|
||||
);
|
||||
|
||||
private ResolvedSkill findResolvedSkillById(Long skillId) {
|
||||
if (skillId == null || skillRuntimeService == null) return null;
|
||||
// resolveAllSkillsStatus returns every skill in the catalog, not
|
||||
|
||||
@ -50,6 +50,7 @@ public class SkillController {
|
||||
private final AgentSkillBindingMapper agentSkillBindingMapper;
|
||||
private final AgentService agentService;
|
||||
private final AgentBindingService agentBindingService;
|
||||
private final vip.mate.skill.mcp.McpSkillBridge mcpSkillBridge;
|
||||
|
||||
@Operation(summary = "获取技能分页列表(RFC-042 §2.1)")
|
||||
@GetMapping
|
||||
@ -60,7 +61,39 @@ public class SkillController {
|
||||
@RequestParam(required = false) String skillType,
|
||||
@RequestParam(required = false) Boolean enabled,
|
||||
@RequestParam(required = false) String scanStatus) {
|
||||
return R.ok(skillService.pageSkills(page, size, keyword, skillType, enabled, scanStatus));
|
||||
IPage<SkillEntity> dbPage = skillService.pageSkills(page, size, keyword, skillType, enabled, scanStatus);
|
||||
|
||||
// RFC-090 §3.2 — surface MCP servers as virtual skills on the
|
||||
// first page so users see GitHub / Filesystem / etc. as cards
|
||||
// alongside built-in and uploaded skills. Restricted to page 1
|
||||
// because virtual rows are unpaginated; a follow-up can push
|
||||
// them through proper SQL UNION ALL when MCP server count gets
|
||||
// into the dozens.
|
||||
if (page == 1 && (skillType == null || skillType.isBlank() || "mcp".equalsIgnoreCase(skillType))) {
|
||||
try {
|
||||
List<SkillEntity> mcpSkills = mcpSkillBridge.listMcpDerivedSkillEntities();
|
||||
if (!mcpSkills.isEmpty()) {
|
||||
// In-memory filter mirrors the DB filters so the
|
||||
// user's filter chips still apply to virtual rows.
|
||||
String kw = keyword == null ? "" : keyword.trim().toLowerCase();
|
||||
List<SkillEntity> filtered = mcpSkills.stream()
|
||||
.filter(s -> kw.isEmpty()
|
||||
|| (s.getName() != null && s.getName().toLowerCase().contains(kw))
|
||||
|| (s.getDescription() != null && s.getDescription().toLowerCase().contains(kw)))
|
||||
.filter(s -> enabled == null || enabled.equals(s.getEnabled()))
|
||||
.toList();
|
||||
if (!filtered.isEmpty()) {
|
||||
java.util.List<SkillEntity> merged = new java.util.ArrayList<>(filtered);
|
||||
merged.addAll(dbPage.getRecords());
|
||||
dbPage.setRecords(merged);
|
||||
dbPage.setTotal(dbPage.getTotal() + filtered.size());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Bridge failure must not break the Skills page.
|
||||
}
|
||||
}
|
||||
return R.ok(dbPage);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取各类型技能计数(tab 徽章用)")
|
||||
@ -96,6 +129,16 @@ public class SkillController {
|
||||
@Operation(summary = "获取技能详情")
|
||||
@GetMapping("/{id}")
|
||||
public R<SkillEntity> get(@PathVariable Long id) {
|
||||
// RFC-090 §3.2 — virtual MCP-derived skills synthesize a row
|
||||
// on demand from the live MCP server entity.
|
||||
if (vip.mate.skill.mcp.McpSkillBridge.isVirtualMcpSkillId(id)) {
|
||||
List<SkillEntity> virt = mcpSkillBridge.listMcpDerivedSkillEntities();
|
||||
return virt.stream()
|
||||
.filter(s -> id.equals(s.getId()))
|
||||
.findFirst()
|
||||
.map(R::ok)
|
||||
.orElse(R.fail("MCP-derived skill not found: " + id));
|
||||
}
|
||||
return R.ok(skillService.getSkill(id));
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,324 @@
|
||||
package vip.mate.skill.mcp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import vip.mate.skill.manifest.SkillManifest;
|
||||
import vip.mate.skill.model.SkillEntity;
|
||||
import vip.mate.skill.runtime.model.ResolvedSkill;
|
||||
import vip.mate.tool.mcp.model.McpServerEntity;
|
||||
import vip.mate.tool.mcp.runtime.McpClientManager;
|
||||
import vip.mate.tool.mcp.service.McpServerService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* RFC-090 §3.2 / §5.7 / §10.2 Q2 — MCP server → virtual skill bridge.
|
||||
*
|
||||
* <p>MCP servers and skills are the same thing from the user's
|
||||
* perspective: capability supply for digital employees. The protocol
|
||||
* ({@code mate_mcp_server}) is implementation detail. This bridge
|
||||
* makes that consistent: every enabled MCP server becomes a virtual
|
||||
* {@link SkillEntity} + {@link ResolvedSkill} and shows up on the
|
||||
* Skills page exactly like a built-in or uploaded skill.
|
||||
*
|
||||
* <p>Why "virtual" not "persisted":
|
||||
* <ul>
|
||||
* <li>Single source of truth — MCP server definition lives in
|
||||
* {@code mate_mcp_server}, not duplicated to {@code mate_skill}.
|
||||
* Avoids the double-write drift §14.6 warns about.</li>
|
||||
* <li>Discovered tools change with the upstream server, not at our
|
||||
* cadence. A virtual skill rebuilds on every list call so the
|
||||
* tool count stays fresh.</li>
|
||||
* <li>Settings ▸ MCP Connections remains the only place to edit
|
||||
* transport / command / args / env. Skills page is read-only
|
||||
* for MCP entries (View opens drawer; "Configure connection"
|
||||
* links back to the MCP page).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>ID namespace: virtual skill ids use a high sentinel
|
||||
* {@link #VIRTUAL_ID_BASE} + mcpServerId so they can never collide
|
||||
* with real {@code mate_skill.id} values (Snowflake longs are bounded
|
||||
* well below this base). Negative numbers were considered but several
|
||||
* existing endpoints {@code abs()} the id for path constraints.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class McpSkillBridge {
|
||||
|
||||
/**
|
||||
* High sentinel for virtual id space. Snowflake ids fit in 63 bits
|
||||
* but in practice never approach this magnitude, so anything
|
||||
* {@code >= VIRTUAL_ID_BASE} is unambiguously a bridged MCP skill.
|
||||
*/
|
||||
public static final long VIRTUAL_ID_BASE = 9_000_000_000_000_000_000L;
|
||||
|
||||
private final McpServerService mcpServerService;
|
||||
private final McpClientManager mcpClientManager;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* @return true iff the given id falls inside the virtual MCP skill
|
||||
* range. Cheap O(1) check, callers use it to route lookups
|
||||
* between the real DB and this bridge.
|
||||
*/
|
||||
public static boolean isVirtualMcpSkillId(Long id) {
|
||||
return id != null && id >= VIRTUAL_ID_BASE;
|
||||
}
|
||||
|
||||
/** Inverse mapping: extract the original MCP server id. */
|
||||
public static Long extractMcpServerId(Long virtualId) {
|
||||
if (!isVirtualMcpSkillId(virtualId)) return null;
|
||||
return virtualId - VIRTUAL_ID_BASE;
|
||||
}
|
||||
|
||||
public static long virtualIdFor(McpServerEntity server) {
|
||||
return VIRTUAL_ID_BASE + server.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot every enabled MCP server as a virtual {@link SkillEntity}.
|
||||
* Used by the Skills list endpoint; rows are non-persistent and
|
||||
* regenerated on each call.
|
||||
*/
|
||||
public List<SkillEntity> listMcpDerivedSkillEntities() {
|
||||
return listEnabledServers().stream().map(this::serverToEntity).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot every enabled MCP server as a virtual {@link ResolvedSkill}
|
||||
* with synthesized manifest, ready to be merged into the runtime
|
||||
* status feed. Status reflects connection health: OK → READY default
|
||||
* feature; ERROR / disconnected → SETUP_NEEDED with a diagnostic
|
||||
* missing-dependency entry.
|
||||
*/
|
||||
public List<ResolvedSkill> listMcpDerivedResolvedSkills() {
|
||||
return listEnabledServers().stream().map(this::serverToResolved).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a single virtual ResolvedSkill by virtual id; null when
|
||||
* the id is out of range or the server has been removed.
|
||||
*/
|
||||
public ResolvedSkill findResolvedById(Long virtualId) {
|
||||
Long serverId = extractMcpServerId(virtualId);
|
||||
if (serverId == null) return null;
|
||||
try {
|
||||
McpServerEntity server = mcpServerService.getById(serverId);
|
||||
return server != null ? serverToResolved(server) : null;
|
||||
} catch (Exception e) {
|
||||
log.debug("MCP bridge lookup failed for virtual id {}: {}", virtualId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<McpServerEntity> listEnabledServers() {
|
||||
try {
|
||||
return mcpServerService.listEnabled();
|
||||
} catch (Exception e) {
|
||||
log.warn("MCP bridge could not list enabled servers: {}", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private SkillEntity serverToEntity(McpServerEntity server) {
|
||||
SkillEntity s = new SkillEntity();
|
||||
s.setId(virtualIdFor(server));
|
||||
s.setName(slugify(server.getName()));
|
||||
s.setNameEn(displayName(server));
|
||||
s.setNameZh(server.getDescription() != null && !server.getDescription().isBlank()
|
||||
? displayName(server) : null);
|
||||
s.setDescription(buildDescription(server));
|
||||
s.setSkillType("mcp");
|
||||
s.setIcon(iconFor(server));
|
||||
s.setVersion("1.0.0");
|
||||
s.setAuthor("mcp-bridge");
|
||||
s.setEnabled(Boolean.TRUE.equals(server.getEnabled()));
|
||||
s.setBuiltin(false);
|
||||
s.setTags("mcp");
|
||||
s.setSecurityScanStatus("PASSED"); // MCP servers don't go through SkillSecurityService
|
||||
s.setConfigJson(buildConfigJson(server));
|
||||
s.setManifestJson(serializeManifest(buildManifest(server)));
|
||||
return s;
|
||||
}
|
||||
|
||||
private ResolvedSkill serverToResolved(McpServerEntity server) {
|
||||
SkillManifest manifest = buildManifest(server);
|
||||
boolean connected = "connected".equalsIgnoreCase(nullSafe(server.getLastStatus()));
|
||||
boolean errored = "error".equalsIgnoreCase(nullSafe(server.getLastStatus()))
|
||||
|| (server.getLastError() != null && !server.getLastError().isBlank());
|
||||
|
||||
Map<String, String> featureStatuses = new LinkedHashMap<>();
|
||||
featureStatuses.put("default", connected ? "READY" : (errored ? "SETUP_NEEDED" : "SETUP_NEEDED"));
|
||||
java.util.Set<String> active = new LinkedHashSet<>();
|
||||
if (connected) active.add("default");
|
||||
|
||||
List<String> missing = new ArrayList<>();
|
||||
if (!connected) {
|
||||
missing.add("mcp:" + server.getName() + " (status: "
|
||||
+ nullSafe(server.getLastStatus()) + ")");
|
||||
}
|
||||
|
||||
return ResolvedSkill.builder()
|
||||
.id(virtualIdFor(server))
|
||||
.name(slugify(server.getName()))
|
||||
.description(buildDescription(server))
|
||||
.content("") // no SKILL.md
|
||||
.source("mcp")
|
||||
.skillDir(null)
|
||||
.configuredSkillDir(null)
|
||||
.runtimeAvailable(connected)
|
||||
.resolutionError(connected ? null : nullSafe(server.getLastError()))
|
||||
.references(Map.of())
|
||||
.scripts(Map.of())
|
||||
.enabled(Boolean.TRUE.equals(server.getEnabled()))
|
||||
.icon(iconFor(server))
|
||||
.builtin(false)
|
||||
.securityBlocked(false)
|
||||
.securitySummary("MCP-derived skill (bypasses SkillSecurityService)")
|
||||
.dependencyReady(connected)
|
||||
.missingDependencies(missing)
|
||||
.dependencySummary(connected
|
||||
? "MCP server '" + server.getName() + "' connected"
|
||||
: "MCP server '" + server.getName() + "' not connected")
|
||||
.manifest(manifest)
|
||||
.featureStatuses(featureStatuses)
|
||||
.activeFeatures(active)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-generate the §10.2 Q2 minimal manifest from the live MCP
|
||||
* server. Tool list is the union of discovered MCP tools; one
|
||||
* synthetic feature {@code default} carries them so the standard
|
||||
* features-aware gate light up correctly.
|
||||
*/
|
||||
private SkillManifest buildManifest(McpServerEntity server) {
|
||||
List<String> toolNames = new ArrayList<>();
|
||||
try {
|
||||
List<McpSchema.Tool> discovered = mcpClientManager.getServerTools(server.getId());
|
||||
for (McpSchema.Tool t : discovered) {
|
||||
if (t == null) continue;
|
||||
String n = t.name();
|
||||
if (n != null && !n.isBlank()) toolNames.add(n);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("MCP bridge manifest build: getServerTools({}) failed: {}",
|
||||
server.getId(), e.getMessage());
|
||||
}
|
||||
|
||||
SkillManifest.FeatureDef defaultFeature = SkillManifest.FeatureDef.builder()
|
||||
.id("default")
|
||||
.label(displayName(server))
|
||||
.requires(List.of("mcp:" + server.getName()))
|
||||
.platforms(List.of())
|
||||
.tools(toolNames)
|
||||
.build();
|
||||
|
||||
SkillManifest.RequirementDef mcpRequirement = SkillManifest.RequirementDef.builder()
|
||||
.key("mcp:" + server.getName())
|
||||
.type("mcp")
|
||||
.check(server.getName())
|
||||
.description("MCP server '" + server.getName() + "' must be connected. Configure in Settings ▸ MCP Connections.")
|
||||
.build();
|
||||
|
||||
return SkillManifest.builder()
|
||||
.id(slugify(server.getName()))
|
||||
.name(slugify(server.getName()))
|
||||
.description(buildDescription(server))
|
||||
.icon(iconFor(server))
|
||||
.version("1.0.0")
|
||||
.author("mcp-bridge")
|
||||
.type("mcp")
|
||||
.category(categoryFor(server))
|
||||
.allowedTools(toolNames)
|
||||
.requires(List.of(mcpRequirement))
|
||||
.features(List.of(defaultFeature))
|
||||
.selfEvolution(SkillManifest.SelfEvolution.builder()
|
||||
// MCP-derived skills don't write LESSONS.md — the
|
||||
// upstream protocol layer is the canonical source.
|
||||
.lessonsEnabled(false)
|
||||
.lessonsMaxEntries(0)
|
||||
.memoryWritesAllowed(true)
|
||||
.build())
|
||||
.extras(Map.of("mcpServerId", server.getId()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String slugify(String raw) {
|
||||
if (raw == null) return "";
|
||||
return raw.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "-");
|
||||
}
|
||||
|
||||
private String displayName(McpServerEntity server) {
|
||||
return server.getName() != null ? server.getName() : "mcp-" + server.getId();
|
||||
}
|
||||
|
||||
private String buildDescription(McpServerEntity server) {
|
||||
if (server.getDescription() != null && !server.getDescription().isBlank()) {
|
||||
return server.getDescription();
|
||||
}
|
||||
int toolCount = server.getToolCount() == null ? 0 : server.getToolCount();
|
||||
return "MCP server " + server.getName()
|
||||
+ (toolCount > 0 ? " · provides " + toolCount + " tools" : "")
|
||||
+ ". Configure in Settings ▸ MCP Connections.";
|
||||
}
|
||||
|
||||
private String iconFor(McpServerEntity server) {
|
||||
// Light heuristic — pick something recognisable for the most
|
||||
// popular MCP servers, fall back to the generic plug emoji.
|
||||
String n = nullSafe(server.getName()).toLowerCase(Locale.ROOT);
|
||||
if (n.contains("github")) return "🐙";
|
||||
if (n.contains("gitlab")) return "🦊";
|
||||
if (n.contains("filesystem") || n.contains("file")) return "📁";
|
||||
if (n.contains("postgres") || n.contains("mysql") || n.contains("sql") || n.contains("db")) return "🗄️";
|
||||
if (n.contains("slack")) return "💬";
|
||||
if (n.contains("notion")) return "📝";
|
||||
if (n.contains("memory")) return "🧠";
|
||||
if (n.contains("brave") || n.contains("search")) return "🔍";
|
||||
if (n.contains("puppeteer") || n.contains("browser")) return "🌐";
|
||||
return "🔌";
|
||||
}
|
||||
|
||||
private String categoryFor(McpServerEntity server) {
|
||||
String n = nullSafe(server.getName()).toLowerCase(Locale.ROOT);
|
||||
if (n.contains("github") || n.contains("gitlab")) return "system";
|
||||
if (n.contains("file")) return "file";
|
||||
if (n.contains("sql") || n.contains("postgres") || n.contains("db")) return "data";
|
||||
if (n.contains("search") || n.contains("brave")) return "web";
|
||||
if (n.contains("slack") || n.contains("notion")) return "comm";
|
||||
return "system";
|
||||
}
|
||||
|
||||
private String buildConfigJson(McpServerEntity server) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(Map.of(
|
||||
"mcpServerId", server.getId(),
|
||||
"transport", nullSafe(server.getTransport()),
|
||||
"source", Map.of("type", "mcp")
|
||||
));
|
||||
} catch (Exception e) {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
private String serializeManifest(SkillManifest manifest) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(manifest);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String nullSafe(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import vip.mate.skill.lessons.SkillLessonsService;
|
||||
import vip.mate.skill.manifest.SkillManifest;
|
||||
import vip.mate.skill.mcp.McpSkillBridge;
|
||||
import vip.mate.skill.model.SkillEntity;
|
||||
import vip.mate.skill.runtime.model.ResolvedSkill;
|
||||
import vip.mate.skill.service.SkillService;
|
||||
@ -40,14 +41,22 @@ public class SkillRuntimeService {
|
||||
* constructor below.
|
||||
*/
|
||||
private final SkillLessonsService lessonsService;
|
||||
/**
|
||||
* RFC-090 §3.2 / §10.2 Q2 — MCP-server → virtual-skill bridge.
|
||||
* {@code @Lazy} because the bridge depends on McpClientManager
|
||||
* which boots later in the lifecycle.
|
||||
*/
|
||||
private final McpSkillBridge mcpSkillBridge;
|
||||
|
||||
@Autowired
|
||||
public SkillRuntimeService(SkillService skillService,
|
||||
SkillPackageResolver packageResolver,
|
||||
@Lazy SkillLessonsService lessonsService) {
|
||||
@Lazy SkillLessonsService lessonsService,
|
||||
@Lazy McpSkillBridge mcpSkillBridge) {
|
||||
this.skillService = skillService;
|
||||
this.packageResolver = packageResolver;
|
||||
this.lessonsService = lessonsService;
|
||||
this.mcpSkillBridge = mcpSkillBridge;
|
||||
}
|
||||
|
||||
// 缓存已解析的 active skills(5分钟过期)
|
||||
@ -125,6 +134,16 @@ public class SkillRuntimeService {
|
||||
.map(packageResolver::resolve)
|
||||
.filter(SkillRuntimeService::passesActiveGate)
|
||||
.collect(Collectors.toList());
|
||||
try {
|
||||
// RFC-090 §3.2 — MCP-derived virtual skills go through the
|
||||
// same active gate so a disconnected MCP server doesn't
|
||||
// pollute the prompt enhancement.
|
||||
for (ResolvedSkill virt : mcpSkillBridge.listMcpDerivedResolvedSkills()) {
|
||||
if (passesActiveGate(virt)) resolved.add(virt);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("MCP skill bridge active merge failed: {}", e.getMessage());
|
||||
}
|
||||
|
||||
activeSkillsCache.put(CACHE_KEY, resolved);
|
||||
log.info("Refreshed active skills: {} enabled", resolved.size());
|
||||
@ -134,12 +153,23 @@ public class SkillRuntimeService {
|
||||
|
||||
/**
|
||||
* 解析所有技能的运行时状态(管理页面使用,包含 disabled 和 error 信息)
|
||||
*
|
||||
* <p>RFC-090 §3.2 — appends virtual MCP-derived skills so the Skills
|
||||
* page can render MCP servers as first-class skill cards. Real
|
||||
* skills resolve through the full pipeline; virtual ones come
|
||||
* pre-built from {@link McpSkillBridge}.
|
||||
*/
|
||||
public List<ResolvedSkill> resolveAllSkillsStatus() {
|
||||
List<SkillEntity> allSkills = skillService.listSkills();
|
||||
return allSkills.stream()
|
||||
List<ResolvedSkill> resolved = allSkills.stream()
|
||||
.map(packageResolver::resolve)
|
||||
.collect(Collectors.toList());
|
||||
try {
|
||||
resolved.addAll(mcpSkillBridge.listMcpDerivedResolvedSkills());
|
||||
} catch (Exception e) {
|
||||
log.warn("MCP skill bridge merge failed: {}", e.getMessage());
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -2047,7 +2047,7 @@ export default {
|
||||
local: '本地',
|
||||
},
|
||||
usedByTitle: '已绑定此技能的 Agent 数量',
|
||||
lessonsCountTitle: '已记录的 lessons — 点击查看',
|
||||
lessonsCountTitle: '已记录的经验数 — 点击查看',
|
||||
actions: {
|
||||
configure: '配置',
|
||||
view: '查看',
|
||||
@ -2075,21 +2075,21 @@ export default {
|
||||
manifest: 'Manifest',
|
||||
tools: '工具',
|
||||
features: '特性',
|
||||
lessons: 'Lessons',
|
||||
lessons: '经验',
|
||||
memory: '记忆',
|
||||
noManifest: '该技能未声明 v3 manifest,使用旧字段。',
|
||||
noTools: '该 skill 未声明 allowed-tools。绑定到 agent 时 LLM 将回退到全局工具集。',
|
||||
noFeatures: '未声明 features[] 矩阵,技能被视为单一默认特性。',
|
||||
noLessons: '尚未记录任何 lesson。Lessons 由 LLM 通过 record_lesson 工具写入 — 在对话中告诉 agent「请记录这条经验到 {skill 名}」即可触发。',
|
||||
noLessons: '尚未记录任何经验。经验由 LLM 通过 record_lesson 工具写入 — 在对话中告诉 agent「请把这条经验记录到「{skill 名}」技能下」即可触发。',
|
||||
noEmployees: '该 skill 当前没有任何 Agent 可以使用。要么在 Agent 的 Skills tab 中显式绑定,要么把 skill 设为全局启用让无显式绑定的 Agent 自动可见。',
|
||||
toolsHint: 'Skill 绑定到 agent 时,这些工具名将合并进 LLM 的 allowed-tools。SETUP_NEEDED 特性下的工具保持隐藏。',
|
||||
lessonsHint: '该 skill 的 LESSONS.md 内容。下次加载时自动注入到 SKILL.md 正文之后。',
|
||||
lessonsHint: '该 skill 累积的经验(LESSONS.md)。下次加载时自动注入到 SKILL.md 正文之后,让 agent 越用越熟。',
|
||||
memoryHint: '可使用此 skill 的 Agents。显式绑定 = 在 Agent Skills tab 中明确选中;隐式可见 = 该 Agent 未做任何 Skill 显式绑定,自动可见所有全局启用的 skill。',
|
||||
openMemory: '查看 Agent 记忆',
|
||||
bindingExplicit: '显式绑定',
|
||||
bindingImplicit: '隐式可见',
|
||||
clearLessons: '全部清空',
|
||||
clearLessonsConfirm: '删除该 skill 的全部 lessons?操作不可撤销。',
|
||||
clearLessonsConfirm: '删除该 skill 的全部经验?操作不可撤销。',
|
||||
},
|
||||
runtime: {
|
||||
disabled: '已停用',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user