@ai-wendao/ant 0.3.16 → 0.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +212 -18
- package/index.js +74 -5
- package/package.json +1 -1
- package/skill-install.js +36 -19
package/client.js
CHANGED
|
@@ -11,6 +11,91 @@ window.__ModuleLoader__.load({
|
|
|
11
11
|
const ANT_API_PROTOCOL_VERSIONS = [1]
|
|
12
12
|
const PLUGIN_INSTALL_RECORD_PREFIX = 'ant-reviewed-plugin-install:'
|
|
13
13
|
const fileCountFormat = new Intl.NumberFormat('zh-CN')
|
|
14
|
+
const marketSkillCategories = {
|
|
15
|
+
'ant-ppt-master': '演示', 'ant-frontend-slides': '演示', 'ant-slidev': '演示',
|
|
16
|
+
'ant-word-docx': '文档', 'ant-excel-xlsx': '表格', 'ant-pdf': 'PDF',
|
|
17
|
+
'ant-internal-comms': '写作', 'ant-formatted-report-writer': '写作',
|
|
18
|
+
'ant-agent-browser': '开发', 'ant-vercel-react-best-practices': '开发',
|
|
19
|
+
'ant-vercel-composition-patterns': '开发', 'ant-supabase-postgres-best-practices': '开发',
|
|
20
|
+
'ant-diagnosing-bugs': '开发', 'ant-improve-codebase-architecture': '开发',
|
|
21
|
+
'ant-code-review': '开发', 'ant-systematic-debugging': '开发',
|
|
22
|
+
'ant-test-driven-development': '开发', 'ant-verification-before-completion': '开发',
|
|
23
|
+
'ant-writing-plans': '开发', 'ant-to-spec': '开发', 'ant-skill-creator': '开发',
|
|
24
|
+
'ant-frontend-design': '设计', 'ant-web-design-guidelines': '设计',
|
|
25
|
+
'ant-ui-ux-pro-max': '设计', 'ant-emil-design-eng': '设计',
|
|
26
|
+
'ant-review-animations': '设计', 'ant-shadcn': '设计',
|
|
27
|
+
'ant-theme-factory': '设计', 'ant-brand-guidelines': '设计',
|
|
28
|
+
'ant-copywriting': '写作', 'ant-seo-audit': '营销', 'ant-brainstorming': '策划',
|
|
29
|
+
}
|
|
30
|
+
const officialOfficeGuide = 'https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.1-rc.2/packages/skill/skill-office/README.zh.md'
|
|
31
|
+
const officialOfficeEnglish = (skill) => `https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v${skill.since}/packages/skill/skill-office/assets/${skill.name}/SKILL.md`
|
|
32
|
+
const officialOfficeSkills = [
|
|
33
|
+
{ name: 'office', title: 'Office 办公入口', category: '办公', since: '0.2.1-rc.2', description: '根据任务选择 Word、PPT、Excel 或文件处理能力。', prompt: '帮我完成这份办公文件' },
|
|
34
|
+
{ name: 'office-docx', title: 'Word 文档', category: '文档', since: '0.2.0-rc.2', description: '创建、修改和检查 DOCX 文档。', prompt: '帮我写一份 Word 报告' },
|
|
35
|
+
{ name: 'office-pptx', title: 'PPT 演示', category: '演示', since: '0.2.0-rc.2', description: '创建、修改和检查 PPTX 演示文稿。', prompt: '帮我做一份项目汇报 PPT' },
|
|
36
|
+
{ name: 'office-xlsx', title: 'Excel 表格', category: '表格', since: '0.2.0-rc.2', description: '创建、修改和检查 XLSX 工作簿。', prompt: '帮我整理这份 Excel 表格' },
|
|
37
|
+
{ name: 'office-render', title: '文件预览', category: '办公', since: '0.2.1-rc.2', description: '把 Office 文件渲染成图片,检查实际外观。', prompt: '请预览这份文档并检查排版' },
|
|
38
|
+
{ name: 'office-recalculate', title: '公式重算', category: '表格', since: '0.2.1-rc.2', description: '重新计算表格公式并核对结果。', prompt: '请重新计算这份表格的公式' },
|
|
39
|
+
{ name: 'office-export-pdf', title: '导出 PDF', category: 'PDF', since: '0.2.1-rc.2', description: '将 Office 文件导出为 PDF。', prompt: '把这份 Word 文档导出成 PDF' },
|
|
40
|
+
]
|
|
41
|
+
const curatedSkillIntroductions = {
|
|
42
|
+
"ant-ppt-master": {"ref": "c350bc31e6d43b7bcf5234183d650ba8f9f4c9d7", "description": "制作和编辑 PowerPoint 演示文稿。"},
|
|
43
|
+
"ant-frontend-slides": {"ref": "9906a34d640d2111f724544cbc50f7f130569ae1", "description": "制作浏览器中的动态 HTML 演示文稿。"},
|
|
44
|
+
"ant-slidev": {"ref": "73053ab2b9653ed475960301b56e1ce4439d9c75", "description": "用 Markdown 制作技术演示文稿。"},
|
|
45
|
+
"ant-word-docx": {"ref": "1.0.2", "description": "创建、阅读和修改 Word 文档,尽量保留原有样式。"},
|
|
46
|
+
"ant-excel-xlsx": {"ref": "1.0.2", "description": "分析和修改 Excel 表格、公式与格式。"},
|
|
47
|
+
"ant-pdf": {"ref": "6e9b3e3b8852f12bf965f888213470574b72803e", "description": "读取、创建、合并和处理 PDF 与表单。"},
|
|
48
|
+
"ant-internal-comms": {"ref": "dbd4588f9e1033efb41dad4bef2f7947c8993d44", "description": "撰写内部通知、项目更新、FAQ 和周报。"},
|
|
49
|
+
"ant-formatted-report-writer": {"ref": "97a13607d669157a4a16d5ba79b77f208319f7b9", "description": "把分析材料整理成结构清楚的业务报告。"},
|
|
50
|
+
"ant-agent-browser": {"ref": "dac13bb87468fe9d74a040d862720f6cc17a9565", "description": "操作网页、截图和页面检查。"},
|
|
51
|
+
"ant-vercel-react-best-practices": {"ref": "063bee94c3f4df8453406c830b0a7df0f2860278", "description": "优化 React 与 Next.js 性能。"},
|
|
52
|
+
"ant-vercel-composition-patterns": {"ref": "063bee94c3f4df8453406c830b0a7df0f2860278", "description": "设计可复用 React 组件。"},
|
|
53
|
+
"ant-supabase-postgres-best-practices": {"ref": "c9be0e931b7930f7d02126d04774d904c381e7d7", "description": "改进 SQL、索引、迁移和数据库安全。"},
|
|
54
|
+
"ant-diagnosing-bugs": {"ref": "5beb35b040e3de97e54fa9e386d7e42d15c295c2", "description": "建立复现信号并定位问题根因。"},
|
|
55
|
+
"ant-improve-codebase-architecture": {"ref": "5beb35b040e3de97e54fa9e386d7e42d15c295c2", "description": "识别模块耦合和重构机会。"},
|
|
56
|
+
"ant-code-review": {"ref": "5beb35b040e3de97e54fa9e386d7e42d15c295c2", "description": "按规范和原始需求审查代码差异。"},
|
|
57
|
+
"ant-systematic-debugging": {"ref": "bb92a77741419a4ab5f06e711a283343f1ada0c3", "description": "先找根因,再修改和复核。"},
|
|
58
|
+
"ant-test-driven-development": {"ref": "bb92a77741419a4ab5f06e711a283343f1ada0c3", "description": "先建立失败样例,再实现和重构。"},
|
|
59
|
+
"ant-verification-before-completion": {"ref": "bb92a77741419a4ab5f06e711a283343f1ada0c3", "description": "交付前用实际证据核对结论。"},
|
|
60
|
+
"ant-writing-plans": {"ref": "bb92a77741419a4ab5f06e711a283343f1ada0c3", "description": "将已明确需求拆成可执行计划。"},
|
|
61
|
+
"ant-to-spec": {"ref": "5beb35b040e3de97e54fa9e386d7e42d15c295c2", "description": "把已有讨论整理为产品和实现规格。"},
|
|
62
|
+
"ant-skill-creator": {"ref": "dbd4588f9e1033efb41dad4bef2f7947c8993d44", "description": "设计、打包和评估新技能。"},
|
|
63
|
+
"ant-frontend-design": {"ref": "dbd4588f9e1033efb41dad4bef2f7947c8993d44", "description": "设计更有辨识度的网页界面。"},
|
|
64
|
+
"ant-web-design-guidelines": {"ref": "063bee94c3f4df8453406c830b0a7df0f2860278", "description": "检查 UI、可访问性和交互细节。"},
|
|
65
|
+
"ant-ui-ux-pro-max": {"ref": "56ea9cf174ed86786187daa9de0e6247cb0747e4", "description": "检索配色、排版、动效和界面规则。"},
|
|
66
|
+
"ant-emil-design-eng": {"ref": "2d228630ba1446365f614a108cb8252f1feda202", "description": "改进界面动效、组件和交互细节。"},
|
|
67
|
+
"ant-review-animations": {"ref": "2d228630ba1446365f614a108cb8252f1feda202", "description": "按照原作者的标准检查动效实现。"},
|
|
68
|
+
"ant-shadcn": {"ref": "ba1503c64472b82256d9de95e2e62c5901a38e8b", "description": "搜索、组合和调整 shadcn/ui 组件。"},
|
|
69
|
+
"ant-theme-factory": {"ref": "dbd4588f9e1033efb41dad4bef2f7947c8993d44", "description": "为文档、页面和演示选择主题。"},
|
|
70
|
+
"ant-brand-guidelines": {"ref": "dbd4588f9e1033efb41dad4bef2f7947c8993d44", "description": "原作者自己的品牌色和字体规范。"},
|
|
71
|
+
"ant-copywriting": {"ref": "365b412152b9542499ca1439426f7fc3cc92bb65", "description": "撰写和改进页面、广告与产品文案。"},
|
|
72
|
+
"ant-seo-audit": {"ref": "365b412152b9542499ca1439426f7fc3cc92bb65", "description": "诊断网站搜索可见性与页面问题。"},
|
|
73
|
+
"ant-brainstorming": {"ref": "bb92a77741419a4ab5f06e711a283343f1ada0c3", "description": "澄清需求、比较方案和形成设计。"},
|
|
74
|
+
}
|
|
75
|
+
const hasChineseIntroduction = (value) => {
|
|
76
|
+
const text = typeof value === 'string' ? value : ''
|
|
77
|
+
const han = (text.match(/[\u3400-\u9fff]/gu) || []).length
|
|
78
|
+
return han >= 2 && han * 5 >= (text.match(/[A-Za-z]/gu) || []).length
|
|
79
|
+
}
|
|
80
|
+
const curatedSkillIntroduction = (skill) => {
|
|
81
|
+
const item = curatedSkillIntroductions[skill.dsh_name]
|
|
82
|
+
return item && item.ref === skill.source_ref ? item.description : null
|
|
83
|
+
}
|
|
84
|
+
const skillIntroduction = (skill) => hasChineseIntroduction(skill.description)
|
|
85
|
+
? skill.description : curatedSkillIntroduction(skill)
|
|
86
|
+
|| '这项技能尚未提供中文介绍。请查看英文原文,或联系企业管理员补充。'
|
|
87
|
+
const skillEnglishSource = (skill) => {
|
|
88
|
+
let url
|
|
89
|
+
try { url = new URL(skill.source_url) } catch { return null }
|
|
90
|
+
if (url.protocol !== 'https:' || url.username || url.password) return null
|
|
91
|
+
const parts = url.pathname.split('/').filter(Boolean)
|
|
92
|
+
const fixedRef = /^[a-f0-9]{40}$/iu.test(skill.source_ref || '') ? skill.source_ref : null
|
|
93
|
+
if (url.hostname === 'github.com' && parts.length >= 4 && parts[2] === 'tree' && fixedRef) {
|
|
94
|
+
return `https://github.com/${parts[0]}/${parts[1]}/blob/${fixedRef}/${parts.slice(4).join('/')}${parts.length > 4 ? '/' : ''}SKILL.md`
|
|
95
|
+
}
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
const skillCategory = (skill) => marketSkillCategories[skill.dsh_name] || '其他'
|
|
14
99
|
const panelStyle = {
|
|
15
100
|
display: 'grid', alignContent: 'start', gap: 14, padding: 18,
|
|
16
101
|
border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 14,
|
|
@@ -277,7 +362,11 @@ window.__ModuleLoader__.load({
|
|
|
277
362
|
const [loadError, setLoadError] = useState(false)
|
|
278
363
|
const [workingId, setWorkingId] = useState('')
|
|
279
364
|
const [message, setMessage] = useState('')
|
|
365
|
+
const [originalView, setOriginalView] = useState(null)
|
|
366
|
+
const originalGeneration = useRef(0)
|
|
280
367
|
const [filter, setFilter] = useState('all')
|
|
368
|
+
const [scopeFilter, setScopeFilter] = useState('all')
|
|
369
|
+
const [categoryFilter, setCategoryFilter] = useState('all')
|
|
281
370
|
const [refreshToken, setRefreshToken] = useState(0)
|
|
282
371
|
const refreshGeneration = useRef(0)
|
|
283
372
|
const refresh = async () => {
|
|
@@ -293,7 +382,7 @@ window.__ModuleLoader__.load({
|
|
|
293
382
|
if (generation !== refreshGeneration.current) return
|
|
294
383
|
setSkills(result.skills)
|
|
295
384
|
setLoadError(false)
|
|
296
|
-
setMessage(result.skills.length ? '' : '
|
|
385
|
+
setMessage(result.warning || (result.skills.length ? '' : '暂无团队或市场精选技能;下方仍可查看 DSH 官方技能。'))
|
|
297
386
|
} catch (error) {
|
|
298
387
|
if (generation !== refreshGeneration.current) return
|
|
299
388
|
setSkills([])
|
|
@@ -306,7 +395,9 @@ window.__ModuleLoader__.load({
|
|
|
306
395
|
const changeInstall = async (skill, remove = false) => {
|
|
307
396
|
const action = remove ? '卸载' : skill.local_status === 'update-available' ? '更新' : '安装'
|
|
308
397
|
const prompt = remove
|
|
309
|
-
?
|
|
398
|
+
? skill.conflict_reason === 'other-source'
|
|
399
|
+
? '这台 DSH 已安装另一个来源的同名技能。确认卸载本机旧来源?已修改的本地文件不会被删除。'
|
|
400
|
+
: `从这台 DSH 卸载 ${skill.title}?已修改的本地文件不会被删除。`
|
|
310
401
|
: `将 ${skill.title} ${action}到这台 DSH?\n\n本机依赖:${skill.requirements}\n\n技能可能让 DSH 读取文件或执行脚本,仍受 DSH 的工具授权控制。ANT 不会自动安装系统依赖。`
|
|
311
402
|
if (!window.confirm(prompt)) return
|
|
312
403
|
setWorkingId(skill.id)
|
|
@@ -314,26 +405,75 @@ window.__ModuleLoader__.load({
|
|
|
314
405
|
try {
|
|
315
406
|
const response = await fetch(`/api/ant-teams/skills/${remove ? 'uninstall' : 'install'}`, {
|
|
316
407
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
317
|
-
body: JSON.stringify(remove ? { dsh_name: skill.dsh_name } : { skill_id: skill.id }),
|
|
408
|
+
body: JSON.stringify(remove ? { dsh_name: skill.dsh_name } : { skill_id: skill.id, scope: skill.scope || 'team' }),
|
|
318
409
|
})
|
|
319
410
|
const result = await readLocalJsonResponse(response)
|
|
320
411
|
if (!response.ok) throw new Error(result.message || `${action}失败(${response.status})。`)
|
|
321
412
|
await refresh()
|
|
322
|
-
setMessage(remove ?
|
|
413
|
+
setMessage(remove ? skill.conflict_reason === 'other-source'
|
|
414
|
+
? '本机旧来源已卸载,可以安装所选技能。'
|
|
415
|
+
: `${skill.title} 已从 ANT 管理的本机目录卸载。`
|
|
323
416
|
: `${skill.title} 的文件已${action}到 DSH 用户技能目录。DSH 会在后续会话或模型步骤中读取;若不可用,请检查当前预设是否启用了本地技能。`)
|
|
324
417
|
} catch (error) { setMessage(error instanceof Error ? error.message : `${action}失败。`) }
|
|
325
418
|
finally { setWorkingId('') }
|
|
326
419
|
}
|
|
420
|
+
const openOriginal = async (skill) => {
|
|
421
|
+
const generation = ++originalGeneration.current
|
|
422
|
+
setOriginalView({ title: skill.title, loading: true })
|
|
423
|
+
try {
|
|
424
|
+
const params = new URLSearchParams({ skill_id: skill.id, scope: skill.scope || 'team' })
|
|
425
|
+
const response = await fetch(`/api/ant-teams/skills/original?${params}`, { cache: 'no-store' })
|
|
426
|
+
if (response.status === 404 && !hasChineseIntroduction(skill.description)) {
|
|
427
|
+
if (generation === originalGeneration.current) setOriginalView({
|
|
428
|
+
title: skill.title, markdown: skill.description, partial: true,
|
|
429
|
+
})
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
if (!response.ok) {
|
|
433
|
+
const error = await readLocalJsonResponse(response)
|
|
434
|
+
throw new Error(error.message || '技能原文暂时无法打开。')
|
|
435
|
+
}
|
|
436
|
+
const markdown = await response.text()
|
|
437
|
+
if (generation === originalGeneration.current) setOriginalView({ title: skill.title, markdown })
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if (generation === originalGeneration.current) setOriginalView({
|
|
440
|
+
title: skill.title, error: error instanceof Error ? error.message : '技能原文暂时无法打开。',
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
}
|
|
327
444
|
const visible = skills.filter((skill) => {
|
|
328
445
|
const matchesFilter = filter === 'all'
|
|
329
446
|
|| filter === 'available' && ['not-installed', 'update-available'].includes(skill.local_status)
|
|
330
447
|
|| filter === 'installed' && ['installed', 'update-available'].includes(skill.local_status)
|
|
331
448
|
|| filter === 'updates' && skill.local_status === 'update-available'
|
|
332
|
-
const matchesQuery = !query.trim() || `${skill.title} ${skill.description} ${skill.dsh_name}`.toLowerCase().includes(query.trim().toLowerCase())
|
|
449
|
+
const matchesQuery = !query.trim() || `${skill.title} ${skillIntroduction(skill)} ${skill.description} ${skill.dsh_name}`.toLowerCase().includes(query.trim().toLowerCase())
|
|
333
450
|
return matchesFilter && matchesQuery
|
|
451
|
+
&& (scopeFilter === 'all' || (skill.scope || 'team') === scopeFilter)
|
|
452
|
+
&& (categoryFilter === 'all' || skillCategory(skill) === categoryFilter)
|
|
453
|
+
})
|
|
454
|
+
const visibleOfficial = officialOfficeSkills.filter((skill) => {
|
|
455
|
+
const matchesQuery = !query.trim() || `${skill.title} ${skill.name} ${skill.description} ${skill.prompt}`.toLowerCase().includes(query.trim().toLowerCase())
|
|
456
|
+
return filter === 'all' && matchesQuery
|
|
457
|
+
&& (scopeFilter === 'all' || scopeFilter === 'official')
|
|
458
|
+
&& (categoryFilter === 'all' || skill.category === categoryFilter)
|
|
334
459
|
})
|
|
460
|
+
const categories = [...new Set([...skills.map(skillCategory), ...officialOfficeSkills.map((skill) => skill.category)])]
|
|
461
|
+
.sort((a, b) => a.localeCompare(b, 'zh-CN'))
|
|
335
462
|
const statusLabel = { installed: '已放入本机目录', 'update-available': '可更新', 'not-installed': '未安装', conflict: '本机冲突' }
|
|
336
|
-
return h('section', { 'aria-label':
|
|
463
|
+
if (originalView) return h('section', { 'aria-label': '技能原文', style: {
|
|
464
|
+
height: '100%', overflowY: 'auto', boxSizing: 'border-box',
|
|
465
|
+
padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
|
|
466
|
+
} }, h('div', { style: { maxWidth: 1100, margin: '0 auto', display: 'grid', gap: 16 } },
|
|
467
|
+
h('button', { type: 'button', onClick: () => { originalGeneration.current += 1; setOriginalView(null) },
|
|
468
|
+
style: { ...secondaryButtonStyle, width: 'fit-content' } }, includePlugins ? '← 返回企业扩展' : '← 返回技能中心'),
|
|
469
|
+
h('h1', { style: { margin: 0, fontSize: 26 } }, `${originalView.title} · 原始技能说明`),
|
|
470
|
+
h('p', { style: mutedTextStyle }, originalView.partial
|
|
471
|
+
? '当前服务端尚未提供完整原文,此处仅显示导入时的英文简介。'
|
|
472
|
+
: '这里显示发布时保存的 SKILL.md。它供 DSH 使用,与技能中心的中文介绍分别维护;ANT 可能给原技能名加前缀。'),
|
|
473
|
+
originalView.loading ? h('p', { role: 'status', style: mutedTextStyle }, '正在读取原文…')
|
|
474
|
+
: originalView.error ? h('p', { role: 'alert', style: mutedTextStyle }, originalView.error)
|
|
475
|
+
: h('pre', { style: { ...panelStyle, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere', fontFamily: 'monospace', fontSize: 13, lineHeight: 1.6 } }, originalView.markdown)))
|
|
476
|
+
return h('section', { 'aria-label': includePlugins ? '企业扩展' : '技能中心', style: {
|
|
337
477
|
height: '100%', overflowY: 'auto', boxSizing: 'border-box',
|
|
338
478
|
padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
|
|
339
479
|
} }, h('div', { style: { maxWidth: 1440, margin: '0 auto', display: 'grid', gap: 22 } },
|
|
@@ -349,10 +489,10 @@ window.__ModuleLoader__.load({
|
|
|
349
489
|
h('button', { type: 'button', onClick: onBack, style: { ...categoryButtonStyle, marginLeft: 'auto' } }, 'ANT 设置')),
|
|
350
490
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'end', justifyContent: 'space-between', gap: 16 } },
|
|
351
491
|
h('div', null,
|
|
352
|
-
h('h1', { style: { margin: 0, fontSize: 28 } }, includePlugins ? '企业扩展' : '
|
|
492
|
+
h('h1', { style: { margin: 0, fontSize: 28 } }, includePlugins ? '企业扩展' : '技能中心'),
|
|
353
493
|
h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, includePlugins
|
|
354
|
-
? '
|
|
355
|
-
: '
|
|
494
|
+
? '浏览全市场与本企业审核的技能、插件;选择需要的扩展安装到这台 DSH。'
|
|
495
|
+
: '浏览全市场精选和本企业技能;安装到这台 DSH 后,由 DSH 原生技能系统发现和使用。')),
|
|
356
496
|
h('button', { type: 'button', disabled: loading || Boolean(workingId), onClick: () => {
|
|
357
497
|
void refresh()
|
|
358
498
|
if (includePlugins) setRefreshToken((value) => value + 1)
|
|
@@ -366,36 +506,90 @@ window.__ModuleLoader__.load({
|
|
|
366
506
|
...[['all', '全部'], ['available', '可安装或更新'], ['installed', '已安装'], ['updates', '可更新']].map(([value, label]) =>
|
|
367
507
|
h('button', { key: value, type: 'button', onClick: () => setFilter(value), 'aria-pressed': filter === value,
|
|
368
508
|
style: { ...secondaryButtonStyle, minHeight: 34, padding: '6px 12px', backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)),
|
|
369
|
-
includePlugins ? null : h('span', { style: { ...mutedTextStyle, marginLeft: 'auto' } }, `显示 ${visible.length} / ${skills.length} 个技能`))
|
|
370
|
-
|
|
509
|
+
includePlugins ? null : h('span', { style: { ...mutedTextStyle, marginLeft: 'auto' } }, `显示 ${visible.length + visibleOfficial.length} / ${skills.length + officialOfficeSkills.length} 个技能`)),
|
|
510
|
+
includePlugins ? null : h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
|
|
511
|
+
h('span', { style: mutedTextStyle }, '来源'),
|
|
512
|
+
...[['all', '全部'], ['official', 'DSH 官方'], ['market', '全市场精选'], ['team', '本企业']].map(([value, label]) =>
|
|
513
|
+
h('button', { key: value, type: 'button', onClick: () => setScopeFilter(value), 'aria-pressed': scopeFilter === value,
|
|
514
|
+
style: { ...categoryButtonStyle, backgroundColor: scopeFilter === value ? 'var(--dsw-alias-bg-layer-2)' : categoryButtonStyle.backgroundColor } }, label)),
|
|
515
|
+
h('label', { style: { ...mutedTextStyle, marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 } }, '用途',
|
|
516
|
+
h('select', { value: categoryFilter, onChange: (event) => setCategoryFilter(event.target.value), style: { ...inputStyle, width: 'auto', minHeight: 34 } },
|
|
517
|
+
h('option', { value: 'all' }, '全部用途'),
|
|
518
|
+
...categories.map((category) => h('option', { key: category, value: category }, category))))),
|
|
519
|
+
),
|
|
520
|
+
includePlugins ? h('h2', { style: { margin: 0, fontSize: 20 } }, loading ? '技能 · 读取中…' : `技能 · ${visible.length + visibleOfficial.length}`) : null,
|
|
371
521
|
message && (!includePlugins || loadError || skills.length) ? h('p', { role: loadError ? 'alert' : 'status', style: mutedTextStyle }, message) : null,
|
|
522
|
+
visibleOfficial.length ? h('section', { 'aria-label': 'DSH 官方 Office 技能', style: { display: 'grid', gap: 12 } },
|
|
523
|
+
h('div', null,
|
|
524
|
+
h('h2', { style: { margin: 0, fontSize: 20 } }, 'DSH 官方 Office'),
|
|
525
|
+
h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '这些能力随对应版本的 DSH 提供,ANT 无需重复安装。在新会话中直接描述任务即可;不计入 ANT 的本机安装筛选。'),
|
|
526
|
+
h('a', { href: officialOfficeGuide, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '查看中文官方说明 ↗')),
|
|
527
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 300px), 1fr))', gap: 14 } },
|
|
528
|
+
...visibleOfficial.map((skill) => h('article', { key: skill.name, style: { ...panelStyle, gap: 10 } },
|
|
529
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 } },
|
|
530
|
+
h('h3', { style: { margin: 0, fontSize: 17 } }, skill.title),
|
|
531
|
+
h('span', { style: mutedTextStyle }, 'DSH 官方')),
|
|
532
|
+
h('p', { style: mutedTextStyle }, skill.description),
|
|
533
|
+
h('p', { style: mutedTextStyle }, `在新会话中说:“${skill.prompt}”。也可输入 /${skill.name}。`),
|
|
534
|
+
h('p', { style: mutedTextStyle }, `DSH ${skill.since} 起提供;旧版请使用 DSH 自带的检查更新。`),
|
|
535
|
+
h('a', { href: officialOfficeEnglish(skill), target: '_blank', rel: 'noopener noreferrer',
|
|
536
|
+
style: { ...secondaryButtonStyle, display: 'inline-flex', alignItems: 'center', width: 'fit-content', textDecoration: 'none' } }, '查看英文原文 ↗'))))) : null,
|
|
372
537
|
visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
|
|
373
538
|
...visible.map((skill) => {
|
|
374
|
-
const description = skill
|
|
539
|
+
const description = skillIntroduction(skill)
|
|
540
|
+
const originalEnglish = Boolean(curatedSkillIntroduction(skill)) || !hasChineseIntroduction(skill.description)
|
|
541
|
+
const englishSource = skillEnglishSource(skill)
|
|
542
|
+
const sourceRef = skill.source_ref || ''
|
|
543
|
+
const versionLabel = /^[a-f0-9]{40,64}$/iu.test(sourceRef) ? sourceRef.slice(0, 12) : sourceRef
|
|
544
|
+
const sourceLabel = /^https:\/\/github\.com\//u.test(skill.source_url || '') ? 'GitHub'
|
|
545
|
+
: /^https:\/\/clawhub\.ai\//u.test(skill.source_url || '') ? 'ClawHub'
|
|
546
|
+
: skill.source_kind === 'upload' ? '管理员上传' : '原作者来源'
|
|
375
547
|
return h('article', { key: skill.id, style: { ...panelStyle, gap: 13 } },
|
|
376
548
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 } },
|
|
377
549
|
h('div', { style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 8 } },
|
|
378
550
|
h('h2', { style: { margin: 0, fontSize: 19 } }, skill.title),
|
|
379
|
-
|
|
551
|
+
h('span', { style: { ...mutedTextStyle, padding: '2px 7px', borderRadius: 6, backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, skill.scope === 'market' ? '全市场精选' : '本企业'),
|
|
552
|
+
h('span', { style: mutedTextStyle }, skillCategory(skill))),
|
|
380
553
|
h('span', { style: mutedTextStyle }, statusLabel[skill.local_status] || '状态未知')),
|
|
381
554
|
h('p', { style: mutedTextStyle }, description.length > 180 ? `${description.slice(0, 180)}…` : description),
|
|
555
|
+
englishSource ? h('a', { href: englishSource, target: '_blank', rel: 'noopener noreferrer',
|
|
556
|
+
style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, originalEnglish ? '查看英文原文 ↗' : '查看原始说明 ↗')
|
|
557
|
+
: h('button', { type: 'button', onClick: () => void openOriginal(skill),
|
|
558
|
+
style: { border: 0, padding: 0, background: 'transparent', cursor: 'pointer',
|
|
559
|
+
color: 'var(--dsw-alias-link)', fontSize: 13, textAlign: 'left' } },
|
|
560
|
+
originalEnglish ? '查看英文原文 →' : '查看原始说明 →'),
|
|
561
|
+
h('p', { style: mutedTextStyle }, `来源:${sourceLabel} · 许可:${skill.license}`),
|
|
382
562
|
description.length > 180 ? h('details', null,
|
|
383
563
|
h('summary', { style: { cursor: 'pointer', fontSize: 13 } }, '查看完整说明'),
|
|
384
564
|
h('p', { style: { ...mutedTextStyle, marginTop: 8, whiteSpace: 'pre-wrap' } }, description)) : null,
|
|
385
|
-
h('
|
|
386
|
-
|
|
565
|
+
h('details', null,
|
|
566
|
+
h('summary', { style: { cursor: 'pointer', fontSize: 13 } }, '查看来源、文件与使用条件'),
|
|
567
|
+
h('div', { style: { display: 'grid', gap: 5, marginTop: 9, overflowWrap: 'anywhere' } },
|
|
568
|
+
h('p', { style: mutedTextStyle }, `${fileCountFormat.format(skill.file_count)} 个文件`),
|
|
569
|
+
h('p', { style: mutedTextStyle }, `本机条件:${skill.requirements}`),
|
|
570
|
+
h('p', { style: mutedTextStyle, title: sourceRef }, `固定版本:${versionLabel || '已锁定制品'}`),
|
|
571
|
+
/^https:\/\//u.test(skill.source_url || '')
|
|
572
|
+
? h('a', { href: skill.source_url, target: '_blank', rel: 'noopener noreferrer', style: { fontSize: 13, color: 'var(--dsw-alias-link)' } }, '查看原始来源 ↗')
|
|
573
|
+
: h('p', { style: mutedTextStyle }, '来源:管理员上传并审核'))),
|
|
387
574
|
skill.local_status === 'conflict'
|
|
388
|
-
? h('
|
|
575
|
+
? h('div', { style: { display: 'grid', gap: 8 } },
|
|
576
|
+
h('p', { role: 'status', style: mutedTextStyle }, skill.conflict_reason === 'other-source'
|
|
577
|
+
? '本机已安装另一个来源的同名技能。可主动卸载本机旧来源,再安装此版本。'
|
|
578
|
+
: '本机同名技能有未受 ANT 管理的内容、文件已被修改或备份损坏;请联系管理员处理。'),
|
|
579
|
+
skill.conflict_reason === 'other-source'
|
|
580
|
+
? h('button', { type: 'button', disabled: Boolean(workingId),
|
|
581
|
+
onClick: () => void changeInstall(skill, true), style: { ...secondaryButtonStyle, width: 'fit-content' } }, '卸载旧来源')
|
|
582
|
+
: null)
|
|
389
583
|
: h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 'auto' } },
|
|
390
584
|
skill.local_status === 'installed' ? null : h('button', { type: 'button', disabled: Boolean(workingId),
|
|
391
585
|
onClick: () => void changeInstall(skill), style: primaryButtonStyle },
|
|
392
586
|
skill.local_status === 'update-available' ? '更新技能' : '安装到这台 DSH'),
|
|
393
587
|
skill.local_status !== 'not-installed' ? h('button', { type: 'button', disabled: Boolean(workingId),
|
|
394
588
|
onClick: () => void changeInstall(skill, true), style: secondaryButtonStyle }, '卸载') : null))
|
|
395
|
-
})) : !includePlugins && !loading && skills.length ? h('p', { style: mutedTextStyle }, '没有符合筛选条件的技能。') : null,
|
|
589
|
+
})) : !includePlugins && !loading && (skills.length || officialOfficeSkills.length) && !visibleOfficial.length ? h('p', { style: mutedTextStyle }, '没有符合筛选条件的技能。') : null,
|
|
396
590
|
includePlugins ? h(PluginCenterPage, { remote, onBack, onAll, onSkills, openBundle,
|
|
397
591
|
embedded: true, query, onQueryChange, externalFilter: filter,
|
|
398
|
-
hasSkillMatches: visible.length > 0, skillCatalogReady: !loading && !loadError,
|
|
592
|
+
hasSkillMatches: visible.length + visibleOfficial.length > 0, skillCatalogReady: !loading && !loadError,
|
|
399
593
|
refreshToken }) : null))
|
|
400
594
|
}
|
|
401
595
|
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const DISCONNECT_PATH = '/api/ant-teams/disconnect'
|
|
|
13
13
|
const REQUEST_CODE_PATH = '/api/ant-teams/request-code'
|
|
14
14
|
const CONNECT_PATH = '/api/ant-teams/connect'
|
|
15
15
|
const SKILLS_PATH = '/api/ant-teams/skills'
|
|
16
|
+
const SKILL_ORIGINAL_PATH = '/api/ant-teams/skills/original'
|
|
16
17
|
const SKILL_INSTALL_PATH = '/api/ant-teams/skills/install'
|
|
17
18
|
const SKILL_UNINSTALL_PATH = '/api/ant-teams/skills/uninstall'
|
|
18
19
|
const PLUGINS_PATH = '/api/ant-teams/plugins'
|
|
@@ -21,6 +22,7 @@ const MAX_MODEL_RESPONSE_BYTES = 1024 * 1024
|
|
|
21
22
|
const MODEL_TOKEN_REF = 'ANT_MODEL_API_KEY'
|
|
22
23
|
const SERVICE_RECORD_KEY = 'ant/team-service'
|
|
23
24
|
const SESSION_HEARTBEAT_INTERVAL_MS = 30_000
|
|
25
|
+
const MARKET_SERVICE_URL = process.env.ANT_MARKET_URL?.trim() || 'https://8.160.178.245'
|
|
24
26
|
|
|
25
27
|
function jsonResponse(status, value) {
|
|
26
28
|
return new Response(JSON.stringify(value), {
|
|
@@ -359,14 +361,72 @@ async function memberSkillConnection(credentials, settings) {
|
|
|
359
361
|
return connection
|
|
360
362
|
}
|
|
361
363
|
|
|
364
|
+
function marketSkillConnection() {
|
|
365
|
+
const serviceUrl = normalizeServiceUrl(MARKET_SERVICE_URL)
|
|
366
|
+
if (!serviceUrl) throw new Error('ANT 公共技能市场地址无效。')
|
|
367
|
+
return { serviceUrl, token: null }
|
|
368
|
+
}
|
|
369
|
+
|
|
362
370
|
async function fetchMemberSkills(request, credentials, settings) {
|
|
363
371
|
if (request.method !== 'GET') return jsonResponse(405, { message: '只支持 GET 请求' })
|
|
364
372
|
try {
|
|
365
373
|
const connection = await memberSkillConnection(credentials, settings)
|
|
366
|
-
|
|
367
|
-
|
|
374
|
+
const settle = async (promise) => {
|
|
375
|
+
try { return { status: 'fulfilled', value: await promise } }
|
|
376
|
+
catch { return { status: 'rejected' } }
|
|
377
|
+
}
|
|
378
|
+
const team = await settle(connection ? listSkills(connection) : Promise.resolve([]))
|
|
379
|
+
const market = await settle(listSkills(marketSkillConnection(), 'market'))
|
|
380
|
+
if (market.status === 'rejected' && (team.status === 'rejected' || !connection)) {
|
|
381
|
+
throw new Error('暂时无法读取公共技能市场;请稍后刷新。')
|
|
382
|
+
}
|
|
383
|
+
const teamSkills = team.status === 'fulfilled' ? team.value : []
|
|
384
|
+
const knownIds = new Set(teamSkills.map((skill) => skill.id))
|
|
385
|
+
const marketSkills = market.status === 'fulfilled'
|
|
386
|
+
? market.value.filter((skill) => !knownIds.has(skill.id)) : []
|
|
387
|
+
const warning = market.status === 'rejected' ? '公共技能市场暂时无法连接,已显示本企业技能。'
|
|
388
|
+
: team.status === 'rejected' ? '本企业技能暂时无法读取,已显示公共市场技能。'
|
|
389
|
+
: !connection ? '连接团队后,还可以看到本企业发布的技能。' : ''
|
|
390
|
+
return jsonResponse(200, warning
|
|
391
|
+
? { skills: [...marketSkills, ...teamSkills], warning }
|
|
392
|
+
: { skills: [...marketSkills, ...teamSkills] })
|
|
368
393
|
} catch (error) {
|
|
369
|
-
return jsonResponse(502, { message: error instanceof Error ? error.message : '
|
|
394
|
+
return jsonResponse(502, { message: error instanceof Error ? error.message : '无法读取技能目录。' })
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function fetchSkillOriginal(request, credentials, settings) {
|
|
399
|
+
if (request.method !== 'GET') return jsonResponse(405, { message: '只支持 GET 请求' })
|
|
400
|
+
let url
|
|
401
|
+
try { url = new URL(request.url) } catch { return jsonResponse(400, { message: '技能原文地址无效。' }) }
|
|
402
|
+
const skillId = url.searchParams.get('skill_id')
|
|
403
|
+
const scope = url.searchParams.get('scope') || 'team'
|
|
404
|
+
if (!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(skillId || '')
|
|
405
|
+
|| !['market', 'team'].includes(scope)) {
|
|
406
|
+
return jsonResponse(400, { message: '技能原文地址无效。' })
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const connection = scope === 'market' ? marketSkillConnection()
|
|
410
|
+
: await memberSkillConnection(credentials, settings)
|
|
411
|
+
if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队。' })
|
|
412
|
+
const path = scope === 'market' ? `/api/market/skills/${skillId}/original`
|
|
413
|
+
: `/api/me/skills/${skillId}/original`
|
|
414
|
+
const upstream = await fetch(new URL(path, connection.serviceUrl), {
|
|
415
|
+
headers: connection.token ? { authorization: `Bearer ${connection.token}`, accept: 'text/plain' }
|
|
416
|
+
: { accept: 'text/plain' },
|
|
417
|
+
redirect: 'manual', signal: AbortSignal.timeout(10_000),
|
|
418
|
+
})
|
|
419
|
+
if (upstream.status === 404) return jsonResponse(404, { message: '技能原文暂不可读取:服务端可能尚未升级,或技能已撤回。' })
|
|
420
|
+
if (upstream.status === 401 || upstream.status === 403) return jsonResponse(401, { message: 'ANT 成员凭证已失效。' })
|
|
421
|
+
if (!upstream.ok || !upstream.headers.get('content-type')?.startsWith('text/plain')) {
|
|
422
|
+
return jsonResponse(502, { message: 'ANT 未返回有效的技能原文。' })
|
|
423
|
+
}
|
|
424
|
+
const text = await readLimitedBody(upstream)
|
|
425
|
+
if (text === null) return jsonResponse(502, { message: '技能原文超过预览上限。' })
|
|
426
|
+
return new Response(text, { headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store',
|
|
427
|
+
'x-content-type-options': 'nosniff', 'content-security-policy': "default-src 'none'" } })
|
|
428
|
+
} catch {
|
|
429
|
+
return jsonResponse(502, { message: '暂时无法读取技能原文。' })
|
|
370
430
|
}
|
|
371
431
|
}
|
|
372
432
|
|
|
@@ -378,10 +438,15 @@ async function installMemberSkill(request, credentials, settings) {
|
|
|
378
438
|
if (text.length > 4096) throw new Error()
|
|
379
439
|
body = JSON.parse(text)
|
|
380
440
|
} catch { return jsonResponse(400, { message: '技能安装请求无效。' }) }
|
|
441
|
+
if (body?.scope !== 'market' && body?.scope !== 'team' && body?.scope !== undefined) {
|
|
442
|
+
return jsonResponse(400, { message: '技能来源无效。' })
|
|
443
|
+
}
|
|
381
444
|
try {
|
|
382
|
-
const
|
|
445
|
+
const scope = body?.scope || 'team'
|
|
446
|
+
const connection = scope === 'market' ? marketSkillConnection()
|
|
447
|
+
: await memberSkillConnection(credentials, settings)
|
|
383
448
|
if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队,或重新验证工作邮箱。' })
|
|
384
|
-
return jsonResponse(200, await installSkill(connection, body?.skill_id))
|
|
449
|
+
return jsonResponse(200, await installSkill(connection, body?.skill_id, scope))
|
|
385
450
|
} catch (error) {
|
|
386
451
|
return jsonResponse(409, { message: error instanceof Error ? error.message : '技能安装失败。' })
|
|
387
452
|
}
|
|
@@ -544,6 +609,10 @@ export function apply(ctx) {
|
|
|
544
609
|
path: SKILLS_PATH, methods: ['GET'], requestBody: 'buffered',
|
|
545
610
|
fetch: (request) => fetchMemberSkills(request, connectionCtx.credentials, connectionCtx.settings),
|
|
546
611
|
}), 'ant-teams: enterprise skill catalog')
|
|
612
|
+
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
613
|
+
path: SKILL_ORIGINAL_PATH, methods: ['GET'], requestBody: 'buffered',
|
|
614
|
+
fetch: (request) => fetchSkillOriginal(request, connectionCtx.credentials, connectionCtx.settings),
|
|
615
|
+
}), 'ant-teams: approved skill original text')
|
|
547
616
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
548
617
|
path: SKILL_INSTALL_PATH, methods: ['POST'], requestBody: 'buffered',
|
|
549
618
|
fetch: (request) => installMemberSkill(request, connectionCtx.credentials, connectionCtx.settings),
|
package/package.json
CHANGED
package/skill-install.js
CHANGED
|
@@ -28,7 +28,9 @@ const skillRoot = () => {
|
|
|
28
28
|
? join(homedir(), configured.slice(2)) : configured
|
|
29
29
|
return join(resolve(home), 'skills')
|
|
30
30
|
}
|
|
31
|
+
const markerScope = (marker) => marker.scope === 'market' ? 'market' : 'team'
|
|
31
32
|
const validManagedMarker = (marker, name) => marker?.dsh_name === name && validHash(marker.content_sha256)
|
|
33
|
+
&& (marker.scope === undefined || marker.scope === 'team' || marker.scope === 'market')
|
|
32
34
|
&& Array.isArray(marker.files) && marker.files.length > 0 && marker.files.length <= MAX_FILES
|
|
33
35
|
&& marker.files.every((file) => safePath(file.path) && validHash(file.sha256) && typeof file.executable === 'boolean')
|
|
34
36
|
&& new Set(marker.files.map((file) => file.path)).size === marker.files.length
|
|
@@ -134,7 +136,8 @@ async function verifyManagedTree(root, name, marker) {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
async function boundedBytes(response, maxBytes) {
|
|
137
|
-
if (
|
|
139
|
+
if (response.status === 404) throw new Error('此技能可能已下架或不再可访问,请刷新目录后重试。')
|
|
140
|
+
if (!response.ok || !response.body) throw new Error(`技能来源无法提供文件(HTTP ${response.status})。`)
|
|
138
141
|
const chunks = []
|
|
139
142
|
let total = 0
|
|
140
143
|
for await (const chunk of response.body) {
|
|
@@ -145,29 +148,37 @@ async function boundedBytes(response, maxBytes) {
|
|
|
145
148
|
return Buffer.concat(chunks, total)
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
async function fetchJson(url, token) {
|
|
149
|
-
const
|
|
151
|
+
async function fetchJson(url, token, timeoutMs = 20_000) {
|
|
152
|
+
const headers = token ? { authorization: `Bearer ${token}`, accept: 'application/json' } : { accept: 'application/json' }
|
|
153
|
+
const response = await fetch(url, { headers, redirect: 'manual', signal: AbortSignal.timeout(timeoutMs) })
|
|
150
154
|
const bytes = await boundedBytes(response, MAX_JSON_BYTES)
|
|
151
|
-
try { return JSON.parse(bytes.toString('utf8')) } catch { throw new Error('
|
|
155
|
+
try { return JSON.parse(bytes.toString('utf8')) } catch { throw new Error('技能目录格式无效。') }
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
export async function listSkills(connection) {
|
|
155
|
-
const
|
|
156
|
-
|
|
158
|
+
export async function listSkills(connection, scope = 'team') {
|
|
159
|
+
const path = scope === 'market' ? '/api/market/skills' : '/api/me/skills'
|
|
160
|
+
const skills = await fetchJson(new URL(path, connection.serviceUrl), connection.token, scope === 'market' ? 8_000 : 20_000)
|
|
161
|
+
if (!Array.isArray(skills)) throw new Error('技能目录格式无效。')
|
|
157
162
|
const { root, conflicts } = await safeRoot()
|
|
158
163
|
return Promise.all(skills.map(async (skill) => {
|
|
159
|
-
if (!safeName(skill.dsh_name) || !validHash(skill.content_sha256)) throw new Error('
|
|
164
|
+
if (!safeName(skill.dsh_name) || !validHash(skill.content_sha256)) throw new Error('技能目录含无效条目。')
|
|
160
165
|
let local_status = conflicts.has(skill.dsh_name) ? 'conflict' : 'not-installed'
|
|
161
|
-
|
|
166
|
+
let conflict_reason = ''
|
|
167
|
+
const actualScope = skill.scope === 'market' ? 'market' : scope
|
|
168
|
+
if (local_status === 'conflict') return { ...skill, scope: actualScope, local_status }
|
|
162
169
|
try {
|
|
163
170
|
const marker = await readManaged(root, skill.dsh_name)
|
|
164
171
|
if (marker) {
|
|
165
172
|
await verifyManagedTree(root, skill.dsh_name, marker)
|
|
166
|
-
if (marker.service_url !== connection.serviceUrl
|
|
173
|
+
if (marker.service_url !== connection.serviceUrl
|
|
174
|
+
|| (markerScope(marker) !== actualScope && marker.skill_id !== skill.id)) {
|
|
175
|
+
conflict_reason = 'other-source'
|
|
176
|
+
throw new Error('该目录属于另一个技能来源。')
|
|
177
|
+
}
|
|
167
178
|
local_status = marker.content_sha256 === skill.content_sha256 ? 'installed' : 'update-available'
|
|
168
179
|
}
|
|
169
180
|
} catch { local_status = 'conflict' }
|
|
170
|
-
return { ...skill, local_status }
|
|
181
|
+
return { ...skill, scope: actualScope, local_status, conflict_reason }
|
|
171
182
|
}))
|
|
172
183
|
}
|
|
173
184
|
|
|
@@ -203,31 +214,37 @@ function contentDigest(files) {
|
|
|
203
214
|
return digest.digest('hex')
|
|
204
215
|
}
|
|
205
216
|
|
|
206
|
-
export async function installSkill(connection, skillId) {
|
|
217
|
+
export async function installSkill(connection, skillId, scope = 'team') {
|
|
207
218
|
if (typeof skillId !== 'string' || !/^[0-9a-f-]{36}$/iu.test(skillId)) throw new Error('技能编号无效。')
|
|
208
|
-
|
|
219
|
+
if (scope !== 'team' && scope !== 'market') throw new Error('技能来源无效。')
|
|
220
|
+
const basePath = scope === 'market' ? '/api/market/skills' : '/api/me/skills'
|
|
221
|
+
const manifest = await fetchJson(new URL(`${basePath}/${skillId}/files`, connection.serviceUrl), connection.token)
|
|
209
222
|
validateManifest(manifest, skillId)
|
|
210
223
|
const { root, conflicts } = await safeRoot()
|
|
211
224
|
if (conflicts.has(manifest.dsh_name)) throw new Error('本机技能备份已损坏;请联系管理员处理,ANT 不会覆盖。')
|
|
212
225
|
if (activeSkills.has(manifest.dsh_name)) throw new Error('这个技能正在安装或卸载,请稍后重试。')
|
|
213
226
|
activeSkills.add(manifest.dsh_name)
|
|
214
|
-
try { return await installValidatedSkill(connection, skillId, manifest, root) }
|
|
227
|
+
try { return await installValidatedSkill(connection, skillId, manifest, root, basePath, scope) }
|
|
215
228
|
finally { activeSkills.delete(manifest.dsh_name) }
|
|
216
229
|
}
|
|
217
230
|
|
|
218
|
-
async function installValidatedSkill(connection, skillId, manifest, root) {
|
|
231
|
+
async function installValidatedSkill(connection, skillId, manifest, root, basePath, scope) {
|
|
219
232
|
const target = join(root, manifest.dsh_name)
|
|
220
233
|
const oldMarker = await readManaged(root, manifest.dsh_name)
|
|
221
234
|
if (oldMarker) {
|
|
222
|
-
if (oldMarker.service_url !== connection.serviceUrl
|
|
235
|
+
if (oldMarker.service_url !== connection.serviceUrl
|
|
236
|
+
|| (markerScope(oldMarker) !== scope && oldMarker.skill_id !== skillId)) {
|
|
237
|
+
throw new Error('同名技能属于另一个来源,请先卸载本机旧来源后再安装。')
|
|
238
|
+
}
|
|
223
239
|
await verifyManagedTree(root, manifest.dsh_name, oldMarker)
|
|
224
240
|
if (oldMarker.content_sha256 === manifest.content_sha256) {
|
|
225
241
|
return { status: 'installed', dsh_name: manifest.dsh_name }
|
|
226
242
|
}
|
|
227
243
|
}
|
|
228
244
|
|
|
229
|
-
const packageUrl = new URL(
|
|
230
|
-
const
|
|
245
|
+
const packageUrl = new URL(`${basePath}/${skillId}/package`, connection.serviceUrl)
|
|
246
|
+
const headers = connection.token ? { authorization: `Bearer ${connection.token}` } : {}
|
|
247
|
+
const packageResponse = await fetch(packageUrl, { headers, redirect: 'manual', signal: AbortSignal.timeout(120_000) })
|
|
231
248
|
const packageBytes = await boundedBytes(packageResponse, manifest.package_size)
|
|
232
249
|
if (packageBytes.length !== manifest.package_size || hash(packageBytes) !== manifest.package_sha256) {
|
|
233
250
|
throw new Error('技能包校验失败,未安装。')
|
|
@@ -265,7 +282,7 @@ async function installValidatedSkill(connection, skillId, manifest, root) {
|
|
|
265
282
|
if (file.executable && process.platform !== 'win32') await chmod(destination, 0o700)
|
|
266
283
|
}
|
|
267
284
|
await writeFile(join(staging, MARKER), JSON.stringify({
|
|
268
|
-
dsh_name: manifest.dsh_name, skill_id: skillId, service_url: connection.serviceUrl,
|
|
285
|
+
dsh_name: manifest.dsh_name, skill_id: skillId, scope, service_url: connection.serviceUrl,
|
|
269
286
|
content_sha256: manifest.content_sha256,
|
|
270
287
|
files: managedFiles,
|
|
271
288
|
}), { flag: 'wx', mode: 0o600 })
|