@ai-wendao/ant 0.3.15 → 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/README.md CHANGED
@@ -6,13 +6,13 @@ ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to its team's
6
6
 
7
7
  ## 企业精选技能
8
8
 
9
- 团队管理员可在 ANT 管理台从 GitHub 目录、ZIP、TAR、TAR.GZ/TGZ 或单份 `SKILL.md` 导入技能,核对来源、文件和依赖后发布。已连接团队的成员从 DSH 左侧“企业扩展”入口进入独立页面浏览并安装技能;ANT 插件设置页只处理连接、模型和更新。ANT Host 将固定版本的完整文件集放进 DSH 的用户技能目录,并重新核对本机文件;启用本地技能的 DSH 会话随后由原生技能提供者发现和使用。ANT 不用 Host 全局技能列表判断会话是否已发现技能。
9
+ 团队管理员可在 ANT 管理台从 GitHub 目录、ZIP、TAR、TAR.GZ/TGZ 或单份 `SKILL.md` 导入技能,核对来源、文件和依赖后发布。已连接团队的成员从 DSH 左侧“企业扩展”入口进入统一目录,默认同时浏览技能和插件,并可按类型、状态筛选或搜索;ANT 插件设置页只处理连接、模型和更新。ANT Host 将固定版本的完整文件集放进 DSH 的用户技能目录,并重新核对本机文件;启用本地技能的 DSH 会话随后由原生技能提供者发现和使用。ANT 不用 Host 全局技能列表判断会话是否已发现技能。
10
10
 
11
11
  安装不会自动运行技能脚本或安装 Python、Node 等系统依赖。ANT 只更新或卸载自己管理且本机内容未被修改的技能目录;发现同名个人技能或本机改动时会停止覆盖。
12
12
 
13
13
  ## 企业精选插件
14
14
 
15
- 管理员可从 npm 官方源的 DSH Bundle 精确版本创建草稿,填写源码、许可证、依赖和安装脚本的审核记录后发布。成员在“企业扩展 → 插件”查看审核信息和本机状态。对无新增依赖、无安装阶段脚本且归档符合首批准入条件的草稿,管理员可从 npm 官方源锁定实际 `.tgz` 并查看文件清单、下载核验制品;这类草稿经审核发布后,成员可让 DSH 从 ANT 的固定制品地址一键安装。其他版本仍是目录项,需联系管理员安排。安全撤回版本会停止新的 ANT 下载,但不会自动删除本机副本;ANT 设置页继续显示相应风险公告。范围与限制见[上线边界](../../docs/decisions/2026-10-15-plugin-center-rollout.md)。
15
+ 管理员可从 npm 官方源的 DSH Bundle 精确版本创建草稿,填写源码、许可证、依赖和安装脚本的审核记录后发布。成员在“企业扩展”总览或“插件”分类查看审核信息和本机状态。对无新增依赖、无安装阶段脚本且归档符合首批准入条件的草稿,管理员可从 npm 官方源锁定实际 `.tgz` 并查看文件清单、下载核验制品;这类草稿经审核发布后,成员可让 DSH 从 ANT 的固定制品地址一键安装。其他版本仍是目录项,需联系管理员安排。安全撤回版本会停止新的 ANT 下载,但不会自动删除本机副本;ANT 设置页继续显示相应风险公告。范围与限制见[上线边界](../../docs/decisions/2026-10-15-plugin-center-rollout.md)。
16
16
 
17
17
  ANT 自身更新使用单独的已审核安装包。管理员将与 npm 发布一致的 `.tgz` 上传到 ANT 服务端并登记版本;新版 ANT 页面可点击“更新”,DSH 原位替换同名 Bundle 后提示重启。旧版 `@ai-wendao/ant` 用户可打开团队服务的 `/ant-update` 指引,在 DSH“添加插件”中粘贴该安装包地址完成一次性原位更新,无需先卸载或使用终端。此入口只分发 ANT 自身,不启用扩展中心其他插件的一键安装。
18
18
 
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,
@@ -270,14 +355,22 @@ window.__ModuleLoader__.load({
270
355
  onServiceUrlChange: setServiceUrl, onConnectionChange }))
271
356
  }
272
357
 
