dsh-plugin-tool-management 0.18.5 → 0.19.1
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 +56 -0
- package/README.md +254 -221
- package/README_EN.md +223 -224
- 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/images/logo.png +0 -0
- package/docs/images/promo-cover-EN.png +0 -0
- package/docs/images/promo-cover.png +0 -0
- package/docs/update.md +28 -500
- package/lib/client.js +1857 -913
- 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 +100 -19
- package/lib/op-registry.js +16 -1
- package/lib/ops/candidates.js +1 -1
- package/lib/ops/prompts.js +64 -7
- package/lib/ops/trash-retention.js +55 -0
- package/lib/prompts/service.js +191 -25
- package/lib/scene-prompt-sync.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/scene.js +4 -1
- package/lib/tools/subagent.js +13 -1
- package/lib/trash-retention.js +256 -0
- 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}
|
|
@@ -487,7 +504,25 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
487
504
|
.dsm-pick-group .dsm-pick{margin-left:16px}
|
|
488
505
|
.dsm-pick-group .dsm-pick+.dsm-pick{margin-top:2px}
|
|
489
506
|
|
|
490
|
-
/*
|
|
507
|
+
/* 回收站比其它列表弹窗高一档(用户 2026-10-10:「回收站高度加长」):640 → 760。
|
|
508
|
+
.dsm-modal-list 那个 640 是给「导出会话」那类"看一眼就走"的列表定的;回收站是"翻找 + 勾选 +
|
|
509
|
+
批量删",本机实测提示词回收站 35 条、技能 7 条 —— 640 时一屏只看得见 8 条。
|
|
510
|
+
760 不是新拍的数:它既是 .dsm-modal 自己的 max-height,也是 .dsm-modal-fixed(新增提示词)
|
|
511
|
+
那个数,本插件"最高的普通弹窗"一直就是它。上一版试过 880,用户 2026-10-10 反馈
|
|
512
|
+
「回收站高度不要超过下面一层的高度」—— 回收站是叠在宿主的设置弹窗上弹出来的,宿主那颗比
|
|
513
|
+
880 矮,880 会从它上面探出去;760 稳稳落在它里面。
|
|
514
|
+
视口那一项取 100vh - 112px 而不是 -48px:宿主设置弹窗自己就留了上下各约 48px 的边,
|
|
515
|
+
窗口矮的时候跟着它一起缩,才不会又探出去。
|
|
516
|
+
分栏那两块是「1fr 1fr」吃掉剩余高度,所以弹窗一长高,「目录」和「技能」两块跟着一起长
|
|
517
|
+
(用户同一条要求:「技能的回收站内部的目录和技能也能加高」)。
|
|
518
|
+
写成两段类名(.dsm-modal.dsm-modal-trash)是为了**不靠书写顺序**压过 .dsm-modal-list 的
|
|
519
|
+
!important(与 .dsm-modal-export-lg 同一手法)。加在回收站那几颗弹窗上 —— 共享 TrashModal
|
|
520
|
+
(提示词 / 场景 / 子智能体 / MCP)与技能页自己那颗:.dsm-modal-list 还被四个页面的导出弹窗
|
|
521
|
+
共用,动它就是它们一起变大。 */
|
|
522
|
+
.dsm-modal.dsm-modal-trash{height:min(760px,calc(100vh - 112px))!important;max-height:none!important}
|
|
523
|
+
|
|
524
|
+
/* 回收站分栏:上下两块各占一半(技能页是 目录 / 技能,提示词页是 提示词预设 / 快捷提示词),
|
|
525
|
+
滚动条在每块列表区里面,大弹窗自己不滚。
|
|
491
526
|
必须放在最后:盖过上面的 .dsm-modal-list .dsm-modal-body>*{flex:none},
|
|
492
527
|
否则外层不撑满、两块就各按内容长,50/50 失效。宽度沿用 .dsm-modal-list 的 560px。 */
|
|
493
528
|
.dsm-modal-trash-split .dsm-modal-body{overflow:hidden}
|
|
@@ -599,6 +634,47 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
599
634
|
.dsm-snap-browse-foot{display:flex;align-items:center;gap:8px}
|
|
600
635
|
.dsm-snap-browse-foot .dsm-help{min-width:0;flex:1;overflow-wrap:anywhere}
|
|
601
636
|
|
|
637
|
+
/* 回收站保留期 + 批量删除(0.19.1)。
|
|
638
|
+
工具栏排在条目上方(Modal 的 body 在滚动,工具条跟着滚走的话,条目一多就够不着
|
|
639
|
+
那一颗「永久删除 (N)」—— 而那正是条目多的时候才需要的按钮)。
|
|
640
|
+
保留期与批量条**同一行**(用户 2026-10-10:「这应该是同一行,现在两行浪费空间」):
|
|
641
|
+
工具栏是行向 flex,保留期那一组不参与收缩,批量条那一组吃掉剩余宽度并把两颗按钮顶到最右。
|
|
642
|
+
真放不下时**整组折到下一行** —— 比把「已选 N 项」或说明挤成省略号好:那等于把信息删掉。 */
|
|
643
|
+
.dsm-trash-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin-bottom:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
|
|
644
|
+
.dsm-trash-toolbar .dsm-trash-retention{flex:0 0 auto}
|
|
645
|
+
.dsm-trash-toolbar .dsm-trash-bulk{flex:1 1 auto;min-width:0;flex-wrap:nowrap}
|
|
646
|
+
/* 「已选 N 项」既要撑开(把两颗按钮顶到最右)又不能被压缩:flex:1 0 auto 正是这个意思。 */
|
|
647
|
+
.dsm-trash-toolbar .dsm-trash-bulk .dsm-note{flex:1 0 auto}
|
|
648
|
+
.dsm-trash-toolbar .dsm-trash-bulk>button{flex:0 0 auto}
|
|
649
|
+
/* 二次确认恒占整行:它是一句问句 + 两颗键,挤在保留期旁边既难看也读不清。 */
|
|
650
|
+
.dsm-trash-toolbar .dsm-trash-bulk-confirm{flex:1 1 100%;flex-wrap:wrap}
|
|
651
|
+
.dsm-trash-retention{display:flex;align-items:center;gap:8px;flex-wrap:nowrap}
|
|
652
|
+
.dsm-trash-retention .dsm-label{flex:none;font-weight:580}
|
|
653
|
+
/* .dsm-control 是 width:100% 的表单控件;塞进这一行要退回内容宽度,否则它会把整行撑满、
|
|
654
|
+
把右边的说明挤到下一行。(这段样式整体在模板字符串里,注释里不能出现反引号。) */
|
|
655
|
+
.dsm-trash-retention .dsm-control{width:auto;min-width:112px;flex:none}
|
|
656
|
+
/* 保留期这一行**恒为一行**(用户 2026-10-10:「保留期一行就够了」)。
|
|
657
|
+
所以说明必须短(trash.retention.hint = 「超期自动删除」),全文走 title 悬停;
|
|
658
|
+
nowrap + 省略号只是兜底:万一窄到放不下,宁可截掉尾部也不折行把下拉框顶上去。 */
|
|
659
|
+
.dsm-trash-hint{min-width:0;flex:0 1 auto;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
660
|
+
/* 出错时反过来:允许换行、不截断 —— 失败原因截成半句等于没说。 */
|
|
661
|
+
.dsm-trash-retention-err{flex:1 1 auto;white-space:normal;overflow-wrap:anywhere;color:var(--dsw-alias-state-error-primary)}
|
|
662
|
+
.dsm-trash-bulk{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
663
|
+
/* 「已选 N 项」占满左侧剩余空间 → 把「全选」与唯一的「永久删除」一起顶到最右。 */
|
|
664
|
+
.dsm-trash-bulk .dsm-note{min-width:0;flex:1}
|
|
665
|
+
.dsm-trash-bulk-confirm{padding:6px 8px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover)}
|
|
666
|
+
.dsm-trash-bulk-confirm .dsm-note{font-weight:570;color:var(--dsw-alias-state-error-primary);white-space:normal}
|
|
667
|
+
/* 技能回收站:两个 pane **各有一套**「全选 + 永久删除」(用户 2026-10-10:「全选要拆成两部分」)。
|
|
668
|
+
上面删的是一条记账、下面删的是真文件,合成一颗键就成了「勾了两条、不知道该删哪个」。
|
|
669
|
+
批量条挂在 pane 里、在列表区之上,不跟着列表滚(列表滚走了也要够得着)。 */
|
|
670
|
+
.dsm-trash-pane-batch{flex:none;padding:7px 13px;border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
671
|
+
.dsm-trash-notice{flex:none}
|
|
672
|
+
.dsm-trash-pick{display:inline-flex;flex:none;align-items:center;padding:4px 1px;cursor:pointer}
|
|
673
|
+
.dsm-trash-pick input{width:15px;height:15px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-error-primary)}
|
|
674
|
+
.dsm-trash-pick input:disabled{cursor:default}
|
|
675
|
+
/* 勾选过的那条给一层底色:勾了十几条之后要一眼看出勾的是哪几条。 */
|
|
676
|
+
.dsm-trash-item-picked{background:var(--dsw-alias-interactive-bg-hover);border-radius:8px}
|
|
677
|
+
|
|
602
678
|
`
|
|
603
679
|
|
|
604
680
|
/** 统一的异常文案:Error 取 message,其余(字符串/对象/null)原样 String。此前 63 处各写一遍。 */
|
|
@@ -1155,6 +1231,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1155
1231
|
'history-import': 'compat.audit.op.sessionImport',
|
|
1156
1232
|
'history-export': 'compat.audit.op.sessionExport',
|
|
1157
1233
|
'history-workspace-register': 'compat.audit.op.sessionRegister',
|
|
1234
|
+
// 回收站保留期:与历史页的保留期分开两条文案 —— 一条管归档会话,一条管回收站,
|
|
1235
|
+
// 混着记会让流水读不出「改的到底是哪个页面的保留期」。
|
|
1236
|
+
'trash-retention-set': 'compat.audit.op.trashRetention',
|
|
1158
1237
|
}
|
|
1159
1238
|
/** 每个操作走哪条路线:仅当有"原生委托"能力可用时才算原生。 */
|
|
1160
1239
|
function operationRoutes(data) {
|
|
@@ -3387,13 +3466,35 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3387
3466
|
"error.rules.reservedScene": "「{name}」是保留场景,不可{action}{reason}",
|
|
3388
3467
|
"error.rules.singleSceneOnly": "除「全局」外同时只能启用一个场景{detail}",
|
|
3389
3468
|
"error.agentsMd.sceneRebindFailed": "无法把场景「{scene}」的提示词绑定改成「{target}」:{reason}",
|
|
3390
|
-
"error.agentsMd.referenced": "「{id}
|
|
3469
|
+
"error.agentsMd.referenced": "「{id}」不能删除:{refs}。先解除引用再删 —— 场景绑定的去场景页换绑提示词、或退出场景;正被应用的那份先点左边那颗开关「取消应用」。", "error.agentsMd.refProbeFailed": "无法确认「{id}」是否被引用,已拒绝删除:引用探测失败(场景绑定 / AGENTS.md 当前内容 / 进场景前的基线,这三类里至少有一类没读到)。请稍后重试。", "error.agentsMd.noUnapplyBackup": "没有可恢复的「应用之前的状态」:AGENTS.md 的当前内容不是由「应用」写进去的(例如首次打开时自动生成的那份预设),所以没有起点可回。要改掉文件里的内容:应用别的预设,或直接编辑 AGENTS.md 本身。", "error.agentsMd.unapplyWhileScene": "场景「{scene}」正在驱动全局基线,不能在这里取消应用:先退出场景,或到场景页改掉它的提示词绑定。",
|
|
3391
3470
|
"error.source.reserved": "{origin}:不能停用或移除(里面的技能可以删除)",
|
|
3392
3471
|
"action.rename": "改名", "action.lock": "锁定",
|
|
3393
3472
|
"trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
|
|
3394
3473
|
"trash.btn.open": "回收站", "trash.items.count": "{count} 项", "trash.purge.confirm": "永久删除?",
|
|
3395
3474
|
"trash.group.agents": "人设", "trash.group.scenes": "场景", "trash.group.presets": "提示词预设",
|
|
3396
3475
|
"trash.restored": "已恢复「{name}」", "trash.purged": "已永久删除「{name}」", "trash.loadFailed": "读取回收站失败",
|
|
3476
|
+
// 保留期 + 批量删除(0.19.1)。保留期那几句刻意与 History 页的 hist.retention.* 分开:
|
|
3477
|
+
// 两处管的东西不同(归档会话 vs 回收站),文案合并会让用户以为改了这边那边也跟着变。
|
|
3478
|
+
"trash.retention.label": "保留期",
|
|
3479
|
+
"trash.retention.forever": "永久保留",
|
|
3480
|
+
"trash.retention.days": "{count} 天",
|
|
3481
|
+
"trash.retention.loading": "读取中…",
|
|
3482
|
+
"trash.retention.loadFailed": "读取保留期失败",
|
|
3483
|
+
"trash.retention.hint": "超期自动删除",
|
|
3484
|
+
"trash.retention.hintFull": "超过保留期的条目会被自动永久删除;修改设置会从此刻重新计时,不会立刻清空。",
|
|
3485
|
+
"trash.retention.saved": "已清理 {count} 项",
|
|
3486
|
+
"trash.retention.savedFailed": "已清理 {count} 项,{failed} 项失败(下一轮再试)",
|
|
3487
|
+
"trash.pick.title": "勾选",
|
|
3488
|
+
"trash.pane.dirs.pick.title": "只有「自定义目录」能永久删除记录;内置来源只能恢复读取",
|
|
3489
|
+
"trash.selected": "已选 {count} 项",
|
|
3490
|
+
"trash.selectAll": "全选", "trash.deselectAll": "取消全选",
|
|
3491
|
+
"trash.btn.purgeSelected": "永久删除 ({count})",
|
|
3492
|
+
"trash.purgeSelected.confirm": "永久删除所选的 {count} 个?不可恢复",
|
|
3493
|
+
"trash.btn.confirmPurgeMany": "确认删除 {count} 项",
|
|
3494
|
+
"trash.purgeMany.result": "已永久删除 {count} 个条目",
|
|
3495
|
+
"trash.purgeMany.partial": "已永久删除 {count} 个,{failed} 个失败:{reason}",
|
|
3496
|
+
"trash.forgetMany.result": "已删除 {count} 条来源记录",
|
|
3497
|
+
"trash.forgetMany.partial": "已删除 {count} 条来源记录,{failed} 条失败:{reason}",
|
|
3397
3498
|
"confirm.trash.title": "移到回收站?", "confirm.trash.desc": "「{name}」将移入回收站,可恢复", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "「{name}」将永久删除,无法恢复",
|
|
3398
3499
|
"result.created": "已创建技能:{name}", "result.imported": "导入完成:{names}", "result.importPartial": "已导入:{imported};已跳过同名技能:{skipped}", "result.importSkipped": "未导入任何技能;已跳过同名技能:{names}", "result.importEmpty": "未导入任何技能。", "result.importWarnings": "{result};警告:{warnings}", "result.restored": "已恢复技能:{name}", "result.trashed": "已移到回收站:{name}", "result.deleted": "已永久删除:{name}", "result.updated": "状态已更新。", "result.preferred": "已改为生效:{name}({source})", "result.unpreferred": "已取消同名首选:{name}", "result.opened": "已用系统默认程序打开:{path}", "error.action": "操作失败:{error}",
|
|
3399
3500
|
"warning.scan.truncated": "技能目录较大或嵌套过深,部分技能未显示:{path}", "warning.state.invalid": "状态文件不可读,已安全停用所有技能:{path}", "warning.backupUncleaned": "旧版本备份未清理:{path}({error})", "warning.project.unavailable": "活动会话的项目目录不可访问,项目技能未显示:{path}",
|
|
@@ -3401,7 +3502,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3401
3502
|
"error.source.notFound": "路径不存在: {path}", "error.source.symlink": "不支持符号链接来源:{path}", "error.source.unrecognized": "无法识别的 skill 来源: {path}", "error.source.tooDeep": "skill 来源目录层级超过 {depth} 层: {path}",
|
|
3402
3503
|
"error.import.overlap": "该目录与 DSH 技能目录重叠", "error.import.emptySource": "目录下没有 skill 条目:{path}", "error.import.invalidName": "名称不合法:{name}", "error.import.duplicateName": "批量来源中存在多个同名技能: {name}", "error.import.failed": "导入失败", "error.import.rollbackFailed": "覆盖导入回滚失败,备份保留在: {path}({error})",
|
|
3403
3504
|
"error.upload.path": "上传内容包含非法路径:{path}", "error.upload.encoding": "上传内容编码无效", "error.upload.empty": "上传内容为空", "error.upload.tooMany": "上传文件过多,最多 {limit} 个", "error.upload.tooLarge": "上传内容过大,限制为 {limit} 字节", "error.upload.archiveTooLarge": "ZIP 压缩包过大,限制为 {limit} 字节", "error.upload.duplicate": "上传内容包含重复路径:{path}", "error.upload.zipInvalid": "ZIP 压缩包无法解压",
|
|
3404
|
-
"error.trash.notFound": "回收站条目不存在: {id}", "error.trash.conflict": "无法恢复,同名技能已存在: {name}", "error.trash.invalid": "回收站条目路径非法: {id}", "error.trash.projectUnavailable": "原项目当前不在活动工作区中,无法恢复:{path}", "error.trash.rollbackFailed": "移入回收站回滚失败,未恢复内容保留在: {path}({error})",
|
|
3505
|
+
"error.trash.notFound": "回收站条目不存在: {id}", "error.trash.conflict": "无法恢复,同名技能已存在: {name}", "error.trash.invalid": "回收站条目路径非法: {id}", "error.trash.projectUnavailable": "原项目当前不在活动工作区中,无法恢复:{path}", "error.trash.rollbackFailed": "移入回收站回滚失败,未恢复内容保留在: {path}({error})", "error.trash.retention.invalid": "保留期需为非负整数(0=永久保留),收到: {value}", "error.trash.retention.writeFailed": "写入保留期失败: {reason}",
|
|
3405
3506
|
"error.state.invalid": "技能管理器状态文件不可读,已拒绝覆盖:{path}",
|
|
3406
3507
|
"error.create.descriptionRequired": "技能简介不能为空", "error.create.bodyRequired": "技能正文不能为空", "error.create.tooLarge": "技能内容过长", "error.create.conflict": "同名技能已存在: {name}",
|
|
3407
3508
|
"error.proto.forbidden": "禁止的修改请求(缺少客户端标记)", "error.proto.forbiddenHost": "禁止的请求来源(非法 Host)", "error.proto.contentType": "请求体必须是 application/json", "error.proto.method": "不支持的请求方法", "error.proto.unknownAction": "未知操作", "error.proto.bodyTooLarge": "请求体过大", "error.proto.invalidJson": "请求体不是合法 JSON", "error.proto.nonJson": "服务端返回非 JSON 响应(HTTP {status})",
|
|
@@ -3533,7 +3634,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3533
3634
|
"compat.audit.op.presetSave": "保存提示词", "compat.audit.op.quickSave": "保存快捷提示词", "compat.audit.op.quickSwitch": "启停快捷提示词", "compat.audit.op.quickDelete": "删除快捷提示词", "compat.audit.op.presetDelete": "删除提示词", "compat.audit.op.presetImport": "导入提示词", "compat.audit.op.presetApply": "应用提示词",
|
|
3534
3635
|
"compat.audit.op.trashRestore": "从回收站恢复", "compat.audit.op.trashDelete": "永久删除",
|
|
3535
3636
|
"compat.audit.op.toolTable": "模型工具表", "compat.audit.op.injectSettings": "注入设置", "compat.audit.op.sceneSettings": "场景页设置", "compat.audit.op.backupsClean": "清理旧备份",
|
|
3536
|
-
"compat.audit.op.sessionArchive": "归档会话", "compat.audit.op.sessionUnarchive": "取消归档", "compat.audit.op.sessionDelete": "删除会话", "compat.audit.op.sessionRetention": "改保留策略", "compat.audit.op.sessionImport": "导入会话", "compat.audit.op.sessionExport": "导出会话", "compat.audit.op.sessionRegister": "登记工作区",
|
|
3637
|
+
"compat.audit.op.sessionArchive": "归档会话", "compat.audit.op.sessionUnarchive": "取消归档", "compat.audit.op.sessionDelete": "删除会话", "compat.audit.op.sessionRetention": "改保留策略", "compat.audit.op.trashRetention": "改回收站保留期", "compat.audit.op.sessionImport": "导入会话", "compat.audit.op.sessionExport": "导出会话", "compat.audit.op.sessionRegister": "登记工作区",
|
|
3537
3638
|
"compat.backups.count": "现有 {count} 份,共 {size}",
|
|
3538
3639
|
"compat.backups.del": "删除份数:", "compat.backups.delOption": "{count} 份",
|
|
3539
3640
|
"compat.backups.level.global": "全局", "compat.backups.level.profile": "配置「{name}」", "compat.backups.level.legacy": "早期备份",
|
|
@@ -3807,7 +3908,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3807
3908
|
"compat.live.state.child": "不在本会话注入",
|
|
3808
3909
|
"compat.live.state.error": "取数失败",
|
|
3809
3910
|
"compat.live.state.error.title": "这个域这一轮取数抛了异常(内容还在,是这一步没取到)—— 兼容页「上下文注入通道」有一行降级写着原因。它不会误发「已清空」,下一步会自动重试。",
|
|
3810
|
-
"compat.live.state.child.title": "
|
|
3911
|
+
"compat.live.state.child.title": "本会话是子代理会话,这个域对子会话不成立 —— 两种情形:① **场景与记忆**只在顶层注入(它们说的是父会话的处境,子代理要什么由父代理写进委派说明);② **人设目录**的注入深度不够(本会话深度超过该人设的 `catalogDepth`,默认 1 = 只在顶层),这一种可以把人设的「目录注入」调大或选「不限制嵌套」。两种都不影响委派 —— 子代理始终可以继续委派。",
|
|
3811
3912
|
"compat.live.adopt": "注入 {injected} 次 · 调用 {used} 次 · 采纳 {adopted} 次",
|
|
3812
3913
|
"compat.live.adopt.none": "注入 {injected} 次 · 从未调用",
|
|
3813
3914
|
"compat.live.adopt.title": "只数**最近这一段对话**(换一段重新数,重启归零)。采纳 = 调用发生时该域正文正在这个会话的上下文里;调用次数不区分成功失败",
|
|
@@ -3918,15 +4019,32 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3918
4019
|
"subagents.create": "新建人设", "subagents.edit": "编辑人设",
|
|
3919
4020
|
"subagents.field.name": "人设名",
|
|
3920
4021
|
"subagents.field.description": "描述", "subagents.field.description.placeholder": "例如 擅长 Java 后端实现与重构",
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
//
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
"subagents.field.
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
4022
|
+
// 模型来源 / 模型 / 目录注入三格的静态说明(provider.hint、catalogDepth.hint)随 0.19.0
|
|
4023
|
+
// 重排移出表单:来源与模型并排成一组,谁跟谁是一对由位置表达;目录注入那条只说了半句
|
|
4024
|
+
// "不影响委派",半句不解释"那影响什么",留着比不留更费解。
|
|
4025
|
+
"subagents.field.model": "模型", "subagents.field.provider": "模型来源",
|
|
4026
|
+
"subagents.field.offCatalog": "{id} · 目录里没有",
|
|
4027
|
+
"subagents.field.catalogDepth": "目录注入", "subagents.field.catalogDepth.onlyTop": "只在顶层", "subagents.field.catalogDepth.toChild": "顶层和子会话", "subagents.field.catalogDepth.toGrand": "顶层和两层子会话", "subagents.field.catalogDepth.unlimited": "不限制嵌套",
|
|
4028
|
+
// 允许追问(0.19.0)。界面只留复选框副标题那一行 —— 机制(官方 startContinuable / 结算通知 /
|
|
4029
|
+
// 返回指引)与代价(占用可继续子代理名额、需要持久化与 session query)留在 CHANGELOG 与
|
|
4030
|
+
// README(2026-10-07 用户裁定:高级选项里的提示句太密,长说明一律不进表单)。
|
|
4031
|
+
"subagents.field.continuable": "允许追问(后台运行)", "subagents.field.continuable.short": "回执一个 id,之后还能继续对话",
|
|
4032
|
+
// 收起态摘要用的两个「尾巴」。分隔号写在值里(而不是在代码里 join ';'):英文侧要用
|
|
4033
|
+
// "; ",中文用「;」,写进词典才不用为语言改代码。空串 = 这一项没值,整段不出现。
|
|
4034
|
+
"subagents.adv.continuable": ";允许追问(后台运行)",
|
|
4035
|
+
"subagents.adv.legacy": ";旧格式:白名单 {allow} / 黑名单 {deny}",
|
|
4036
|
+
// 思考强度(0.14.0)。界面上只留"这一格现在能选什么"与**状态**(未选模型 / 拉取中 / 拉不到),
|
|
4037
|
+
// 不解释机制 —— 官方语义(改 provider/model 会丢掉继承值、不支持的档位直接拒)记在
|
|
4038
|
+
// CHANGELOG,不搬进表单。needModel 保留但压到最短:它解释的是"这一格为什么灰着"。
|
|
4039
|
+
"subagents.field.effort": "思考强度", "subagents.effort.default": "默认(模型的默认强度)", "subagents.effort.none": "默认", "subagents.effort.needModel": "先选模型", "subagents.effort.loading": "正在问该模型支持哪些档位…", "subagents.effort.unavailable": "该模型没有暴露可选档位,默认思考强度", "subagents.effort.dropped": "换模型后「{effort}」不在新模型的档位里,已清空。",
|
|
4040
|
+
// 来源下拉的空选项。措辞刻意与模型那条不同("继承主会话"):两者含义一致,但并排两个
|
|
4041
|
+
// 下拉出现同一句话会让人以为点错了格。这一句同时接住被撤掉的那条静态说明
|
|
4042
|
+
// (provider.hint「只填模型会落在主会话的来源上」)要说的事。
|
|
4043
|
+
"subagents.provider.inherit": "不指定(用主会话的来源)",
|
|
4044
|
+
"subagents.model.inherit": "继承主会话(不指定)",
|
|
4045
|
+
// 标题改由高级选项的分组头承担(subagents.adv.group.tools),字段自己的 label 撤掉 ——
|
|
4046
|
+
// 同一句话在 10px 内说两遍("工具限制" + "工具限制(按 Agent 预设)")。
|
|
4047
|
+
"subagents.field.modes.hint": "只有当前模式那一行生效;MCP 工具不在候选里,始终可用",
|
|
3930
4048
|
"subagents.mode.startAllow": "启动白名单", "subagents.mode.startDeny": "启动黑名单", "subagents.mode.stopAllow": "关闭白名单", "subagents.mode.stopDeny": "关闭黑名单",
|
|
3931
4049
|
"subagents.mode.off": "未限制", "subagents.mode.empty": "已启动但没勾选(= 不限制)",
|
|
3932
4050
|
"subagents.mode.allowOn": "白名单 {count} 个", "subagents.mode.denyOn": "黑名单 {count} 个",
|
|
@@ -3939,8 +4057,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3939
4057
|
"subagents.legacy.note": "这个文件里还有一份旧写法的工具限制(白名单 {allow} 个 / 黑名单 {deny} 个),它对所有模式都生效。",
|
|
3940
4058
|
"subagents.legacy.convert": "转换到按模式设置",
|
|
3941
4059
|
"subagents.legacy.hint": "转换会把旧名单搬进你选的模式;不点它,文件里的旧键原样保留(运行时照旧生效)。",
|
|
3942
|
-
"subagents.tools.filter": "筛选工具名", "subagents.tools.remove": "移除", "subagents.toolFailed": "子代理工具没注册上:{names}({reason})。这几个工具在子智能体流程里用不了;重启 DSH 后仍如此请核对宿主版本。",
|
|
3943
|
-
|
|
4060
|
+
"subagents.tools.filter": "筛选工具名", "subagents.tools.remove": "移除", "subagents.tools.group.plugin": "插件工具", "subagents.tools.group.official": "官方工具", "subagents.tools.group.other": "其他工具", "subagents.toolFailed": "子代理工具没注册上:{names}({reason})。这几个工具在子智能体流程里用不了;重启 DSH 后仍如此请核对宿主版本。",
|
|
4061
|
+
// 收起态摘要最多两行、只报有值的项:第一行模型 / 思考强度(+ 允许追问),第二行工具限制。
|
|
4062
|
+
// 目录注入不进摘要 —— 它只决定目录出现在哪些会话,不影响委派本身。
|
|
4063
|
+
"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
4064
|
"preset.name.standard": "标准模式", "preset.name.ptc": "PTC模式", "preset.name.minimal": "极简模式", "preset.name.cordis": "创造模式",
|
|
3945
4065
|
"preset.short.standard": "标准", "preset.short.ptc": "PTC", "preset.short.minimal": "极简", "preset.short.cordis": "创造",
|
|
3946
4066
|
"subagents.field.body": "人设提示词", "subagents.field.body.placeholder": "写下这个人设的身份、职责与工作方式…",
|
|
@@ -3958,7 +4078,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3958
4078
|
"import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
|
|
3959
4079
|
"error.import.noFiles": "没有选择要导入的文件",
|
|
3960
4080
|
"nav.title": "工具",
|
|
3961
|
-
"agm.stat.total": "个全局提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.empty.search": "没有匹配的提示词", "agm.search.placeholder": "搜索提示词 id 或描述", "agm.btn.delete.blocked.refs": "不能删除:{refs}
|
|
4081
|
+
"agm.stat.total": "个全局提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.empty.search": "没有匹配的提示词", "agm.search.placeholder": "搜索提示词 id 或描述", "agm.btn.delete.blocked.refs": "不能删除:{refs}。先解除引用再删 —— 场景绑定的去场景页换绑提示词、或退出场景;正被应用的那份先点左边那颗开关「取消应用」。", "agm.ref.sep": ";", "agm.ref.scene": "场景「{scene}」绑定了它", "agm.ref.sceneActive": "场景「{scene}」(已启用)绑定了它", "agm.ref.file": "AGENTS.md 当前内容就是它", "agm.ref.fileApplied": "它正被应用在 AGENTS.md 里(当前生效的那一份)", "agm.ref.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "场景「{scene}」接管中:当前生效的是它绑定的「{id}」。在这里应用别的预设 = 把该场景改绑到那一份(会同步写入场景档案)。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.apply.switch": "应用提示词", "agm.apply.switch.on": "当前生效的就是这一份 · 点一下重新写回 AGENTS.md(撤销手动改动)", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.btn.import": "导入提示词", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、点都可以;不能含斜杠、反斜杠与 : * ? < > 等符号;≤64 字符。改 id 等于目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.desc": "描述(模型在清单里看得到)", "agm.field.desc.placeholder": "这份预设是干什么的、什么时候用它", "agm.result.renamed": "已保存,id 改为「{id}」", "agm.result.rebound": "(同步了 {count} 个场景的绑定)", "agm.remove.suffix": " ?此操作不可恢复。", "agm.apply.prefix": "将把 ", "agm.apply.suffix": " 的内容写入 ~/.dsh/AGENTS.md。", "agm.apply.note": "下一轮对话生效;当前内容已备份到 __last-applied__(保留最近 5 代)", "agm.unapply.title": "取消应用", "agm.unapply.body": "将把 ~/.dsh/AGENTS.md 恢复成开始应用提示词之前的那份内容(__last-applied__ 里保存的),此后不再有任何提示词预设生效。", "agm.unapply.note": "回到的是开始应用之前的那一份,中途应用过别的预设也一并撤销 —— 想换成其中某一份,直接点它自己的开关即可(效果等价)。这一步本身也会备份,可以再应用回来。", "agm.btn.confirmUnapply": "确认取消应用", "agm.unapply.switch.on": "AGENTS.md 里就是这一份 · 点一下取消应用(把 AGENTS.md 恢复成开始应用之前的内容)", "agm.unapply.switch.blocked": "AGENTS.md 里就是这一份,但没有「应用之前的状态」可恢复 —— 它不是由「应用」写进去的,所以关不掉。要改掉文件内容请应用别的预设或直接编辑 AGENTS.md;删除这份预设不会改动 AGENTS.md", "agm.unapply.switch.onRemove": "AGENTS.md 里就是这一份 · 点一下取消应用(开始应用之前没有 AGENTS.md,这一步会把它删掉)", "agm.unapply.bodyRemove": "开始应用提示词之前 ~/.dsh/AGENTS.md 并不存在,所以取消应用会把这个文件删掉(而不是写回内容),此后不再有任何提示词预设生效。", "agm.unapply.noteRemove": "删掉的是「应用」写进去的内容;这一份已备份到 __last-applied__(保留最近 5 代),需要时可以再应用回来。", "agm.btn.confirmUnapplyRemove": "确认取消应用并删除文件", "agm.result.unapplied": "已取消应用:AGENTS.md 已恢复成开始应用之前的内容", "agm.result.unappliedRemove": "已取消应用:AGENTS.md 已删除(开始应用之前它不存在)", "agm.unapply.unavailable": "AGENTS.md 的当前内容不是由「应用」写进去的(例如首次打开时自动生成的那份预设),没有「应用之前的状态」可恢复,所以关不掉。要改掉文件里的内容:应用别的预设,或直接编辑 AGENTS.md 本身;删除这份预设不会改动 AGENTS.md。", "agm.remove.fileNote": "注意:AGENTS.md 里仍然是这份内容,全局提示词照旧生效 —— 删除预设只把它移进回收站,不会改动 AGENTS.md。要让它不再生效,请改 AGENTS.md 本身或应用别的预设。", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.err.archive": "归档失败", "hist.retention.forever": "永久保留", "hist.retention.days": "{count} 天", "hist.arch.all": "全部会话", "hist.arch.archived": "仅已归档", "hist.arch.live": "仅未归档", "hist.arch.missing": "仅目录丢失", "hist.ws.all": "全部工作区", "hist.ungrouped": "未分组", "hist.group.removed": "已移除", "hist.group.removed.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建,重新登记后会自动并回同一组", "hist.group.dirMissing": "目录已不存在", "hist.group.dirMissing.hint": "工作区目录已从磁盘删除,无法重新登记;组内会话仍可恢复(归入未分组)或永久删除", "hist.btn.registerWs": "重新登记", "hist.btn.registerWs.hint": "重新登记这个目录,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "全局提示词",
|
|
4082
|
+
"scenes.field.prompt.none": "无(不绑定全局提示词)", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.agents.note.baselineLost": "⚠ 退出场景时将恢复不回原来的 AGENTS.md:{reason}", "scenes.field.prompt.hint": "为当前场景设置全局提示词", "scenes.prompt.activeTag": "生效中", "scenes.prompt.noPresets": "还没有提示词预设;可先去「提示词」页新建一个。", "scenes.prompt.bound": "提示词:{id}", "scenes.prompt.live": "提示词生效中:{id}", "scenes.prompt.missing": "预设不存在:{id}", "scenes.prompt.tag.hint": "这个场景绑定的提示词预设", "scenes.prompt.mismatch.hint": "场景绑定的「{id}」与 AGENTS.md 当前内容不一致(可能被手动改过);到提示词页对它点「重新应用」写回", "scenes.enable.blocked": "已启用场景「{name}」;先关掉它才能启用别的", "scenes.result.created.collapsed": "场景「{name}」已创建,默认不启动(原先的「全部启用」已收敛为单选)", "scenes.legacyAll": "检测到 {count} 个场景同时处于启用状态(历史「全部启用」遗留);除「全局」外同时只能启用一个。", "scenes.legacyAll.fix": "收敛为单选", "scenes.preview.title": "进入场景「{name}」前,先看一眼会改什么", "scenes.preview.loading": "正在计算这次会改什么…", "scenes.preview.failed": "预览没算出来:{error}", "scenes.preview.enter": "确认进入", "scenes.preview.dismiss": "不再显示", "scenes.preview.dismiss.title": "关闭这张卡,并且以后进入场景前不再弹(想恢复用页面右上角的「开启提醒」)", "scenes.preview.toggle.off": "关闭提醒", "scenes.preview.toggle.on": "开启提醒", "scenes.preview.toggle.title": "进入场景前先弹一张「会改什么」的卡", "scenes.preview.nochange": "什么都不用改:当前运行时已经和这个场景要的一致了。", "scenes.preview.noArchive": "这个场景还没保存过档案 —— MCP / 技能 / 人设三个域都按「一个都没勾」处理,进入即全部停用。", "scenes.preview.exitFirst": "先退出「{scene}」:它改过的东西按进场景前的快照还原。", "scenes.preview.exitNoSnapshot": "当前在场景「{scene}」里,但进场景前的快照已丢失:退出只能清掉模式标记,运行时回不到原样。", "scenes.preview.row.serversOn": "启用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.serversOff": "停用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.toolsOn": "放回 {count} 个 MCP 工具:{names}", "scenes.preview.row.toolsOff": "停用 {count} 个 MCP 工具:{names}", "scenes.preview.row.sourcesOn": "启用 {count} 个技能目录:{names}", "scenes.preview.row.sourcesOff": "停用 {count} 个技能目录:{names}", "scenes.preview.row.skillsOn": "启用 {count} 个技能:{names}", "scenes.preview.row.skillsOff": "停用 {count} 个技能:{names}", "scenes.preview.row.personasOn": "启用 {count} 个人设:{names}", "scenes.preview.row.personasOff": "停用 {count} 个人设:{names}", "scenes.preview.row.quickOn": "启用 {count} 条快捷提示词:{names}", "scenes.preview.row.quickOff": "停用 {count} 条快捷提示词:{names}", "scenes.preview.enter.servers": "启用 {count} 台 MCP:{names}", "scenes.preview.enter.memories": "启用 {count} 条记忆:{names}", "scenes.preview.row.notes": "用场景备注覆盖 {count} 台服务器的备注", "scenes.field.toolTable": "模型工具表方案", "scenes.field.toolTable.hint": "为当前场景设置可使用的模型工具方案", "scenes.field.quickPrompts": "快捷提示词", "scenes.field.quickPrompts.current": "当前启用的(保持现状)", "scenes.field.quickPrompts.custom": "自定义", "scenes.field.quickPrompts.none": "还没有快捷提示词,先到「提示词」页新增",
|
|
4083
|
+
"scenes.field.quickPrompts.noneOption": "无(不设置快捷提示词)", "scenes.field.quickPrompts.hint": "为当前场景设置快捷提示词", "scenes.field.quickPrompts.list": "本场景启用", "scenes.form.section.basic": "基本信息", "scenes.form.section.binding": "场景绑定", "scenes.field.toolTable.none": "不绑定(保持现状)", "scenes.field.toolTable.factory": "出厂默认(关 {count} 条)", "scenes.field.toolTable.count": "关 {count} 条", "scenes.field.toolTable.missing": "方案已不存在", "scenes.preview.row.toolPreset": "工具表将切到方案「{name}」(关 {count} 条)", "scenes.preview.row.toolPresetMissing": "工具表绑的方案「{name}」已不存在,这次不切换", "scenes.preview.more": " 等", "scenes.preview.truncated": "(每一类最多列前 60 项)", "scenes.preview.stale": "档案里有 {count} 项在当前环境里已经不存在,这次不会生效:{items}", "scenes.doctor.title": "有 {count} 处引用指向已经不存在的名字", "scenes.doctor.noneButSkipped": "体检没发现悬空引用", "scenes.doctor.hint": "改名或删除留下的:这一条看着还绑着,运行时按那个名字已经找不到东西了。改回正确的名字,或到档案里取消勾选。", "scenes.doctor.row": "{where}:「{name}」已不存在", "scenes.doctor.more": "另有 {count} 处未列出", "scenes.doctor.skipped": "({domains} 这次没读到,这一类没有算进体检)", "agm.restore.tag": "退出场景后恢复它", "agm.restore.tag.hint": "它是进场景前的全局提示词,退出场景时会按快照里的正文恢复;快照存的是正文,删这份预设不影响恢复", "agm.file.mismatch": "文件里不是它", "agm.file.mismatch.hint": "AGENTS.md 当前内容与这份预设不一致(可能被手动改过);点左边那颗开关把它写回", "hist.restore.note.registered": "已恢复 {count} 个会话;工作区「{title}」已重新登记,会话已挂回该组。", "hist.restore.note.attached": "已恢复 {count} 个会话,并挂回工作区「{title}」。", "hist.restore.note.skip": "{count} 个会话的工作区归属未能恢复:{reason}", "hist.purge.title": "永久删除?", "hist.purge.one": "永久删除会话「{title}」及其全部记录,不可恢复", "hist.purge.batch": "永久删除所选 {count} 个会话及其全部记录,不可恢复", "hist.btn.restore": "恢复", "hist.btn.purge": "永久删除", "hist.btn.confirmPurge": "确认删除", "hist.selectGroup": "全选组 {title}", "hist.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入会话", "hist.btn.export": "导出会话", "hist.selected": "已选 {count} 项", "hist.btn.restoreSelected": "恢复所选 ({count})", "hist.btn.deleteSelected": "删除所选 ({count})", "hist.stat.archived": "个归档", "hist.stat.projects": "个项目", "hist.stat.retention": "天保留", "hist.search.placeholder": "搜索标题 / 会话 ID / 项目路径", "hist.loading": "加载中…", "hist.empty.search": "无匹配的归档会话", "hist.empty.none": "暂无归档会话", "hist.import.cwd": "工作区(可留空)", "hist.import.cwd.placeholder": "选择、浏览或直接输入绝对路径;留空 = 归入未分组", "hist.import.workspace": "选工作区", "hist.import.browse": "浏览", "hist.import.workspace.none": "本机还没有已登记的工作区:可以直接输入绝对路径,或用「浏览」选一个文件夹。", "hist.import.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.import.lossy": "⚠ 整份文本没有识别出任何说话人标记,已折成一条用户消息:原对话的分轮结构可能已丢失。若源文件是 JSONL 或 Markdown 转录稿,请按原扩展名重新导入。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "hist.export.detail": "导出内容", "hist.export.detail.simple": "仅正文", "hist.export.detail.tools": "正文 + 工具", "hist.export.detail.full": "正文 + 工具 + 思考", "hist.export.scope": "会话范围", "hist.export.workspace": "工作区", "hist.export.sessions": "会话({count} 个)", "hist.export.loading": "加载会话列表…", "hist.cwd.missing": "⚠ 工作区目录已不存在:", "hist.tag.archived": "已归档", "hist.tag.live": "未归档", "hist.export.empty": "没有符合条件的会话", "hist.result.exported": "已导出 {count} 个会话到 {dir}", "hist.result.skipped": ";跳过 {count} 个({ids})", "hist.result.archived": "已归档 {count} 个会话到 History,可到 History 页继续管理", "hist.result.archiveFailed": ";其中 {count} 个未能归档", "hist.btn.archiveSelected": "归档所选", "hist.btn.archiveSelected.title": "把选中的会话收进 History,纳入保留期管理", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
|
|
3962
4084
|
},
|
|
3963
4085
|
en: {
|
|
3964
4086
|
"title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
|
|
@@ -4011,13 +4133,33 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4011
4133
|
"error.rules.reservedScene": "「{name}」 is a reserved scene: {action} is not allowed{reason}",
|
|
4012
4134
|
"error.rules.singleSceneOnly": "Only one scene besides 「global」 may be enabled at a time{detail}",
|
|
4013
4135
|
"error.agentsMd.sceneRebindFailed": "Cannot change the prompt binding of scene 「{scene}」 to 「{target}」: {reason}",
|
|
4014
|
-
"error.agentsMd.referenced": "
|
|
4136
|
+
"error.agentsMd.referenced": "“{id}” cannot be deleted: {refs}. Remove that reference first — for a scene binding, rebind the prompt on the scene page or leave the scene; for the one currently applied, flip the switch on the left to un-apply.", "error.agentsMd.refProbeFailed": "Cannot tell whether “{id}” is still referenced, so the delete was refused: the reference probe failed (at least one of scene bindings / the current AGENTS.md content / the pre-scene baseline could not be read). Please try again later.", "error.agentsMd.noUnapplyBackup": "There is no “state from before the apply” to restore: the current AGENTS.md was not written by “Apply” (the preset created automatically the first time you open this page is one such case), so there is no starting point to go back to. To change what AGENTS.md holds: apply another preset, or edit AGENTS.md itself.", "error.agentsMd.unapplyWhileScene": "Scene “{scene}” is driving the global baseline, so it cannot be un-applied here: leave the scene first, or change its prompt binding on the Scenes page.",
|
|
4015
4137
|
"error.source.reserved": "{origin}: cannot be disabled or removed (the skills inside can still be deleted)",
|
|
4016
4138
|
"action.rename": "rename", "action.lock": "locking",
|
|
4017
4139
|
"trash.title": "Trash", "trash.count.one": "{count} skill pending", "trash.count.other": "{count} skills pending", "trash.empty": "Trash is empty", "trash.deletedAt": "Deleted {time}", "trash.source": "Source: {source}",
|
|
4018
4140
|
"trash.btn.open": "Trash", "trash.items.count": "{count} item(s)", "trash.purge.confirm": "Delete forever?",
|
|
4019
4141
|
"trash.group.agents": "Personas", "trash.group.scenes": "Scenes", "trash.group.presets": "Prompt presets",
|
|
4020
4142
|
"trash.restored": "Restored “{name}”", "trash.purged": "Permanently deleted “{name}”", "trash.loadFailed": "Could not read the trash",
|
|
4143
|
+
"trash.retention.label": "Retention",
|
|
4144
|
+
"trash.retention.forever": "Keep forever",
|
|
4145
|
+
"trash.retention.days": "{count} days",
|
|
4146
|
+
"trash.retention.loading": "Loading…",
|
|
4147
|
+
"trash.retention.loadFailed": "Could not read the retention setting",
|
|
4148
|
+
"trash.retention.hint": "Auto-delete when expired",
|
|
4149
|
+
"trash.retention.hintFull": "Items older than the retention period are deleted forever automatically; changing this setting restarts the countdown from now, so nothing is wiped immediately.",
|
|
4150
|
+
"trash.retention.saved": "Cleaned up {count} item(s)",
|
|
4151
|
+
"trash.retention.savedFailed": "Cleaned up {count}; {failed} failed (will retry next round)",
|
|
4152
|
+
"trash.pick.title": "Select",
|
|
4153
|
+
"trash.pane.dirs.pick.title": "Only custom directories can be deleted; built-in sources can only be restored",
|
|
4154
|
+
"trash.selected": "{count} selected",
|
|
4155
|
+
"trash.selectAll": "Select all", "trash.deselectAll": "Deselect all",
|
|
4156
|
+
"trash.btn.purgeSelected": "Delete ({count})",
|
|
4157
|
+
"trash.purgeSelected.confirm": "Delete the {count} selected forever? This cannot be undone",
|
|
4158
|
+
"trash.btn.confirmPurgeMany": "Delete {count}",
|
|
4159
|
+
"trash.purgeMany.result": "Permanently deleted {count} item(s)",
|
|
4160
|
+
"trash.purgeMany.partial": "Permanently deleted {count}; {failed} failed: {reason}",
|
|
4161
|
+
"trash.forgetMany.result": "Removed {count} source record(s)",
|
|
4162
|
+
"trash.forgetMany.partial": "Removed {count} source record(s); {failed} failed: {reason}",
|
|
4021
4163
|
"confirm.trash.title": "Move to trash?", "confirm.trash.desc": "“{name}” moves to trash and can be restored", "confirm.delete.title": "Delete forever?", "confirm.delete.desc": "“{name}” is deleted forever and cannot be recovered",
|
|
4022
4164
|
"result.created": "Created skill: {name}", "result.imported": "Import complete: {names}", "result.importPartial": "Imported: {imported}; skipped existing skills: {skipped}", "result.importSkipped": "No skills were imported; existing skills were skipped: {names}", "result.importEmpty": "No skills were imported.", "result.importWarnings": "{result}; warnings: {warnings}", "result.restored": "Restored skill: {name}", "result.trashed": "Moved to trash: {name}", "result.deleted": "Permanently deleted: {name}", "result.updated": "Status updated.", "result.preferred": "Now active: {name} ({source})", "result.unpreferred": "Cleared the manual choice: {name}", "error.action": "Action failed: {error}",
|
|
4023
4165
|
"warning.scan.truncated": "Some skills were not shown because the directory is too large or deeply nested: {path}", "warning.state.invalid": "The manager state file could not be read; all skills are disabled and state writes are blocked until it is repaired: {path}", "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})", "warning.project.unavailable": "The project directory of an active session is not accessible, so its project skills are hidden: {path}",
|
|
@@ -4025,7 +4167,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4025
4167
|
"error.source.notFound": "Path does not exist: {path}", "error.source.symlink": "Symlinked sources are not supported: {path}", "error.source.unrecognized": "Unrecognized skill source: {path}", "error.source.tooDeep": "Skill source directory depth exceeds {depth} levels: {path}",
|
|
4026
4168
|
"error.import.overlap": "This folder overlaps the DSH skills folder", "error.import.emptySource": "No skill entries in this folder: {path}", "error.import.invalidName": "Invalid name: {name}", "error.import.duplicateName": "Batch source contains duplicate skill names: {name}", "error.import.failed": "Import failed", "error.import.rollbackFailed": "Overwrite import rollback failed; backups kept at: {path} ({error})",
|
|
4027
4169
|
"error.upload.path": "Upload contains an invalid path: {path}", "error.upload.encoding": "Upload encoding is invalid", "error.upload.empty": "Upload is empty", "error.upload.tooMany": "Too many uploaded files; maximum {limit}", "error.upload.tooLarge": "Upload is too large; limit {limit} bytes", "error.upload.archiveTooLarge": "ZIP archive is too large; limit {limit} bytes", "error.upload.duplicate": "Upload contains a duplicate path: {path}", "error.upload.zipInvalid": "ZIP archive could not be extracted",
|
|
4028
|
-
"error.trash.notFound": "Trash item not found: {id}", "error.trash.conflict": "Cannot restore because a skill with the same name exists: {name}", "error.trash.invalid": "Invalid trash item path: {id}", "error.trash.projectUnavailable": "The original project is not an active workspace, so this skill cannot be restored: {path}", "error.trash.rollbackFailed": "Move-to-trash rollback failed; unrecovered content was kept at: {path} ({error})",
|
|
4170
|
+
"error.trash.notFound": "Trash item not found: {id}", "error.trash.conflict": "Cannot restore because a skill with the same name exists: {name}", "error.trash.invalid": "Invalid trash item path: {id}", "error.trash.projectUnavailable": "The original project is not an active workspace, so this skill cannot be restored: {path}", "error.trash.rollbackFailed": "Move-to-trash rollback failed; unrecovered content was kept at: {path} ({error})", "error.trash.retention.invalid": "Retention must be a non-negative integer (0 = keep forever); got: {value}", "error.trash.retention.writeFailed": "Could not save the retention setting: {reason}",
|
|
4029
4171
|
"error.state.invalid": "The manager state file could not be read, so overwriting it was refused: {path}",
|
|
4030
4172
|
"error.create.descriptionRequired": "Skill description is required", "error.create.bodyRequired": "Skill body is required", "error.create.tooLarge": "Skill content is too large", "error.create.conflict": "A skill with the same name already exists: {name}",
|
|
4031
4173
|
"error.proto.forbidden": "Forbidden mutation request (missing client marker)", "error.proto.forbiddenHost": "Forbidden request origin (invalid host)", "error.proto.contentType": "Content type must be application/json", "error.proto.method": "Method not allowed", "error.proto.unknownAction": "Unknown action", "error.proto.bodyTooLarge": "Request body too large", "error.proto.invalidJson": "Invalid JSON request body", "error.proto.nonJson": "Server returned a non-JSON response (HTTP {status})",
|
|
@@ -4157,7 +4299,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4157
4299
|
"compat.audit.op.presetSave": "Saved a prompt", "compat.audit.op.quickSave": "Saved a quick prompt", "compat.audit.op.quickSwitch": "Toggled a quick prompt", "compat.audit.op.quickDelete": "Deleted a quick prompt", "compat.audit.op.presetDelete": "Deleted a prompt", "compat.audit.op.presetImport": "Imported a prompt", "compat.audit.op.presetApply": "Applied a prompt",
|
|
4158
4300
|
"compat.audit.op.trashRestore": "Restored from trash", "compat.audit.op.trashDelete": "Deleted forever",
|
|
4159
4301
|
"compat.audit.op.toolTable": "Model tool table", "compat.audit.op.injectSettings": "Injection settings", "compat.audit.op.sceneSettings": "Scenes page settings", "compat.audit.op.backupsClean": "Cleaned old backups",
|
|
4160
|
-
"compat.audit.op.sessionArchive": "Archived a session", "compat.audit.op.sessionUnarchive": "Unarchived a session", "compat.audit.op.sessionDelete": "Deleted a session", "compat.audit.op.sessionRetention": "Changed retention", "compat.audit.op.sessionImport": "Imported sessions", "compat.audit.op.sessionExport": "Exported sessions", "compat.audit.op.sessionRegister": "Registered a workspace",
|
|
4302
|
+
"compat.audit.op.sessionArchive": "Archived a session", "compat.audit.op.sessionUnarchive": "Unarchived a session", "compat.audit.op.sessionDelete": "Deleted a session", "compat.audit.op.sessionRetention": "Changed retention", "compat.audit.op.trashRetention": "Changed trash retention", "compat.audit.op.sessionImport": "Imported sessions", "compat.audit.op.sessionExport": "Exported sessions", "compat.audit.op.sessionRegister": "Registered a workspace",
|
|
4161
4303
|
"compat.backups.count": "{count} copies, {size} in total",
|
|
4162
4304
|
"compat.backups.del": "How many to delete:", "compat.backups.delOption": "{count}",
|
|
4163
4305
|
"compat.backups.level.global": "Global", "compat.backups.level.profile": "Profile “{name}”", "compat.backups.level.legacy": "Older backups",
|
|
@@ -4426,7 +4568,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4426
4568
|
"compat.live.state.child": "Not injected here",
|
|
4427
4569
|
"compat.live.state.error": "Failed to load",
|
|
4428
4570
|
"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
|
|
4571
|
+
"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
4572
|
"compat.live.adopt": "injected {injected} · called {used} · adopted {adopted}",
|
|
4431
4573
|
"compat.live.adopt.none": "injected {injected} · never called",
|
|
4432
4574
|
"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 +4679,37 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4537
4679
|
"subagents.create": "New persona", "subagents.edit": "Edit persona",
|
|
4538
4680
|
"subagents.field.name": "Persona name",
|
|
4539
4681
|
"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
|
-
|
|
4682
|
+
// The static help lines for provider / model / catalog depth left the form with the 0.19.0
|
|
4683
|
+
// re-layout: provider and model now sit side by side as one group, so the pairing is carried
|
|
4684
|
+
// by position; the catalog-depth line only stated half a fact ("affects the catalog only")
|
|
4685
|
+
// without saying what it does affect, which is more confusing than saying nothing.
|
|
4686
|
+
"subagents.field.model": "Model", "subagents.field.provider": "Model provider",
|
|
4687
|
+
"subagents.field.offCatalog": "{id} · not in the catalogue",
|
|
4688
|
+
"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",
|
|
4689
|
+
// Continuable (0.19.0). The form keeps only the checkbox subtitle; the mechanism (the
|
|
4690
|
+
// official startContinuable, the settlement notice, the return guidance) and the cost
|
|
4691
|
+
// (capacity slot, persistence + session query) live in CHANGELOG and README, not here
|
|
4692
|
+
// (user ruling 2026-10-07: the advanced section carried far too many help sentences).
|
|
4693
|
+
"subagents.field.continuable": "Continuable (runs in the background)", "subagents.field.continuable.short": "Returns an id you can keep talking to",
|
|
4694
|
+
// The two "tails" of the collapsed summary. The separator lives inside the value (not a
|
|
4695
|
+
// hard-coded join in code): English needs "; " where Chinese needs ";". Empty = no value,
|
|
4696
|
+
// so the whole clause disappears.
|
|
4697
|
+
"subagents.adv.continuable": "; continuable (runs in the background)",
|
|
4698
|
+
"subagents.adv.legacy": "; legacy allow: {allow} / deny: {deny}",
|
|
4699
|
+
// Reasoning effort (0.14.0). The form keeps only what this field offers right now plus the
|
|
4700
|
+
// transient states (no model picked / loading / none available); the official semantics (the
|
|
4701
|
+
// host drops the inherited level when provider/model change, and an unsupported level is
|
|
4702
|
+
// rejected rather than clamped) live in CHANGELOG, not here.
|
|
4703
|
+
"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.",
|
|
4704
|
+
// The blank entry of the provider dropdown. Deliberately worded differently from the model
|
|
4705
|
+
// one ("Inherit the main session"): same meaning, but the same sentence in two adjacent
|
|
4706
|
+
// dropdowns reads like a mis-click. This line also carries what the removed static hint
|
|
4707
|
+
// (provider.hint) used to say.
|
|
4708
|
+
"subagents.provider.inherit": "Unspecified (main session's provider)",
|
|
4709
|
+
"subagents.model.inherit": "Inherit the main session (unspecified)",
|
|
4710
|
+
// The heading moved to the advanced section's group head (subagents.adv.group.tools) and the
|
|
4711
|
+
// field's own label was dropped — the same phrase twice within 10px otherwise.
|
|
4712
|
+
"subagents.field.modes.hint": "Only the current preset's row applies; MCP tools are not listed and always stay available",
|
|
4549
4713
|
"subagents.mode.startAllow": "Enable allowlist", "subagents.mode.startDeny": "Enable denylist", "subagents.mode.stopAllow": "Disable allowlist", "subagents.mode.stopDeny": "Disable denylist",
|
|
4550
4714
|
"subagents.mode.off": "no restriction", "subagents.mode.empty": "enabled with nothing picked (= no restriction)",
|
|
4551
4715
|
"subagents.mode.allowOn": "allowlist {count}", "subagents.mode.denyOn": "denylist {count}",
|
|
@@ -4558,8 +4722,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4558
4722
|
"subagents.legacy.note": "This file still carries the old-style tool limits (allowlist {allow} / denylist {deny}); they apply to every preset.",
|
|
4559
4723
|
"subagents.legacy.convert": "Convert into a preset row",
|
|
4560
4724
|
"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
|
-
|
|
4725
|
+
"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.",
|
|
4726
|
+
// The collapsed summary is at most two lines and reports only what has a value: line 1 is
|
|
4727
|
+
// model / effort (+ continuable), line 2 is the tool limits. Catalog injection is left out —
|
|
4728
|
+
// it only decides which sessions see the catalogue, not how a delegation runs.
|
|
4729
|
+
"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
4730
|
"preset.name.standard": "Standard mode", "preset.name.ptc": "PTC mode", "preset.name.minimal": "Minimal mode", "preset.name.cordis": "Creator mode",
|
|
4564
4731
|
"preset.short.standard": "Standard", "preset.short.ptc": "PTC", "preset.short.minimal": "Minimal", "preset.short.cordis": "Creator",
|
|
4565
4732
|
"subagents.field.body": "Persona prompt", "subagents.field.body.placeholder": "Describe the persona's role, responsibilities, and working style…",
|
|
@@ -4577,7 +4744,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4577
4744
|
"import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
|
|
4578
4745
|
"error.import.noFiles": "No files selected",
|
|
4579
4746
|
"nav.title": "Tools",
|
|
4580
|
-
"agm.stat.total": " global prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Change those references first (rebind the prompt, leave the scene, or apply another preset).", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "Scene “{scene}” is driving the baseline: the prompt in effect is its bound “{id}”. Applying another preset here rebinds that scene to it (written into the scene profile as well).", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.apply.switch": "Apply prompt", "agm.apply.switch.on": "This one is in effect · click to write AGENTS.md again (undoing hand edits)", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New prompt", "agm.btn.import": "Import prompts", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "id (directory name)", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.desc": "Description (the model sees this in the list)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.err.archive": "Archive failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Directory missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Prompt preset", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.agents.note.baselineLost": "⚠ Leaving the scene will not restore the previous AGENTS.md: {reason}", "scenes.field.prompt.hint": "Sets the global prompt for this scene", "scenes.prompt.activeTag": "in effect", "scenes.prompt.noPresets": "No prompt presets yet — create one on the Prompts page.", "scenes.prompt.bound": "Prompt: {id}", "scenes.prompt.live": "Prompt active: {id}", "scenes.prompt.missing": "Preset missing: {id}", "scenes.prompt.tag.hint": "The prompt preset bound to this scene", "scenes.prompt.mismatch.hint": "The scene-bound “{id}” does not match the current contents of AGENTS.md (it may have been edited by hand); click “Apply again” for it on the Prompts page to write it back", "scenes.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "scenes.preview.title": "Before entering scene “{name}” — what this will change", "scenes.preview.loading": "Working out what changes…", "scenes.preview.failed": "Preview failed: {error}", "scenes.preview.enter": "Enter the scene", "scenes.preview.dismiss": "Don't show again", "scenes.preview.dismiss.title": "Close this card and stop showing it before entering scenes (bring it back with “Turn the reminder on” at the top of this page)", "scenes.preview.toggle.off": "Turn the reminder off", "scenes.preview.toggle.on": "Turn the reminder on", "scenes.preview.toggle.title": "Show a “what will change” card before entering a scene", "scenes.preview.nochange": "Nothing to change: the runtime already matches what this scene asks for.", "scenes.preview.noArchive": "This scene has no saved profile — MCP, skills and personas are all read as “none checked”, so entering disables every one of them.", "scenes.preview.exitFirst": "Leaves “{scene}” first: what it changed is restored from the before-entering snapshot.", "scenes.preview.exitNoSnapshot": "“{scene}” is active but its before-entering snapshot is gone: leaving only clears the mode flag, the runtime will not go back.", "scenes.preview.row.serversOn": "Enables {count} MCP server(s): {names}", "scenes.preview.row.serversOff": "Disables {count} MCP server(s): {names}", "scenes.preview.row.toolsOn": "Puts back {count} MCP tool(s): {names}", "scenes.preview.row.toolsOff": "Disables {count} MCP tool(s): {names}", "scenes.preview.row.sourcesOn": "Enables {count} skill director(ies): {names}", "scenes.preview.row.sourcesOff": "Disables {count} skill director(ies): {names}", "scenes.preview.row.skillsOn": "Enables {count} skill(s): {names}", "scenes.preview.row.skillsOff": "Disables {count} skill(s): {names}", "scenes.preview.row.personasOn": "Enables {count} persona(s): {names}", "scenes.preview.row.personasOff": "Disables {count} persona(s): {names}", "scenes.preview.row.quickOn": "Enables {count} quick prompt(s): {names}", "scenes.preview.row.quickOff": "Disables {count} quick prompt(s): {names}", "scenes.preview.enter.servers": "{count} MCP server(s) stay on: {names}", "scenes.preview.enter.memories": "{count} memory(ies) enabled: {names}", "scenes.preview.row.notes": "Overrides the notes of {count} server(s) with scene notes", "scenes.field.toolTable": "Model tool-table scheme", "scenes.field.toolTable.hint": "Choose the model tool-table scheme this scene may use", "scenes.field.quickPrompts": "Quick prompts", "scenes.field.quickPrompts.current": "The enabled ones (keep as is)", "scenes.field.quickPrompts.custom": "Pick per scene", "scenes.field.quickPrompts.none": "No quick prompts yet — add some on the Prompts page", "scenes.field.quickPrompts.hint": "Sets the quick prompts for this scene", "scenes.field.quickPrompts.list": "Enabled in this scene", "scenes.form.section.basic": "Basics", "scenes.form.section.binding": "Scene bindings", "scenes.field.toolTable.none": "Not bound (keep as is)", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.row.toolPreset": "Tool table switches to scheme “{name}” (hides {count})", "scenes.preview.row.toolPresetMissing": "The tool-table scheme “{name}” bound here no longer exists — nothing switches this time", "scenes.preview.more": " etc.", "scenes.preview.truncated": "(each kind lists its first 60 entries only)", "scenes.preview.stale": "{count} profile entry(ies) no longer exist in this environment and will not take effect: {items}", "scenes.doctor.title": "{count} reference(s) point at a name that no longer exists", "scenes.doctor.noneButSkipped": "The check found no dangling references", "scenes.doctor.hint": "Left behind by a rename or a delete: it still looks bound here, but nothing matches that name at runtime. Point it at the right name, or untick it in the profile.", "scenes.doctor.row": "{where}: “{name}” no longer exists", "scenes.doctor.more": "{count} more not listed", "scenes.doctor.skipped": "({domains} could not be read, so that kind was not checked)", "agm.restore.tag": "Restored after the scene", "agm.restore.tag.hint": "The global prompt from before the scene; leaving the scene restores it, so deleting it would lose that fallback", "agm.file.mismatch": "Not in AGENTS.md", "agm.file.mismatch.hint": "AGENTS.md does not currently match this preset (it may have been edited by hand); flip the switch on the left to write it back", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Workspace (optional)", "hist.import.cwd.placeholder": "Pick, browse, or type an absolute path; leave blank to go to Ungrouped", "hist.import.workspace": "Workspace", "hist.import.browse": "Browse", "hist.import.workspace.none": "No registered workspace on this machine yet — type an absolute path, or use Browse to pick a folder.", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.import.lossy": "⚠ No speaker markers were found in the text, so it was folded into a single user message: the original turn structure may have been lost. If the source is a JSONL or Markdown transcript, import it with its original extension.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.detail": "Export detail", "hist.export.detail.simple": "Text only", "hist.export.detail.tools": "Text + tools", "hist.export.detail.full": "Text + tools + thinking", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.result.archiveFailed": "; {count} could not be archived", "hist.btn.archiveSelected": "Archive selected", "hist.btn.archiveSelected.title": "Move the selected sessions into History so retention applies to them", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
|
|
4747
|
+
"agm.stat.total": " global prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Remove that reference first — for a scene binding, rebind the prompt on the scene page or leave the scene; for the one currently applied, flip the switch on the left to un-apply.", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.fileApplied": "it is applied in AGENTS.md (the one currently in effect)", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "Scene “{scene}” is driving the baseline: the prompt in effect is its bound “{id}”. Applying another preset here rebinds that scene to it (written into the scene profile as well).", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.apply.switch": "Apply prompt", "agm.apply.switch.on": "This one is in effect · click to write AGENTS.md again (undoing hand edits)", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New prompt", "agm.btn.import": "Import prompts", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "id (directory name)", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.desc": "Description (the model sees this in the list)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "agm.unapply.title": "Un-apply", "agm.unapply.body": "This restores ~/.dsh/AGENTS.md to the content from before you started applying presets (kept in __last-applied__); no preset takes effect afterwards.", "agm.unapply.note": "What comes back is the content from before you started applying: presets you applied in between are rolled back too — to switch to one of them instead, just flip its own switch (same result). This step is backed up too, so you can apply it again.", "agm.btn.confirmUnapply": "Un-apply", "agm.unapply.switch.on": "This one is in AGENTS.md · click to un-apply (restore AGENTS.md to the content from before you started applying)", "agm.unapply.switch.blocked": "This one is in AGENTS.md, but there is no “state from before the apply” to restore — it was not written by “Apply”, so it cannot be turned off. To change the file, apply another preset or edit AGENTS.md directly; deleting this preset does not touch AGENTS.md", "agm.unapply.switch.onRemove": "This one is in AGENTS.md · click to un-apply (there was no AGENTS.md before you started applying, so this deletes the file)", "agm.unapply.bodyRemove": "There was no ~/.dsh/AGENTS.md before you started applying presets, so un-applying deletes that file instead of restoring content; no preset takes effect afterwards.", "agm.unapply.noteRemove": "What gets deleted is the content “Apply” wrote; that content is backed up to __last-applied__ (last 5 kept), so you can apply it back if needed.", "agm.btn.confirmUnapplyRemove": "Un-apply and delete the file", "agm.result.unapplied": "Un-applied: AGENTS.md is back to the content from before you started applying", "agm.result.unappliedRemove": "Un-applied: AGENTS.md was deleted (it did not exist before you started applying)", "agm.unapply.unavailable": "The current AGENTS.md was not written by “Apply” (the preset created automatically the first time you open this page is one such case), so there is no “state from before the apply” to restore and it cannot be turned off. To change what AGENTS.md holds: apply another preset, or edit AGENTS.md itself; deleting this preset does not touch AGENTS.md.", "agm.remove.fileNote": "Note: AGENTS.md still holds this content and your global prompt stays in effect — deleting the preset only moves it to the trash and does not touch AGENTS.md. To stop it taking effect, edit AGENTS.md itself or apply another preset.", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.err.archive": "Archive failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Directory missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Global prompt",
|
|
4748
|
+
"scenes.field.prompt.none": "None (no global prompt bound)", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.agents.note.baselineLost": "⚠ Leaving the scene will not restore the previous AGENTS.md: {reason}", "scenes.field.prompt.hint": "Sets the global prompt for this scene", "scenes.prompt.activeTag": "in effect", "scenes.prompt.noPresets": "No prompt presets yet — create one on the Prompts page.", "scenes.prompt.bound": "Prompt: {id}", "scenes.prompt.live": "Prompt active: {id}", "scenes.prompt.missing": "Preset missing: {id}", "scenes.prompt.tag.hint": "The prompt preset bound to this scene", "scenes.prompt.mismatch.hint": "The scene-bound “{id}” does not match the current contents of AGENTS.md (it may have been edited by hand); click “Apply again” for it on the Prompts page to write it back", "scenes.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "scenes.preview.title": "Before entering scene “{name}” — what this will change", "scenes.preview.loading": "Working out what changes…", "scenes.preview.failed": "Preview failed: {error}", "scenes.preview.enter": "Enter the scene", "scenes.preview.dismiss": "Don't show again", "scenes.preview.dismiss.title": "Close this card and stop showing it before entering scenes (bring it back with “Turn the reminder on” at the top of this page)", "scenes.preview.toggle.off": "Turn the reminder off", "scenes.preview.toggle.on": "Turn the reminder on", "scenes.preview.toggle.title": "Show a “what will change” card before entering a scene", "scenes.preview.nochange": "Nothing to change: the runtime already matches what this scene asks for.", "scenes.preview.noArchive": "This scene has no saved profile — MCP, skills and personas are all read as “none checked”, so entering disables every one of them.", "scenes.preview.exitFirst": "Leaves “{scene}” first: what it changed is restored from the before-entering snapshot.", "scenes.preview.exitNoSnapshot": "“{scene}” is active but its before-entering snapshot is gone: leaving only clears the mode flag, the runtime will not go back.", "scenes.preview.row.serversOn": "Enables {count} MCP server(s): {names}", "scenes.preview.row.serversOff": "Disables {count} MCP server(s): {names}", "scenes.preview.row.toolsOn": "Puts back {count} MCP tool(s): {names}", "scenes.preview.row.toolsOff": "Disables {count} MCP tool(s): {names}", "scenes.preview.row.sourcesOn": "Enables {count} skill director(ies): {names}", "scenes.preview.row.sourcesOff": "Disables {count} skill director(ies): {names}", "scenes.preview.row.skillsOn": "Enables {count} skill(s): {names}", "scenes.preview.row.skillsOff": "Disables {count} skill(s): {names}", "scenes.preview.row.personasOn": "Enables {count} persona(s): {names}", "scenes.preview.row.personasOff": "Disables {count} persona(s): {names}", "scenes.preview.row.quickOn": "Enables {count} quick prompt(s): {names}", "scenes.preview.row.quickOff": "Disables {count} quick prompt(s): {names}", "scenes.preview.enter.servers": "{count} MCP server(s) stay on: {names}", "scenes.preview.enter.memories": "{count} memory(ies) enabled: {names}", "scenes.preview.row.notes": "Overrides the notes of {count} server(s) with scene notes", "scenes.field.toolTable": "Model tool-table scheme", "scenes.field.toolTable.hint": "Choose the model tool-table scheme this scene may use", "scenes.field.quickPrompts": "Quick prompts", "scenes.field.quickPrompts.current": "The enabled ones (keep as is)", "scenes.field.quickPrompts.custom": "Custom", "scenes.field.quickPrompts.none": "No quick prompts yet — add some on the Prompts page",
|
|
4749
|
+
"scenes.field.quickPrompts.noneOption": "None (no quick prompts set)", "scenes.field.quickPrompts.hint": "Sets the quick prompts for this scene", "scenes.field.quickPrompts.list": "Enabled in this scene", "scenes.form.section.basic": "Basics", "scenes.form.section.binding": "Scene bindings", "scenes.field.toolTable.none": "Not bound (keep as is)", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.row.toolPreset": "Tool table switches to scheme “{name}” (hides {count})", "scenes.preview.row.toolPresetMissing": "The tool-table scheme “{name}” bound here no longer exists — nothing switches this time", "scenes.preview.more": " etc.", "scenes.preview.truncated": "(each kind lists its first 60 entries only)", "scenes.preview.stale": "{count} profile entry(ies) no longer exist in this environment and will not take effect: {items}", "scenes.doctor.title": "{count} reference(s) point at a name that no longer exists", "scenes.doctor.noneButSkipped": "The check found no dangling references", "scenes.doctor.hint": "Left behind by a rename or a delete: it still looks bound here, but nothing matches that name at runtime. Point it at the right name, or untick it in the profile.", "scenes.doctor.row": "{where}: “{name}” no longer exists", "scenes.doctor.more": "{count} more not listed", "scenes.doctor.skipped": "({domains} could not be read, so that kind was not checked)", "agm.restore.tag": "Restored after the scene", "agm.restore.tag.hint": "The global prompt from before the scene; leaving the scene restores it from the snapshot's text, so deleting this preset does not break the restore", "agm.file.mismatch": "Not in AGENTS.md", "agm.file.mismatch.hint": "AGENTS.md does not currently match this preset (it may have been edited by hand); flip the switch on the left to write it back", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Workspace (optional)", "hist.import.cwd.placeholder": "Pick, browse, or type an absolute path; leave blank to go to Ungrouped", "hist.import.workspace": "Workspace", "hist.import.browse": "Browse", "hist.import.workspace.none": "No registered workspace on this machine yet — type an absolute path, or use Browse to pick a folder.", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.import.lossy": "⚠ No speaker markers were found in the text, so it was folded into a single user message: the original turn structure may have been lost. If the source is a JSONL or Markdown transcript, import it with its original extension.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.detail": "Export detail", "hist.export.detail.simple": "Text only", "hist.export.detail.tools": "Text + tools", "hist.export.detail.full": "Text + tools + thinking", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.result.archiveFailed": "; {count} could not be archived", "hist.btn.archiveSelected": "Archive selected", "hist.btn.archiveSelected.title": "Move the selected sessions into History so retention applies to them", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
|
|
4581
4750
|
}
|
|
4582
4751
|
};
|
|
4583
4752
|
|
|
@@ -5794,6 +5963,42 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5794
5963
|
else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
|
|
5795
5964
|
}).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
|
|
5796
5965
|
}
|
|
5966
|
+
/**
|
|
5967
|
+
* 批量永久删除(回收站弹窗工具栏那一颗「永久删除 (N)」,先「全选」再删即等于清空)。
|
|
5968
|
+
*
|
|
5969
|
+
* 逐条调**已有的单条 op**(`agentsmd-trash-delete` / `quickprompt-trash-delete`):
|
|
5970
|
+
* 服务端不另造批量版 —— 那两条各自带着存在性校验,再造一份等于把判据抄两遍,
|
|
5971
|
+
* 而新 op 还要重新过一遍令牌与冻结登记(见 TrashModal 的注释)。
|
|
5972
|
+
*
|
|
5973
|
+
* 串行而不是并发:条目数天然有限(用户手删出来的),串行才给得出
|
|
5974
|
+
* 「删了几个、为什么没删掉」,并发只会让失败原因混成一团。
|
|
5975
|
+
*/
|
|
5976
|
+
function purgePresetMany(items) {
|
|
5977
|
+
if (!items.length) return
|
|
5978
|
+
setTrashBusy(true)
|
|
5979
|
+
var done = 0, failed = 0, firstErr = null
|
|
5980
|
+
var step = function (i) {
|
|
5981
|
+
if (i >= items.length) {
|
|
5982
|
+
setTrashBusy(false)
|
|
5983
|
+
var text = failed === 0
|
|
5984
|
+
? t('trash.purgeMany.result', { count: done })
|
|
5985
|
+
: t('trash.purgeMany.partial', { count: done, failed: failed, reason: firstErr || '' })
|
|
5986
|
+
setState(function (s) { return Object.assign({}, s, { notice: { kind: failed === 0 ? 'ok' : 'err', text: text } }) })
|
|
5987
|
+
loadTrash(true)
|
|
5988
|
+
return
|
|
5989
|
+
}
|
|
5990
|
+
apiCall(trashOp(items[i], 'delete'), { id: items[i].id }).then(function (res) {
|
|
5991
|
+
if (res && res.ok) done += 1
|
|
5992
|
+
else { failed += 1; if (!firstErr) firstErr = (res && res.error) || t('mcp.msg.failed') }
|
|
5993
|
+
step(i + 1)
|
|
5994
|
+
}).catch(function (e) {
|
|
5995
|
+
failed += 1
|
|
5996
|
+
if (!firstErr) firstErr = errMsg(e)
|
|
5997
|
+
step(i + 1)
|
|
5998
|
+
})
|
|
5999
|
+
}
|
|
6000
|
+
step(0)
|
|
6001
|
+
}
|
|
5797
6002
|
// 导入:与技能页同一个 ImportModal(拖拽区 + 文件要求清单 + 结果反馈)。
|
|
5798
6003
|
// 一次可选多份 .md,逐个读取正文导入;同名/失败逐条回报,不因一个失败中断其余的。
|
|
5799
6004
|
// 两个提示词域共用这一颗按钮,类型在弹窗顶部选(`extra` 槽)—— 选完导入的是哪一档
|
|
@@ -5889,13 +6094,40 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5889
6094
|
// (把被手改的 AGENTS.md 写回场景绑定的内容)→ 仍放行。
|
|
5890
6095
|
var sceneDriver = state.scenePrompt && state.scenePrompt.scene && state.scenePrompt.presetId && state.scenePrompt.missing !== true ? state.scenePrompt : null
|
|
5891
6096
|
var sceneDriverLabel = sceneDriver ? (sceneDriver.label || sceneDriver.scene) : ''
|
|
6097
|
+
// 「取消应用」可不可用 = 宿主那边 `__last-applied__` 里有没有「链起点」
|
|
6098
|
+
// (`agentsmd-get-current` 回的 `unapplyAvailable`)。**播种出来的 default 没有记录**
|
|
6099
|
+
// —— 它内容与 AGENTS.md 相同、显示「生效中」,却从未发生过写入,所以关不掉;
|
|
6100
|
+
// 那种情况点开关只给一条说明,并把出路(应用别的预设 / 直接编辑 AGENTS.md)说清楚。
|
|
6101
|
+
var unapplyReady = !!(state.current && state.current.unapplyAvailable === true)
|
|
6102
|
+
// 取消应用会**删掉文件**(开始应用之前 AGENTS.md 根本不存在)—— 这是个不可逆感很强的动作,
|
|
6103
|
+
// 确认弹窗与开关悬停都必须提前说清,不能点下去才知道(用户 2026-10-10 反馈
|
|
6104
|
+
// 「取消应用,AGENTS.md 还是存在」,那次的成因是删文件这一支压根走不通)。
|
|
6105
|
+
var unapplyRemoves = unapplyReady && state.current.unapplyRemoves === true
|
|
5892
6106
|
/** 引用说明:判定在宿主(scene-prompt-sync),这里只负责把它说成人话。 */
|
|
5893
6107
|
function refText(r) {
|
|
5894
6108
|
if (r.kind === 'scene') return t(r.active ? 'agm.ref.sceneActive' : 'agm.ref.scene', { scene: r.label || r.scene })
|
|
5895
6109
|
if (r.kind === 'restore') return t('agm.ref.restore')
|
|
5896
|
-
|
|
6110
|
+
// 两处 `t(...)` 分开写,**不要合成 `t(cond ? 'a' : 'b')`**:check-i18n 只认
|
|
6111
|
+
// `t('字面量')`,写成三元之后两个键都不再被统计,改名或打错字就没人拦得住了。
|
|
6112
|
+
return r.applied === true ? t('agm.ref.fileApplied') : t('agm.ref.file')
|
|
6113
|
+
}
|
|
6114
|
+
/**
|
|
6115
|
+
* **拦得住删除的**引用说明(其余引用由行上的标签承担)。
|
|
6116
|
+
*
|
|
6117
|
+
* 判据与宿主逐字一致(`ops/prompts.ts` 的 `agentsmd-remove`),两边必须同源 ——
|
|
6118
|
+
* 不同源就会出现「按钮点得动、点了却被拒」或反过来「按钮灰着、其实删得掉」。
|
|
6119
|
+
* ① 场景绑定 → 删了场景静默失效。
|
|
6120
|
+
* ② **当前应用的**那份(`file` + `applied`)→ 删了会留下一个用户看不懂的中间态:
|
|
6121
|
+
* AGENTS.md 内容一字不变、**照旧生效**,但界面上再没有预设对应它。用户看到的是
|
|
6122
|
+
* 「我把它删了,怎么全局提示词还在?」(2026-10-10 反馈),所以拦住并引导先取消应用。
|
|
6123
|
+
*
|
|
6124
|
+
* **`applied === false` 的 `file` 必须放行**:那是首次打开时自动播种出来的副本(内容与
|
|
6125
|
+
* AGENTS.md 相同、却从未被「应用」过),拦它没有任何出路 —— 它没有链起点可回,
|
|
6126
|
+
* 取消应用也关不掉,就是死路。
|
|
6127
|
+
*/
|
|
6128
|
+
function blockingRefsOf(p) {
|
|
6129
|
+
return (p.refs || []).filter(function (r) { return r.kind === 'scene' || (r.kind === 'file' && r.applied === true) }).map(refText)
|
|
5897
6130
|
}
|
|
5898
|
-
function refsOf(p) { return (p.refs || []).map(refText) }
|
|
5899
6131
|
// 结果提示的存续时长:**成功**短暂显示(与场景页的结果条同一节奏),
|
|
5900
6132
|
// **失败常驻**到下一次操作为止 —— 这正是 Notice 的既定语义(「警告 / 错误需要
|
|
5901
6133
|
// 用户处理,常驻到下一次操作」)。此前不分种类一律 4 秒消失,等于把唯一的线索
|
|
@@ -5940,11 +6172,38 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5940
6172
|
else refresh()
|
|
5941
6173
|
}).catch(function (e) { setBusy(null); setApplyConfirm(null); actionFailed(e) })
|
|
5942
6174
|
}
|
|
6175
|
+
/**
|
|
6176
|
+
* 取消应用:把全局基线恢复成**链起点**(宿主按 `__last-applied__` 里的记录办)——
|
|
6177
|
+
* 起点有内容就把那份写回去,起点文件不存在就把 AGENTS.md 删掉。**中途应用过别的预设
|
|
6178
|
+
* 也一并撤销**(一步回到起点,不是只退一步;见 `service.ts` 的 `backupGlobal`)。
|
|
6179
|
+
* 这是那颗「生效中」开关的**关**方向 —— 此前只有「应用」没有反向动作,于是
|
|
6180
|
+
* 「只有一份预设且它生效中」时开关关不掉、删除又被引用拦着(2026-10-10 实测反馈)。
|
|
6181
|
+
* 显示路径与 doApply 完全相同:失败走一次性横幅(不顶掉列表),成功重读页面。
|
|
6182
|
+
*
|
|
6183
|
+
* 成功时给一条**回执**(0.19.1,用户 2026-10-10 反馈「取消应用,AGENTS.md 还是存在」):
|
|
6184
|
+
* 原来只 `refresh()`,界面上唯一的变化是那颗开关灭了 —— 而"文件被删掉了"和"文件还在、
|
|
6185
|
+
* 只是内容换回去了"在界面上长得一模一样,用户只能自己去翻磁盘。回执按宿主回的
|
|
6186
|
+
* `removed` 分两种说法,把"到底发生了什么"直接写在页面上。
|
|
6187
|
+
*/
|
|
6188
|
+
function doUnapply() {
|
|
6189
|
+
beginAction()
|
|
6190
|
+
setBusy('unapply')
|
|
6191
|
+
apiCall('agentsmd-unapply', {}).then(function (res) {
|
|
6192
|
+
setBusy(null); setApplyConfirm(null)
|
|
6193
|
+
if (res && res.ok === false) { actionFailed(res); return }
|
|
6194
|
+
setState(function (s) {
|
|
6195
|
+
// 同样分开写(见 refText 的说明):check-i18n 只统计字面量调用。
|
|
6196
|
+
return Object.assign({}, s, { notice: { kind: 'ok', text: (res && res.removed) ? t('agm.result.unappliedRemove') : t('agm.result.unapplied') } })
|
|
6197
|
+
})
|
|
6198
|
+
refresh()
|
|
6199
|
+
}).catch(function (e) { setBusy(null); setApplyConfirm(null); actionFailed(e) })
|
|
6200
|
+
}
|
|
5943
6201
|
function doRemove(p) {
|
|
5944
6202
|
beginAction()
|
|
5945
6203
|
setBusy('rm-' + p.id)
|
|
5946
|
-
//
|
|
5947
|
-
//
|
|
6204
|
+
// 删除只有**场景绑定**拦得住(宿主侧 agentsmd-remove 同一判据)。被场景绑定的那份
|
|
6205
|
+
// 按钮点得动但删不掉(点了当面列出「谁在用」),所以响应里的错误必须显示出来,
|
|
6206
|
+
// 不能静默什么都不发生。
|
|
5948
6207
|
apiCall('agentsmd-remove', { id: p.id }).then(function (res) {
|
|
5949
6208
|
setBusy(null); setApplyConfirm(null)
|
|
5950
6209
|
if (res && res.ok === false) actionFailed(res)
|
|
@@ -6163,7 +6422,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
6163
6422
|
var viaScene = p.activeVia === 'scene'
|
|
6164
6423
|
var refs = p.refs || []
|
|
6165
6424
|
var restoreRef = refs.some(function (r) { return r.kind === 'restore' })
|
|
6166
|
-
|
|
6425
|
+
// 场景绑定与「当前应用的」那份拦得住删除(判据见 blockingRefsOf 的说明)。
|
|
6426
|
+
var blockingTexts = blockingRefsOf(p)
|
|
6167
6427
|
// 场景绑的就是它,但 AGENTS.md 里的内容不是它(被手改过?)→ 说清楚,
|
|
6168
6428
|
// 并留"再点一次那颗开关"这条路写回。
|
|
6169
6429
|
var fileMismatch = !!sceneDriver && sceneDriver.presetId === p.id && !p.fileApplied
|
|
@@ -6186,24 +6446,40 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
6186
6446
|
restoreRef ? React.createElement('span', { className: 'dsm-tag', title: t('agm.restore.tag.hint') }, t('agm.restore.tag')) : null),
|
|
6187
6447
|
p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
|
|
6188
6448
|
React.createElement('div', { className: 'dsm-source-actions' },
|
|
6189
|
-
// 「应用 /
|
|
6190
|
-
//
|
|
6191
|
-
//
|
|
6192
|
-
//
|
|
6193
|
-
//
|
|
6449
|
+
// 「应用 / 取消应用」同一颗药丸开关(0.19.1 起有两个方向):
|
|
6450
|
+
// 关 → 点 = 应用(把这份内容写进 AGENTS.md);
|
|
6451
|
+
// 开(AGENTS.md 里就是它)→ 点 = **取消应用**(把 AGENTS.md 恢复成
|
|
6452
|
+
// **开始应用提示词之前**的那一份 —— 中途应用过别的预设也一并撤销,
|
|
6453
|
+
// 不是只退一步;见 `service.ts` 的 `backupGlobal`)。此前这颗开关只有
|
|
6454
|
+
// 「应用」一个方向,用户实测「开启后无法关闭,也不能删除」—— 那是开关
|
|
6455
|
+
// 与删除保护一起构成的死锁。
|
|
6456
|
+
// 开但由**场景**驱动(`activeVia === 'scene'`)→ 点 = 应用(重新写回 /
|
|
6457
|
+
// 换绑场景);撤销归场景页管,本页不该把场景的绑定偷偷撤掉。
|
|
6458
|
+
// 没有链起点可回时(播种出来的 default 就是这样)不开弹窗,
|
|
6459
|
+
// 只给一条说明,并把出路(应用别的预设 / 直接编辑 AGENTS.md)说清楚。
|
|
6194
6460
|
React.createElement(Switch, {
|
|
6195
6461
|
on: p.active === true,
|
|
6196
6462
|
disabled: busy !== null || state.anyLocked === true,
|
|
6197
6463
|
label: t('agm.apply.switch') + ' ' + p.id,
|
|
6198
6464
|
title: applyRebinds ? t('agm.apply.rebindScene', { scene: sceneDriverLabel })
|
|
6199
|
-
: p.active
|
|
6200
|
-
|
|
6465
|
+
: p.active
|
|
6466
|
+
? (viaScene ? t('agm.apply.switch.on')
|
|
6467
|
+
: (unapplyReady ? (unapplyRemoves ? t('agm.unapply.switch.onRemove') : t('agm.unapply.switch.on')) : t('agm.unapply.switch.blocked')))
|
|
6468
|
+
: t('agm.apply.hint'),
|
|
6469
|
+
onClick: function () {
|
|
6470
|
+
if (!p.active || viaScene) { setApplyConfirm({ id: p.id }); return }
|
|
6471
|
+
if (!unapplyReady) { setState(function (s) { return Object.assign({}, s, { deny: t('agm.unapply.unavailable') }) }); return }
|
|
6472
|
+
setApplyConfirm({ id: p.id, unapply: true })
|
|
6473
|
+
} }),
|
|
6201
6474
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: state.anyLocked === true, onClick: function () { openEdit(p) } }, t('agm.btn.edit')),
|
|
6202
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || state.anyLocked === true, title:
|
|
6203
|
-
//
|
|
6204
|
-
//
|
|
6205
|
-
|
|
6206
|
-
|
|
6475
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || state.anyLocked === true, title: blockingTexts.length > 0 ? t('agm.btn.delete.blocked.refs', { refs: blockingTexts.join(t('agm.ref.sep')) }) : t('scenes.lock.blockedEdit'), onClick: function () {
|
|
6476
|
+
// 被引用中的预设**点得动但删不掉**:当面说出「谁在用 / 该怎么解除」,
|
|
6477
|
+
// 而不是让用户对着禁用按钮猜(disabled 的按钮连 title 都不弹)。
|
|
6478
|
+
// 宿主侧同一判据同样拒绝,这里只是先说。
|
|
6479
|
+
// 拦的是两类:场景绑定、以及**当前应用的**那份(后者删了 AGENTS.md
|
|
6480
|
+
// 照旧生效,会留下一个看不懂的中间态)。播种出来的副本不在其列。
|
|
6481
|
+
if (blockingTexts.length > 0) { setState(function (s) { return Object.assign({}, s, { deny: t('agm.btn.delete.blocked.refs', { refs: blockingTexts.join(t('agm.ref.sep')) }) }) }); return }
|
|
6482
|
+
setApplyConfirm({ id: p.id, remove: true, removeFileApplied: p.fileApplied === true })
|
|
6207
6483
|
} }, t('agm.btn.delete')))))
|
|
6208
6484
|
}))
|
|
6209
6485
|
|
|
@@ -6287,6 +6563,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
6287
6563
|
{ title: t('trash.group.presets'), sub: t('trash.section.presets.sub'), entries: trash.presets || [] },
|
|
6288
6564
|
{ title: t('trash.group.quick'), sub: t('trash.section.quick.sub'), entries: trash.quick || [] },
|
|
6289
6565
|
],
|
|
6566
|
+
// 上下两块各占一半、各自滚动、**每块自带一条**「已选 N 项 + 全选 + 永久删除 (N)」
|
|
6567
|
+
// (用户 2026-10-10:「提示词的回收站也要像技能回收站一样分成两部分」)。
|
|
6568
|
+
// 原先两块排在一条长列表里:35 条预设把 1 条快捷提示词压在滚不到底的下面,
|
|
6569
|
+
// 而顶部那一颗全局「永久删除」跨两块删 —— 勾完之后看不出会删掉哪几个。
|
|
6570
|
+
split: true,
|
|
6290
6571
|
locked: state.anyLocked === true,
|
|
6291
6572
|
loading: trash.loading,
|
|
6292
6573
|
error: trash.error,
|
|
@@ -6294,6 +6575,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
6294
6575
|
onClose: function () { setTrash(null) },
|
|
6295
6576
|
onRestore: restorePreset,
|
|
6296
6577
|
onPurge: purgePreset,
|
|
6578
|
+
// 批量删除 + 保留期改完后的重读(见 42-shared-ui.js 的 TrashModal / RetentionSelect)。
|
|
6579
|
+
onPurgeMany: purgePresetMany,
|
|
6580
|
+
onReload: function () { loadTrash(true) },
|
|
6297
6581
|
}) : null,
|
|
6298
6582
|
// 编辑:id 与内容都可改(改 id = 目录改名,场景绑定由宿主一起改名)。
|
|
6299
6583
|
editModal ? React.createElement(Modal, { title: t('agm.edit.title') + ' · ' + editModal.id, closeLabel: t('btn.close'), onClose: function () { setEditModal(null) } },
|
|
@@ -6339,14 +6623,26 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
6339
6623
|
createModal.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, createModal.error) : null,
|
|
6340
6624
|
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
6341
6625
|
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(createModal.id || '').trim(), onClick: function () { doCreate(createModal.id, createModal.content, createModal.description) } }, t('agm.btn.create')))) : null,
|
|
6342
|
-
|
|
6626
|
+
// 三种确认共用一个弹窗:删除 / 取消应用 / 应用(`applyConfirm` 上的标记位区分)。
|
|
6627
|
+
applyConfirm ? React.createElement(Modal, { title: applyConfirm.remove ? (t('agm.remove.title') + ' · ' + applyConfirm.id) : ((applyConfirm.unapply ? t('agm.unapply.title') : t('agm.applyModal.title')) + ' · ' + applyConfirm.id), closeLabel: t('btn.close'), onClose: function () { setApplyConfirm(null) } },
|
|
6343
6628
|
applyConfirm.remove
|
|
6344
|
-
? React.createElement('
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6629
|
+
? React.createElement('div', null,
|
|
6630
|
+
React.createElement('p', { className: 'dsm-help' }, t('agm.remove.title') + ' ' + applyConfirm.id + t('agm.remove.suffix')),
|
|
6631
|
+
// 删的正是「AGENTS.md 里就是它」的那一份 → 必须说清**文件不会被改动、
|
|
6632
|
+
// 全局提示词照旧生效**,否则用户会以为删除把全局提示词一起清掉了。
|
|
6633
|
+
// 注意这条只可能落在**播种出来的副本**上:真正「当前应用的」那份已经在
|
|
6634
|
+
// blockingRefsOf 里被拦住,根本走不到这个弹窗(用户 2026-10-10 的要求)。
|
|
6635
|
+
applyConfirm.removeFileApplied ? React.createElement('div', { className: 'dsm-feedback dsm-warning' }, t('agm.remove.fileNote')) : null)
|
|
6636
|
+
: applyConfirm.unapply
|
|
6637
|
+
? React.createElement('div', null,
|
|
6638
|
+
// 删文件那一支必须换文案:说"恢复成开始应用之前的那份内容"会让用户以为文件还在。
|
|
6639
|
+
React.createElement('p', { className: 'dsm-help' }, unapplyRemoves ? t('agm.unapply.bodyRemove', { id: applyConfirm.id }) : t('agm.unapply.body', { id: applyConfirm.id })),
|
|
6640
|
+
React.createElement('div', { className: 'dsm-feedback dsm-warning' }, unapplyRemoves ? t('agm.unapply.noteRemove') : t('agm.unapply.note')))
|
|
6641
|
+
: React.createElement('div', null,
|
|
6642
|
+
React.createElement('p', { className: 'dsm-help' }, t('agm.apply.prefix') + applyConfirm.id + t('agm.apply.suffix')),
|
|
6643
|
+
React.createElement('div', { className: 'dsm-feedback dsm-warning' }, t('agm.apply.note'))),
|
|
6348
6644
|
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
6349
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn ' + (applyConfirm.remove ? 'dsm-btn-danger' : ''), disabled: busy !== null, onClick: function () { if (applyConfirm.remove) doRemove({ id: applyConfirm.id }); else doApply(applyConfirm.id) } }, applyConfirm.remove ? t('agm.btn.confirmRemove') : t('agm.btn.confirmApply')))) : null,
|
|
6645
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn ' + (applyConfirm.remove || (applyConfirm.unapply && unapplyRemoves) ? 'dsm-btn-danger' : ''), disabled: busy !== null, onClick: function () { if (applyConfirm.remove) doRemove({ id: applyConfirm.id }); else if (applyConfirm.unapply) doUnapply(); else doApply(applyConfirm.id) } }, applyConfirm.remove ? t('agm.btn.confirmRemove') : (applyConfirm.unapply ? (unapplyRemoves ? t('agm.btn.confirmUnapplyRemove') : t('agm.btn.confirmUnapply')) : t('agm.btn.confirmApply'))))) : null,
|
|
6350
6646
|
importOpen ? React.createElement(ImportModal, {
|
|
6351
6647
|
key: 'imp', t: t, title: t('agm.btn.import'), accept: '.md',
|
|
6352
6648
|
busy: importBusy, result: importResult,
|
|
@@ -6991,17 +7287,224 @@ function enabledMemoryCount(rules) {
|
|
|
6991
7287
|
props.body)
|
|
6992
7288
|
}
|
|
6993
7289
|
/**
|
|
6994
|
-
*
|
|
7290
|
+
* 回收站保留期选择器(0.19.1)。
|
|
7291
|
+
*
|
|
7292
|
+
* 自己拉、自己写:它是一个**全局设置**(`hub/trash-retention.json`),而回收站的界面
|
|
7293
|
+
* 散在五个页面(提示词 / 场景 / 子智能体 / 技能 / 记忆)。把状态放在每个页面的父组件里,
|
|
7294
|
+
* 等于同一段读-写-回执逻辑抄五遍,任何一遍漂了都会表现成「这页设了、那页没生效」。
|
|
7295
|
+
*
|
|
7296
|
+
* 回执写在选择器右边而不是弹窗顶部:用户改完这一格马上要看的是「这次清掉了什么」,
|
|
7297
|
+
* 而不是去别处找消息(清扫是异步发生的,没有回执就等于"改了个数字,什么也没看见")。
|
|
7298
|
+
*
|
|
7299
|
+
* 副作用面:`trash-retention-set` 成功后服务端会**立即扫一次**,可能真的永久删掉条目。
|
|
7300
|
+
* 所以调用方要传 `onReload` —— 否则弹窗里还列着已经不存在的条目,点恢复会报"条目不存在"。
|
|
7301
|
+
*/
|
|
7302
|
+
function RetentionSelect(props) {
|
|
7303
|
+
var t = props.t
|
|
7304
|
+
var st = React.useState({ loading: true, days: null, busy: false, note: null, error: null })
|
|
7305
|
+
var s = st[0], set = st[1]
|
|
7306
|
+
React.useEffect(function () {
|
|
7307
|
+
var alive = true
|
|
7308
|
+
apiCall('trash-retention-get', {}).then(function (res) {
|
|
7309
|
+
if (!alive) return
|
|
7310
|
+
if (res && res.ok) set({ loading: false, days: Number(res.retentionDays) || 0, busy: false, note: null, error: null })
|
|
7311
|
+
else set({ loading: false, days: null, busy: false, note: null, error: (res && res.error) || t('trash.retention.loadFailed') })
|
|
7312
|
+
}).catch(function (e) {
|
|
7313
|
+
if (alive) set({ loading: false, days: null, busy: false, note: null, error: errMsg(e) })
|
|
7314
|
+
})
|
|
7315
|
+
return function () { alive = false }
|
|
7316
|
+
}, [])
|
|
7317
|
+
function choose(value) {
|
|
7318
|
+
var days = Number(value) || 0
|
|
7319
|
+
set(function (prev) { return Object.assign({}, prev, { busy: true, note: null, error: null }) })
|
|
7320
|
+
apiCall('trash-retention-set', { retentionDays: days }).then(function (res) {
|
|
7321
|
+
if (res && res.ok) {
|
|
7322
|
+
var swept = Number(res.swept) || 0
|
|
7323
|
+
var failed = Number(res.failed) || 0
|
|
7324
|
+
set({
|
|
7325
|
+
loading: false, days: Number(res.retentionDays) || 0, busy: false, error: null,
|
|
7326
|
+
// 只在**真的发生了事**的时候出回执:一条都没清掉时写「已保存;本次清理 0 项」
|
|
7327
|
+
// 是纯噪音(下拉框本身的值就是"已保存"的回执)。删除失败要如实说 ——
|
|
7328
|
+
// `failed > 0` 说明有条目删不掉(IO 错误),只报清掉几项会让用户以为回收站干净了。
|
|
7329
|
+
note: failed > 0
|
|
7330
|
+
? t('trash.retention.savedFailed', { count: swept, failed: failed })
|
|
7331
|
+
: swept > 0 ? t('trash.retention.saved', { count: swept }) : null,
|
|
7332
|
+
})
|
|
7333
|
+
// 只在本轮真的删了东西时重读列表:没删东西还重读一次会让弹窗闪一下。
|
|
7334
|
+
if (swept > 0 && props.onReload) props.onReload()
|
|
7335
|
+
} else {
|
|
7336
|
+
set(function (prev) { return Object.assign({}, prev, { busy: false, error: translateError(t, res) }) })
|
|
7337
|
+
}
|
|
7338
|
+
}).catch(function (e) {
|
|
7339
|
+
set(function (prev) { return Object.assign({}, prev, { busy: false, error: errMsg(e) }) })
|
|
7340
|
+
})
|
|
7341
|
+
}
|
|
7342
|
+
// 选项与 History 页那套同形(永久保留 / 7 / 30 天),外加 90 天 —— 回收站里的东西
|
|
7343
|
+
// 比归档会话更常需要"留久一点再看"。`0` 是默认值,也是唯一不删任何东西的那一档。
|
|
7344
|
+
var PRESETS = [0, 7, 30, 90]
|
|
7345
|
+
var days = s.days
|
|
7346
|
+
// 侧车里的值可能是手改的、或来自更早的版本(例如 15)—— 不在预设里时把它补成一项,
|
|
7347
|
+
// 否则下拉框会显示成空白,用户会以为"根本没设置过"而重设一遍。
|
|
7348
|
+
var values = days !== null && PRESETS.indexOf(days) < 0 ? [days].concat(PRESETS) : PRESETS
|
|
7349
|
+
return React.createElement('div', { className: 'dsm-trash-retention' },
|
|
7350
|
+
React.createElement('span', { className: 'dsm-label' }, t('trash.retention.label')),
|
|
7351
|
+
React.createElement('select', {
|
|
7352
|
+
className: 'dsm-control',
|
|
7353
|
+
value: days === null ? '' : String(days),
|
|
7354
|
+
disabled: s.loading || s.busy || days === null,
|
|
7355
|
+
'aria-label': t('trash.retention.label'),
|
|
7356
|
+
onChange: function (e) { choose(e.target.value) },
|
|
7357
|
+
},
|
|
7358
|
+
days === null ? React.createElement('option', { value: '' }, t('trash.retention.loading')) : null,
|
|
7359
|
+
values.map(function (v) {
|
|
7360
|
+
return React.createElement('option', { key: v, value: String(v) },
|
|
7361
|
+
v === 0 ? t('trash.retention.forever') : t('trash.retention.days', { count: v }))
|
|
7362
|
+
})),
|
|
7363
|
+
s.error ? React.createElement('span', { className: 'dsm-trash-hint dsm-trash-retention-err' }, s.error) : null,
|
|
7364
|
+
s.note ? React.createElement('span', { className: 'dsm-trash-hint' }, s.note) : null,
|
|
7365
|
+
// 说明只在**真的开了保留期**时出现:选着「永久保留」还写「超期自动删除」是自相矛盾的。
|
|
7366
|
+
// 文案本身压到 6 个字(用户 2026-10-10:「保留期一行就够了」),全文进 title ——
|
|
7367
|
+
// 这一行恒为一行,长句放不下的问题从源头消掉,而不是靠折行兜着。
|
|
7368
|
+
days !== null && days > 0
|
|
7369
|
+
? React.createElement('span', { className: 'dsm-trash-hint', title: t('trash.retention.hintFull') }, t('trash.retention.hint'))
|
|
7370
|
+
: null)
|
|
7371
|
+
}
|
|
7372
|
+
|
|
7373
|
+
/**
|
|
7374
|
+
* 通用回收站弹窗:一行一个条目(勾选框 + 名字 + 删除时间),右侧「恢复 / 永久删除」。
|
|
6995
7375
|
*
|
|
6996
7376
|
* 子智能体 / 场景 / 提示词预设三处共用;永久删除就在本弹窗里就地二次确认,
|
|
6997
7377
|
* 不再叠一层弹窗。恢复失败的原因显示在弹窗顶部 —— 不静默(同名已存在时必须说清)。
|
|
7378
|
+
*
|
|
7379
|
+
* 0.19.1 加的两件事:
|
|
7380
|
+
* · **保留期选择器**(`RetentionSelect`):回收站此前只有"进来"没有"出去"。
|
|
7381
|
+
* · **批量永久删除**:条目多的时候(本机实测场景回收站 68 条、记忆 120 条)
|
|
7382
|
+
* 一条条点等于没法用。勾选 + 「全选」+「永久删除 (N)」。
|
|
7383
|
+
*
|
|
7384
|
+
* 批量控件**只有一颗删除键**,与「全选」同处工具栏那一行、靠最右(用户 2026-10-10 裁定):
|
|
7385
|
+
* 原先并列「永久删除所选」与「清空回收站」两颗,两者是同一件事的两种范围,
|
|
7386
|
+
* 并列必然有一颗是废话 —— 要清空就先「全选」再删,两步。
|
|
7387
|
+
* 同理,勾选是**跨组**的(提示词页一次列两组),所以「全选」也必须是全局那一颗:
|
|
7388
|
+
* 每组各留一颗「全选」就等于每组也要各配一颗删除键,又变回多颗。
|
|
7389
|
+
*
|
|
7390
|
+
* 批量为什么走**前端循环调已有的单条删除 op**(`onPurgeMany` 由调用方实现):
|
|
7391
|
+
* 服务端那六条 `*-trash-delete` 各自带着自己的存在性校验与回滚,再写一条"批量版"
|
|
7392
|
+
* 就等于把六份判据复制一遍 —— 而且新 op 还要重新过一遍令牌/冻结登记。
|
|
7393
|
+
* 条数天然有限(用户手删出来的),循环没有性能问题。
|
|
7394
|
+
*
|
|
7395
|
+
* `split: true` = **分栏模式**(用户 2026-10-10:「提示词的回收站也要像技能回收站一样分成两部分」):
|
|
7396
|
+
* 两块上下各占一半、各自滚动、**每块自带一条**「已选 N 项 + 全选 + 永久删除 (N)」,
|
|
7397
|
+
* 大弹窗自己不滚。为什么不沿用顶部那一颗全局删除键:两块的后果不同(提示词页是
|
|
7398
|
+
* 「提示词预设目录」与「快捷提示词目录」),全局「全选 + 永久删除」要跨两块删 ——
|
|
7399
|
+
* 勾完之后根本看不出那一键会删掉哪几个。与技能回收站(43-skills.js 的 `trashPane`)
|
|
7400
|
+
* 同一套结构与类名,只是那份是手写的,这份做成了 `TrashModal` 的一个模式。
|
|
6998
7401
|
*/
|
|
7402
|
+
// 非分栏时那条唯一的批量条用这个哨兵当"确认中"的标记(分栏时用的是那一块的组标题)。
|
|
7403
|
+
// 用对象而不是字符串:组标题是用户可见文案,理论上可能撞上任何字符串字面量。
|
|
7404
|
+
const TRASH_BULK_ALL = { all: true }
|
|
6999
7405
|
function TrashModal(props) {
|
|
7000
7406
|
const t = props.t
|
|
7407
|
+
// 分栏模式:上下两块各占一半、各自滚动、**每块自带一条**批量条(见函数头注)。
|
|
7408
|
+
const split = props.split === true
|
|
7001
7409
|
const c = React.useState(null)
|
|
7002
7410
|
const confirmId = c[0], setConfirmId = c[1]
|
|
7003
|
-
|
|
7004
|
-
|
|
7411
|
+
// 勾选集合。key = `<组标题>|<条目 id>`,**不能只按 id 记**:提示词页一次列两组
|
|
7412
|
+
// (全局预设 + 快捷提示词),两组的 id 来自同一条生成器,只按 id 记会让两组互相勾中。
|
|
7413
|
+
const sel = React.useState({})
|
|
7414
|
+
const selected = sel[0], setSelected = sel[1]
|
|
7415
|
+
// 批量二次确认。存的不是布尔而是"哪一条批量条在确认":分栏时上下两块**各有一条**,
|
|
7416
|
+
// 共用一颗布尔会让下面那块也跟着翻成确认态 —— 它的按钮并不是刚点的那颗。
|
|
7417
|
+
// 非分栏时用 `TRASH_BULK_ALL` 这个哨兵(整份列表只有一条批量条)。
|
|
7418
|
+
const bc = React.useState(null)
|
|
7419
|
+
const bulkConfirm = bc[0], setBulkConfirm = bc[1]
|
|
7420
|
+
// 组:默认就是调用方传的那一组(`groupTitle` / `entries`);给了 `groups` 就按数组列多组
|
|
7421
|
+
// —— 提示词页的回收站要一次看完"全局"与"快捷"两类,让用户先猜被删的东西在哪个域里,
|
|
7422
|
+
// 等于把恢复这条路堵在他自己手里。每组各自有空的说明,不合并成一句"什么都没删"。
|
|
7423
|
+
const groups = props.groups || [{ title: props.groupTitle, sub: props.groupSub, entries: props.entries || [] }]
|
|
7424
|
+
const rowKey = function (g, item) { return String(g.title) + '|' + String(item.id) }
|
|
7425
|
+
// 平铺一份:非分栏时「全选」与「永久删除」按**整份列表**算,不能只看某一组。
|
|
7426
|
+
const flat = []
|
|
7427
|
+
groups.forEach(function (g) {
|
|
7428
|
+
(g.entries || []).forEach(function (item) { flat.push({ group: g, item: item, key: rowKey(g, item) }) })
|
|
7429
|
+
})
|
|
7430
|
+
const allKeys = flat.map(function (x) { return x.key })
|
|
7431
|
+
// 场景锁定时批量删除一并禁用:单条「永久删除」已经禁用,批量是同一件事,
|
|
7432
|
+
// 只禁单条等于留了一个绕过锁定一次删光的口子。
|
|
7433
|
+
const batchDisabled = props.busy === true || props.locked === true
|
|
7434
|
+
// 没传 onPurgeMany 的调用方(目前没有,但 props 是可选的)不渲染批量控件,
|
|
7435
|
+
// 而不是渲染出按不动的按钮。
|
|
7436
|
+
const batchEnabled = typeof props.onPurgeMany === 'function' && flat.length > 0
|
|
7437
|
+
const toggle = function (key) {
|
|
7438
|
+
setSelected(function (prev) {
|
|
7439
|
+
var next = Object.assign({}, prev)
|
|
7440
|
+
if (next[key] === true) delete next[key]
|
|
7441
|
+
else next[key] = true
|
|
7442
|
+
return next
|
|
7443
|
+
})
|
|
7444
|
+
}
|
|
7445
|
+
const pickedOf = function (keys) { return keys.filter(function (k) { return selected[k] === true }) }
|
|
7446
|
+
const toggleAll = function (keys, on) {
|
|
7447
|
+
setSelected(function (prev) {
|
|
7448
|
+
var next = Object.assign({}, prev)
|
|
7449
|
+
keys.forEach(function (k) { if (on) next[k] = true; else delete next[k] })
|
|
7450
|
+
return next
|
|
7451
|
+
})
|
|
7452
|
+
}
|
|
7453
|
+
/**
|
|
7454
|
+
* 跑一条批量删除。`keys` 是这条批量条管的范围(非分栏 = 整份列表;分栏 = 那一块)。
|
|
7455
|
+
* 按**范围**过滤而不是直接拿整份勾选:分栏时上面那块点确认,不该把下面那块的勾选也删掉。
|
|
7456
|
+
* 删完也只清这一批的勾 —— 条目删掉之后 key 就失效了,留着会变成一串永远勾不中的残留;
|
|
7457
|
+
* 而另一块的勾选是用户还没处理的选择,不该替他清掉。
|
|
7458
|
+
*/
|
|
7459
|
+
const runBulk = function (keys) {
|
|
7460
|
+
var inScope = {}
|
|
7461
|
+
keys.forEach(function (k) { inScope[k] = true })
|
|
7462
|
+
var items = flat.filter(function (x) { return inScope[x.key] === true && selected[x.key] === true }).map(function (x) { return x.item })
|
|
7463
|
+
setBulkConfirm(null)
|
|
7464
|
+
setSelected(function (prev) {
|
|
7465
|
+
var next = Object.assign({}, prev)
|
|
7466
|
+
keys.forEach(function (k) { delete next[k] })
|
|
7467
|
+
return next
|
|
7468
|
+
})
|
|
7469
|
+
if (items.length) props.onPurgeMany(items)
|
|
7470
|
+
}
|
|
7471
|
+
/**
|
|
7472
|
+
* 一条批量条:「已选 N 项 + 全选 + 永久删除 (N)」;二次确认就地替换这一行
|
|
7473
|
+
* (避免"确认了但按钮还在原位"的错觉)。非分栏与分栏共用,只是 `keys` 的范围不同。
|
|
7474
|
+
*/
|
|
7475
|
+
const bulkRow = function (keys, confirmKey) {
|
|
7476
|
+
var pickedHere = pickedOf(keys)
|
|
7477
|
+
var allHere = keys.length > 0 && pickedHere.length === keys.length
|
|
7478
|
+
if (bulkConfirm === confirmKey) {
|
|
7479
|
+
return React.createElement('div', { className: 'dsm-trash-bulk dsm-trash-bulk-confirm' },
|
|
7480
|
+
React.createElement('span', { className: 'dsm-note' }, t('trash.purgeSelected.confirm', { count: pickedHere.length })),
|
|
7481
|
+
React.createElement('button', {
|
|
7482
|
+
type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy,
|
|
7483
|
+
onClick: function () { runBulk(keys) },
|
|
7484
|
+
}, t('trash.btn.confirmPurgeMany', { count: pickedHere.length })),
|
|
7485
|
+
React.createElement('button', {
|
|
7486
|
+
type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy,
|
|
7487
|
+
onClick: function () { setBulkConfirm(null) },
|
|
7488
|
+
}, t('btn.cancel')))
|
|
7489
|
+
}
|
|
7490
|
+
return React.createElement('div', { className: 'dsm-trash-bulk' },
|
|
7491
|
+
React.createElement('span', { className: 'dsm-note' }, t('trash.selected', { count: pickedHere.length })),
|
|
7492
|
+
// 「全选 ↔ 全不选」二合一(`bulkPair` 定宽,避免两种文案宽度不同把按钮推来推去)。
|
|
7493
|
+
React.createElement('button', {
|
|
7494
|
+
type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-bulk',
|
|
7495
|
+
disabled: batchDisabled,
|
|
7496
|
+
onClick: function () { toggleAll(keys, !allHere) },
|
|
7497
|
+
}, bulkPair(allHere ? t('trash.deselectAll') : t('trash.selectAll'), allHere ? t('trash.selectAll') : t('trash.deselectAll'))),
|
|
7498
|
+
React.createElement('button', {
|
|
7499
|
+
type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger',
|
|
7500
|
+
disabled: batchDisabled || pickedHere.length === 0,
|
|
7501
|
+
onClick: function () { setBulkConfirm(confirmKey) },
|
|
7502
|
+
}, t('trash.btn.purgeSelected', { count: pickedHere.length })))
|
|
7503
|
+
}
|
|
7504
|
+
const row = function (g, item) {
|
|
7505
|
+
const key = rowKey(g, item)
|
|
7506
|
+
const confirming = confirmId === key
|
|
7507
|
+
const isPicked = selected[key] === true
|
|
7005
7508
|
const actions = confirming
|
|
7006
7509
|
? [
|
|
7007
7510
|
React.createElement('span', { className: 'dsm-note', key: 'ask' }, t('trash.purge.confirm')),
|
|
@@ -7010,9 +7513,21 @@ function enabledMemoryCount(rules) {
|
|
|
7010
7513
|
]
|
|
7011
7514
|
: [
|
|
7012
7515
|
React.createElement('button', { key: 'restore', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy || props.locked === true, onClick: function () { props.onRestore(item) } }, t('btn.restore')),
|
|
7013
|
-
React.createElement('button', { key: 'purge', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy || props.locked === true, onClick: function () { setConfirmId(
|
|
7516
|
+
React.createElement('button', { key: 'purge', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy || props.locked === true, onClick: function () { setConfirmId(key) } }, t('btn.delete.forever')),
|
|
7014
7517
|
]
|
|
7015
|
-
return React.createElement('div', { className: 'dsm-trash-item', key: item.id },
|
|
7518
|
+
return React.createElement('div', { className: 'dsm-trash-item' + (isPicked ? ' dsm-trash-item-picked' : ''), key: item.id },
|
|
7519
|
+
// 勾选框只在能批量删的时候出现:没有 onPurgeMany 的调用方(见上面的 batchEnabled)
|
|
7520
|
+
// 渲染一颗永远禁用的勾选框,只会让人以为"这里本来能勾、只是现在不行"。
|
|
7521
|
+
batchEnabled
|
|
7522
|
+
? React.createElement('label', { className: 'dsm-trash-pick', title: t('trash.pick.title') },
|
|
7523
|
+
React.createElement('input', {
|
|
7524
|
+
type: 'checkbox',
|
|
7525
|
+
checked: isPicked,
|
|
7526
|
+
disabled: batchDisabled,
|
|
7527
|
+
'aria-label': t('trash.pick.title') + ' ' + String(item.name || ''),
|
|
7528
|
+
onChange: function () { toggle(key) },
|
|
7529
|
+
}))
|
|
7530
|
+
: null,
|
|
7016
7531
|
React.createElement('div', { className: 'dsm-trash-main' },
|
|
7017
7532
|
React.createElement('div', { className: 'dsm-name' }, item.name),
|
|
7018
7533
|
React.createElement('div', { className: 'dsm-note' }, t('trash.deletedAt', {
|
|
@@ -7020,38 +7535,79 @@ function enabledMemoryCount(rules) {
|
|
|
7020
7535
|
}))),
|
|
7021
7536
|
actions)
|
|
7022
7537
|
}
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7538
|
+
/**
|
|
7539
|
+
* 组头(标题 + 计数 + 后果说明)。分栏与非分栏共用同一套排版。
|
|
7540
|
+
*
|
|
7541
|
+
* 组头**不放按钮**:非分栏时勾选与删除都是跨组的(提示词页一次列两组),组头各放一颗
|
|
7542
|
+
* 「全选」就得各配一颗删除键 —— 那正是被砍掉的多颗形态;分栏时两颗动作下到每块自己的
|
|
7543
|
+
* 批量条里(`.dsm-trash-pane-batch`),组头仍然只是标题。
|
|
7544
|
+
*/
|
|
7545
|
+
const groupHead = function (g) {
|
|
7546
|
+
const list = g.entries || []
|
|
7547
|
+
return React.createElement('div', { className: 'dsm-trash-group-head' },
|
|
7548
|
+
React.createElement('div', { className: 'dsm-trash-group-head-row' },
|
|
7549
|
+
React.createElement('span', { className: 'dsm-trash-group-title' }, g.title),
|
|
7550
|
+
React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: list.length }))),
|
|
7551
|
+
// 后果说明进组头(sticky 跟着滚):这组删的是记录还是文件,一眼可辨。
|
|
7552
|
+
g.sub ? React.createElement('p', { className: 'dsm-trash-group-sub' }, g.sub) : null)
|
|
7553
|
+
}
|
|
7554
|
+
/**
|
|
7555
|
+
* 分栏模式的一块(组头 + 批量条 + 列表区),与技能回收站(43-skills.js 的 `trashPane`)
|
|
7556
|
+
* 同一套结构与类名。批量条挂在**每块里**:两块的后果不同(预设目录 / 快捷提示词目录),
|
|
7557
|
+
* 一颗共用的「全选 + 永久删除」会让人勾完之后不知道那一键要删哪个。
|
|
7558
|
+
*/
|
|
7559
|
+
const renderPane = function (g) {
|
|
7560
|
+
const list = g.entries || []
|
|
7561
|
+
const keys = (g.entries || []).map(function (item) { return rowKey(g, item) })
|
|
7562
|
+
const rows = props.loading
|
|
7563
|
+
? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
7564
|
+
: list.length === 0
|
|
7565
|
+
? React.createElement('div', { className: 'dsm-empty' }, t('trash.empty'))
|
|
7566
|
+
: list.map(function (item) { return row(g, item) })
|
|
7567
|
+
return React.createElement('div', { className: 'dsm-trash-group', key: String(g.title) },
|
|
7568
|
+
groupHead(g),
|
|
7569
|
+
// 批量条只在**这一块真有可删的东西**时出现:空块上挂一条「已选 0 项 + 全选 + 永久删除 (0)」
|
|
7570
|
+
// 是纯噪音,两块各占一半高度,那条还白吃掉一行的列表空间(技能页同一条判据)。
|
|
7571
|
+
batchEnabled && keys.length
|
|
7572
|
+
? React.createElement('div', { className: 'dsm-trash-pane-batch' }, bulkRow(keys, String(g.title)))
|
|
7573
|
+
: null,
|
|
7574
|
+
React.createElement('div', { className: 'dsm-trash-pane-body' }, rows))
|
|
7575
|
+
}
|
|
7027
7576
|
const renderGroup = function (g) {
|
|
7028
7577
|
const list = g.entries || []
|
|
7029
7578
|
const rows = props.loading
|
|
7030
7579
|
? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
7031
7580
|
: list.length === 0
|
|
7032
7581
|
? React.createElement('div', { className: 'dsm-empty' }, t('trash.empty'))
|
|
7033
|
-
: list.map(row)
|
|
7582
|
+
: list.map(function (item) { return row(g, item) })
|
|
7034
7583
|
return React.createElement('div', { className: 'dsm-trash-group', key: String(g.title) },
|
|
7035
|
-
|
|
7036
|
-
React.createElement('div', { className: 'dsm-trash-group-head-row' },
|
|
7037
|
-
React.createElement('span', { className: 'dsm-trash-group-title' }, g.title),
|
|
7038
|
-
React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: list.length }))),
|
|
7039
|
-
// 后果说明进组头(sticky 跟着滚):这组删的是记录还是文件,一眼可辨。
|
|
7040
|
-
g.sub ? React.createElement('p', { className: 'dsm-trash-group-sub' }, g.sub) : null),
|
|
7584
|
+
groupHead(g),
|
|
7041
7585
|
React.createElement('div', { className: 'dsm-trash-group-body' }, rows))
|
|
7042
7586
|
}
|
|
7043
|
-
//
|
|
7044
|
-
//
|
|
7587
|
+
// 批量那一行:「已选 N 项」占左侧,**「全选」与唯一的「永久删除」并排靠最右**
|
|
7588
|
+
// (用户 2026-10-10 裁定:删除键一个就够,与全选同一行、放最右边)。
|
|
7589
|
+
// 分栏时这一行只剩保留期 —— 批量条已经下到两块里,与技能回收站同形。
|
|
7590
|
+
const toolbar = split
|
|
7591
|
+
? React.createElement(RetentionSelect, { key: 'retention', t: t, onReload: props.onReload })
|
|
7592
|
+
: React.createElement('div', { className: 'dsm-trash-toolbar', key: 'toolbar' },
|
|
7593
|
+
React.createElement(RetentionSelect, { t: t, onReload: props.onReload }),
|
|
7594
|
+
batchEnabled ? bulkRow(allKeys, TRASH_BULK_ALL) : null)
|
|
7595
|
+
// 列表类弹窗:宽高都固定(`.dsm-modal-list` + `.dsm-modal-trash`),滚动只发生在
|
|
7596
|
+
// Modal 的 body 这一层 —— 条目数变化时弹窗不跳,也不会出现「弹窗滚 + 组内滚」的嵌套滚动条。
|
|
7597
|
+
// 分栏时反过来:body 不滚,滚动下到每块的列表区里(`.dsm-modal-trash-split`)。
|
|
7045
7598
|
return React.createElement(Modal, {
|
|
7046
7599
|
key: 'trash-modal',
|
|
7047
7600
|
title: props.title,
|
|
7048
7601
|
closeLabel: t('btn.close'),
|
|
7049
7602
|
list: true,
|
|
7050
|
-
className: 'dsm-modal-list',
|
|
7603
|
+
className: 'dsm-modal-list dsm-modal-trash' + (split ? ' dsm-modal-trash-split' : ''),
|
|
7051
7604
|
onClose: props.onClose,
|
|
7052
7605
|
},
|
|
7053
7606
|
props.error ? React.createElement(Notice, { kind: 'err', text: props.error }) : null,
|
|
7054
|
-
|
|
7607
|
+
toolbar,
|
|
7608
|
+
split
|
|
7609
|
+
? React.createElement('div', { className: 'dsm-trash-split' }, groups.map(renderPane))
|
|
7610
|
+
: groups.map(renderGroup))
|
|
7055
7611
|
}
|
|
7056
7612
|
|
|
7057
7613
|
/** 勾选行:复选框 + 名称/说明 + 右侧指标 + 可选行内动作。 */
|
|
@@ -7602,6 +8158,14 @@ function enabledMemoryCount(rules) {
|
|
|
7602
8158
|
var us = react.useState(null), upload = us[0], setUpload = us[1];
|
|
7603
8159
|
var ds = react.useState(null), detail = ds[0], setDetail = ds[1];
|
|
7604
8160
|
var cs = react.useState({ path: "", label: "" }), customForm = cs[0], setCustomForm = cs[1];
|
|
8161
|
+
// 技能回收站的两个 pane **各有一套勾选**(见下方 modal === "trash"):上「不再读取的来源」
|
|
8162
|
+
// 删的是一条记账(磁盘一个字节不动),下「回收站里的技能」删的是真文件 —— 合成一套勾选,
|
|
8163
|
+
// 一颗删除键就不知道该删哪个了。所以 key 是 { dirs: {key:true}, skills: {id:true} }。
|
|
8164
|
+
var tps = react.useState({ dirs: {}, skills: {} }), trashSel = tps[0], setTrashSel = tps[1];
|
|
8165
|
+
// 就地二次确认(`'dirs' | 'skills' | null`):删的是不可恢复的东西,问一次再动手。
|
|
8166
|
+
var tcs = react.useState(null), trashConfirm = tcs[0], setTrashConfirm = tcs[1];
|
|
8167
|
+
var tbs = react.useState(false), trashBusy = tbs[0], setTrashBusy = tbs[1];
|
|
8168
|
+
var tns = react.useState(null), trashNotice = tns[0], setTrashNotice = tns[1];
|
|
7605
8169
|
var ups = useUndoState(), undo = ups[0], setUndo = ups[1], dismissUndo = ups[2];
|
|
7606
8170
|
var inflightRef = react.useRef(false);
|
|
7607
8171
|
// 详情弹窗的请求序号(见 openDetail / closeModal):晚到的响应不得复活/顶掉弹窗内容。
|
|
@@ -7973,38 +8537,138 @@ function enabledMemoryCount(rules) {
|
|
|
7973
8537
|
// 卡片结构复用共享回收站的 .dsm-trash-group(组头 + 计数 + 后果说明 + 行列表),观感与其它页一致。
|
|
7974
8538
|
var skillTrashRows = data.trash || []
|
|
7975
8539
|
var removedSourceRows = removedRoots || []
|
|
7976
|
-
|
|
8540
|
+
// 勾选只在**真能删**的行上出现:目录块里内置来源只有「恢复读取」(它们随时能被重新发现,
|
|
8541
|
+
// 删掉记账没有意义),只有 `custom === true` 的行有「永久删除」。给不可删的行渲染勾选框,
|
|
8542
|
+
// 勾了也删不掉,比没有勾选框更让人困惑 —— 所以可勾集合就是可删集合。
|
|
8543
|
+
var forgettableKeys = removedSourceRows.filter(function (r) { return r.custom === true }).map(function (r) { return r.key })
|
|
8544
|
+
var skillKeys = skillTrashRows.map(function (i) { return i.id })
|
|
8545
|
+
function pickedIn(pane) { return trashSel[pane] || {} }
|
|
8546
|
+
function togglePick(pane, key) {
|
|
8547
|
+
setTrashSel(function (prev) {
|
|
8548
|
+
var paneMap = Object.assign({}, prev[pane] || {})
|
|
8549
|
+
if (paneMap[key]) delete paneMap[key]; else paneMap[key] = true
|
|
8550
|
+
var next = Object.assign({}, prev)
|
|
8551
|
+
next[pane] = paneMap
|
|
8552
|
+
return next
|
|
8553
|
+
})
|
|
8554
|
+
}
|
|
8555
|
+
function setPanePicks(pane, keys, on) {
|
|
8556
|
+
setTrashSel(function (prev) {
|
|
8557
|
+
var paneMap = Object.assign({}, prev[pane] || {})
|
|
8558
|
+
keys.forEach(function (k) { if (on) paneMap[k] = true; else delete paneMap[k] })
|
|
8559
|
+
var next = Object.assign({}, prev)
|
|
8560
|
+
next[pane] = paneMap
|
|
8561
|
+
return next
|
|
8562
|
+
})
|
|
8563
|
+
}
|
|
8564
|
+
/**
|
|
8565
|
+
* 一个 pane 的批量动作:串行逐个走**已有的单条 op**(零新增 op)。
|
|
8566
|
+
*
|
|
8567
|
+
* 串行而不是并发:条目数天然有限(用户手删出来的),串行才给得出「删了几个、哪个没删掉」,
|
|
8568
|
+
* 并发只会让失败原因混成一团。失败不中断 —— 一条删不掉不该把其余十条一起卡住。
|
|
8569
|
+
*/
|
|
8570
|
+
function runPaneBatch(pane, keys) {
|
|
8571
|
+
if (!keys.length) return
|
|
8572
|
+
var path = pane === "dirs" ? "/source-forget" : "/trash-delete"
|
|
8573
|
+
var resultKey = pane === "dirs" ? "trash.forgetMany" : "trash.purgeMany"
|
|
8574
|
+
setTrashBusy(true)
|
|
8575
|
+
setTrashConfirm(null)
|
|
8576
|
+
setTrashNotice(null)
|
|
8577
|
+
var done = 0, failed = 0, firstErr = null
|
|
8578
|
+
var step = function (i) {
|
|
8579
|
+
if (i >= keys.length) {
|
|
8580
|
+
setTrashBusy(false)
|
|
8581
|
+
setPanePicks(pane, keys, false)
|
|
8582
|
+
setTrashNotice({
|
|
8583
|
+
ok: failed === 0,
|
|
8584
|
+
text: failed === 0
|
|
8585
|
+
? t(resultKey + ".result", { count: done })
|
|
8586
|
+
: t(resultKey + ".partial", { count: done, failed: failed, reason: firstErr || "" }),
|
|
8587
|
+
})
|
|
8588
|
+
refresh(true)
|
|
8589
|
+
return
|
|
8590
|
+
}
|
|
8591
|
+
var body = pane === "dirs" ? { root: keys[i] } : { id: keys[i] }
|
|
8592
|
+
callApi(path, { body: JSON.stringify(body) }).then(function () {
|
|
8593
|
+
done += 1
|
|
8594
|
+
step(i + 1)
|
|
8595
|
+
}).catch(function (e) {
|
|
8596
|
+
failed += 1
|
|
8597
|
+
if (!firstErr) firstErr = translateError(t, e)
|
|
8598
|
+
step(i + 1)
|
|
8599
|
+
})
|
|
8600
|
+
}
|
|
8601
|
+
step(0)
|
|
8602
|
+
}
|
|
8603
|
+
/**
|
|
8604
|
+
* 一个 pane(组头 + 批量条 + 列表区)。
|
|
8605
|
+
*
|
|
8606
|
+
* 批量条挂在**每个 pane 里**而不是弹窗顶部一颗:两块的后果不同(上=删记账,下=删文件),
|
|
8607
|
+
* 一颗共用的「全选 / 永久删除」会让人勾了两条之后不知道那一键要删哪个。用户 2026-10-10
|
|
8608
|
+
* 裁定:「全选要拆成两部分」。
|
|
8609
|
+
*/
|
|
8610
|
+
function trashPane(key, paneId, title, count, note, danger, rows, emptyText, pickKeys, pickTitle) {
|
|
8611
|
+
var pickedMap = pickedIn(paneId)
|
|
8612
|
+
var picked = pickKeys.filter(function (k) { return pickedMap[k] === true })
|
|
8613
|
+
var allPicked = pickKeys.length > 0 && picked.length === pickKeys.length
|
|
8614
|
+
var batchRow = trashConfirm === paneId
|
|
8615
|
+
? h("div", { className: "dsm-trash-bulk dsm-trash-bulk-confirm" },
|
|
8616
|
+
h("span", { className: "dsm-note" }, t("trash.purgeSelected.confirm", { count: picked.length })),
|
|
8617
|
+
h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: trashBusy, onClick: function () { runPaneBatch(paneId, picked); } }, t("trash.btn.confirmPurgeMany", { count: picked.length })),
|
|
8618
|
+
h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: trashBusy, onClick: function () { setTrashConfirm(null); } }, t("btn.cancel")))
|
|
8619
|
+
: h("div", { className: "dsm-trash-bulk" },
|
|
8620
|
+
h("span", { className: "dsm-note" }, t("trash.selected", { count: picked.length })),
|
|
8621
|
+
h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-bulk", disabled: trashBusy || !pickKeys.length, title: pickTitle, onClick: function () { setPanePicks(paneId, pickKeys, !allPicked); } }, bulkPair(allPicked ? t("trash.deselectAll") : t("trash.selectAll"), allPicked ? t("trash.selectAll") : t("trash.deselectAll"))),
|
|
8622
|
+
h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: trashBusy || !picked.length, onClick: function () { setTrashConfirm(paneId); } }, t("trash.btn.purgeSelected", { count: picked.length })))
|
|
7977
8623
|
return h("div", { key: key, className: "dsm-trash-group" + (danger ? " dsm-trash-group-danger" : "") },
|
|
7978
8624
|
h("div", { className: "dsm-trash-group-head" },
|
|
7979
8625
|
h("div", { className: "dsm-trash-group-head-row" },
|
|
7980
8626
|
h("span", { className: "dsm-trash-group-title" }, title),
|
|
7981
8627
|
h("span", { className: "dsm-count" }, count)),
|
|
7982
8628
|
h("p", { className: "dsm-trash-group-sub" }, note)),
|
|
8629
|
+
// 批量条只在**这一块真有可勾的东西**时出现:空块上挂一条「已选 0 项 + 全选 + 永久删除 (0)」
|
|
8630
|
+
// 是纯噪音,而且两块各占一半高度、那条还白吃掉一行的列表空间。
|
|
8631
|
+
pickKeys.length ? h("div", { className: "dsm-trash-pane-batch" }, batchRow) : null,
|
|
7983
8632
|
h("div", { className: "dsm-trash-pane-body" }, rows.length ? rows : h("div", { className: "dsm-empty" }, emptyText)))
|
|
7984
8633
|
}
|
|
7985
|
-
content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), list: true, className: "dsm-modal-list dsm-modal-trash-split", onClose: function () { setModal(null); } },
|
|
8634
|
+
content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), list: true, className: "dsm-modal-list dsm-modal-trash dsm-modal-trash-split", onClose: function () { setModal(null); setTrashConfirm(null); setTrashNotice(null); } },
|
|
8635
|
+
// 保留期选择器(0.19.1):技能回收站此前只能一条条点永久删除,而且技能域还留了一句
|
|
8636
|
+
// 「回收站没有自动清理,超龄条目通过 warnings 提示」的旧裁定 —— 保留期默认 `0`(永久保留)
|
|
8637
|
+
// 正是为了不与那条裁定冲突:只有用户在这里显式打开,才会自动清。
|
|
8638
|
+
h(RetentionSelect, { key: "retention", t: t, onReload: function () { refresh(false); } }),
|
|
8639
|
+
// 批量结果就地回报:页面级的那条 `result` 横幅被弹窗盖着,看不见等于没说。
|
|
8640
|
+
trashNotice ? h("div", { key: "trash-notice", className: "dsm-feedback dsm-trash-notice" + (trashNotice.ok ? "" : " dsm-error"), role: "alert" }, trashNotice.text) : null,
|
|
7986
8641
|
h("div", { className: "dsm-trash-split" },
|
|
7987
|
-
trashPane("pane-dirs", t("trash.section.dirs"), t(countKey("trash.dirs.count", removedSourceRows.length), { count: removedSourceRows.length }), t("trash.section.dirs.sub"), false,
|
|
8642
|
+
trashPane("pane-dirs", "dirs", t("trash.section.dirs"), t(countKey("trash.dirs.count", removedSourceRows.length), { count: removedSourceRows.length }), t("trash.section.dirs.sub"), false,
|
|
7988
8643
|
removedSourceRows.map(function (root) {
|
|
7989
8644
|
var name = rootDisplayName(t, root)
|
|
7990
|
-
|
|
8645
|
+
var canForget = root.custom === true
|
|
8646
|
+
var picked = pickedIn("dirs")[root.key] === true
|
|
8647
|
+
return h("div", { key: root.key, className: "dsm-trash-item" + (picked ? " dsm-trash-item-picked" : "") },
|
|
8648
|
+
canForget ? h("label", { className: "dsm-trash-pick", title: t("trash.pick.title") },
|
|
8649
|
+
h("input", { type: "checkbox", checked: picked, disabled: trashBusy, "aria-label": t("trash.pick.title") + " " + name, onChange: function () { togglePick("dirs", root.key); } })) : null,
|
|
7991
8650
|
h("div", { className: "dsm-trash-main" },
|
|
7992
8651
|
h("div", { className: "dsm-name" }, name),
|
|
7993
8652
|
h("div", { className: "dsm-note", title: root.path }, root.path)),
|
|
7994
|
-
h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, onClick: function () { post("/source-restore", { root: root.key }, "result.sourceRestored", { name: name }); } }, t("btn.source.restore")),
|
|
7995
|
-
|
|
8653
|
+
h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy || trashBusy, onClick: function () { post("/source-restore", { root: root.key }, "result.sourceRestored", { name: name }); } }, t("btn.source.restore")),
|
|
8654
|
+
canForget ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy || trashBusy, onClick: function () { setModal({ type: "source-forget-confirm", key: root.key, name: name, path: root.path }); } }, t("btn.source.forget")) : null)
|
|
7996
8655
|
}),
|
|
7997
|
-
t("trash.section.dirs.empty")
|
|
7998
|
-
|
|
8656
|
+
t("trash.section.dirs.empty"),
|
|
8657
|
+
forgettableKeys, t("trash.pane.dirs.pick.title")),
|
|
8658
|
+
trashPane("pane-skills", "skills", t("trash.section.skills"), t(countKey("trash.count", skillTrashRows.length), { count: skillTrashRows.length }), t("trash.section.skills.sub"), false,
|
|
7999
8659
|
skillTrashRows.map(function (item) {
|
|
8000
|
-
|
|
8660
|
+
var picked = pickedIn("skills")[item.id] === true
|
|
8661
|
+
return h("div", { key: item.id, className: "dsm-trash-item" + (picked ? " dsm-trash-item-picked" : "") },
|
|
8662
|
+
h("label", { className: "dsm-trash-pick", title: t("trash.pick.title") },
|
|
8663
|
+
h("input", { type: "checkbox", checked: picked, disabled: trashBusy, "aria-label": t("trash.pick.title") + " " + item.name, onChange: function () { togglePick("skills", item.id); } })),
|
|
8001
8664
|
h("div", { className: "dsm-trash-main" },
|
|
8002
8665
|
h("div", { className: "dsm-name" }, item.name),
|
|
8003
8666
|
h("div", { className: "dsm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }))),
|
|
8004
|
-
h("button", { className: "dsm-btn dsm-btn-quiet", disabled: busy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")),
|
|
8005
|
-
h("button", { className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever")))
|
|
8667
|
+
h("button", { className: "dsm-btn dsm-btn-quiet", disabled: busy || trashBusy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")),
|
|
8668
|
+
h("button", { className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy || trashBusy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever")))
|
|
8006
8669
|
}),
|
|
8007
|
-
t("trash.empty")
|
|
8670
|
+
t("trash.empty"),
|
|
8671
|
+
skillKeys, null))))
|
|
8008
8672
|
}
|
|
8009
8673
|
if (modal && modal.type === "source-forget-confirm") content.push(h(Modal, { key: "source-forget-confirm", title: t("confirm.source.forget.title"), closeLabel: t("btn.close"), onClose: function () { setModal("trash"); } }, h("p", { className: "dsm-desc" }, t("confirm.source.forget.desc", { name: modal.name })), h("p", { className: "dsm-help" }, t("confirm.source.forget.hint", { path: modal.path })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-danger", disabled: busy, onClick: function () { submitSourceForget(modal.key, modal.name); } }, t("btn.delete.forever")))));
|
|
8010
8674
|
if (modal && modal.type === "trash-confirm") content.push(h(Modal, { key: "trash-confirm", title: t("confirm.trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("p", { className: "dsm-desc" }, t("confirm.trash.desc", { name: modal.name })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn", disabled: busy, onClick: function () { post("/delete", { root: modal.root, name: modal.name }, "result.trashed", { name: modal.name }).then(function () { setModal(null); }).catch(swallowPostError); } }, t("btn.trash")))));
|
|
@@ -8368,7 +9032,15 @@ function enabledMemoryCount(rules) {
|
|
|
8368
9032
|
{ value: 0, label: t('hist.retention.forever') },
|
|
8369
9033
|
{ value: 7, label: t('hist.retention.days', { count: 7 }) },
|
|
8370
9034
|
{ value: 30, label: t('hist.retention.days', { count: 30 }) },
|
|
9035
|
+
{ value: 90, label: t('hist.retention.days', { count: 90 }) },
|
|
8371
9036
|
]
|
|
9037
|
+
// 侧车里的值不在预设里时把它补成一项。`SourceSelect` 取不到就 `|| options[0]` 回退,
|
|
9038
|
+
// 于是「实际设的是 15 天」会显示成「永久保留」—— 是**静默误报**(用户以为不会自动删,
|
|
9039
|
+
// 其实会)。与回收站那套 RetentionSelect 同形;90 天之外用户仍可能从旧版本或手改带来别的值。
|
|
9040
|
+
var retentionDays = Number(data.retentionDays) || 0
|
|
9041
|
+
var retentionOpts = RETENTION_OPTS.some(function (o) { return o.value === retentionDays })
|
|
9042
|
+
? RETENTION_OPTS
|
|
9043
|
+
: [{ value: retentionDays, label: t('hist.retention.days', { count: retentionDays }) }].concat(RETENTION_OPTS)
|
|
8372
9044
|
var EXPORT_FORMAT_OPTS = [
|
|
8373
9045
|
{ value: 'markdown', label: 'Markdown' },
|
|
8374
9046
|
{ value: 'jsonl', label: 'JSONL' },
|
|
@@ -8501,7 +9173,7 @@ function enabledMemoryCount(rules) {
|
|
|
8501
9173
|
React.createElement('input', { className: 'dsm-control dsm-search', type: 'text', placeholder: t('hist.search.placeholder'), value: query, onChange: function (e) { setQuery(e.target.value) } }),
|
|
8502
9174
|
React.createElement('div', { className: 'dsm-source-filter' },
|
|
8503
9175
|
React.createElement(SourceSelect, {
|
|
8504
|
-
options:
|
|
9176
|
+
options: retentionOpts, value: data.retentionDays, onChange: function (v) { setRetention(Number(v)) },
|
|
8505
9177
|
}))),
|
|
8506
9178
|
// 走 Notice 而不是裸 div:令牌没过的提醒会在右侧自动多出一颗「填写令牌」按钮。
|
|
8507
9179
|
data.error ? React.createElement(Notice, { kind: 'err', text: String(data.error) }) : null,
|
|
@@ -8805,6 +9477,31 @@ function enabledMemoryCount(rules) {
|
|
|
8805
9477
|
else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
|
|
8806
9478
|
}).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
|
|
8807
9479
|
}
|
|
9480
|
+
/** 批量永久删除:逐条调单条 op,串行(理由见 42-shared-ui.js 的 TrashModal 注释)。 */
|
|
9481
|
+
function purgeSceneMany(items) {
|
|
9482
|
+
if (!items.length) return
|
|
9483
|
+
setTrashBusy(true)
|
|
9484
|
+
var done = 0, failed = 0, firstErr = null
|
|
9485
|
+
var step = function (i) {
|
|
9486
|
+
if (i >= items.length) {
|
|
9487
|
+
setTrashBusy(false)
|
|
9488
|
+
setResult({
|
|
9489
|
+
ok: failed === 0,
|
|
9490
|
+
text: failed === 0
|
|
9491
|
+
? t('trash.purgeMany.result', { count: done })
|
|
9492
|
+
: t('trash.purgeMany.partial', { count: done, failed: failed, reason: firstErr || '' }),
|
|
9493
|
+
})
|
|
9494
|
+
loadTrash(true)
|
|
9495
|
+
return
|
|
9496
|
+
}
|
|
9497
|
+
apiCall('scene-trash-delete', { id: items[i].id }).then(function (res) {
|
|
9498
|
+
if (res && res.ok) done += 1
|
|
9499
|
+
else { failed += 1; if (!firstErr) firstErr = (res && res.error) || t('mcp.msg.failed') }
|
|
9500
|
+
step(i + 1)
|
|
9501
|
+
}).catch(function (e) { failed += 1; if (!firstErr) firstErr = errMsg(e); step(i + 1) })
|
|
9502
|
+
}
|
|
9503
|
+
step(0)
|
|
9504
|
+
}
|
|
8808
9505
|
React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
|
|
8809
9506
|
function refresh(silent) {
|
|
8810
9507
|
if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
|
|
@@ -8905,7 +9602,8 @@ function enabledMemoryCount(rules) {
|
|
|
8905
9602
|
function loadPresetOptions(onLoaded) {
|
|
8906
9603
|
apiCall('agentsmd-list', {}).then(function (res) {
|
|
8907
9604
|
var list = (res && res.presets) || []
|
|
8908
|
-
//
|
|
9605
|
+
// 这里只放预设本身;表单里那颗下拉会在最前面补一条「无(不绑定提示词)」。
|
|
9606
|
+
// 默认选中当前生效的那份(用户裁定「默认就是当前启动的」)。
|
|
8909
9607
|
var opts = []
|
|
8910
9608
|
list.forEach(function (p) { opts.push({ value: p.id, label: p.active ? (p.id + ' · ' + t('scenes.prompt.activeTag')) : p.id }) })
|
|
8911
9609
|
setPresetOptions(opts)
|
|
@@ -8920,14 +9618,29 @@ function enabledMemoryCount(rules) {
|
|
|
8920
9618
|
setSceneForm(function (f) { return (f && String(f.prompt || '') !== '') ? f : Object.assign({}, f, { prompt: activeId }) })
|
|
8921
9619
|
}
|
|
8922
9620
|
// ── 场景建 / 改描述 / 改绑定的提示词 / 删 ──
|
|
8923
|
-
// `quickPrompts`
|
|
8924
|
-
//
|
|
8925
|
-
|
|
9621
|
+
// 快捷提示词在表单里是**三选一**,落盘时映射成 `quickPrompts` 的三种取值:
|
|
9622
|
+
// 'current' → null(这一域不绑、进出不碰)/'none' → [](绑空表:进入后全部停用)
|
|
9623
|
+
// /'custom' → 勾选数组(**空数组也算 custom**)。
|
|
9624
|
+
//
|
|
9625
|
+
// 为什么表单要单独存一个 `quickMode`,而不是像原来那样从数组反推下拉值:
|
|
9626
|
+
// 「无」现在**不显示**「本场景启用」那张勾选卡(用户 2026-10-10),于是"数组为空"
|
|
9627
|
+
// 必须能分出两种来 —— 用户主动选了「无」,和用户选了「自定义」但一条都还没勾。
|
|
9628
|
+
// 反推的话后者会立刻翻成「无」、卡片当场消失,连勾第二条的机会都没有。
|
|
9629
|
+
//
|
|
9630
|
+
// 新建场景默认 'none'(用户 2026-10-10:「无应该是第一条,新建场景默认选择」)。
|
|
9631
|
+
// 口径与场景档案里另外三域一致:MCP / 技能 / 人设本来就是「一段都没建 = 该域全部停用」,
|
|
9632
|
+
// 新场景起手就是"一个都不启用",快捷提示词跟着走才不别扭;而 `null`(进出都不碰)
|
|
9633
|
+
// 留给 0.18.0 之前建的老场景,编辑时按档案原样回填。
|
|
9634
|
+
function openCreateScene() { setSceneForm({ name: '', description: '', prompt: '', toolTablePreset: '', quickMode: 'none', quickPrompts: [], error: null }); setModal({ type: 'scene-create' }); loadPresetOptions(defaultPromptToActive); loadToolTablePresets(); loadQuickOptions() }
|
|
8926
9635
|
function openEditScene(scene) {
|
|
8927
9636
|
var sceneArchive = (data.archives || {})[scene.name] || {}
|
|
8928
9637
|
setSceneForm({
|
|
8929
9638
|
name: scene.name, description: scene.description || '', prompt: scene.prompt || '',
|
|
8930
9639
|
toolTablePreset: typeof sceneArchive.toolTablePreset === 'string' ? sceneArchive.toolTablePreset : '',
|
|
9640
|
+
// 档案里没有 quickPrompts 段 = 0.18.0 之前的老场景 = 'current'(进出不碰);
|
|
9641
|
+
// 空数组 = 'none';非空 = 'custom'。落盘的值与这里一一对应。
|
|
9642
|
+
quickMode: !Array.isArray(sceneArchive.quickPrompts)
|
|
9643
|
+
? 'current' : (sceneArchive.quickPrompts.length ? 'custom' : 'none'),
|
|
8931
9644
|
quickPrompts: Array.isArray(sceneArchive.quickPrompts) ? sceneArchive.quickPrompts.slice() : null,
|
|
8932
9645
|
error: null,
|
|
8933
9646
|
})
|
|
@@ -8953,8 +9666,11 @@ function enabledMemoryCount(rules) {
|
|
|
8953
9666
|
var toolTablePreset = String(sceneForm.toolTablePreset || '')
|
|
8954
9667
|
// 快捷提示词:null = 解绑(回到"保持现状"),数组 = 勾选集(空数组是"一条都不勾")。
|
|
8955
9668
|
// 两个方向都必须**显式送出去**,不能省略 —— 省略在服务端那边是"这一域不碰"。
|
|
8956
|
-
|
|
8957
|
-
|
|
9669
|
+
// 以 `quickMode` 为准而不是从数组反推:'custom' 且一条都没勾时数组是空的,
|
|
9670
|
+
// 那要落成 `[]`(= 进入后全部停用)—— 与「无」落盘的值一样。两者在磁盘上本就
|
|
9671
|
+
// 不可区分(都是"一条都不启用"),区分只在表单里:要不要显示那张勾选卡。
|
|
9672
|
+
var quickPrompts = sceneForm.quickMode === 'current' ? null
|
|
9673
|
+
: (sceneForm.quickMode === 'custom' ? (sceneForm.quickPrompts || []).slice() : [])
|
|
8958
9674
|
var payload = isEdit
|
|
8959
9675
|
? { name: originalName, nextName: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset, quickPrompts: quickPrompts }
|
|
8960
9676
|
: { name: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset, quickPrompts: quickPrompts }
|
|
@@ -9932,33 +10648,60 @@ function enabledMemoryCount(rules) {
|
|
|
9932
10648
|
React.createElement('div', { className: 'dsm-field' },
|
|
9933
10649
|
React.createElement('span', { className: 'dsm-label' }, t('scenes.field.prompt')),
|
|
9934
10650
|
presetOptions.length > 0
|
|
9935
|
-
|
|
10651
|
+
// 「无」排在第一位,是**不绑定**这一态在表单里唯一的入口(用户 2026-10-10 要求)。
|
|
10652
|
+
// 此前这里只有预设本身,于是「选了某份预设之后想改回不绑」在表单里无路可走。
|
|
10653
|
+
// 语义上是安全的:`prompt` 为空时 `resolveScenePreset()` 直接跳过这个场景,
|
|
10654
|
+
// 而 `sync()` 在没有场景驱动时会把进场景前的基线写回去 —— 从「绑了」切到
|
|
10655
|
+
// 「无」不会把上一份预设留在 AGENTS.md 里。
|
|
10656
|
+
// 候选用 `presetOptions` 而不是「无 + presetOptions」判空:一份预设都没有时
|
|
10657
|
+
// 该说的是「先去建一份」,而不是给一颗只有「无」的下拉(默认本来就是无)。
|
|
10658
|
+
? React.createElement(SourceSelect, {
|
|
10659
|
+
options: [{ value: '', label: t('scenes.field.prompt.none') }].concat(presetOptions),
|
|
10660
|
+
value: sceneForm.prompt || '',
|
|
10661
|
+
onChange: function (v) { setSceneForm(Object.assign({}, sceneForm, { prompt: v })) },
|
|
10662
|
+
})
|
|
9936
10663
|
: React.createElement('p', { className: 'dsm-help' }, t('scenes.prompt.noPresets')),
|
|
9937
10664
|
helpBullets(t, 'scenes.field.prompt.hint')),
|
|
9938
10665
|
// 快捷提示词(0.18.0):默认「当前启用的」= 这一域**不绑**,进出不碰它;
|
|
9939
|
-
//
|
|
10666
|
+
// 切到「自定义」才给清单 —— 勾进来的进入本场景后启用、没勾的停用,
|
|
9940
10667
|
// 退出场景按进场景前那批开关还原(与子智能体那一档同一条机制)。
|
|
9941
10668
|
// 空表是"绑了但一条都不勾"(进入后全部停用),与"不绑"是相反的意思,
|
|
9942
|
-
//
|
|
10669
|
+
// 所以这里是**三态**而不是两态:第三条「无」正是那个空表(用户 2026-10-10 要求)。
|
|
10670
|
+
// 下拉显示的就是 `quickMode` 本身(不从数组反推,理由见 openCreateScene 上面)。
|
|
9943
10671
|
React.createElement('div', { className: 'dsm-field' },
|
|
9944
10672
|
React.createElement('span', { className: 'dsm-label' }, t('scenes.field.quickPrompts')),
|
|
9945
10673
|
React.createElement(SourceSelect, {
|
|
9946
|
-
value:
|
|
9947
|
-
|
|
10674
|
+
value: sceneForm.quickMode || 'current',
|
|
10675
|
+
// 「无」排第一(用户 2026-10-10:「无应该是第一条」)—— 与全局提示词那一格
|
|
10676
|
+
// 同一条排法,把"新建场景会看到的那个"放在最上面。
|
|
10677
|
+
options: [
|
|
10678
|
+
{ value: 'none', label: t('scenes.field.quickPrompts.noneOption') },
|
|
10679
|
+
{ value: 'current', label: t('scenes.field.quickPrompts.current') },
|
|
10680
|
+
{ value: 'custom', label: t('scenes.field.quickPrompts.custom') },
|
|
10681
|
+
],
|
|
9948
10682
|
onChange: function (v) {
|
|
9949
10683
|
setSceneForm(Object.assign({}, sceneForm, {
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
//
|
|
9953
|
-
?
|
|
9954
|
-
:
|
|
10684
|
+
quickMode: v,
|
|
10685
|
+
quickPrompts: v === 'none'
|
|
10686
|
+
// 「无」= 绑一张空表:进入后这一域全部停用,退出时按进场景前的开关还原。
|
|
10687
|
+
? []
|
|
10688
|
+
: v === 'custom'
|
|
10689
|
+
// 切过来时从**当前开着的那些**起步(用户裁定:默认就是当前启用的),
|
|
10690
|
+
// 而不是给一张空表 —— 空表说的是"进入后全部停用"。一条都没开时起步
|
|
10691
|
+
// 就是空表,但模式仍是 'custom',卡片照常显示、勾得上。
|
|
10692
|
+
? (quickOptions || []).filter(function (p) { return p.enabled !== false }).map(function (p) { return p.id })
|
|
10693
|
+
: null,
|
|
9955
10694
|
}))
|
|
9956
10695
|
},
|
|
9957
10696
|
}),
|
|
9958
10697
|
// 勾选清单是一张段卡片(与档案编辑器同一套排版):段头报"已勾几条 / 共几条"并给
|
|
9959
10698
|
// 全选,段体按内容长(超过 6 行才内滚)。原先这里是一块裸的固定 216px 滚动体,
|
|
9960
10699
|
// 一条也占 216px —— 弹窗中间凭空一片空白(用户 2026-10-01 反馈)。
|
|
9961
|
-
|
|
10700
|
+
//
|
|
10701
|
+
// 只在「自定义」时出现(用户 2026-10-10:「快捷提示词是无时,本场景启用不要显示」):
|
|
10702
|
+
// 「无」说的是"这个场景不设置快捷提示词",底下再挂一张全空的勾选卡是自相矛盾;
|
|
10703
|
+
// 「当前启用的(保持现状)」本来就一个字节都不碰,更没有可勾的。
|
|
10704
|
+
sceneForm.quickMode === 'custom'
|
|
9962
10705
|
? (quickOptions || []).length
|
|
9963
10706
|
? seg({
|
|
9964
10707
|
title: t('scenes.field.quickPrompts.list'),
|
|
@@ -10037,813 +10780,1004 @@ function enabledMemoryCount(rules) {
|
|
|
10037
10780
|
onClose: function () { setTrash(null) },
|
|
10038
10781
|
onRestore: restoreScene,
|
|
10039
10782
|
onPurge: purgeScene,
|
|
10783
|
+
onPurgeMany: purgeSceneMany,
|
|
10784
|
+
onReload: function () { loadTrash(true) },
|
|
10040
10785
|
}) : null,
|
|
10041
10786
|
)
|
|
10042
10787
|
}
|
|
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
|
-
|
|
10119
|
-
function
|
|
10120
|
-
if (!
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
if (
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
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
|
-
var
|
|
10216
|
-
if (
|
|
10217
|
-
|
|
10218
|
-
|
|
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
|
-
if (
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
function
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
return
|
|
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
|
-
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
|
|
10317
|
-
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
10349
|
-
return
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
*
|
|
10356
|
-
*
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
var
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
var
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
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
|
-
|
|
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
|
-
var
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
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
|
-
if (!
|
|
10530
|
-
var
|
|
10531
|
-
|
|
10532
|
-
if (
|
|
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
|
-
return
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
React.createElement('
|
|
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
|
-
React.createElement('
|
|
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
|
-
React.createElement('
|
|
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
|
-
|
|
10788
|
+
// ---------- 子智能体页:人设文件(~/.dsh/subagents/*.md)管理 ----------
|
|
10789
|
+
/**
|
|
10790
|
+
* 「思考强度」档位的按 (provider, model) 缓存。
|
|
10791
|
+
*
|
|
10792
|
+
* 为什么放在**组件外**:放进组件里每次渲染都会重建,等于没有缓存,而档位清单要问
|
|
10793
|
+
* adapter(`llm.resolveModelInfo`,异步、可能联网)—— 编辑人设时来回切模型不该每次都问。
|
|
10794
|
+
* TTL 5 分钟:档位是模型能力声明,短时间内不会变;换模型时才重新问。
|
|
10795
|
+
*/
|
|
10796
|
+
var EFFORT_CACHE_TTL_MS = 5 * 60 * 1000
|
|
10797
|
+
var effortCacheByRoute = {}
|
|
10798
|
+
function readEffortCache(key) {
|
|
10799
|
+
var hit = effortCacheByRoute[key]
|
|
10800
|
+
if (!hit) return null
|
|
10801
|
+
if (Date.now() - hit.at > EFFORT_CACHE_TTL_MS) { delete effortCacheByRoute[key]; return null }
|
|
10802
|
+
return hit.value
|
|
10803
|
+
}
|
|
10804
|
+
function writeEffortCache(key, value) { effortCacheByRoute[key] = { at: Date.now(), value: value } }
|
|
10805
|
+
function SubagentsPage() {
|
|
10806
|
+
var state = React.useState({ loading: true, error: null, subagents: [] })
|
|
10807
|
+
var data = state[0], setData = state[1]
|
|
10808
|
+
var bs = React.useState(false)
|
|
10809
|
+
var busy = bs[0], setBusy = bs[1]
|
|
10810
|
+
var rs = React.useState(null)
|
|
10811
|
+
var result = rs[0], setResult = rs[1]
|
|
10812
|
+
var ms = React.useState(null)
|
|
10813
|
+
var modal = ms[0], setModal = ms[1]
|
|
10814
|
+
/**
|
|
10815
|
+
* 「思考强度」的加载状态。key = 已拉取/正在拉取的 `provider\0model`(空串 = 还没选模型)。
|
|
10816
|
+
* 与 `cands` 分开两个 state:档位是**按模型**懒加载的,而候选目录是一次性拉的,
|
|
10817
|
+
* 混在一起会让"切换模型"误触发整份候选重拉。
|
|
10818
|
+
*/
|
|
10819
|
+
var es = React.useState({ key: '', loading: false, error: null, efforts: [], defaultEffort: null })
|
|
10820
|
+
var eff = es[0], setEff = es[1]
|
|
10821
|
+
/** 换模型导致已存档位被清掉时的一次性提示(下次换模型或关弹窗即消失)。 */
|
|
10822
|
+
var ens = React.useState(null)
|
|
10823
|
+
var effNotice = ens[0], setEffNotice = ens[1]
|
|
10824
|
+
// 搜索词独立一处:`data` 在 refresh 时被整对象替换,寄在它里面会被刷新清空。
|
|
10825
|
+
// 过滤用防抖值,输入框仍绑原值(与提示词页同一写法)。
|
|
10826
|
+
var qs = React.useState('')
|
|
10827
|
+
var query = qs[0], setQuery = qs[1]
|
|
10828
|
+
var dq = useDebouncedValue(query)
|
|
10829
|
+
// 场景锁定(v0.8):任一场景锁定 = 五个域整体冻结;本页全部写控件禁用。
|
|
10830
|
+
var anyLocked = data.anyLocked === true
|
|
10831
|
+
// 场景内开关由档案定义:人设开关置灰(要改就去场景页的档案编辑器里改)。
|
|
10832
|
+
var sceneName = data.activeScene || null
|
|
10833
|
+
// 回收站:删除人设 = 移入回收站(宿主侧 subagent-delete),这里列出/恢复/永久删除。
|
|
10834
|
+
var ts = React.useState(null)
|
|
10835
|
+
var trash = ts[0], setTrash = ts[1]
|
|
10836
|
+
var tb = React.useState(false)
|
|
10837
|
+
var trashBusy = tb[0], setTrashBusy = tb[1]
|
|
10838
|
+
var flipRef = React.useRef(null)
|
|
10839
|
+
useFlipReorder(flipRef)
|
|
10840
|
+
function loadTrash(keepOpen) {
|
|
10841
|
+
setTrash(Object.assign({ loading: true, error: null, entries: [] }, keepOpen ? trash || {} : {}))
|
|
10842
|
+
apiCall('subagent-trash-list', {}).then(function (res) {
|
|
10843
|
+
if (res && res.ok) setTrash({ loading: false, error: null, entries: res.trash || [] })
|
|
10844
|
+
else setTrash({ loading: false, error: (res && res.error) || t('trash.loadFailed'), entries: [] })
|
|
10845
|
+
}).catch(function (e) { setTrash({ loading: false, error: errMsg(e), entries: [] }) })
|
|
10846
|
+
}
|
|
10847
|
+
function restorePersona(item) {
|
|
10848
|
+
setTrashBusy(true)
|
|
10849
|
+
apiCall('subagent-trash-restore', { id: item.id }).then(function (res) {
|
|
10850
|
+
setTrashBusy(false)
|
|
10851
|
+
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) }
|
|
10852
|
+
else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
|
|
10853
|
+
}).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
|
|
10854
|
+
}
|
|
10855
|
+
function purgePersona(item) {
|
|
10856
|
+
setTrashBusy(true)
|
|
10857
|
+
apiCall('subagent-trash-delete', { id: item.id }).then(function (res) {
|
|
10858
|
+
setTrashBusy(false)
|
|
10859
|
+
if (res && res.ok) { setResult({ ok: true, text: t('trash.purged', { name: item.name }) }); loadTrash(true) }
|
|
10860
|
+
else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
|
|
10861
|
+
}).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
|
|
10862
|
+
}
|
|
10863
|
+
/** 批量永久删除:逐条调单条 op,串行(理由见 42-shared-ui.js 的 TrashModal 注释)。 */
|
|
10864
|
+
function purgePersonaMany(items) {
|
|
10865
|
+
if (!items.length) return
|
|
10866
|
+
setTrashBusy(true)
|
|
10867
|
+
var done = 0, failed = 0, firstErr = null
|
|
10868
|
+
var step = function (i) {
|
|
10869
|
+
if (i >= items.length) {
|
|
10870
|
+
setTrashBusy(false)
|
|
10871
|
+
setResult({
|
|
10872
|
+
ok: failed === 0,
|
|
10873
|
+
text: failed === 0
|
|
10874
|
+
? t('trash.purgeMany.result', { count: done })
|
|
10875
|
+
: t('trash.purgeMany.partial', { count: done, failed: failed, reason: firstErr || '' }),
|
|
10876
|
+
})
|
|
10877
|
+
loadTrash(true)
|
|
10878
|
+
return
|
|
10879
|
+
}
|
|
10880
|
+
apiCall('subagent-trash-delete', { id: items[i].id }).then(function (res) {
|
|
10881
|
+
if (res && res.ok) done += 1
|
|
10882
|
+
else { failed += 1; if (!firstErr) firstErr = (res && res.error) || t('mcp.msg.failed') }
|
|
10883
|
+
step(i + 1)
|
|
10884
|
+
}).catch(function (e) { failed += 1; if (!firstErr) firstErr = errMsg(e); step(i + 1) })
|
|
10885
|
+
}
|
|
10886
|
+
step(0)
|
|
10887
|
+
}
|
|
10888
|
+
React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
|
|
10889
|
+
function refresh(silent) {
|
|
10890
|
+
if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
|
|
10891
|
+
apiCall('subagent-list', {}).then(function (r) {
|
|
10892
|
+
// `activeScene` 必须一起搬进来:这里是把 `data` 整对象替换掉(搜索词另存就是为了
|
|
10893
|
+
// 不被冲掉),漏一个字段等于那个字段永远是初始值 —— 场景横幅因此从来没显示过
|
|
10894
|
+
// (2026-09-19 用户报的:技能 / 子智能体 / 记忆三页都看不到"当前处于场景…")。
|
|
10895
|
+
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 : [] })
|
|
10896
|
+
else setData({ loading: false, error: translateError(t, r), subagents: [], toolFailures: [] })
|
|
10897
|
+
}).catch(function (e) { setData({ loading: false, error: errMsg(e), subagents: [], toolFailures: [] }) })
|
|
10898
|
+
}
|
|
10899
|
+
React.useEffect(function () { refresh() }, [])
|
|
10900
|
+
/**
|
|
10901
|
+
* 子智能体开关(与记忆页同一套交互):停用 = 不注入目录段、subagent_manager_list/run
|
|
10902
|
+
* 不可见;人设文件不动。开关联动全局状态,立即提交。
|
|
10903
|
+
*/
|
|
10904
|
+
function togglePersona(p) {
|
|
10905
|
+
var next = !(p.enabled !== false)
|
|
10906
|
+
setBusy(true)
|
|
10907
|
+
apiCall('subagent-toggle', { name: p.name, enabled: next }).then(function (r) {
|
|
10908
|
+
setBusy(false)
|
|
10909
|
+
if (r && r.ok) {
|
|
10910
|
+
// 场景内改开关:人设改了、档案没跟上时说清楚(详见 sceneSyncWarn)。
|
|
10911
|
+
var warn = sceneSyncWarn(t, r)
|
|
10912
|
+
setResult(warn ? { ok: false, warning: true, text: warn } : null)
|
|
10913
|
+
refresh(true)
|
|
10914
|
+
} else if (r) setResult({ ok: false, text: translateError(t, r) })
|
|
10915
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
|
|
10916
|
+
}
|
|
10917
|
+
/**
|
|
10918
|
+
* 人设表单的候选数据(模型目录 / 全体预设工具并集)只在**首次展开高级选项**时拉取:
|
|
10919
|
+
* 宿主枚举预设需要为尚未挂载的预设建立 standing mount,不该在打开弹窗时就付这个代价。
|
|
10920
|
+
*/
|
|
10921
|
+
var cands = React.useState({ loaded: false, loading: false, error: null, models: [], providers: [], tools: [], presets: [] })
|
|
10922
|
+
var cand = cands[0], setCand = cands[1]
|
|
10923
|
+
function loadCandidates() {
|
|
10924
|
+
if (cand.loaded || cand.loading) return
|
|
10925
|
+
setCand(Object.assign({}, cand, { loading: true, error: null }))
|
|
10926
|
+
Promise.all([apiCall('model-candidates', {}), apiCall('preset-tools', {})]).then(function (rs) {
|
|
10927
|
+
var mres = rs[0], tres = rs[1]
|
|
10928
|
+
var failed = (!mres || mres.ok === false) && (!tres || tres.ok === false)
|
|
10929
|
+
setCand({
|
|
10930
|
+
loaded: true, loading: false,
|
|
10931
|
+
error: failed ? t('subagents.adv.loadFailed') : null,
|
|
10932
|
+
models: (mres && mres.models) || [],
|
|
10933
|
+
// providers 是去重后的来源清单,**不能**由 models 去重反推:适配器不提供
|
|
10934
|
+
// listModels 时该来源一个模型都报不出来,而"来源有、目录里没模型"正是最需要
|
|
10935
|
+
// 手填模型 id 的场景 —— 反推会让这类来源在「模型来源」下拉里整个消失。
|
|
10936
|
+
providers: (mres && mres.providers) || [],
|
|
10937
|
+
tools: (tres && tres.tools) || [],
|
|
10938
|
+
// presets 必须原样带过来:四行模式的名字、顺序、每行的工具清单全靠它,
|
|
10939
|
+
// 早先这里只留 models/tools,于是分组标题退回裸 id(中文界面里显示英文)。
|
|
10940
|
+
presets: (tres && tres.presets) || [],
|
|
10941
|
+
})
|
|
10942
|
+
}).catch(function (e) {
|
|
10943
|
+
setCand({ loaded: true, loading: false, error: errMsg(e), models: [], providers: [], tools: [], presets: [] })
|
|
10944
|
+
})
|
|
10945
|
+
}
|
|
10946
|
+
/**
|
|
10947
|
+
* 拉取某个 (provider, model) 的思考强度档位。**只有真正展开了高级选项、且选了模型**才会
|
|
10948
|
+
* 被调到(三个调用点:展开高级选项、模型下拉变更、自定义模型输入失焦)。
|
|
10949
|
+
*
|
|
10950
|
+
* 两个语义要点(都来自官方源码,见 src/subagents/service.ts 的 `reasoningEffort` 注释):
|
|
10951
|
+
* · 档位跟模型走 —— 换模型必须重拉,旧清单对新模型无效;
|
|
10952
|
+
* · **拉取失败一律不动已存的值**:只有"成功且当前值不在清单里"才清档。否则一次网络
|
|
10953
|
+
* 抖动就把用户设置抹了,而失败本身已经有提示。
|
|
10954
|
+
*/
|
|
10955
|
+
function loadEfforts(provider, model) {
|
|
10956
|
+
setEffNotice(null)
|
|
10957
|
+
var pv = String(provider || ''), md = String(model || '')
|
|
10958
|
+
if (!pv || !md) { setEff({ key: '', loading: false, error: null, efforts: [], defaultEffort: null }); return }
|
|
10959
|
+
var key = pv + '\u0000' + md
|
|
10960
|
+
var cached = readEffortCache(key)
|
|
10961
|
+
if (cached) {
|
|
10962
|
+
setEff({ key: key, loading: false, error: cached.ok ? null : cached.error, efforts: cached.efforts || [], defaultEffort: cached.defaultEffort || null })
|
|
10963
|
+
if (cached.ok) dropEffortIfGone(cached.efforts || [])
|
|
10964
|
+
return
|
|
10965
|
+
}
|
|
10966
|
+
setEff({ key: key, loading: true, error: null, efforts: [], defaultEffort: null })
|
|
10967
|
+
apiCall('model-reasoning', { provider: pv, model: md }).then(function (res) {
|
|
10968
|
+
var value = res && res.ok !== false
|
|
10969
|
+
? { ok: true, efforts: res.efforts || [], defaultEffort: res.defaultEffort || null }
|
|
10970
|
+
: { ok: false, error: (res && res.error) || t('subagents.effort.unavailable'), efforts: [], defaultEffort: null }
|
|
10971
|
+
writeEffortCache(key, value)
|
|
10972
|
+
setEff({ key: key, loading: false, error: value.ok ? null : value.error, efforts: value.efforts, defaultEffort: value.defaultEffort })
|
|
10973
|
+
if (value.ok) dropEffortIfGone(value.efforts)
|
|
10974
|
+
}).catch(function (e) {
|
|
10975
|
+
var value = { ok: false, error: errMsg(e), efforts: [], defaultEffort: null }
|
|
10976
|
+
writeEffortCache(key, value)
|
|
10977
|
+
setEff({ key: key, loading: false, error: value.error, efforts: [], defaultEffort: null })
|
|
10978
|
+
})
|
|
10979
|
+
}
|
|
10980
|
+
/**
|
|
10981
|
+
* 自动清档:清单里没有当前存的那一档 → 清掉并给一次性提示。
|
|
10982
|
+
*
|
|
10983
|
+
* 为什么必须清:官方对**不支持**的显式档位是在 provider I/O 之前直接拒(不夹紧、不别名),
|
|
10984
|
+
* 留着它 = 下一次委派直接起不来,而错误要到那时才看得见。
|
|
10985
|
+
* 为什么只在"拉取成功"时判断:失败时清单是空的,拿空清单当"没有这一档"会把设置误清。
|
|
10986
|
+
*/
|
|
10987
|
+
function dropEffortIfGone(list) {
|
|
10988
|
+
if (!modal || modal.type !== 'editor') return
|
|
10989
|
+
var cur = String(modal.form.reasoningEffort || '')
|
|
10990
|
+
if (!cur) return
|
|
10991
|
+
var has = (list || []).some(function (e) { return String(e && e.id) === cur })
|
|
10992
|
+
if (has) return
|
|
10993
|
+
setEffNotice(t('subagents.effort.dropped', { effort: cur }))
|
|
10994
|
+
setForm({ reasoningEffort: '' })
|
|
10995
|
+
}
|
|
10996
|
+
/** 当前表单的 (provider, model) 路由键 —— 用来与 `eff.key` 比,确认清单属于当前模型。 */
|
|
10997
|
+
function currentRouteKey() {
|
|
10998
|
+
return String((modal && modal.form.provider) || '') + '\u0000' + String((modal && modal.form.model) || '')
|
|
10999
|
+
}
|
|
11000
|
+
/**
|
|
11001
|
+
* 当前模型**已就绪**的档位。未就绪一律返回空数组:切换模型后 `eff` 里还留着上一个模型的
|
|
11002
|
+
* 清单(新请求还没回来),照它渲染会让用户选中一个不属于这个模型的档位 —— 而官方对不支持的
|
|
11003
|
+
* 档位是直接拒,等于埋一次"委派起不来"。
|
|
11004
|
+
*/
|
|
11005
|
+
function currentEfforts() {
|
|
11006
|
+
if (!modal || !modal.form.model || eff.loading || eff.error) return []
|
|
11007
|
+
return eff.key === currentRouteKey() ? (eff.efforts || []) : []
|
|
11008
|
+
}
|
|
11009
|
+
/** 空档(不指定)那一行的悬停说明:把 adapter 给的默认档位名字显示出来(拿不到就不显示)。 */
|
|
11010
|
+
function defaultEffortTitle() {
|
|
11011
|
+
var id = eff.defaultEffort
|
|
11012
|
+
if (!id || eff.key !== currentRouteKey()) return undefined
|
|
11013
|
+
var hit = (eff.efforts || []).filter(function (e) { return String(e && e.id) === String(id) })[0]
|
|
11014
|
+
return hit ? String(hit.name || hit.id) : String(id)
|
|
11015
|
+
}
|
|
11016
|
+
/** 高级选项:默认收起;已经在用模型/工具限制的人设自动展开(否则用户看不见自己配了什么)。 */
|
|
11017
|
+
/** 「不限制嵌套」的取值(与 src/subagents/service.ts 的 UNLIMITED_PERSONA_CATALOG_DEPTH 同值)。 */
|
|
11018
|
+
var CATALOG_DEPTH_UNLIMITED = 99
|
|
11019
|
+
/** 目录注入深度的选项文案:下拉与折叠摘要共用一处,免得两处说法分叉。 */
|
|
11020
|
+
function catalogDepthLabel(n) {
|
|
11021
|
+
if (n === 1) return t('subagents.field.catalogDepth.onlyTop')
|
|
11022
|
+
if (n === 2) return t('subagents.field.catalogDepth.toChild')
|
|
11023
|
+
if (n >= CATALOG_DEPTH_UNLIMITED) return t('subagents.field.catalogDepth.unlimited')
|
|
11024
|
+
return t('subagents.field.catalogDepth.toGrand')
|
|
11025
|
+
}
|
|
11026
|
+
function initialAdvanced(p) {
|
|
11027
|
+
var modes = p && p.toolsByPreset ? Object.keys(p.toolsByPreset).length : 0
|
|
11028
|
+
// 目录注入深度不是默认值(1)时也算"配过":它决定常驻目录出现在哪些会话,
|
|
11029
|
+
// 藏起来会让"为什么子会话看不到目录"变得无从查起。
|
|
11030
|
+
var budget = !!(p && typeof p.catalogDepth === 'number' && p.catalogDepth !== 1)
|
|
11031
|
+
// 允许追问同理:它改变的是"下一次委派会怎么跑"(回执一个 id 而不是产出),
|
|
11032
|
+
// 藏在收起的高级选项里等于用户改过又忘了自己改过。
|
|
11033
|
+
var continuable = !!(p && p.continuable === true)
|
|
11034
|
+
// 思考强度同理:配过就得让人看得见 —— 它是"下一次委派会怎么跑"的一部分,
|
|
11035
|
+
// 藏在收起的高级选项里等于用户改过又忘了自己改过。
|
|
11036
|
+
return !!(budget || continuable || (p && (p.model || p.provider || p.reasoningEffort)) || (p && ((p.tools || []).length || (p.toolsDeny || []).length || modes)))
|
|
11037
|
+
}
|
|
11038
|
+
/** 编辑态里按模式分组的名单(深拷贝:取消编辑不留痕)。 */
|
|
11039
|
+
function cloneRules(source) {
|
|
11040
|
+
var out = {}
|
|
11041
|
+
Object.keys(source || {}).forEach(function (id) {
|
|
11042
|
+
var rule = source[id] || {}
|
|
11043
|
+
out[id] = { mode: rule.mode === 'deny' ? 'deny' : 'allow', names: (rule.names || []).slice() }
|
|
11044
|
+
})
|
|
11045
|
+
return out
|
|
11046
|
+
}
|
|
11047
|
+
function openEditor(name) {
|
|
11048
|
+
if (!name) {
|
|
11049
|
+
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 } })
|
|
11050
|
+
return
|
|
11051
|
+
}
|
|
11052
|
+
setBusy(true)
|
|
11053
|
+
apiCall('subagent-get', { name: name }).then(function (res) {
|
|
11054
|
+
setBusy(false)
|
|
11055
|
+
if (res && res.ok) {
|
|
11056
|
+
var p = res.persona || {}
|
|
11057
|
+
var advanced = initialAdvanced(p)
|
|
11058
|
+
setModal({ type: 'editor', mode: 'edit', originalName: String(p.name || name), advanced: advanced, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: {
|
|
11059
|
+
name: p.name || name, description: p.description || '', provider: p.provider || '', model: p.model || '',
|
|
11060
|
+
// 思考强度:服务端回空串 = 这份人设没指定(不是"没有档位")。空值不落盘,见 serializePersona。
|
|
11061
|
+
reasoningEffort: p.reasoningEffort || '',
|
|
11062
|
+
// 服务端回的是**生效值**(没写就是默认 1),所以这里不必再兜默认。
|
|
11063
|
+
catalogDepth: typeof p.catalogDepth === 'number' ? p.catalogDepth : 1,
|
|
11064
|
+
// 服务端回布尔;老服务端不带这个字段时按 false(一次性)算。
|
|
11065
|
+
continuable: p.continuable === true,
|
|
11066
|
+
tools: (p.tools || []).slice(), toolsDeny: (p.toolsDeny || []).slice(),
|
|
11067
|
+
toolsByPreset: cloneRules(p.toolsByPreset), body: p.body || '', output: p.output || '', error: null,
|
|
11068
|
+
} })
|
|
11069
|
+
// 已配过限制的人设**一打开就是展开的**(initialAdvanced)→ 候选数据必须在这里也拉,
|
|
11070
|
+
// 否则四行模式先亮"宿主没有回传预设名单",非得点两次「高级选项」才补上(用户实测)。
|
|
11071
|
+
// 思考强度同理:展开着就要有档位清单(它按模型懒加载,所以这里显式带上 model)。
|
|
11072
|
+
if (advanced) { loadCandidates(); loadEfforts(p.provider, p.model) }
|
|
11073
|
+
} else setResult({ ok: false, text: translateError(t, res) })
|
|
11074
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
|
|
11075
|
+
}
|
|
11076
|
+
function setForm(patch) { if (modal && modal.type === 'editor') setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, patch) })) }
|
|
11077
|
+
/** 列表里增删一项(勾选语义:存在 = 勾上)。 */
|
|
11078
|
+
function toggled(list, name) {
|
|
11079
|
+
var next = (list || []).slice()
|
|
11080
|
+
var i = next.indexOf(name)
|
|
11081
|
+
if (i >= 0) next.splice(i, 1); else next.push(name)
|
|
11082
|
+
return next
|
|
11083
|
+
}
|
|
11084
|
+
/**
|
|
11085
|
+
* 模型下拉的当前值:`provider\0model` 复合键(与每个选项的 value 同构)。
|
|
11086
|
+
*
|
|
11087
|
+
* 目录里查不到这对 (来源, 模型) 时也**照原样给**,靠 `offCatalogModel()` 那条额外选项
|
|
11088
|
+
* 显示出来。0.19.0 之前这里会退回一个「自定义 / 目录里没有…」菜单项并把输入框露出来,
|
|
11089
|
+
* 2026-10-07 用户裁定删掉整条手填通道:来源与模型都只从宿主目录里选。
|
|
11090
|
+
*/
|
|
11091
|
+
function modelSelectValue() {
|
|
11092
|
+
if (!modal || modal.type !== 'editor') return ''
|
|
11093
|
+
var f = modal.form
|
|
11094
|
+
if (!f.model) return ''
|
|
11095
|
+
return String(f.provider || '') + '\u0000' + String(f.model)
|
|
11096
|
+
}
|
|
11097
|
+
/**
|
|
11098
|
+
* 存的 (来源, 模型) **不在**宿主目录里时,返回 `modelSelectValue()` 的那个值(多列一条
|
|
11099
|
+
* 只读选项用);在目录里、或模型为空时返回 ''。
|
|
11100
|
+
*
|
|
11101
|
+
* 为什么必须有这一条:0.19.0 之前这两个字段是纯文本框,现存人设里就有目录外的值;
|
|
11102
|
+
* 下拉里找不到对应项时浏览器会显示第一项(「继承主会话」)—— **表单在说谎**,而保存时
|
|
11103
|
+
* 又把那个看不见的值写回去。多列一条带标记的选项,值就永远看得见。
|
|
11104
|
+
*/
|
|
11105
|
+
function offCatalogModel() {
|
|
11106
|
+
if (!modal || modal.type !== 'editor') return ''
|
|
11107
|
+
var f = modal.form
|
|
11108
|
+
if (!f.model) return ''
|
|
11109
|
+
var provider = String(f.provider || '')
|
|
11110
|
+
var hit = (cand.models || []).filter(function (m) { return m.provider === provider && m.id === f.model })[0]
|
|
11111
|
+
return hit ? '' : modelSelectValue()
|
|
11112
|
+
}
|
|
11113
|
+
/** 「模型来源」下拉的当前值 —— 就是存的来源本身(目录外的靠 `offCatalogProvider()` 显示)。 */
|
|
11114
|
+
function providerSelectValue() {
|
|
11115
|
+
if (!modal || modal.type !== 'editor') return ''
|
|
11116
|
+
return String(modal.form.provider || '')
|
|
11117
|
+
}
|
|
11118
|
+
/** 存的来源不在宿主目录里时返回它(多列一条只读选项用);在目录里或为空返回 ''。 */
|
|
11119
|
+
function offCatalogProvider() {
|
|
11120
|
+
if (!modal || modal.type !== 'editor') return ''
|
|
11121
|
+
var v = String(modal.form.provider || '')
|
|
11122
|
+
if (!v) return ''
|
|
11123
|
+
var hit = (cand.providers || []).filter(function (p) { return p.id === v })[0]
|
|
11124
|
+
return hit ? '' : v
|
|
11125
|
+
}
|
|
11126
|
+
/**
|
|
11127
|
+
* 目录这次**真的读到了**吗。
|
|
11128
|
+
*
|
|
11129
|
+
* 「目录里没有」是一句断言,只有读成功才配说:读失败(`cand.error`)时清单是空的,
|
|
11130
|
+
* 照断言会把"没读到"说成"不存在"。读不到时那条额外选项仍然渲染,只是不带标记。
|
|
11131
|
+
*/
|
|
11132
|
+
function catalogReady() {
|
|
11133
|
+
return !!(cand.loaded && !cand.loading && !cand.error)
|
|
11134
|
+
}
|
|
11135
|
+
/**
|
|
11136
|
+
* 来源下拉的选项文字:显示名与 id 不同时两个都给(`DeepSeek · deepseek`)——
|
|
11137
|
+
* 显示名给人看,id 是真正写进 frontmatter 的那个键,只给一个都算缺信息。
|
|
11138
|
+
*/
|
|
11139
|
+
function providerLabel(p) {
|
|
11140
|
+
var id = String((p && p.id) || '')
|
|
11141
|
+
var name = String((p && p.name) || '')
|
|
11142
|
+
return name && name !== id ? name + ' · ' + id : id
|
|
11143
|
+
}
|
|
11144
|
+
/**
|
|
11145
|
+
* 模型下拉要列哪些模型:**来源选定后只列该来源的**(用户 2026-10-07)。
|
|
11146
|
+
* 不按来源收窄时,一份几十条的扁平列表里选错来源是常态,而错配的 (来源, 模型) 要到
|
|
11147
|
+
* 子代理真正启动时才由官方报错 —— 那时人已经在别的页面了。
|
|
11148
|
+
* 来源留空则列全部:此时挑中哪一条,就把它的来源一并填上(见 onChange)。
|
|
11149
|
+
*/
|
|
11150
|
+
function modelOptions() {
|
|
11151
|
+
if (!modal || modal.type !== 'editor') return []
|
|
11152
|
+
var all = cand.models || []
|
|
11153
|
+
var p = modal.form.provider || ''
|
|
11154
|
+
if (!p) return all
|
|
11155
|
+
return all.filter(function (m) { return m.provider === p })
|
|
11156
|
+
}
|
|
11157
|
+
/** 模型选项文字:已按来源收窄时不再重复来源前缀(左边那一格就是来源)。 */
|
|
11158
|
+
function modelOptionLabel(m) {
|
|
11159
|
+
var narrowed = !!(modal && modal.type === 'editor' && modal.form.provider)
|
|
11160
|
+
return narrowed ? String((m && (m.name || m.id)) || '') : m.provider + ' · ' + m.name
|
|
11161
|
+
}
|
|
11162
|
+
/**
|
|
11163
|
+
* 「高级选项」收起态**第二行**的内容;返回 null = 这一行整个不出现。
|
|
11164
|
+
* 只有真配了工具限制才报:收起态挂一句「模式限制:0 项;旧格式:白名单 0 / 黑名单 0」
|
|
11165
|
+
* 是用一行废话占位置(用户 2026-10-07 指出)。
|
|
11166
|
+
*/
|
|
11167
|
+
function advLimitLine() {
|
|
11168
|
+
if (!modal || modal.type !== 'editor') return null
|
|
11169
|
+
var modes = Object.keys(modal.form.toolsByPreset || {}).length
|
|
11170
|
+
var allow = (modal.form.tools || []).length
|
|
11171
|
+
var deny = (modal.form.toolsDeny || []).length
|
|
11172
|
+
if (!modes && !allow && !deny) return null
|
|
11173
|
+
return t('subagents.adv.summary2', {
|
|
11174
|
+
modes: modes,
|
|
11175
|
+
// 旧格式只在有值时报;分隔号写在词典值里(见 adv.summary 的 tail)。
|
|
11176
|
+
tail: (allow || deny) ? t('subagents.adv.legacy', { allow: allow, deny: deny }) : '',
|
|
11177
|
+
})
|
|
11178
|
+
}
|
|
11179
|
+
/**
|
|
11180
|
+
* 官方四个预设的中英名走插件词典(官方 preset.yml 里只有中文名,直接用会让英文界面
|
|
11181
|
+
* 露出中文);**自建预设用它自己的名字,不翻译** —— 与官方 display 规则一致。
|
|
11182
|
+
* 四个预设 id → 词典键的映射只认官方发布的这四个 id。
|
|
11183
|
+
*/
|
|
11184
|
+
var SHIPPED_PRESET_IDS = { minimal: 1, standard: 1, ptc: 1, cordis: 1 }
|
|
11185
|
+
function presetLabel(p) {
|
|
11186
|
+
if (!p) return ''
|
|
11187
|
+
var id = String(p.id || '')
|
|
11188
|
+
if (SHIPPED_PRESET_IDS[id] === 1 && String(p.trust || 'system') !== 'user') return t('preset.name.' + id)
|
|
11189
|
+
return String(p.name || id)
|
|
11190
|
+
}
|
|
11191
|
+
/**
|
|
11192
|
+
* 该模式下可勾选的工具名 + 每个名字的归属组。
|
|
11193
|
+
* `null` = 宿主没给出该模式的清单(读不到,不能假装是空)。
|
|
11194
|
+
*
|
|
11195
|
+
* `groups` 缺项(老服务端没这个字段)时按 `other` 处理 —— 落错组只是位置不对,
|
|
11196
|
+
* 丢掉就是"界面上勾不到",那是静默降级。
|
|
11197
|
+
*/
|
|
11198
|
+
function modeTools(id) {
|
|
11199
|
+
var hit = (cand.presets || []).filter(function (p) { return p.id === id })[0]
|
|
11200
|
+
if (!hit || !Array.isArray(hit.tools)) return null
|
|
11201
|
+
return { names: hit.tools, groups: hit.toolGroups || {} }
|
|
11202
|
+
}
|
|
11203
|
+
/** 三组的固定顺序:本插件 → 官方 → 其他。顺序即语义,别按字典序排。 */
|
|
11204
|
+
var TOOL_ORIGINS = ['plugin', 'official', 'other']
|
|
11205
|
+
function modeRule(id) { return (modal.form.toolsByPreset || {})[id] }
|
|
11206
|
+
function setModeRule(id, rule) {
|
|
11207
|
+
var next = Object.assign({}, modal.form.toolsByPreset || {})
|
|
11208
|
+
if (rule) next[id] = rule; else delete next[id]
|
|
11209
|
+
setModal(Object.assign({}, modal, {
|
|
11210
|
+
form: Object.assign({}, modal.form, { toolsByPreset: next }),
|
|
11211
|
+
openMode: modal.openMode === id && !rule ? null : modal.openMode,
|
|
11212
|
+
}))
|
|
11213
|
+
}
|
|
11214
|
+
/** 模式行上的一句话状态:未开启 / 白名单 N 个 / 黑名单 N 个 / 已开启但没勾(= 不限制)。 */
|
|
11215
|
+
function modeSummary(id) {
|
|
11216
|
+
var rule = modeRule(id)
|
|
11217
|
+
if (!rule) return { text: t('subagents.mode.off'), cls: 'dsm-mode-off' }
|
|
11218
|
+
var n = (rule.names || []).length
|
|
11219
|
+
if (!n) return { text: t('subagents.mode.empty'), cls: 'dsm-mode-off' }
|
|
11220
|
+
return {
|
|
11221
|
+
text: t(rule.mode === 'deny' ? 'subagents.mode.denyOn' : 'subagents.mode.allowOn', { count: n }),
|
|
11222
|
+
cls: rule.mode === 'deny' ? 'dsm-mode-deny' : 'dsm-mode-allow',
|
|
11223
|
+
}
|
|
11224
|
+
}
|
|
11225
|
+
/**
|
|
11226
|
+
* 启动某一侧名单:生效 + 就地展开。同一模式内白/黑互斥 —— 启动一侧就把模式切过去,
|
|
11227
|
+
* **已勾选的名字保留**(只换语义),这样"改成黑名单"不用从头再勾一遍。
|
|
11228
|
+
*/
|
|
11229
|
+
function startMode(id, mode) {
|
|
11230
|
+
// 刚被「关闭」掉的名字记在 stoppedRules 里:重新启用时带回,免得"关一下再开"把
|
|
11231
|
+
// 已勾好的名单清空(用户实测踩到:以为「关闭白名单」是收起设置,再启用就空了)。
|
|
11232
|
+
var prev = modeRule(id) || (modal.stoppedRules || {})[id]
|
|
11233
|
+
var names = prev && Array.isArray(prev.names) ? prev.names.slice() : []
|
|
11234
|
+
setModal(Object.assign({}, modal, {
|
|
11235
|
+
openMode: id, modeQuery: '',
|
|
11236
|
+
form: Object.assign({}, modal.form, {
|
|
11237
|
+
toolsByPreset: Object.assign({}, modal.form.toolsByPreset || {}, { [id]: { mode: mode, names: names } }),
|
|
11238
|
+
}),
|
|
11239
|
+
}))
|
|
11240
|
+
}
|
|
11241
|
+
function stopMode(id) {
|
|
11242
|
+
var prev = modeRule(id)
|
|
11243
|
+
var stopped = Object.assign({}, modal.stoppedRules || {})
|
|
11244
|
+
if (prev) stopped[id] = { mode: prev.mode, names: (prev.names || []).slice() }
|
|
11245
|
+
var next = Object.assign({}, modal.form.toolsByPreset || {})
|
|
11246
|
+
delete next[id]
|
|
11247
|
+
setModal(Object.assign({}, modal, {
|
|
11248
|
+
stoppedRules: stopped,
|
|
11249
|
+
openMode: modal.openMode === id ? null : modal.openMode,
|
|
11250
|
+
form: Object.assign({}, modal.form, { toolsByPreset: next }),
|
|
11251
|
+
}))
|
|
11252
|
+
}
|
|
11253
|
+
function toggleModeTool(id, name) {
|
|
11254
|
+
var rule = modeRule(id)
|
|
11255
|
+
if (!rule) return
|
|
11256
|
+
setModeRule(id, { mode: rule.mode, names: toggled(rule.names || [], name) })
|
|
11257
|
+
}
|
|
11258
|
+
/**
|
|
11259
|
+
* 展开后的勾选区:只列**该模式自己的**工具,并按归属分三段
|
|
11260
|
+
* (插件工具 / 官方工具 / 其他工具)。这样勾出来的名字天然都属于这个预设,
|
|
11261
|
+
* 不会出现"在标准模式勾了只属于 PTC 的工具 → 换模式跑就启动失败"。
|
|
11262
|
+
* MCP 工具不在这里(用户裁定):子代理照旧能用当前在跑的 MCP,那份名单在运行时并入。
|
|
11263
|
+
*
|
|
11264
|
+
* 分组只是地标,**不改变勾选语义**:三段共用一个 `picked`,「全选 / 取消全选」仍跨组
|
|
11265
|
+
* 对整份清单生效;空组不渲染,筛选后为空的组也不渲染。地标复用 `segGroup`(左内缩与
|
|
11266
|
+
* `.dsm-pick` 同为 8px,天然对齐,所以不新增样式)。
|
|
11267
|
+
*
|
|
11268
|
+
* ⚠️ 分组买的是「一眼看出这是谁的东西」,不是「更快找到某个工具」—— 后者是上面那个
|
|
11269
|
+
* 筛选框的活,分组不减少滚动量。
|
|
11270
|
+
*/
|
|
11271
|
+
function modeEditor(p) {
|
|
11272
|
+
var id = p.id
|
|
11273
|
+
var rule = modeRule(id)
|
|
11274
|
+
if (!rule) return null
|
|
11275
|
+
var view = modeTools(id)
|
|
11276
|
+
var all = view ? view.names : null
|
|
11277
|
+
if (all === null || all.length === 0) {
|
|
11278
|
+
return React.createElement('div', { className: 'dsm-mode-body' },
|
|
11279
|
+
React.createElement('div', { className: 'dsm-pick-empty' }, p.broken ? t('subagents.mode.broken') : t('subagents.mode.noTools')))
|
|
11280
|
+
}
|
|
11281
|
+
var q = String(modal.modeQuery || '').trim().toLowerCase()
|
|
11282
|
+
var list = q ? all.filter(function (n) { return n.toLowerCase().indexOf(q) >= 0 }) : all
|
|
11283
|
+
var picked = rule.names || []
|
|
11284
|
+
// 与其余「全选」同一口径:全勾了就只给「取消全选」(二合一,不并列)。
|
|
11285
|
+
var allPickedAll = all.length > 0 && all.every(function (n) { return picked.indexOf(n) >= 0 })
|
|
11286
|
+
// 按归属切段。认不出的归属(服务端没给这一项,或给了个没见过的值)一律落「其他」——
|
|
11287
|
+
// 落错组只是位置不对,丢掉就是"界面上勾不到"。
|
|
11288
|
+
var byOrigin = { plugin: [], official: [], other: [] }
|
|
11289
|
+
list.forEach(function (name) {
|
|
11290
|
+
var origin = view.groups[name]
|
|
11291
|
+
byOrigin[origin === 'plugin' || origin === 'official' ? origin : 'other'].push(name)
|
|
11292
|
+
})
|
|
11293
|
+
var grid = []
|
|
11294
|
+
TOOL_ORIGINS.forEach(function (origin) {
|
|
11295
|
+
var names = byOrigin[origin]
|
|
11296
|
+
if (!names.length) return
|
|
11297
|
+
grid.push(segGroup(t('subagents.tools.group.' + origin)))
|
|
11298
|
+
names.forEach(function (name) {
|
|
11299
|
+
grid.push(pickRow({ key: 'm:' + id + ':' + name, disabled: busy, checked: picked.indexOf(name) >= 0, name: name, onChange: function () { toggleModeTool(id, name) } }))
|
|
11300
|
+
})
|
|
11301
|
+
})
|
|
11302
|
+
return React.createElement('div', { className: 'dsm-mode-body' },
|
|
11303
|
+
picked.length
|
|
11304
|
+
? React.createElement('div', { className: 'dsm-tools-chips' }, picked.map(function (name) {
|
|
11305
|
+
return React.createElement('span', { key: 'c:' + name, className: 'dsm-chip' + (rule.mode === 'deny' ? ' dsm-chip-deny' : '') },
|
|
11306
|
+
name,
|
|
11307
|
+
React.createElement('button', { type: 'button', title: t('subagents.tools.remove'), onClick: function () { toggleModeTool(id, name) } }, '×'))
|
|
11308
|
+
}))
|
|
11309
|
+
: React.createElement('span', { className: 'dsm-adv-note' }, t(rule.mode === 'deny' ? 'subagents.mode.denyEmpty' : 'subagents.mode.allowEmpty')),
|
|
11310
|
+
React.createElement('div', { className: 'dsm-combo-row' },
|
|
11311
|
+
React.createElement('input', {
|
|
11312
|
+
className: 'dsm-control',
|
|
11313
|
+
value: modal.modeQuery || '',
|
|
11314
|
+
placeholder: t('subagents.tools.filter'),
|
|
11315
|
+
onChange: function (e) { setModal(Object.assign({}, modal, { modeQuery: e.target.value })) },
|
|
11316
|
+
}),
|
|
11317
|
+
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')))),
|
|
11318
|
+
list.length
|
|
11319
|
+
? React.createElement('div', { className: 'dsm-tools-grid' }, grid)
|
|
11320
|
+
: React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.mem.noMatch')))
|
|
11321
|
+
}
|
|
11322
|
+
/**
|
|
11323
|
+
* 一行一个 Agent 预设(roster 顺序:标准 / PTC / 极简 / 创造 / 自建预设)。
|
|
11324
|
+
* 默认全部折叠、全部未启动;右侧两个按钮「启动 白名单」「启动 黑名单」互斥。
|
|
11325
|
+
*
|
|
11326
|
+
* 不再自带标题与说明(0.19.0 重排):标题由所在分组头承担(`subagents.adv.group.tools`),
|
|
11327
|
+
* 白/黑名单语义与「MCP 工具不在候选里」那一行移到分组头正下方。这两句原先挂在字段尾巴上,
|
|
11328
|
+
* 叠上另外几处提示后,展开态读起来是一堵墙(用户 2026-10-07 指出)。
|
|
11329
|
+
*/
|
|
11330
|
+
function modeRows() {
|
|
11331
|
+
var presets = (cand.presets || []).slice()
|
|
11332
|
+
if (!presets.length) {
|
|
11333
|
+
return React.createElement('div', { className: 'dsm-pick-empty' }, cand.loading ? t('memory.loading') : t('subagents.mode.noPresets'))
|
|
11334
|
+
}
|
|
11335
|
+
return React.createElement('div', null,
|
|
11336
|
+
React.createElement('div', { className: 'dsm-modes' }, presets.map(function (p) {
|
|
11337
|
+
var id = p.id
|
|
11338
|
+
var rule = modeRule(id)
|
|
11339
|
+
var summary = modeSummary(id)
|
|
11340
|
+
var startButton = function (mode, isDeny) {
|
|
11341
|
+
var active = !!rule && rule.mode === mode
|
|
11342
|
+
return React.createElement('button', {
|
|
11343
|
+
key: mode,
|
|
11344
|
+
type: 'button',
|
|
11345
|
+
className: 'dsm-btn dsm-btn-quiet' + (active ? (mode === 'deny' ? ' dsm-mode-btn-deny' : ' dsm-mode-btn-allow') : ''),
|
|
11346
|
+
disabled: busy,
|
|
11347
|
+
onClick: function () { if (active) stopMode(id); else startMode(id, mode) },
|
|
11348
|
+
}, t(active
|
|
11349
|
+
? (isDeny ? 'subagents.mode.stopDeny' : 'subagents.mode.stopAllow')
|
|
11350
|
+
: (isDeny ? 'subagents.mode.startDeny' : 'subagents.mode.startAllow')))
|
|
11351
|
+
}
|
|
11352
|
+
var open = modal.openMode === id
|
|
11353
|
+
return React.createElement('div', { className: 'dsm-mode-row' + (rule ? ' dsm-mode-on' : ''), key: id },
|
|
11354
|
+
React.createElement('div', { className: 'dsm-mode-head' },
|
|
11355
|
+
// 行首按钮 = 展开 / 收起名单(用户实测:已配好的模式只有「关闭白名单」可点,
|
|
11356
|
+
// 点下去是把模式停掉、名单设置再也进不去 —— 查看/编辑与启用/停用必须分开)。
|
|
11357
|
+
React.createElement('button', {
|
|
11358
|
+
type: 'button',
|
|
11359
|
+
className: 'dsm-mode-head-main',
|
|
11360
|
+
disabled: !rule,
|
|
11361
|
+
title: rule ? t('subagents.mode.toggleHint') : '',
|
|
11362
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
11363
|
+
onClick: function () {
|
|
11364
|
+
setModal(Object.assign({}, modal, { openMode: open ? null : id, modeQuery: '' }))
|
|
11365
|
+
},
|
|
11366
|
+
},
|
|
11367
|
+
React.createElement('span', { className: 'dsm-mode-caret', 'aria-hidden': 'true' }, rule ? (open ? '▾' : '▸') : ''),
|
|
11368
|
+
React.createElement('span', { className: 'dsm-mode-name' }, presetLabel(p)),
|
|
11369
|
+
p.broken ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('compat.reach.broken')) : null,
|
|
11370
|
+
React.createElement('span', { className: 'dsm-mode-sum ' + summary.cls }, summary.text)),
|
|
11371
|
+
React.createElement('span', { className: 'dsm-mode-actions' },
|
|
11372
|
+
startButton('allow', false),
|
|
11373
|
+
startButton('deny', true))),
|
|
11374
|
+
open ? modeEditor(p) : null)
|
|
11375
|
+
})))
|
|
11376
|
+
}
|
|
11377
|
+
/**
|
|
11378
|
+
* 旧格式(全局 `tools:` / `toolsDeny:`)的只读小结 + 转换入口。
|
|
11379
|
+
* 旧键对所有模式生效,运行时仍然照旧执行;**不点转换就不动文件里的旧键**。
|
|
11380
|
+
*/
|
|
11381
|
+
function legacyNotice() {
|
|
11382
|
+
var allow = modal.form.tools || []
|
|
11383
|
+
var deny = modal.form.toolsDeny || []
|
|
11384
|
+
if (!allow.length && !deny.length) return null
|
|
11385
|
+
var presets = cand.presets || []
|
|
11386
|
+
var target = modal.legacyTarget || (presets[0] ? presets[0].id : '')
|
|
11387
|
+
return React.createElement('div', { className: 'dsm-legacy' },
|
|
11388
|
+
React.createElement('span', { className: 'dsm-adv-note' }, t('subagents.legacy.note', { allow: allow.length, deny: deny.length })),
|
|
11389
|
+
React.createElement('div', { className: 'dsm-combo-row' },
|
|
11390
|
+
React.createElement('div', { className: 'dsm-select' },
|
|
11391
|
+
React.createElement('select', {
|
|
11392
|
+
className: 'dsm-control',
|
|
11393
|
+
value: target,
|
|
11394
|
+
disabled: busy || !presets.length,
|
|
11395
|
+
onChange: function (e) { setModal(Object.assign({}, modal, { legacyTarget: e.target.value })) },
|
|
11396
|
+
}, presets.map(function (p) { return React.createElement('option', { key: p.id, value: p.id }, presetLabel(p)) }))),
|
|
11397
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !target || !presets.length, onClick: function () {
|
|
11398
|
+
var rule = { mode: allow.length ? 'allow' : 'deny', names: (allow.length ? allow : deny).slice() }
|
|
11399
|
+
setModal(Object.assign({}, modal, {
|
|
11400
|
+
openMode: target,
|
|
11401
|
+
modeQuery: '',
|
|
11402
|
+
legacyTarget: target,
|
|
11403
|
+
form: Object.assign({}, modal.form, {
|
|
11404
|
+
tools: [], toolsDeny: [],
|
|
11405
|
+
toolsByPreset: Object.assign({}, modal.form.toolsByPreset || {}, { [target]: rule }),
|
|
11406
|
+
}),
|
|
11407
|
+
}))
|
|
11408
|
+
} }, t('subagents.legacy.convert'))),
|
|
11409
|
+
React.createElement('p', { className: 'dsm-help' }, t('subagents.legacy.hint')))
|
|
11410
|
+
}
|
|
11411
|
+
function toggleAdvanced() {
|
|
11412
|
+
if (!modal || modal.type !== 'editor') return
|
|
11413
|
+
var next = !modal.advanced
|
|
11414
|
+
setModal(Object.assign({}, modal, { advanced: next }))
|
|
11415
|
+
if (next) {
|
|
11416
|
+
loadCandidates()
|
|
11417
|
+
// 档位清单与候选目录**分开拉**:候选是本地目录(一次性),档位要问 adapter
|
|
11418
|
+
// (按模型、可能联网),所以这里用表单里当前的模型去拉;没选模型就什么都不发。
|
|
11419
|
+
loadEfforts(modal.form.provider, modal.form.model)
|
|
11420
|
+
}
|
|
11421
|
+
}
|
|
11422
|
+
function submitEditor() {
|
|
11423
|
+
if (!modal || modal.type !== 'editor') return
|
|
11424
|
+
setBusy(true)
|
|
11425
|
+
var op = modal.mode === 'create' ? 'subagent-create' : 'subagent-update'
|
|
11426
|
+
// 编辑时名字可改:nextName 只在真的改过时才传(服务端按原名定位文件)。
|
|
11427
|
+
var renamed = modal.mode === 'edit' && modal.originalName && modal.originalName !== modal.form.name
|
|
11428
|
+
apiCall(op, {
|
|
11429
|
+
name: renamed ? String(modal.originalName) : modal.form.name,
|
|
11430
|
+
...(renamed ? { nextName: String(modal.form.name || '').trim() } : {}),
|
|
11431
|
+
description: modal.form.description,
|
|
11432
|
+
provider: modal.form.provider, model: modal.form.model,
|
|
11433
|
+
catalogDepth: modal.form.catalogDepth,
|
|
11434
|
+
continuable: modal.form.continuable === true,
|
|
11435
|
+
// 旧格式的全局名单原样回写(旧键不点转换就不动),新模式名单另存一块。
|
|
11436
|
+
tools: modal.form.tools || [], toolsDeny: modal.form.toolsDeny || [],
|
|
11437
|
+
toolsByPreset: modal.form.toolsByPreset || {},
|
|
11438
|
+
body: modal.form.body,
|
|
11439
|
+
output: modal.form.output || '',
|
|
11440
|
+
}).then(function (res) {
|
|
11441
|
+
setBusy(false)
|
|
11442
|
+
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) }
|
|
11443
|
+
else setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, { error: translateError(t, res) }) }))
|
|
11444
|
+
}).catch(function (e) { setBusy(false); setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, { error: errMsg(e) }) })) })
|
|
11445
|
+
}
|
|
11446
|
+
function submitDelete(name) {
|
|
11447
|
+
setBusy(true)
|
|
11448
|
+
apiCall('subagent-delete', { name: name }).then(function (res) {
|
|
11449
|
+
setBusy(false); setModal(null)
|
|
11450
|
+
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) }
|
|
11451
|
+
else setResult({ ok: false, text: translateError(t, res) })
|
|
11452
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
|
|
11453
|
+
}
|
|
11454
|
+
// ── 导入人设(.md / .zip,多选或拖入;同名跳过并报告)──
|
|
11455
|
+
function submitImport(files) {
|
|
11456
|
+
if (!files || !files.length) return
|
|
11457
|
+
setBusy(true)
|
|
11458
|
+
apiCall('subagent-import', { files: files }).then(function (res) {
|
|
11459
|
+
setBusy(false)
|
|
11460
|
+
if (res && res.ok) {
|
|
11461
|
+
setModal(null)
|
|
11462
|
+
var names = res.imported || [], skipped = res.skipped || []
|
|
11463
|
+
var text = names.length ? t('subagents.result.imported', { count: names.length, names: names.join('、') }) : t('import.none')
|
|
11464
|
+
if (skipped.length) text += ' · ' + t('import.skipped', { items: skipped.map(function (s) { return s.name + '(' + s.reason + ')' }).join(';') })
|
|
11465
|
+
var sw = stateSyncWarn(t, res)
|
|
11466
|
+
if (sw) text += ' ' + sw
|
|
11467
|
+
setResult({ ok: !sw, warning: !!sw, text: text }); refresh(true)
|
|
11468
|
+
} else setResult({ ok: false, text: translateError(t, res) })
|
|
11469
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
|
|
11470
|
+
}
|
|
11471
|
+
// 过滤只跟数据与(防抖后的)搜索词有关:每次渲染重扫一整份列表没有必要。
|
|
11472
|
+
// FLIP 的 key 仍是 `p.name`(过滤只改变"列表里有哪些",不改变身份)。
|
|
11473
|
+
var visibleSubagents = React.useMemo(function () {
|
|
11474
|
+
return data.subagents.filter(function (p) { return matchPersonaQuery(p, dq) })
|
|
11475
|
+
}, [data.subagents, dq])
|
|
11476
|
+
// 「启用的排前面」(用户要求):与过滤分开记忆 —— 只跟过滤结果有关,弹窗 / 结果提示条
|
|
11477
|
+
// 之类的状态变化不再重跑分区。FLIP 的 data-flip-on 仍来自 p.enabled,未动。
|
|
11478
|
+
var orderedSubagents = React.useMemo(function () {
|
|
11479
|
+
return enabledFirst(visibleSubagents, function (p) { return p.enabled !== false; })
|
|
11480
|
+
}, [visibleSubagents])
|
|
11481
|
+
var limitedCount = React.useMemo(function () {
|
|
11482
|
+
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
|
|
11483
|
+
}, [data.subagents])
|
|
11484
|
+
return React.createElement('section', { className: 'dsm-section' },
|
|
11485
|
+
React.createElement('div', { className: 'dsm-head' },
|
|
11486
|
+
React.createElement('div', { className: 'dsm-title-block' },
|
|
11487
|
+
React.createElement('div', { className: 'dsm-title-row' },
|
|
11488
|
+
React.createElement('h2', { className: 'dsm-title' }, t('subagents.title'))),
|
|
11489
|
+
React.createElement('p', { className: 'dsm-desc' }, t('subagents.desc'))),
|
|
11490
|
+
React.createElement('div', { className: 'dsm-actions' },
|
|
11491
|
+
refreshButton(t, busy || data.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }),
|
|
11492
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { openEditor(null) } }, t('subagents.new')),
|
|
11493
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { setResult(null); setModal({ type: 'import' }) } }, t('subagents.import')),
|
|
11494
|
+
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')),
|
|
11495
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
|
|
11496
|
+
anyLocked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('lock.banner')) : null,
|
|
11497
|
+
// 锁定期间不显示场景那条长句:开关全是灰的,"改动会同步写进档案"没有落点,
|
|
11498
|
+
// 两条并排还会互相矛盾(用户 2026-09-19)。
|
|
11499
|
+
(anyLocked !== true && sceneName) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('scene.switch.banner', { scene: sceneName })) : null,
|
|
11500
|
+
React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '2' } },
|
|
11501
|
+
[['total', data.subagents.length, t('subagents.stat.total')],
|
|
11502
|
+
['limited', limitedCount, t('subagents.stat.limited')]].map(function (item) {
|
|
11503
|
+
return React.createElement('div', { key: item[0], className: 'dsm-stat' },
|
|
11504
|
+
React.createElement('strong', null, item[1]), item[2])
|
|
11505
|
+
})),
|
|
11506
|
+
// 搜索框:统计条之后(与 MCP / 技能 / 记忆 / 提示词页同一位置与样式)。
|
|
11507
|
+
React.createElement('div', { className: 'dsm-filters' },
|
|
11508
|
+
React.createElement('input', {
|
|
11509
|
+
className: 'dsm-control dsm-search', value: query, 'aria-label': t('search'),
|
|
11510
|
+
placeholder: t('subagents.search.placeholder'),
|
|
11511
|
+
onChange: function (e) { setQuery(e.target.value) },
|
|
11512
|
+
})),
|
|
11513
|
+
React.createElement(Notice, { kind: result && result.warning ? 'warn' : result && result.ok ? 'ok' : 'err', text: result && result.text }),
|
|
11514
|
+
// 走 Notice 而不是裸 div:令牌没过的提醒会在右侧自动多出一颗「填写令牌」按钮。
|
|
11515
|
+
data.error ? React.createElement(Notice, { kind: 'err', text: String(data.error) }) : null,
|
|
11516
|
+
// 注册失败的工具(`subagent_manager_list` / `_run`):模型侧只会「查无此工具」,
|
|
11517
|
+
// 这条横幅是用户唯一能看到的地方。名字与原因来自服务端,句子在这里按当前语言拼。
|
|
11518
|
+
(data.toolFailures || []).length ? React.createElement(Notice, {
|
|
11519
|
+
kind: 'warn',
|
|
11520
|
+
text: t('subagents.toolFailed', {
|
|
11521
|
+
names: (data.toolFailures || []).map(function (f) { return f.name }).join('、'),
|
|
11522
|
+
reason: String(((data.toolFailures || [])[0] || {}).reason || ''),
|
|
11523
|
+
}),
|
|
11524
|
+
}) : null,
|
|
11525
|
+
data.loading && !data.subagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
11526
|
+
: !data.subagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('subagents.empty'))
|
|
11527
|
+
: !visibleSubagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('subagents.empty.search'))
|
|
11528
|
+
: React.createElement('div', { className: 'dsm-sources', ref: flipRef }, orderedSubagents.map(function (p) {
|
|
11529
|
+
return React.createElement('div', { key: p.name, className: 'dsm-source', 'data-flip-key': p.name, 'data-flip-on': p.enabled !== false ? '1' : '0' },
|
|
11530
|
+
React.createElement('div', { className: 'dsm-source-head' },
|
|
11531
|
+
React.createElement('div', { className: 'dsm-source-head-main dsm-persona-main' },
|
|
11532
|
+
React.createElement('div', { className: 'dsm-persona-name-row' },
|
|
11533
|
+
React.createElement('span', { className: 'dsm-source-title', title: p.name }, p.name),
|
|
11534
|
+
// 两个状态都给胶囊:以前只在停用时挂标签,启用的一行什么都不留,
|
|
11535
|
+
// 用户没法一眼看出「没标签 = 启用」。
|
|
11536
|
+
p.enabled === false
|
|
11537
|
+
? React.createElement(StatusTag, { tone: 'muted', text: t('subagents.disabled'), title: t('subagents.disabled.hint') })
|
|
11538
|
+
: React.createElement(StatusTag, { tone: 'ok', text: t('subagents.enabled') })),
|
|
11539
|
+
// 没有描述就不渲染这一行:空 div 仍占一行行高,行会变成一条无内容的空隙。
|
|
11540
|
+
p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
|
|
11541
|
+
React.createElement('div', { className: 'dsm-source-actions' },
|
|
11542
|
+
React.createElement(Switch, { on: p.enabled !== false, disabled: busy || anyLocked, label: t('subagents.toggle') + ' ' + p.name, onClick: function () { togglePersona(p) } }),
|
|
11543
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || anyLocked, onClick: function () { openEditor(p.name) } }, t('memory.edit')),
|
|
11544
|
+
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')))))
|
|
11545
|
+
})),
|
|
11546
|
+
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) } },
|
|
11547
|
+
React.createElement('div', { className: 'dsm-form' },
|
|
11548
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
11549
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.name')),
|
|
11550
|
+
React.createElement('input', { className: 'dsm-control', value: modal.form.name || '', placeholder: 'code-review', onChange: function (e) { setForm({ name: e.target.value }) } })),
|
|
11551
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
11552
|
+
// 描述会整段进「可委派的子智能体」目录,上限就是注入侧的截断长度(`CATALOG_DESC_MAX`);
|
|
11553
|
+
// 存量人设的超长描述标红(服务端不校验这个长度,只有注入时截)。
|
|
11554
|
+
React.createElement('div', { className: 'dsm-budget-meta' },
|
|
11555
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.description')),
|
|
11556
|
+
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)),
|
|
11557
|
+
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 }) } })),
|
|
11558
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
11559
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.body')),
|
|
11560
|
+
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 }) } })),
|
|
11561
|
+
// 输出要求(frontmatter `output:`,一条一行):单独成节写进子代理的系统提示词。
|
|
11562
|
+
// 与正文分开是有意的 —— 正文是"这个角色是什么"(散文),这里放"产出必须长什么样"
|
|
11563
|
+
// (可检验的硬要求)。混在一起时散文会把硬要求稀释成风格提示(用户实测)。
|
|
11564
|
+
React.createElement('label', { className: 'dsm-field' },
|
|
11565
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.output')),
|
|
11566
|
+
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 }) } }),
|
|
11567
|
+
React.createElement('p', { className: 'dsm-help' }, t('subagents.field.output.hint'))),
|
|
11568
|
+
// ── 高级选项(默认收起)──
|
|
11569
|
+
// 模型 / 工具限制是「少数人才改」的字段,但一旦改错代价高(跨来源模型、工具名打错
|
|
11570
|
+
// 会让子代理直接启动失败)。所以:收起来但**有值就自动展开**,并把候选做成选择器。
|
|
11571
|
+
React.createElement('div', { className: 'dsm-adv' },
|
|
11572
|
+
React.createElement('button', { type: 'button', className: 'dsm-adv-head', 'aria-expanded': modal.advanced === true, onClick: toggleAdvanced },
|
|
11573
|
+
React.createElement('span', { className: 'dsm-adv-caret' }, modal.advanced ? '▼' : '▶'),
|
|
11574
|
+
React.createElement('span', { className: 'dsm-adv-title' }, t('subagents.adv.title')),
|
|
11575
|
+
modal.advanced ? null : React.createElement('span', { className: 'dsm-adv-note dsm-adv-sum' },
|
|
11576
|
+
// 摘要**最多两行,且只报有值的那几项**(用户 2026-10-07 定稿):
|
|
11577
|
+
// 第一行是"下一次委派会怎么跑"(模型 / 思考强度 / 允许追问),
|
|
11578
|
+
// 第二行是工具限制的存量 —— 一项都没有时整行不渲染,不拿「0 项」占位置。
|
|
11579
|
+
// 目录注入不进摘要:它只决定目录出现在哪些会话,不影响委派本身。
|
|
11580
|
+
React.createElement('span', null, t('subagents.adv.summary', {
|
|
11581
|
+
model: modal.form.model ? (modal.form.provider ? modal.form.provider + '/' + modal.form.model : modal.form.model) : t('subagents.adv.inherit'),
|
|
11582
|
+
// 思考强度收起来也要看得见:它是"下一次委派会怎么跑"的一部分。
|
|
11583
|
+
// 没配时报「默认」而**不是**「继承」—— 官方语义里换模型会把继承来的那一档
|
|
11584
|
+
// 删掉(`dsh-subagent/lib/index.js:482`),报「继承」是句假话。
|
|
11585
|
+
effort: modal.form.reasoningEffort ? String(modal.form.reasoningEffort) : t('subagents.effort.none'),
|
|
11586
|
+
// 允许追问接在同一行尾巴上:它同样是"会怎么跑",而且是**最容易忘**的一项
|
|
11587
|
+
// (结果变成异步回执)。分隔号写在词典值里,英文侧才不用跟着改代码。
|
|
11588
|
+
tail: modal.form.continuable === true ? t('subagents.adv.continuable') : '',
|
|
11589
|
+
})),
|
|
11590
|
+
advLimitLine() ? React.createElement('span', null, advLimitLine()) : null)),
|
|
11591
|
+
modal.advanced ? React.createElement('div', { className: 'dsm-adv-body' },
|
|
11592
|
+
// 三组:模型 / 运行方式 / 工具限制(0.19.0 重排)。重排前是五个字段平铺、
|
|
11593
|
+
// 九处提示文字混用三种形态(单行 dsm-help / 项目符号 / 选择器副标题),
|
|
11594
|
+
// 展开态读起来是一堵墙(用户 2026-10-07 裁定)。现在:分组头 + 一条细线划开;
|
|
11595
|
+
// 静态说明一律不进表单,只留**状态类**提示(拉取中 / 未选模型 / 拉不到 / 报错)。
|
|
11596
|
+
React.createElement('div', { className: 'dsm-adv-group' },
|
|
11597
|
+
React.createElement('div', { className: 'dsm-adv-group-head' }, t('subagents.adv.group.model')),
|
|
11598
|
+
// 模型来源与模型并排:官方把 (provider, model) 当一对解析,跨来源(如 sensenova)
|
|
11599
|
+
// 必须两个键同时给。顺序按解析顺序 —— 先来源,后模型。
|
|
11600
|
+
// 两个字段并排用 `.dsm-field-row`;`.dsm-combo-row` 是**字段内部**的
|
|
11601
|
+
// "下拉 + 它旁边的东西"(这里只剩下拉,`.dsm-combo-row .dsm-select{flex:1}`
|
|
11602
|
+
// 让它填满整格 —— 缺这条时下拉宽度由选项文字撑出,换来源后标签变短、框跟着缩)。
|
|
11603
|
+
React.createElement('div', { className: 'dsm-field-row' },
|
|
11604
|
+
// 模型来源:去重后的来源清单,只从宿主目录里选(手填通道 2026-10-07 删除)。
|
|
11605
|
+
// 换来源要重拉档位 —— 档位清单是按 (provider, model) 给的。
|
|
11606
|
+
// 换来源**不清模型**:清掉是"悄悄改掉用户已配好的值",而留着会立刻显形 ——
|
|
11607
|
+
// 模型下拉找不到这对 (来源, 模型) 就多列一条带「目录里没有」标记的选项。
|
|
11608
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
11609
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.provider')),
|
|
11610
|
+
React.createElement('div', { className: 'dsm-combo-row' },
|
|
11611
|
+
React.createElement('div', { className: 'dsm-select' },
|
|
11612
|
+
React.createElement('select', {
|
|
11613
|
+
className: 'dsm-control',
|
|
11614
|
+
value: providerSelectValue(),
|
|
11615
|
+
disabled: busy || cand.loading,
|
|
11616
|
+
onChange: function (e) {
|
|
11617
|
+
var v = e.target.value
|
|
11618
|
+
setForm({ provider: v })
|
|
11619
|
+
loadEfforts(v, modal.form.model || '')
|
|
11620
|
+
},
|
|
11621
|
+
},
|
|
11622
|
+
React.createElement('option', { value: '' }, t('subagents.provider.inherit')),
|
|
11623
|
+
(cand.providers || []).map(function (p) {
|
|
11624
|
+
return React.createElement('option', { key: p.id, value: p.id }, providerLabel(p))
|
|
11625
|
+
}),
|
|
11626
|
+
// 目录外的旧值:列一条只读选项,值看得见、要改只能重选目录里的。
|
|
11627
|
+
offCatalogProvider()
|
|
11628
|
+
? React.createElement('option', { value: offCatalogProvider() },
|
|
11629
|
+
catalogReady() ? t('subagents.field.offCatalog', { id: offCatalogProvider() }) : offCatalogProvider())
|
|
11630
|
+
: null)))),
|
|
11631
|
+
// 模型:宿主 LLM 目录里的 (provider, model) 对,只从目录里选。
|
|
11632
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
11633
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.model')),
|
|
11634
|
+
React.createElement('div', { className: 'dsm-combo-row' },
|
|
11635
|
+
React.createElement('div', { className: 'dsm-select' },
|
|
11636
|
+
React.createElement('select', {
|
|
11637
|
+
className: 'dsm-control',
|
|
11638
|
+
value: modelSelectValue(),
|
|
11639
|
+
disabled: busy || cand.loading,
|
|
11640
|
+
onChange: function (e) {
|
|
11641
|
+
var v = e.target.value
|
|
11642
|
+
if (v === '') { setForm({ provider: '', model: '' }); loadEfforts('', '') }
|
|
11643
|
+
else {
|
|
11644
|
+
var parts = v.split('\u0000')
|
|
11645
|
+
setForm({ provider: parts[0], model: parts[1] })
|
|
11646
|
+
// 档位跟模型走:换模型必须重拉(缓存命中时是同步的,不会闪)。
|
|
11647
|
+
loadEfforts(parts[0], parts[1])
|
|
11648
|
+
}
|
|
11649
|
+
},
|
|
11650
|
+
},
|
|
11651
|
+
React.createElement('option', { value: '' }, t('subagents.model.inherit')),
|
|
11652
|
+
modelOptions().map(function (m) {
|
|
11653
|
+
return React.createElement('option', { key: m.provider + '/' + m.id, value: m.provider + '\u0000' + m.id }, modelOptionLabel(m))
|
|
11654
|
+
}),
|
|
11655
|
+
// 目录外的旧值:同来源那条,列一条只读选项。
|
|
11656
|
+
offCatalogModel()
|
|
11657
|
+
? React.createElement('option', { value: offCatalogModel() },
|
|
11658
|
+
catalogReady() ? t('subagents.field.offCatalog', { id: modal.form.model }) : modal.form.model)
|
|
11659
|
+
: null))),
|
|
11660
|
+
// 目录还在读时给一行状态;读完了不占行 —— 这里原来放的是"留空继承主会话"
|
|
11661
|
+
// 那句静态说明,已随 0.19.0 重排移出表单。
|
|
11662
|
+
cand.loading ? React.createElement('p', { className: 'dsm-help' }, t('memory.loading')) : null)),
|
|
11663
|
+
// 思考强度:值就是 adapter 给的档位 id(不校验合法性,官方在 provider I/O 前会拒)。
|
|
11664
|
+
// 单独占左半列(`.dsm-adv-half`),与上一行的列边界对齐。
|
|
11665
|
+
React.createElement('div', { className: 'dsm-field dsm-adv-half' },
|
|
11666
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.effort')),
|
|
11667
|
+
React.createElement('div', { className: 'dsm-select' },
|
|
11668
|
+
React.createElement('select', {
|
|
11669
|
+
className: 'dsm-control',
|
|
11670
|
+
value: String(modal.form.reasoningEffort || ''),
|
|
11671
|
+
disabled: busy || eff.loading || !modal.form.model,
|
|
11672
|
+
onChange: function (e) { setEffNotice(null); setForm({ reasoningEffort: e.target.value }) },
|
|
11673
|
+
},
|
|
11674
|
+
React.createElement('option', { value: '', title: defaultEffortTitle() }, t('subagents.effort.default')),
|
|
11675
|
+
currentEfforts().map(function (x) {
|
|
11676
|
+
return React.createElement('option', { key: x.id, value: x.id, title: x.description || undefined }, x.name)
|
|
11677
|
+
}))),
|
|
11678
|
+
// 状态行只在"这一格为什么用不了 / 没有档位"的四种情况下出现;有档位可选时
|
|
11679
|
+
// 这一行不存在 —— 原来这里常驻的是"当前可选择多种思考强度"那句同义反复。
|
|
11680
|
+
!modal.form.model || eff.loading || eff.error || !currentEfforts().length
|
|
11681
|
+
? React.createElement('p', { className: 'dsm-help' },
|
|
11682
|
+
!modal.form.model
|
|
11683
|
+
? t('subagents.effort.needModel')
|
|
11684
|
+
: eff.loading
|
|
11685
|
+
? t('subagents.effort.loading')
|
|
11686
|
+
: eff.error
|
|
11687
|
+
? String(eff.error)
|
|
11688
|
+
: t('subagents.effort.unavailable'))
|
|
11689
|
+
: null,
|
|
11690
|
+
effNotice ? React.createElement('p', { className: 'dsm-feedback dsm-warning' }, effNotice) : null)),
|
|
11691
|
+
// ── 运行方式:这份人设"什么时候跑、跑在哪些会话里" ──
|
|
11692
|
+
React.createElement('div', { className: 'dsm-adv-group' },
|
|
11693
|
+
React.createElement('div', { className: 'dsm-adv-group-head' }, t('subagents.adv.group.runtime')),
|
|
11694
|
+
// 目录注入深度:人设目录注入到哪些会话(默认 1 = 只在顶层)。用下拉而不是数字
|
|
11695
|
+
// 输入:只有 1/2/3 三个有意义的档,手写数字写错要到注入时才暴露。
|
|
11696
|
+
//
|
|
11697
|
+
// ⚠️ 它**不是**递归上限。2026-09-17 用户实测后改名(原名 maxDepth /「委派预算」):
|
|
11698
|
+
// 官方 `dsh-tool-subagent` 默认 `maxDepth: 3`,子代理本来就能继续嵌套,本插件
|
|
11699
|
+
// 也不再向官方传 maxDepth。这个字段只决定常驻目录出现在哪些深度的会话里。
|
|
11700
|
+
React.createElement('label', { className: 'dsm-field dsm-adv-half' },
|
|
11701
|
+
React.createElement('span', { className: 'dsm-label' }, t('subagents.field.catalogDepth')),
|
|
11702
|
+
React.createElement('div', { className: 'dsm-select' },
|
|
11703
|
+
React.createElement('select', {
|
|
11704
|
+
className: 'dsm-control',
|
|
11705
|
+
value: String(typeof modal.form.catalogDepth === 'number' ? modal.form.catalogDepth : 1),
|
|
11706
|
+
disabled: busy,
|
|
11707
|
+
onChange: function (e) { setForm({ catalogDepth: Number(e.target.value) }) },
|
|
11708
|
+
}, [1, 2, 3, CATALOG_DEPTH_UNLIMITED].map(function (n) {
|
|
11709
|
+
// 选项文字不带序号:前面再加一个「2 ·」是同一件事说两遍(用户 2026-09-17
|
|
11710
|
+
// 指出)。下拉的 value 仍是数字,存的还是 catalogDepth 本身。
|
|
11711
|
+
return React.createElement('option', { key: n, value: String(n) }, catalogDepthLabel(n))
|
|
11712
|
+
})))),
|
|
11713
|
+
// 允许追问(frontmatter `continuable: true`):这条通道的**产出是异步的** ——
|
|
11714
|
+
// 委派只回执一个 agent id,子代理在后台跑、完成后自己把结果发回来,
|
|
11715
|
+
// 之后可以用宿主自带的 `send_message` 追问、`interrupt_agent` 中止。
|
|
11716
|
+
// 默认关:它占用宿主"可继续子代理"的名额(`maxActiveSubagents`,默认 8),
|
|
11717
|
+
// 且要求宿主加载了持久化与 session query 两个服务。
|
|
11718
|
+
// 机制与代价的完整说明记在 CHANGELOG / README,不进表单(0.19.0 重排)。
|
|
11719
|
+
React.createElement('label', { className: 'dsm-pick' },
|
|
11720
|
+
React.createElement('input', { type: 'checkbox', checked: modal.form.continuable === true, disabled: busy, onChange: function (e) { setForm({ continuable: e.target.checked === true }) } }),
|
|
11721
|
+
React.createElement('span', { className: 'dsm-pick-main' },
|
|
11722
|
+
React.createElement('span', { className: 'dsm-pick-name' }, t('subagents.field.continuable')),
|
|
11723
|
+
React.createElement('span', { className: 'dsm-pick-desc' }, t('subagents.field.continuable.short'))))),
|
|
11724
|
+
// ── 工具限制:按 Agent 预设一行一个模式(默认全折叠、全部未启动,白/黑互斥)。
|
|
11725
|
+
// 旧格式的全局名单(老文件 / 别处导入)在这里只读呈现,点「转换」才搬进某个模式。
|
|
11726
|
+
React.createElement('div', { className: 'dsm-adv-group' },
|
|
11727
|
+
React.createElement('div', { className: 'dsm-adv-group-head' }, t('subagents.adv.group.tools')),
|
|
11728
|
+
// 这是整块里唯一留下的静态说明,两条理由:它回答"这一屏会不会生效",
|
|
11729
|
+
// 而且藏着一条反直觉的事实(MCP 工具不在候选里)—— 删掉会让人以为能限制 MCP。
|
|
11730
|
+
// 白/黑名单语义那三句已移出表单(词面自解释,按钮与模式摘要里都在用)。
|
|
11731
|
+
React.createElement('p', { className: 'dsm-help' }, t('subagents.field.modes.hint')),
|
|
11732
|
+
legacyNotice(),
|
|
11733
|
+
modeRows()),
|
|
11734
|
+
cand.error ? React.createElement('div', { className: 'dsm-feedback dsm-warning' }, String(cand.error)) : null)
|
|
11735
|
+
: null),
|
|
11736
|
+
modal.form.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(modal.form.error)) : null),
|
|
11737
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
11738
|
+
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,
|
|
11739
|
+
modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'sdel2', title: t('subagents.delete.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
|
|
11740
|
+
React.createElement('p', { className: 'dsm-help' }, t('subagents.delete.desc', { name: modal.name })),
|
|
11741
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
11742
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDelete(modal.name) } }, t('memory.btn.delete.confirm')))) : null,
|
|
11743
|
+
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,
|
|
11744
|
+
modal && modal.type === 'export' ? React.createElement(ExportModal, {
|
|
11745
|
+
key: 'export', t: t, title: t('export.subagents'),
|
|
11746
|
+
items: data.subagents.map(function (p) { return { key: p.name, name: p.name, desc: p.description || '' } }),
|
|
11747
|
+
busy: busy, result: result,
|
|
11748
|
+
onClose: function () { setResult(null); setModal(null) },
|
|
11749
|
+
onSubmit: function (names, outDir) {
|
|
11750
|
+
setBusy(true); setResult(null);
|
|
11751
|
+
apiCall('bundle-export', { kind: 'subagents', names: names, outDir: outDir }).then(function (res) {
|
|
11752
|
+
setBusy(false);
|
|
11753
|
+
if (res && res.ok) setResult({ ok: true, text: exportResultText(t, res) });
|
|
11754
|
+
else setResult({ ok: false, text: translateError(t, res) });
|
|
11755
|
+
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) });
|
|
11756
|
+
},
|
|
11757
|
+
}) : null,
|
|
11758
|
+
trash ? React.createElement(TrashModal, {
|
|
11759
|
+
t: t,
|
|
11760
|
+
title: t('trash.title'),
|
|
11761
|
+
groupTitle: t('trash.group.agents'),
|
|
11762
|
+
groupSub: t('trash.section.agents.sub'),
|
|
11763
|
+
locked: anyLocked,
|
|
11764
|
+
entries: trash.entries,
|
|
11765
|
+
loading: trash.loading,
|
|
11766
|
+
error: trash.error,
|
|
11767
|
+
busy: trashBusy,
|
|
11768
|
+
onClose: function () { setTrash(null) },
|
|
11769
|
+
onRestore: restorePersona,
|
|
11770
|
+
onPurge: purgePersona,
|
|
11771
|
+
onPurgeMany: purgePersonaMany,
|
|
11772
|
+
onReload: function () { loadTrash(true) },
|
|
11773
|
+
}) : null)
|
|
11774
|
+
}
|
|
11775
|
+
|
|
11776
|
+
/**
|
|
11777
|
+
* 记忆页的视图(按场景分桶 + 过滤 + 排序)。
|
|
11778
|
+
*
|
|
11779
|
+
* 提到组件外是为了能被 `useMemo` 包住:这段要遍历全部记忆并逐桶排序,而本页的
|
|
11780
|
+
* 编辑器弹窗每敲一个字都会重渲染整页(`editor` 是页内 state)。传进来的 `q` 已防抖。
|
|
10847
11781
|
*/
|
|
10848
11782
|
function memoryView(data, q, sceneFilter) {
|
|
10849
11783
|
function matchRule(r) {
|
|
@@ -11601,6 +12535,10 @@ function enabledMemoryCount(rules) {
|
|
|
11601
12535
|
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || editorNameInvalid || editorGroupInvalid || editorDescLen > 500 || !String(editor.name || '').trim() || !String(editor.body || '').trim(), onClick: editor.mode === 'create' ? submitCreate : submitUpdate }, editor.mode === 'create' ? t('memory.btn.create') : t('memory.btn.save')))) : null,
|
|
11602
12536
|
// 场景建/删/档案弹窗已移至「场景」页(ScenesPage)。
|
|
11603
12537
|
modal && modal.type === 'trash' ? React.createElement(Modal, { key: 'trash', wide: true, title: t('memory.trash.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
|
|
12538
|
+
// 保留期选择器(0.19.1):记忆回收站是本机条目最多的一处(实测 120 条),
|
|
12539
|
+
// 没有保留期就只能一条条点永久删除。放在 loading 判断**之前** ——
|
|
12540
|
+
// 它有自己的加载状态,不该跟着列表一起消失。
|
|
12541
|
+
React.createElement(RetentionSelect, { key: 'retention', t: t, onReload: loadTrash }),
|
|
11604
12542
|
trash.loading ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
11605
12543
|
: React.createElement(React.Fragment, null,
|
|
11606
12544
|
trash.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(trash.error)) : null,
|
|
@@ -12438,6 +13376,12 @@ function enabledMemoryCount(rules) {
|
|
|
12438
13376
|
// 常量用函数包一层:测试会把 _pages 的每个值当组件渲染一遍,数字会被 React 当成非法元素类型报警告。
|
|
12439
13377
|
_pages.sceneDescMax = function () { return SCENE_DESC_MAX }
|
|
12440
13378
|
_pages.memDescMax = function () { return MEM_DESC_MAX }
|
|
13379
|
+
// 回收站弹窗与保留期选择器(0.19.1):它们不是页面,但**四个页面共用**
|
|
13380
|
+
// (提示词 / 场景 / 子智能体走 TrashModal,技能页与记忆页各挂一个 RetentionSelect)。
|
|
13381
|
+
// 导出是为了能脱离宿主单独渲染一次 —— 这类"工具栏 + 固定高度列表"的排版
|
|
13382
|
+
// 只有真渲染才看得出挤不挤(同 `_pages.VersionBadge` 那条理由)。
|
|
13383
|
+
_pages.TrashModal = TrashModal
|
|
13384
|
+
_pages.RetentionSelect = RetentionSelect
|
|
12441
13385
|
},
|
|
12442
13386
|
}
|
|
12443
13387
|
return module.exports
|