@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 +2 -2
- package/client.js +298 -51
- package/index.js +74 -5
- package/package.json +1 -1
- package/skill-install.js +36 -19
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
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
-
?
|
|
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 ?
|
|
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'
|
|
321
|
-
|
|
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': '
|
|
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
|
-
|
|
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 } },
|
|
338
|
-
|
|
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) =>
|
|
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', '全部'], ['
|
|
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
|
-
|
|
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
|
|
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('
|
|
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('
|
|
360
|
-
|
|
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('
|
|
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 && (
|
|
496
|
-
||
|
|
497
|
-
||
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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 ?
|
|
514
|
-
h('
|
|
515
|
-
h('
|
|
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) =>
|
|
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('
|
|
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
|
|
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('
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
:
|
|
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
|
-
|
|
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 })
|