273
- function SkillCenterPage({ onBack, onPlugins }) {
358
+ function SkillCenterPage({ remote, onBack, onAll, onSkills, onPlugins, openBundle,
359
+ includePlugins, query, onQueryChange }) {
274
360
  const [skills, setSkills] = useState([])
275
361
  const [loading, setLoading] = useState(false)
362
+ const [loadError, setLoadError] = useState(false)
276
363
  const [workingId, setWorkingId] = useState('')
277
364
  const [message, setMessage] = useState('')
278
- const [query, setQuery] = useState('')
365
+ const [originalView, setOriginalView] = useState(null)
366
+ const originalGeneration = useRef(0)
279
367
  const [filter, setFilter] = useState('all')
368
+ const [scopeFilter, setScopeFilter] = useState('all')
369
+ const [categoryFilter, setCategoryFilter] = useState('all')
370
+ const [refreshToken, setRefreshToken] = useState(0)
371
+ const refreshGeneration = useRef(0)
280
372
  const refresh = async () => {
373
+ const generation = ++refreshGeneration.current
281
374
  setLoading(true)
282
375
  try {
283
376
  const response = await fetch('/api/ant-teams/skills', { cache: 'no-store' })
@@ -286,19 +379,25 @@ window.__ModuleLoader__.load({
286
379
  ? '请返回 ANT 插件设置,先验证工作邮箱并连接团队。'
287
380
  : result.message || `读取技能失败(${response.status})。`)
288
381
  if (!Array.isArray(result.skills)) throw new Error('企业技能目录格式无效。')
382
+ if (generation !== refreshGeneration.current) return
289
383
  setSkills(result.skills)
290
- setMessage(result.skills.length ? '' : '团队暂未发布精选技能。')
384
+ setLoadError(false)
385
+ setMessage(result.warning || (result.skills.length ? '' : '暂无团队或市场精选技能;下方仍可查看 DSH 官方技能。'))
291
386
  } catch (error) {
387
+ if (generation !== refreshGeneration.current) return
292
388
  setSkills([])
389
+ setLoadError(true)
293
390
  setMessage(error instanceof Error ? error.message : '无法读取企业精选技能。')
294
391
  }
295
- finally { setLoading(false) }
392
+ finally { if (generation === refreshGeneration.current) setLoading(false) }
296
393
  }
297
394
  useEffect(() => { void refresh() }, [])
298
395
  const changeInstall = async (skill, remove = false) => {
299
396
  const action = remove ? '卸载' : skill.local_status === 'update-available' ? '更新' : '安装'
300
397
  const prompt = remove
301
- ? `从这台 DSH 卸载 ${skill.title}?已修改的本地文件不会被删除。`
398
+ ? skill.conflict_reason === 'other-source'
399
+ ? '这台 DSH 已安装另一个来源的同名技能。确认卸载本机旧来源?已修改的本地文件不会被删除。'
400
+ : `从这台 DSH 卸载 ${skill.title}?已修改的本地文件不会被删除。`
302
401
  : `将 ${skill.title} ${action}到这台 DSH?\n\n本机依赖:${skill.requirements}\n\n技能可能让 DSH 读取文件或执行脚本,仍受 DSH 的工具授权控制。ANT 不会自动安装系统依赖。`
303
402
  if (!window.confirm(prompt)) return
304
403
  setWorkingId(skill.id)
@@ -306,70 +405,197 @@ window.__ModuleLoader__.load({
306
405
  try {
307
406
  const response = await fetch(`/api/ant-teams/skills/${remove ? 'uninstall' : 'install'}`, {
308
407
  method: 'POST', headers: { 'Content-Type': 'application/json' },
309
- 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' }),
310
409
  })
311
410
  const result = await readLocalJsonResponse(response)
312
411
  if (!response.ok) throw new Error(result.message || `${action}失败(${response.status})。`)
313
412
  await refresh()
314
- setMessage(remove ? `${skill.title} 已从 ANT 管理的本机目录卸载。`
413
+ setMessage(remove ? skill.conflict_reason === 'other-source'
414
+ ? '本机旧来源已卸载,可以安装所选技能。'
415
+ : `${skill.title} 已从 ANT 管理的本机目录卸载。`
315
416
  : `${skill.title} 的文件已${action}到 DSH 用户技能目录。DSH 会在后续会话或模型步骤中读取;若不可用,请检查当前预设是否启用了本地技能。`)
316
417
  } catch (error) { setMessage(error instanceof Error ? error.message : `${action}失败。`) }
317
418
  finally { setWorkingId('') }
318
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
+ }
319
444
  const visible = skills.filter((skill) => {
320
- const matchesFilter = filter === 'all' || skill.local_status === filter
321
- const matchesQuery = !query.trim() || `${skill.title} ${skill.description} ${skill.dsh_name}`.toLowerCase().includes(query.trim().toLowerCase())
445
+ const matchesFilter = filter === 'all'
446
+ || filter === 'available' && ['not-installed', 'update-available'].includes(skill.local_status)
447
+ || filter === 'installed' && ['installed', 'update-available'].includes(skill.local_status)
448
+ || filter === 'updates' && skill.local_status === 'update-available'
449
+ const matchesQuery = !query.trim() || `${skill.title} ${skillIntroduction(skill)} ${skill.description} ${skill.dsh_name}`.toLowerCase().includes(query.trim().toLowerCase())
322
450
  return matchesFilter && matchesQuery
451
+ && (scopeFilter === 'all' || (skill.scope || 'team') === scopeFilter)
452
+ && (categoryFilter === 'all' || skillCategory(skill) === categoryFilter)
323
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)
459
+ })
460
+ const categories = [...new Set([...skills.map(skillCategory), ...officialOfficeSkills.map((skill) => skill.category)])]
461
+ .sort((a, b) => a.localeCompare(b, 'zh-CN'))
324
462
  const statusLabel = { installed: '已放入本机目录', 'update-available': '可更新', 'not-installed': '未安装', conflict: '本机冲突' }
325
- return h('section', { 'aria-label': '企业精选技能', style: {
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: {
326
477
  height: '100%', overflowY: 'auto', boxSizing: 'border-box',
327
478
  padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
328
479
  } }, h('div', { style: { maxWidth: 1440, margin: '0 auto', display: 'grid', gap: 22 } },
329
480
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
330
481
  h('nav', { 'aria-label': '企业扩展分类', style: { display: 'flex', gap: 6 } },
331
- h('span', { 'aria-current': 'page', style: { ...categoryButtonStyle, display: 'inline-flex', alignItems: 'center', cursor: 'default', backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, '技能'),
482
+ includePlugins
483
+ ? h('span', { 'aria-current': 'page', style: { ...categoryButtonStyle, display: 'inline-flex', alignItems: 'center', cursor: 'default', backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, '总览')
484
+ : h('button', { type: 'button', onClick: onAll, style: categoryButtonStyle }, '总览'),
485
+ includePlugins
486
+ ? h('button', { type: 'button', onClick: onSkills, style: categoryButtonStyle }, '技能')
487
+ : h('span', { 'aria-current': 'page', style: { ...categoryButtonStyle, display: 'inline-flex', alignItems: 'center', cursor: 'default', backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, '技能'),
332
488
  h('button', { type: 'button', onClick: onPlugins, style: categoryButtonStyle }, '插件')),
333
489
  h('button', { type: 'button', onClick: onBack, style: { ...categoryButtonStyle, marginLeft: 'auto' } }, 'ANT 设置')),
334
490
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'end', justifyContent: 'space-between', gap: 16 } },
335
491
  h('div', null,
336
- h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选技能'),
337
- h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '团队管理员审核后发布;安装到这台 DSH 后,由 DSH 原生技能系统发现和使用。')),
338
- h('button', { type: 'button', disabled: loading || Boolean(workingId), onClick: () => void refresh(), style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
492
+ h('h1', { style: { margin: 0, fontSize: 28 } }, includePlugins ? '企业扩展' : '技能中心'),
493
+ h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, includePlugins
494
+ ? '浏览全市场与本企业审核的技能、插件;选择需要的扩展安装到这台 DSH。'
495
+ : '浏览全市场精选和本企业技能;安装到这台 DSH 后,由 DSH 原生技能系统发现和使用。')),
496
+ h('button', { type: 'button', disabled: loading || Boolean(workingId), onClick: () => {
497
+ void refresh()
498
+ if (includePlugins) setRefreshToken((value) => value + 1)
499
+ }, style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
339
500
  h(ExtensionRequestNotice),
340
501
  h('div', { style: { ...panelStyle, gap: 12 } },
341
- h('input', { type: 'search', value: query, onChange: (event) => setQuery(event.target.value), placeholder: '搜索技能名称或说明', 'aria-label': '搜索企业精选技能', style: inputStyle }),
502
+ h('input', { type: 'search', value: query, onChange: (event) => onQueryChange(event.target.value),
503
+ placeholder: includePlugins ? '搜索技能或插件名称、用途' : '搜索技能名称或说明',
504
+ 'aria-label': includePlugins ? '搜索企业扩展' : '搜索企业精选技能', style: inputStyle }),
342
505
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
343
- ...[['all', '全部'], ['not-installed', '可安装'], ['installed', '已安装'], ['update-available', '可更新']].map(([value, label]) =>
506
+ ...[['all', '全部'], ['available', '可安装或更新'], ['installed', '已安装'], ['updates', '可更新']].map(([value, label]) =>
344
507
  h('button', { key: value, type: 'button', onClick: () => setFilter(value), 'aria-pressed': filter === value,
345
508
  style: { ...secondaryButtonStyle, minHeight: 34, padding: '6px 12px', backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)),
346
- h('span', { style: { ...mutedTextStyle, marginLeft: 'auto' } }, `显示 ${visible.length} / ${skills.length} 个技能`))),
347
- message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
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,
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,
348
537
  visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
349
538
  ...visible.map((skill) => {
350
- const description = skill.description || ''
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' ? '管理员上传' : '原作者来源'
351
547
  return h('article', { key: skill.id, style: { ...panelStyle, gap: 13 } },
352
548
  h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 } },
353
- h('h2', { style: { margin: 0, fontSize: 19 } }, skill.title),
549
+ h('div', { style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 8 } },
550
+ h('h2', { style: { margin: 0, fontSize: 19 } }, skill.title),
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))),
354
553
  h('span', { style: mutedTextStyle }, statusLabel[skill.local_status] || '状态未知')),
355
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}`),
356
562
  description.length > 180 ? h('details', null,
357
563
  h('summary', { style: { cursor: 'pointer', fontSize: 13 } }, '查看完整说明'),
358
564
  h('p', { style: { ...mutedTextStyle, marginTop: 8, whiteSpace: 'pre-wrap' } }, description)) : null,
359
- h('p', { style: mutedTextStyle }, `来源:${skill.source_kind === 'github' ? 'GitHub' : '管理员上传'} · 许可证:${skill.license} · ${fileCountFormat.format(skill.file_count)} 个文件`),
360
- h('p', { style: mutedTextStyle }, `本机依赖:${skill.requirements}`),
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 }, '来源:管理员上传并审核'))),
361
574
  skill.local_status === 'conflict'
362
- ? h('p', { role: 'status', style: mutedTextStyle }, '本机同名技能有未受 ANT 管理的内容、文件已被修改或备份损坏;请联系管理员处理。')
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)
363
583
  : h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 'auto' } },
364
584
  skill.local_status === 'installed' ? null : h('button', { type: 'button', disabled: Boolean(workingId),
365
585
  onClick: () => void changeInstall(skill), style: primaryButtonStyle },
366
586
  skill.local_status === 'update-available' ? '更新技能' : '安装到这台 DSH'),
367
587
  skill.local_status !== 'not-installed' ? h('button', { type: 'button', disabled: Boolean(workingId),
368
588
  onClick: () => void changeInstall(skill, true), style: secondaryButtonStyle }, '卸载') : null))
369
- })) : !loading && skills.length ? h('p', { style: mutedTextStyle }, '没有符合筛选条件的技能。') : null))
589
+ })) : !includePlugins && !loading && (skills.length || officialOfficeSkills.length) && !visibleOfficial.length ? h('p', { style: mutedTextStyle }, '没有符合筛选条件的技能。') : null,
590
+ includePlugins ? h(PluginCenterPage, { remote, onBack, onAll, onSkills, openBundle,
591
+ embedded: true, query, onQueryChange, externalFilter: filter,
592
+ hasSkillMatches: visible.length + visibleOfficial.length > 0, skillCatalogReady: !loading && !loadError,
593
+ refreshToken }) : null))
370
594
  }
371
595
 
372
- function PluginCenterPage({ remote, onBack, onSkills, openBundle }) {
596
+ function PluginCenterPage({ remote, onBack, onAll, onSkills, openBundle,
597
+ embedded = false, query, onQueryChange, externalFilter = 'all',
598
+ hasSkillMatches = false, skillCatalogReady = false, refreshToken }) {
373
599
  const [plugins, setPlugins] = useState([])
374
600
  const [bundles, setBundles] = useState([])
375
601
  const [loading, setLoading] = useState(true)
@@ -378,9 +604,10 @@ window.__ModuleLoader__.load({
378
604
  const [uncertainId, setUncertainId] = useState('')
379
605
  const [completedId, setCompletedId] = useState('')
380
606
  const [message, setMessage] = useState('')
381
- const [query, setQuery] = useState('')
382
607
  const [filter, setFilter] = useState('all')
608
+ const refreshGeneration = useRef(0)
383
609
  const refresh = async () => {
610
+ const generation = ++refreshGeneration.current
384
611
  setLoading(true)
385
612
  try {
386
613
  const manager = remote?.pluginManager
@@ -392,18 +619,20 @@ window.__ModuleLoader__.load({
392
619
  if (!response.ok) throw new Error(result.message || `读取插件目录失败(${response.status})。`)
393
620
  if (!Array.isArray(result.plugins)) throw new Error('企业插件目录格式无效。')
394
621
  if (!listed.ok) throw new Error(listed.error?.message || '无法读取 DSH 本机插件。')
622
+ if (generation !== refreshGeneration.current) return
395
623
  setPlugins(result.plugins)
396
624
  setBundles(listed.value)
397
625
  setLoadError(false)
398
626
  setMessage(result.plugins.length ? '' : '团队暂未发布精选插件。')
399
627
  } catch (error) {
628
+ if (generation !== refreshGeneration.current) return
400
629
  setPlugins([])
401
630
  setBundles([])
402
631
  setLoadError(true)
403
632
  setMessage(error instanceof Error ? error.message : '无法读取企业精选插件。')
404
- } finally { setLoading(false) }
633
+ } finally { if (generation === refreshGeneration.current) setLoading(false) }
405
634
  }
406
- useEffect(() => { void refresh() }, [])
635
+ useEffect(() => { void refresh() }, [refreshToken])
407
636
  const install = async (plugin) => {
408
637
  const artifactUrl = approvedPluginArtifactUrl(plugin)
409
638
  if (!artifactUrl || workingId || uncertainId === plugin.id || completedId === plugin.id) return
@@ -488,35 +717,46 @@ window.__ModuleLoader__.load({
488
717
  setMessage(error instanceof Error ? error.message : '插件安装未完成,请稍后重试。')
489
718
  } finally { setWorkingId('') }
490
719
  }
720
+ const activeFilter = embedded ? externalFilter : filter
491
721
  const visible = plugins.filter((plugin) => {
492
722
  const installed = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
493
723
  const artifactUrl = approvedPluginArtifactUrl(plugin)
494
724
  const matches = !query.trim() || `${plugin.title} ${plugin.description} ${plugin.package_name}`.toLowerCase().includes(query.trim().toLowerCase())
495
- return matches && (filter === 'all' || filter === 'installed' && Boolean(installed)
496
- || filter === 'available' && artifactUrl && (!installed || (installed.version && installed.version !== plugin.version))
497
- || filter === 'retired' && plugin.status !== 'published')
725
+ return matches && (activeFilter === 'all' || activeFilter === 'installed' && Boolean(installed)
726
+ || activeFilter === 'available' && artifactUrl && (!installed || (installed.version && installed.version !== plugin.version))
727
+ || activeFilter === 'updates' && artifactUrl && installed && installed.version && installed.version !== plugin.version
728
+ || activeFilter === 'retired' && plugin.status !== 'published')
498
729
  })
499
- return h('section', { 'aria-label': '企业精选插件', style: {
500
- height: '100%', overflowY: 'auto', boxSizing: 'border-box',
501
- padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
502
- } }, h('div', { style: { maxWidth: 1440, margin: '0 auto', display: 'grid', gap: 22 } },
503
- h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
730
+ const noMatches = embedded
731
+ ? skillCatalogReady ? '没有找到符合条件的企业扩展。' : '插件中没有符合条件的结果。'
732
+ : activeFilter === 'available'
733
+ ? '当前没有可一键安装的插件。可在“全部”查看审核目录,或联系企业管理员。'
734
+ : '没有符合筛选条件的插件。'
735
+ return h('section', { 'aria-label': '企业精选插件', style: embedded
736
+ ? { display: 'grid', gap: 16 }
737
+ : { height: '100%', overflowY: 'auto', boxSizing: 'border-box',
738
+ padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px' },
739
+ }, h('div', { style: embedded ? { display: 'grid', gap: 16 } : { maxWidth: 1440, margin: '0 auto', display: 'grid', gap: 22 } },
740
+ embedded ? h('h2', { style: { margin: 0, fontSize: 20 } }, loading ? '插件 · 读取中…' : `插件 · ${visible.length}`) : h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
504
741
  h('nav', { 'aria-label': '企业扩展分类', style: { display: 'flex', gap: 6 } },
742
+ h('button', { type: 'button', onClick: onAll, style: categoryButtonStyle }, '总览'),
505
743
  h('button', { type: 'button', onClick: onSkills, style: categoryButtonStyle }, '技能'),
506
744
  h('span', { 'aria-current': 'page', style: { ...categoryButtonStyle, display: 'inline-flex', alignItems: 'center', cursor: 'default', backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, '插件')),
507
745
  h('button', { type: 'button', onClick: onBack, style: { ...categoryButtonStyle, marginLeft: 'auto' } }, 'ANT 设置')),
508
- h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 16 } },
746
+ embedded ? null : h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 16 } },
509
747
  h('div', null,
510
748
  h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选插件'),
511
749
  h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '这里展示团队审核过的插件版本及本机状态。')),
512
750
  h('button', { type: 'button', disabled: loading, onClick: () => void refresh(), style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
513
- !loading && !plugins.length && !loadError ? h('section', { role: 'status', style: panelStyle },
514
- h('h2', { style: { margin: 0, fontSize: 18 } }, '团队暂未发布精选插件'),
515
- h('p', { style: mutedTextStyle }, '需要某个插件?请联系企业管理员,说明用途或提供插件地址。审核发布后会显示在这里。')) : null,
751
+ !loading && !plugins.length && !loadError ? embedded
752
+ ? h('p', { role: 'status', style: mutedTextStyle }, '团队暂未发布精选插件。')
753
+ : h('section', { role: 'status', style: panelStyle },
754
+ h('h2', { style: { margin: 0, fontSize: 18 } }, '团队暂未发布精选插件'),
755
+ h('p', { style: mutedTextStyle }, '需要某个插件?请联系企业管理员,说明用途或提供插件地址。审核发布后会显示在这里。')) : null,
516
756
  !loading && loadError ? h('p', { role: 'alert', style: mutedTextStyle }, message) : null,
517
- plugins.length ? h(ExtensionRequestNotice) : null,
518
- plugins.length ? h('div', { style: panelStyle },
519
- h('input', { type: 'search', value: query, onChange: (event) => setQuery(event.target.value), placeholder: '搜索插件名称或用途', 'aria-label': '搜索企业精选插件', style: inputStyle }),
757
+ plugins.length && !embedded ? h(ExtensionRequestNotice) : null,
758
+ plugins.length && !embedded ? h('div', { style: panelStyle },
759
+ h('input', { type: 'search', value: query, onChange: (event) => onQueryChange(event.target.value), placeholder: '搜索插件名称或用途', 'aria-label': '搜索企业精选插件', style: inputStyle }),
520
760
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
521
761
  ...[['all', '全部'], ['available', '可安装或升级'], ['installed', '已安装'], ['retired', '已废弃']].map(([value, label]) =>
522
762
  h('button', { key: value, type: 'button', onClick: () => setFilter(value), 'aria-pressed': filter === value,
@@ -541,7 +781,9 @@ window.__ModuleLoader__.load({
541
781
  return h('article', { key: plugin.id, style: security
542
782
  ? { ...panelStyle, borderColor: 'var(--dsw-alias-label-danger, #d44)' } : panelStyle },
543
783
  h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 8 } },
544
- h('h2', { style: { margin: 0, fontSize: 19 } }, plugin.title),
784
+ h('div', { style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 8 } },
785
+ h('h2', { style: { margin: 0, fontSize: 19 } }, plugin.title),
786
+ embedded ? h('span', { style: mutedTextStyle }, '插件') : null),
545
787
  h('span', { style: security
546
788
  ? { ...mutedTextStyle, color: 'var(--dsw-alias-label-danger, #d44)', fontWeight: 650 } : mutedTextStyle }, status)),
547
789
  h('p', { style: mutedTextStyle }, plugin.description),
@@ -568,9 +810,8 @@ window.__ModuleLoader__.load({
568
810
  local || uncertainId === plugin.id ? h('button', { type: 'button', onClick: () => openBundle(plugin.package_name), style: secondaryButtonStyle }, '打开 DSH 插件管理') : null,
569
811
  local && !sameVersion && plugin.status === 'published' && !artifactUrl
570
812
  ? h('p', { style: mutedTextStyle }, '本机版本与团队审核版本不同;请联系企业管理员核对。') : null))
571
- })) : !loading && plugins.length ? h('p', { style: mutedTextStyle }, filter === 'available'
572
- ? '当前没有可一键安装的插件。可在“全部”查看审核目录,或联系企业管理员。'
573
- : '没有符合筛选条件的插件。') : null))
813
+ })) : !loading && plugins.length && (!embedded || !hasSkillMatches)
814
+ ? h('p', { style: mutedTextStyle }, noMatches) : null))
574
815
  }
575
816
 
576
817
  function ModelSyncFooter({ remote, serviceUrl, bindingReady, onServiceUrlChange, onConnectionChange }) {
@@ -1167,10 +1408,16 @@ window.__ModuleLoader__.load({
1167
1408
  }
1168
1409
 
1169
1410
  function EnterpriseExtensionsPage({ remote, onBack, openBundle }) {
1170
- const [tab, setTab] = useState('skills')
1171
- return tab === 'skills'
1172
- ? h(SkillCenterPage, { onBack, onPlugins: () => setTab('plugins') })
1173
- : h(PluginCenterPage, { remote, onBack, onSkills: () => setTab('skills'), openBundle })
1411
+ const [tab, setTab] = useState('all')
1412
+ const [query, setQuery] = useState('')
1413
+ const navigation = {
1414
+ onAll: () => setTab('all'), onSkills: () => setTab('skills'),
1415
+ onPlugins: () => setTab('plugins'),
1416
+ }
1417
+ return tab === 'plugins'
1418
+ ? h(PluginCenterPage, { remote, onBack, openBundle, query, onQueryChange: setQuery, ...navigation })
1419
+ : h(SkillCenterPage, { remote, onBack, openBundle, query, onQueryChange: setQuery,
1420
+ includePlugins: tab === 'all', ...navigation })
1174
1421
  }
1175
1422
 
1176
1423
  function apply(ctx) {
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
- if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队,或重新验证工作邮箱。' })
367
- return jsonResponse(200, { skills: await listSkills(connection) })
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 connection = await memberSkillConnection(credentials, settings)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-wendao/ant",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to team identity and approved models.",
5
5
  "type": "module",
6
6
  "scripts": {
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 (!response.ok || !response.body) throw new Error(`团队服务无法提供技能文件(HTTP ${response.status})。`)
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 response = await fetch(url, { headers: { authorization: `Bearer ${token}`, accept: 'application/json' }, redirect: 'manual', signal: AbortSignal.timeout(20_000) })
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 skills = await fetchJson(new URL('/api/me/skills', connection.serviceUrl), connection.token)
156
- if (!Array.isArray(skills)) throw new Error('团队技能目录格式无效。')
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
- if (local_status === 'conflict') return { ...skill, local_status }
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) throw new Error('该目录属于另一个 ANT 服务。')
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
- const manifest = await fetchJson(new URL(`/api/me/skills/${skillId}/files`, connection.serviceUrl), connection.token)
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) throw new Error('同名技能属于另一个 ANT 服务,无法覆盖。')
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(`/api/me/skills/${skillId}/package`, connection.serviceUrl)
230
- const packageResponse = await fetch(packageUrl, { headers: { authorization: `Bearer ${connection.token}` }, redirect: 'manual', signal: AbortSignal.timeout(120_000) })
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 })