Skip to content

Commit 7040c59

Browse files
liuxy0551claude
andcommitted
feat(agents): resolve built-in skill descriptions from agent SKILL.md
- getAgentDetail: for uncatalogued entrypoint, built-in (private) and uncatalogued dependency skills, fall back to the in-package SKILL.md (agent_files) to resolve real name/description - skill-utils: add extractSkillMdName to read frontmatter name - Agent detail: hide built-in Skills card when empty Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 06f099a commit 7040c59

4 files changed

Lines changed: 229 additions & 22 deletions

File tree

‎app/service/agents.js‎

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ const path = require('path');
66
const yaml = require('js-yaml');
77
const mime = require('mime-types');
88

9-
const { normalizeRelativePath } = require('../utils/skill-utils');
9+
const {
10+
normalizeRelativePath,
11+
extractSkillMdDescription,
12+
extractSkillMdName,
13+
} = require('../utils/skill-utils');
1014
const {
1115
isValidSkillCategory,
1216
SKILL_CATEGORY_OPTIONS,
@@ -69,6 +73,19 @@ class AgentsService extends Service {
6973
return normalized;
7074
}
7175

76+
// agent.yaml 里的 ref 指向目录(skills/bugfix-workflow)或 SKILL.md 本身,
77+
// 统一归一化为包内 SKILL.md 相对路径,供 agent_files 精确匹配。
78+
resolveSkillMdPath(refOrPath) {
79+
const normalized = String(refOrPath || '').trim();
80+
if (!normalized) return '';
81+
return normalized.toLowerCase().endsWith('.md') ? normalized : `${normalized}/SKILL.md`;
82+
}
83+
84+
lookupSkillMd(skillMdMap, refOrPath) {
85+
if (!skillMdMap) return '';
86+
return skillMdMap.get(this.resolveSkillMdPath(refOrPath)) || '';
87+
}
88+
7289
parseJsonArray(value) {
7390
if (!value) return [];
7491
if (Array.isArray(value)) return value;
@@ -929,7 +946,8 @@ class AgentsService extends Service {
929946

930947
async getAgentDetail(name) {
931948
await this.ensureStorageReady();
932-
const { Agent, AgentSkill, SkillsItem } = this.app.model;
949+
const { Agent, AgentSkill, SkillsItem, AgentFile } = this.app.model;
950+
const { Op } = this.app.Sequelize;
933951
const row = await Agent.findOne({
934952
where: {
935953
name,
@@ -964,6 +982,8 @@ class AgentsService extends Service {
964982
const skillMap = new Map(skillRows.map((item) => [item.slug, item]));
965983

966984
const entrypoint = relations.find((item) => item.relation_type === 'entrypoint') || null;
985+
const entrypointSkill = entrypoint ? skillMap.get(entrypoint.skill_slug) : null;
986+
967987
const dependencies = relations
968988
.filter((item) => item.relation_type === 'dependency')
969989
.map((item) => {
@@ -987,6 +1007,61 @@ class AgentsService extends Service {
9871007
path: '',
9881008
}));
9891009

1010+
// 未收录的入口 Skill 和内置 Skills 不在 SkillsItem 表,其真实 name/description
1011+
// 从 agent 包内自带的 SKILL.md 解析(agent_files 已在导入时保存文件内容)。
1012+
const skillMdPaths = [
1013+
...(entrypoint && !entrypointSkill ? [this.resolveSkillMdPath(row.entrypoint_ref)] : []),
1014+
...privateSkills.map((item) => `skills/${item.slug}/SKILL.md`),
1015+
...dependencies
1016+
.filter((item) => !item.collected)
1017+
.map((item) => `skills/${item.slug}/SKILL.md`),
1018+
].filter(Boolean);
1019+
const skillMdRows =
1020+
skillMdPaths.length > 0 && AgentFile
1021+
? await AgentFile.findAll({
1022+
where: {
1023+
agent_id: row.id,
1024+
file_path: { [Op.in]: skillMdPaths },
1025+
is_delete: 0,
1026+
},
1027+
})
1028+
: [];
1029+
const skillMdMap = new Map(skillMdRows.map((item) => [item.file_path, item.content || '']));
1030+
1031+
// 入口 Skill:已收录用 SkillsItem 描述;未收录回填包内 SKILL.md 的 name/description
1032+
const entrypointItem = entrypoint
1033+
? (() => {
1034+
const skill = entrypointSkill;
1035+
const skillMd = skill ? '' : this.lookupSkillMd(skillMdMap, row.entrypoint_ref);
1036+
return {
1037+
slug: entrypoint.skill_slug,
1038+
name: skill ? skill.name : extractSkillMdName(skillMd) || entrypoint.skill_slug,
1039+
description: skill ? skill.description || '' : extractSkillMdDescription(skillMd),
1040+
collected: Boolean(skill),
1041+
path: `/page/skills/${entrypoint.skill_slug}`,
1042+
};
1043+
})()
1044+
: null;
1045+
1046+
// 内置 Skills:总是从包内 SKILL.md 回填
1047+
privateSkills.forEach((item) => {
1048+
const skillMd = skillMdMap.get(`skills/${item.slug}/SKILL.md`) || '';
1049+
const name = extractSkillMdName(skillMd);
1050+
if (name) item.name = name;
1051+
const description = extractSkillMdDescription(skillMd);
1052+
if (description) item.description = description;
1053+
});
1054+
1055+
// 未收录的依赖 Skills:包内有 SKILL.md 时同样回填
1056+
dependencies.forEach((item) => {
1057+
if (item.collected) return;
1058+
const skillMd = skillMdMap.get(`skills/${item.slug}/SKILL.md`) || '';
1059+
const name = extractSkillMdName(skillMd);
1060+
if (name) item.name = name;
1061+
const description = extractSkillMdDescription(skillMd);
1062+
if (description) item.description = description;
1063+
});
1064+
9901065
const detail = row.toJSON();
9911066
const demoImages = this.parseJsonArray(detail.demo_images).map((item) => ({
9921067
...item,
@@ -1008,19 +1083,7 @@ class AgentsService extends Service {
10081083
logoUrl: this.buildAssetUrl(detail.name, detail.logo_path),
10091084
logoPath: detail.logo_path,
10101085
demoImages,
1011-
entrypoint: entrypoint
1012-
? {
1013-
slug: entrypoint.skill_slug,
1014-
name: skillMap.get(entrypoint.skill_slug)
1015-
? skillMap.get(entrypoint.skill_slug).name
1016-
: entrypoint.skill_slug,
1017-
description: skillMap.get(entrypoint.skill_slug)
1018-
? skillMap.get(entrypoint.skill_slug).description || ''
1019-
: '',
1020-
collected: Boolean(skillMap.get(entrypoint.skill_slug)),
1021-
path: `/page/skills/${entrypoint.skill_slug}`,
1022-
}
1023-
: null,
1086+
entrypoint: entrypointItem,
10241087
dependencies,
10251088
privateSkills,
10261089
updatedAt: detail.updated_at ? detail.updated_at.toISOString() : '',

‎app/utils/skill-utils.js‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,31 @@ function extractSkillMdDescription(skillMdContent) {
9191
return extractBodySummary(body) || extractBodySummary(normalized);
9292
}
9393

94+
/** Frontmatter `name:` from SKILL.md, quoted-scalar aware. Empty when absent. */
95+
function extractSkillMdName(content) {
96+
const text = String(content || '');
97+
const normalized = text.replace(/\r\n/g, '\n');
98+
if (!normalized.startsWith('---\n')) return '';
99+
const endMarkerIndex = normalized.indexOf('\n---\n', 4);
100+
if (endMarkerIndex === -1) return '';
101+
const frontmatterText = normalized.slice(4, endMarkerIndex);
102+
const lines = frontmatterText.split('\n');
103+
for (let i = 0; i < lines.length; i += 1) {
104+
const keyMatch = lines[i].match(/^name:\s*(.*)$/i);
105+
if (!keyMatch) continue;
106+
const rest = keyMatch[1].trim();
107+
if (!rest) continue;
108+
if (
109+
(rest.startsWith('"') && rest.endsWith('"')) ||
110+
(rest.startsWith("'") && rest.endsWith("'"))
111+
) {
112+
return rest.slice(1, -1).trim();
113+
}
114+
return rest;
115+
}
116+
return '';
117+
}
118+
94119
/**
95120
* Market card description (CLI registry + Web zip/import/update).
96121
* Explicit override wins; else keep non-empty card; else SKILL.md default.
@@ -110,6 +135,7 @@ function resolveMarketCardDescription(opts = {}) {
110135
module.exports = {
111136
normalizeRelativePath,
112137
extractSkillMdDescription,
138+
extractSkillMdName,
113139
extractBodySummary,
114140
resolveMarketCardDescription,
115141
};

‎app/web/pages/agents/detail/AgentDetailContent.tsx‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -435,9 +435,9 @@ const AgentDetailContent: React.FC<AgentDetailContentProps> = ({ name, history }
435435
)}
436436
</Card>
437437

438-
<Card className="agent-section-card">
439-
<Title level={4}>内置 Skills</Title>
440-
{detail.privateSkills.length > 0 ? (
438+
{detail.privateSkills.length > 0 ? (
439+
<Card className="agent-section-card">
440+
<Title level={4}>内置 Skills</Title>
441441
<div className="agent-skill-grid">
442442
{detail.privateSkills.map((item) => (
443443
<SkillRelationCard
@@ -447,10 +447,8 @@ const AgentDetailContent: React.FC<AgentDetailContentProps> = ({ name, history }
447447
/>
448448
))}
449449
</div>
450-
) : (
451-
<Empty description="暂无内置 Skills" />
452-
)}
453-
</Card>
450+
</Card>
451+
) : null}
454452

455453
{detail.dependencies.length > 0 ? (
456454
<Card className="agent-section-card">

‎test/agent-market-service.test.js‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,3 +462,123 @@ test('getAgentArchiveStream 返回当前 hash 对应的原始 ZIP', async () =>
462462
fs.rmSync(storageDir, { recursive: true, force: true });
463463
}
464464
});
465+
466+
function createDetailService() {
467+
const service = createService();
468+
service.storageReady = true;
469+
service.app.Sequelize = { Op: require('sequelize').Op };
470+
471+
const row = {
472+
id: 1,
473+
name: 'bugfix-agent',
474+
display_name: 'Bugfix Agent',
475+
description: 'Agent 简短描述',
476+
profile: '简介',
477+
author_name: 'DTStack',
478+
category: '工程效率',
479+
tags: '[]',
480+
prompts: '[]',
481+
capabilities: '[]',
482+
version: '1.0.0',
483+
logo_path: '',
484+
demo_images: '[]',
485+
updated_at: new Date('2026-01-01T00:00:00Z'),
486+
entrypoint_ref: 'skills/bugfix-workflow',
487+
toJSON() {
488+
return { ...this };
489+
},
490+
};
491+
492+
const skillMdContents = {
493+
'skills/bugfix-workflow/SKILL.md':
494+
'---\nname: Bugfix Workflow\n---\n# Bugfix Workflow\n修复 Bug 的完整流程',
495+
'skills/builtin-review/SKILL.md':
496+
'---\nname: 内置审查 Skill\n---\n# 内置审查\n用于代码审查',
497+
'skills/systematic-debugging/SKILL.md':
498+
'---\nname: Systematic Debugging\n---\n# Systematic Debugging\n系统化调试方法论',
499+
};
500+
501+
service.app.model = {
502+
Agent: {
503+
async findOne() {
504+
return row;
505+
},
506+
},
507+
AgentSkill: {
508+
async findAll() {
509+
return [
510+
{ skill_slug: 'bugfix-workflow', relation_type: 'entrypoint', sort_order: 0 },
511+
{ skill_slug: 'builtin-review', relation_type: 'private', sort_order: 0 },
512+
{
513+
skill_slug: 'systematic-debugging',
514+
relation_type: 'dependency',
515+
sort_order: 0,
516+
},
517+
];
518+
},
519+
},
520+
SkillsItem: {
521+
async findAll() {
522+
return [];
523+
},
524+
},
525+
AgentFile: {
526+
async findAll({ where }) {
527+
const { Op } = service.app.Sequelize;
528+
const paths = where.file_path[Op.in] || [];
529+
return paths
530+
.filter((filePath) => skillMdContents[filePath] !== undefined)
531+
.map((filePath) => ({
532+
file_path: filePath,
533+
content: skillMdContents[filePath],
534+
}));
535+
},
536+
},
537+
};
538+
539+
return service;
540+
}
541+
542+
test('getAgentDetail 未收录入口/内置/依赖 Skill 从包内 SKILL.md 回填描述', async () => {
543+
const detail = await createDetailService().getAgentDetail('bugfix-agent');
544+
545+
// 核心工作流:未收录时回填 SKILL.md 的 name/description
546+
assert.equal(detail.entrypoint.name, 'Bugfix Workflow');
547+
assert.match(detail.entrypoint.description, /修复 Bug/);
548+
assert.equal(detail.entrypoint.collected, false);
549+
550+
// 内置 Skills:总是回填,name 取 frontmatter,标记 builtin
551+
assert.equal(detail.privateSkills.length, 1);
552+
assert.equal(detail.privateSkills[0].name, '内置审查 Skill');
553+
assert.match(detail.privateSkills[0].description, /代码审查/);
554+
assert.equal(detail.privateSkills[0].builtin, true);
555+
assert.equal(detail.privateSkills[0].path, '');
556+
557+
// 未收录的依赖 Skills:包内有 SKILL.md 时同样回填
558+
assert.equal(detail.dependencies.length, 1);
559+
assert.equal(detail.dependencies[0].name, 'Systematic Debugging');
560+
assert.match(detail.dependencies[0].description, /系统化调试/);
561+
});
562+
563+
test('getAgentDetail 已收录入口 Skill 用 SkillsItem 描述,且不查包内 SKILL.md', async () => {
564+
const service = createDetailService();
565+
service.app.model.SkillsItem.findAll = async () => [
566+
{ slug: 'bugfix-workflow', name: 'Bugfix Workflow(已收录)', description: '来自 Skills Hub 的描述' },
567+
];
568+
// 包内不提供 SKILL.md,验证已收录场景不读取它
569+
service.app.model.AgentFile.findAll = async ({ where }) => {
570+
const { Op } = service.app.Sequelize;
571+
const paths = where.file_path[Op.in] || [];
572+
assert.equal(paths.length, 2); // 仅内置 + 未收录依赖,不再包含入口
573+
return [];
574+
};
575+
576+
const detail = await service.getAgentDetail('bugfix-agent');
577+
578+
assert.equal(detail.entrypoint.name, 'Bugfix Workflow(已收录)');
579+
assert.equal(detail.entrypoint.description, '来自 Skills Hub 的描述');
580+
assert.equal(detail.entrypoint.collected, true);
581+
// 内置 Skill 无 SKILL.md:name 回退 slug,description 为空(前端显示"暂无描述")
582+
assert.equal(detail.privateSkills[0].name, 'builtin-review');
583+
assert.equal(detail.privateSkills[0].description, '');
584+
});

0 commit comments

Comments
 (0)