dsh-plugin-tool-management 0.18.5 → 0.19.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/CHANGELOG.md +25 -0
- package/README.md +48 -46
- package/README_EN.md +64 -65
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +18 -0
- package/lib/client.js +1054 -831
- package/lib/context-inject-frame.js +13 -0
- package/lib/context-inject-select.js +4 -3
- package/lib/context-inject.js +4 -3
- package/lib/index.js +60 -19
- package/lib/ops/candidates.js +1 -1
- package/lib/scenes/candidates.js +162 -69
- package/lib/settings-sidecars.js +2 -1
- package/lib/subagents/catalog.js +2 -2
- package/lib/subagents/persona-parse.js +36 -1
- package/lib/subagents/persona-write.js +7 -1
- package/lib/subagents/run-provider.js +55 -21
- package/lib/subagents/service.js +3 -3
- package/lib/subagents/tools.js +17 -2
- package/lib/tools/subagent.js +13 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -291,12 +291,29 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
|
|
|
291
291
|
.dsm-adv-caret{flex:none;color:var(--dsw-alias-label-tertiary);font-size:10px}
|
|
292
292
|
/* 「高级选项」永不折行:它在收起时与两行摘要并排,被挤成两字一行就不再像个标题了。 */
|
|
293
293
|
.dsm-adv-title{flex:none;white-space:nowrap}
|
|
294
|
-
/*
|
|
294
|
+
/* 折叠摘要最多两行(第一行模型 / 思考强度 / 允许追问,第二行工具限制;没有值的那一行不出现),
|
|
295
|
+
靠 align-items:center 与标题垂直居中。 */
|
|
295
296
|
.dsm-adv-sum{display:flex;min-width:0;flex-direction:column;gap:1px}
|
|
296
|
-
.dsm-adv-body{display:flex;flex-direction:column;gap:
|
|
297
|
+
.dsm-adv-body{display:flex;flex-direction:column;gap:16px;padding:11px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
298
|
+
/* 分组(0.19.0 重排):组标题 + 一条横贯的细线,组间距靠 .dsm-adv-body 的 16px。
|
|
299
|
+
重排前是五组平铺、只靠 12px gap 分隔,视觉上连成一片 —— 分组是这次清理的主手段。 */
|
|
300
|
+
.dsm-adv-group{display:flex;flex-direction:column;gap:10px}
|
|
301
|
+
.dsm-adv-group-head{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-primary);font-size:12px;font-weight:570}
|
|
302
|
+
.dsm-adv-group-head::after{content:'';height:1px;flex:1;background:var(--dsw-alias-border-l1)}
|
|
303
|
+
/* 半宽字段:与 .dsm-field-row 的列宽同源(calc(50% - 6px) = 12px gap 的一半),
|
|
304
|
+
所以单独一个半宽字段和并排两个的列边界对齐。 */
|
|
305
|
+
.dsm-adv-half{width:calc(50% - 6px)}
|
|
306
|
+
/* 下拉在 .dsm-combo-row 里必须自己撑满整格。缺这条时 .dsm-select 是 flex:0 1 auto、宽度
|
|
307
|
+
由**选项文字**撑出:收窄来源后标签变短(去掉了「来源 · 」前缀、候选只剩几条),下拉当场
|
|
308
|
+
跟着缩 —— 用户 2026-10-07 截图「模型来源选择后,模型框变短了」。实测(headless Chrome,
|
|
309
|
+
字段宽 251):标签带前缀 251 / 去前缀 251 / 只剩 id 162 / 只有一个模型 135,加了这条全部 251。
|
|
310
|
+
min-width:0 是配套的:flex 项默认 min-width:auto = min-content,不放宽就压不到整格以下。 */
|
|
311
|
+
.dsm-combo-row .dsm-select{flex:1;min-width:0}
|
|
297
312
|
.dsm-adv-note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
|
|
298
313
|
/* ── 工具限制四行(人设表单:一行一个 Agent 预设,白/黑名单互斥、默认折叠)──────── */
|
|
299
|
-
|
|
314
|
+
/* 无 margin-top:这个名单原先紧跟在自己的字段 label 后面,靠 6px 拉开;0.19.0 重排后
|
|
315
|
+
label 撤掉、由分组头承担,间距交给 .dsm-adv-group 的 gap。 */
|
|
316
|
+
.dsm-modes{display:flex;flex-direction:column;gap:6px}
|
|
300
317
|
.dsm-mode-row{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}
|
|
301
318
|
.dsm-mode-row.dsm-mode-on{border-color:var(--dsw-alias-border-l3)}
|
|
302
319
|
.dsm-mode-head{display:flex;min-height:38px;align-items:center;gap:9px;padding:0 10px}
|
|
@@ -3807,7 +3824,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3807
3824
|
"compat.live.state.child": "不在本会话注入",
|
|
3808
3825
|
"compat.live.state.error": "取数失败",
|
|
3809
3826
|
"compat.live.state.error.title": "这个域这一轮取数抛了异常(内容还在,是这一步没取到)—— 兼容页「上下文注入通道」有一行降级写着原因。它不会误发「已清空」,下一步会自动重试。",
|
|
3810
|
-
"compat.live.state.child.title": "
|
|
3827
|
+
"compat.live.state.child.title": "本会话是子代理会话,这个域对子会话不成立 —— 两种情形:① **场景与记忆**只在顶层注入(它们说的是父会话的处境,子代理要什么由父代理写进委派说明);② **人设目录**的注入深度不够(本会话深度超过该人设的 `catalogDepth`,默认 1 = 只在顶层),这一种可以把人设的「目录注入」调大或选「不限制嵌套」。两种都不影响委派 —— 子代理始终可以继续委派。",
|
|
3811
3828
|
"compat.live.adopt": "注入 {injected} 次 · 调用 {used} 次 · 采纳 {adopted} 次",
|
|
3812
3829
|
"compat.live.adopt.none": "注入 {injected} 次 · 从未调用",
|
|
3813
3830
|
"compat.live.adopt.title": "只数**最近这一段对话**(换一段重新数,重启归零)。采纳 = 调用发生时该域正文正在这个会话的上下文里;调用次数不区分成功失败",
|
|
@@ -3918,15 +3935,32 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3918
3935
|
"subagents.create": "新建人设", "subagents.edit": "编辑人设",
|
|
3919
3936
|
"subagents.field.name": "人设名",
|
|
3920
3937
|
"subagents.field.description": "描述", "subagents.field.description.placeholder": "例如 擅长 Java 后端实现与重构",
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
//
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
"subagents.field.
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3938
|
+
// 模型来源 / 模型 / 目录注入三格的静态说明(provider.hint、catalogDepth.hint)随 0.19.0
|
|
3939
|
+
// 重排移出表单:来源与模型并排成一组,谁跟谁是一对由位置表达;目录注入那条只说了半句
|
|
3940
|
+
// "不影响委派",半句不解释"那影响什么",留着比不留更费解。
|
|
3941
|
+
"subagents.field.model": "模型", "subagents.field.provider": "模型来源",
|
|
3942
|
+
"subagents.field.offCatalog": "{id} · 目录里没有",
|
|
3943
|
+
"subagents.field.catalogDepth": "目录注入", "subagents.field.catalogDepth.onlyTop": "只在顶层", "subagents.field.catalogDepth.toChild": "顶层和子会话", "subagents.field.catalogDepth.toGrand": "顶层和两层子会话", "subagents.field.catalogDepth.unlimited": "不限制嵌套",
|
|
3944
|
+
// 允许追问(0.19.0)。界面只留复选框副标题那一行 —— 机制(官方 startContinuable / 结算通知 /
|
|
3945
|
+
// 返回指引)与代价(占用可继续子代理名额、需要持久化与 session query)留在 CHANGELOG 与
|
|
3946
|
+
// README(2026-10-07 用户裁定:高级选项里的提示句太密,长说明一律不进表单)。
|
|
3947
|
+
"subagents.field.continuable": "允许追问(后台运行)", "subagents.field.continuable.short": "回执一个 id,之后还能继续对话",
|
|
3948
|
+
// 收起态摘要用的两个「尾巴」。分隔号写在值里(而不是在代码里 join ';'):英文侧要用
|
|
3949
|
+
// "; ",中文用「;」,写进词典才不用为语言改代码。空串 = 这一项没值,整段不出现。
|
|
3950
|
+
"subagents.adv.continuable": ";允许追问(后台运行)",
|
|
3951
|
+
"subagents.adv.legacy": ";旧格式:白名单 {allow} / 黑名单 {deny}",
|
|
3952
|
+
// 思考强度(0.14.0)。界面上只留"这一格现在能选什么"与**状态**(未选模型 / 拉取中 / 拉不到),
|
|
3953
|
+
// 不解释机制 —— 官方语义(改 provider/model 会丢掉继承值、不支持的档位直接拒)记在
|
|
3954
|
+
// CHANGELOG / update.md,不搬进表单。needModel 保留但压到最短:它解释的是"这一格为什么灰着"。
|
|
3955
|
+
"subagents.field.effort": "思考强度", "subagents.effort.default": "默认(模型的默认强度)", "subagents.effort.none": "默认", "subagents.effort.needModel": "先选模型", "subagents.effort.loading": "正在问该模型支持哪些档位…", "subagents.effort.unavailable": "该模型没有暴露可选档位,默认思考强度", "subagents.effort.dropped": "换模型后「{effort}」不在新模型的档位里,已清空。",
|
|
3956
|
+
// 来源下拉的空选项。措辞刻意与模型那条不同("继承主会话"):两者含义一致,但并排两个
|
|
3957
|
+
// 下拉出现同一句话会让人以为点错了格。这一句同时接住被撤掉的那条静态说明
|
|
3958
|
+
// (provider.hint「只填模型会落在主会话的来源上」)要说的事。
|
|
3959
|
+
"subagents.provider.inherit": "不指定(用主会话的来源)",
|
|
3960
|
+
"subagents.model.inherit": "继承主会话(不指定)",
|
|
3961
|
+
// 标题改由高级选项的分组头承担(subagents.adv.group.tools),字段自己的 label 撤掉 ——
|
|
3962
|
+
// 同一句话在 10px 内说两遍("工具限制" + "工具限制(按 Agent 预设)")。
|
|
3963
|
+
"subagents.field.modes.hint": "只有当前模式那一行生效;MCP 工具不在候选里,始终可用",
|
|
3930
3964
|
"subagents.mode.startAllow": "启动白名单", "subagents.mode.startDeny": "启动黑名单", "subagents.mode.stopAllow": "关闭白名单", "subagents.mode.stopDeny": "关闭黑名单",
|
|
3931
3965
|
"subagents.mode.off": "未限制", "subagents.mode.empty": "已启动但没勾选(= 不限制)",
|
|
3932
3966
|
"subagents.mode.allowOn": "白名单 {count} 个", "subagents.mode.denyOn": "黑名单 {count} 个",
|
|
@@ -3939,8 +3973,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3939
3973
|
"subagents.legacy.note": "这个文件里还有一份旧写法的工具限制(白名单 {allow} 个 / 黑名单 {deny} 个),它对所有模式都生效。",
|
|
3940
3974
|
"subagents.legacy.convert": "转换到按模式设置",
|
|
3941
3975
|
"subagents.legacy.hint": "转换会把旧名单搬进你选的模式;不点它,文件里的旧键原样保留(运行时照旧生效)。",
|
|
3942
|
-
"subagents.tools.filter": "筛选工具名", "subagents.tools.remove": "移除", "subagents.toolFailed": "子代理工具没注册上:{names}({reason})。这几个工具在子智能体流程里用不了;重启 DSH 后仍如此请核对宿主版本。",
|
|
3943
|
-
|
|
3976
|
+
"subagents.tools.filter": "筛选工具名", "subagents.tools.remove": "移除", "subagents.tools.group.plugin": "插件工具", "subagents.tools.group.official": "官方工具", "subagents.tools.group.other": "其他工具", "subagents.toolFailed": "子代理工具没注册上:{names}({reason})。这几个工具在子智能体流程里用不了;重启 DSH 后仍如此请核对宿主版本。",
|
|
3977
|
+
// 收起态摘要最多两行、只报有值的项:第一行模型 / 思考强度(+ 允许追问),第二行工具限制。
|
|
3978
|
+
// 目录注入不进摘要 —— 它只决定目录出现在哪些会话,不影响委派本身。
|
|
3979
|
+
"subagents.adv.title": "高级选项", "subagents.adv.inherit": "继承主会话", "subagents.adv.summary": "模型:{model};思考强度:{effort}{tail}", "subagents.adv.summary2": "模式限制:{modes} 项{tail}", "subagents.adv.loadFailed": "读取候选数据失败(模型目录 / 工具清单);仍可手动填写。", "subagents.adv.group.model": "模型", "subagents.adv.group.runtime": "运行方式", "subagents.adv.group.tools": "工具限制",
|
|
3944
3980
|
"preset.name.standard": "标准模式", "preset.name.ptc": "PTC模式", "preset.name.minimal": "极简模式", "preset.name.cordis": "创造模式",
|
|
3945
3981
|
"preset.short.standard": "标准", "preset.short.ptc": "PTC", "preset.short.minimal": "极简", "preset.short.cordis": "创造",
|
|
3946
3982
|
"subagents.field.body": "人设提示词", "subagents.field.body.placeholder": "写下这个人设的身份、职责与工作方式…",
|
|
@@ -4426,7 +4462,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4426
4462
|
"compat.live.state.child": "Not injected here",
|
|
4427
4463
|
"compat.live.state.error": "Failed to load",
|
|
4428
4464
|
"compat.live.state.error.title": "This domain threw while producing its text this round (the content exists; this step just could not read it). The Compat page's \"Context injection channel\" row carries a degraded note with the reason. No false \"cleared\" notice is sent, and the next step retries automatically.",
|
|
4429
|
-
"compat.live.state.child.title": "This session is
|
|
4465
|
+
"compat.live.state.child.title": "This session is a subagent session and this domain does not apply to it — two cases: (1) **scene and memory** are injected at the top level only (they describe the parent session's situation; what a subagent needs belongs in the delegation brief); (2) the **persona catalog**'s injection depth is too shallow (this session is deeper than that persona's `catalogDepth`, default 1 = top level only) — raise that persona's catalog injection depth or pick “No nesting limit”. Neither affects delegation — subagents can always delegate further.",
|
|
4430
4466
|
"compat.live.adopt": "injected {injected} · called {used} · adopted {adopted}",
|
|
4431
4467
|
"compat.live.adopt.none": "injected {injected} · never called",
|
|
4432
4468
|
"compat.live.adopt.title": "Counts the most recent conversation only (it restarts when you switch, and on every launch). Adopted = the domain text was in context when the call happened; calls are counted whether or not they succeeded",
|
|
@@ -4537,15 +4573,37 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4537
4573
|
"subagents.create": "New persona", "subagents.edit": "Edit persona",
|
|
4538
4574
|
"subagents.field.name": "Persona name",
|
|
4539
4575
|
"subagents.field.description": "Description", "subagents.field.description.placeholder": "e.g. Senior Java engineer",
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
//
|
|
4543
|
-
//
|
|
4544
|
-
|
|
4545
|
-
"subagents.field.
|
|
4546
|
-
"subagents.
|
|
4547
|
-
|
|
4548
|
-
|
|
4576
|
+
// The static help lines for provider / model / catalog depth left the form with the 0.19.0
|
|
4577
|
+
// re-layout: provider and model now sit side by side as one group, so the pairing is carried
|
|
4578
|
+
// by position; the catalog-depth line only stated half a fact ("affects the catalog only")
|
|
4579
|
+
// without saying what it does affect, which is more confusing than saying nothing.
|
|
4580
|
+
"subagents.field.model": "Model", "subagents.field.provider": "Model provider",
|
|
4581
|
+
"subagents.field.offCatalog": "{id} · not in the catalogue",
|
|
4582
|
+
"subagents.field.catalogDepth": "Catalog injection", "subagents.field.catalogDepth.onlyTop": "Top level only", "subagents.field.catalogDepth.toChild": "Top level and subagents", "subagents.field.catalogDepth.toGrand": "Top level and two levels down", "subagents.field.catalogDepth.unlimited": "No nesting limit",
|
|
4583
|
+
// Continuable (0.19.0). The form keeps only the checkbox subtitle; the mechanism (the
|
|
4584
|
+
// official startContinuable, the settlement notice, the return guidance) and the cost
|
|
4585
|
+
// (capacity slot, persistence + session query) live in CHANGELOG and README, not here
|
|
4586
|
+
// (user ruling 2026-10-07: the advanced section carried far too many help sentences).
|
|
4587
|
+
"subagents.field.continuable": "Continuable (runs in the background)", "subagents.field.continuable.short": "Returns an id you can keep talking to",
|
|
4588
|
+
// The two "tails" of the collapsed summary. The separator lives inside the value (not a
|
|
4589
|
+
// hard-coded join in code): English needs "; " where Chinese needs ";". Empty = no value,
|
|
4590
|
+
// so the whole clause disappears.
|
|
4591
|
+
"subagents.adv.continuable": "; continuable (runs in the background)",
|
|
4592
|
+
"subagents.adv.legacy": "; legacy allow: {allow} / deny: {deny}",
|
|
4593
|
+
// Reasoning effort (0.14.0). The form keeps only what this field offers right now plus the
|
|
4594
|
+
// transient states (no model picked / loading / none available); the official semantics (the
|
|
4595
|
+
// host drops the inherited level when provider/model change, and an unsupported level is
|
|
4596
|
+
// rejected rather than clamped) live in CHANGELOG / update.md, not here.
|
|
4597
|
+
"subagents.field.effort": "Reasoning effort", "subagents.effort.default": "Default (the model's default)", "subagents.effort.none": "default", "subagents.effort.needModel": "Pick a model first", "subagents.effort.loading": "Asking the model which levels it supports…", "subagents.effort.unavailable": "This model exposes no selectable levels — default reasoning effort.", "subagents.effort.dropped": "“{effort}” is not among the new model's levels, so it was cleared.",
|
|
4598
|
+
// The blank entry of the provider dropdown. Deliberately worded differently from the model
|
|
4599
|
+
// one ("Inherit the main session"): same meaning, but the same sentence in two adjacent
|
|
4600
|
+
// dropdowns reads like a mis-click. This line also carries what the removed static hint
|
|
4601
|
+
// (provider.hint) used to say.
|
|
4602
|
+
"subagents.provider.inherit": "Unspecified (main session's provider)",
|
|
4603
|
+
"subagents.model.inherit": "Inherit the main session (unspecified)",
|
|
4604
|
+
// The heading moved to the advanced section's group head (subagents.adv.group.tools) and the
|
|
4605
|
+
// field's own label was dropped — the same phrase twice within 10px otherwise.
|
|
4606
|
+
"subagents.field.modes.hint": "Only the current preset's row applies; MCP tools are not listed and always stay available",
|
|
4549
4607
|
"subagents.mode.startAllow": "Enable allowlist", "subagents.mode.startDeny": "Enable denylist", "subagents.mode.stopAllow": "Disable allowlist", "subagents.mode.stopDeny": "Disable denylist",
|
|
4550
4608
|
"subagents.mode.off": "no restriction", "subagents.mode.empty": "enabled with nothing picked (= no restriction)",
|
|
4551
4609
|
"subagents.mode.allowOn": "allowlist {count}", "subagents.mode.denyOn": "denylist {count}",
|
|
@@ -4558,8 +4616,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4558
4616
|
"subagents.legacy.note": "This file still carries the old-style tool limits (allowlist {allow} / denylist {deny}); they apply to every preset.",
|
|
4559
4617
|
"subagents.legacy.convert": "Convert into a preset row",
|
|
4560
4618
|
"subagents.legacy.hint": "Converting moves the old list into the preset you pick; until then the old keys stay untouched in the file (and keep working at run time).",
|
|
4561
|
-
"subagents.tools.filter": "Filter tool names", "subagents.tools.remove": "Remove", "subagents.toolFailed": "Subagent tools failed to register: {names} ({reason}). They cannot be used in the subagent flow; if this persists after a DSH restart, check the host version.",
|
|
4562
|
-
|
|
4619
|
+
"subagents.tools.filter": "Filter tool names", "subagents.tools.remove": "Remove", "subagents.tools.group.plugin": "Plugin tools", "subagents.tools.group.official": "Official tools", "subagents.tools.group.other": "Other tools", "subagents.toolFailed": "Subagent tools failed to register: {names} ({reason}). They cannot be used in the subagent flow; if this persists after a DSH restart, check the host version.",
|
|
4620
|
+
// The collapsed summary is at most two lines and reports only what has a value: line 1 is
|
|
4621
|
+
// model / effort (+ continuable), line 2 is the tool limits. Catalog injection is left out —
|
|
4622
|
+
// it only decides which sessions see the catalogue, not how a delegation runs.
|
|
4623
|
+
"subagents.adv.title": "Advanced options", "subagents.adv.inherit": "inherit", "subagents.adv.summary": "model: {model}; effort: {effort}{tail}", "subagents.adv.summary2": "preset limits: {modes}{tail}", "subagents.adv.loadFailed": "Could not read the candidate data (model catalogue / tool list); you can still fill the fields by hand.", "subagents.adv.group.model": "Model", "subagents.adv.group.runtime": "Runtime", "subagents.adv.group.tools": "Tool limits",
|
|
4563
4624
|
"preset.name.standard": "Standard mode", "preset.name.ptc": "PTC mode", "preset.name.minimal": "Minimal mode", "preset.name.cordis": "Creator mode",
|
|
4564
4625
|
"preset.short.standard": "Standard", "preset.short.ptc": "PTC", "preset.short.minimal": "Minimal", "preset.short.cordis": "Creator",
|
|
4565
4626
|
"subagents.field.body": "Persona prompt", "subagents.field.body.placeholder": "Describe the persona's role, responsibilities, and working style…",
|
|
@@ -10040,810 +10101,972 @@ function enabledMemoryCount(rules) {
|
|
|
10040
10101
|
}) : null,
|
|
10041
10102
|
)
|
|
10042
10103
|
}
|
|
10043
|
-
// ---------- 子智能体页:人设文件(~/.dsh/subagents/*.md)管理 ----------
|
|
10044
|
-
/**
|
|
10045
|
-
* 「思考强度」档位的按 (provider, model) 缓存。
|
|
10046
|
-
*
|
|
10047
|
-
* 为什么放在**组件外**:放进组件里每次渲染都会重建,等于没有缓存,而档位清单要问
|
|
10048
|
-
* adapter(`llm.resolveModelInfo`,异步、可能联网)—— 编辑人设时来回切模型不该每次都问。
|
|
10049
|
-
* TTL 5 分钟:档位是模型能力声明,短时间内不会变;换模型时才重新问。
|
|
10050
|
-
*/
|
|
10051
|
-
var EFFORT_CACHE_TTL_MS = 5 * 60 * 1000
|
|
10052
|
-
var effortCacheByRoute = {}
|
|
10053
|
-
function readEffortCache(key) {
|
|
10054
|
-
var hit = effortCacheByRoute[key]
|
|
10055
|
-
if (!hit) return null
|
|
10056
|
-
if (Date.now() - hit.at > EFFORT_CACHE_TTL_MS) { delete effortCacheByRoute[key]; return null }
|
|
10057
|
-
return hit.value
|
|
10058
|
-
}
|
|
10059
|
-
function writeEffortCache(key, value) { effortCacheByRoute[key] = { at: Date.now(), value: value } }
|
|
10060
|
-
function SubagentsPage() {
|
|
10061
|
-
var state = React.useState({ loading: true, error: null, subagents: [] })
|
|
10062
|
-
var data = state[0], setData = state[1]
|
|
10063
|
-
var bs = React.useState(false)
|
|
10064
|
-
var busy = bs[0], setBusy = bs[1]
|
|
10065
|
-
var rs = React.useState(null)
|
|
10066
|
-
var result = rs[0], setResult = rs[1]
|
|
10067
|
-
var ms = React.useState(null)
|
|
10068
|
-
var modal = ms[0], setModal = ms[1]
|
|
10069
|
-
/**
|
|
10070
|
-
* 「思考强度」的加载状态。key = 已拉取/正在拉取的 `provider\0model`(空串 = 还没选模型)。
|
|
10071
|
-
* 与 `cands` 分开两个 state:档位是**按模型**懒加载的,而候选目录是一次性拉的,
|
|
10072
|
-
* 混在一起会让"切换模型"误触发整份候选重拉。
|
|
10073
|
-
*/
|
|
10074
|
-
var es = React.useState({ key: '', loading: false, error: null, efforts: [], defaultEffort: null })
|
|
10075
|
-
var eff = es[0], setEff = es[1]
|
|
10076
|
-
/** 换模型导致已存档位被清掉时的一次性提示(下次换模型或关弹窗即消失)。 */
|
|
10077
|
-
var ens = React.useState(null)
|
|
10078
|
-
var effNotice = ens[0], setEffNotice = ens[1]
|
|
10079
|
-
// 搜索词独立一处:`data` 在 refresh 时被整对象替换,寄在它里面会被刷新清空。
|
|
10080
|
-
// 过滤用防抖值,输入框仍绑原值(与提示词页同一写法)。
|
|
10081
|
-
var qs = React.useState('')
|
|
10082
|
-
var query = qs[0], setQuery = qs[1]
|
|
10083
|
-
var dq = useDebouncedValue(query)
|
|
10084
|
-
// 场景锁定(v0.8):任一场景锁定 = 五个域整体冻结;本页全部写控件禁用。
|
|
10085
|
-
var anyLocked = data.anyLocked === true
|
|
10086
|
-
// 场景内开关由档案定义:人设开关置灰(要改就去场景页的档案编辑器里改)。
|
|
10087
|
-
var sceneName = data.activeScene || null
|
|
10088
|
-
// 回收站:删除人设 = 移入回收站(宿主侧 subagent-delete),这里列出/恢复/永久删除。
|
|
10089
|
-
var ts = React.useState(null)
|
|
10090
|
-
var trash = ts[0], setTrash = ts[1]
|
|
10091
|
-
var tb = React.useState(false)
|
|
10092
|
-
var trashBusy = tb[0], setTrashBusy = tb[1]
|
|
10093
|
-
var flipRef = React.useRef(null)
|
|
10094
|
-
useFlipReorder(flipRef)
|
|
10095
|
-
function loadTrash(keepOpen) {
|
|
10096
|
-
setTrash(Object.assign({ loading: true, error: null, entries: [] }, keepOpen ? trash || {} : {}))
|
|
10097
|
-
apiCall('subagent-trash-list', {}).then(function (res) {
|
|
10098
|
-
if (res && res.ok) setTrash({ loading: false, error: null, entries: res.trash || [] })
|
|
10099
|
-
else setTrash({ loading: false, error: (res && res.error) || t('trash.loadFailed'), entries: [] })
|
|
10100
|
-
}).catch(function (e) { setTrash({ loading: false, error: errMsg(e), entries: [] }) })
|
|
10101
|
-
}
|
|
10102
|
-
function restorePersona(item) {
|
|
10103
|
-
setTrashBusy(true)
|
|
10104
|
-
apiCall('subagent-trash-restore', { id: item.id }).then(function (res) {
|
|
10105
|
-
setTrashBusy(false)
|
|
10106
|
-
if (res && res.ok) { var sw = stateSyncWarn(t, res); setResult({ ok: !sw, warning: !!sw, text: t('trash.restored', { name: item.name }) + (sw ? ' ' + sw : '') }); refresh(true); loadTrash(true) }
|
|
10107
|
-
else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
|
|
10108
|
-
}).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
|
|
10109
|
-
}
|
|
10110
|
-
function purgePersona(item) {
|
|
10111
|
-
setTrashBusy(true)
|
|
10112
|
-
apiCall('subagent-trash-delete', { id: item.id }).then(function (res) {
|
|
10113
|
-
setTrashBusy(false)
|
|
10114
|
-
if (res && res.ok) { setResult({ ok: true, text: t('trash.purged', { name: item.name }) }); loadTrash(true) }
|
|
10115
|
-
else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
|
|
10116
|
-
}).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
|
|
10117
|
-
}
|
|
10118
|
-
React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
|
|
10119
|
-
function refresh(silent) {
|
|
10120
|
-
if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
|
|
10121
|
-
apiCall('subagent-list', {}).then(function (r) {
|
|
10122
|
-
// `activeScene` 必须一起搬进来:这里是把 `data` 整对象替换掉(搜索词另存就是为了
|
|
10123
|
-
// 不被冲掉),漏一个字段等于那个字段永远是初始值 —— 场景横幅因此从来没显示过
|
|
10124
|
-
// (2026-09-19 用户报的:技能 / 子智能体 / 记忆三页都看不到"当前处于场景…")。
|
|
10125
|
-
if (r && r.ok) setData({ loading: false, error: null, subagents: r.subagents || [], anyLocked: r.anyLocked === true, activeScene: r.activeScene || null, toolFailures: Array.isArray(r.toolFailures) ? r.toolFailures : [] })
|
|
10126
|
-
else setData({ loading: false, error: translateError(t, r), subagents: [], toolFailures: [] })
|
|
10127
|
-
}).catch(function (e) { setData({ loading: false, error: errMsg(e), subagents: [], toolFailures: [] }) })
|
|
10128
|
-
}
|
|
10129
|
-
React.useEffect(function () { refresh() }, [])
|
|
10130
|
-
/**
|
|
10131
|
-
* 子智能体开关(与记忆页同一套交互):停用 = 不注入目录段、subagent_manager_list/run
|
|
10132
|
-
* 不可见;人设文件不动。开关联动全局状态,立即提交。
|
|
10133
|
-
*/
|
|
10134
|
-
function togglePersona(p) {
|
|
10135
|
-
var next = !(p.enabled !== false)
|
|
10136
|
-
setBusy(true)
|
|
10137
|
-
apiCall('subagent-toggle', { name: p.name, enabled: next }).then(function (r) {
|
|
10138
|
-
setBusy(false)
|
|
10139
|
-
if (r && r.ok) {
|
|
10140
|
-
// 场景内改开关:人设改了、档案没跟上时说清楚(详见 sceneSyncWarn)。
|
|
10141
|
-
var warn = sceneSyncWarn(t, r)
|
|
10142
|
-
setResult(warn ? { ok: false, warning: true, text: warn } : null)
|
|
10143
|
-
refresh(true)
|
|
10144
|
-
} else if (r) setResult({ ok: false, text: translateError(t, r) })
|
|
10145
|
-
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
|
|
10146
|
-
}
|
|
10147
|
-
/**
|
|
10148
|
-
* 人设表单的候选数据(模型目录 / 全体预设工具并集)只在**首次展开高级选项**时拉取:
|
|
10149
|
-
* 宿主枚举预设需要为尚未挂载的预设建立 standing mount,不该在打开弹窗时就付这个代价。
|
|
10150
|
-
*/
|
|
10151
|
-
var cands = React.useState({ loaded: false, loading: false, error: null, models: [], tools: [], presets: [] })
|
|
10152
|
-
var cand = cands[0], setCand = cands[1]
|
|
10153
|
-
function loadCandidates() {
|
|
10154
|
-
if (cand.loaded || cand.loading) return
|
|
10155
|
-
setCand(Object.assign({}, cand, { loading: true, error: null }))
|
|
10156
|
-
Promise.all([apiCall('model-candidates', {}), apiCall('preset-tools', {})]).then(function (rs) {
|
|
10157
|
-
var mres = rs[0], tres = rs[1]
|
|
10158
|
-
var failed = (!mres || mres.ok === false) && (!tres || tres.ok === false)
|
|
10159
|
-
setCand({
|
|
10160
|
-
loaded: true, loading: false,
|
|
10161
|
-
error: failed ? t('subagents.adv.loadFailed') : null,
|
|
10162
|
-
models: (mres && mres.models) || [],
|
|
10163
|
-
|
|
10164
|
-
//
|
|
10165
|
-
//
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
*
|
|
10178
|
-
*
|
|
10179
|
-
*
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
})
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
*
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
if (
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
}
|
|
10226
|
-
/**
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
//
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
var
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
}
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
10278
|
-
|
|
10279
|
-
|
|
10280
|
-
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10311
|
-
if (
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
|
|
10317
|
-
|
|
10318
|
-
*
|
|
10319
|
-
*
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
if (!
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
function
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
}
|
|
10343
|
-
/**
|
|
10344
|
-
function
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
var
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
var
|
|
10384
|
-
if (!
|
|
10385
|
-
|
|
10386
|
-
}
|
|
10387
|
-
/**
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
var
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
}
|
|
10450
|
-
return
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
}
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
*
|
|
10496
|
-
*
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
var
|
|
10503
|
-
var
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
})
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
:
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
10104
|
+
// ---------- 子智能体页:人设文件(~/.dsh/subagents/*.md)管理 ----------
|
|
10105
|
+
/**
|
|
10106
|
+
* 「思考强度」档位的按 (provider, model) 缓存。
|
|
10107
|
+
*
|
|
10108
|
+
* 为什么放在**组件外**:放进组件里每次渲染都会重建,等于没有缓存,而档位清单要问
|
|
10109
|
+
* adapter(`llm.resolveModelInfo`,异步、可能联网)—— 编辑人设时来回切模型不该每次都问。
|
|
10110
|
+
* TTL 5 分钟:档位是模型能力声明,短时间内不会变;换模型时才重新问。
|
|
10111
|
+
*/
|
|
10112
|
+
var EFFORT_CACHE_TTL_MS = 5 * 60 * 1000
|
|
10113
|
+
var effortCacheByRoute = {}
|
|
10114
|
+
function readEffortCache(key) {
|
|
10115
|
+
var hit = effortCacheByRoute[key]
|
|
10116
|
+
if (!hit) return null
|
|
10117
|
+
if (Date.now() - hit.at > EFFORT_CACHE_TTL_MS) { delete effortCacheByRoute[key]; return null }
|
|
10118
|
+
return hit.value
|
|
10119
|
+
}
|
|
10120
|
+
function writeEffortCache(key, value) { effortCacheByRoute[key] = { at: Date.now(), value: value } }
|
|
10121
|
+
function SubagentsPage() {
|
|
10122
|
+
var state = React.useState({ loading: true, error: null, subagents: [] })
|
|
10123
|
+
var data = state[0], setData = state[1]
|
|
10124
|
+
var bs = React.useState(false)
|
|
10125
|
+
var busy = bs[0], setBusy = bs[1]
|
|
10126
|
+
var rs = React.useState(null)
|
|
10127
|
+
var result = rs[0], setResult = rs[1]
|
|
10128
|
+
var ms = React.useState(null)
|
|
10129
|
+
var modal = ms[0], setModal = ms[1]
|
|
10130
|
+
/**
|
|
10131
|
+
* 「思考强度」的加载状态。key = 已拉取/正在拉取的 `provider\0model`(空串 = 还没选模型)。
|
|
10132
|
+
* 与 `cands` 分开两个 state:档位是**按模型**懒加载的,而候选目录是一次性拉的,
|
|
10133
|
+
* 混在一起会让"切换模型"误触发整份候选重拉。
|
|
10134
|
+
*/
|
|
10135
|
+
var es = React.useState({ key: '', loading: false, error: null, efforts: [], defaultEffort: null })
|
|
10136
|
+
var eff = es[0], setEff = es[1]
|
|
10137
|
+
/** 换模型导致已存档位被清掉时的一次性提示(下次换模型或关弹窗即消失)。 */
|
|
10138
|
+
var ens = React.useState(null)
|
|
10139
|
+
var effNotice = ens[0], setEffNotice = ens[1]
|
|
10140
|
+
// 搜索词独立一处:`data` 在 refresh 时被整对象替换,寄在它里面会被刷新清空。
|
|
10141
|
+
// 过滤用防抖值,输入框仍绑原值(与提示词页同一写法)。
|
|
10142
|
+
var qs = React.useState('')
|
|
10143
|
+
var query = qs[0], setQuery = qs[1]
|
|
10144
|
+
var dq = useDebouncedValue(query)
|
|
10145
|
+
// 场景锁定(v0.8):任一场景锁定 = 五个域整体冻结;本页全部写控件禁用。
|
|
10146
|
+
var anyLocked = data.anyLocked === true
|
|
10147
|
+
// 场景内开关由档案定义:人设开关置灰(要改就去场景页的档案编辑器里改)。
|
|
10148
|
+
var sceneName = data.activeScene || null
|
|
10149
|
+
// 回收站:删除人设 = 移入回收站(宿主侧 subagent-delete),这里列出/恢复/永久删除。
|
|
10150
|
+
var ts = React.useState(null)
|
|
10151
|
+
var trash = ts[0], setTrash = ts[1]
|
|
10152
|
+
var tb = React.useState(false)
|
|
10153
|
+
var trashBusy = tb[0], setTrashBusy = tb[1]
|
|
10154
|
+
var flipRef = React.useRef(null)
|
|
10155
|
+
useFlipReorder(flipRef)
|
|
10156
|
+
function loadTrash(keepOpen) {
|
|
10157
|
+
setTrash(Object.assign({ loading: true, error: null, entries: [] }, keepOpen ? trash || {} : {}))
|
|
10158
|
+
apiCall('subagent-trash-list', {}).then(function (res) {
|
|
10159
|
+
if (res && res.ok) setTrash({ loading: false, error: null, entries: res.trash || [] })
|
|
10160
|
+
else setTrash({ loading: false, error: (res && res.error) || t('trash.loadFailed'), entries: [] })
|
|
10161
|
+
}).catch(function (e) { setTrash({ loading: false, error: errMsg(e), entries: [] }) })
|
|
10162
|
+
}
|
|
10163
|
+
function restorePersona(item) {
|
|
10164
|
+
setTrashBusy(true)
|
|
10165
|
+
apiCall('subagent-trash-restore', { id: item.id }).then(function (res) {
|
|
10166
|
+
setTrashBusy(false)
|
|
10167
|
+
if (res && res.ok) { var sw = stateSyncWarn(t, res); setResult({ ok: !sw, warning: !!sw, text: t('trash.restored', { name: item.name }) + (sw ? ' ' + sw : '') }); refresh(true); loadTrash(true) }
|
|
10168
|
+
else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
|
|
10169
|
+
}).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
|
|
10170
|
+
}
|
|
10171
|
+
function purgePersona(item) {
|
|
10172
|
+
setTrashBusy(true)
|
|
10173
|
+
apiCall('subagent-trash-delete', { id: item.id }).then(function (res) {
|
|
10174
|
+
setTrashBusy(false)
|
|
10175
|
+
if (res && res.ok) { setResult({ ok: true, text: t('trash.purged', { name: item.name }) }); loadTrash(true) }
|
|
10176
|
+
else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
|
|
10177
|
+
}).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
|
|
10178
|
+
}
|
|
10179
|
+
React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
|
|
10180
|
+
function refresh(silent) {
|
|
10181
|
+
if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
|
|
10182
|
+
apiCall('subagent-list', {}).then(function (r) {
|
|
10183
|
+
// `activeScene` 必须一起搬进来:这里是把 `data` 整对象替换掉(搜索词另存就是为了
|
|
10184
|
+
// 不被冲掉),漏一个字段等于那个字段永远是初始值 —— 场景横幅因此从来没显示过
|
|
10185
|
+
// (2026-09-19 用户报的:技能 / 子智能体 / 记忆三页都看不到"当前处于场景…")。
|
|
10186
|
+
if (r && r.ok) setData({ loading: false, error: null, subagents: r.subagents || [], anyLocked: r.anyLocked === true, activeScene: r.activeScene || null, toolFailures: Array.isArray(r.toolFailures) ? r.toolFailures : [] })
|
|
10187
|
+
else setData({ loading: false, error: translateError(t, r), subagents: [], toolFailures: [] })
|
|
10188
|
+
}).catch(function (e) { setData({ loading: false, error: errMsg(e), subagents: [], toolFailures: [] }) })
|
|
10189
|
+
}
|
|
10190
|
+
React.useEffect(function () { refresh() }, [])
|
|
10191
|
+
/**
|
|
10192
|
+
* 子智能体开关(与记忆页同一套交互):停用 = 不注入目录段、subagent_manager_list/run
|
|
10193
|
+
* 不可见;人设文件不动。开关联动全局状态,立即提交。
|
|
10194
|
+
*/
|
|
10195
|
+
function togglePersona(p) {
|
|
10196
|
+
var next = !(p.enabled !== false)
|
|
10197
|
+
setBusy(true)
|
|
10198
|
+
apiCall('subagent-toggle', { name: p.name, enabled: next }).then(function (r) {
|
|
10199
|
+
setBusy(false)
|
|
10200
|
+
if (r && r.ok) {
|
|
10201
|
+
// 场景内改开关:人设改了、档案没跟上时说清楚(详见 sceneSyncWarn)。
|
|
10202
|
+
var warn = sceneSyncWarn(t, r)
|
|
10203
|
+
setResult(warn ? { ok: false, warning: true, text: warn } : null)
|
|
10204
|
+
refresh(true)
|
|
10205
|
+
} else if (r) setResult({ ok: false, text: translateError(t, r) })
|
|
10206
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
|
|
10207
|
+
}
|
|
10208
|
+
/**
|
|
10209
|
+
* 人设表单的候选数据(模型目录 / 全体预设工具并集)只在**首次展开高级选项**时拉取:
|
|
10210
|
+
* 宿主枚举预设需要为尚未挂载的预设建立 standing mount,不该在打开弹窗时就付这个代价。
|
|
10211
|
+
*/
|
|
10212
|
+
var cands = React.useState({ loaded: false, loading: false, error: null, models: [], providers: [], tools: [], presets: [] })
|
|
10213
|
+
var cand = cands[0], setCand = cands[1]
|
|
10214
|
+
function loadCandidates() {
|
|
10215
|
+
if (cand.loaded || cand.loading) return
|
|
10216
|
+
setCand(Object.assign({}, cand, { loading: true, error: null }))
|
|
10217
|
+
Promise.all([apiCall('model-candidates', {}), apiCall('preset-tools', {})]).then(function (rs) {
|
|
10218
|
+
var mres = rs[0], tres = rs[1]
|
|
10219
|
+
var failed = (!mres || mres.ok === false) && (!tres || tres.ok === false)
|
|
10220
|
+
setCand({
|
|
10221
|
+
loaded: true, loading: false,
|
|
10222
|
+
error: failed ? t('subagents.adv.loadFailed') : null,
|
|
10223
|
+
models: (mres && mres.models) || [],
|
|
10224
|
+
// providers 是去重后的来源清单,**不能**由 models 去重反推:适配器不提供
|
|
10225
|
+
// listModels 时该来源一个模型都报不出来,而"来源有、目录里没模型"正是最需要
|
|
10226
|
+
// 手填模型 id 的场景 —— 反推会让这类来源在「模型来源」下拉里整个消失。
|
|
10227
|
+
providers: (mres && mres.providers) || [],
|
|
10228
|
+
tools: (tres && tres.tools) || [],
|
|
10229
|
+
// presets 必须原样带过来:四行模式的名字、顺序、每行的工具清单全靠它,
|
|
10230
|
+
// 早先这里只留 models/tools,于是分组标题退回裸 id(中文界面里显示英文)。
|
|
10231
|
+
presets: (tres && tres.presets) || [],
|
|
10232
|
+
})
|
|
10233
|
+
}).catch(function (e) {
|
|
10234
|
+
setCand({ loaded: true, loading: false, error: errMsg(e), models: [], providers: [], tools: [], presets: [] })
|
|
10235
|
+
})
|
|
10236
|
+
}
|
|
10237
|
+
/**
|
|
10238
|
+
* 拉取某个 (provider, model) 的思考强度档位。**只有真正展开了高级选项、且选了模型**才会
|
|
10239
|
+
* 被调到(三个调用点:展开高级选项、模型下拉变更、自定义模型输入失焦)。
|
|
10240
|
+
*
|
|
10241
|
+
* 两个语义要点(都来自官方源码,见 src/subagents/service.ts 的 `reasoningEffort` 注释):
|
|
10242
|
+
* · 档位跟模型走 —— 换模型必须重拉,旧清单对新模型无效;
|
|
10243
|
+
* · **拉取失败一律不动已存的值**:只有"成功且当前值不在清单里"才清档。否则一次网络
|
|
10244
|
+
* 抖动就把用户设置抹了,而失败本身已经有提示。
|
|
10245
|
+
*/
|
|
10246
|
+
function loadEfforts(provider, model) {
|
|
10247
|
+
setEffNotice(null)
|
|
10248
|
+
var pv = String(provider || ''), md = String(model || '')
|
|
10249
|
+
if (!pv || !md) { setEff({ key: '', loading: false, error: null, efforts: [], defaultEffort: null }); return }
|
|
10250
|
+
var key = pv + '\u0000' + md
|
|
10251
|
+
var cached = readEffortCache(key)
|
|
10252
|
+
if (cached) {
|
|
10253
|
+
setEff({ key: key, loading: false, error: cached.ok ? null : cached.error, efforts: cached.efforts || [], defaultEffort: cached.defaultEffort || null })
|
|
10254
|
+
if (cached.ok) dropEffortIfGone(cached.efforts || [])
|
|
10255
|
+
return
|
|
10256
|
+
}
|
|
10257
|
+
setEff({ key: key, loading: true, error: null, efforts: [], defaultEffort: null })
|
|
10258
|
+
apiCall('model-reasoning', { provider: pv, model: md }).then(function (res) {
|
|
10259
|
+
var value = res && res.ok !== false
|
|
10260
|
+
? { ok: true, efforts: res.efforts || [], defaultEffort: res.defaultEffort || null }
|
|
10261
|
+
: { ok: false, error: (res && res.error) || t('subagents.effort.unavailable'), efforts: [], defaultEffort: null }
|
|
10262
|
+
writeEffortCache(key, value)
|
|
10263
|
+
setEff({ key: key, loading: false, error: value.ok ? null : value.error, efforts: value.efforts, defaultEffort: value.defaultEffort })
|
|
10264
|
+
if (value.ok) dropEffortIfGone(value.efforts)
|
|
10265
|
+
}).catch(function (e) {
|
|
10266
|
+
var value = { ok: false, error: errMsg(e), efforts: [], defaultEffort: null }
|
|
10267
|
+
writeEffortCache(key, value)
|
|
10268
|
+
setEff({ key: key, loading: false, error: value.error, efforts: [], defaultEffort: null })
|
|
10269
|
+
})
|
|
10270
|
+
}
|
|
10271
|
+
/**
|
|
10272
|
+
* 自动清档:清单里没有当前存的那一档 → 清掉并给一次性提示。
|
|
10273
|
+
*
|
|
10274
|
+
* 为什么必须清:官方对**不支持**的显式档位是在 provider I/O 之前直接拒(不夹紧、不别名),
|
|
10275
|
+
* 留着它 = 下一次委派直接起不来,而错误要到那时才看得见。
|
|
10276
|
+
* 为什么只在"拉取成功"时判断:失败时清单是空的,拿空清单当"没有这一档"会把设置误清。
|
|
10277
|
+
*/
|
|
10278
|
+
function dropEffortIfGone(list) {
|
|
10279
|
+
if (!modal || modal.type !== 'editor') return
|
|
10280
|
+
var cur = String(modal.form.reasoningEffort || '')
|
|
10281
|
+
if (!cur) return
|
|
10282
|
+
var has = (list || []).some(function (e) { return String(e && e.id) === cur })
|
|
10283
|
+
if (has) return
|
|
10284
|
+
setEffNotice(t('subagents.effort.dropped', { effort: cur }))
|
|
10285
|
+
setForm({ reasoningEffort: '' })
|
|
10286
|
+
}
|
|
10287
|
+
/** 当前表单的 (provider, model) 路由键 —— 用来与 `eff.key` 比,确认清单属于当前模型。 */
|
|
10288
|
+
function currentRouteKey() {
|
|
10289
|
+
return String((modal && modal.form.provider) || '') + '\u0000' + String((modal && modal.form.model) || '')
|
|
10290
|
+
}
|
|
10291
|
+
/**
|
|
10292
|
+
* 当前模型**已就绪**的档位。未就绪一律返回空数组:切换模型后 `eff` 里还留着上一个模型的
|
|
10293
|
+
* 清单(新请求还没回来),照它渲染会让用户选中一个不属于这个模型的档位 —— 而官方对不支持的
|
|
10294
|
+
* 档位是直接拒,等于埋一次"委派起不来"。
|
|
10295
|
+
*/
|
|
10296
|
+
function currentEfforts() {
|
|
10297
|
+
if (!modal || !modal.form.model || eff.loading || eff.error) return []
|
|
10298
|
+
return eff.key === currentRouteKey() ? (eff.efforts || []) : []
|
|
10299
|
+
}
|
|
10300
|
+
/** 空档(不指定)那一行的悬停说明:把 adapter 给的默认档位名字显示出来(拿不到就不显示)。 */
|
|
10301
|
+
function defaultEffortTitle() {
|
|
10302
|
+
var id = eff.defaultEffort
|
|
10303
|
+
if (!id || eff.key !== currentRouteKey()) return undefined
|
|
10304
|
+
var hit = (eff.efforts || []).filter(function (e) { return String(e && e.id) === String(id) })[0]
|
|
10305
|
+
return hit ? String(hit.name || hit.id) : String(id)
|
|
10306
|
+
}
|
|
10307
|
+
/** 高级选项:默认收起;已经在用模型/工具限制的人设自动展开(否则用户看不见自己配了什么)。 */
|
|
10308
|
+
/** 「不限制嵌套」的取值(与 src/subagents/service.ts 的 UNLIMITED_PERSONA_CATALOG_DEPTH 同值)。 */
|
|
10309
|
+
var CATALOG_DEPTH_UNLIMITED = 99
|
|
10310
|
+
/** 目录注入深度的选项文案:下拉与折叠摘要共用一处,免得两处说法分叉。 */
|
|
10311
|
+
function catalogDepthLabel(n) {
|
|
10312
|
+
if (n === 1) return t('subagents.field.catalogDepth.onlyTop')
|
|
10313
|
+
if (n === 2) return t('subagents.field.catalogDepth.toChild')
|
|
10314
|
+
if (n >= CATALOG_DEPTH_UNLIMITED) return t('subagents.field.catalogDepth.unlimited')
|
|
10315
|
+
return t('subagents.field.catalogDepth.toGrand')
|
|
10316
|
+
}
|
|
10317
|
+
function initialAdvanced(p) {
|
|
10318
|
+
var modes = p && p.toolsByPreset ? Object.keys(p.toolsByPreset).length : 0
|
|
10319
|
+
// 目录注入深度不是默认值(1)时也算"配过":它决定常驻目录出现在哪些会话,
|
|
10320
|
+
// 藏起来会让"为什么子会话看不到目录"变得无从查起。
|
|
10321
|
+
var budget = !!(p && typeof p.catalogDepth === 'number' && p.catalogDepth !== 1)
|
|
10322
|
+
// 允许追问同理:它改变的是"下一次委派会怎么跑"(回执一个 id 而不是产出),
|
|
10323
|
+
// 藏在收起的高级选项里等于用户改过又忘了自己改过。
|
|
10324
|
+
var continuable = !!(p && p.continuable === true)
|
|
10325
|
+
// 思考强度同理:配过就得让人看得见 —— 它是"下一次委派会怎么跑"的一部分,
|
|
10326
|
+
// 藏在收起的高级选项里等于用户改过又忘了自己改过。
|
|
10327
|
+
return !!(budget || continuable || (p && (p.model || p.provider || p.reasoningEffort)) || (p && ((p.tools || []).length || (p.toolsDeny || []).length || modes)))
|
|
10328
|
+
}
|
|
10329
|
+
/** 编辑态里按模式分组的名单(深拷贝:取消编辑不留痕)。 */
|
|
10330
|
+
function cloneRules(source) {
|
|
10331
|
+
var out = {}
|
|
10332
|
+
Object.keys(source || {}).forEach(function (id) {
|
|
10333
|
+
var rule = source[id] || {}
|
|
10334
|
+
out[id] = { mode: rule.mode === 'deny' ? 'deny' : 'allow', names: (rule.names || []).slice() }
|
|
10335
|
+
})
|
|
10336
|
+
return out
|
|
10337
|
+
}
|
|
10338
|
+
function openEditor(name) {
|
|
10339
|
+
if (!name) {
|
|
10340
|
+
setModal({ type: 'editor', mode: 'create', advanced: false, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: { name: '', description: '', provider: '', model: '', reasoningEffort: '', catalogDepth: 1, continuable: false, tools: [], toolsDeny: [], toolsByPreset: {}, body: '', output: '', error: null } })
|
|
10341
|
+
return
|
|
10342
|
+
}
|
|
10343
|
+
setBusy(true)
|
|
10344
|
+
apiCall('subagent-get', { name: name }).then(function (res) {
|
|
10345
|
+
setBusy(false)
|
|
10346
|
+
if (res && res.ok) {
|
|
10347
|
+
var p = res.persona || {}
|
|
10348
|
+
var advanced = initialAdvanced(p)
|
|
10349
|
+
setModal({ type: 'editor', mode: 'edit', originalName: String(p.name || name), advanced: advanced, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: {
|
|
10350
|
+
name: p.name || name, description: p.description || '', provider: p.provider || '', model: p.model || '',
|
|
10351
|
+
// 思考强度:服务端回空串 = 这份人设没指定(不是"没有档位")。空值不落盘,见 serializePersona。
|
|
10352
|
+
reasoningEffort: p.reasoningEffort || '',
|
|
10353
|
+
// 服务端回的是**生效值**(没写就是默认 1),所以这里不必再兜默认。
|
|
10354
|
+
catalogDepth: typeof p.catalogDepth === 'number' ? p.catalogDepth : 1,
|
|
10355
|
+
// 服务端回布尔;老服务端不带这个字段时按 false(一次性)算。
|
|
10356
|
+
continuable: p.continuable === true,
|
|
10357
|
+
tools: (p.tools || []).slice(), toolsDeny: (p.toolsDeny || []).slice(),
|
|
10358
|
+
toolsByPreset: cloneRules(p.toolsByPreset), body: p.body || '', output: p.output || '', error: null,
|
|
10359
|
+
} })
|
|
10360
|
+
// 已配过限制的人设**一打开就是展开的**(initialAdvanced)→ 候选数据必须在这里也拉,
|
|
10361
|
+
// 否则四行模式先亮"宿主没有回传预设名单",非得点两次「高级选项」才补上(用户实测)。
|
|
10362
|
+
// 思考强度同理:展开着就要有档位清单(它按模型懒加载,所以这里显式带上 model)。
|
|
10363
|
+
if (advanced) { loadCandidates(); loadEfforts(p.provider, p.model) }
|
|
10364
|
+
} else setResult({ ok: false, text: translateError(t, res) })
|
|
10365
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
|
|
10366
|
+
}
|
|
10367
|
+
function setForm(patch) { if (modal && modal.type === 'editor') setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, patch) })) }
|
|
10368
|
+
/** 列表里增删一项(勾选语义:存在 = 勾上)。 */
|
|
10369
|
+
function toggled(list, name) {
|
|
10370
|
+
var next = (list || []).slice()
|
|
10371
|
+
var i = next.indexOf(name)
|
|
10372
|
+
if (i >= 0) next.splice(i, 1); else next.push(name)
|
|
10373
|
+
return next
|
|
10374
|
+
}
|
|
10375
|
+
/**
|
|
10376
|
+
* 模型下拉的当前值:`provider\0model` 复合键(与每个选项的 value 同构)。
|
|
10377
|
+
*
|
|
10378
|
+
* 目录里查不到这对 (来源, 模型) 时也**照原样给**,靠 `offCatalogModel()` 那条额外选项
|
|
10379
|
+
* 显示出来。0.19.0 之前这里会退回一个「自定义 / 目录里没有…」菜单项并把输入框露出来,
|
|
10380
|
+
* 2026-10-07 用户裁定删掉整条手填通道:来源与模型都只从宿主目录里选。
|
|
10381
|
+
*/
|
|
10382
|
+
function modelSelectValue() {
|
|
10383
|
+
if (!modal || modal.type !== 'editor') return ''
|
|
10384
|
+
var f = modal.form
|
|
10385
|
+
if (!f.model) return ''
|
|
10386
|
+
return String(f.provider || '') + '\u0000' + String(f.model)
|
|
10387
|
+
}
|
|
10388
|
+
/**
|
|
10389
|
+
* 存的 (来源, 模型) **不在**宿主目录里时,返回 `modelSelectValue()` 的那个值(多列一条
|
|
10390
|
+
* 只读选项用);在目录里、或模型为空时返回 ''。
|
|
10391
|
+
*
|
|
10392
|
+
* 为什么必须有这一条:0.19.0 之前这两个字段是纯文本框,现存人设里就有目录外的值;
|
|
10393
|
+
* 下拉里找不到对应项时浏览器会显示第一项(「继承主会话」)—— **表单在说谎**,而保存时
|
|
10394
|
+
* 又把那个看不见的值写回去。多列一条带标记的选项,值就永远看得见。
|
|
10395
|
+
*/
|
|
10396
|
+
function offCatalogModel() {
|
|
10397
|
+
if (!modal || modal.type !== 'editor') return ''
|
|
10398
|
+
var f = modal.form
|
|
10399
|
+
if (!f.model) return ''
|
|
10400
|
+
var provider = String(f.provider || '')
|
|
10401
|
+
var hit = (cand.models || []).filter(function (m) { return m.provider === provider && m.id === f.model })[0]
|
|
10402
|
+
return hit ? '' : modelSelectValue()
|
|
10403
|
+
}
|
|
10404
|
+
/** 「模型来源」下拉的当前值 —— 就是存的来源本身(目录外的靠 `offCatalogProvider()` 显示)。 */
|
|
10405
|
+
function providerSelectValue() {
|
|
10406
|
+
if (!modal || modal.type !== 'editor') return ''
|
|
10407
|
+
return String(modal.form.provider || '')
|
|
10408
|
+
}
|
|
10409
|
+
/** 存的来源不在宿主目录里时返回它(多列一条只读选项用);在目录里或为空返回 ''。 */
|
|
10410
|
+
function offCatalogProvider() {
|
|
10411
|
+
if (!modal || modal.type !== 'editor') return ''
|
|
10412
|
+
var v = String(modal.form.provider || '')
|
|
10413
|
+
if (!v) return ''
|
|
10414
|
+
var hit = (cand.providers || []).filter(function (p) { return p.id === v })[0]
|
|
10415
|
+
return hit ? '' : v
|
|
10416
|
+
}
|
|
10417
|
+
/**
|
|
10418
|
+
* 目录这次**真的读到了**吗。
|
|
10419
|
+
*
|
|
10420
|
+
* 「目录里没有」是一句断言,只有读成功才配说:读失败(`cand.error`)时清单是空的,
|
|
10421
|
+
* 照断言会把"没读到"说成"不存在"。读不到时那条额外选项仍然渲染,只是不带标记。
|
|
10422
|
+
*/
|
|
10423
|
+
function catalogReady() {
|
|
10424
|
+
return !!(cand.loaded && !cand.loading && !cand.error)
|
|
10425
|
+
}
|
|
10426
|
+
/**
|
|
10427
|
+
* 来源下拉的选项文字:显示名与 id 不同时两个都给(`DeepSeek · deepseek`)——
|
|
10428
|
+
* 显示名给人看,id 是真正写进 frontmatter 的那个键,只给一个都算缺信息。
|
|
10429
|
+
*/
|
|
10430
|
+
function providerLabel(p) {
|
|
10431
|
+
var id = String((p && p.id) || '')
|
|
10432
|
+
var name = String((p && p.name) || '')
|
|
10433
|
+
return name && name !== id ? name + ' · ' + id : id
|
|
10434
|
+
}
|
|
10435
|
+
/**
|
|
10436
|
+
* 模型下拉要列哪些模型:**来源选定后只列该来源的**(用户 2026-10-07)。
|
|
10437
|
+
* 不按来源收窄时,一份几十条的扁平列表里选错来源是常态,而错配的 (来源, 模型) 要到
|
|
10438
|
+
* 子代理真正启动时才由官方报错 —— 那时人已经在别的页面了。
|
|
10439
|
+
* 来源留空则列全部:此时挑中哪一条,就把它的来源一并填上(见 onChange)。
|
|
10440
|
+
*/
|
|
10441
|
+
function modelOptions() {
|
|
10442
|
+
if (!modal || modal.type !== 'editor') return []
|
|
10443
|
+
var all = cand.models || []
|
|
10444
|
+
var p = modal.form.provider || ''
|
|
10445
|
+
if (!p) return all
|
|
10446
|
+
return all.filter(function (m) { return m.provider === p })
|
|
10447
|
+
}
|
|
10448
|
+
/** 模型选项文字:已按来源收窄时不再重复来源前缀(左边那一格就是来源)。 */
|
|
10449
|
+
function modelOptionLabel(m) {
|
|
10450
|
+
var narrowed = !!(modal && modal.type === 'editor' && modal.form.provider)
|
|
10451
|
+
return narrowed ? String((m && (m.name || m.id)) || '') : m.provider + ' · ' + m.name
|
|
10452
|
+
}
|
|
10453
|
+
/**
|
|
10454
|
+
* 「高级选项」收起态**第二行**的内容;返回 null = 这一行整个不出现。
|
|
10455
|
+
* 只有真配了工具限制才报:收起态挂一句「模式限制:0 项;旧格式:白名单 0 / 黑名单 0」
|
|
10456
|
+
* 是用一行废话占位置(用户 2026-10-07 指出)。
|
|
10457
|
+
*/
|
|
10458
|
+
function advLimitLine() {
|
|
10459
|
+
if (!modal || modal.type !== 'editor') return null
|
|
10460
|
+
var modes = Object.keys(modal.form.toolsByPreset || {}).length
|
|
10461
|
+
var allow = (modal.form.tools || []).length
|
|
10462
|
+
var deny = (modal.form.toolsDeny || []).length
|
|
10463
|
+
if (!modes && !allow && !deny) return null
|
|
10464
|
+
return t('subagents.adv.summary2', {
|
|
10465
|
+
modes: modes,
|
|
10466
|
+
// 旧格式只在有值时报;分隔号写在词典值里(见 adv.summary 的 tail)。
|
|
10467
|
+
tail: (allow || deny) ? t('subagents.adv.legacy', { allow: allow, deny: deny }) : '',
|
|
10468
|
+
})
|
|
10469
|
+
}
|
|
10470
|
+
/**
|
|
10471
|
+
* 官方四个预设的中英名走插件词典(官方 preset.yml 里只有中文名,直接用会让英文界面
|
|
10472
|
+
* 露出中文);**自建预设用它自己的名字,不翻译** —— 与官方 display 规则一致。
|
|
10473
|
+
* 四个预设 id → 词典键的映射只认官方发布的这四个 id。
|
|
10474
|
+
*/
|
|
10475
|
+
var SHIPPED_PRESET_IDS = { minimal: 1, standard: 1, ptc: 1, cordis: 1 }
|
|
10476
|
+
function presetLabel(p) {
|
|
10477
|
+
if (!p) return ''
|
|
10478
|
+
var id = String(p.id || '')
|
|
10479
|
+
if (SHIPPED_PRESET_IDS[id] === 1 && String(p.trust || 'system') !== 'user') return t('preset.name.' + id)
|
|
10480
|
+
return String(p.name || id)
|
|
10481
|
+
}
|
|
10482
|
+
/**
|
|
10483
|
+
* 该模式下可勾选的工具名 + 每个名字的归属组。
|
|
10484
|
+
* `null` = 宿主没给出该模式的清单(读不到,不能假装是空)。
|
|
10485
|
+
*
|
|
10486
|
+
* `groups` 缺项(老服务端没这个字段)时按 `other` 处理 —— 落错组只是位置不对,
|
|
10487
|
+
* 丢掉就是"界面上勾不到",那是静默降级。
|
|
10488
|
+
*/
|
|
10489
|
+
function modeTools(id) {
|
|
10490
|
+
var hit = (cand.presets || []).filter(function (p) { return p.id === id })[0]
|
|
10491
|
+
if (!hit || !Array.isArray(hit.tools)) return null
|
|
10492
|
+
return { names: hit.tools, groups: hit.toolGroups || {} }
|
|
10493
|
+
}
|
|
10494
|
+
/** 三组的固定顺序:本插件 → 官方 → 其他。顺序即语义,别按字典序排。 */
|
|
10495
|
+
var TOOL_ORIGINS = ['plugin', 'official', 'other']
|
|
10496
|
+
function modeRule(id) { return (modal.form.toolsByPreset || {})[id] }
|
|
10497
|
+
function setModeRule(id, rule) {
|
|
10498
|
+
var next = Object.assign({}, modal.form.toolsByPreset || {})
|
|
10499
|
+
if (rule) next[id] = rule; else delete next[id]
|
|
10500
|
+
setModal(Object.assign({}, modal, {
|
|
10501
|
+
form: Object.assign({}, modal.form, { toolsByPreset: next }),
|
|
10502
|
+
openMode: modal.openMode === id && !rule ? null : modal.openMode,
|
|
10503
|
+
}))
|
|
10504
|
+
}
|
|
10505
|
+
/** 模式行上的一句话状态:未开启 / 白名单 N 个 / 黑名单 N 个 / 已开启但没勾(= 不限制)。 */
|
|
10506
|
+
function modeSummary(id) {
|
|
10507
|
+
var rule = modeRule(id)
|
|
10508
|
+
if (!rule) return { text: t('subagents.mode.off'), cls: 'dsm-mode-off' }
|
|
10509
|
+
var n = (rule.names || []).length
|
|
10510
|
+
if (!n) return { text: t('subagents.mode.empty'), cls: 'dsm-mode-off' }
|
|
10511
|
+
return {
|
|
10512
|
+
text: t(rule.mode === 'deny' ? 'subagents.mode.denyOn' : 'subagents.mode.allowOn', { count: n }),
|
|
10513
|
+
cls: rule.mode === 'deny' ? 'dsm-mode-deny' : 'dsm-mode-allow',
|
|
10514
|
+
}
|
|
10515
|
+
}
|
|
10516
|
+
/**
|
|
10517
|
+
* 启动某一侧名单:生效 + 就地展开。同一模式内白/黑互斥 —— 启动一侧就把模式切过去,
|
|
10518
|
+
* **已勾选的名字保留**(只换语义),这样"改成黑名单"不用从头再勾一遍。
|
|
10519
|
+
*/
|
|
10520
|
+
function startMode(id, mode) {
|
|
10521
|
+
// 刚被「关闭」掉的名字记在 stoppedRules 里:重新启用时带回,免得"关一下再开"把
|
|
10522
|
+
// 已勾好的名单清空(用户实测踩到:以为「关闭白名单」是收起设置,再启用就空了)。
|
|
10523
|
+
var prev = modeRule(id) || (modal.stoppedRules || {})[id]
|
|
10524
|
+
var names = prev && Array.isArray(prev.names) ? prev.names.slice() : []
|
|
10525
|
+
setModal(Object.assign({}, modal, {
|
|
10526
|
+
openMode: id, modeQuery: '',
|
|
10527
|
+
form: Object.assign({}, modal.form, {
|
|
10528
|
+
toolsByPreset: Object.assign({}, modal.form.toolsByPreset || {}, { [id]: { mode: mode, names: names } }),
|
|
10529
|
+
}),
|
|
10530
|
+
}))
|
|
10531
|
+
}
|
|
10532
|
+
function stopMode(id) {
|
|
10533
|
+
var prev = modeRule(id)
|
|
10534
|
+
var stopped = Object.assign({}, modal.stoppedRules || {})
|
|
10535
|
+
if (prev) stopped[id] = { mode: prev.mode, names: (prev.names || []).slice() }
|
|
10536
|
+
var next = Object.assign({}, modal.form.toolsByPreset || {})
|
|
10537
|
+
delete next[id]
|
|
10538
|
+
setModal(Object.assign({}, modal, {
|
|
10539
|
+
stoppedRules: stopped,
|
|
10540
|
+
openMode: modal.openMode === id ? null : modal.openMode,
|
|
10541
|
+
form: Object.assign({}, modal.form, { toolsByPreset: next }),
|
|
10542
|
+
}))
|
|
10543
|
+
}
|
|
10544
|
+
function toggleModeTool(id, name) {
|
|
10545
|
+
var rule = modeRule(id)
|
|
10546
|
+
if (!rule) return
|
|
10547
|
+
setModeRule(id, { mode: rule.mode, names: toggled(rule.names || [], name) })
|
|
10548
|
+
}
|
|
10549
|
+
/**
|
|
10550
|
+
* 展开后的勾选区:只列**该模式自己的**工具,并按归属分三段
|
|
10551
|
+
* (插件工具 / 官方工具 / 其他工具)。这样勾出来的名字天然都属于这个预设,
|
|
10552
|
+
* 不会出现"在标准模式勾了只属于 PTC 的工具 → 换模式跑就启动失败"。
|
|
10553
|
+
* MCP 工具不在这里(用户裁定):子代理照旧能用当前在跑的 MCP,那份名单在运行时并入。
|
|
10554
|
+
*
|
|
10555
|
+
* 分组只是地标,**不改变勾选语义**:三段共用一个 `picked`,「全选 / 取消全选」仍跨组
|
|
10556
|
+
* 对整份清单生效;空组不渲染,筛选后为空的组也不渲染。地标复用 `segGroup`(左内缩与
|
|
10557
|
+
* `.dsm-pick` 同为 8px,天然对齐,所以不新增样式)。
|
|
10558
|
+
*
|
|
10559
|
+
* ⚠️ 分组买的是「一眼看出这是谁的东西」,不是「更快找到某个工具」—— 后者是上面那个
|
|
10560
|
+
* 筛选框的活,分组不减少滚动量。
|
|
10561
|
+
*/
|
|
10562
|
+
function modeEditor(p) {
|
|
10563
|
+
var id = p.id
|
|
10564
|
+
var rule = modeRule(id)
|
|
10565
|
+
if (!rule) return null
|
|
10566
|
+
var view = modeTools(id)
|
|
10567
|
+
var all = view ? view.names : null
|
|
10568
|
+
if (all === null || all.length === 0) {
|
|
10569
|
+
return React.createElement('div', { className: 'dsm-mode-body' },
|
|
10570
|
+
React.createElement('div', { className: 'dsm-pick-empty' }, p.broken ? t('subagents.mode.broken') : t('subagents.mode.noTools')))
|
|
10571
|
+
}
|
|
10572
|
+
var q = String(modal.modeQuery || '').trim().toLowerCase()
|
|
10573
|
+
var list = q ? all.filter(function (n) { return n.toLowerCase().indexOf(q) >= 0 }) : all
|
|
10574
|
+
var picked = rule.names || []
|
|
10575
|
+
// 与其余「全选」同一口径:全勾了就只给「取消全选」(二合一,不并列)。
|
|
10576
|
+
var allPickedAll = all.length > 0 && all.every(function (n) { return picked.indexOf(n) >= 0 })
|
|
10577
|
+
// 按归属切段。认不出的归属(服务端没给这一项,或给了个没见过的值)一律落「其他」——
|
|
10578
|
+
// 落错组只是位置不对,丢掉就是"界面上勾不到"。
|
|
10579
|
+
var byOrigin = { plugin: [], official: [], other: [] }
|
|
10580
|
+
list.forEach(function (name) {
|
|
10581
|
+
var origin = view.groups[name]
|
|
10582
|
+
byOrigin[origin === 'plugin' || origin === 'official' ? origin : 'other'].push(name)
|
|
10583
|
+
})
|
|
10584
|
+
var grid = []
|
|
10585
|
+
TOOL_ORIGINS.forEach(function (origin) {
|
|
10586
|
+
var names = byOrigin[origin]
|
|
10587
|
+
if (!names.length) return
|
|
10588
|
+
grid.push(segGroup(t('subagents.tools.group.' + origin)))
|
|
10589
|
+
names.forEach(function (name) {
|
|
10590
|
+
grid.push(pickRow({ key: 'm:' + id + ':' + name, disabled: busy, checked: picked.indexOf(name) >= 0, name: name, onChange: function () { toggleModeTool(id, name) } }))
|
|
10591
|
+
})
|
|
10592
|
+
})
|
|
10593
|
+
return React.createElement('div', { className: 'dsm-mode-body' },
|
|
10594
|
+
picked.length
|
|
10595
|
+
? React.createElement('div', { className: 'dsm-tools-chips' }, picked.map(function (name) {
|
|
10596
|
+
return React.createElement('span', { key: 'c:' + name, className: 'dsm-chip' + (rule.mode === 'deny' ? ' dsm-chip-deny' : '') },
|
|
10597
|
+
name,
|
|
10598
|
+
React.createElement('button', { type: 'button', title: t('subagents.tools.remove'), onClick: function () { toggleModeTool(id, name) } }, '×'))
|
|
10599
|
+
}))
|
|
10600
|
+
: React.createElement('span', { className: 'dsm-adv-note' }, t(rule.mode === 'deny' ? 'subagents.mode.denyEmpty' : 'subagents.mode.allowEmpty')),
|
|
10601
|
+
React.createElement('div', { className: 'dsm-combo-row' },
|
|
10602
|
+
React.createElement('input', {
|
|
10603
|
+
className: 'dsm-control',
|
|
10604
|
+
value: modal.modeQuery || '',
|
|
10605
|
+
placeholder: t('subagents.tools.filter'),
|
|
10606
|
+
onChange: function (e) { setModal(Object.assign({}, modal, { modeQuery: e.target.value })) },
|
|
10607
|
+
}),
|
|
10608
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary dsm-btn-bulk', disabled: busy, onClick: function () { setModeRule(id, { mode: rule.mode, names: allPickedAll ? [] : all.slice() }) } }, bulkPair(allPickedAll ? t('bulk.unselectAll') : t('bulk.selectAll'), allPickedAll ? t('bulk.selectAll') : t('bulk.unselectAll')))),
|
|
10609
|
+
list.length
|
|
10610
|
+
? React.createElement('div', { className: 'dsm-tools-grid' }, grid)
|
|
10611
|
+
: React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.mem.noMatch')))
|
|
10612
|
+
}
|
|
10613
|
+
/**
|
|
10614
|
+
* 一行一个 Agent 预设(roster 顺序:标准 / PTC / 极简 / 创造 / 自建预设)。
|
|
10615
|
+
* 默认全部折叠、全部未启动;右侧两个按钮「启动 白名单」「启动 黑名单」互斥。
|
|
10616
|
+
*
|
|
10617
|
+
* 不再自带标题与说明(0.19.0 重排):标题由所在分组头承担(`subagents.adv.group.tools`),
|
|
10618
|
+
* 白/黑名单语义与「MCP 工具不在候选里」那一行移到分组头正下方。这两句原先挂在字段尾巴上,
|
|
10619
|
+
* 叠上另外几处提示后,展开态读起来是一堵墙(用户 2026-10-07 指出)。
|
|
10620
|
+
*/
|
|
10621
|
+
function modeRows() {
|
|
10622
|
+
var presets = (cand.presets || []).slice()
|
|
10623
|
+
if (!presets.length) {
|
|
10624
|
+
return React.createElement('div', { className: 'dsm-pick-empty' }, cand.loading ? t('memory.loading') : t('subagents.mode.noPresets'))
|
|
10625
|
+
}
|
|
10626
|
+
return React.createElement('div', null,
|
|
10627
|
+
React.createElement('div', { className: 'dsm-modes' }, presets.map(function (p) {
|
|
10628
|
+
var id = p.id
|
|
10629
|
+
var rule = modeRule(id)
|
|
10630
|
+
var summary = modeSummary(id)
|
|
10631
|
+
var startButton = function (mode, isDeny) {
|
|
10632
|
+
var active = !!rule && rule.mode === mode
|
|
10633
|
+
return React.createElement('button', {
|
|
10634
|
+
key: mode,
|
|
10635
|
+
type: 'button',
|
|
10636
|
+
className: 'dsm-btn dsm-btn-quiet' + (active ? (mode === 'deny' ? ' dsm-mode-btn-deny' : ' dsm-mode-btn-allow') : ''),
|
|
10637
|
+
disabled: busy,
|
|
10638
|
+
onClick: function () { if (active) stopMode(id); else startMode(id, mode) },
|
|
10639
|
+
}, t(active
|
|
10640
|
+
? (isDeny ? 'subagents.mode.stopDeny' : 'subagents.mode.stopAllow')
|
|
10641
|
+
: (isDeny ? 'subagents.mode.startDeny' : 'subagents.mode.startAllow')))
|
|
10642
|
+
}
|
|
10643
|
+
var open = modal.openMode === id
|
|
10644
|
+
return React.createElement('div', { className: 'dsm-mode-row' + (rule ? ' dsm-mode-on' : ''), key: id },
|
|
10645
|
+
React.createElement('div', { className: 'dsm-mode-head' },
|
|
10646
|
+
// 行首按钮 = 展开 / 收起名单(用户实测:已配好的模式只有「关闭白名单」可点,
|
|
10647
|
+
// 点下去是把模式停掉、名单设置再也进不去 —— 查看/编辑与启用/停用必须分开)。
|
|
10648
|
+
React.createElement('button', {
|
|
10649
|
+
type: 'button',
|
|
10650
|
+
className: 'dsm-mode-head-main',
|
|
10651
|
+
disabled: !rule,
|
|
10652
|
+
title: rule ? t('subagents.mode.toggleHint') : '',
|
|
10653
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
10654
|
+
onClick: function () {
|
|
10655
|
+
setModal(Object.assign({}, modal, { openMode: open ? null : id, modeQuery: '' }))
|
|
10656
|
+
},
|
|
10657
|
+
},
|
|
10658
|
+
React.createElement('span', { className: 'dsm-mode-caret', 'aria-hidden': 'true' }, rule ? (open ? '▾' : '▸') : ''),
|
|
10659
|
+
React.createElement('span', { className: 'dsm-mode-name' }, presetLabel(p)),
|
|
10660
|
+
p.broken ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('compat.reach.broken')) : null,
|
|
10661
|
+
React.createElement('span', { className: 'dsm-mode-sum ' + summary.cls }, summary.text)),
|
|
10662
|
+
React.createElement('span', { className: 'dsm-mode-actions' },
|
|
10663
|
+
startButton('allow', false),
|
|
10664
|
+
startButton('deny', true))),
|
|
10665
|
+
open ? modeEditor(p) : null)
|
|
10666
|
+
})))
|
|
10667
|
+
}
|
|
10668
|
+
/**
|
|
10669
|
+
* 旧格式(全局 `tools:` / `toolsDeny:`)的只读小结 + 转换入口。
|
|
10670
|
+
* 旧键对所有模式生效,运行时仍然照旧执行;**不点转换就不动文件里的旧键**。
|
|
10671
|
+
*/
|
|
10672
|
+
function legacyNotice() {
|
|
10673
|
+
var allow = modal.form.tools || []
|
|
10674
|
+
var deny = modal.form.toolsDeny || []
|
|
10675
|
+
if (!allow.length && !deny.length) return null
|
|
10676
|
+
var presets = cand.presets || []
|
|
10677
|
+
var target = modal.legacyTarget || (presets[0] ? presets[0].id : '')
|
|
10678
|
+
return React.createElement('div', { className: 'dsm-legacy' },
|
|
10679
|
+
React.createElement('span', { className: 'dsm-adv-note' }, t('subagents.legacy.note', { allow: allow.length, deny: deny.length })),
|
|
10680
|
+
React.createElement('div', { className: 'dsm-combo-row' },
|
|
10681
|
+
React.createElement('div', { className: 'dsm-select' },
|
|
10682
|
+
React.createElement('select', {
|
|
10683
|
+
className: 'dsm-control',
|
|
10684
|
+
value: target,
|
|
10685
|
+
disabled: busy || !presets.length,
|
|
10686
|
+
onChange: function (e) { setModal(Object.assign({}, modal, { legacyTarget: e.target.value })) },
|
|
10687
|
+
}, presets.map(function (p) { return React.createElement('option', { key: p.id, value: p.id }, presetLabel(p)) }))),
|
|
10688
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !target || !presets.length, onClick: function () {
|
|
10689
|
+
var rule = { mode: allow.length ? 'allow' : 'deny', names: (allow.length ? allow : deny).slice() }
|
|
10690
|
+
setModal(Object.assign({}, modal, {
|
|
10691
|
+
openMode: target,
|
|
10692
|
+
modeQuery: '',
|
|
10693
|
+
legacyTarget: target,
|
|
10694
|
+
form: Object.assign({}, modal.form, {
|
|
10695
|
+
tools: [], toolsDeny: [],
|
|
10696
|
+
toolsByPreset: Object.assign({}, modal.form.toolsByPreset || {}, { [target]: rule }),
|
|
10697
|
+
}),
|
|
10698
|
+
}))
|
|
10699
|
+
} }, t('subagents.legacy.convert'))),
|
|
10700
|
+
React.createElement('p', { className: 'dsm-help' }, t('subagents.legacy.hint')))
|
|
10701
|
+
}
|
|
10702
|
+
function toggleAdvanced() {
|
|
10703
|
+
if (!modal || modal.type !== 'editor') return
|
|
10704
|
+
var next = !modal.advanced
|
|
10705
|
+
setModal(Object.assign({}, modal, { advanced: next }))
|
|
10706
|
+
if (next) {
|
|
10707
|
+
loadCandidates()
|
|
10708
|
+
// 档位清单与候选目录**分开拉**:候选是本地目录(一次性),档位要问 adapter
|
|
10709
|
+
// (按模型、可能联网),所以这里用表单里当前的模型去拉;没选模型就什么都不发。
|
|
10710
|
+
loadEfforts(modal.form.provider, modal.form.model)
|
|
10711
|
+
}
|
|
10712
|
+
}
|
|
10713
|
+
function submitEditor() {
|
|
10714
|
+
if (!modal || modal.type !== 'editor') return
|
|
10715
|
+
setBusy(true)
|
|
10716
|
+
var op = modal.mode === 'create' ? 'subagent-create' : 'subagent-update'
|
|
10717
|
+
// 编辑时名字可改:nextName 只在真的改过时才传(服务端按原名定位文件)。
|
|
10718
|
+
var renamed = modal.mode === 'edit' && modal.originalName && modal.originalName !== modal.form.name
|
|
10719
|
+
apiCall(op, {
|
|
10720
|
+
name: renamed ? String(modal.originalName) : modal.form.name,
|
|
10721
|
+
...(renamed ? { nextName: String(modal.form.name || '').trim() } : {}),
|
|
10722
|
+
description: modal.form.description,
|
|
10723
|
+
provider: modal.form.provider, model: modal.form.model,
|
|
10724
|
+
catalogDepth: modal.form.catalogDepth,
|
|
10725
|
+
continuable: modal.form.continuable === true,
|
|
10726
|
+
// 旧格式的全局名单原样回写(旧键不点转换就不动),新模式名单另存一块。
|
|
10727
|
+
tools: modal.form.tools || [], toolsDeny: modal.form.toolsDeny || [],
|
|
10728
|
+
toolsByPreset: modal.form.toolsByPreset || {},
|
|
10729
|
+
body: modal.form.body,
|
|
10730
|
+
output: modal.form.output || '',
|
|
10731
|
+
}).then(function (res) {
|
|
10732
|
+
setBusy(false)
|
|
10733
|
+
if (res && res.ok) { var sw = stateSyncWarn(t, res); setModal(null); setResult({ ok: !sw, warning: !!sw, text: t('subagents.result.saved', { name: modal.form.name }) + (sw ? ' ' + sw : '') }); refresh(true) }
|
|
10734
|
+
else setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, { error: translateError(t, res) }) }))
|
|
10735
|
+
}).catch(function (e) { setBusy(false); setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, { error: errMsg(e) }) })) })
|
|
10736
|
+
}
|
|
10737
|
+
function submitDelete(name) {
|
|
10738
|
+
setBusy(true)
|
|
10739
|
+
apiCall('subagent-delete', { name: name }).then(function (res) {
|
|
10740
|
+
setBusy(false); setModal(null)
|
|
10741
|
+
if (res && res.ok) { var sw = stateSyncWarn(t, res); setResult({ ok: !sw, warning: !!sw, text: t('subagents.result.deleted', { name: name }) + (sw ? ' ' + sw : '') }); refresh(true) }
|
|
10742
|
+
else setResult({ ok: false, text: translateError(t, res) })
|
|
10743
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
|
|
10744
|
+
}
|
|
10745
|
+
// ── 导入人设(.md / .zip,多选或拖入;同名跳过并报告)──
|
|
10746
|
+
function submitImport(files) {
|
|
10747
|
+
if (!files || !files.length) return
|
|
10748
|
+
setBusy(true)
|
|
10749
|
+
apiCall('subagent-import', { files: files }).then(function (res) {
|
|
10750
|
+
setBusy(false)
|
|
10751
|
+
if (res && res.ok) {
|
|
10752
|
+
setModal(null)
|
|
10753
|
+
var names = res.imported || [], skipped = res.skipped || []
|
|
10754
|
+
var text = names.length ? t('subagents.result.imported', { count: names.length, names: names.join('、') }) : t('import.none')
|
|
10755
|
+
if (skipped.length) text += ' · ' + t('import.skipped', { items: skipped.map(function (s) { return s.name + '(' + s.reason + ')' }).join(';') })
|
|
10756
|
+
var sw = stateSyncWarn(t, res)
|
|
10757
|
+
if (sw) text += ' ' + sw
|
|
10758
|
+
setResult({ ok: !sw, warning: !!sw, text: text }); refresh(true)
|
|
10759
|
+
} else setResult({ ok: false, text: translateError(t, res) })
|
|
10760
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
|
|
10761
|
+
}
|
|
10762
|
+
// 过滤只跟数据与(防抖后的)搜索词有关:每次渲染重扫一整份列表没有必要。
|
|
10763
|
+
// FLIP 的 key 仍是 `p.name`(过滤只改变"列表里有哪些",不改变身份)。
|
|
10764
|
+
var visibleSubagents = React.useMemo(function () {
|
|
10765
|
+
return data.subagents.filter(function (p) { return matchPersonaQuery(p, dq) })
|
|
10766
|
+
}, [data.subagents, dq])
|
|
10767
|
+
// 「启用的排前面」(用户要求):与过滤分开记忆 —— 只跟过滤结果有关,弹窗 / 结果提示条
|
|
10768
|
+
// 之类的状态变化不再重跑分区。FLIP 的 data-flip-on 仍来自 p.enabled,未动。
|
|
10769
|
+
var orderedSubagents = React.useMemo(function () {
|
|
10770
|
+
return enabledFirst(visibleSubagents, function (p) { return p.enabled !== false; })
|
|
10771
|
+
}, [visibleSubagents])
|
|
10772
|
+
var limitedCount = React.useMemo(function () {
|
|
10773
|
+
return data.subagents.filter(function (p) { return (p.tools && p.tools.length) || (p.toolsDeny && p.toolsDeny.length) || (p.toolsByPreset && Object.keys(p.toolsByPreset).length) }).length
|
|
10774
|
+
}, [data.subagents])
|
|
10775
|
+
return React.createElement('section', { className: 'dsm-section' },
|
|
10776
|
+
React.createElement('div', { className: 'dsm-head' },
|
|
10777
|
+
React.createElement('div', { className: 'dsm-title-block' },
|
|
10778
|
+
React.createElement('div', { className: 'dsm-title-row' },
|
|
10779
|
+
React.createElement('h2', { className: 'dsm-title' }, t('subagents.title'))),
|
|
10780
|
+
React.createElement('p', { className: 'dsm-desc' }, t('subagents.desc'))),
|
|
10781
|
+
React.createElement('div', { className: 'dsm-actions' },
|
|
10782
|
+
refreshButton(t, busy || data.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }),
|
|
10783
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { openEditor(null) } }, t('subagents.new')),
|
|
10784
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { setResult(null); setModal({ type: 'import' }) } }, t('subagents.import')),
|
|
10785
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || !data.subagents.length, onClick: function () { setResult(null); setModal({ type: 'export' }) } }, t('export.subagents')),
|
|
10786
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
|
|
10787
|
+
anyLocked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('lock.banner')) : null,
|
|
10788
|
+
// 锁定期间不显示场景那条长句:开关全是灰的,"改动会同步写进档案"没有落点,
|
|
10789
|
+
// 两条并排还会互相矛盾(用户 2026-09-19)。
|
|
10790
|
+
(anyLocked !== true && sceneName) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('scene.switch.banner', { scene: sceneName })) : null,
|
|
10791
|
+
React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '2' } },
|
|
10792
|
+
[['total', data.subagents.length, t('subagents.stat.total')],
|
|
10793
|
+
['limited', limitedCount, t('subagents.stat.limited')]].map(function (item) {
|
|
10794
|
+
return React.createElement('div', { key: item[0], className: 'dsm-stat' },
|
|
10795
|
+
React.createElement('strong', null, item[1]), item[2])
|
|
10796
|
+
})),
|
|
10797
|
+
// 搜索框:统计条之后(与 MCP / 技能 / 记忆 / 提示词页同一位置与样式)。
|
|
10798
|
+
React.createElement('div', { className: 'dsm-filters' },
|
|
10799
|
+
React.createElement('input', {
|
|
10800
|
+
className: 'dsm-control dsm-search', value: query, 'aria-label': t('search'),
|
|
10801
|
+
placeholder: t('subagents.search.placeholder'),
|
|
10802
|
+
onChange: function (e) { setQuery(e.target.value) },
|
|
10803
|
+
})),
|
|
10804
|
+
React.createElement(Notice, { kind: result && result.warning ? 'warn' : result && result.ok ? 'ok' : 'err', text: result && result.text }),
|
|
10805
|
+
// 走 Notice 而不是裸 div:令牌没过的提醒会在右侧自动多出一颗「填写令牌」按钮。
|
|
10806
|
+
data.error ? React.createElement(Notice, { kind: 'err', text: String(data.error) }) : null,
|
|
10807
|
+
// 注册失败的工具(`subagent_manager_list` / `_run`):模型侧只会「查无此工具」,
|
|
10808
|
+
// 这条横幅是用户唯一能看到的地方。名字与原因来自服务端,句子在这里按当前语言拼。
|
|
10809
|
+
(data.toolFailures || []).length ? React.createElement(Notice, {
|
|
10810
|
+
kind: 'warn',
|
|
10811
|
+
text: t('subagents.toolFailed', {
|
|
10812
|
+
names: (data.toolFailures || []).map(function (f) { return f.name }).join('、'),
|
|
10813
|
+
reason: String(((data.toolFailures || [])[0] || {}).reason || ''),
|
|
10814
|
+
}),
|
|
10815
|
+
}) : null,
|
|
10816
|
+
data.loading && !data.subagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
10817
|
+
: !data.subagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('subagents.empty'))
|
|
10818
|
+
: !visibleSubagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('subagents.empty.search'))
|
|
10819
|
+
: React.createElement('div', { className: 'dsm-sources', ref: flipRef }, orderedSubagents.map(function (p) {
|
|
10820
|
+
return React.createElement('div', { key: p.name, className: 'dsm-source', 'data-flip-key': p.name, 'data-flip-on': p.enabled !== false ? '1' : '0' },
|
|
10821
|
+
React.createElement('div', { className: 'dsm-source-head' },
|
|
10822
|
+
React.createElement('div', { className: 'dsm-source-head-main dsm-persona-main' },
|
|
10823
|
+
React.createElement('div', { className: 'dsm-persona-name-row' },
|
|
10824
|
+
React.createElement('span', { className: 'dsm-source-title', title: p.name }, p.name),
|
|
10825
|
+
// 两个状态都给胶囊:以前只在停用时挂标签,启用的一行什么都不留,
|
|
10826
|
+
// 用户没法一眼看出「没标签 = 启用」。
|
|
10827
|
+
p.enabled === false
|
|
10828
|
+
? React.createElement(StatusTag, { tone: 'muted', text: t('subagents.disabled'), title: t('subagents.disabled.hint') })
|
|
10829
|
+
: React.createElement(StatusTag, { tone: 'ok', text: t('subagents.enabled') })),
|
|
10830
|
+
// 没有描述就不渲染这一行:空 div 仍占一行行高,行会变成一条无内容的空隙。
|
|
10831
|
+
p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
|
|
10832
|
+
React.createElement('div', { className: 'dsm-source-actions' },
|
|
10833
|
+
React.createElement(Switch, { on: p.enabled !== false, disabled: busy || anyLocked, label: t('subagents.toggle') + ' ' + p.name, onClick: function () { togglePersona(p) } }),
|
|
10834
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || anyLocked, onClick: function () { openEditor(p.name) } }, t('memory.edit')),
|
|
10835
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy || anyLocked, onClick: function () { setModal({ type: 'delete', name: p.name }) } }, t('memory.delete')))))
|
|
10836
|
+
})),
|
|
10837
|
+
modal && modal.type === 'editor' ? React.createElement(Modal, { key: 'sedit', wide: true, title: modal.mode === 'create' ? t('subagents.create') : t('subagents.edit') + ' · ' + modal.form.name, closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
|
|
10838
|
+
React.createElement('div', { className: 'dsm-form' },
|
|
10839
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
10840
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.name')),
|
|
10841
|
+
React.createElement('input', { className: 'dsm-control', value: modal.form.name || '', placeholder: 'code-review', onChange: function (e) { setForm({ name: e.target.value }) } })),
|
|
10842
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
10843
|
+
// 描述会整段进「可委派的子智能体」目录,上限就是注入侧的截断长度(`CATALOG_DESC_MAX`);
|
|
10844
|
+
// 存量人设的超长描述标红(服务端不校验这个长度,只有注入时截)。
|
|
10845
|
+
React.createElement('div', { className: 'dsm-budget-meta' },
|
|
10846
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.description')),
|
|
10847
|
+
React.createElement('span', { className: 'dsm-char-count' + (String(modal.form.description || '').length > CATALOG_DESC_MAX ? ' dsm-char-over' : '') }, String(modal.form.description || '').length + ' / ' + CATALOG_DESC_MAX)),
|
|
10848
|
+
React.createElement('input', { className: 'dsm-control', value: modal.form.description || '', maxLength: CATALOG_DESC_MAX, placeholder: t('subagents.field.description.placeholder'), onChange: function (e) { setForm({ description: e.target.value }) } })),
|
|
10849
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
10850
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.body')),
|
|
10851
|
+
React.createElement('textarea', { className: 'dsm-control dsm-textarea-md', value: modal.form.body || '', placeholder: t('subagents.field.body.placeholder'), onChange: function (e) { setForm({ body: e.target.value }) } })),
|
|
10852
|
+
// 输出要求(frontmatter `output:`,一条一行):单独成节写进子代理的系统提示词。
|
|
10853
|
+
// 与正文分开是有意的 —— 正文是"这个角色是什么"(散文),这里放"产出必须长什么样"
|
|
10854
|
+
// (可检验的硬要求)。混在一起时散文会把硬要求稀释成风格提示(用户实测)。
|
|
10855
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
10856
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.output')),
|
|
10857
|
+
React.createElement('textarea', { className: 'dsm-control dsm-textarea-md', value: modal.form.output || '', placeholder: t('subagents.field.output.placeholder'), onChange: function (e) { setForm({ output: e.target.value }) } }),
|
|
10858
|
+
React.createElement('p', { className: 'dsm-help' }, t('subagents.field.output.hint'))),
|
|
10859
|
+
// ── 高级选项(默认收起)──
|
|
10860
|
+
// 模型 / 工具限制是「少数人才改」的字段,但一旦改错代价高(跨来源模型、工具名打错
|
|
10861
|
+
// 会让子代理直接启动失败)。所以:收起来但**有值就自动展开**,并把候选做成选择器。
|
|
10862
|
+
React.createElement('div', { className: 'dsm-adv' },
|
|
10863
|
+
React.createElement('button', { type: 'button', className: 'dsm-adv-head', 'aria-expanded': modal.advanced === true, onClick: toggleAdvanced },
|
|
10864
|
+
React.createElement('span', { className: 'dsm-adv-caret' }, modal.advanced ? '▼' : '▶'),
|
|
10865
|
+
React.createElement('span', { className: 'dsm-adv-title' }, t('subagents.adv.title')),
|
|
10866
|
+
modal.advanced ? null : React.createElement('span', { className: 'dsm-adv-note dsm-adv-sum' },
|
|
10867
|
+
// 摘要**最多两行,且只报有值的那几项**(用户 2026-10-07 定稿):
|
|
10868
|
+
// 第一行是"下一次委派会怎么跑"(模型 / 思考强度 / 允许追问),
|
|
10869
|
+
// 第二行是工具限制的存量 —— 一项都没有时整行不渲染,不拿「0 项」占位置。
|
|
10870
|
+
// 目录注入不进摘要:它只决定目录出现在哪些会话,不影响委派本身。
|
|
10871
|
+
React.createElement('span', null, t('subagents.adv.summary', {
|
|
10872
|
+
model: modal.form.model ? (modal.form.provider ? modal.form.provider + '/' + modal.form.model : modal.form.model) : t('subagents.adv.inherit'),
|
|
10873
|
+
// 思考强度收起来也要看得见:它是"下一次委派会怎么跑"的一部分。
|
|
10874
|
+
// 没配时报「默认」而**不是**「继承」—— 官方语义里换模型会把继承来的那一档
|
|
10875
|
+
// 删掉(`dsh-subagent/lib/index.js:482`),报「继承」是句假话。
|
|
10876
|
+
effort: modal.form.reasoningEffort ? String(modal.form.reasoningEffort) : t('subagents.effort.none'),
|
|
10877
|
+
// 允许追问接在同一行尾巴上:它同样是"会怎么跑",而且是**最容易忘**的一项
|
|
10878
|
+
// (结果变成异步回执)。分隔号写在词典值里,英文侧才不用跟着改代码。
|
|
10879
|
+
tail: modal.form.continuable === true ? t('subagents.adv.continuable') : '',
|
|
10880
|
+
})),
|
|
10881
|
+
advLimitLine() ? React.createElement('span', null, advLimitLine()) : null)),
|
|
10882
|
+
modal.advanced ? React.createElement('div', { className: 'dsm-adv-body' },
|
|
10883
|
+
// 三组:模型 / 运行方式 / 工具限制(0.19.0 重排)。重排前是五个字段平铺、
|
|
10884
|
+
// 九处提示文字混用三种形态(单行 dsm-help / 项目符号 / 选择器副标题),
|
|
10885
|
+
// 展开态读起来是一堵墙(用户 2026-10-07 裁定)。现在:分组头 + 一条细线划开;
|
|
10886
|
+
// 静态说明一律不进表单,只留**状态类**提示(拉取中 / 未选模型 / 拉不到 / 报错)。
|
|
10887
|
+
React.createElement('div', { className: 'dsm-adv-group' },
|
|
10888
|
+
React.createElement('div', { className: 'dsm-adv-group-head' }, t('subagents.adv.group.model')),
|
|
10889
|
+
// 模型来源与模型并排:官方把 (provider, model) 当一对解析,跨来源(如 sensenova)
|
|
10890
|
+
// 必须两个键同时给。顺序按解析顺序 —— 先来源,后模型。
|
|
10891
|
+
// 两个字段并排用 `.dsm-field-row`;`.dsm-combo-row` 是**字段内部**的
|
|
10892
|
+
// "下拉 + 它旁边的东西"(这里只剩下拉,`.dsm-combo-row .dsm-select{flex:1}`
|
|
10893
|
+
// 让它填满整格 —— 缺这条时下拉宽度由选项文字撑出,换来源后标签变短、框跟着缩)。
|
|
10894
|
+
React.createElement('div', { className: 'dsm-field-row' },
|
|
10895
|
+
// 模型来源:去重后的来源清单,只从宿主目录里选(手填通道 2026-10-07 删除)。
|
|
10896
|
+
// 换来源要重拉档位 —— 档位清单是按 (provider, model) 给的。
|
|
10897
|
+
// 换来源**不清模型**:清掉是"悄悄改掉用户已配好的值",而留着会立刻显形 ——
|
|
10898
|
+
// 模型下拉找不到这对 (来源, 模型) 就多列一条带「目录里没有」标记的选项。
|
|
10899
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
10900
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.provider')),
|
|
10901
|
+
React.createElement('div', { className: 'dsm-combo-row' },
|
|
10902
|
+
React.createElement('div', { className: 'dsm-select' },
|
|
10903
|
+
React.createElement('select', {
|
|
10904
|
+
className: 'dsm-control',
|
|
10905
|
+
value: providerSelectValue(),
|
|
10906
|
+
disabled: busy || cand.loading,
|
|
10907
|
+
onChange: function (e) {
|
|
10908
|
+
var v = e.target.value
|
|
10909
|
+
setForm({ provider: v })
|
|
10910
|
+
loadEfforts(v, modal.form.model || '')
|
|
10911
|
+
},
|
|
10912
|
+
},
|
|
10913
|
+
React.createElement('option', { value: '' }, t('subagents.provider.inherit')),
|
|
10914
|
+
(cand.providers || []).map(function (p) {
|
|
10915
|
+
return React.createElement('option', { key: p.id, value: p.id }, providerLabel(p))
|
|
10916
|
+
}),
|
|
10917
|
+
// 目录外的旧值:列一条只读选项,值看得见、要改只能重选目录里的。
|
|
10918
|
+
offCatalogProvider()
|
|
10919
|
+
? React.createElement('option', { value: offCatalogProvider() },
|
|
10920
|
+
catalogReady() ? t('subagents.field.offCatalog', { id: offCatalogProvider() }) : offCatalogProvider())
|
|
10921
|
+
: null)))),
|
|
10922
|
+
// 模型:宿主 LLM 目录里的 (provider, model) 对,只从目录里选。
|
|
10923
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
10924
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.model')),
|
|
10925
|
+
React.createElement('div', { className: 'dsm-combo-row' },
|
|
10926
|
+
React.createElement('div', { className: 'dsm-select' },
|
|
10927
|
+
React.createElement('select', {
|
|
10928
|
+
className: 'dsm-control',
|
|
10929
|
+
value: modelSelectValue(),
|
|
10930
|
+
disabled: busy || cand.loading,
|
|
10931
|
+
onChange: function (e) {
|
|
10932
|
+
var v = e.target.value
|
|
10933
|
+
if (v === '') { setForm({ provider: '', model: '' }); loadEfforts('', '') }
|
|
10934
|
+
else {
|
|
10935
|
+
var parts = v.split('\u0000')
|
|
10936
|
+
setForm({ provider: parts[0], model: parts[1] })
|
|
10937
|
+
// 档位跟模型走:换模型必须重拉(缓存命中时是同步的,不会闪)。
|
|
10938
|
+
loadEfforts(parts[0], parts[1])
|
|
10939
|
+
}
|
|
10940
|
+
},
|
|
10941
|
+
},
|
|
10942
|
+
React.createElement('option', { value: '' }, t('subagents.model.inherit')),
|
|
10943
|
+
modelOptions().map(function (m) {
|
|
10944
|
+
return React.createElement('option', { key: m.provider + '/' + m.id, value: m.provider + '\u0000' + m.id }, modelOptionLabel(m))
|
|
10945
|
+
}),
|
|
10946
|
+
// 目录外的旧值:同来源那条,列一条只读选项。
|
|
10947
|
+
offCatalogModel()
|
|
10948
|
+
? React.createElement('option', { value: offCatalogModel() },
|
|
10949
|
+
catalogReady() ? t('subagents.field.offCatalog', { id: modal.form.model }) : modal.form.model)
|
|
10950
|
+
: null))),
|
|
10951
|
+
// 目录还在读时给一行状态;读完了不占行 —— 这里原来放的是"留空继承主会话"
|
|
10952
|
+
// 那句静态说明,已随 0.19.0 重排移出表单。
|
|
10953
|
+
cand.loading ? React.createElement('p', { className: 'dsm-help' }, t('memory.loading')) : null)),
|
|
10954
|
+
// 思考强度:值就是 adapter 给的档位 id(不校验合法性,官方在 provider I/O 前会拒)。
|
|
10955
|
+
// 单独占左半列(`.dsm-adv-half`),与上一行的列边界对齐。
|
|
10956
|
+
React.createElement('div', { className: 'dsm-field dsm-adv-half' },
|
|
10957
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.effort')),
|
|
10958
|
+
React.createElement('div', { className: 'dsm-select' },
|
|
10959
|
+
React.createElement('select', {
|
|
10960
|
+
className: 'dsm-control',
|
|
10961
|
+
value: String(modal.form.reasoningEffort || ''),
|
|
10962
|
+
disabled: busy || eff.loading || !modal.form.model,
|
|
10963
|
+
onChange: function (e) { setEffNotice(null); setForm({ reasoningEffort: e.target.value }) },
|
|
10964
|
+
},
|
|
10965
|
+
React.createElement('option', { value: '', title: defaultEffortTitle() }, t('subagents.effort.default')),
|
|
10966
|
+
currentEfforts().map(function (x) {
|
|
10967
|
+
return React.createElement('option', { key: x.id, value: x.id, title: x.description || undefined }, x.name)
|
|
10968
|
+
}))),
|
|
10969
|
+
// 状态行只在"这一格为什么用不了 / 没有档位"的四种情况下出现;有档位可选时
|
|
10970
|
+
// 这一行不存在 —— 原来这里常驻的是"当前可选择多种思考强度"那句同义反复。
|
|
10971
|
+
!modal.form.model || eff.loading || eff.error || !currentEfforts().length
|
|
10972
|
+
? React.createElement('p', { className: 'dsm-help' },
|
|
10973
|
+
!modal.form.model
|
|
10974
|
+
? t('subagents.effort.needModel')
|
|
10975
|
+
: eff.loading
|
|
10976
|
+
? t('subagents.effort.loading')
|
|
10977
|
+
: eff.error
|
|
10978
|
+
? String(eff.error)
|
|
10979
|
+
: t('subagents.effort.unavailable'))
|
|
10980
|
+
: null,
|
|
10981
|
+
effNotice ? React.createElement('p', { className: 'dsm-feedback dsm-warning' }, effNotice) : null)),
|
|
10982
|
+
// ── 运行方式:这份人设"什么时候跑、跑在哪些会话里" ──
|
|
10983
|
+
React.createElement('div', { className: 'dsm-adv-group' },
|
|
10984
|
+
React.createElement('div', { className: 'dsm-adv-group-head' }, t('subagents.adv.group.runtime')),
|
|
10985
|
+
// 目录注入深度:人设目录注入到哪些会话(默认 1 = 只在顶层)。用下拉而不是数字
|
|
10986
|
+
// 输入:只有 1/2/3 三个有意义的档,手写数字写错要到注入时才暴露。
|
|
10987
|
+
//
|
|
10988
|
+
// ⚠️ 它**不是**递归上限。2026-09-17 用户实测后改名(原名 maxDepth /「委派预算」):
|
|
10989
|
+
// 官方 `dsh-tool-subagent` 默认 `maxDepth: 3`,子代理本来就能继续嵌套,本插件
|
|
10990
|
+
// 也不再向官方传 maxDepth。这个字段只决定常驻目录出现在哪些深度的会话里。
|
|
10991
|
+
React.createElement('label', { className: 'dsm-field dsm-adv-half' },
|
|
10992
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.catalogDepth')),
|
|
10993
|
+
React.createElement('div', { className: 'dsm-select' },
|
|
10994
|
+
React.createElement('select', {
|
|
10995
|
+
className: 'dsm-control',
|
|
10996
|
+
value: String(typeof modal.form.catalogDepth === 'number' ? modal.form.catalogDepth : 1),
|
|
10997
|
+
disabled: busy,
|
|
10998
|
+
onChange: function (e) { setForm({ catalogDepth: Number(e.target.value) }) },
|
|
10999
|
+
}, [1, 2, 3, CATALOG_DEPTH_UNLIMITED].map(function (n) {
|
|
11000
|
+
// 选项文字不带序号:前面再加一个「2 ·」是同一件事说两遍(用户 2026-09-17
|
|
11001
|
+
// 指出)。下拉的 value 仍是数字,存的还是 catalogDepth 本身。
|
|
11002
|
+
return React.createElement('option', { key: n, value: String(n) }, catalogDepthLabel(n))
|
|
11003
|
+
})))),
|
|
11004
|
+
// 允许追问(frontmatter `continuable: true`):这条通道的**产出是异步的** ——
|
|
11005
|
+
// 委派只回执一个 agent id,子代理在后台跑、完成后自己把结果发回来,
|
|
11006
|
+
// 之后可以用宿主自带的 `send_message` 追问、`interrupt_agent` 中止。
|
|
11007
|
+
// 默认关:它占用宿主"可继续子代理"的名额(`maxActiveSubagents`,默认 8),
|
|
11008
|
+
// 且要求宿主加载了持久化与 session query 两个服务。
|
|
11009
|
+
// 机制与代价的完整说明记在 CHANGELOG / README,不进表单(0.19.0 重排)。
|
|
11010
|
+
React.createElement('label', { className: 'dsm-pick' },
|
|
11011
|
+
React.createElement('input', { type: 'checkbox', checked: modal.form.continuable === true, disabled: busy, onChange: function (e) { setForm({ continuable: e.target.checked === true }) } }),
|
|
11012
|
+
React.createElement('span', { className: 'dsm-pick-main' },
|
|
11013
|
+
React.createElement('span', { className: 'dsm-pick-name' }, t('subagents.field.continuable')),
|
|
11014
|
+
React.createElement('span', { className: 'dsm-pick-desc' }, t('subagents.field.continuable.short'))))),
|
|
11015
|
+
// ── 工具限制:按 Agent 预设一行一个模式(默认全折叠、全部未启动,白/黑互斥)。
|
|
11016
|
+
// 旧格式的全局名单(老文件 / 别处导入)在这里只读呈现,点「转换」才搬进某个模式。
|
|
11017
|
+
React.createElement('div', { className: 'dsm-adv-group' },
|
|
11018
|
+
React.createElement('div', { className: 'dsm-adv-group-head' }, t('subagents.adv.group.tools')),
|
|
11019
|
+
// 这是整块里唯一留下的静态说明,两条理由:它回答"这一屏会不会生效",
|
|
11020
|
+
// 而且藏着一条反直觉的事实(MCP 工具不在候选里)—— 删掉会让人以为能限制 MCP。
|
|
11021
|
+
// 白/黑名单语义那三句已移出表单(词面自解释,按钮与模式摘要里都在用)。
|
|
11022
|
+
React.createElement('p', { className: 'dsm-help' }, t('subagents.field.modes.hint')),
|
|
11023
|
+
legacyNotice(),
|
|
11024
|
+
modeRows()),
|
|
11025
|
+
cand.error ? React.createElement('div', { className: 'dsm-feedback dsm-warning' }, String(cand.error)) : null)
|
|
11026
|
+
: null),
|
|
11027
|
+
modal.form.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(modal.form.error)) : null),
|
|
11028
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
11029
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || (modal.mode === 'create' && !String(modal.form.name || '').trim()) || !String(modal.form.body || '').trim(), onClick: submitEditor }, t('memory.btn.save')))) : null,
|
|
11030
|
+
modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'sdel2', title: t('subagents.delete.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
|
|
11031
|
+
React.createElement('p', { className: 'dsm-help' }, t('subagents.delete.desc', { name: modal.name })),
|
|
11032
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
11033
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDelete(modal.name) } }, t('memory.btn.delete.confirm')))) : null,
|
|
11034
|
+
modal && modal.type === 'import' ? React.createElement(ImportModal, { key: 'simp', t: t, title: t('subagents.import.title'), busy: busy, requirements: [t('upload.requirement.persona.1'), t('upload.requirement.persona.2'), t('upload.requirement.persona.3')], onClose: function () { setModal(null) }, onSubmit: submitImport }) : null,
|
|
11035
|
+
modal && modal.type === 'export' ? React.createElement(ExportModal, {
|
|
11036
|
+
key: 'export', t: t, title: t('export.subagents'),
|
|
11037
|
+
items: data.subagents.map(function (p) { return { key: p.name, name: p.name, desc: p.description || '' } }),
|
|
11038
|
+
busy: busy, result: result,
|
|
11039
|
+
onClose: function () { setResult(null); setModal(null) },
|
|
11040
|
+
onSubmit: function (names, outDir) {
|
|
11041
|
+
setBusy(true); setResult(null);
|
|
11042
|
+
apiCall('bundle-export', { kind: 'subagents', names: names, outDir: outDir }).then(function (res) {
|
|
11043
|
+
setBusy(false);
|
|
11044
|
+
if (res && res.ok) setResult({ ok: true, text: exportResultText(t, res) });
|
|
11045
|
+
else setResult({ ok: false, text: translateError(t, res) });
|
|
11046
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) });
|
|
11047
|
+
},
|
|
11048
|
+
}) : null,
|
|
11049
|
+
trash ? React.createElement(TrashModal, {
|
|
11050
|
+
t: t,
|
|
11051
|
+
title: t('trash.title'),
|
|
11052
|
+
groupTitle: t('trash.group.agents'),
|
|
11053
|
+
groupSub: t('trash.section.agents.sub'),
|
|
11054
|
+
locked: anyLocked,
|
|
11055
|
+
entries: trash.entries,
|
|
11056
|
+
loading: trash.loading,
|
|
11057
|
+
error: trash.error,
|
|
11058
|
+
busy: trashBusy,
|
|
11059
|
+
onClose: function () { setTrash(null) },
|
|
11060
|
+
onRestore: restorePersona,
|
|
11061
|
+
onPurge: purgePersona,
|
|
11062
|
+
}) : null)
|
|
11063
|
+
}
|
|
11064
|
+
|
|
11065
|
+
/**
|
|
11066
|
+
* 记忆页的视图(按场景分桶 + 过滤 + 排序)。
|
|
11067
|
+
*
|
|
11068
|
+
* 提到组件外是为了能被 `useMemo` 包住:这段要遍历全部记忆并逐桶排序,而本页的
|
|
11069
|
+
* 编辑器弹窗每敲一个字都会重渲染整页(`editor` 是页内 state)。传进来的 `q` 已防抖。
|
|
10847
11070
|
*/
|
|
10848
11071
|
function memoryView(data, q, sceneFilter) {
|
|
10849
11072
|
function matchRule(r) {
|