@weibaohui/experts-management 0.2.0 → 0.4.0
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 +4 -0
- package/client/bundle.js +345 -11
- package/client/index.js +238 -11
- package/package.json +2 -1
- package/src/index.js +226 -4
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
- **专家详情**:角色定义全文、关联技能、团队成员、快捷指令、plugin.json 原文、文件清单与体积
|
|
18
18
|
- **内置自动同步**:与技能市场同款管线——clone `--depth 1 --filter=blob:none --sparse` + 每日 fetch/reset,支持 GitCode 私有仓库 access token(只写不回读)
|
|
19
19
|
|
|
20
|
+
- **编辑已安装专家**:角色定义(Agent MD)全文编辑、元数据表单(显示名/职业/描述/标签/快捷指令/默认开场,中英双语)、关联技能管理(从技能库复制副本进专家、移除仅删专家副本)、头像上传——仅「我的」(用户库)专家可编辑,内置只读;保存即生效
|
|
21
|
+
|
|
22
|
+
- **分享专家**:详情弹窗一键分享——AI 读取本机 GitCode 令牌,fork 官方仓库 → 建分支 → 提交专家目录到 `experts/` 子树 → 创建 PR;提示词可编辑,执行过程实时可见(与技能分享同管线)
|
|
23
|
+
|
|
20
24
|
## 安装
|
|
21
25
|
|
|
22
26
|
```bash
|
package/client/bundle.js
CHANGED
|
@@ -8,6 +8,113 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
var exports = module.exports
|
|
9
9
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
|
|
10
10
|
var React = require("react")
|
|
11
|
+
/**
|
|
12
|
+
* @weibaohui/dsh-plugin-kit — client source(由消费者构建脚本内联进 bundle,
|
|
13
|
+
* 不经 loader 运行时加载)。对外暴露 PluginKit:
|
|
14
|
+
*
|
|
15
|
+
* PluginKit.substituteParams(template, params) — {{key}} 模板插值
|
|
16
|
+
* PluginKit.makeActionShareDialog(React, opts) — 返回 ActionShareDialog 组件
|
|
17
|
+
*
|
|
18
|
+
* ActionShareDialog props:
|
|
19
|
+
* title / hint / rows: [[label, value], ...] / initialPrompt
|
|
20
|
+
* run: async (prompt) => { jobId } — 发起执行
|
|
21
|
+
* poll: async (jobId) => { status, output, code }
|
|
22
|
+
* labels: { copy, copied, run, running, done, failed, outputLabel, openSession, close }
|
|
23
|
+
* onOpenSession: (sessionId) => void — 可选;job 出现 sessionId 时渲染「打开会话」
|
|
24
|
+
* onClose
|
|
25
|
+
*
|
|
26
|
+
* 全部样式内联(主题 token + 回退值),消费者无需自带 CSS。
|
|
27
|
+
*/
|
|
28
|
+
var PluginKit = (function () {
|
|
29
|
+
function substituteParams(template, params) {
|
|
30
|
+
var out = String(template || '')
|
|
31
|
+
for (var key in (params || {})) out = out.split('{{' + key + '}}').join(String(params[key]))
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeActionShareDialog(React, options) {
|
|
36
|
+
options = options || {}
|
|
37
|
+
var h = React.createElement
|
|
38
|
+
var useState = React.useState
|
|
39
|
+
var useEffect = React.useEffect
|
|
40
|
+
var doFetch = options.fetch || (typeof fetch !== 'undefined' ? fetch : null)
|
|
41
|
+
var inputStyle = { width: '100%', minHeight: 190, resize: 'vertical', fontFamily: 'var(--dsw-font-family)', lineHeight: 1.6, fontSize: 12, background: 'var(--dsw-alias-bg-layer-2,transparent)', color: 'inherit', border: '1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3))', borderRadius: '8px', padding: '10px', boxSizing: 'border-box' }
|
|
42
|
+
var btnStyle = { background: 'transparent', color: 'inherit', border: '1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3))', borderRadius: '8px', padding: '5px 12px', fontSize: 13, cursor: 'pointer', font: 'inherit' }
|
|
43
|
+
var primaryStyle = Object.assign({}, btnStyle, { background: 'var(--dsw-alias-brand-primary,#4a7dff)', borderColor: 'var(--dsw-alias-brand-primary,#4a7dff)', color: '#fff' })
|
|
44
|
+
|
|
45
|
+
return function ActionShareDialog(props) {
|
|
46
|
+
var title = props.title
|
|
47
|
+
var hint = props.hint
|
|
48
|
+
var labels = props.labels || {}
|
|
49
|
+
var _p = useState(props.initialPrompt || '')
|
|
50
|
+
var prompt = _p[0]; var setPrompt = _p[1]
|
|
51
|
+
var _j = useState(null)
|
|
52
|
+
var job = _j[0]; var setJob = _j[1]
|
|
53
|
+
var _b = useState(false)
|
|
54
|
+
var busy = _b[0]; var setBusy = _b[1]
|
|
55
|
+
var _c = useState(false)
|
|
56
|
+
var copied = _c[0]; var setCopied = _c[1]
|
|
57
|
+
var _e = useState('')
|
|
58
|
+
var error = _e[0]; var setError = _e[1]
|
|
59
|
+
var _d = useState(false)
|
|
60
|
+
var dirty = _d[0]; var setDirty = _d[1]
|
|
61
|
+
|
|
62
|
+
// initialPrompt 异步到位(如宿主先要下发真实路径)时跟随刷新;用户编辑过则不打断
|
|
63
|
+
useEffect(function () {
|
|
64
|
+
if (!dirty) setPrompt(props.initialPrompt || '')
|
|
65
|
+
}, [props.initialPrompt])
|
|
66
|
+
|
|
67
|
+
useEffect(function () {
|
|
68
|
+
if (job === null || job.status !== 'running' || typeof props.poll !== 'function') return
|
|
69
|
+
var timer = setInterval(function () {
|
|
70
|
+
props.poll(job.jobId).then(function (d) {
|
|
71
|
+
setJob({ jobId: job.jobId, status: d.status, output: d.output || '', code: d.code !== undefined ? d.code : null, sessionId: d.sessionId })
|
|
72
|
+
}).catch(function () {})
|
|
73
|
+
}, 1500)
|
|
74
|
+
return function () { clearInterval(timer) }
|
|
75
|
+
}, [job !== null && job.jobId])
|
|
76
|
+
|
|
77
|
+
var doRun = function () {
|
|
78
|
+
if (typeof props.run !== 'function') return
|
|
79
|
+
setBusy(true); setError('')
|
|
80
|
+
props.run(prompt).then(function (r) {
|
|
81
|
+
setJob({ jobId: r.jobId, status: 'running', output: '', code: null })
|
|
82
|
+
}).catch(function (e) { setError(String(e && e.message)) }).finally(function () { setBusy(false) })
|
|
83
|
+
}
|
|
84
|
+
var canOpenSession = typeof props.onOpenSession === 'function' && job !== null && job.sessionId
|
|
85
|
+
var openSession = function () { props.onOpenSession(job.sessionId) }
|
|
86
|
+
var copy = function () {
|
|
87
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
|
|
88
|
+
navigator.clipboard.writeText(prompt).then(function () { setCopied(true); setTimeout(function () { setCopied(false) }, 1500) }).catch(function () {})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
var statusText = job === null ? '' : job.status === 'running' ? (labels.running || 'running') : job.status === 'done' ? (labels.done || 'done') : (labels.failed || 'failed') + (job.code != null ? ' (' + job.code + ')' : '')
|
|
92
|
+
|
|
93
|
+
return h('div', { onClick: function (e) { if (e.target === e.currentTarget && props.onClose) props.onClose() }, style: { position: 'fixed', inset: 0, zIndex: 2147483000, background: 'rgba(0,0,0,.45)', display: 'flex', alignItems: 'center', justifyContent: 'center' } },
|
|
94
|
+
h('div', { style: { width: 'min(640px,92vw)', maxHeight: '86vh', overflow: 'auto', background: 'var(--dsw-alias-bg-layer-1,#fff)', border: '1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3))', borderRadius: '16px', padding: '20px', display: 'flex', flexDirection: 'column', gap: 12, color: 'var(--dsw-alias-label-primary,inherit)', font: 'var(--dsw-font-family,inherit)' } },
|
|
95
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
96
|
+
h('div', { style: { fontSize: 17, fontWeight: 600 } }, title || ''),
|
|
97
|
+
h('button', { onClick: props.onClose, style: Object.assign({}, btnStyle, { marginLeft: 'auto', width: 28, height: 28, padding: 0, borderRadius: 28 }) }, '✕')),
|
|
98
|
+
hint ? h('div', { style: { fontSize: 12, opacity: .7 } }, hint) : null,
|
|
99
|
+
(props.rows || []).length > 0 ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13 } },
|
|
100
|
+
props.rows.map(function (r, i) {
|
|
101
|
+
return r[1] ? h('div', { key: i }, h('b', null, r[0] + ':'), h('span', null, r[1])) : null
|
|
102
|
+
})) : null,
|
|
103
|
+
h('textarea', { value: prompt, onChange: function (e) { setDirty(true); setPrompt(e.target.value) }, spellCheck: false, style: inputStyle }),
|
|
104
|
+
error !== '' ? h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error,#c75050)' } }, error) : null,
|
|
105
|
+
job !== null ? h('div', null,
|
|
106
|
+
h('div', { style: { fontSize: 12, opacity: .7, margin: '4px 0' } }, (labels.outputLabel || 'Output') + ' · ' + statusText),
|
|
107
|
+
h('pre', { style: { maxHeight: 220, margin: 0, overflow: 'auto', whiteSpace: 'pre-wrap', fontSize: 12, background: 'var(--dsw-alias-bg-layer-2,transparent)', border: '1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2))', borderRadius: '8px', padding: '8px' } }, job.output || '…')) : null,
|
|
108
|
+
h('div', { style: { display: 'flex', gap: 8 } },
|
|
109
|
+
canOpenSession ? h('button', { onClick: openSession, style: btnStyle }, labels.openSession || 'Open chat') : null,
|
|
110
|
+
h('button', { onClick: copy, style: btnStyle }, copied ? (labels.copied || 'Copied') : (labels.copy || 'Copy')),
|
|
111
|
+
h('button', { onClick: doRun, disabled: busy || (job !== null && job.status === 'running'), style: primaryStyle }, job !== null && job.status === 'running' ? (labels.running || 'Running…') : (labels.run || 'Run')))))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { substituteParams: substituteParams, makeActionShareDialog: makeActionShareDialog }
|
|
116
|
+
})()
|
|
117
|
+
|
|
11
118
|
/**
|
|
12
119
|
* dsh-plugin-experts-management - Browser half.
|
|
13
120
|
*
|
|
@@ -92,6 +199,35 @@ window.__ModuleLoader__.load({
|
|
|
92
199
|
install: '安装',
|
|
93
200
|
installing: '安装中…',
|
|
94
201
|
installedDone: '已安装到用户库',
|
|
202
|
+
shareBtn: '分享',
|
|
203
|
+
openChat: '打开对话',
|
|
204
|
+
shareTitle: '分享专家到官方仓库',
|
|
205
|
+
shareHint: 'AI 将读取本机令牌,fork 官方仓库 → 建分支 → 提交该专家目录 → 创建 PR。确认或修改提示词后,复制到当前会话发送执行。',
|
|
206
|
+
shareParamName: '专家名',
|
|
207
|
+
shareParamVersion: '版本',
|
|
208
|
+
shareParamDir: '本机目录',
|
|
209
|
+
copyPrompt: '复制提示词',
|
|
210
|
+
copied: '已复制',
|
|
211
|
+
runBtn: '执行',
|
|
212
|
+
running: '执行中…',
|
|
213
|
+
runDone: '完成',
|
|
214
|
+
runFailed: '失败',
|
|
215
|
+
outputLabel: '执行输出',
|
|
216
|
+
editMeta: '编辑资料',
|
|
217
|
+
displayName: '显示名',
|
|
218
|
+
professionLabel: '职业',
|
|
219
|
+
displayDescription: '描述',
|
|
220
|
+
defaultInitPrompt: '默认开场',
|
|
221
|
+
tagsLabel: '标签',
|
|
222
|
+
edit: '编辑',
|
|
223
|
+
save: '保存',
|
|
224
|
+
cancel: '取消',
|
|
225
|
+
saved: '已保存',
|
|
226
|
+
attachSkill: '添加技能',
|
|
227
|
+
detach: '移除',
|
|
228
|
+
detachConfirm: '仅移除专家的技能副本,不影响技能库本体。确认移除?',
|
|
229
|
+
uploadAvatar: '更换头像',
|
|
230
|
+
uploading: '上传中…',
|
|
95
231
|
overwrite: '覆盖安装',
|
|
96
232
|
remove: '删除',
|
|
97
233
|
removing: '删除中…',
|
|
@@ -161,6 +297,35 @@ window.__ModuleLoader__.load({
|
|
|
161
297
|
install: 'Install',
|
|
162
298
|
installing: 'Installing…',
|
|
163
299
|
installedDone: 'Installed to the user library',
|
|
300
|
+
shareBtn: 'Share',
|
|
301
|
+
openChat: 'Open chat',
|
|
302
|
+
shareTitle: 'Share expert to the official repo',
|
|
303
|
+
shareHint: 'AI will read the local token, fork the official repo → create a branch → commit the expert directory → open a PR. Review or edit the prompt, then copy it into the conversation to run.',
|
|
304
|
+
shareParamName: 'Expert',
|
|
305
|
+
shareParamVersion: 'Version',
|
|
306
|
+
shareParamDir: 'Local dir',
|
|
307
|
+
copyPrompt: 'Copy prompt',
|
|
308
|
+
copied: 'Copied',
|
|
309
|
+
runBtn: 'Run',
|
|
310
|
+
running: 'Running…',
|
|
311
|
+
runDone: 'Done',
|
|
312
|
+
runFailed: 'Failed',
|
|
313
|
+
outputLabel: 'Output',
|
|
314
|
+
editMeta: 'Edit profile',
|
|
315
|
+
displayName: 'Display name',
|
|
316
|
+
professionLabel: 'Profession',
|
|
317
|
+
displayDescription: 'Description',
|
|
318
|
+
defaultInitPrompt: 'Default opener',
|
|
319
|
+
tagsLabel: 'Tags',
|
|
320
|
+
edit: 'Edit',
|
|
321
|
+
save: 'Save',
|
|
322
|
+
cancel: 'Cancel',
|
|
323
|
+
saved: 'Saved',
|
|
324
|
+
attachSkill: 'Add skill',
|
|
325
|
+
detach: 'Remove',
|
|
326
|
+
detachConfirm: "Only the expert's copy is removed — the skill library is untouched. Remove?",
|
|
327
|
+
uploadAvatar: 'Change avatar',
|
|
328
|
+
uploading: 'Uploading…',
|
|
164
329
|
overwrite: 'Overwrite install',
|
|
165
330
|
remove: 'Delete',
|
|
166
331
|
removing: 'Deleting…',
|
|
@@ -262,7 +427,7 @@ window.__ModuleLoader__.load({
|
|
|
262
427
|
.exp-skill-row:last-child{border-bottom:0}
|
|
263
428
|
.exp-form-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
264
429
|
.exp-form-row label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary);flex:1;min-width:160px}
|
|
265
|
-
.exp-input{background:var(--dsw-alias-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;width:100%}
|
|
430
|
+
.exp-flash{font-size:12px;color:var(--dsw-alias-state-positive,#3aa76d)}.exp-input{background:var(--dsw-alias-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;width:100%}
|
|
266
431
|
.exp-status-line{display:flex;gap:14px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
267
432
|
.exp-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-primary);border-radius:10px;padding:8px 18px;font-size:13px;z-index:80;box-shadow:0 8px 24px rgba(0,0,0,.25)}
|
|
268
433
|
.exp-checkline{display:flex;gap:6px;align-items:center;font-size:13px;color:var(--dsw-alias-label-secondary)}
|
|
@@ -626,18 +791,138 @@ window.__ModuleLoader__.load({
|
|
|
626
791
|
: h('div', { className: 'exp-member-avatar', title: t('avatarLoadFailed') }, '👤')
|
|
627
792
|
}
|
|
628
793
|
|
|
629
|
-
|
|
794
|
+
let shareDialogComponent = null
|
|
795
|
+
function getShareDialogComponent() {
|
|
796
|
+
if (shareDialogComponent === null) shareDialogComponent = PluginKit.makeActionShareDialog(__React)
|
|
797
|
+
return shareDialogComponent
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
let sessionsApi = null // 「打开对话」用的宿主 sessions 服务(apply 时动态注入捕获)
|
|
801
|
+
|
|
802
|
+
/** 专家分享提示词:提交到 ntd-resource 的 experts/ 子树(与技能分享同管线、同 token)。 */
|
|
803
|
+
const EXPERT_SHARE_PROMPT = [
|
|
804
|
+
'请把本地专家「{{expertName}}」{{version}}打包提交到 GitCode 官方仓库 weibaohui/ntd-resource 的 experts/ 子树,作为一个 PR 供维护者审核。',
|
|
805
|
+
'',
|
|
806
|
+
'## 关键信息',
|
|
807
|
+
'- 专家目录:{{resourceDir}}(~ 表示当前用户家目录,执行前先展开为绝对路径)',
|
|
808
|
+
'- 官方仓库:weibaohui/ntd-resource(GitCode,API base = https://api.gitcode.com)',
|
|
809
|
+
'- PAT 位置:{{settingsFile}} 中 skills-management.market 段的 token 字段(由技能市场设置面板保存,与技能分享同源)。',
|
|
810
|
+
'',
|
|
811
|
+
'## 执行步骤(严格按顺序)',
|
|
812
|
+
'1. 读取 PAT:读取 {{settingsFile}},定位 skills-management.market 段下的 token 字段。token 是敏感凭据,读取后不要把明文打印到输出、日志或最终结果里。',
|
|
813
|
+
'2. 展开专家目录为绝对路径,遍历该目录(含 agents/、skills/、avatars/ 子目录与 .codebuddy-plugin/plugin.json),收集每个文件的「相对该目录的路径」与内容;跳过 .git 一类同步元数据。',
|
|
814
|
+
'3. 把第 1 步读到的 token 作为 HTTP 认证令牌(bearer),按顺序调用 GitCode API:',
|
|
815
|
+
' a. 验证用户:`GET https://api.gitcode.com/api/v5/user`,拿到 login 字段——后续所有 URL 里的 {owner} 一律用它。',
|
|
816
|
+
' b. fork:`POST https://api.gitcode.com/api/v5/repos/weibaohui/ntd-resource/forks`;若返回 409/422 表示已 fork,视为成功。',
|
|
817
|
+
' c. 建分支:`POST https://api.gitcode.com/api/v5/repos/{owner}/ntd-resource/branches`,JSON body 为 {"branch_name":"experts/{{expertName}}-<unix 时间戳>","refs":"main"}。',
|
|
818
|
+
' d. 写文件:对第 2 步收集的每个文件,`POST https://api.gitcode.com/api/v5/repos/{owner}/ntd-resource/contents/experts/{{该文件相对专家目录的路径}}`。**必须**用 experts/ 前缀,不能写到仓库根目录。表单字段 content=<文件字节的 base64>、message="贡献专家 {{expertName}} {{version}}"、branch=<步骤 c 的分支名>。',
|
|
819
|
+
' e. 创建 PR:`POST https://api.gitcode.com/api/v5/repos/weibaohui/ntd-resource/pulls`,JSON body 为 {"title":"[专家] {{expertName}} {{version}}","body":"专家目录 {{resourceDir}} 的文件清单与用途简介","head":"{owner}:{branch}","base":"main"}。',
|
|
820
|
+
'4. 完成后,最终输出 PR 的网页链接(响应里的 web_url 字段)。',
|
|
821
|
+
'',
|
|
822
|
+
'## 注意',
|
|
823
|
+
'- token 是敏感凭据,任何输出里都不要回显其明文。',
|
|
824
|
+
'- 如果任一步骤失败,先检查错误信息,不要盲目重试;若 token 失效,提示用户到技能市场的 ⚙ 设置面板重新填写。',
|
|
825
|
+
'- 全程与最终汇报都使用中文。',
|
|
826
|
+
].join('\n')
|
|
827
|
+
|
|
828
|
+
function DetailModal({ name, source, t, onClose, onInstalled, onDeleted, onToast }) {
|
|
630
829
|
const [detail, setDetail] = useState(null)
|
|
631
830
|
const [error, setError] = useState('')
|
|
632
831
|
const [busy, setBusy] = useState(false)
|
|
633
832
|
const [agentMd, setAgentMd] = useState(null)
|
|
833
|
+
const detailUrl = () => `${API}/detail?name=${encodeURIComponent(name)}${source ? `&source=${encodeURIComponent(source)}` : ''}`
|
|
834
|
+
const loadDetail = () => fetchJson(detailUrl())
|
|
835
|
+
.then((d) => setDetail(d))
|
|
836
|
+
.catch((e) => setError(String(e && e.message)))
|
|
837
|
+
useEffect(() => { loadDetail() }, [name, source])
|
|
838
|
+
// ── 编辑态(v0.3,仅 dsh 用户库专家)──
|
|
839
|
+
const [editingMd, setEditingMd] = useState(null) // {agent, content} | null
|
|
840
|
+
const [metaEdit, setMetaEdit] = useState(false)
|
|
841
|
+
const [metaForm, setMetaForm] = useState(null)
|
|
842
|
+
const [skillsEdit, setSkillsEdit] = useState(false)
|
|
843
|
+
const [availSkills, setAvailSkills] = useState(null)
|
|
844
|
+
const [saving, setSaving] = useState(false)
|
|
845
|
+
const [savedFlash, setSavedFlash] = useState('')
|
|
846
|
+
const [avatarBusy, setAvatarBusy] = useState(false)
|
|
847
|
+
const [shareOpen, setShareOpen] = useState(false)
|
|
848
|
+
const [shareSettingsFile, setShareSettingsFile] = useState('')
|
|
849
|
+
const avatarInputRef = useRef(null)
|
|
634
850
|
useEffect(() => {
|
|
635
|
-
|
|
636
|
-
fetchJson(`${API}/
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
851
|
+
if (shareOpen === false) return
|
|
852
|
+
fetchJson(`${API}/share/status`).then((d) => setShareSettingsFile(d.settingsFile || '')).catch(() => setShareSettingsFile(''))
|
|
853
|
+
}, [shareOpen])
|
|
854
|
+
const flash = (text) => { setSavedFlash(text); setTimeout(() => setSavedFlash(''), 1600) }
|
|
855
|
+
const startEditMd = (agentName) => {
|
|
856
|
+
fetchJson(`${API}/agent-md?name=${encodeURIComponent(name)}${source ? `&source=${encodeURIComponent(source)}` : ''}&agent=${encodeURIComponent(agentName)}`)
|
|
857
|
+
.then((r) => setEditingMd({ agent: agentName, content: r.content }))
|
|
858
|
+
.catch((e) => setError(String(e && e.message)))
|
|
859
|
+
}
|
|
860
|
+
const saveMd = async () => {
|
|
861
|
+
setSaving(true); setError('')
|
|
862
|
+
try {
|
|
863
|
+
await fetchJson(`${API}/agent-md`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, agent: editingMd.agent, content: editingMd.content }) })
|
|
864
|
+
setEditingMd(null); flash(t('saved')); loadDetail()
|
|
865
|
+
} catch (e) { setError(String(e && e.message)) } finally { setSaving(false) }
|
|
866
|
+
}
|
|
867
|
+
const startMetaEdit = () => {
|
|
868
|
+
const p = (detail && detail.pluginJson) || {}
|
|
869
|
+
const loc = (v) => ({ zh: (v && v.zh) || '', en: (v && v.en) || '' })
|
|
870
|
+
const tags = Array.isArray(p.tags) ? p.tags : []
|
|
871
|
+
setMetaForm({
|
|
872
|
+
displayName: loc(p.displayName), profession: loc(p.profession),
|
|
873
|
+
displayDescription: loc(p.displayDescription), defaultInitPrompt: loc(p.defaultInitPrompt),
|
|
874
|
+
tagsZh: tags.map((x) => x.zh || '').filter(Boolean).join(','),
|
|
875
|
+
tagsEn: tags.map((x) => x.en || '').filter(Boolean).join(','),
|
|
876
|
+
quickPrompts: Array.isArray(p.quickPrompts) ? p.quickPrompts : [],
|
|
877
|
+
})
|
|
878
|
+
setMetaEdit(true)
|
|
879
|
+
}
|
|
880
|
+
const saveMeta = async () => {
|
|
881
|
+
setSaving(true); setError('')
|
|
882
|
+
const splitList = (v) => String(v || '').split(/[,,]/).map((x) => x.trim()).filter(Boolean)
|
|
883
|
+
const body = { metadata: {
|
|
884
|
+
displayName: metaForm.displayName, profession: metaForm.profession,
|
|
885
|
+
displayDescription: metaForm.displayDescription, defaultInitPrompt: metaForm.defaultInitPrompt,
|
|
886
|
+
tags: (function () {
|
|
887
|
+
const zhList = splitList(metaForm.tagsZh); const enList = splitList(metaForm.tagsEn)
|
|
888
|
+
const len = Math.max(zhList.length, enList.length)
|
|
889
|
+
return Array.from({ length: len }, (_, i) => ({ zh: zhList[i] || '', en: enList[i] || '' })).filter((t) => t.zh !== '' || t.en !== '')
|
|
890
|
+
})(),
|
|
891
|
+
quickPrompts: metaForm.quickPrompts,
|
|
892
|
+
} }
|
|
893
|
+
try {
|
|
894
|
+
await fetchJson(`${API}/metadata`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, ...body }) })
|
|
895
|
+
setMetaEdit(false); flash(t('saved')); loadDetail()
|
|
896
|
+
} catch (e) { setError(String(e && e.message)) } finally { setSaving(false) }
|
|
897
|
+
}
|
|
898
|
+
const detachSkill = (skillName) => {
|
|
899
|
+
if (!window.confirm(t('detachConfirm'))) return
|
|
900
|
+
fetchJson(`${API}/expert-skills`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, detach: [skillName] }) })
|
|
901
|
+
.then(() => { flash(t('saved')); loadDetail() })
|
|
902
|
+
.catch((e) => setError(String(e && e.message)))
|
|
903
|
+
}
|
|
904
|
+
const openSkillsEdit = () => {
|
|
905
|
+
setSkillsEdit(true)
|
|
906
|
+
if (availSkills === null) {
|
|
907
|
+
fetchJson(`${API}/available-skills`).then((d) => setAvailSkills(d.skills || [])).catch((e) => setError(String(e && e.message)))
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const attachSkill = (skillName) => {
|
|
911
|
+
fetchJson(`${API}/expert-skills`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, attach: [skillName] }) })
|
|
912
|
+
.then(() => { flash(t('saved')); loadDetail() })
|
|
913
|
+
.catch((e) => setError(String(e && e.message)))
|
|
914
|
+
}
|
|
915
|
+
const onAvatarFile = async (e) => {
|
|
916
|
+
const f = e.target.files && e.target.files[0]
|
|
917
|
+
e.target.value = ''
|
|
918
|
+
if (!f) return
|
|
919
|
+
setAvatarBusy(true); setError('')
|
|
920
|
+
try {
|
|
921
|
+
const buf = new Uint8Array(await f.arrayBuffer())
|
|
922
|
+
await fetchJson(`${API}/avatar?name=${encodeURIComponent(name)}`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: buf })
|
|
923
|
+
flash(t('saved')); loadDetail()
|
|
924
|
+
} catch (ex) { setError(String(ex && ex.message)) } finally { setAvatarBusy(false) }
|
|
925
|
+
}
|
|
641
926
|
const loadAgentMd = () => {
|
|
642
927
|
fetchJson(`${API}/agent-md?name=${encodeURIComponent(name)}${source ? `&source=${encodeURIComponent(source)}` : ''}`)
|
|
643
928
|
.then((r) => setAgentMd(r.content))
|
|
@@ -679,6 +964,24 @@ window.__ModuleLoader__.load({
|
|
|
679
964
|
kv(t('filesLabel'), `${detail.fileCount} / ${formatSize(detail.totalSize)}`),
|
|
680
965
|
kv(t('dirLabel'), detail.dir)),
|
|
681
966
|
detail.readOnly ? h('div', { className: 'exp-kv' }, h('span', { style: { color: 'var(--dsw-alias-label-tertiary)' } }, t('sourceReadonly'))) : null,
|
|
967
|
+
detail.source === 'dsh' ? h('div', { className: 'exp-form-row' },
|
|
968
|
+
h('button', { className: 'exp-btn', disabled: busy || saving || avatarBusy, onClick: startMetaEdit }, t('editMeta')),
|
|
969
|
+
h('button', { className: 'exp-btn', disabled: busy || saving || avatarBusy, onClick: () => { if (avatarInputRef.current) avatarInputRef.current.click() } }, avatarBusy ? t('uploading') : t('uploadAvatar')),
|
|
970
|
+
savedFlash ? h('span', { className: 'exp-flash' }, savedFlash) : null,
|
|
971
|
+
h('input', { ref: avatarInputRef, type: 'file', accept: 'image/png,image/jpeg,image/gif,image/webp', style: { display: 'none' }, onChange: onAvatarFile })) : null,
|
|
972
|
+
metaEdit && metaForm ? h('div', { className: 'exp-section' },
|
|
973
|
+
h('div', { className: 'exp-section-title' }, t('editMeta')),
|
|
974
|
+
...['displayName', 'profession', 'displayDescription', 'defaultInitPrompt'].map((key) => h('div', { key, style: { marginBottom: '8px' } },
|
|
975
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, t(key === 'profession' ? 'professionLabel' : key === 'tags' ? 'tagsLabel' : key)),
|
|
976
|
+
h('input', { className: 'exp-input', value: metaForm[key].zh, placeholder: 'zh', onChange: (e) => setMetaForm({ ...metaForm, [key]: { ...metaForm[key], zh: e.target.value } }), style: { marginBottom: '4px', width: '100%' } }),
|
|
977
|
+
h('input', { className: 'exp-input', value: metaForm[key].en, placeholder: 'en', onChange: (e) => setMetaForm({ ...metaForm, [key]: { ...metaForm[key], en: e.target.value } }), style: { width: '100%' } }))),
|
|
978
|
+
h('div', { style: { marginBottom: '8px' } },
|
|
979
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, t('tagsLabel')),
|
|
980
|
+
h('input', { className: 'exp-input', value: metaForm.tagsZh, placeholder: 'zh,逗号分隔', onChange: (e) => setMetaForm({ ...metaForm, tagsZh: e.target.value }), style: { marginBottom: '4px', width: '100%' } }),
|
|
981
|
+
h('input', { className: 'exp-input', value: metaForm.tagsEn, placeholder: 'en, comma separated', onChange: (e) => setMetaForm({ ...metaForm, tagsEn: e.target.value }), style: { width: '100%' } })),
|
|
982
|
+
h('div', { className: 'exp-form-row' },
|
|
983
|
+
h('button', { className: 'exp-btn', 'data-primary': 'true', disabled: saving, onClick: saveMeta }, saving ? '…' : t('save')),
|
|
984
|
+
h('button', { className: 'exp-btn', disabled: saving, onClick: () => setMetaEdit(false) }, t('cancel')))) : null,
|
|
682
985
|
(detail.quickPromptsZh || []).length > 0 ? h('div', { className: 'exp-section' },
|
|
683
986
|
h('div', { className: 'exp-section-title' }, t('quickPrompts')),
|
|
684
987
|
...detail.quickPromptsZh.slice(0, 5).map((q, i) => h('div', { className: 'exp-kv', key: i }, '• ', q))) : null,
|
|
@@ -694,13 +997,33 @@ window.__ModuleLoader__.load({
|
|
|
694
997
|
h('div', { className: 'exp-section-title' }, t('skills')),
|
|
695
998
|
...detail.skillMeta.map((s) => h('div', { className: 'exp-skill-row', key: s.skillName },
|
|
696
999
|
h('div', { style: { fontSize: 13 } }, `${s.emoji ? s.emoji + ' ' : ''}${s.skillName}`),
|
|
697
|
-
h('div', { className: 'exp-profession' }, s.descriptionZh || s.descriptionEn || s.description || '')
|
|
1000
|
+
h('div', { className: 'exp-profession' }, s.descriptionZh || s.descriptionEn || s.description || ''),
|
|
1001
|
+
detail.source === 'dsh' ? h('button', { className: 'exp-btn', style: { marginLeft: 'auto' }, onClick: () => detachSkill(s.skillName) }, t('detach')) : null)),
|
|
1002
|
+
detail.source === 'dsh' ? h('div', { className: 'exp-form-row' },
|
|
1003
|
+
skillsEdit ? null : h('button', { className: 'exp-btn', onClick: openSkillsEdit }, t('attachSkill'))) : null,
|
|
1004
|
+
detail.source === 'dsh' && skillsEdit ? h('div', { className: 'exp-form-row' },
|
|
1005
|
+
availSkills === null ? h('span', { className: 'exp-profession' }, '…')
|
|
1006
|
+
: availSkills.length === 0 ? h('span', { className: 'exp-profession' }, '—')
|
|
1007
|
+
: availSkills.map((sk) => h('button', { key: sk.name, className: 'exp-btn', title: sk.description, onClick: () => attachSkill(sk.name) }, `+ ${sk.name}`))) : null) : null,
|
|
698
1008
|
(detail.agentFiles || []).length > 0 ? h('div', { className: 'exp-section' },
|
|
699
1009
|
h('div', { className: 'exp-section-title' }, t('agents')),
|
|
700
1010
|
...detail.agentFiles.map((a) => h('div', { className: 'exp-skill-row', key: a.relPath },
|
|
701
1011
|
h('div', { style: { fontSize: 13 } }, `${a.emoji ? a.emoji + ' ' : ''}${a.name}${a.name === detail.leadAgentFile ? ' ★' : ''}`),
|
|
702
|
-
h('div', { className: 'exp-profession' }, a.description || '')
|
|
1012
|
+
h('div', { className: 'exp-profession' }, a.description || ''),
|
|
1013
|
+
detail.source === 'dsh' ? h('button', { className: 'exp-btn', style: { marginLeft: 'auto' }, onClick: () => startEditMd(a.name) }, t('edit')) : null))) : null,
|
|
1014
|
+
editingMd !== null ? h('div', { className: 'exp-section' },
|
|
1015
|
+
h('div', { className: 'exp-section-title' }, `${t('edit')} · ${editingMd.agent}`),
|
|
1016
|
+
h('textarea', {
|
|
1017
|
+
className: 'exp-input', value: editingMd.content,
|
|
1018
|
+
onChange: (e) => setEditingMd({ ...editingMd, content: e.target.value }),
|
|
1019
|
+
spellCheck: false,
|
|
1020
|
+
style: { width: '100%', minHeight: '260px', fontFamily: 'ui-monospace,monospace', fontSize: '12px', lineHeight: 1.6, whiteSpace: 'pre-wrap', background: 'var(--dsw-alias-bg-layer-2,transparent)', color: 'inherit', border: '1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3))', borderRadius: '8px', padding: '10px', boxSizing: 'border-box' },
|
|
1021
|
+
}),
|
|
1022
|
+
h('div', { className: 'exp-form-row' },
|
|
1023
|
+
h('button', { className: 'exp-btn', 'data-primary': 'true', disabled: saving, onClick: saveMd }, saving ? '…' : t('save')),
|
|
1024
|
+
h('button', { className: 'exp-btn', disabled: saving, onClick: () => setEditingMd(null) }, t('cancel')))) : null,
|
|
703
1025
|
h('div', { className: 'exp-form-row' },
|
|
1026
|
+
h('button', { className: 'exp-btn', onClick: () => setShareOpen(true) }, t('shareBtn')),
|
|
704
1027
|
h('button', { className: 'exp-btn', onClick: () => (agentMd === null ? loadAgentMd() : setAgentMd(null)) }, agentMd === null ? t('viewAgentMd') : t('hideAgentMd')),
|
|
705
1028
|
detail.source !== 'dsh'
|
|
706
1029
|
? h('button', { className: 'exp-btn', 'data-primary': 'true', disabled: busy, onClick: () => install(false) }, busy ? t('installing') : t('install'))
|
|
@@ -712,7 +1035,17 @@ window.__ModuleLoader__.load({
|
|
|
712
1035
|
detail.plugin ? h('details', null,
|
|
713
1036
|
h('summary', { style: { cursor: 'pointer', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } }, t('pluginJson')),
|
|
714
1037
|
h('pre', { className: 'exp-pre' }, JSON.stringify(detail.plugin, null, 2))) : null,
|
|
715
|
-
) : null
|
|
1038
|
+
) : null,
|
|
1039
|
+
shareOpen ? h(getShareDialogComponent(), {
|
|
1040
|
+
title: t('shareTitle'), hint: t('shareHint'),
|
|
1041
|
+
rows: [[t('shareParamName'), detail.name], [t('shareParamVersion'), detail.version || '1.0.0'], [t('shareParamDir'), detail.dir]],
|
|
1042
|
+
initialPrompt: PluginKit.substituteParams(EXPERT_SHARE_PROMPT, { expertName: detail.name, version: detail.version || '1.0.0', resourceDir: detail.dir, settingsFile: shareSettingsFile }),
|
|
1043
|
+
labels: { copy: t('copyPrompt'), copied: t('copied'), run: t('runBtn'), running: t('running'), done: t('runDone'), failed: t('runFailed'), outputLabel: t('outputLabel'), openSession: t('openChat') },
|
|
1044
|
+
run: (prompt) => fetchJson(`${API}/share/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, dir: detail.dir }) }),
|
|
1045
|
+
poll: (id) => fetchJson(`${API}/share/run?id=${encodeURIComponent(id)}`),
|
|
1046
|
+
onOpenSession: (sessionId) => { if (onOpenSession) onOpenSession(sessionId) },
|
|
1047
|
+
onClose: () => setShareOpen(false),
|
|
1048
|
+
}) : null))
|
|
716
1049
|
}
|
|
717
1050
|
|
|
718
1051
|
function BuiltinSettingsDialog({ t, onClose, onToast, onSynced }) {
|
|
@@ -864,6 +1197,7 @@ window.__ModuleLoader__.load({
|
|
|
864
1197
|
: null,
|
|
865
1198
|
selected !== null ? h(DetailModal, {
|
|
866
1199
|
name: selected.name, source: selected.source, t, onClose: () => setSelected(null),
|
|
1200
|
+
onOpenSession: (sessionId) => { try { sessionsApi.open(sessionId) } catch (e) { showToast(String(e && e.message)) } },
|
|
867
1201
|
onInstalled: () => { setSelected(null); showToast(t('installedDone')); reload() },
|
|
868
1202
|
onDeleted: () => { setSelected(null); showToast(t('removedDone')); reload() },
|
|
869
1203
|
}) : null,
|
package/client/index.js
CHANGED
|
@@ -82,6 +82,35 @@ const ZH = {
|
|
|
82
82
|
install: '安装',
|
|
83
83
|
installing: '安装中…',
|
|
84
84
|
installedDone: '已安装到用户库',
|
|
85
|
+
shareBtn: '分享',
|
|
86
|
+
openChat: '打开对话',
|
|
87
|
+
shareTitle: '分享专家到官方仓库',
|
|
88
|
+
shareHint: 'AI 将读取本机令牌,fork 官方仓库 → 建分支 → 提交该专家目录 → 创建 PR。确认或修改提示词后,复制到当前会话发送执行。',
|
|
89
|
+
shareParamName: '专家名',
|
|
90
|
+
shareParamVersion: '版本',
|
|
91
|
+
shareParamDir: '本机目录',
|
|
92
|
+
copyPrompt: '复制提示词',
|
|
93
|
+
copied: '已复制',
|
|
94
|
+
runBtn: '执行',
|
|
95
|
+
running: '执行中…',
|
|
96
|
+
runDone: '完成',
|
|
97
|
+
runFailed: '失败',
|
|
98
|
+
outputLabel: '执行输出',
|
|
99
|
+
editMeta: '编辑资料',
|
|
100
|
+
displayName: '显示名',
|
|
101
|
+
professionLabel: '职业',
|
|
102
|
+
displayDescription: '描述',
|
|
103
|
+
defaultInitPrompt: '默认开场',
|
|
104
|
+
tagsLabel: '标签',
|
|
105
|
+
edit: '编辑',
|
|
106
|
+
save: '保存',
|
|
107
|
+
cancel: '取消',
|
|
108
|
+
saved: '已保存',
|
|
109
|
+
attachSkill: '添加技能',
|
|
110
|
+
detach: '移除',
|
|
111
|
+
detachConfirm: '仅移除专家的技能副本,不影响技能库本体。确认移除?',
|
|
112
|
+
uploadAvatar: '更换头像',
|
|
113
|
+
uploading: '上传中…',
|
|
85
114
|
overwrite: '覆盖安装',
|
|
86
115
|
remove: '删除',
|
|
87
116
|
removing: '删除中…',
|
|
@@ -151,6 +180,35 @@ const EN = {
|
|
|
151
180
|
install: 'Install',
|
|
152
181
|
installing: 'Installing…',
|
|
153
182
|
installedDone: 'Installed to the user library',
|
|
183
|
+
shareBtn: 'Share',
|
|
184
|
+
openChat: 'Open chat',
|
|
185
|
+
shareTitle: 'Share expert to the official repo',
|
|
186
|
+
shareHint: 'AI will read the local token, fork the official repo → create a branch → commit the expert directory → open a PR. Review or edit the prompt, then copy it into the conversation to run.',
|
|
187
|
+
shareParamName: 'Expert',
|
|
188
|
+
shareParamVersion: 'Version',
|
|
189
|
+
shareParamDir: 'Local dir',
|
|
190
|
+
copyPrompt: 'Copy prompt',
|
|
191
|
+
copied: 'Copied',
|
|
192
|
+
runBtn: 'Run',
|
|
193
|
+
running: 'Running…',
|
|
194
|
+
runDone: 'Done',
|
|
195
|
+
runFailed: 'Failed',
|
|
196
|
+
outputLabel: 'Output',
|
|
197
|
+
editMeta: 'Edit profile',
|
|
198
|
+
displayName: 'Display name',
|
|
199
|
+
professionLabel: 'Profession',
|
|
200
|
+
displayDescription: 'Description',
|
|
201
|
+
defaultInitPrompt: 'Default opener',
|
|
202
|
+
tagsLabel: 'Tags',
|
|
203
|
+
edit: 'Edit',
|
|
204
|
+
save: 'Save',
|
|
205
|
+
cancel: 'Cancel',
|
|
206
|
+
saved: 'Saved',
|
|
207
|
+
attachSkill: 'Add skill',
|
|
208
|
+
detach: 'Remove',
|
|
209
|
+
detachConfirm: "Only the expert's copy is removed — the skill library is untouched. Remove?",
|
|
210
|
+
uploadAvatar: 'Change avatar',
|
|
211
|
+
uploading: 'Uploading…',
|
|
154
212
|
overwrite: 'Overwrite install',
|
|
155
213
|
remove: 'Delete',
|
|
156
214
|
removing: 'Deleting…',
|
|
@@ -252,7 +310,7 @@ const STYLE = `<style>
|
|
|
252
310
|
.exp-skill-row:last-child{border-bottom:0}
|
|
253
311
|
.exp-form-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
254
312
|
.exp-form-row label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary);flex:1;min-width:160px}
|
|
255
|
-
.exp-input{background:var(--dsw-alias-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;width:100%}
|
|
313
|
+
.exp-flash{font-size:12px;color:var(--dsw-alias-state-positive,#3aa76d)}.exp-input{background:var(--dsw-alias-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;width:100%}
|
|
256
314
|
.exp-status-line{display:flex;gap:14px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
257
315
|
.exp-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-primary);border-radius:10px;padding:8px 18px;font-size:13px;z-index:80;box-shadow:0 8px 24px rgba(0,0,0,.25)}
|
|
258
316
|
.exp-checkline{display:flex;gap:6px;align-items:center;font-size:13px;color:var(--dsw-alias-label-secondary)}
|
|
@@ -616,18 +674,138 @@ function MemberAvatar({ expertName, source, member, t }) {
|
|
|
616
674
|
: h('div', { className: 'exp-member-avatar', title: t('avatarLoadFailed') }, '👤')
|
|
617
675
|
}
|
|
618
676
|
|
|
619
|
-
|
|
677
|
+
let shareDialogComponent = null
|
|
678
|
+
function getShareDialogComponent() {
|
|
679
|
+
if (shareDialogComponent === null) shareDialogComponent = PluginKit.makeActionShareDialog(__React)
|
|
680
|
+
return shareDialogComponent
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
let sessionsApi = null // 「打开对话」用的宿主 sessions 服务(apply 时动态注入捕获)
|
|
684
|
+
|
|
685
|
+
/** 专家分享提示词:提交到 ntd-resource 的 experts/ 子树(与技能分享同管线、同 token)。 */
|
|
686
|
+
const EXPERT_SHARE_PROMPT = [
|
|
687
|
+
'请把本地专家「{{expertName}}」{{version}}打包提交到 GitCode 官方仓库 weibaohui/ntd-resource 的 experts/ 子树,作为一个 PR 供维护者审核。',
|
|
688
|
+
'',
|
|
689
|
+
'## 关键信息',
|
|
690
|
+
'- 专家目录:{{resourceDir}}(~ 表示当前用户家目录,执行前先展开为绝对路径)',
|
|
691
|
+
'- 官方仓库:weibaohui/ntd-resource(GitCode,API base = https://api.gitcode.com)',
|
|
692
|
+
'- PAT 位置:{{settingsFile}} 中 skills-management.market 段的 token 字段(由技能市场设置面板保存,与技能分享同源)。',
|
|
693
|
+
'',
|
|
694
|
+
'## 执行步骤(严格按顺序)',
|
|
695
|
+
'1. 读取 PAT:读取 {{settingsFile}},定位 skills-management.market 段下的 token 字段。token 是敏感凭据,读取后不要把明文打印到输出、日志或最终结果里。',
|
|
696
|
+
'2. 展开专家目录为绝对路径,遍历该目录(含 agents/、skills/、avatars/ 子目录与 .codebuddy-plugin/plugin.json),收集每个文件的「相对该目录的路径」与内容;跳过 .git 一类同步元数据。',
|
|
697
|
+
'3. 把第 1 步读到的 token 作为 HTTP 认证令牌(bearer),按顺序调用 GitCode API:',
|
|
698
|
+
' a. 验证用户:`GET https://api.gitcode.com/api/v5/user`,拿到 login 字段——后续所有 URL 里的 {owner} 一律用它。',
|
|
699
|
+
' b. fork:`POST https://api.gitcode.com/api/v5/repos/weibaohui/ntd-resource/forks`;若返回 409/422 表示已 fork,视为成功。',
|
|
700
|
+
' c. 建分支:`POST https://api.gitcode.com/api/v5/repos/{owner}/ntd-resource/branches`,JSON body 为 {"branch_name":"experts/{{expertName}}-<unix 时间戳>","refs":"main"}。',
|
|
701
|
+
' d. 写文件:对第 2 步收集的每个文件,`POST https://api.gitcode.com/api/v5/repos/{owner}/ntd-resource/contents/experts/{{该文件相对专家目录的路径}}`。**必须**用 experts/ 前缀,不能写到仓库根目录。表单字段 content=<文件字节的 base64>、message="贡献专家 {{expertName}} {{version}}"、branch=<步骤 c 的分支名>。',
|
|
702
|
+
' e. 创建 PR:`POST https://api.gitcode.com/api/v5/repos/weibaohui/ntd-resource/pulls`,JSON body 为 {"title":"[专家] {{expertName}} {{version}}","body":"专家目录 {{resourceDir}} 的文件清单与用途简介","head":"{owner}:{branch}","base":"main"}。',
|
|
703
|
+
'4. 完成后,最终输出 PR 的网页链接(响应里的 web_url 字段)。',
|
|
704
|
+
'',
|
|
705
|
+
'## 注意',
|
|
706
|
+
'- token 是敏感凭据,任何输出里都不要回显其明文。',
|
|
707
|
+
'- 如果任一步骤失败,先检查错误信息,不要盲目重试;若 token 失效,提示用户到技能市场的 ⚙ 设置面板重新填写。',
|
|
708
|
+
'- 全程与最终汇报都使用中文。',
|
|
709
|
+
].join('\n')
|
|
710
|
+
|
|
711
|
+
function DetailModal({ name, source, t, onClose, onInstalled, onDeleted, onToast }) {
|
|
620
712
|
const [detail, setDetail] = useState(null)
|
|
621
713
|
const [error, setError] = useState('')
|
|
622
714
|
const [busy, setBusy] = useState(false)
|
|
623
715
|
const [agentMd, setAgentMd] = useState(null)
|
|
716
|
+
const detailUrl = () => `${API}/detail?name=${encodeURIComponent(name)}${source ? `&source=${encodeURIComponent(source)}` : ''}`
|
|
717
|
+
const loadDetail = () => fetchJson(detailUrl())
|
|
718
|
+
.then((d) => setDetail(d))
|
|
719
|
+
.catch((e) => setError(String(e && e.message)))
|
|
720
|
+
useEffect(() => { loadDetail() }, [name, source])
|
|
721
|
+
// ── 编辑态(v0.3,仅 dsh 用户库专家)──
|
|
722
|
+
const [editingMd, setEditingMd] = useState(null) // {agent, content} | null
|
|
723
|
+
const [metaEdit, setMetaEdit] = useState(false)
|
|
724
|
+
const [metaForm, setMetaForm] = useState(null)
|
|
725
|
+
const [skillsEdit, setSkillsEdit] = useState(false)
|
|
726
|
+
const [availSkills, setAvailSkills] = useState(null)
|
|
727
|
+
const [saving, setSaving] = useState(false)
|
|
728
|
+
const [savedFlash, setSavedFlash] = useState('')
|
|
729
|
+
const [avatarBusy, setAvatarBusy] = useState(false)
|
|
730
|
+
const [shareOpen, setShareOpen] = useState(false)
|
|
731
|
+
const [shareSettingsFile, setShareSettingsFile] = useState('')
|
|
732
|
+
const avatarInputRef = useRef(null)
|
|
624
733
|
useEffect(() => {
|
|
625
|
-
|
|
626
|
-
fetchJson(`${API}/
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
734
|
+
if (shareOpen === false) return
|
|
735
|
+
fetchJson(`${API}/share/status`).then((d) => setShareSettingsFile(d.settingsFile || '')).catch(() => setShareSettingsFile(''))
|
|
736
|
+
}, [shareOpen])
|
|
737
|
+
const flash = (text) => { setSavedFlash(text); setTimeout(() => setSavedFlash(''), 1600) }
|
|
738
|
+
const startEditMd = (agentName) => {
|
|
739
|
+
fetchJson(`${API}/agent-md?name=${encodeURIComponent(name)}${source ? `&source=${encodeURIComponent(source)}` : ''}&agent=${encodeURIComponent(agentName)}`)
|
|
740
|
+
.then((r) => setEditingMd({ agent: agentName, content: r.content }))
|
|
741
|
+
.catch((e) => setError(String(e && e.message)))
|
|
742
|
+
}
|
|
743
|
+
const saveMd = async () => {
|
|
744
|
+
setSaving(true); setError('')
|
|
745
|
+
try {
|
|
746
|
+
await fetchJson(`${API}/agent-md`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, agent: editingMd.agent, content: editingMd.content }) })
|
|
747
|
+
setEditingMd(null); flash(t('saved')); loadDetail()
|
|
748
|
+
} catch (e) { setError(String(e && e.message)) } finally { setSaving(false) }
|
|
749
|
+
}
|
|
750
|
+
const startMetaEdit = () => {
|
|
751
|
+
const p = (detail && detail.pluginJson) || {}
|
|
752
|
+
const loc = (v) => ({ zh: (v && v.zh) || '', en: (v && v.en) || '' })
|
|
753
|
+
const tags = Array.isArray(p.tags) ? p.tags : []
|
|
754
|
+
setMetaForm({
|
|
755
|
+
displayName: loc(p.displayName), profession: loc(p.profession),
|
|
756
|
+
displayDescription: loc(p.displayDescription), defaultInitPrompt: loc(p.defaultInitPrompt),
|
|
757
|
+
tagsZh: tags.map((x) => x.zh || '').filter(Boolean).join(','),
|
|
758
|
+
tagsEn: tags.map((x) => x.en || '').filter(Boolean).join(','),
|
|
759
|
+
quickPrompts: Array.isArray(p.quickPrompts) ? p.quickPrompts : [],
|
|
760
|
+
})
|
|
761
|
+
setMetaEdit(true)
|
|
762
|
+
}
|
|
763
|
+
const saveMeta = async () => {
|
|
764
|
+
setSaving(true); setError('')
|
|
765
|
+
const splitList = (v) => String(v || '').split(/[,,]/).map((x) => x.trim()).filter(Boolean)
|
|
766
|
+
const body = { metadata: {
|
|
767
|
+
displayName: metaForm.displayName, profession: metaForm.profession,
|
|
768
|
+
displayDescription: metaForm.displayDescription, defaultInitPrompt: metaForm.defaultInitPrompt,
|
|
769
|
+
tags: (function () {
|
|
770
|
+
const zhList = splitList(metaForm.tagsZh); const enList = splitList(metaForm.tagsEn)
|
|
771
|
+
const len = Math.max(zhList.length, enList.length)
|
|
772
|
+
return Array.from({ length: len }, (_, i) => ({ zh: zhList[i] || '', en: enList[i] || '' })).filter((t) => t.zh !== '' || t.en !== '')
|
|
773
|
+
})(),
|
|
774
|
+
quickPrompts: metaForm.quickPrompts,
|
|
775
|
+
} }
|
|
776
|
+
try {
|
|
777
|
+
await fetchJson(`${API}/metadata`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, ...body }) })
|
|
778
|
+
setMetaEdit(false); flash(t('saved')); loadDetail()
|
|
779
|
+
} catch (e) { setError(String(e && e.message)) } finally { setSaving(false) }
|
|
780
|
+
}
|
|
781
|
+
const detachSkill = (skillName) => {
|
|
782
|
+
if (!window.confirm(t('detachConfirm'))) return
|
|
783
|
+
fetchJson(`${API}/expert-skills`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, detach: [skillName] }) })
|
|
784
|
+
.then(() => { flash(t('saved')); loadDetail() })
|
|
785
|
+
.catch((e) => setError(String(e && e.message)))
|
|
786
|
+
}
|
|
787
|
+
const openSkillsEdit = () => {
|
|
788
|
+
setSkillsEdit(true)
|
|
789
|
+
if (availSkills === null) {
|
|
790
|
+
fetchJson(`${API}/available-skills`).then((d) => setAvailSkills(d.skills || [])).catch((e) => setError(String(e && e.message)))
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const attachSkill = (skillName) => {
|
|
794
|
+
fetchJson(`${API}/expert-skills`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, attach: [skillName] }) })
|
|
795
|
+
.then(() => { flash(t('saved')); loadDetail() })
|
|
796
|
+
.catch((e) => setError(String(e && e.message)))
|
|
797
|
+
}
|
|
798
|
+
const onAvatarFile = async (e) => {
|
|
799
|
+
const f = e.target.files && e.target.files[0]
|
|
800
|
+
e.target.value = ''
|
|
801
|
+
if (!f) return
|
|
802
|
+
setAvatarBusy(true); setError('')
|
|
803
|
+
try {
|
|
804
|
+
const buf = new Uint8Array(await f.arrayBuffer())
|
|
805
|
+
await fetchJson(`${API}/avatar?name=${encodeURIComponent(name)}`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: buf })
|
|
806
|
+
flash(t('saved')); loadDetail()
|
|
807
|
+
} catch (ex) { setError(String(ex && ex.message)) } finally { setAvatarBusy(false) }
|
|
808
|
+
}
|
|
631
809
|
const loadAgentMd = () => {
|
|
632
810
|
fetchJson(`${API}/agent-md?name=${encodeURIComponent(name)}${source ? `&source=${encodeURIComponent(source)}` : ''}`)
|
|
633
811
|
.then((r) => setAgentMd(r.content))
|
|
@@ -669,6 +847,24 @@ function DetailModal({ name, source, t, onClose, onInstalled, onDeleted }) {
|
|
|
669
847
|
kv(t('filesLabel'), `${detail.fileCount} / ${formatSize(detail.totalSize)}`),
|
|
670
848
|
kv(t('dirLabel'), detail.dir)),
|
|
671
849
|
detail.readOnly ? h('div', { className: 'exp-kv' }, h('span', { style: { color: 'var(--dsw-alias-label-tertiary)' } }, t('sourceReadonly'))) : null,
|
|
850
|
+
detail.source === 'dsh' ? h('div', { className: 'exp-form-row' },
|
|
851
|
+
h('button', { className: 'exp-btn', disabled: busy || saving || avatarBusy, onClick: startMetaEdit }, t('editMeta')),
|
|
852
|
+
h('button', { className: 'exp-btn', disabled: busy || saving || avatarBusy, onClick: () => { if (avatarInputRef.current) avatarInputRef.current.click() } }, avatarBusy ? t('uploading') : t('uploadAvatar')),
|
|
853
|
+
savedFlash ? h('span', { className: 'exp-flash' }, savedFlash) : null,
|
|
854
|
+
h('input', { ref: avatarInputRef, type: 'file', accept: 'image/png,image/jpeg,image/gif,image/webp', style: { display: 'none' }, onChange: onAvatarFile })) : null,
|
|
855
|
+
metaEdit && metaForm ? h('div', { className: 'exp-section' },
|
|
856
|
+
h('div', { className: 'exp-section-title' }, t('editMeta')),
|
|
857
|
+
...['displayName', 'profession', 'displayDescription', 'defaultInitPrompt'].map((key) => h('div', { key, style: { marginBottom: '8px' } },
|
|
858
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, t(key === 'profession' ? 'professionLabel' : key === 'tags' ? 'tagsLabel' : key)),
|
|
859
|
+
h('input', { className: 'exp-input', value: metaForm[key].zh, placeholder: 'zh', onChange: (e) => setMetaForm({ ...metaForm, [key]: { ...metaForm[key], zh: e.target.value } }), style: { marginBottom: '4px', width: '100%' } }),
|
|
860
|
+
h('input', { className: 'exp-input', value: metaForm[key].en, placeholder: 'en', onChange: (e) => setMetaForm({ ...metaForm, [key]: { ...metaForm[key], en: e.target.value } }), style: { width: '100%' } }))),
|
|
861
|
+
h('div', { style: { marginBottom: '8px' } },
|
|
862
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, t('tagsLabel')),
|
|
863
|
+
h('input', { className: 'exp-input', value: metaForm.tagsZh, placeholder: 'zh,逗号分隔', onChange: (e) => setMetaForm({ ...metaForm, tagsZh: e.target.value }), style: { marginBottom: '4px', width: '100%' } }),
|
|
864
|
+
h('input', { className: 'exp-input', value: metaForm.tagsEn, placeholder: 'en, comma separated', onChange: (e) => setMetaForm({ ...metaForm, tagsEn: e.target.value }), style: { width: '100%' } })),
|
|
865
|
+
h('div', { className: 'exp-form-row' },
|
|
866
|
+
h('button', { className: 'exp-btn', 'data-primary': 'true', disabled: saving, onClick: saveMeta }, saving ? '…' : t('save')),
|
|
867
|
+
h('button', { className: 'exp-btn', disabled: saving, onClick: () => setMetaEdit(false) }, t('cancel')))) : null,
|
|
672
868
|
(detail.quickPromptsZh || []).length > 0 ? h('div', { className: 'exp-section' },
|
|
673
869
|
h('div', { className: 'exp-section-title' }, t('quickPrompts')),
|
|
674
870
|
...detail.quickPromptsZh.slice(0, 5).map((q, i) => h('div', { className: 'exp-kv', key: i }, '• ', q))) : null,
|
|
@@ -684,13 +880,33 @@ function DetailModal({ name, source, t, onClose, onInstalled, onDeleted }) {
|
|
|
684
880
|
h('div', { className: 'exp-section-title' }, t('skills')),
|
|
685
881
|
...detail.skillMeta.map((s) => h('div', { className: 'exp-skill-row', key: s.skillName },
|
|
686
882
|
h('div', { style: { fontSize: 13 } }, `${s.emoji ? s.emoji + ' ' : ''}${s.skillName}`),
|
|
687
|
-
h('div', { className: 'exp-profession' }, s.descriptionZh || s.descriptionEn || s.description || '')
|
|
883
|
+
h('div', { className: 'exp-profession' }, s.descriptionZh || s.descriptionEn || s.description || ''),
|
|
884
|
+
detail.source === 'dsh' ? h('button', { className: 'exp-btn', style: { marginLeft: 'auto' }, onClick: () => detachSkill(s.skillName) }, t('detach')) : null)),
|
|
885
|
+
detail.source === 'dsh' ? h('div', { className: 'exp-form-row' },
|
|
886
|
+
skillsEdit ? null : h('button', { className: 'exp-btn', onClick: openSkillsEdit }, t('attachSkill'))) : null,
|
|
887
|
+
detail.source === 'dsh' && skillsEdit ? h('div', { className: 'exp-form-row' },
|
|
888
|
+
availSkills === null ? h('span', { className: 'exp-profession' }, '…')
|
|
889
|
+
: availSkills.length === 0 ? h('span', { className: 'exp-profession' }, '—')
|
|
890
|
+
: availSkills.map((sk) => h('button', { key: sk.name, className: 'exp-btn', title: sk.description, onClick: () => attachSkill(sk.name) }, `+ ${sk.name}`))) : null) : null,
|
|
688
891
|
(detail.agentFiles || []).length > 0 ? h('div', { className: 'exp-section' },
|
|
689
892
|
h('div', { className: 'exp-section-title' }, t('agents')),
|
|
690
893
|
...detail.agentFiles.map((a) => h('div', { className: 'exp-skill-row', key: a.relPath },
|
|
691
894
|
h('div', { style: { fontSize: 13 } }, `${a.emoji ? a.emoji + ' ' : ''}${a.name}${a.name === detail.leadAgentFile ? ' ★' : ''}`),
|
|
692
|
-
h('div', { className: 'exp-profession' }, a.description || '')
|
|
895
|
+
h('div', { className: 'exp-profession' }, a.description || ''),
|
|
896
|
+
detail.source === 'dsh' ? h('button', { className: 'exp-btn', style: { marginLeft: 'auto' }, onClick: () => startEditMd(a.name) }, t('edit')) : null))) : null,
|
|
897
|
+
editingMd !== null ? h('div', { className: 'exp-section' },
|
|
898
|
+
h('div', { className: 'exp-section-title' }, `${t('edit')} · ${editingMd.agent}`),
|
|
899
|
+
h('textarea', {
|
|
900
|
+
className: 'exp-input', value: editingMd.content,
|
|
901
|
+
onChange: (e) => setEditingMd({ ...editingMd, content: e.target.value }),
|
|
902
|
+
spellCheck: false,
|
|
903
|
+
style: { width: '100%', minHeight: '260px', fontFamily: 'ui-monospace,monospace', fontSize: '12px', lineHeight: 1.6, whiteSpace: 'pre-wrap', background: 'var(--dsw-alias-bg-layer-2,transparent)', color: 'inherit', border: '1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3))', borderRadius: '8px', padding: '10px', boxSizing: 'border-box' },
|
|
904
|
+
}),
|
|
905
|
+
h('div', { className: 'exp-form-row' },
|
|
906
|
+
h('button', { className: 'exp-btn', 'data-primary': 'true', disabled: saving, onClick: saveMd }, saving ? '…' : t('save')),
|
|
907
|
+
h('button', { className: 'exp-btn', disabled: saving, onClick: () => setEditingMd(null) }, t('cancel')))) : null,
|
|
693
908
|
h('div', { className: 'exp-form-row' },
|
|
909
|
+
h('button', { className: 'exp-btn', onClick: () => setShareOpen(true) }, t('shareBtn')),
|
|
694
910
|
h('button', { className: 'exp-btn', onClick: () => (agentMd === null ? loadAgentMd() : setAgentMd(null)) }, agentMd === null ? t('viewAgentMd') : t('hideAgentMd')),
|
|
695
911
|
detail.source !== 'dsh'
|
|
696
912
|
? h('button', { className: 'exp-btn', 'data-primary': 'true', disabled: busy, onClick: () => install(false) }, busy ? t('installing') : t('install'))
|
|
@@ -702,7 +918,17 @@ function DetailModal({ name, source, t, onClose, onInstalled, onDeleted }) {
|
|
|
702
918
|
detail.plugin ? h('details', null,
|
|
703
919
|
h('summary', { style: { cursor: 'pointer', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } }, t('pluginJson')),
|
|
704
920
|
h('pre', { className: 'exp-pre' }, JSON.stringify(detail.plugin, null, 2))) : null,
|
|
705
|
-
) : null
|
|
921
|
+
) : null,
|
|
922
|
+
shareOpen ? h(getShareDialogComponent(), {
|
|
923
|
+
title: t('shareTitle'), hint: t('shareHint'),
|
|
924
|
+
rows: [[t('shareParamName'), detail.name], [t('shareParamVersion'), detail.version || '1.0.0'], [t('shareParamDir'), detail.dir]],
|
|
925
|
+
initialPrompt: PluginKit.substituteParams(EXPERT_SHARE_PROMPT, { expertName: detail.name, version: detail.version || '1.0.0', resourceDir: detail.dir, settingsFile: shareSettingsFile }),
|
|
926
|
+
labels: { copy: t('copyPrompt'), copied: t('copied'), run: t('runBtn'), running: t('running'), done: t('runDone'), failed: t('runFailed'), outputLabel: t('outputLabel'), openSession: t('openChat') },
|
|
927
|
+
run: (prompt) => fetchJson(`${API}/share/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, dir: detail.dir }) }),
|
|
928
|
+
poll: (id) => fetchJson(`${API}/share/run?id=${encodeURIComponent(id)}`),
|
|
929
|
+
onOpenSession: (sessionId) => { if (onOpenSession) onOpenSession(sessionId) },
|
|
930
|
+
onClose: () => setShareOpen(false),
|
|
931
|
+
}) : null))
|
|
706
932
|
}
|
|
707
933
|
|
|
708
934
|
function BuiltinSettingsDialog({ t, onClose, onToast, onSynced }) {
|
|
@@ -854,6 +1080,7 @@ function ExpertsPage({ t, embedded, onClose }) {
|
|
|
854
1080
|
: null,
|
|
855
1081
|
selected !== null ? h(DetailModal, {
|
|
856
1082
|
name: selected.name, source: selected.source, t, onClose: () => setSelected(null),
|
|
1083
|
+
onOpenSession: (sessionId) => { try { sessionsApi.open(sessionId) } catch (e) { showToast(String(e && e.message)) } },
|
|
857
1084
|
onInstalled: () => { setSelected(null); showToast(t('installedDone')); reload() },
|
|
858
1085
|
onDeleted: () => { setSelected(null); showToast(t('removedDone')); reload() },
|
|
859
1086
|
}) : null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weibaohui/experts-management",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "dsh 插件 · 专家管理:管理 ntd 格式的专家与专家团队(plugin.json + Agent MD + 技能集),浏览/安装 50+ 内置专家市场;每个专家注册为「仅用户可调用」的技能,在对话输入框用 /expert-名称 即可以该专家的身份执行任务(宿主确定性注入角色定义与技能清单,不占用模型目录 token)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"node": ">=22.5"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"@weibaohui/dsh-plugin-kit": "^0.2.0",
|
|
47
48
|
"yaml": "^2.9.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
package/src/index.js
CHANGED
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
const { createReadStream } = require('node:fs')
|
|
22
|
-
const { execFile } = require('node:child_process')
|
|
23
22
|
const { randomUUID } = require('node:crypto')
|
|
23
|
+
const { execFile } = require('node:child_process')
|
|
24
|
+
const { createShareRunJob } = require('@weibaohui/dsh-plugin-kit')
|
|
24
25
|
const fsP = require('node:fs/promises')
|
|
25
26
|
const { basename, join, relative, resolve, sep } = require('node:path')
|
|
26
27
|
const { homedir } = require('node:os')
|
|
@@ -516,11 +517,47 @@ function baseSettings(config) {
|
|
|
516
517
|
return base
|
|
517
518
|
}
|
|
518
519
|
|
|
520
|
+
// ── 编辑端点(v0.3):只写 dsh 用户库;内置只读 ─────────────────────────
|
|
521
|
+
const EDIT_BODY_MAX_BYTES = 8 * 1024 * 1024
|
|
522
|
+
|
|
523
|
+
/** 魔数嗅探图片类型;非白名单格式返回 null。 */
|
|
524
|
+
function sniffImage(buf) {
|
|
525
|
+
if (!buf || buf.length < 12) return null
|
|
526
|
+
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return { ext: 'png', type: 'image/png' }
|
|
527
|
+
if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return { ext: 'jpg', type: 'image/jpeg' }
|
|
528
|
+
const head = buf.slice(0, 12)
|
|
529
|
+
if (head.toString('latin1').startsWith('GIF8')) return { ext: 'gif', type: 'image/gif' }
|
|
530
|
+
if (head.toString('latin1').startsWith('RIFF') && head.toString('latin1').slice(8) === 'WEBP') return { ext: 'webp', type: 'image/webp' }
|
|
531
|
+
return null
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const readRawBody = (req, cap) => new Promise((fulfil, reject) => {
|
|
535
|
+
let size = 0
|
|
536
|
+
const chunks = []
|
|
537
|
+
req.on('data', (chunk) => {
|
|
538
|
+
size += chunk.length
|
|
539
|
+
if (size > cap) { reject(new Error(`image exceeds ${cap} bytes`)); if (typeof req.destroy === 'function') req.destroy(); return }
|
|
540
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
541
|
+
})
|
|
542
|
+
req.on('end', () => fulfil(Buffer.concat(chunks)))
|
|
543
|
+
req.on('error', reject)
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
/** 编辑端点公共前置:只解析 dsh 用户库副本(内置只读);名字围栏。 */
|
|
547
|
+
async function locateEditable(locateExpert, name) {
|
|
548
|
+
if (typeof name !== 'string' || name === '') throw new Error('body must provide name')
|
|
549
|
+
const { expert } = await locateExpert(name, 'dsh')
|
|
550
|
+
if (!isSafeExpertName(expert.name)) throw new Error(`invalid expert name: ${expert.name}`)
|
|
551
|
+
return expert
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const MD_MAX_CHARS = 512 * 1024
|
|
555
|
+
|
|
519
556
|
// ── Module export ────────────────────────────────────────────────────────
|
|
520
557
|
|
|
521
558
|
module.exports = {
|
|
522
559
|
name: 'experts-management',
|
|
523
|
-
inject: ['skills', 'webServer', 'settings'],
|
|
560
|
+
inject: ['skills', 'webServer', 'settings', 'agents', 'agentDefaultModel', 'sessions'],
|
|
524
561
|
__internals: {
|
|
525
562
|
extractFrontmatter, parseFrontmatter, parseAgentMd, parseSkillMd, parsePluginJson,
|
|
526
563
|
localized, truncateDescription, resolveWithin, isSafeExpertName,
|
|
@@ -774,6 +811,10 @@ module.exports = {
|
|
|
774
811
|
})
|
|
775
812
|
|
|
776
813
|
// ── HTTP API ─────────────────────────────────────────────────────────
|
|
814
|
+
const shareRunJobs = new Map()
|
|
815
|
+
// 分享执行:进程内 agents 服务(静态注入,apply 时已就绪)。动态 ctx.inject
|
|
816
|
+
// 在 apply 内不触发是平台 gotcha(skills-management 同款教训)——改静态捕获
|
|
817
|
+
const shareServices = { agents: ctx.agents, agentDefaultModel: ctx.agentDefaultModel, sessions: ctx.sessions }
|
|
777
818
|
ctx.effect(() => ctx.webServer.register({
|
|
778
819
|
kind: 'prefix',
|
|
779
820
|
path: '/experts-management/api',
|
|
@@ -813,9 +854,11 @@ module.exports = {
|
|
|
813
854
|
const name = query.get('name') || ''
|
|
814
855
|
const { expert, row } = await locateExpert(name, query.get('source') || undefined)
|
|
815
856
|
const { fileCount, totalSize } = await countFilesAndSize(expert.dir)
|
|
857
|
+
const rawPluginText = await fsP.readFile(expert.pluginJsonPath, 'utf8')
|
|
816
858
|
sendJson(res, 200, {
|
|
817
859
|
...expert,
|
|
818
|
-
plugin: parsePluginJson(
|
|
860
|
+
plugin: parsePluginJson(rawPluginText),
|
|
861
|
+
pluginJson: JSON.parse(rawPluginText),
|
|
819
862
|
leadAgentFile: resolveLeadAgentFile(expert)?.name,
|
|
820
863
|
dir: displayPath(expert.dir),
|
|
821
864
|
sourceLabel: row.label,
|
|
@@ -870,7 +913,12 @@ module.exports = {
|
|
|
870
913
|
if (req.method === 'POST' && apiPath.endsWith('/experts-management/api/install')) {
|
|
871
914
|
const body = await readJsonBody(req)
|
|
872
915
|
if (typeof body.name !== 'string' || body.name === '') { sendJson(res, 400, { error: 'body must provide name' }); return }
|
|
873
|
-
|
|
916
|
+
// from 兼容 client 的 source 字段;'auto'/缺省一律钉死为 builtin——
|
|
917
|
+
// 若解析到 dsh 源,overwrite 会先 rm 自己再空拷(v0.2.0 数据丢失事故),此路彻底封死
|
|
918
|
+
const from = typeof body.from === 'string' && body.from !== '' ? body.from
|
|
919
|
+
: typeof body.source === 'string' && body.source !== '' ? body.source : 'builtin'
|
|
920
|
+
if (from === 'dsh') throw new Error('cannot install from the dsh library (it is the install destination)')
|
|
921
|
+
const { expert } = await locateExpert(body.name, from)
|
|
874
922
|
if (!isSafeExpertName(expert.name)) throw new Error(`invalid expert name: ${expert.name}`)
|
|
875
923
|
const target = join(installedDir, expert.name)
|
|
876
924
|
if (body.overwrite !== true) {
|
|
@@ -899,6 +947,180 @@ module.exports = {
|
|
|
899
947
|
return
|
|
900
948
|
}
|
|
901
949
|
|
|
950
|
+
// ── 编辑端点(v0.3):仅 dsh 用户库可编辑,内置只读 ──
|
|
951
|
+
|
|
952
|
+
// PUT /experts-management/api/agent-md {name, agent?, content} — 角色定义全文
|
|
953
|
+
if (req.method === 'PUT' && apiPath.endsWith('/experts-management/api/agent-md')) {
|
|
954
|
+
const body = await readJsonBody(req)
|
|
955
|
+
const expert = await locateEditable(locateExpert, body.name)
|
|
956
|
+
if (typeof body.content !== 'string' || body.content.trim() === '') throw new Error('content must be a non-empty string')
|
|
957
|
+
if (body.content.length > MD_MAX_CHARS) throw new Error(`content exceeds ${MD_MAX_CHARS} chars`)
|
|
958
|
+
const normRel = (p0) => String(p0 || '').replace(/^\.\//, '')
|
|
959
|
+
const agentFile = body.agent !== undefined && body.agent !== ''
|
|
960
|
+
? expert.agentFiles.find((a) => a.name === body.agent || normRel(a.relPath) === normRel(body.agent) || basename(a.mdPath) === body.agent)
|
|
961
|
+
: resolveLeadAgentFile(expert)
|
|
962
|
+
if (agentFile === undefined) throw new Error(`agent not found in expert '${expert.name}'`)
|
|
963
|
+
const full = resolveWithin(expert.dir, agentFile.relPath)
|
|
964
|
+
if (full === undefined || resolve(full) !== resolve(agentFile.mdPath)) throw new Error('agent file path escaped the expert dir')
|
|
965
|
+
await atomicWriteJs(full, body.content)
|
|
966
|
+
invalidate()
|
|
967
|
+
sendJson(res, 200, { ok: true, agent: agentFile.name })
|
|
968
|
+
return
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// PUT /experts-management/api/metadata {name, metadata} — plugin.json 展示字段(读-改-写保留未知键)
|
|
972
|
+
if (req.method === 'PUT' && apiPath.endsWith('/experts-management/api/metadata')) {
|
|
973
|
+
const body = await readJsonBody(req)
|
|
974
|
+
const meta = body.metadata
|
|
975
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) throw new Error('metadata must be an object')
|
|
976
|
+
const expert = await locateEditable(locateExpert, body.name)
|
|
977
|
+
const pluginJson = JSON.parse(await fsP.readFile(expert.pluginJsonPath, 'utf8'))
|
|
978
|
+
const normLocalized = (v) => ({ zh: typeof v.zh === 'string' ? v.zh : '', en: typeof v.en === 'string' ? v.en : '' })
|
|
979
|
+
for (const key of ['displayName', 'profession', 'displayDescription', 'defaultInitPrompt']) {
|
|
980
|
+
if (meta[key] === undefined) continue
|
|
981
|
+
const v = meta[key]
|
|
982
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) throw new Error(`${key} must be an object`)
|
|
983
|
+
for (const lang of ['zh', 'en']) {
|
|
984
|
+
if (v[lang] !== undefined && (typeof v[lang] !== 'string' || v[lang].length > 2000)) throw new Error(`${key}.${lang} must be a string (≤2000 chars)`)
|
|
985
|
+
}
|
|
986
|
+
pluginJson[key] = normLocalized(v)
|
|
987
|
+
}
|
|
988
|
+
const listOfLocalized = (v, label) => {
|
|
989
|
+
if (!Array.isArray(v) || v.length > 20) throw new Error(`${label} must be an array (≤20)`)
|
|
990
|
+
return v.map((item) => {
|
|
991
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) throw new Error(`${label} items must be objects`)
|
|
992
|
+
return { zh: typeof item.zh === 'string' ? item.zh.slice(0, 2000) : '', en: typeof item.en === 'string' ? item.en.slice(0, 2000) : '' }
|
|
993
|
+
}).filter((item) => item.zh !== '' || item.en !== '')
|
|
994
|
+
}
|
|
995
|
+
if (meta.tags !== undefined) pluginJson.tags = listOfLocalized(meta.tags, 'tags')
|
|
996
|
+
if (meta.quickPrompts !== undefined) pluginJson.quickPrompts = listOfLocalized(meta.quickPrompts, 'quickPrompts')
|
|
997
|
+
await atomicWriteJs(expert.pluginJsonPath, JSON.stringify(pluginJson, null, 2))
|
|
998
|
+
invalidate()
|
|
999
|
+
sendJson(res, 200, { ok: true, plugin: pluginJson })
|
|
1000
|
+
return
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// PUT /experts-management/api/expert-skills {name, attach?, detach?} — 技能副本同步
|
|
1004
|
+
if (req.method === 'PUT' && apiPath.endsWith('/experts-management/api/expert-skills')) {
|
|
1005
|
+
const body = await readJsonBody(req)
|
|
1006
|
+
const attach = Array.isArray(body.attach) ? body.attach.map(String) : []
|
|
1007
|
+
const detach = Array.isArray(body.detach) ? body.detach.map(String) : []
|
|
1008
|
+
if (attach.length === 0 && detach.length === 0) throw new Error('attach and detach must not both be empty')
|
|
1009
|
+
const kebab = (n) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(n)
|
|
1010
|
+
for (const n of [...attach, ...detach]) {
|
|
1011
|
+
if (!kebab(n)) throw new Error(`invalid skill name: ${n}`)
|
|
1012
|
+
}
|
|
1013
|
+
const expert = await locateEditable(locateExpert, body.name)
|
|
1014
|
+
const pluginJson = JSON.parse(await fsP.readFile(expert.pluginJsonPath, 'utf8'))
|
|
1015
|
+
// detach 先验后删(任一未附 → 整体拒绝,避免半套变更)
|
|
1016
|
+
const detachDirs = []
|
|
1017
|
+
for (const n of detach) {
|
|
1018
|
+
const dir = resolveWithin(expert.dir, `./skills/${n}`)
|
|
1019
|
+
if (dir === undefined) throw new Error(`skill '${n}' is not attached to expert '${expert.name}'`)
|
|
1020
|
+
const st = await fsP.stat(dir).catch(() => undefined)
|
|
1021
|
+
if (st === undefined || !st.isDirectory()) throw new Error(`skill '${n}' is not attached to expert '${expert.name}'`)
|
|
1022
|
+
detachDirs.push({ n, dir })
|
|
1023
|
+
}
|
|
1024
|
+
// attach 全部先在用户技能库解析源目录(任一缺失整体拒绝)
|
|
1025
|
+
const libRoot = join(dshHome(), 'skills')
|
|
1026
|
+
const attachDirs = []
|
|
1027
|
+
for (const n of attach) {
|
|
1028
|
+
const from = join(libRoot, n)
|
|
1029
|
+
const st = await fsP.stat(join(from, 'SKILL.md')).catch(() => undefined)
|
|
1030
|
+
if (st === undefined || !st.isFile()) throw new Error(`skill '${n}' not found in the user skill library (${libRoot})`)
|
|
1031
|
+
attachDirs.push({ n, from })
|
|
1032
|
+
}
|
|
1033
|
+
for (const d of detachDirs) await fsP.rm(d.dir, { recursive: true, force: true })
|
|
1034
|
+
for (const a of attachDirs) {
|
|
1035
|
+
const target = join(expert.dir, 'skills', a.n)
|
|
1036
|
+
await fsP.rm(target, { recursive: true, force: true }) // 同名覆盖 = 技能库更新同步进专家
|
|
1037
|
+
await copyDir(a.from, target)
|
|
1038
|
+
}
|
|
1039
|
+
// plugin.json.skills = 声明同步:原序保留存活项 + 追加新 attach(以 skills/ 目录实况为准)
|
|
1040
|
+
const skillRoot = join(expert.dir, 'skills')
|
|
1041
|
+
const present = new Set()
|
|
1042
|
+
try {
|
|
1043
|
+
for (const ent of await fsP.readdir(skillRoot, { withFileTypes: true })) if (ent.isDirectory()) present.add(ent.name)
|
|
1044
|
+
} catch { /* 无 skills 目录 */ }
|
|
1045
|
+
const oldNames = (Array.isArray(pluginJson.skills) ? pluginJson.skills : []).map((r) => String(r).replace(/^\.\/skills\//, '').replace(/^\.\//, ''))
|
|
1046
|
+
const finalNames = []
|
|
1047
|
+
for (const n of [...oldNames, ...attach]) {
|
|
1048
|
+
if (present.has(n) && !finalNames.includes(n)) finalNames.push(n)
|
|
1049
|
+
}
|
|
1050
|
+
pluginJson.skills = finalNames.map((n) => `./skills/${n}`)
|
|
1051
|
+
await atomicWriteJs(expert.pluginJsonPath, JSON.stringify(pluginJson, null, 2))
|
|
1052
|
+
invalidate()
|
|
1053
|
+
sendJson(res, 200, { ok: true, skills: pluginJson.skills })
|
|
1054
|
+
return
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// POST /experts-management/api/avatar?name= — 原始图片体(魔数嗅探)
|
|
1058
|
+
if (req.method === 'POST' && apiPath.endsWith('/experts-management/api/avatar')) {
|
|
1059
|
+
const expert = await locateEditable(locateExpert, query.get('name') || '')
|
|
1060
|
+
const imgBody = await readRawBody(req, EDIT_BODY_MAX_BYTES)
|
|
1061
|
+
const img = sniffImage(imgBody)
|
|
1062
|
+
if (img === null) throw new Error('unsupported image (png/jpg/gif/webp only)')
|
|
1063
|
+
const rel = `avatars/expert.${img.ext}`
|
|
1064
|
+
await atomicWriteJs(join(expert.dir, rel), imgBody)
|
|
1065
|
+
const pluginJson = JSON.parse(await fsP.readFile(expert.pluginJsonPath, 'utf8'))
|
|
1066
|
+
pluginJson.avatar = rel
|
|
1067
|
+
await atomicWriteJs(expert.pluginJsonPath, JSON.stringify(pluginJson, null, 2))
|
|
1068
|
+
invalidate()
|
|
1069
|
+
sendJson(res, 200, { ok: true, avatar: rel })
|
|
1070
|
+
return
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// GET /experts-management/api/available-skills — 技能关联选择器数据源:
|
|
1074
|
+
// 用户技能库(~/.dsh/skills)目录直读。刻意不走 skills 注册表——那会把
|
|
1075
|
+
// 市场货架库存(5900+ 条)漏进来;也不附 bundled/项目级技能。
|
|
1076
|
+
if (req.method === 'GET' && apiPath.endsWith('/experts-management/api/available-skills')) {
|
|
1077
|
+
const libRoot = join(dshHome(), 'skills')
|
|
1078
|
+
const list = []
|
|
1079
|
+
let libEntries = []
|
|
1080
|
+
try { libEntries = await fsP.readdir(libRoot, { withFileTypes: true }) } catch { /* 无技能库 */ }
|
|
1081
|
+
for (const ent of libEntries) {
|
|
1082
|
+
if (!ent.isDirectory() || !isSafeExpertName(ent.name)) continue
|
|
1083
|
+
let content
|
|
1084
|
+
try { content = await fsP.readFile(join(libRoot, ent.name, 'SKILL.md'), 'utf8') } catch { continue }
|
|
1085
|
+
const parsed = parseSkillMd(content)
|
|
1086
|
+
const description = String(parsed.descriptionZh ?? parsed.descriptionEn ?? parsed.description ?? '').slice(0, 200)
|
|
1087
|
+
list.push({ name: ent.name, description })
|
|
1088
|
+
}
|
|
1089
|
+
list.sort((a, b) => a.name.localeCompare(b.name))
|
|
1090
|
+
sendJson(res, 200, { skills: list })
|
|
1091
|
+
return
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// GET /experts-management/api/share/status — 分享弹窗数据(settings 真实路径)
|
|
1095
|
+
if (req.method === 'GET' && apiPath.endsWith('/experts-management/api/share/status')) {
|
|
1096
|
+
sendJson(res, 200, { settingsFile: join(dshHome(), 'settings.yaml') })
|
|
1097
|
+
return
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// POST /experts-management/api/share/run {prompt, dir} → 真实 agent 会话执行
|
|
1101
|
+
if (req.method === 'POST' && apiPath.endsWith('/experts-management/api/share/run')) {
|
|
1102
|
+
const body = await readJsonBody(req)
|
|
1103
|
+
if (typeof body.prompt !== 'string' || body.prompt.trim() === '') { sendJson(res, 400, { error: 'body must provide prompt' }); return }
|
|
1104
|
+
if (typeof body.dir !== 'string' || body.dir === '') { sendJson(res, 400, { error: 'body must provide dir' }); return }
|
|
1105
|
+
// 支持 ~ 前缀(client 传的是 displayPath 折叠过的路径)
|
|
1106
|
+
const dir = resolve(String(body.dir).startsWith('~') ? join(homedir(), String(body.dir).slice(2)) : body.dir)
|
|
1107
|
+
const stat = await fsP.stat(dir).catch(() => undefined)
|
|
1108
|
+
if (stat === undefined || !stat.isDirectory()) { sendJson(res, 400, { error: `dir not found: ${displayPath(dir)}` }); return }
|
|
1109
|
+
const binary = process.env.EXPERTS_DSH_BIN || 'dsh'
|
|
1110
|
+
const job = createShareRunJob({ binary, prompt: body.prompt, dir, jobs: shareRunJobs, logger: ctx.logger, services: shareServices })
|
|
1111
|
+
sendJson(res, 202, { jobId: job.id, status: job.status })
|
|
1112
|
+
return
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// GET /experts-management/api/share/run?id= → 任务状态/输出
|
|
1116
|
+
if (req.method === 'GET' && apiPath.endsWith('/experts-management/api/share/run')) {
|
|
1117
|
+
const id = query.get('id') || ''
|
|
1118
|
+
const job = shareRunJobs.get(id)
|
|
1119
|
+
if (job === undefined) { sendJson(res, 404, { error: 'job not found' }); return }
|
|
1120
|
+
sendJson(res, 200, { ...job, output: job.output.slice(-32 * 1024) })
|
|
1121
|
+
return
|
|
1122
|
+
}
|
|
1123
|
+
|
|
902
1124
|
// GET /experts-management/api/builtin/status
|
|
903
1125
|
if (req.method === 'GET' && apiPath.endsWith('/experts-management/api/builtin/status')) {
|
|
904
1126
|
await builtinStateLoaded
|