fix(skill): blank Tools / Features / Memory detail tabs

This commit is contained in:
matevip 2026-05-01 09:50:32 +08:00
parent d76a5b994f
commit f8524bf122
6 changed files with 114 additions and 30 deletions

View File

@ -9,6 +9,7 @@ import vip.mate.common.result.R;
import vip.mate.agent.AgentService;
import vip.mate.agent.binding.model.AgentSkillBinding;
import vip.mate.agent.binding.repository.AgentSkillBindingMapper;
import vip.mate.agent.binding.service.AgentBindingService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import vip.mate.agent.model.AgentEntity;
import vip.mate.skill.lessons.SkillLessonsService;
@ -48,6 +49,7 @@ public class SkillController {
private final SkillLessonsService lessonsService;
private final AgentSkillBindingMapper agentSkillBindingMapper;
private final AgentService agentService;
private final AgentBindingService agentBindingService;
@Operation(summary = "获取技能分页列表RFC-042 §2.1")
@GetMapping
@ -230,35 +232,84 @@ public class SkillController {
// ==================== Reverse lookup (RFC-090 §7) ====================
/**
* RFC-090 §7 list agents (employees) currently bound to this
* skill so the skill card can render "Used by N employees" with a
* click-through. Returns lightweight {id, name, icon} rows; the
* full agent object would balloon the card payload.
* RFC-090 §7 / §14.2 list agents (employees) for which this
* skill is reachable.
*
* <p>Two coverage paths, both reflected in the response:
* <ol>
* <li><b>Explicit</b> there's a {@code mate_agent_skill} row
* with this skill id and {@code enabled=true}.</li>
* <li><b>Implicit</b> the agent has no explicit skill binding
* at all ({@code AgentBindingService.getBoundSkillIds}
* returns null), which the three-state contract treats as
* "use every globally-enabled skill". Most users never wire
* explicit bindings, so without this branch the count was
* always zero even for skills clearly visible to the LLM.</li>
* </ol>
*
* <p>Each row carries {@code binding: "explicit" | "implicit"} so
* the UI can label the relationship.
*/
@Operation(summary = "List agents bound to this skill (RFC-090)")
@Operation(summary = "List agents that can use this skill (RFC-090 §14.2)")
@GetMapping("/{id}/employees")
public R<List<Map<String, Object>>> employees(@PathVariable Long id) {
List<AgentSkillBinding> bindings = agentSkillBindingMapper.selectList(
// Explicit bindings: agent_skill rows pointing to this skill.
List<AgentSkillBinding> explicitBindings = agentSkillBindingMapper.selectList(
new LambdaQueryWrapper<AgentSkillBinding>()
.eq(AgentSkillBinding::getSkillId, id)
.eq(AgentSkillBinding::getEnabled, true));
List<Map<String, Object>> rows = new ArrayList<>(bindings.size());
for (AgentSkillBinding b : bindings) {
try {
AgentEntity agent = agentService.getAgent(b.getAgentId());
if (agent == null) continue;
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", agent.getId());
row.put("name", agent.getName());
row.put("icon", agent.getIcon());
rows.add(row);
} catch (Exception ignored) {
// agent may have been deleted; skip rather than fail the whole list
java.util.Set<Long> explicitAgentIds = new java.util.LinkedHashSet<>();
for (AgentSkillBinding b : explicitBindings) explicitAgentIds.add(b.getAgentId());
// Implicit bindings: agents whose getBoundSkillIds() == null
// (no explicit row in agent_skill at all). They get every
// globally-enabled skill, which includes this one provided the
// skill itself is enabled.
List<AgentEntity> allAgents = agentService.listAgents();
java.util.Set<Long> implicitAgentIds = new java.util.LinkedHashSet<>();
SkillEntity skill;
try {
skill = skillService.getSkill(id);
} catch (Exception e) {
skill = null;
}
boolean skillGloballyAvailable = skill != null && Boolean.TRUE.equals(skill.getEnabled());
if (skillGloballyAvailable) {
for (AgentEntity agent : allAgents) {
if (explicitAgentIds.contains(agent.getId())) continue;
java.util.Set<Long> bound = agentBindingService.getBoundSkillIds(agent.getId());
// null no explicit bindings uses every enabled skill
if (bound == null) implicitAgentIds.add(agent.getId());
}
}
java.util.List<Map<String, Object>> rows = new ArrayList<>();
// Stable order: explicit first (most "intentional" relationship),
// then implicit, agents inside each group keep DB insert order.
for (Long agentId : explicitAgentIds) {
appendAgentRow(rows, agentId, "explicit");
}
for (Long agentId : implicitAgentIds) {
appendAgentRow(rows, agentId, "implicit");
}
return R.ok(rows);
}
private void appendAgentRow(List<Map<String, Object>> rows, Long agentId, String binding) {
try {
AgentEntity agent = agentService.getAgent(agentId);
if (agent == null) return;
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", agent.getId());
row.put("name", agent.getName());
row.put("icon", agent.getIcon());
row.put("binding", binding);
rows.add(row);
} catch (Exception ignored) {
// agent may have been deleted; skip rather than fail the whole list
}
}
// ==================== Lessons API (RFC-090 §7 + §11.4) ====================
/**

View File

@ -71,6 +71,18 @@ public class SkillManifestParser {
}
Map<String, Object> fm = parsed.getFrontmatter();
// RFC-090 §5.4 Anthropic-compatible `allowed-tools` is preferred, but
// 99% of legacy SKILL.md files declare tools via `dependencies.tools`
// (per the original SkillFrontmatterParser shape). Fall back to that
// list when v3 fields are absent so old skills stop rendering an
// empty Tools tab in the detail drawer.
List<String> v3AllowedTools = stringList(coalesce(fm, "allowed-tools", "allowed_tools"));
List<String> effectiveAllowedTools = v3AllowedTools.isEmpty()
&& parsed.getDependencies() != null
&& !parsed.getDependencies().getTools().isEmpty()
? new ArrayList<>(parsed.getDependencies().getTools())
: v3AllowedTools;
SkillManifest.SkillManifestBuilder b = SkillManifest.builder()
.id(string(fm, "id"))
.name(string(fm, "name"))
@ -80,7 +92,7 @@ public class SkillManifestParser {
.author(string(fm, "author"))
.type(string(fm, "type"))
.category(string(fm, "category"))
.allowedTools(stringList(coalesce(fm, "allowed-tools", "allowed_tools")))
.allowedTools(effectiveAllowedTools)
.platforms(parsed.getPlatforms() == null ? List.of() : parsed.getPlatforms())
.requires(parseRequires(fm.get("requires"), parsed.getDependencies()))
.features(parseFeatures(fm.get("features")))

View File

@ -494,6 +494,12 @@ public class SkillPackageResolver {
.requires(defaultRequires)
.platforms(manifest.getPlatforms())
.build());
// Write the synthesized feature back so the UI's Features
// tab and the LLM's prompt enhancement both see one row
// instead of "no features declared". Functionally
// equivalent to the no-features-declared case, but
// observable in the manifest_json projection.
manifest.setFeatures(effectiveFeatures);
} else {
effectiveFeatures = features;
}

View File

@ -2040,14 +2040,16 @@ export default {
lessons: 'Lessons',
memory: 'Memory',
noManifest: 'This skill does not declare a v3 manifest. Legacy fields apply.',
noTools: 'No tools advertised by active features.',
noTools: 'No allowed-tools declared by this skill. The LLM will fall back to the global tool set when this skill is bound to an agent.',
noFeatures: 'No features[] matrix declared. The skill is treated as a single default feature.',
noLessons: 'No lessons recorded yet. The skill writes here after each call when self-evolution is enabled.',
noEmployees: 'No agent has bound this skill yet.',
noLessons: 'No lessons recorded yet. Lessons are written by the LLM via the record_lesson tool — ask an agent during a session "remember this lesson for {skill name}".',
noEmployees: 'This skill is not yet reachable by any agent. Either bind it explicitly in an agent\'s Skills tab, or enable the skill globally so agents without explicit bindings can use it.',
toolsHint: 'These tool names are merged into the LLM allowed-tool list when this skill is bound to an agent. Tools owned by SETUP_NEEDED features stay hidden.',
lessonsHint: 'Per-skill LESSONS.md. Auto-injected after SKILL.md body when this skill is loaded.',
memoryHint: 'Agents that have bound this skill. Click an agent to inspect its MEMORY.md / structured memory entries that may reference this skill.',
memoryHint: 'Agents that can use this skill. Explicit = wired in the agent\'s Skills tab; Implicit = the agent has no explicit Skill bindings, so it sees every globally-enabled skill including this one.',
openMemory: 'Open agent memory',
bindingExplicit: 'Explicit',
bindingImplicit: 'Implicit',
clearLessons: 'Clear all',
clearLessonsConfirm: 'Delete all recorded lessons for this skill? This cannot be undone.',
},

View File

@ -2042,14 +2042,16 @@ export default {
lessons: 'Lessons',
memory: '记忆',
noManifest: '该技能未声明 v3 manifest使用旧字段。',
noTools: '当前激活特性未暴露任何工具。',
noTools: '该 skill 未声明 allowed-tools。绑定到 agent 时 LLM 将回退到全局工具集。',
noFeatures: '未声明 features[] 矩阵,技能被视为单一默认特性。',
noLessons: '尚未记录任何 lesson。开启 self-evolution 后,每次调用结束 skill 可向此处写入经验。',
noEmployees: '暂无 Agent 绑定此 skill。',
noLessons: '尚未记录任何 lesson。Lessons 由 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 正文之后。',
memoryHint: '已绑定该 skill 的 Agents。点击进入 Agent 的 MEMORY.md / 结构化记忆查看与该 skill 相关的条目。',
memoryHint: '可使用此 skill 的 Agents。显式绑定 = 在 Agent Skills tab 中明确选中;隐式可见 = 该 Agent 未做任何 Skill 显式绑定,自动可见所有全局启用的 skill。',
openMemory: '查看 Agent 记忆',
bindingExplicit: '显式绑定',
bindingImplicit: '隐式可见',
clearLessons: '全部清空',
clearLessonsConfirm: '删除该 skill 的全部 lessons操作不可撤销。',
},

View File

@ -346,7 +346,14 @@
<ul v-else class="memory-agent-list">
<li v-for="agent in detailEmployees" :key="agent.id" class="memory-agent-item">
<span class="memory-agent-icon">{{ agent.icon || '🤖' }}</span>
<span class="memory-agent-name">{{ agent.name }}</span>
<div class="memory-agent-info">
<span class="memory-agent-name">{{ agent.name }}</span>
<span class="memory-agent-binding" :class="`binding-${agent.binding || 'explicit'}`">
{{ agent.binding === 'implicit'
? t('skills.detail.bindingImplicit')
: t('skills.detail.bindingExplicit') }}
</span>
</div>
<button class="memory-link-btn" @click="$router.push(`/memory?agentId=${agent.id}`)">
{{ t('skills.detail.openMemory') }}
</button>
@ -502,7 +509,7 @@ const detailSkill = ref<Skill | null>(null)
const detailTab = ref<'manifest' | 'tools' | 'features' | 'lessons' | 'memory'>('manifest')
const detailLessonsRaw = ref<string>('')
const detailLessonsLoading = ref(false)
const detailEmployees = ref<Array<{ id: number; name: string; icon?: string }>>([])
const detailEmployees = ref<Array<{ id: number; name: string; icon?: string; binding?: 'explicit' | 'implicit' }>>([])
const detailEmployeesLoading = ref(false)
/** RFC-090 §4.2 card surface — per-skill side data (lessons count, used-by). */
@ -1490,7 +1497,11 @@ html.dark .scan-finding-item { background: rgba(255, 255, 255, 0.05); }
.memory-agent-list { list-style: none; padding: 0; margin: 8px 0 0; display: flex; flex-direction: column; gap: 8px; }
.memory-agent-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--mc-border-light); border-radius: 10px; background: var(--mc-bg-muted); }
.memory-agent-icon { font-size: 20px; }
.memory-agent-name { flex: 1; font-weight: 600; color: var(--mc-text-primary); }
.memory-agent-info { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.memory-agent-name { font-weight: 600; color: var(--mc-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.memory-agent-binding { font-size: 10px; padding: 1px 6px; border-radius: 999px; font-weight: 700; letter-spacing: 0.04em; align-self: flex-start; }
.binding-explicit { background: var(--mc-primary-bg); color: var(--mc-primary); }
.binding-implicit { background: var(--mc-bg-sunken); color: var(--mc-text-tertiary); }
.memory-link-btn { padding: 4px 10px; border: 1px solid var(--mc-border); background: var(--mc-bg-elevated); color: var(--mc-primary); border-radius: 8px; font-size: 12px; cursor: pointer; font-weight: 500; }
.memory-link-btn:hover { background: var(--mc-primary-bg); border-color: var(--mc-primary); }