@weibaohui/experts-management 0.4.2 → 0.5.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/client/bundle.js +304 -14
- package/client/index.js +249 -4
- package/package.json +2 -2
- package/src/index.js +102 -2
package/client/bundle.js
CHANGED
|
@@ -17,6 +17,11 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
*
|
|
18
18
|
* ActionShareDialog props:
|
|
19
19
|
* title / hint / rows: [[label, value], ...] / initialPrompt
|
|
20
|
+
* params: [{ key, label?, placeholder?, multiline?, value? }] — 可选;模板参数
|
|
21
|
+
* 输入区(idle 态渲染在 prompt 上方),值实时替换进 prompt 的 {{key}} 占位符
|
|
22
|
+
* completedView: ({ job, output, close, retry }) => node — 可选;完成态插槽,
|
|
23
|
+
* 提供后 job done 不再渲染默认「输出原文」,改由插槽全权负责(如解析 AI 输出
|
|
24
|
+
* 成可编辑表单 + 创建按钮),Dialog footer 同时置空,操作按钮由插槽自承
|
|
20
25
|
* run: async (prompt) => { jobId } — 发起执行
|
|
21
26
|
* poll: async (jobId) => { status, output, code }
|
|
22
27
|
* labels: { copy, copied, run, running, done, failed, outputLabel, openSession, close }
|
|
@@ -37,8 +42,10 @@ window.__ModuleLoader__.load({
|
|
|
37
42
|
var h = React.createElement
|
|
38
43
|
var useState = React.useState
|
|
39
44
|
var useEffect = React.useEffect
|
|
45
|
+
var useRef = React.useRef
|
|
40
46
|
var doFetch = options.fetch || (typeof fetch !== 'undefined' ? fetch : null)
|
|
41
47
|
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' }
|
|
48
|
+
var paramStyle = { width: '100%', fontFamily: 'var(--dsw-font-family)', lineHeight: 1.5, fontSize: 13, 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: '6px 10px', boxSizing: 'border-box' }
|
|
42
49
|
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
50
|
// 主按钮亮暗跟随:与 skills-management .sk-btn-primary 同款 token 组合
|
|
44
51
|
var primaryStyle = Object.assign({}, btnStyle, { background: 'var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary,#4a7dff))', borderColor: 'transparent', color: 'var(--dsw-alias-label-primary-inverted,#fff)' })
|
|
@@ -47,8 +54,23 @@ window.__ModuleLoader__.load({
|
|
|
47
54
|
var title = props.title
|
|
48
55
|
var hint = props.hint
|
|
49
56
|
var labels = props.labels || {}
|
|
57
|
+
// 模板参数定义([{key,label,placeholder,multiline,value}])→ 值表
|
|
58
|
+
var paramDefs = Array.isArray(props.params) ? props.params : []
|
|
59
|
+
var initialParamValues = {}
|
|
60
|
+
for (var pi = 0; pi < paramDefs.length; pi++) {
|
|
61
|
+
var def = paramDefs[pi]
|
|
62
|
+
initialParamValues[def.key] = def.value !== undefined && def.value !== null ? String(def.value) : ''
|
|
63
|
+
}
|
|
64
|
+
var _pv = useState(initialParamValues)
|
|
65
|
+
var paramValues = _pv[0]; var setParamValues = _pv[1]
|
|
50
66
|
var _p = useState(props.initialPrompt || '')
|
|
51
67
|
var prompt = _p[0]; var setPrompt = _p[1]
|
|
68
|
+
// 「上次自动生成的 prompt」ref 镜像:effect 里比较当前 prompt 是否等于它,
|
|
69
|
+
// 判断用户是否手动编辑过——未手改则参数/模板变化可安全覆盖,手改过则保留
|
|
70
|
+
// 手动编辑(ntd ActionButton 的 lastGenerated 同款规则)。旧 dirty 单标记
|
|
71
|
+
// 无法表达「手改后又想让参数替换生效」的场景,且要同时服务 initialPrompt
|
|
72
|
+
// 异步到位的跟随行为,故统一收敛到这一处比较。
|
|
73
|
+
var lastGeneratedRef = useRef(null)
|
|
52
74
|
var _j = useState(null)
|
|
53
75
|
var job = _j[0]; var setJob = _j[1]
|
|
54
76
|
var _b = useState(false)
|
|
@@ -57,13 +79,14 @@ window.__ModuleLoader__.load({
|
|
|
57
79
|
var copied = _c[0]; var setCopied = _c[1]
|
|
58
80
|
var _e = useState('')
|
|
59
81
|
var error = _e[0]; var setError = _e[1]
|
|
60
|
-
var _d = useState(false)
|
|
61
|
-
var dirty = _d[0]; var setDirty = _d[1]
|
|
62
82
|
|
|
63
|
-
//
|
|
83
|
+
// 参数值/模板变化 → 重新生成 prompt;仅当用户未手改时覆盖
|
|
64
84
|
useEffect(function () {
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
var generated = substituteParams(props.initialPrompt || '', paramValues)
|
|
86
|
+
var userEdited = lastGeneratedRef.current !== null && prompt !== lastGeneratedRef.current
|
|
87
|
+
lastGeneratedRef.current = generated
|
|
88
|
+
if (!userEdited) setPrompt(generated)
|
|
89
|
+
}, [props.initialPrompt, paramValues])
|
|
67
90
|
|
|
68
91
|
useEffect(function () {
|
|
69
92
|
if (job === null || job.status !== 'running' || typeof props.poll !== 'function') return
|
|
@@ -75,6 +98,15 @@ window.__ModuleLoader__.load({
|
|
|
75
98
|
return function () { clearInterval(timer) }
|
|
76
99
|
}, [job !== null && job.jobId])
|
|
77
100
|
|
|
101
|
+
var setParam = function (key, value) {
|
|
102
|
+
setParamValues(function (prev) {
|
|
103
|
+
var next = {}
|
|
104
|
+
for (var k in prev) next[k] = prev[k]
|
|
105
|
+
next[key] = value
|
|
106
|
+
return next
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
78
110
|
var doRun = function () {
|
|
79
111
|
if (typeof props.run !== 'function') return
|
|
80
112
|
setBusy(true); setError('')
|
|
@@ -90,6 +122,8 @@ window.__ModuleLoader__.load({
|
|
|
90
122
|
}
|
|
91
123
|
}
|
|
92
124
|
var statusText = job === null ? '' : job.status === 'running' ? (labels.running || 'running') : job.status === 'done' ? (labels.done || 'done') : (labels.failed || 'failed') + (job.code != null ? ' (' + job.code + ')' : '')
|
|
125
|
+
// 完成态插槽:提供后 job done 由插槽全权渲染(footer 置空,操作按钮插槽自承)
|
|
126
|
+
var completedSlot = typeof props.completedView === 'function' && job !== null && job.status === 'done'
|
|
93
127
|
|
|
94
128
|
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' } },
|
|
95
129
|
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)' } },
|
|
@@ -97,16 +131,27 @@ window.__ModuleLoader__.load({
|
|
|
97
131
|
h('div', { style: { fontSize: 17, fontWeight: 600 } }, title || ''),
|
|
98
132
|
h('button', { onClick: props.onClose, style: Object.assign({}, btnStyle, { marginLeft: 'auto', width: 28, height: 28, padding: 0, borderRadius: 28 }) }, '✕')),
|
|
99
133
|
hint ? h('div', { style: { fontSize: 12, opacity: .7 } }, hint) : null,
|
|
134
|
+
// 模板参数输入区(idle 态;值实时替换进 prompt,位于 prompt 上方与 ntd 同布局)
|
|
135
|
+
paramDefs.length > 0 ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
136
|
+
paramDefs.map(function (d) {
|
|
137
|
+
return h('label', { key: d.key, style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: 12, opacity: .85 } },
|
|
138
|
+
h('span', null, d.label || d.key),
|
|
139
|
+
d.multiline
|
|
140
|
+
? h('textarea', { value: paramValues[d.key] || '', placeholder: d.placeholder || '', onChange: function (e) { setParam(d.key, e.target.value) }, spellCheck: false, style: Object.assign({}, paramStyle, { minHeight: 64, resize: 'vertical' }) })
|
|
141
|
+
: h('input', { value: paramValues[d.key] || '', placeholder: d.placeholder || '', onChange: function (e) { setParam(d.key, e.target.value) }, style: paramStyle }))
|
|
142
|
+
})) : null,
|
|
100
143
|
(props.rows || []).length > 0 ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13 } },
|
|
101
144
|
props.rows.map(function (r, i) {
|
|
102
145
|
return r[1] ? h('div', { key: i }, h('b', null, r[0] + ':'), h('span', null, r[1])) : null
|
|
103
146
|
})) : null,
|
|
104
|
-
h('textarea', { value: prompt, onChange: function (e) {
|
|
147
|
+
h('textarea', { value: prompt, onChange: function (e) { setPrompt(e.target.value) }, spellCheck: false, style: inputStyle }),
|
|
105
148
|
error !== '' ? h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error,#c75050)' } }, error) : null,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
149
|
+
completedSlot
|
|
150
|
+
? props.completedView({ job: job, output: job.output || '', close: props.onClose, retry: doRun })
|
|
151
|
+
: (job !== null ? h('div', null,
|
|
152
|
+
h('div', { style: { fontSize: 12, opacity: .7, margin: '4px 0' } }, (labels.outputLabel || 'Output') + ' · ' + statusText),
|
|
153
|
+
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),
|
|
154
|
+
completedSlot ? null : h('div', { style: { display: 'flex', gap: 8 } },
|
|
110
155
|
canOpenSession ? h('button', { onClick: openSession, style: btnStyle }, labels.openSession || 'Open chat') : null,
|
|
111
156
|
h('button', { onClick: copy, style: btnStyle }, copied ? (labels.copied || 'Copied') : (labels.copy || 'Copy')),
|
|
112
157
|
h('button', { onClick: doRun, disabled: busy || (job !== null && job.status === 'running'), style: primaryStyle }, job !== null && job.status === 'running' ? (labels.running || 'Running…') : (labels.run || 'Run')))))
|
|
@@ -190,7 +235,7 @@ window.__ModuleLoader__.load({
|
|
|
190
235
|
tabMine: '我的',
|
|
191
236
|
tabBuiltin: '内置',
|
|
192
237
|
searchPlaceholder: '搜索专家名称、职业、描述…',
|
|
193
|
-
mineEmpty: '
|
|
238
|
+
mineEmpty: '用户库还没有专家。去「内置」页浏览安装,或点上方按钮 AI 创建。',
|
|
194
239
|
builtinEmpty: '内置专家为空。请在设置中同步内置仓库。',
|
|
195
240
|
expertTypeAgent: '专家',
|
|
196
241
|
expertTypeTeam: '团队',
|
|
@@ -280,6 +325,24 @@ window.__ModuleLoader__.load({
|
|
|
280
325
|
pickerEmpty: '没有匹配的专家',
|
|
281
326
|
pickerTabAgents: '专家',
|
|
282
327
|
pickerTabTeams: '专家团',
|
|
328
|
+
createExpertBtn: 'AI 创建专家',
|
|
329
|
+
createTeamBtn: 'AI 创建专家团',
|
|
330
|
+
createTitleAgent: 'AI 创建专家',
|
|
331
|
+
createTitleTeam: 'AI 创建专家团',
|
|
332
|
+
createHint: '描述你想要的专家,AI 生成 plugin.json 与角色定义;确认或修改后写入用户库。',
|
|
333
|
+
createParamDescription: '专家描述',
|
|
334
|
+
createParamPlaceholder: '一句话描述,如:精通 Rust 的后端架构师',
|
|
335
|
+
createParseFailed: 'AI 输出不符合约定格式(未找到代码块),可修改描述重试',
|
|
336
|
+
createReady: 'AI 已生成专家定义,确认无误后点击创建',
|
|
337
|
+
createNow: '创建',
|
|
338
|
+
creating: '创建中…',
|
|
339
|
+
createdDone: '已创建专家 {name}',
|
|
340
|
+
createInvalidJson: 'plugin.json 格式无效',
|
|
341
|
+
createTypeMismatch: '生成的 expertType 与所选类型不符',
|
|
342
|
+
createLeadMissing: 'teamInfo.leadAgent 未对齐任何成员文件',
|
|
343
|
+
createEmptyFile: '角色定义内容不能为空',
|
|
344
|
+
retry: '重试',
|
|
345
|
+
teamAgentFiles: '成员角色定义',
|
|
283
346
|
}
|
|
284
347
|
|
|
285
348
|
const EN = {
|
|
@@ -288,7 +351,7 @@ window.__ModuleLoader__.load({
|
|
|
288
351
|
tabMine: 'Mine',
|
|
289
352
|
tabBuiltin: 'Built-in',
|
|
290
353
|
searchPlaceholder: 'Search experts by name, profession, description…',
|
|
291
|
-
mineEmpty: 'No experts in the user library yet. Browse the Built-in tab and install one.',
|
|
354
|
+
mineEmpty: 'No experts in the user library yet. Browse the Built-in tab and install one, or create one with the buttons above.',
|
|
292
355
|
builtinEmpty: 'Built-in experts are empty. Sync the built-in repo in settings.',
|
|
293
356
|
expertTypeAgent: 'Expert',
|
|
294
357
|
expertTypeTeam: 'Team',
|
|
@@ -378,6 +441,24 @@ window.__ModuleLoader__.load({
|
|
|
378
441
|
pickerEmpty: 'No matching experts',
|
|
379
442
|
pickerTabAgents: 'Experts',
|
|
380
443
|
pickerTabTeams: 'Teams',
|
|
444
|
+
createExpertBtn: 'AI Create Expert',
|
|
445
|
+
createTeamBtn: 'AI Create Expert Team',
|
|
446
|
+
createTitleAgent: 'AI Create Expert',
|
|
447
|
+
createTitleTeam: 'AI Create Expert Team',
|
|
448
|
+
createHint: 'Describe the expert you want; AI generates plugin.json and role definitions. Review or edit, then save into the user library.',
|
|
449
|
+
createParamDescription: 'Expert description',
|
|
450
|
+
createParamPlaceholder: 'One sentence, e.g. a Rust backend architect',
|
|
451
|
+
createParseFailed: 'AI output does not follow the agreed format (code blocks not found) — edit the description and retry',
|
|
452
|
+
createReady: 'AI generated the expert definition — review and click Create',
|
|
453
|
+
createNow: 'Create',
|
|
454
|
+
creating: 'Creating…',
|
|
455
|
+
createdDone: 'Expert {name} created',
|
|
456
|
+
createInvalidJson: 'Invalid plugin.json',
|
|
457
|
+
createTypeMismatch: 'Generated expertType does not match the selected kind',
|
|
458
|
+
createLeadMissing: 'teamInfo.leadAgent does not match any member file',
|
|
459
|
+
createEmptyFile: 'Role definition content must not be empty',
|
|
460
|
+
retry: 'Retry',
|
|
461
|
+
teamAgentFiles: 'Member role definitions',
|
|
381
462
|
}
|
|
382
463
|
|
|
383
464
|
// ── Styles ───────────────────────────────────────────────────────────────
|
|
@@ -405,8 +486,8 @@ window.__ModuleLoader__.load({
|
|
|
405
486
|
.exp-badge{font-size:11px;border-radius:6px;padding:2px 7px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}
|
|
406
487
|
.exp-badge[data-kind="type"]{color:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
|
|
407
488
|
.exp-badge[data-kind="installed"]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}
|
|
408
|
-
.exp-card-actions{display:flex;gap:8px;justify-content:flex-end}
|
|
409
|
-
.exp-btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:5px 12px;cursor:pointer;font:inherit;font-size:13px}
|
|
489
|
+
.exp-card-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
|
|
490
|
+
.exp-btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:5px 12px;cursor:pointer;font:inherit;font-size:13px;white-space:nowrap;flex:none}
|
|
410
491
|
.exp-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
411
492
|
.exp-btn[data-primary="true"]{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted)}
|
|
412
493
|
.exp-btn[data-danger="true"]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
|
|
@@ -427,6 +508,7 @@ window.__ModuleLoader__.load({
|
|
|
427
508
|
.exp-skill-row{display:flex;flex-direction:column;gap:2px;padding:8px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
428
509
|
.exp-skill-row:last-child{border-bottom:0}
|
|
429
510
|
.exp-form-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
511
|
+
.exp-form-row .exp-btn{white-space:nowrap;flex:none}
|
|
430
512
|
.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}
|
|
431
513
|
.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%}
|
|
432
514
|
.exp-status-line{display:flex;gap:14px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
@@ -826,6 +908,106 @@ window.__ModuleLoader__.load({
|
|
|
826
908
|
'- 全程与最终汇报都使用中文。',
|
|
827
909
|
].join('\n')
|
|
828
910
|
|
|
911
|
+
// ── AI 创建专家(ntd ExpertCreateModal 同款:一句话描述 → 围栏输出 → 预览确认 → 落盘)──
|
|
912
|
+
|
|
913
|
+
/** 创建型模板共用的输出纪律:除约定围栏外禁止出现任何代码围栏——
|
|
914
|
+
* 执行器输出是流式全文(含中间叙述与 [tool] 行),前端按围栏解析,多余围栏会污染提取。 */
|
|
915
|
+
const EXPERT_CREATE_OUTPUT_RULES = [
|
|
916
|
+
'## 输出格式(严格遵守)',
|
|
917
|
+
'- 只输出约定的代码块,代码块之外不要输出任何解释、标题或结论。',
|
|
918
|
+
'- 正文中不要出现任何其他代码围栏(```)。',
|
|
919
|
+
'- 所有展示文案(displayName/profession/displayDescription/tags/提示词)中英双语,正文内容用中文。',
|
|
920
|
+
'- 不要输出 avatar 字段(头像暂不支持,缺省即用默认图标)。',
|
|
921
|
+
].join('\n')
|
|
922
|
+
|
|
923
|
+
const EXPERT_CREATE_CATEGORY_LIST = [
|
|
924
|
+
'01-ProductDesign(产品设计)', '02-Engineering(工程技术)', '03-GameSpatial(游戏与空间)',
|
|
925
|
+
'04-DataAI(数据与 AI)', '06-ContentCreative(内容与创意)', '08-FinanceInvestment(金融投资)',
|
|
926
|
+
'10-ProjectQuality(项目质量)', '11-SecurityCompliance(安全合规)', '12-IndustryConsultant(行业咨询)',
|
|
927
|
+
].join('、')
|
|
928
|
+
|
|
929
|
+
const EXPERT_CREATE_PROMPT_AGENT = [
|
|
930
|
+
'你是专家系统设计师。根据用户的描述,生成一个完整的专家定义(ntd/WorkBuddy 格式:plugin.json + agent.md)。',
|
|
931
|
+
'',
|
|
932
|
+
'用户描述:{{description}}',
|
|
933
|
+
'',
|
|
934
|
+
'第一步:用 ```json 围栏输出完整 plugin.json,字段要求:',
|
|
935
|
+
'- name: 专家 ID,只允许小写字母/数字/连字符(如 rust-backend-architect),与描述语义相符',
|
|
936
|
+
'- version: "1.0.0";expertType: "agent"',
|
|
937
|
+
'- description: 一句话英文简介',
|
|
938
|
+
'- displayName / profession / displayDescription: {zh, en} 双语对象',
|
|
939
|
+
`- categoryId: 从这些值里选一个:${EXPERT_CREATE_CATEGORY_LIST}`,
|
|
940
|
+
'- tags: [{zh, en}],至少 3 个',
|
|
941
|
+
'- agentName: 与 name 相同;agents: ["./agents/<name>.md"]',
|
|
942
|
+
'- defaultInitPrompt: {zh, en};quickPrompts: [{zh, en}] 至少 2 条',
|
|
943
|
+
'',
|
|
944
|
+
'第二步:用 ```markdown 围栏输出完整 agent.md:',
|
|
945
|
+
'- 开头 YAML frontmatter:name(=plugin.json 的 agentName)、description、color(英文颜色词)、emoji、vibe',
|
|
946
|
+
'- 正文中文撰写,包含:身份与记忆、核心使命、专业技能、工作流程、约束规则等章节,内容专业、具体、可执行',
|
|
947
|
+
'',
|
|
948
|
+
EXPERT_CREATE_OUTPUT_RULES,
|
|
949
|
+
].join('\n')
|
|
950
|
+
|
|
951
|
+
const EXPERT_CREATE_PROMPT_TEAM = [
|
|
952
|
+
'你是专家团队设计师。根据用户的描述,生成一个完整的专家团队定义(ntd/WorkBuddy 格式,expertType=team:一名负责人 + 若干成员,各带角色定义文件)。',
|
|
953
|
+
'',
|
|
954
|
+
'用户描述:{{description}}',
|
|
955
|
+
'',
|
|
956
|
+
'第一步:用 ```json 围栏输出完整 plugin.json,字段要求:',
|
|
957
|
+
'- name: 团队 ID,只允许小写字母/数字/连字符(如 fullstack-delivery-team)',
|
|
958
|
+
'- version: "1.0.0";expertType: "team"',
|
|
959
|
+
'- description: 一句话英文简介',
|
|
960
|
+
'- displayName / profession / displayDescription: 团队级 {zh, en} 双语对象',
|
|
961
|
+
`- categoryId: 从这些值里选一个:${EXPERT_CREATE_CATEGORY_LIST}`,
|
|
962
|
+
'- tags: [{zh, en}],至少 3 个',
|
|
963
|
+
'- 成员规模 3–6 人(含负责人),按描述合理分工,成员 id 全部为小写字母/数字/连字符',
|
|
964
|
+
'- agentName: 负责人成员 id',
|
|
965
|
+
'- teamInfo: { "leadAgent": "<负责人id>", "memberAgents": ["<成员id>", ...] }(不含负责人)',
|
|
966
|
+
'- agents: ["./agents/<id>.md", ...],负责人的文件必须排在第一个',
|
|
967
|
+
'- members: [{ "id", "name": {zh,en}, "displayName": {zh,en}, "profession": {zh,en}, "role": "lead"|"member" }],id 与 agents 文件一一对应',
|
|
968
|
+
'- defaultInitPrompt: {zh, en};quickPrompts: [{zh, en}] 至少 2 条',
|
|
969
|
+
'',
|
|
970
|
+
'第二步:逐成员输出角色定义——每个成员一个 ```markdown agents/<成员id>.md 围栏(围栏起始行必须带上该文件路径),负责人的块排第一:',
|
|
971
|
+
'- 每个文件开头 YAML frontmatter:name(=成员 id)、description、color(英文颜色词)、emoji、vibe',
|
|
972
|
+
'- 正文中文撰写:身份与记忆、核心使命、专业技能、工作流程、约束规则;写明在团队中的分工与交接关系(负责人负责任务拆分与汇总)',
|
|
973
|
+
'',
|
|
974
|
+
EXPERT_CREATE_OUTPUT_RULES,
|
|
975
|
+
].join('\n')
|
|
976
|
+
|
|
977
|
+
/** 创建模板注册表:key = expertType,入口按钮只是预选条目的快捷方式。 */
|
|
978
|
+
const CREATE_TEMPLATES = {
|
|
979
|
+
agent: { key: 'agent', btnKey: 'createExpertBtn', titleKey: 'createTitleAgent', prompt: EXPERT_CREATE_PROMPT_AGENT },
|
|
980
|
+
team: { key: 'team', btnKey: 'createTeamBtn', titleKey: 'createTitleTeam', prompt: EXPERT_CREATE_PROMPT_TEAM },
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* 解析 AI 生成输出 → 创建载荷(纯函数,便于测试)。
|
|
985
|
+
* 执行器 output 是流式全文(含 [tool] 行与中间叙述),按围栏标签提取:
|
|
986
|
+
* agent 型取第一个 ```json + 第一个 ```markdown;team 型取 ```json +
|
|
987
|
+
* 全部带路径标注的 ```markdown agents/<id>.md(同名去重,按出现顺序)。
|
|
988
|
+
* 围栏缺失 → { ok:false, raw },UI 展示原文并允许重试。
|
|
989
|
+
*/
|
|
990
|
+
function parseCreateResult(output, type) {
|
|
991
|
+
const text = String(output || '')
|
|
992
|
+
const jsonMatch = text.match(/```json[ \t]*\r?\n([\s\S]*?)```/)
|
|
993
|
+
if (!jsonMatch) return { ok: false, raw: text }
|
|
994
|
+
const pluginJson = jsonMatch[1].trim()
|
|
995
|
+
if (type === 'team') {
|
|
996
|
+
const files = []
|
|
997
|
+
const re = /```markdown[ \t]+([^\s`]+)[ \t]*\r?\n([\s\S]*?)```/g
|
|
998
|
+
let m
|
|
999
|
+
while ((m = re.exec(text)) !== null) {
|
|
1000
|
+
const file = m[1].trim()
|
|
1001
|
+
if (!files.some((f) => f.file === file)) files.push({ file, content: m[2].trim() })
|
|
1002
|
+
}
|
|
1003
|
+
if (files.length === 0) return { ok: false, raw: text }
|
|
1004
|
+
return { ok: true, pluginJson, files }
|
|
1005
|
+
}
|
|
1006
|
+
const mdMatch = text.match(/```markdown[ \t]*\r?\n([\s\S]*?)```/)
|
|
1007
|
+
if (!mdMatch) return { ok: false, raw: text }
|
|
1008
|
+
return { ok: true, pluginJson, agentMd: mdMatch[1].trim() }
|
|
1009
|
+
}
|
|
1010
|
+
|
|
829
1011
|
function DetailModal({ name, source, t, onClose, onInstalled, onDeleted, onToast, onOpenSession }) {
|
|
830
1012
|
const [detail, setDetail] = useState(null)
|
|
831
1013
|
const [error, setError] = useState('')
|
|
@@ -1129,6 +1311,99 @@ window.__ModuleLoader__.load({
|
|
|
1129
1311
|
h('button', { className: 'exp-btn', 'data-primary': 'true', disabled: busy || status.syncing, onClick: sync }, busy || status.syncing ? t('syncing') : t('syncNow'))))))
|
|
1130
1312
|
}
|
|
1131
1313
|
|
|
1314
|
+
// ── AI 创建专家:对话框(消费 kit 的 params + completedView 扩展)──────────
|
|
1315
|
+
|
|
1316
|
+
const CREATE_AREA_STYLE = { width: '100%', minHeight: '200px', fontFamily: 'ui-monospace,monospace', fontSize: '12px', lineHeight: 1.6, whiteSpace: 'pre-wrap', boxSizing: 'border-box', resize: 'vertical' }
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* 创建完成态:解析执行器输出 → 可编辑预览(plugin.json + 角色定义)→ POST /api/create。
|
|
1320
|
+
* 解析失败展示原文 + 重试(ntd ExpertCreateCompleted 同款三态:可解析/不可解析/错误)。
|
|
1321
|
+
*/
|
|
1322
|
+
function CreateCompleted({ t, type, ctx, onCreated }) {
|
|
1323
|
+
const parsed = useMemo(() => parseCreateResult(ctx.output, type), [ctx.output, type])
|
|
1324
|
+
const [pluginText, setPluginText] = useState(parsed.pluginJson || '')
|
|
1325
|
+
const [agentMd, setAgentMd] = useState(parsed.agentMd || '')
|
|
1326
|
+
const [files, setFiles] = useState(Array.isArray(parsed.files) ? parsed.files : [])
|
|
1327
|
+
const [creating, setCreating] = useState(false)
|
|
1328
|
+
const [error, setError] = useState('')
|
|
1329
|
+
if (!parsed.ok) {
|
|
1330
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
1331
|
+
h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, t('createParseFailed')),
|
|
1332
|
+
h('pre', { className: 'exp-pre', style: { maxHeight: 260 } }, parsed.raw),
|
|
1333
|
+
h('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } },
|
|
1334
|
+
h('button', { className: 'exp-btn', onClick: ctx.close }, t('close')),
|
|
1335
|
+
h('button', { className: 'exp-btn', 'data-primary': 'true', onClick: ctx.retry }, t('retry'))))
|
|
1336
|
+
}
|
|
1337
|
+
let preview = null
|
|
1338
|
+
try { preview = JSON.parse(pluginText) } catch { preview = null }
|
|
1339
|
+
const leadFile = preview !== null && preview.teamInfo !== null && typeof preview.teamInfo === 'object' && typeof preview.teamInfo.leadAgent === 'string'
|
|
1340
|
+
? `agents/${preview.teamInfo.leadAgent}.md` : ''
|
|
1341
|
+
const create = async () => {
|
|
1342
|
+
setError('')
|
|
1343
|
+
let plugin
|
|
1344
|
+
try { plugin = JSON.parse(pluginText) } catch { plugin = null }
|
|
1345
|
+
if (plugin === null || typeof plugin !== 'object' || Array.isArray(plugin)) { setError(t('createInvalidJson')); return }
|
|
1346
|
+
if (plugin.expertType !== type) { setError(t('createTypeMismatch')); return }
|
|
1347
|
+
const body = { pluginJson: pluginText }
|
|
1348
|
+
if (type === 'agent') {
|
|
1349
|
+
if (typeof agentMd !== 'string' || agentMd.trim() === '') { setError(t('createEmptyFile')); return }
|
|
1350
|
+
body.agentMd = agentMd
|
|
1351
|
+
} else {
|
|
1352
|
+
if (!Array.isArray(files) || files.length === 0) { setError(t('createEmptyFile')); return }
|
|
1353
|
+
for (const f of files) {
|
|
1354
|
+
if (typeof f.content !== 'string' || f.content.trim() === '') { setError(t('createEmptyFile')); return }
|
|
1355
|
+
}
|
|
1356
|
+
body.agents = files.map((f) => ({ file: f.file, content: f.content }))
|
|
1357
|
+
}
|
|
1358
|
+
setCreating(true)
|
|
1359
|
+
try {
|
|
1360
|
+
const r = await fetchJson(`${API}/create`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
1361
|
+
onCreated((r.created && r.created.name) || plugin.name)
|
|
1362
|
+
ctx.close()
|
|
1363
|
+
} catch (e) { setError(String(e && e.message)) } finally { setCreating(false) }
|
|
1364
|
+
}
|
|
1365
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
1366
|
+
h('div', { className: 'exp-checkline' }, '✅', t('createReady')),
|
|
1367
|
+
preview !== null ? h('div', { className: 'exp-kv' },
|
|
1368
|
+
h('b', null, preview.expertType === 'team' ? t('expertTypeTeam') : t('expertTypeAgent')),
|
|
1369
|
+
h('span', null, (preview.displayName && (preview.displayName.zh || preview.displayName.en)) || preview.name || '')) : null,
|
|
1370
|
+
h('div', null,
|
|
1371
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, t('pluginJson')),
|
|
1372
|
+
h('textarea', { className: 'exp-input', value: pluginText, onChange: (e) => setPluginText(e.target.value), spellCheck: false, style: CREATE_AREA_STYLE })),
|
|
1373
|
+
type === 'agent'
|
|
1374
|
+
? h('div', null,
|
|
1375
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, t('agents')),
|
|
1376
|
+
h('textarea', { className: 'exp-input', value: agentMd, onChange: (e) => setAgentMd(e.target.value), spellCheck: false, style: CREATE_AREA_STYLE }))
|
|
1377
|
+
: h('div', null,
|
|
1378
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, t('teamAgentFiles')),
|
|
1379
|
+
...files.map((f, i) => h('div', { key: f.file, style: { marginBottom: 8 } },
|
|
1380
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, `${f.file}${f.file !== '' && f.file === leadFile ? ' ★' : ''}`),
|
|
1381
|
+
h('textarea', { className: 'exp-input', value: f.content, onChange: (e) => setFiles(files.map((x, j) => (j === i ? { ...x, content: e.target.value } : x))), spellCheck: false, style: CREATE_AREA_STYLE })))),
|
|
1382
|
+
error !== '' ? h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, error) : null,
|
|
1383
|
+
h('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', paddingTop: 8, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
1384
|
+
h('button', { className: 'exp-btn', onClick: ctx.close }, t('close')),
|
|
1385
|
+
h('button', { className: 'exp-btn', onClick: ctx.retry }, t('retry')),
|
|
1386
|
+
h('button', { className: 'exp-btn', 'data-primary': 'true', disabled: creating, onClick: create }, creating ? t('creating') : t('createNow'))))
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** 创建入口对话框:kit ActionShareDialog 的创建态用法(params 输入 + completedView 接管)。
|
|
1390
|
+
* 两个工具栏按钮各自预选模板条目(CREATE_TEMPLATES[type]),对话框管线与入口无关。 */
|
|
1391
|
+
function CreateExpertDialog({ t, type, onClose, onCreated, onOpenSession }) {
|
|
1392
|
+
const tpl = CREATE_TEMPLATES[type] || CREATE_TEMPLATES.agent
|
|
1393
|
+
return h(getShareDialogComponent(), {
|
|
1394
|
+
title: t(tpl.titleKey),
|
|
1395
|
+
hint: t('createHint'),
|
|
1396
|
+
params: [{ key: 'description', label: t('createParamDescription'), placeholder: t('createParamPlaceholder'), multiline: true }],
|
|
1397
|
+
initialPrompt: tpl.prompt,
|
|
1398
|
+
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') },
|
|
1399
|
+
run: (prompt) => fetchJson(`${API}/create/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }) }),
|
|
1400
|
+
poll: (id) => fetchJson(`${API}/create/run?id=${encodeURIComponent(id)}`),
|
|
1401
|
+
completedView: (ctx) => h(CreateCompleted, { t, type, ctx, onCreated }),
|
|
1402
|
+
onOpenSession: (sessionId) => { onClose(); if (onOpenSession) onOpenSession(sessionId) },
|
|
1403
|
+
onClose,
|
|
1404
|
+
})
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1132
1407
|
// ── Page ─────────────────────────────────────────────────────────────────
|
|
1133
1408
|
|
|
1134
1409
|
function ExpertsPage({ t, embedded, onClose }) {
|
|
@@ -1138,6 +1413,7 @@ window.__ModuleLoader__.load({
|
|
|
1138
1413
|
const [search, setSearch] = useState('')
|
|
1139
1414
|
const [selected, setSelected] = useState(null) // {name, source}
|
|
1140
1415
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
1416
|
+
const [createType, setCreateType] = useState(null) // null | 'agent' | 'team'
|
|
1141
1417
|
const [busyName, setBusyName] = useState(null)
|
|
1142
1418
|
const [toast, setToast] = useState(null)
|
|
1143
1419
|
const showToast = (text) => { setToast(text); setTimeout(() => setToast(null), 2600) }
|
|
@@ -1187,6 +1463,8 @@ window.__ModuleLoader__.load({
|
|
|
1187
1463
|
h('button', { className: 'exp-tab', 'data-on': tab === 'builtin', onClick: () => setTab('builtin') }, `${t('tabBuiltin')}${data ? ` (${data.builtin.length})` : ''}`)),
|
|
1188
1464
|
h('input', { className: 'exp-input exp-search', placeholder: t('searchPlaceholder'), value: search, onChange: (e) => setSearch(e.target.value) }),
|
|
1189
1465
|
h('span', { className: 'exp-count' }, `${rows.length}`),
|
|
1466
|
+
tab === 'mine' ? h('button', { className: 'exp-btn', 'data-primary': 'true', title: t('createHint'), onClick: () => setCreateType('agent') }, `⚡ ${t('createExpertBtn')}`) : null,
|
|
1467
|
+
tab === 'mine' ? h('button', { className: 'exp-btn', title: t('createHint'), onClick: () => setCreateType('team') }, `👥 ${t('createTeamBtn')}`) : null,
|
|
1190
1468
|
tab === 'builtin' ? h('button', { className: 'exp-btn', title: t('builtinSettings'), onClick: () => setSettingsOpen(true) }, t('builtinSettings')) : null),
|
|
1191
1469
|
error !== '' ? h('div', { className: 'exp-empty' }, `${t('loadFailed')}: ${error}`) : null,
|
|
1192
1470
|
data !== null && rows.length === 0 ? h('div', { className: 'exp-empty' }, tab === 'mine' ? t('mineEmpty') : t('builtinEmpty')) : null,
|
|
@@ -1205,6 +1483,17 @@ window.__ModuleLoader__.load({
|
|
|
1205
1483
|
settingsOpen ? h(BuiltinSettingsDialog, {
|
|
1206
1484
|
t, onClose: () => setSettingsOpen(false), onToast: showToast, onSynced: reload,
|
|
1207
1485
|
}) : null,
|
|
1486
|
+
createType !== null ? h(CreateExpertDialog, {
|
|
1487
|
+
t, type: createType,
|
|
1488
|
+
onClose: () => setCreateType(null),
|
|
1489
|
+
onCreated: (name) => {
|
|
1490
|
+
setCreateType(null)
|
|
1491
|
+
showToast(t('createdDone', { name }))
|
|
1492
|
+
reload()
|
|
1493
|
+
fetchRoster(true) // composer 候选立即见到新专家(60s TTL 缓存强制失效)
|
|
1494
|
+
},
|
|
1495
|
+
onOpenSession: (sessionId) => { try { sessionsApi.open(sessionId) } catch (e) { showToast(String(e && e.message)) } },
|
|
1496
|
+
}) : null,
|
|
1208
1497
|
toast !== null ? h('div', { className: 'exp-toast' }, toast) : null)
|
|
1209
1498
|
}
|
|
1210
1499
|
|
|
@@ -1217,6 +1506,7 @@ window.__ModuleLoader__.load({
|
|
|
1217
1506
|
NS, ZH, EN, matchExpert, formatSize, formatTime, avatarUrl,
|
|
1218
1507
|
EXPERT_SOURCE_NAME, makeExpertSource, openTriggerSource, fetchRoster,
|
|
1219
1508
|
toRosterRows, insertComposerText, splitRosterByType, pickerRowMatch,
|
|
1509
|
+
parseCreateResult, CREATE_TEMPLATES, EXPERT_CREATE_PROMPT_AGENT, EXPERT_CREATE_PROMPT_TEAM,
|
|
1220
1510
|
},
|
|
1221
1511
|
/** Test/host helper: mount a standalone page into any container. */
|
|
1222
1512
|
__boot(container, opts = {}) {
|
package/client/index.js
CHANGED
|
@@ -72,7 +72,7 @@ const ZH = {
|
|
|
72
72
|
tabMine: '我的',
|
|
73
73
|
tabBuiltin: '内置',
|
|
74
74
|
searchPlaceholder: '搜索专家名称、职业、描述…',
|
|
75
|
-
mineEmpty: '
|
|
75
|
+
mineEmpty: '用户库还没有专家。去「内置」页浏览安装,或点上方按钮 AI 创建。',
|
|
76
76
|
builtinEmpty: '内置专家为空。请在设置中同步内置仓库。',
|
|
77
77
|
expertTypeAgent: '专家',
|
|
78
78
|
expertTypeTeam: '团队',
|
|
@@ -162,6 +162,24 @@ const ZH = {
|
|
|
162
162
|
pickerEmpty: '没有匹配的专家',
|
|
163
163
|
pickerTabAgents: '专家',
|
|
164
164
|
pickerTabTeams: '专家团',
|
|
165
|
+
createExpertBtn: 'AI 创建专家',
|
|
166
|
+
createTeamBtn: 'AI 创建专家团',
|
|
167
|
+
createTitleAgent: 'AI 创建专家',
|
|
168
|
+
createTitleTeam: 'AI 创建专家团',
|
|
169
|
+
createHint: '描述你想要的专家,AI 生成 plugin.json 与角色定义;确认或修改后写入用户库。',
|
|
170
|
+
createParamDescription: '专家描述',
|
|
171
|
+
createParamPlaceholder: '一句话描述,如:精通 Rust 的后端架构师',
|
|
172
|
+
createParseFailed: 'AI 输出不符合约定格式(未找到代码块),可修改描述重试',
|
|
173
|
+
createReady: 'AI 已生成专家定义,确认无误后点击创建',
|
|
174
|
+
createNow: '创建',
|
|
175
|
+
creating: '创建中…',
|
|
176
|
+
createdDone: '已创建专家 {name}',
|
|
177
|
+
createInvalidJson: 'plugin.json 格式无效',
|
|
178
|
+
createTypeMismatch: '生成的 expertType 与所选类型不符',
|
|
179
|
+
createLeadMissing: 'teamInfo.leadAgent 未对齐任何成员文件',
|
|
180
|
+
createEmptyFile: '角色定义内容不能为空',
|
|
181
|
+
retry: '重试',
|
|
182
|
+
teamAgentFiles: '成员角色定义',
|
|
165
183
|
}
|
|
166
184
|
|
|
167
185
|
const EN = {
|
|
@@ -170,7 +188,7 @@ const EN = {
|
|
|
170
188
|
tabMine: 'Mine',
|
|
171
189
|
tabBuiltin: 'Built-in',
|
|
172
190
|
searchPlaceholder: 'Search experts by name, profession, description…',
|
|
173
|
-
mineEmpty: 'No experts in the user library yet. Browse the Built-in tab and install one.',
|
|
191
|
+
mineEmpty: 'No experts in the user library yet. Browse the Built-in tab and install one, or create one with the buttons above.',
|
|
174
192
|
builtinEmpty: 'Built-in experts are empty. Sync the built-in repo in settings.',
|
|
175
193
|
expertTypeAgent: 'Expert',
|
|
176
194
|
expertTypeTeam: 'Team',
|
|
@@ -260,6 +278,24 @@ const EN = {
|
|
|
260
278
|
pickerEmpty: 'No matching experts',
|
|
261
279
|
pickerTabAgents: 'Experts',
|
|
262
280
|
pickerTabTeams: 'Teams',
|
|
281
|
+
createExpertBtn: 'AI Create Expert',
|
|
282
|
+
createTeamBtn: 'AI Create Expert Team',
|
|
283
|
+
createTitleAgent: 'AI Create Expert',
|
|
284
|
+
createTitleTeam: 'AI Create Expert Team',
|
|
285
|
+
createHint: 'Describe the expert you want; AI generates plugin.json and role definitions. Review or edit, then save into the user library.',
|
|
286
|
+
createParamDescription: 'Expert description',
|
|
287
|
+
createParamPlaceholder: 'One sentence, e.g. a Rust backend architect',
|
|
288
|
+
createParseFailed: 'AI output does not follow the agreed format (code blocks not found) — edit the description and retry',
|
|
289
|
+
createReady: 'AI generated the expert definition — review and click Create',
|
|
290
|
+
createNow: 'Create',
|
|
291
|
+
creating: 'Creating…',
|
|
292
|
+
createdDone: 'Expert {name} created',
|
|
293
|
+
createInvalidJson: 'Invalid plugin.json',
|
|
294
|
+
createTypeMismatch: 'Generated expertType does not match the selected kind',
|
|
295
|
+
createLeadMissing: 'teamInfo.leadAgent does not match any member file',
|
|
296
|
+
createEmptyFile: 'Role definition content must not be empty',
|
|
297
|
+
retry: 'Retry',
|
|
298
|
+
teamAgentFiles: 'Member role definitions',
|
|
263
299
|
}
|
|
264
300
|
|
|
265
301
|
// ── Styles ───────────────────────────────────────────────────────────────
|
|
@@ -287,8 +323,8 @@ const STYLE = `<style>
|
|
|
287
323
|
.exp-badge{font-size:11px;border-radius:6px;padding:2px 7px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}
|
|
288
324
|
.exp-badge[data-kind="type"]{color:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
|
|
289
325
|
.exp-badge[data-kind="installed"]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}
|
|
290
|
-
.exp-card-actions{display:flex;gap:8px;justify-content:flex-end}
|
|
291
|
-
.exp-btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:5px 12px;cursor:pointer;font:inherit;font-size:13px}
|
|
326
|
+
.exp-card-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
|
|
327
|
+
.exp-btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:5px 12px;cursor:pointer;font:inherit;font-size:13px;white-space:nowrap;flex:none}
|
|
292
328
|
.exp-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
293
329
|
.exp-btn[data-primary="true"]{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted)}
|
|
294
330
|
.exp-btn[data-danger="true"]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
|
|
@@ -309,6 +345,7 @@ const STYLE = `<style>
|
|
|
309
345
|
.exp-skill-row{display:flex;flex-direction:column;gap:2px;padding:8px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
310
346
|
.exp-skill-row:last-child{border-bottom:0}
|
|
311
347
|
.exp-form-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
348
|
+
.exp-form-row .exp-btn{white-space:nowrap;flex:none}
|
|
312
349
|
.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}
|
|
313
350
|
.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%}
|
|
314
351
|
.exp-status-line{display:flex;gap:14px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
@@ -708,6 +745,106 @@ const EXPERT_SHARE_PROMPT = [
|
|
|
708
745
|
'- 全程与最终汇报都使用中文。',
|
|
709
746
|
].join('\n')
|
|
710
747
|
|
|
748
|
+
// ── AI 创建专家(ntd ExpertCreateModal 同款:一句话描述 → 围栏输出 → 预览确认 → 落盘)──
|
|
749
|
+
|
|
750
|
+
/** 创建型模板共用的输出纪律:除约定围栏外禁止出现任何代码围栏——
|
|
751
|
+
* 执行器输出是流式全文(含中间叙述与 [tool] 行),前端按围栏解析,多余围栏会污染提取。 */
|
|
752
|
+
const EXPERT_CREATE_OUTPUT_RULES = [
|
|
753
|
+
'## 输出格式(严格遵守)',
|
|
754
|
+
'- 只输出约定的代码块,代码块之外不要输出任何解释、标题或结论。',
|
|
755
|
+
'- 正文中不要出现任何其他代码围栏(```)。',
|
|
756
|
+
'- 所有展示文案(displayName/profession/displayDescription/tags/提示词)中英双语,正文内容用中文。',
|
|
757
|
+
'- 不要输出 avatar 字段(头像暂不支持,缺省即用默认图标)。',
|
|
758
|
+
].join('\n')
|
|
759
|
+
|
|
760
|
+
const EXPERT_CREATE_CATEGORY_LIST = [
|
|
761
|
+
'01-ProductDesign(产品设计)', '02-Engineering(工程技术)', '03-GameSpatial(游戏与空间)',
|
|
762
|
+
'04-DataAI(数据与 AI)', '06-ContentCreative(内容与创意)', '08-FinanceInvestment(金融投资)',
|
|
763
|
+
'10-ProjectQuality(项目质量)', '11-SecurityCompliance(安全合规)', '12-IndustryConsultant(行业咨询)',
|
|
764
|
+
].join('、')
|
|
765
|
+
|
|
766
|
+
const EXPERT_CREATE_PROMPT_AGENT = [
|
|
767
|
+
'你是专家系统设计师。根据用户的描述,生成一个完整的专家定义(ntd/WorkBuddy 格式:plugin.json + agent.md)。',
|
|
768
|
+
'',
|
|
769
|
+
'用户描述:{{description}}',
|
|
770
|
+
'',
|
|
771
|
+
'第一步:用 ```json 围栏输出完整 plugin.json,字段要求:',
|
|
772
|
+
'- name: 专家 ID,只允许小写字母/数字/连字符(如 rust-backend-architect),与描述语义相符',
|
|
773
|
+
'- version: "1.0.0";expertType: "agent"',
|
|
774
|
+
'- description: 一句话英文简介',
|
|
775
|
+
'- displayName / profession / displayDescription: {zh, en} 双语对象',
|
|
776
|
+
`- categoryId: 从这些值里选一个:${EXPERT_CREATE_CATEGORY_LIST}`,
|
|
777
|
+
'- tags: [{zh, en}],至少 3 个',
|
|
778
|
+
'- agentName: 与 name 相同;agents: ["./agents/<name>.md"]',
|
|
779
|
+
'- defaultInitPrompt: {zh, en};quickPrompts: [{zh, en}] 至少 2 条',
|
|
780
|
+
'',
|
|
781
|
+
'第二步:用 ```markdown 围栏输出完整 agent.md:',
|
|
782
|
+
'- 开头 YAML frontmatter:name(=plugin.json 的 agentName)、description、color(英文颜色词)、emoji、vibe',
|
|
783
|
+
'- 正文中文撰写,包含:身份与记忆、核心使命、专业技能、工作流程、约束规则等章节,内容专业、具体、可执行',
|
|
784
|
+
'',
|
|
785
|
+
EXPERT_CREATE_OUTPUT_RULES,
|
|
786
|
+
].join('\n')
|
|
787
|
+
|
|
788
|
+
const EXPERT_CREATE_PROMPT_TEAM = [
|
|
789
|
+
'你是专家团队设计师。根据用户的描述,生成一个完整的专家团队定义(ntd/WorkBuddy 格式,expertType=team:一名负责人 + 若干成员,各带角色定义文件)。',
|
|
790
|
+
'',
|
|
791
|
+
'用户描述:{{description}}',
|
|
792
|
+
'',
|
|
793
|
+
'第一步:用 ```json 围栏输出完整 plugin.json,字段要求:',
|
|
794
|
+
'- name: 团队 ID,只允许小写字母/数字/连字符(如 fullstack-delivery-team)',
|
|
795
|
+
'- version: "1.0.0";expertType: "team"',
|
|
796
|
+
'- description: 一句话英文简介',
|
|
797
|
+
'- displayName / profession / displayDescription: 团队级 {zh, en} 双语对象',
|
|
798
|
+
`- categoryId: 从这些值里选一个:${EXPERT_CREATE_CATEGORY_LIST}`,
|
|
799
|
+
'- tags: [{zh, en}],至少 3 个',
|
|
800
|
+
'- 成员规模 3–6 人(含负责人),按描述合理分工,成员 id 全部为小写字母/数字/连字符',
|
|
801
|
+
'- agentName: 负责人成员 id',
|
|
802
|
+
'- teamInfo: { "leadAgent": "<负责人id>", "memberAgents": ["<成员id>", ...] }(不含负责人)',
|
|
803
|
+
'- agents: ["./agents/<id>.md", ...],负责人的文件必须排在第一个',
|
|
804
|
+
'- members: [{ "id", "name": {zh,en}, "displayName": {zh,en}, "profession": {zh,en}, "role": "lead"|"member" }],id 与 agents 文件一一对应',
|
|
805
|
+
'- defaultInitPrompt: {zh, en};quickPrompts: [{zh, en}] 至少 2 条',
|
|
806
|
+
'',
|
|
807
|
+
'第二步:逐成员输出角色定义——每个成员一个 ```markdown agents/<成员id>.md 围栏(围栏起始行必须带上该文件路径),负责人的块排第一:',
|
|
808
|
+
'- 每个文件开头 YAML frontmatter:name(=成员 id)、description、color(英文颜色词)、emoji、vibe',
|
|
809
|
+
'- 正文中文撰写:身份与记忆、核心使命、专业技能、工作流程、约束规则;写明在团队中的分工与交接关系(负责人负责任务拆分与汇总)',
|
|
810
|
+
'',
|
|
811
|
+
EXPERT_CREATE_OUTPUT_RULES,
|
|
812
|
+
].join('\n')
|
|
813
|
+
|
|
814
|
+
/** 创建模板注册表:key = expertType,入口按钮只是预选条目的快捷方式。 */
|
|
815
|
+
const CREATE_TEMPLATES = {
|
|
816
|
+
agent: { key: 'agent', btnKey: 'createExpertBtn', titleKey: 'createTitleAgent', prompt: EXPERT_CREATE_PROMPT_AGENT },
|
|
817
|
+
team: { key: 'team', btnKey: 'createTeamBtn', titleKey: 'createTitleTeam', prompt: EXPERT_CREATE_PROMPT_TEAM },
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* 解析 AI 生成输出 → 创建载荷(纯函数,便于测试)。
|
|
822
|
+
* 执行器 output 是流式全文(含 [tool] 行与中间叙述),按围栏标签提取:
|
|
823
|
+
* agent 型取第一个 ```json + 第一个 ```markdown;team 型取 ```json +
|
|
824
|
+
* 全部带路径标注的 ```markdown agents/<id>.md(同名去重,按出现顺序)。
|
|
825
|
+
* 围栏缺失 → { ok:false, raw },UI 展示原文并允许重试。
|
|
826
|
+
*/
|
|
827
|
+
function parseCreateResult(output, type) {
|
|
828
|
+
const text = String(output || '')
|
|
829
|
+
const jsonMatch = text.match(/```json[ \t]*\r?\n([\s\S]*?)```/)
|
|
830
|
+
if (!jsonMatch) return { ok: false, raw: text }
|
|
831
|
+
const pluginJson = jsonMatch[1].trim()
|
|
832
|
+
if (type === 'team') {
|
|
833
|
+
const files = []
|
|
834
|
+
const re = /```markdown[ \t]+([^\s`]+)[ \t]*\r?\n([\s\S]*?)```/g
|
|
835
|
+
let m
|
|
836
|
+
while ((m = re.exec(text)) !== null) {
|
|
837
|
+
const file = m[1].trim()
|
|
838
|
+
if (!files.some((f) => f.file === file)) files.push({ file, content: m[2].trim() })
|
|
839
|
+
}
|
|
840
|
+
if (files.length === 0) return { ok: false, raw: text }
|
|
841
|
+
return { ok: true, pluginJson, files }
|
|
842
|
+
}
|
|
843
|
+
const mdMatch = text.match(/```markdown[ \t]*\r?\n([\s\S]*?)```/)
|
|
844
|
+
if (!mdMatch) return { ok: false, raw: text }
|
|
845
|
+
return { ok: true, pluginJson, agentMd: mdMatch[1].trim() }
|
|
846
|
+
}
|
|
847
|
+
|
|
711
848
|
function DetailModal({ name, source, t, onClose, onInstalled, onDeleted, onToast, onOpenSession }) {
|
|
712
849
|
const [detail, setDetail] = useState(null)
|
|
713
850
|
const [error, setError] = useState('')
|
|
@@ -1011,6 +1148,99 @@ function BuiltinSettingsDialog({ t, onClose, onToast, onSynced }) {
|
|
|
1011
1148
|
h('button', { className: 'exp-btn', 'data-primary': 'true', disabled: busy || status.syncing, onClick: sync }, busy || status.syncing ? t('syncing') : t('syncNow'))))))
|
|
1012
1149
|
}
|
|
1013
1150
|
|
|
1151
|
+
// ── AI 创建专家:对话框(消费 kit 的 params + completedView 扩展)──────────
|
|
1152
|
+
|
|
1153
|
+
const CREATE_AREA_STYLE = { width: '100%', minHeight: '200px', fontFamily: 'ui-monospace,monospace', fontSize: '12px', lineHeight: 1.6, whiteSpace: 'pre-wrap', boxSizing: 'border-box', resize: 'vertical' }
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* 创建完成态:解析执行器输出 → 可编辑预览(plugin.json + 角色定义)→ POST /api/create。
|
|
1157
|
+
* 解析失败展示原文 + 重试(ntd ExpertCreateCompleted 同款三态:可解析/不可解析/错误)。
|
|
1158
|
+
*/
|
|
1159
|
+
function CreateCompleted({ t, type, ctx, onCreated }) {
|
|
1160
|
+
const parsed = useMemo(() => parseCreateResult(ctx.output, type), [ctx.output, type])
|
|
1161
|
+
const [pluginText, setPluginText] = useState(parsed.pluginJson || '')
|
|
1162
|
+
const [agentMd, setAgentMd] = useState(parsed.agentMd || '')
|
|
1163
|
+
const [files, setFiles] = useState(Array.isArray(parsed.files) ? parsed.files : [])
|
|
1164
|
+
const [creating, setCreating] = useState(false)
|
|
1165
|
+
const [error, setError] = useState('')
|
|
1166
|
+
if (!parsed.ok) {
|
|
1167
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
1168
|
+
h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, t('createParseFailed')),
|
|
1169
|
+
h('pre', { className: 'exp-pre', style: { maxHeight: 260 } }, parsed.raw),
|
|
1170
|
+
h('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } },
|
|
1171
|
+
h('button', { className: 'exp-btn', onClick: ctx.close }, t('close')),
|
|
1172
|
+
h('button', { className: 'exp-btn', 'data-primary': 'true', onClick: ctx.retry }, t('retry'))))
|
|
1173
|
+
}
|
|
1174
|
+
let preview = null
|
|
1175
|
+
try { preview = JSON.parse(pluginText) } catch { preview = null }
|
|
1176
|
+
const leadFile = preview !== null && preview.teamInfo !== null && typeof preview.teamInfo === 'object' && typeof preview.teamInfo.leadAgent === 'string'
|
|
1177
|
+
? `agents/${preview.teamInfo.leadAgent}.md` : ''
|
|
1178
|
+
const create = async () => {
|
|
1179
|
+
setError('')
|
|
1180
|
+
let plugin
|
|
1181
|
+
try { plugin = JSON.parse(pluginText) } catch { plugin = null }
|
|
1182
|
+
if (plugin === null || typeof plugin !== 'object' || Array.isArray(plugin)) { setError(t('createInvalidJson')); return }
|
|
1183
|
+
if (plugin.expertType !== type) { setError(t('createTypeMismatch')); return }
|
|
1184
|
+
const body = { pluginJson: pluginText }
|
|
1185
|
+
if (type === 'agent') {
|
|
1186
|
+
if (typeof agentMd !== 'string' || agentMd.trim() === '') { setError(t('createEmptyFile')); return }
|
|
1187
|
+
body.agentMd = agentMd
|
|
1188
|
+
} else {
|
|
1189
|
+
if (!Array.isArray(files) || files.length === 0) { setError(t('createEmptyFile')); return }
|
|
1190
|
+
for (const f of files) {
|
|
1191
|
+
if (typeof f.content !== 'string' || f.content.trim() === '') { setError(t('createEmptyFile')); return }
|
|
1192
|
+
}
|
|
1193
|
+
body.agents = files.map((f) => ({ file: f.file, content: f.content }))
|
|
1194
|
+
}
|
|
1195
|
+
setCreating(true)
|
|
1196
|
+
try {
|
|
1197
|
+
const r = await fetchJson(`${API}/create`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
1198
|
+
onCreated((r.created && r.created.name) || plugin.name)
|
|
1199
|
+
ctx.close()
|
|
1200
|
+
} catch (e) { setError(String(e && e.message)) } finally { setCreating(false) }
|
|
1201
|
+
}
|
|
1202
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
1203
|
+
h('div', { className: 'exp-checkline' }, '✅', t('createReady')),
|
|
1204
|
+
preview !== null ? h('div', { className: 'exp-kv' },
|
|
1205
|
+
h('b', null, preview.expertType === 'team' ? t('expertTypeTeam') : t('expertTypeAgent')),
|
|
1206
|
+
h('span', null, (preview.displayName && (preview.displayName.zh || preview.displayName.en)) || preview.name || '')) : null,
|
|
1207
|
+
h('div', null,
|
|
1208
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, t('pluginJson')),
|
|
1209
|
+
h('textarea', { className: 'exp-input', value: pluginText, onChange: (e) => setPluginText(e.target.value), spellCheck: false, style: CREATE_AREA_STYLE })),
|
|
1210
|
+
type === 'agent'
|
|
1211
|
+
? h('div', null,
|
|
1212
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, t('agents')),
|
|
1213
|
+
h('textarea', { className: 'exp-input', value: agentMd, onChange: (e) => setAgentMd(e.target.value), spellCheck: false, style: CREATE_AREA_STYLE }))
|
|
1214
|
+
: h('div', null,
|
|
1215
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, t('teamAgentFiles')),
|
|
1216
|
+
...files.map((f, i) => h('div', { key: f.file, style: { marginBottom: 8 } },
|
|
1217
|
+
h('div', { className: 'exp-section-title', style: { margin: '4px 0' } }, `${f.file}${f.file !== '' && f.file === leadFile ? ' ★' : ''}`),
|
|
1218
|
+
h('textarea', { className: 'exp-input', value: f.content, onChange: (e) => setFiles(files.map((x, j) => (j === i ? { ...x, content: e.target.value } : x))), spellCheck: false, style: CREATE_AREA_STYLE })))),
|
|
1219
|
+
error !== '' ? h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, error) : null,
|
|
1220
|
+
h('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', paddingTop: 8, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
1221
|
+
h('button', { className: 'exp-btn', onClick: ctx.close }, t('close')),
|
|
1222
|
+
h('button', { className: 'exp-btn', onClick: ctx.retry }, t('retry')),
|
|
1223
|
+
h('button', { className: 'exp-btn', 'data-primary': 'true', disabled: creating, onClick: create }, creating ? t('creating') : t('createNow'))))
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/** 创建入口对话框:kit ActionShareDialog 的创建态用法(params 输入 + completedView 接管)。
|
|
1227
|
+
* 两个工具栏按钮各自预选模板条目(CREATE_TEMPLATES[type]),对话框管线与入口无关。 */
|
|
1228
|
+
function CreateExpertDialog({ t, type, onClose, onCreated, onOpenSession }) {
|
|
1229
|
+
const tpl = CREATE_TEMPLATES[type] || CREATE_TEMPLATES.agent
|
|
1230
|
+
return h(getShareDialogComponent(), {
|
|
1231
|
+
title: t(tpl.titleKey),
|
|
1232
|
+
hint: t('createHint'),
|
|
1233
|
+
params: [{ key: 'description', label: t('createParamDescription'), placeholder: t('createParamPlaceholder'), multiline: true }],
|
|
1234
|
+
initialPrompt: tpl.prompt,
|
|
1235
|
+
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') },
|
|
1236
|
+
run: (prompt) => fetchJson(`${API}/create/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }) }),
|
|
1237
|
+
poll: (id) => fetchJson(`${API}/create/run?id=${encodeURIComponent(id)}`),
|
|
1238
|
+
completedView: (ctx) => h(CreateCompleted, { t, type, ctx, onCreated }),
|
|
1239
|
+
onOpenSession: (sessionId) => { onClose(); if (onOpenSession) onOpenSession(sessionId) },
|
|
1240
|
+
onClose,
|
|
1241
|
+
})
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1014
1244
|
// ── Page ─────────────────────────────────────────────────────────────────
|
|
1015
1245
|
|
|
1016
1246
|
function ExpertsPage({ t, embedded, onClose }) {
|
|
@@ -1020,6 +1250,7 @@ function ExpertsPage({ t, embedded, onClose }) {
|
|
|
1020
1250
|
const [search, setSearch] = useState('')
|
|
1021
1251
|
const [selected, setSelected] = useState(null) // {name, source}
|
|
1022
1252
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
1253
|
+
const [createType, setCreateType] = useState(null) // null | 'agent' | 'team'
|
|
1023
1254
|
const [busyName, setBusyName] = useState(null)
|
|
1024
1255
|
const [toast, setToast] = useState(null)
|
|
1025
1256
|
const showToast = (text) => { setToast(text); setTimeout(() => setToast(null), 2600) }
|
|
@@ -1069,6 +1300,8 @@ function ExpertsPage({ t, embedded, onClose }) {
|
|
|
1069
1300
|
h('button', { className: 'exp-tab', 'data-on': tab === 'builtin', onClick: () => setTab('builtin') }, `${t('tabBuiltin')}${data ? ` (${data.builtin.length})` : ''}`)),
|
|
1070
1301
|
h('input', { className: 'exp-input exp-search', placeholder: t('searchPlaceholder'), value: search, onChange: (e) => setSearch(e.target.value) }),
|
|
1071
1302
|
h('span', { className: 'exp-count' }, `${rows.length}`),
|
|
1303
|
+
tab === 'mine' ? h('button', { className: 'exp-btn', 'data-primary': 'true', title: t('createHint'), onClick: () => setCreateType('agent') }, `⚡ ${t('createExpertBtn')}`) : null,
|
|
1304
|
+
tab === 'mine' ? h('button', { className: 'exp-btn', title: t('createHint'), onClick: () => setCreateType('team') }, `👥 ${t('createTeamBtn')}`) : null,
|
|
1072
1305
|
tab === 'builtin' ? h('button', { className: 'exp-btn', title: t('builtinSettings'), onClick: () => setSettingsOpen(true) }, t('builtinSettings')) : null),
|
|
1073
1306
|
error !== '' ? h('div', { className: 'exp-empty' }, `${t('loadFailed')}: ${error}`) : null,
|
|
1074
1307
|
data !== null && rows.length === 0 ? h('div', { className: 'exp-empty' }, tab === 'mine' ? t('mineEmpty') : t('builtinEmpty')) : null,
|
|
@@ -1087,6 +1320,17 @@ function ExpertsPage({ t, embedded, onClose }) {
|
|
|
1087
1320
|
settingsOpen ? h(BuiltinSettingsDialog, {
|
|
1088
1321
|
t, onClose: () => setSettingsOpen(false), onToast: showToast, onSynced: reload,
|
|
1089
1322
|
}) : null,
|
|
1323
|
+
createType !== null ? h(CreateExpertDialog, {
|
|
1324
|
+
t, type: createType,
|
|
1325
|
+
onClose: () => setCreateType(null),
|
|
1326
|
+
onCreated: (name) => {
|
|
1327
|
+
setCreateType(null)
|
|
1328
|
+
showToast(t('createdDone', { name }))
|
|
1329
|
+
reload()
|
|
1330
|
+
fetchRoster(true) // composer 候选立即见到新专家(60s TTL 缓存强制失效)
|
|
1331
|
+
},
|
|
1332
|
+
onOpenSession: (sessionId) => { try { sessionsApi.open(sessionId) } catch (e) { showToast(String(e && e.message)) } },
|
|
1333
|
+
}) : null,
|
|
1090
1334
|
toast !== null ? h('div', { className: 'exp-toast' }, toast) : null)
|
|
1091
1335
|
}
|
|
1092
1336
|
|
|
@@ -1099,6 +1343,7 @@ module.exports = {
|
|
|
1099
1343
|
NS, ZH, EN, matchExpert, formatSize, formatTime, avatarUrl,
|
|
1100
1344
|
EXPERT_SOURCE_NAME, makeExpertSource, openTriggerSource, fetchRoster,
|
|
1101
1345
|
toRosterRows, insertComposerText, splitRosterByType, pickerRowMatch,
|
|
1346
|
+
parseCreateResult, CREATE_TEMPLATES, EXPERT_CREATE_PROMPT_AGENT, EXPERT_CREATE_PROMPT_TEAM,
|
|
1102
1347
|
},
|
|
1103
1348
|
/** Test/host helper: mount a standalone page into any container. */
|
|
1104
1349
|
__boot(container, opts = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weibaohui/experts-management",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "dsh 插件 · 专家管理:管理 ntd 格式的专家与专家团队(plugin.json + Agent MD + 技能集),浏览/安装 50+ 内置专家市场;每个专家注册为「仅用户可调用」的技能,在对话输入框用 /expert-名称 即可以该专家的身份执行任务(宿主确定性注入角色定义与技能清单,不占用模型目录 token)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"node": ">=22.5"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@weibaohui/dsh-plugin-kit": "^0.
|
|
47
|
+
"@weibaohui/dsh-plugin-kit": "^0.3.0",
|
|
48
48
|
"yaml": "^2.9.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
package/src/index.js
CHANGED
|
@@ -343,12 +343,12 @@ function buildExpertPrompt(agentMdBody, skillsText, expert) {
|
|
|
343
343
|
|
|
344
344
|
// ── Shared route helpers ─────────────────────────────────────────────────
|
|
345
345
|
|
|
346
|
-
function readJsonBody(req) {
|
|
346
|
+
function readJsonBody(req, cap = MAX_BODY_BYTES) {
|
|
347
347
|
return new Promise((fulfil, reject) => {
|
|
348
348
|
let size = 0, chunks = []
|
|
349
349
|
req.on('data', (chunk) => {
|
|
350
350
|
size += chunk.length
|
|
351
|
-
if (size >
|
|
351
|
+
if (size > cap) { reject(new Error(`request body too large (cap ${cap} bytes)`)); req.destroy(); return }
|
|
352
352
|
chunks.push(chunk)
|
|
353
353
|
})
|
|
354
354
|
req.on('end', () => {
|
|
@@ -552,6 +552,11 @@ async function locateEditable(locateExpert, name) {
|
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
const MD_MAX_CHARS = 512 * 1024
|
|
555
|
+
// 创建端点(v0.5):plugin.json + 团队全员 agent md 一起进一个 JSON body,
|
|
556
|
+
// 64KB 的默认上限装不下多人团队,放宽到与头像上传同档
|
|
557
|
+
const CREATE_BODY_MAX_BYTES = 8 * 1024 * 1024
|
|
558
|
+
// 创建端点里 agents/<file>.md 的文件名白名单(含 agentName 兜底校验共用)
|
|
559
|
+
const AGENT_FILE_RE = /^agents\/[A-Za-z0-9][A-Za-z0-9._-]*\.md$/
|
|
555
560
|
|
|
556
561
|
// ── Module export ────────────────────────────────────────────────────────
|
|
557
562
|
|
|
@@ -1121,6 +1126,101 @@ module.exports = {
|
|
|
1121
1126
|
return
|
|
1122
1127
|
}
|
|
1123
1128
|
|
|
1129
|
+
// ── 创建端点(v0.5):AI 生成 → 预览确认 → 写入 dsh 用户库 ──
|
|
1130
|
+
// 与 share/run 共用 job 通道(createShareRunJob 进程内执行器),
|
|
1131
|
+
// cwd 固定为用户库根(生成类动作无需读盘,仅要一个存在的目录)
|
|
1132
|
+
|
|
1133
|
+
// POST /experts-management/api/create/run {prompt} → 起生成任务
|
|
1134
|
+
if (req.method === 'POST' && apiPath.endsWith('/experts-management/api/create/run')) {
|
|
1135
|
+
const body = await readJsonBody(req)
|
|
1136
|
+
if (typeof body.prompt !== 'string' || body.prompt.trim() === '') { sendJson(res, 400, { error: 'body must provide prompt' }); return }
|
|
1137
|
+
await fsP.mkdir(installedDir, { recursive: true })
|
|
1138
|
+
const binary = process.env.EXPERTS_DSH_BIN || 'dsh'
|
|
1139
|
+
const job = createShareRunJob({ binary, prompt: body.prompt, dir: installedDir, jobs: shareRunJobs, logger: ctx.logger, services: shareServices })
|
|
1140
|
+
sendJson(res, 202, { jobId: job.id, status: job.status })
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// GET /experts-management/api/create/run?id= → 生成任务状态/输出
|
|
1145
|
+
if (req.method === 'GET' && apiPath.endsWith('/experts-management/api/create/run')) {
|
|
1146
|
+
const id = query.get('id') || ''
|
|
1147
|
+
const job = shareRunJobs.get(id)
|
|
1148
|
+
if (job === undefined) { sendJson(res, 404, { error: 'job not found' }); return }
|
|
1149
|
+
sendJson(res, 200, { ...job, output: job.output.slice(-32 * 1024) })
|
|
1150
|
+
return
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// POST /experts-management/api/create {pluginJson, agentMd?} | {pluginJson, agents?: [{file, content}]}
|
|
1154
|
+
// agent 型:pluginJson + 单个 agentMd;team 型:pluginJson + 逐成员 agents[]。
|
|
1155
|
+
// 全量先验后写:任一校验失败不落盘;写失败清理半成品目录(ntd create_expert 同款)。
|
|
1156
|
+
if (req.method === 'POST' && apiPath.endsWith('/experts-management/api/create')) {
|
|
1157
|
+
const body = await readJsonBody(req, CREATE_BODY_MAX_BYTES)
|
|
1158
|
+
let plugin
|
|
1159
|
+
try { plugin = JSON.parse(typeof body.pluginJson === 'string' ? body.pluginJson : '') } catch { throw new Error('pluginJson is not valid JSON') }
|
|
1160
|
+
if (plugin === null || typeof plugin !== 'object' || Array.isArray(plugin)) throw new Error('pluginJson must be a JSON object')
|
|
1161
|
+
const name = typeof plugin.name === 'string' ? plugin.name : ''
|
|
1162
|
+
// 注册表硬约束:非 kebab 名的专家会被 skill provider 静默跳过(/expert- 手势永远出不来),必须在创建时就拒绝
|
|
1163
|
+
if (!KEBAB_NAME_RE.test(name)) throw new Error(`expert name must be lowercase kebab-case, got '${name}' (registers the /expert-${name || '?'} gesture)`)
|
|
1164
|
+
if (plugin.expertType !== 'agent' && plugin.expertType !== 'team') throw new Error("expertType must be 'agent' or 'team'")
|
|
1165
|
+
// provider 对空描述的专家同样静默跳过(describe() 为空不入目录)
|
|
1166
|
+
const hasText = (v) => typeof v === 'string' && v.trim() !== ''
|
|
1167
|
+
const describeOk = hasText(plugin.description)
|
|
1168
|
+
|| (plugin.profession !== null && typeof plugin.profession === 'object' && (hasText(plugin.profession.zh) || hasText(plugin.profession.en)))
|
|
1169
|
+
|| (plugin.displayDescription !== null && typeof plugin.displayDescription === 'object' && (hasText(plugin.displayDescription.zh) || hasText(plugin.displayDescription.en)))
|
|
1170
|
+
if (!describeOk) throw new Error('expert needs a non-empty description/profession/displayDescription (the skill registry skips empty ones)')
|
|
1171
|
+
|
|
1172
|
+
const target = join(installedDir, name)
|
|
1173
|
+
const exists = await fsP.stat(target).then(() => true).catch(() => false)
|
|
1174
|
+
if (exists) throw new Error(`expert '${name}' already exists in the dsh library`)
|
|
1175
|
+
|
|
1176
|
+
const files = []
|
|
1177
|
+
if (plugin.expertType === 'agent') {
|
|
1178
|
+
const agentMd = typeof body.agentMd === 'string' ? body.agentMd : ''
|
|
1179
|
+
if (agentMd.trim() === '') throw new Error('agentMd must be a non-empty string')
|
|
1180
|
+
if (agentMd.length > MD_MAX_CHARS) throw new Error(`agentMd exceeds ${MD_MAX_CHARS} chars`)
|
|
1181
|
+
const agentName = hasText(plugin.agentName) ? plugin.agentName : name
|
|
1182
|
+
if (!AGENT_FILE_RE.test(`agents/${agentName}.md`)) throw new Error(`agentName must be plain ascii id, got '${agentName}'`)
|
|
1183
|
+
plugin.agentName = agentName
|
|
1184
|
+
plugin.agents = [`./agents/${agentName}.md`]
|
|
1185
|
+
files.push({ rel: `agents/${agentName}.md`, content: agentMd })
|
|
1186
|
+
} else {
|
|
1187
|
+
const list = Array.isArray(body.agents) ? body.agents : []
|
|
1188
|
+
if (list.length === 0) throw new Error('agents must be a non-empty array of {file, content}')
|
|
1189
|
+
if (list.length > 20) throw new Error('agents supports at most 20 member files')
|
|
1190
|
+
const agentRels = []
|
|
1191
|
+
const stems = new Set()
|
|
1192
|
+
const seenFiles = new Set()
|
|
1193
|
+
for (const item of list) {
|
|
1194
|
+
const file = typeof item === 'object' && item !== null && typeof item.file === 'string' ? item.file : ''
|
|
1195
|
+
if (!AGENT_FILE_RE.test(file)) throw new Error(`invalid agent file name: '${file}' (expected agents/<id>.md)`)
|
|
1196
|
+
if (seenFiles.has(file)) throw new Error(`duplicate agent file: '${file}'`)
|
|
1197
|
+
seenFiles.add(file)
|
|
1198
|
+
const content = typeof item.content === 'string' ? item.content : ''
|
|
1199
|
+
if (content.trim() === '') throw new Error(`agent file '${file}' content must be non-empty`)
|
|
1200
|
+
if (content.length > MD_MAX_CHARS) throw new Error(`agent file '${file}' exceeds ${MD_MAX_CHARS} chars`)
|
|
1201
|
+
stems.add(basename(file).replace(/\.md$/, ''))
|
|
1202
|
+
agentRels.push(`./${file}`)
|
|
1203
|
+
files.push({ rel: file, content })
|
|
1204
|
+
}
|
|
1205
|
+
const lead = plugin.teamInfo !== null && typeof plugin.teamInfo === 'object' && typeof plugin.teamInfo.leadAgent === 'string' ? plugin.teamInfo.leadAgent : ''
|
|
1206
|
+
if (!stems.has(lead)) throw new Error(`teamInfo.leadAgent '${lead || '(missing)'}' must match one of the agents files`)
|
|
1207
|
+
plugin.agentName = lead
|
|
1208
|
+
plugin.agents = agentRels
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
try {
|
|
1212
|
+
await fsP.mkdir(join(target, '.codebuddy-plugin'), { recursive: true })
|
|
1213
|
+
await atomicWriteJs(join(target, PLUGIN_JSON_REL), JSON.stringify(plugin, null, 2))
|
|
1214
|
+
for (const f of files) await atomicWriteJs(join(target, f.rel), f.content)
|
|
1215
|
+
} catch (e) {
|
|
1216
|
+
await fsP.rm(target, { recursive: true, force: true }).catch(() => {})
|
|
1217
|
+
throw e
|
|
1218
|
+
}
|
|
1219
|
+
invalidate()
|
|
1220
|
+
sendJson(res, 201, { created: { name, expertType: plugin.expertType, dir: displayPath(target), agents: files.map((f) => f.rel) } })
|
|
1221
|
+
return
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1124
1224
|
// GET /experts-management/api/builtin/status
|
|
1125
1225
|
if (req.method === 'GET' && apiPath.endsWith('/experts-management/api/builtin/status')) {
|
|
1126
1226
|
await builtinStateLoaded
|