dsh-plugin-tool-management 0.8.5 → 0.9.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 +275 -206
- package/README.md +71 -69
- package/README_EN.md +65 -65
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +132 -86
- package/lib/agents-md/preset-id.js +1 -1
- package/lib/agents-md/service.js +46 -9
- package/lib/approval-policy.js +1 -1
- package/lib/client.js +867 -146
- package/lib/compat/preset-reach.js +155 -93
- package/lib/context-inject.js +401 -0
- package/lib/hub.js +155 -13
- package/lib/imports/upload.js +34 -14
- package/lib/index.js +1072 -243
- package/lib/mcp/state-section.js +46 -15
- package/lib/rules/archive-engine.js +158 -64
- package/lib/rules/archive.js +41 -1
- package/lib/rules/service.js +424 -162
- package/lib/scene-prompt-sync.js +93 -12
- package/lib/skills/catalog.js +125 -0
- package/lib/skills/core.js +6 -4
- package/lib/skills/service.js +56 -2
- package/lib/subagents/catalog.js +15 -6
- package/lib/subagents/service.js +25 -12
- package/lib/subagents/tools.js +18 -13
- package/package.json +11 -6
- package/screenshots.json +8 -8
- package/docs/images/1/345/234/272/346/231/257.png +0 -0
- package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
- package/docs/images/2MCP.png +0 -0
- package/docs/images/2MCP_en.png +0 -0
- package/docs/images/3/346/212/200/350/203/275.png +0 -0
- package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
- package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
- package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
- package/docs/images/6/350/256/260/345/277/206.png +0 -0
- package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
- package/docs/images/7/344/274/232/350/257/235.png +0 -0
- package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
- package/docs/images/8/345/205/274/345/256/271.png +0 -0
- package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
- package/lib/prompt-sections.js +0 -148
package/lib/client.js
CHANGED
|
@@ -26,9 +26,12 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
'.dsm-tools{display:flex;flex-direction:column;gap:8px}' +
|
|
27
27
|
'.dsm-tool{padding:9px 11px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}' +
|
|
28
28
|
'.dsm-tool-name{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;font-weight:600;word-break:break-all}' +
|
|
29
|
-
'.dsm-tool-name-row{display:flex;align-items:center;
|
|
29
|
+
'.dsm-tool-name-row{display:flex;align-items:center;gap:8px}' +
|
|
30
|
+
// 标签紧跟工具名(原来是 space-between,标签被推到中间、与名字隔着一大片空白);
|
|
31
|
+
// 开关单独用 margin-left:auto 顶到最右,标签不再受它的位置影响。
|
|
32
|
+
'.dsm-tool-name-row .dsm-switch{margin-left:auto;flex:none}' +
|
|
30
33
|
'.dsm-tool-off .dsm-tool-name,.dsm-tool-off .dsm-tool-desc{opacity:.45}' +
|
|
31
|
-
'.dsm-tool-desc{margin-top:3px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px}' +
|
|
34
|
+
'.dsm-tool-desc{min-width:0;flex:1;margin-top:3px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px}' +
|
|
32
35
|
'.dsm-tool-params{display:flex;flex-direction:column;gap:3px;margin-top:7px;padding-top:7px;border-top:1px dashed var(--dsw-alias-border-l2)}' +
|
|
33
36
|
'.dsm-tool-param{display:flex;gap:8px;font-size:11px;align-items:baseline}' +
|
|
34
37
|
'.dsm-tool-param-key{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-weight:600;min-width:80px;word-break:break-all}' +
|
|
@@ -40,15 +43,49 @@ window.__ModuleLoader__.load({
|
|
|
40
43
|
'.dsm-detail-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
|
|
41
44
|
'.dsm-note-user{color:var(--dsw-alias-label-tertiary)}' +
|
|
42
45
|
`
|
|
43
|
-
.dsm-hint{display:flex;min-width:0;align-items:center;gap:5px;margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dsm-hint-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsm-hint-mark{display:inline-flex;width:14px;height:14px;flex:none;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2);border-radius:50%;font-size:9px;line-height:1;cursor:help}.dsm-pick-list{display:flex;max-height:240px;flex-direction:column;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;padding:4px}.dsm-tabs-end{display:flex;align-items:center;gap:4px;margin-left:auto}.dsm-tab{padding:8px 14px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}.dsm-tab-active{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-success-primary)}.dsm-section{box-sizing:border-box;display:flex;width:100%;max-width:820px;min-width:0;margin:0 auto;padding:2px 0 36px;container-type:inline-size;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary);font-family:inherit}.dsm-head{display:flex;flex-direction:column;align-items:stretch;gap:16px}.dsm-title-block{min-width:0}.dsm-title-row{display:flex;align-items:center;gap:8px 12px;min-width:0;flex-wrap:wrap}.dsm-feedback-links{display:flex;flex-direction:column;align-items:flex-end;gap:2px}.dsm-title{margin:0;font-size:24px;line-height:32px;font-weight:600;letter-spacing:-.4px;white-space:nowrap}.dsm-feedback-link{display:inline-flex;min-height:28px;align-items:center;gap:5px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;font-weight:500;line-height:18px;text-decoration:none;white-space:nowrap}.dsm-feedback-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dsm-feedback-link:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsm-feedback-link svg{flex:none}.dsm-desc{margin:12px 0 0;display:-webkit-box;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px;-webkit-box-orient:vertical;-webkit-line-clamp:2}.dsm-actions{display:flex;flex-wrap:wrap;gap:
|
|
46
|
+
.dsm-hint{display:flex;min-width:0;align-items:center;gap:5px;margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dsm-hint-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsm-hint-mark{display:inline-flex;width:14px;height:14px;flex:none;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2);border-radius:50%;font-size:9px;line-height:1;cursor:help}.dsm-pick-list{display:flex;max-height:240px;flex-direction:column;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;padding:4px}.dsm-tabs-end{display:flex;align-items:center;gap:4px;margin-left:auto}.dsm-tab{padding:8px 14px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}.dsm-tab-active{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-success-primary)}.dsm-section{box-sizing:border-box;display:flex;width:100%;max-width:820px;min-width:0;margin:0 auto;padding:2px 0 36px;container-type:inline-size;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary);font-family:inherit}.dsm-head{display:flex;flex-direction:column;align-items:stretch;gap:16px}.dsm-title-block{min-width:0}.dsm-title-row{display:flex;align-items:center;gap:8px 12px;min-width:0;flex-wrap:wrap}.dsm-feedback-links{display:flex;flex-direction:column;align-items:flex-end;gap:2px}.dsm-title{margin:0;font-size:24px;line-height:32px;font-weight:600;letter-spacing:-.4px;white-space:nowrap}.dsm-feedback-link{display:inline-flex;min-height:28px;align-items:center;gap:5px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;font-weight:500;line-height:18px;text-decoration:none;white-space:nowrap}.dsm-feedback-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dsm-feedback-link:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsm-feedback-link svg{flex:none}.dsm-desc{margin:12px 0 0;display:-webkit-box;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px;-webkit-box-orient:vertical;-webkit-line-clamp:2}.dsm-actions{display:flex;flex-wrap:wrap;gap:7px;margin-left:0;flex:none}
|
|
47
|
+
/* 页头动作行:内边距收 2px。批量按钮要按「取消全选」定宽(见 .dsm-btn-bulk),
|
|
48
|
+
收这一点正好把定宽多出来的宽度抵掉,整行仍在一行内。 */
|
|
49
|
+
.dsm-actions .dsm-btn{padding:0 11px}
|
|
50
|
+
/* 「全选 ↔ 取消全选」二合一:两种文案都进 DOM、叠在同一格里,隐身那份只负责占位 →
|
|
51
|
+
按钮宽度恒等于较宽的那个文案,点击不再改行宽、也就不会把整行挤到第二行。
|
|
52
|
+
不写死像素是有意的:中文「取消全选」与英文 Deselect all 宽度差很多,各自自适应。
|
|
53
|
+
注意选择器要写两段类名:.dsm-btn 的 display:inline-flex 在本规则之后出现,
|
|
54
|
+
同权重下会赢,两段类名才是不依赖书写顺序的写法(改 CSS 时别顺手简化成一段)。 */
|
|
55
|
+
.dsm-btn.dsm-btn-bulk{display:inline-grid;align-items:center;justify-items:center}
|
|
56
|
+
.dsm-btn.dsm-btn-bulk>.dsm-bulk-label,.dsm-btn.dsm-btn-bulk>.dsm-bulk-ghost{grid-area:1/1}
|
|
57
|
+
.dsm-btn.dsm-btn-bulk>.dsm-bulk-ghost{visibility:hidden}
|
|
58
|
+
/* 纯图标按钮(如目录卡片上的「移除来源」):与 quiet 按钮同高、只占一个图标宽。
|
|
59
|
+
同样要两段类名 —— .dsm-btn / .dsm-btn-quiet 的 padding 在本规则之后出现,同权重会赢。 */
|
|
60
|
+
.dsm-btn.dsm-btn-icon{width:28px;min-width:28px;padding:0}
|
|
61
|
+
/* 目录卡片右侧动作组:间距收窄一档,配合图标化后不再显挤。 */
|
|
62
|
+
/* 卡片头里的标签抬到 11px:左侧「N 个技能」是 12px,10px 的标签在这里显得忽大忽小。 */
|
|
63
|
+
.dsm-source-head .dsm-tag{font-size:11px;min-height:20px;padding:0 7px}
|
|
64
|
+
/* 「N 个附件」标签:和 tag 同形,但它是**能点的**(点了去编辑弹窗的附件区),
|
|
65
|
+
所以要把 button 的默认外观抹掉,并给一个可点的悬停反馈 —— 光靠颜色区分不出"能不能点"。 */
|
|
66
|
+
button.dsm-tag{background:transparent;font:inherit;font-size:10px;cursor:pointer}
|
|
67
|
+
button.dsm-tag:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
|
|
68
|
+
button.dsm-tag:disabled{cursor:default;opacity:.6}
|
|
69
|
+
.dsm-tag-attach{display:inline-flex;align-items:center;gap:4px}
|
|
70
|
+
.dsm-tag-attach-empty{color:var(--dsw-alias-label-tertiary);border-color:var(--dsw-alias-border-l3)}
|
|
44
71
|
.dsm-btn{box-sizing:border-box;display:inline-flex;min-height:34px;align-items:center;justify-content:center;padding:0 13px;border:1px solid transparent;border-radius:8px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;font-size:13px;font-weight:580;white-space:nowrap;cursor:pointer}.dsm-btn:hover:not(:disabled){filter:brightness(1.08)}.dsm-btn:disabled{opacity:.48;cursor:default}.dsm-btn-secondary{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary)}.dsm-btn-quiet{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);min-height:28px;padding:0 9px;font-size:12px}.dsm-btn-danger{border-color:var(--dsw-alias-state-error-primary);background:transparent;color:var(--dsw-alias-state-error-primary)}.dsm-btn-lock{border-color:#d49245;background:transparent;color:#d49245}.dsm-btn:focus-visible,.dsm-control:focus-visible,.dsm-select-trigger:focus-visible,.dsm-source-head:focus-visible,.dsm-switch:focus-visible,.dsm-upload-link:focus-visible,.dsm-file-remove:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}
|
|
45
72
|
.dsm-summary{display:grid;grid-template-columns:repeat(var(--dsm-stat-cols,3),minmax(0,1fr));overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-stat{padding:12px 14px;border-right:1px solid var(--dsw-alias-border-l1);font-size:13px;color:var(--dsw-alias-label-secondary)}.dsm-stat:last-child{border-right:0}.dsm-stat strong,.dsm-stat-num{margin-right:5px;color:var(--dsw-alias-label-primary);font-size:17px;font-weight:680}.dsm-filters{display:flex;gap:9px}.dsm-search{flex:1}.dsm-source-filter{width:210px;flex:none}
|
|
46
73
|
.dsm-token-panel{display:flex;flex-direction:column;gap:8px}.dsm-token-row{display:flex;gap:8px;align-items:center;min-width:0}.dsm-token-row .dsm-control{flex:1;min-width:0}
|
|
47
74
|
.dsm-control,.dsm-select-trigger{box-sizing:border-box;width:100%;min-height:34px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}.dsm-control::placeholder{color:var(--dsw-alias-label-tertiary)}textarea.dsm-control{min-height:160px;padding-top:9px;resize:vertical;line-height:20px}.dsm-select{position:relative}.dsm-select-trigger{display:flex;align-items:center;justify-content:space-between;text-align:left;cursor:pointer}.dsm-select-menu{position:absolute;z-index:40;top:calc(100% + 5px);right:0;left:0;display:flex;max-height:260px;padding:5px;overflow:auto;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2)}.dsm-option{padding:8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;text-align:left;cursor:pointer}.dsm-option:hover,.dsm-option[aria-selected=true]{background:var(--dsw-alias-interactive-bg-hover)}
|
|
48
|
-
.dsm-sources{display:flex;flex-direction:column;gap:9px}.dsm-source{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-source-head{box-sizing:border-box;display:flex;width:100%;min-height:48px;align-items:center;padding:0 13px}.dsm-source-head-main{display:flex;min-width:0;min-height:48px;flex:1;align-items:center;gap:10px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsm-source-head:hover,.dsm-row:hover,.dsm-trash-row:hover,.dsm-hist-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-source-title{font-size:14px;font-weight:650;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsm-count{color:var(--dsw-alias-label-tertiary);font-size:12px;flex:none}.dsm-path{min-width:0;margin-left:auto;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.dsm-source-actions{display:flex;align-items:center;gap:
|
|
75
|
+
.dsm-sources{display:flex;flex-direction:column;gap:9px}.dsm-source{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-source-head{box-sizing:border-box;display:flex;width:100%;min-height:48px;align-items:center;padding:0 13px}.dsm-source-head-main{display:flex;min-width:0;min-height:48px;flex:1;align-items:center;gap:10px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsm-source-head:hover,.dsm-row:hover,.dsm-trash-row:hover,.dsm-hist-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-source-title{font-size:14px;font-weight:650;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsm-count{color:var(--dsw-alias-label-tertiary);font-size:12px;flex:none}.dsm-path{min-width:0;margin-left:auto;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.dsm-source-actions{display:flex;align-items:center;gap:6px;margin-left:8px}.dsm-source-body{border-top:1px solid var(--dsw-alias-border-l1)}.dsm-source-note{padding:9px 13px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
|
|
49
76
|
.dsm-table-head,.dsm-row{display:grid;grid-template-columns:minmax(180px,1fr) 120px 90px max-content;align-items:center;column-gap:12px;padding:0 13px}.dsm-table-head{min-height:32px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-row{min-height:58px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-row:last-child{border-bottom:0}.dsm-main{min-width:0}.dsm-name{overflow:hidden;font-size:13px;font-weight:570;text-overflow:ellipsis;white-space:nowrap}.dsm-note{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dsm-tags{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.dsm-tag{display:inline-flex;min-height:19px;align-items:center;padding:0 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:10px;white-space:nowrap}.dsm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dsm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dsm-enabled{color:var(--dsw-alias-state-success-primary);font-size:12px;white-space:nowrap}.dsm-disabled{color:#d49245;font-size:12px;white-space:nowrap}.dsm-shadowed{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dsm-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px}
|
|
50
77
|
.dsm-switch{position:relative;width:34px;height:20px;flex:none;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer}.dsm-switch:after{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:#fff;content:"";transition:transform 160ms ease}.dsm-switch-on{background:var(--dsw-alias-state-success-primary)}.dsm-switch-on:after{transform:translateX(14px)}.dsm-switch:disabled{opacity:.45;cursor:default}.dsm-trash-row{display:flex;min-height:48px;align-items:center;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);color:inherit;font:inherit;font-size:13px;cursor:pointer}.dsm-trash-count{margin-left:auto;padding:2px 7px;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:11px}.dsm-empty{padding:25px 14px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}.dsm-feedback{padding:9px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-warning{border-color:#d49245;color:#d49245}.dsm-error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
51
78
|
.dsm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.dsm-modal{box-sizing:border-box;display:flex;width:min(560px,100%)!important;max-height:min(760px,calc(100vh - 48px));min-width:0;flex-direction:column;gap:16px;padding:22px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dsm-modal-sm,.dsm-modal-import{width:min(480px,100%)!important}.dsm-modal-md{width:min(560px,100%)!important}.dsm-modal-lg,.dsm-modal-wide{width:min(720px,100%)!important}.dsm-modal-list{width:min(560px,100%)!important;height:min(640px,calc(100vh - 48px))!important;max-height:none!important;overflow:hidden}.dsm-modal-body{display:flex;min-height:0;flex-direction:column;gap:10px}.dsm-modal-list .dsm-modal-body{flex:1;overflow:auto;overscroll-behavior:contain}.dsm-modal-import{padding:24px}.dsm-modal-head{display:flex;align-items:flex-start;gap:12px}.dsm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:670}.dsm-form,.dsm-field,.dsm-detail-section{display:flex;flex-direction:column}.dsm-form{gap:12px}.dsm-field{gap:6px}.dsm-label,.dsm-detail-title{font-size:12px}.dsm-label{color:var(--dsw-alias-label-secondary)}.dsm-detail-title{font-weight:650}.dsm-help{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dsm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dsm-hidden-input{display:none}.dsm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:170px;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:22px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);transition:border-color 180ms ease,background 180ms ease;cursor:pointer}.dsm-dropzone:hover,.dsm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:18px;font-weight:700}.dsm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dsm-upload-choices{display:flex;align-items:center;gap:7px}.dsm-upload-link,.dsm-file-remove{padding:0;border:0;background:transparent;font:inherit;font-size:12px;cursor:pointer}.dsm-upload-link{color:var(--dsw-alias-label-secondary)}.dsm-upload-link:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.dsm-upload-link:disabled{opacity:.45;cursor:default}.dsm-upload-divider{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-file{display:flex;align-items:center;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsm-file-kind{display:inline-flex;min-width:30px;height:24px;align-items:center;justify-content:center;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:9px;font-weight:700}.dsm-file-name{min-width:0;overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dsm-file-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}.dsm-file-remove{width:24px;height:24px;color:var(--dsw-alias-label-secondary);font-size:18px}.dsm-upload-requirements{padding:1px 1px 0}.dsm-upload-requirements ul{display:flex;margin:7px 0 0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dsm-detail-section{gap:7px}.dsm-detail-path,.dsm-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}.dsm-detail-path{padding:8px 10px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}.dsm-code{max-height:280px;margin:0;padding:12px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);line-height:18px;white-space:pre-wrap}.dsm-diag{padding:8px 10px;border-left:2px solid #d49245;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-trash-group-sub{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
|
|
79
|
+
.dsm-modal-title-row{display:inline-flex;align-items:center;gap:9px;min-width:0}
|
|
80
|
+
.dsm-pill{display:inline-flex;min-height:20px;align-items:center;padding:0 8px;border:1px solid var(--dsw-alias-border-l3);border-radius:999px;font-size:11px;font-weight:500;white-space:nowrap}
|
|
81
|
+
.dsm-pill-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}
|
|
82
|
+
.dsm-pill-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
83
|
+
.dsm-pill-warn{border-color:#d49245;color:#d49245}
|
|
84
|
+
.dsm-pill-muted{color:var(--dsw-alias-label-tertiary)}
|
|
85
|
+
.dsm-tool-desc-wrap{display:flex;align-items:flex-end;gap:8px}
|
|
86
|
+
.dsm-tool-desc-clamp{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:2}
|
|
87
|
+
.dsm-tool-desc-toggle{flex:none;margin-left:auto;padding:0;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:pointer}
|
|
88
|
+
.dsm-tool-desc-toggle:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}
|
|
52
89
|
.dsm-trash-group{display:flex;flex-direction:column;overflow:hidden;margin-bottom:12px;border:1px solid var(--dsw-alias-border-l2);border-left-width:3px;border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
|
|
53
90
|
/* 「这组动的是磁盘文件」用左侧色条区分:中性 = 文件不动;危险色 = 真的会删文件。 */
|
|
54
91
|
.dsm-trash-group-danger{border-left-color:var(--dsw-alias-state-error-primary)}
|
|
@@ -75,9 +112,15 @@ window.__ModuleLoader__.load({
|
|
|
75
112
|
「没有滚动条 + 内容缺一截」。列表项的尺寸一律由内容决定,滚动只交给 body 这一层。 */
|
|
76
113
|
.dsm-modal-list .dsm-modal-body>*{flex:none}
|
|
77
114
|
.dsm-fm{display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}.dsm-fm-row{display:grid;grid-template-columns:minmax(96px,150px) minmax(0,1fr);gap:10px;padding:8px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:18px}.dsm-fm-row:last-child{border-bottom:0}.dsm-fm-key{color:var(--dsw-alias-label-secondary);word-break:break-word}.dsm-fm-raw{margin-left:5px;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px}.dsm-fm-val{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-word;white-space:pre-wrap}
|
|
78
|
-
@container(max-width:780px){.dsm-table-head{display:none}.dsm-row{grid-template-columns:minmax(0,1fr) max-content;gap:8px;padding:11px 13px}.dsm-row>.dsm-tags,.dsm-row>.dsm-status{grid-column:1}.dsm-row-actions{grid-column:2;grid-row:1 / span 3}.dsm-path{display:none}
|
|
115
|
+
@container(max-width:780px){.dsm-table-head{display:none}.dsm-row{grid-template-columns:minmax(0,1fr) max-content;gap:8px;padding:11px 13px}.dsm-row>.dsm-tags,.dsm-row>.dsm-status{grid-column:1}.dsm-row-actions{grid-column:2;grid-row:1 / span 3}.dsm-path{display:none}
|
|
116
|
+
/* 一个标签都没有的记忆(flat 且没有场景 / 派生标签):那一行是空的,高度 0 却仍要付两个
|
|
117
|
+
8px 的行间距 —— 于是「描述」和「已启用」之间凭空多出 16px(用户实测)。让它不占位,
|
|
118
|
+
并把右侧动作组的跨度跟着收到两行。**只在这一档布局里做**:宽布局下标签是固定 120px 的
|
|
119
|
+
一列,抽掉它后面几列会整体左移。 */
|
|
120
|
+
.dsm-row>.dsm-tags:empty{display:none}
|
|
121
|
+
.dsm-row>.dsm-tags:empty~.dsm-row-actions{grid-row:1 / span 2}}@media(max-width:760px){.dsm-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.dsm-title-row{flex-wrap:wrap}}@container(max-width:520px){.dsm-head{flex-direction:column}.dsm-actions{width:100%;margin-left:0}.dsm-actions .dsm-btn{flex:1}.dsm-filters{flex-direction:column}.dsm-source-filter{width:100%}.dsm-summary{grid-template-columns:1fr}}
|
|
79
122
|
.dsm-hist-row{display:flex;align-items:center;gap:11px;padding:10px 13px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-hist-row:last-child{border-bottom:0}.dsm-hist-main{min-width:0;flex:1}.dsm-hist-title{overflow:hidden;font-size:13px;font-weight:580;text-overflow:ellipsis;white-space:nowrap}.dsm-hist-cwd{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dsm-hist-cwd-missing{color:var(--dsw-alias-state-error-primary)}.dsm-hist-actions{display:flex;gap:6px;flex:none}.dsm-hist-check,.dsm-hist-group-check{flex:none;width:15px;height:15px;margin:0 4px 0 0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}.dsm-hist-group-head{gap:10px}.dsm-hist-group-extra{display:flex;align-items:center;gap:8px;flex:none;margin-left:4px}.dsm-hist-group-extra .dsm-tag{flex:none}.dsm-hist-dir-missing{flex:none;color:var(--dsw-alias-state-error-primary);font-size:11px;white-space:nowrap}.dsm-hist-register-path{margin-top:-4px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-mono,monospace);font-size:11px;word-break:break-all}.dsm-hist-batch{display:flex;align-items:center;gap:8px;margin-left:2px;padding-left:10px;border-left:1px solid var(--dsw-alias-border-l2)}.dsm-hist-batch-count{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:nowrap}.dsm-hist-batch-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px;flex:none}.dsm-dir-row{display:flex;gap:8px}.dsm-dir-row .dsm-control{flex:1}.dsm-dir-list{display:flex;max-height:220px;flex-direction:column;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}.dsm-dir-item{display:flex;width:100%;align-items:center;padding:8px 12px;border:0;border-bottom:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-align:left;cursor:pointer}.dsm-dir-item:last-child{border-bottom:0}.dsm-dir-item:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-dir-item::before{content:"📁";margin-right:8px;font-size:12px}
|
|
80
|
-
.dsm-rule-shadowed .dsm-name,.dsm-rule-shadowed .dsm-note{color:var(--dsw-alias-state-error-primary)}.dsm-rule-shadow-hint{margin-top:3px;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}.dsm-rule-invalid{border-color:var(--dsw-alias-state-error-primary)!important}.dsm-rule-hint{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}.dsm-rule-budget{display:flex;flex-direction:column;gap:6px}.dsm-budget-meta{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.dsm-budget-meta strong{margin-right:0;font-size:13px;font-weight:680}.dsm-budget-over-text{color:var(--dsw-alias-state-error-primary);font-size:11px}.dsm-budget-bar{height:6px;overflow:hidden;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover)}.dsm-budget-fill{height:100%;border-radius:99px;background:var(--dsw-alias-state-success-primary);transition:width 160ms ease}.dsm-budget-fill.dsm-budget-over{background:var(--dsw-alias-state-error-primary)}.dsm-char-count{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-char-over{color:var(--dsw-alias-state-error-primary)}
|
|
123
|
+
.dsm-rule-shadowed .dsm-name,.dsm-rule-shadowed .dsm-note{color:var(--dsw-alias-state-error-primary)}.dsm-rule-off .dsm-name,.dsm-rule-off .dsm-note{color:var(--dsw-alias-label-tertiary)}.dsm-rule-shadow-hint{margin-top:3px;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}.dsm-rule-invalid{border-color:var(--dsw-alias-state-error-primary)!important}.dsm-rule-hint{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}.dsm-rule-budget{display:flex;flex-direction:column;gap:6px}.dsm-budget-meta{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.dsm-budget-meta strong{margin-right:0;font-size:13px;font-weight:680}.dsm-budget-over-text{color:var(--dsw-alias-state-error-primary);font-size:11px}.dsm-budget-bar{height:6px;overflow:hidden;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover)}.dsm-budget-fill{height:100%;border-radius:99px;background:var(--dsw-alias-state-success-primary);transition:width 160ms ease}.dsm-budget-fill.dsm-budget-over{background:var(--dsw-alias-state-error-primary)}.dsm-char-count{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-char-over{color:var(--dsw-alias-state-error-primary)}
|
|
81
124
|
.dsm-combo-row{display:flex;gap:8px}.dsm-combo-row .dsm-control{flex:1}
|
|
82
125
|
.dsm-attach-list{display:flex;flex-direction:column;gap:6px}.dsm-attach-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
83
126
|
.dsm-toast{position:fixed;right:22px;bottom:22px;z-index:1250;box-sizing:border-box;max-width:min(420px,calc(100vw - 44px));padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;animation:dsm-toast-in 160ms ease-out}@keyframes dsm-toast-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.dsm-toast{animation:none}}
|
|
@@ -163,6 +206,11 @@ window.__ModuleLoader__.load({
|
|
|
163
206
|
.dsm-mode-row{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}
|
|
164
207
|
.dsm-mode-row.dsm-mode-on{border-color:var(--dsw-alias-border-l3)}
|
|
165
208
|
.dsm-mode-head{display:flex;min-height:38px;align-items:center;gap:9px;padding:0 10px}
|
|
209
|
+
/* 行首是按钮:已配好的模式点它就展开/收起名单(右侧两个按钮只管启用/停用,两者别混)。 */
|
|
210
|
+
.dsm-mode-head-main{display:flex;min-width:0;flex:1;align-items:center;gap:9px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}
|
|
211
|
+
.dsm-mode-head-main:disabled{cursor:default}
|
|
212
|
+
.dsm-mode-head-main:not(:disabled):hover .dsm-mode-name{color:var(--dsw-alias-label-primary)}
|
|
213
|
+
.dsm-mode-caret{flex:none;width:10px;color:var(--dsw-alias-label-tertiary);font-size:10px}
|
|
166
214
|
.dsm-mode-name{font-size:12px;font-weight:570;white-space:nowrap}
|
|
167
215
|
.dsm-mode-sum{color:var(--dsw-alias-label-tertiary);font-size:11px}
|
|
168
216
|
.dsm-mode-sum.dsm-mode-allow{color:var(--dsw-alias-state-success-primary)}
|
|
@@ -206,6 +254,17 @@ window.__ModuleLoader__.load({
|
|
|
206
254
|
.dsm-compat-pill-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}
|
|
207
255
|
.dsm-compat-pill-warn{border-color:#d49245;color:#d49245}
|
|
208
256
|
.dsm-compat-reach-marks{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px;min-width:0}
|
|
257
|
+
.dsm-inject-settings{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
|
|
258
|
+
/* 注入域勾选:横排、窄容器自动换行;标签一律短名(记忆 / MCP / …),不截断也不竖排。 */
|
|
259
|
+
.dsm-inject-domains{display:flex;flex-wrap:wrap;gap:7px 18px;padding:0 2px}
|
|
260
|
+
.dsm-inject-domain{display:inline-flex;align-items:center;gap:7px;color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;white-space:nowrap;cursor:pointer}
|
|
261
|
+
.dsm-inject-domain input[type=checkbox]{flex:none;width:15px;height:15px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
|
|
262
|
+
/* 注入实况(只读):与「注入」设置同屏。行 = 域 + 体积 + 状态胶囊,点域名展开正文。 */
|
|
263
|
+
.dsm-inject-live-actions{display:flex;flex:none;align-items:center;gap:6px;margin-left:auto}
|
|
264
|
+
.dsm-inject-live-toggle{padding:0;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer}
|
|
265
|
+
.dsm-inject-live-toggle:hover{text-decoration:underline}
|
|
266
|
+
.dsm-inject-live-toggle:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}
|
|
267
|
+
.dsm-inject-live-size{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}
|
|
209
268
|
.dsm-reach-name{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
|
|
210
269
|
.dsm-reach-chip{display:inline-flex;min-height:22px;align-items:center;padding:0 9px;border:1px solid var(--dsw-alias-border-l3);border-radius:6px;color:var(--dsw-alias-label-secondary);font-size:11px;white-space:nowrap}
|
|
211
270
|
.dsm-reach-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}
|
|
@@ -256,6 +315,58 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
256
315
|
document.head.appendChild(tag)
|
|
257
316
|
}
|
|
258
317
|
|
|
318
|
+
/**
|
|
319
|
+
* 「两种状态文案 + 定宽」的按钮内容:两份文案都渲染、叠在同一格,隐身那份只负责占位
|
|
320
|
+
* (配合 `.dsm-btn.dsm-btn-bulk` 的两段类名选择器)—— 按钮宽度恒等于较宽的那个文案,
|
|
321
|
+
* 状态切换时按钮不跳宽、整行也不重排。「全选 ↔ 取消全选」与「刷新 ↔ 刷新中…」共用这一套。
|
|
322
|
+
* 不写死像素宽度是有意的:中文「取消全选 / 刷新中…」与英文各自自适应,加语言不用改样式。
|
|
323
|
+
*/
|
|
324
|
+
function fixedLabelPair(label, other) {
|
|
325
|
+
return [
|
|
326
|
+
React.createElement('span', { key: 'v', className: 'dsm-bulk-label' }, label),
|
|
327
|
+
React.createElement('span', { key: 'g', className: 'dsm-bulk-ghost', 'aria-hidden': 'true' }, other),
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 垃圾桶图标:给「移除来源」这类破坏性操作做纯图标按钮用(按钮本身必须带 title /
|
|
333
|
+
* aria-label —— 图标按钮没有可见文字,说明只能靠它们,别省)。
|
|
334
|
+
*/
|
|
335
|
+
function TrashIcon(props) {
|
|
336
|
+
var size = (props && props.size) || 14
|
|
337
|
+
return React.createElement('svg', { viewBox: '0 0 16 16', width: size, height: size, 'aria-hidden': 'true', focusable: 'false' },
|
|
338
|
+
React.createElement('path', { fill: 'currentColor', d: 'M6.5 1h3l.5 1h3v1.5H3V2h3zM4 5h8l-.6 9.2a1 1 0 0 1-1 .8H5.6a1 1 0 0 1-1-.8z' }))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 回形针图标:bundle 记忆的「N 个附件」标签用(表示"随记忆一起存放的文件")。
|
|
343
|
+
* 只有一并带文字时才用它 —— 纯图标按钮的说明必须写在 title / aria-label 里。
|
|
344
|
+
*/
|
|
345
|
+
function PaperclipIcon(props) {
|
|
346
|
+
var size = (props && props.size) || 11
|
|
347
|
+
return React.createElement('svg', { viewBox: '0 0 16 16', width: size, height: size, fill: 'none', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round', 'aria-hidden': 'true', focusable: 'false' },
|
|
348
|
+
React.createElement('path', { d: 'M9.6 4.2 5.1 8.7a1.9 1.9 0 0 0 2.7 2.7l5-5a3.4 3.4 0 0 0-4.8-4.8L3 6.6a4.9 4.9 0 0 0 6.9 6.9l4.1-4.1' }))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 刷新按钮(全站八处管理页共用:MCP / 技能 / 提示词 / 会话 / 场景 / 子智能体 / 记忆 / 兼容)。
|
|
353
|
+
* 宽度同样按两种文案里更宽的那个固定(见 fixedLabelPair),所以点一下「刷新」不会因为
|
|
354
|
+
* 文案变成「刷新中…」把整行挤走。
|
|
355
|
+
*
|
|
356
|
+
* @param tFn 取词函数 —— MCP 页传 `mt`(模块作用域的 t 会被宿主 locale 覆盖),其余页传各自的 t
|
|
357
|
+
* @param busy 是否正在刷新:决定显示哪份文案(通常也同时决定 disabled)
|
|
358
|
+
* @param args 透传给 <button> 的其余属性(className / disabled / onClick / title …)
|
|
359
|
+
*/
|
|
360
|
+
function refreshButton(tFn, busy, args) {
|
|
361
|
+
var idle = tFn('btn.refresh')
|
|
362
|
+
var working = tFn('btn.refreshing')
|
|
363
|
+
// 定宽靠 `.dsm-btn.dsm-btn-bulk`(两段类名,见 CSS 里的注释)—— 由这里统一补上,
|
|
364
|
+
// 调用方只管自己的样式,不用记这个类。
|
|
365
|
+
var cls = ((args && args.className) || 'dsm-btn dsm-btn-secondary') + ' dsm-btn-bulk'
|
|
366
|
+
return React.createElement('button', Object.assign({ type: 'button' }, args, { className: cls }),
|
|
367
|
+
fixedLabelPair(busy ? working : idle, busy ? idle : working))
|
|
368
|
+
}
|
|
369
|
+
|
|
259
370
|
// Optional access token for write ops (host config.token or
|
|
260
371
|
// DSH_SKILL_MCP_MANAGER_TOKEN). Read once from localStorage — there is no
|
|
261
372
|
// UI row for it — and sent as `x-dsh-token` on every request (the host
|
|
@@ -338,6 +449,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
338
449
|
const t = props.t
|
|
339
450
|
const [data, setData] = React.useState(null)
|
|
340
451
|
const [presetReach, setPresetReach] = React.useState(null)
|
|
452
|
+
// 注入设置(本插件五个注入域的开关;见 src/context-inject.ts)。它和可达性矩阵是一体两面:
|
|
453
|
+
// 矩阵说"到不到得了",这里决定"要不要"。读不到时这一节不显示。
|
|
454
|
+
const [inject, setInject] = React.useState(null)
|
|
455
|
+
// 注入实况(只读):最近活跃会话里模型**真正看到**的五域文本 + 本次运行的投递统计。
|
|
456
|
+
// 与「注入」设置是一体两面:设置说"要送什么",这里说"实际送到了什么"。
|
|
457
|
+
const [live, setLive] = React.useState(null)
|
|
458
|
+
const [liveOpen, setLiveOpen] = React.useState('')
|
|
459
|
+
const [liveCopied, setLiveCopied] = React.useState(false)
|
|
341
460
|
const [busy, setBusy] = React.useState(false)
|
|
342
461
|
const [error, setError] = React.useState(null)
|
|
343
462
|
const apply = function (r, alive) {
|
|
@@ -356,6 +475,16 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
356
475
|
.then(function (r) { applyReach(r, alive) })
|
|
357
476
|
.catch(function () { /* 未挂载预设服务 → 不渲染这一节 */ })
|
|
358
477
|
}
|
|
478
|
+
const loadInject = function (alive) {
|
|
479
|
+
apiCall('inject-settings', {})
|
|
480
|
+
.then(function (r) { if (alive !== false && r && r.ok && r.settings) setInject(r.settings) })
|
|
481
|
+
.catch(function () { /* 读不到 → 不渲染这一节 */ })
|
|
482
|
+
}
|
|
483
|
+
const loadLive = function (alive) {
|
|
484
|
+
apiCall('injection-live', {})
|
|
485
|
+
.then(function (r) { if (alive !== false && r && r.ok) setLive(r) })
|
|
486
|
+
.catch(function () { /* 读不到 → 不渲染这一节 */ })
|
|
487
|
+
}
|
|
359
488
|
React.useEffect(function () {
|
|
360
489
|
let alive = true
|
|
361
490
|
setBusy(true)
|
|
@@ -363,6 +492,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
363
492
|
.then(function (r) { apply(r, alive) })
|
|
364
493
|
.catch(function (e) { if (alive) { setError(String((e && e.message) || e)); setBusy(false) } })
|
|
365
494
|
loadReach(alive)
|
|
495
|
+
loadInject(alive)
|
|
496
|
+
loadLive(alive)
|
|
366
497
|
return function () { alive = false }
|
|
367
498
|
}, [])
|
|
368
499
|
const reload = function () {
|
|
@@ -371,6 +502,21 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
371
502
|
.then(function (r) { apply(r, true) })
|
|
372
503
|
.catch(function (e) { setError(String((e && e.message) || e)); setBusy(false) })
|
|
373
504
|
loadReach(true)
|
|
505
|
+
loadInject(true)
|
|
506
|
+
loadLive(true)
|
|
507
|
+
}
|
|
508
|
+
// 保存注入设置:先乐观更新界面(点一下就该有反馈),服务端返回值到达后以其为准;
|
|
509
|
+
// 失败则重新拉一次。可达性矩阵跟着刷新 —— 芯片要反映新的开关状态。
|
|
510
|
+
const saveInject = function (patch) {
|
|
511
|
+
if (!inject) return
|
|
512
|
+
const next = Object.assign({}, inject, patch)
|
|
513
|
+
setInject(next)
|
|
514
|
+
apiCall('inject-settings', Object.assign({ set: true }, next))
|
|
515
|
+
.then(function (r) {
|
|
516
|
+
if (r && r.ok && r.settings) setInject(r.settings)
|
|
517
|
+
loadReach(true)
|
|
518
|
+
})
|
|
519
|
+
.catch(function () { loadInject(true); loadReach(true) })
|
|
374
520
|
}
|
|
375
521
|
|
|
376
522
|
const findings = (data && data.findings) || []
|
|
@@ -414,6 +560,115 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
414
560
|
const push = function (node) {
|
|
415
561
|
body.push(React.cloneElement(node, { key: 'compat-' + String(body.length) }))
|
|
416
562
|
}
|
|
563
|
+
// 注入设置块(就放在刷新按钮下面):本插件注入给模型哪些内容的开关。总开关只管
|
|
564
|
+
// "极简这类预设下要不要破例",五个域勾选在任何预设下都生效。
|
|
565
|
+
//
|
|
566
|
+
// 排版口径(用户裁定 2026-09-16):勾选**横排**(短标签一行放得下,不再一人一行竖着排),
|
|
567
|
+
// 文案只说到"有这个内容"为止 —— 用户是普通使用者,不解释 persona/载体/路径这些内部机制。
|
|
568
|
+
if (inject) {
|
|
569
|
+
const domains = (inject && inject.domains) || {}
|
|
570
|
+
push(section(t('compat.inject'), t('compat.inject.hint'),
|
|
571
|
+
React.createElement('div', { className: 'dsm-inject-settings' },
|
|
572
|
+
React.createElement('label', { className: 'dsm-pick' },
|
|
573
|
+
React.createElement('input', { type: 'checkbox', checked: inject.underSuppressingPresets === true, onChange: function () { saveInject({ underSuppressingPresets: inject.underSuppressingPresets !== true }) } }),
|
|
574
|
+
React.createElement('span', { className: 'dsm-pick-main' },
|
|
575
|
+
React.createElement('span', { className: 'dsm-pick-name' }, t('compat.inject.force')),
|
|
576
|
+
React.createElement('span', { className: 'dsm-pick-desc' }, t('compat.inject.force.desc')))),
|
|
577
|
+
React.createElement('div', { className: 'dsm-seg-group' },
|
|
578
|
+
React.createElement('span', null, t('compat.inject.domains')),
|
|
579
|
+
React.createElement('span', { className: 'dsm-seg-group-line' })),
|
|
580
|
+
React.createElement('div', { className: 'dsm-inject-domains' },
|
|
581
|
+
['memory', 'mcp', 'skills', 'subagents', 'prompt'].map(function (key) {
|
|
582
|
+
return React.createElement('label', { className: 'dsm-inject-domain', key: 'domain-' + key },
|
|
583
|
+
React.createElement('input', { type: 'checkbox', checked: domains[key] !== false, onChange: function () {
|
|
584
|
+
const nextDomains = Object.assign({}, domains)
|
|
585
|
+
nextDomains[key] = domains[key] === false
|
|
586
|
+
saveInject({ domains: nextDomains })
|
|
587
|
+
} }),
|
|
588
|
+
React.createElement('span', null, t('compat.inject.domain.' + key)))
|
|
589
|
+
})))))
|
|
590
|
+
}
|
|
591
|
+
// 注入实况:紧挨「注入」设置块下方。数据来自 `injection-live`(宿主读最近活跃会话的
|
|
592
|
+
// 可见表面),是"回放"不是"重算"—— 界面显示已注入而模型实际没收到时(场景接管、
|
|
593
|
+
// 压制型预设、开关被关、压缩后还没补发),这里一眼可见。
|
|
594
|
+
// 排版口径:与可达性矩阵同款(名称 + 状态胶囊),正文默认收起、点域名展开;
|
|
595
|
+
// 体积与 ≈tokens 只做展示(token 是估算,标「≈」)。
|
|
596
|
+
if (live) {
|
|
597
|
+
const domainLabel = function (key) { return t('compat.inject.domain.' + key) }
|
|
598
|
+
const fmtSize = function (bytes) {
|
|
599
|
+
var v = Number(bytes) || 0
|
|
600
|
+
if (v < 1024) return v + ' B'
|
|
601
|
+
return (Math.round((v / 1024) * 10) / 10) + ' KB'
|
|
602
|
+
}
|
|
603
|
+
// 中英混合估算:ASCII 按 4 字符/词、非 ASCII 按 2 字符/词(只影响展示)。
|
|
604
|
+
const estTokens = function (text) {
|
|
605
|
+
var ascii = 0
|
|
606
|
+
var wide = 0
|
|
607
|
+
for (var i = 0; i < text.length; i += 1) { if (text.charCodeAt(i) < 128) ascii += 1; else wide += 1 }
|
|
608
|
+
return Math.ceil(ascii / 4 + wide / 2)
|
|
609
|
+
}
|
|
610
|
+
// 状态 → 胶囊:绿 = 本插件注入且已在上下文;琥珀只留给"该投却没投"。
|
|
611
|
+
// 官方载体(技能 / 提示词在标准类预设下由官方送)、已关闭、无内容一律灰的
|
|
612
|
+
// —— 它们都不是问题,报成琥珀会把用户自己的选择读成故障(用户 2026-09-16 指出)。
|
|
613
|
+
const stateOf = function (row) {
|
|
614
|
+
if (row.state === 'in-context') return { cls: ' dsm-compat-pill-ok', text: t('compat.live.state.inContext'), title: '' }
|
|
615
|
+
if (row.state === 'absent') return { cls: ' dsm-compat-pill-warn', text: t('compat.live.state.absent'), title: t('compat.live.state.absent.title') }
|
|
616
|
+
if (row.state === 'official') return { cls: '', text: t('compat.live.state.official'), title: t('compat.live.state.official.title') }
|
|
617
|
+
if (row.state === 'off') return { cls: '', text: t('compat.live.state.off'), title: '' }
|
|
618
|
+
if (row.state === 'empty') return { cls: '', text: t('compat.live.state.empty'), title: '' }
|
|
619
|
+
if (row.state === 'cleared') return { cls: '', text: t('compat.live.state.cleared'), title: '' }
|
|
620
|
+
return { cls: '', text: t('compat.live.state.unknown'), title: '' }
|
|
621
|
+
}
|
|
622
|
+
// 有正文的域就是"模型看到的内容":本插件注入的与官方注入的(技能 / 提示词)都算。
|
|
623
|
+
const hasText = function (row) { return (row.state === 'in-context' || row.state === 'official') && row.text !== '' }
|
|
624
|
+
const copyAll = function () {
|
|
625
|
+
const parts = []
|
|
626
|
+
;(live.domains || []).forEach(function (row) {
|
|
627
|
+
if (hasText(row)) parts.push('## ' + domainLabel(row.key) + '\n' + row.text)
|
|
628
|
+
})
|
|
629
|
+
const text = parts.join('\n\n')
|
|
630
|
+
if (!text) return
|
|
631
|
+
try {
|
|
632
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
633
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
634
|
+
setLiveCopied(true)
|
|
635
|
+
setTimeout(function () { setLiveCopied(false) }, 1500)
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
} catch (e) { /* 剪贴板不可用就算了:正文仍可手动选中复制 */ }
|
|
639
|
+
}
|
|
640
|
+
const delivered = live.delivered || {}
|
|
641
|
+
const deliveredText = delivered.count > 0
|
|
642
|
+
? t('compat.live.delivered', { count: delivered.count, time: new Date(delivered.lastAt).toTimeString().slice(0, 5) })
|
|
643
|
+
: t('compat.live.never')
|
|
644
|
+
const canCopy = (live.domains || []).some(hasText)
|
|
645
|
+
push(React.createElement('section', { className: 'dsm-compat-section' },
|
|
646
|
+
React.createElement('div', { className: 'dsm-compat-section-head' },
|
|
647
|
+
React.createElement('h3', { className: 'dsm-compat-section-title' }, t('compat.live.title')),
|
|
648
|
+
React.createElement('span', { className: 'dsm-compat-section-hint' }, t('compat.live.hint') + ' · ' + deliveredText),
|
|
649
|
+
React.createElement('span', { className: 'dsm-inject-live-actions' },
|
|
650
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: !canCopy, onClick: copyAll },
|
|
651
|
+
liveCopied ? t('compat.live.copied') : t('compat.live.copy')))),
|
|
652
|
+
live.hasAgent === false
|
|
653
|
+
? React.createElement('div', { className: 'dsm-empty' }, t('compat.live.noAgent'))
|
|
654
|
+
: React.createElement('div', { className: 'dsm-compat-mod-list dsm-compat-cards' },
|
|
655
|
+
(live.domains || []).map(function (row) {
|
|
656
|
+
const st = stateOf(row)
|
|
657
|
+
const open = liveOpen === row.key
|
|
658
|
+
const canOpen = hasText(row)
|
|
659
|
+
return React.createElement('div', { className: 'dsm-compat-mod-row', key: row.key },
|
|
660
|
+
React.createElement('span', { className: 'dsm-compat-name' },
|
|
661
|
+
canOpen
|
|
662
|
+
? React.createElement('button', { type: 'button', className: 'dsm-inject-live-toggle', onClick: function () { setLiveOpen(open ? '' : row.key) } },
|
|
663
|
+
(open ? '▾ ' : '▸ ') + domainLabel(row.key))
|
|
664
|
+
: React.createElement('span', null, domainLabel(row.key)),
|
|
665
|
+
canOpen
|
|
666
|
+
? React.createElement('span', { className: 'dsm-inject-live-size' }, '· ' + fmtSize(row.bytes) + ' · ' + t('compat.live.est', { count: estTokens(row.text) }))
|
|
667
|
+
: null),
|
|
668
|
+
React.createElement('span', { className: 'dsm-compat-pill' + st.cls, title: st.title || undefined }, st.text),
|
|
669
|
+
open && canOpen ? React.createElement('pre', { className: 'dsm-code dsm-compat-mod-self' }, row.text) : null)
|
|
670
|
+
}))))
|
|
671
|
+
}
|
|
417
672
|
if (error) body.push(React.createElement('div', { className: 'dsm-feedback dsm-error' }, t('compat.failed') + ': ' + error))
|
|
418
673
|
if (!busy && !data && !error) body.push(React.createElement('div', { className: 'dsm-empty' }, t('compat.empty')))
|
|
419
674
|
|
|
@@ -521,10 +776,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
521
776
|
React.createElement('code', { className: 'dsm-compat-code' }, ' node scripts/doctor.mjs')))
|
|
522
777
|
}
|
|
523
778
|
|
|
524
|
-
//
|
|
525
|
-
// 预设**下到不到得了模型。回答的是"面板标着已注入,模型真的看得到吗"
|
|
526
|
-
//
|
|
527
|
-
//
|
|
779
|
+
// 预设注入边界:五类内容(记忆 / 全局提示词 / 技能 / MCP / 子智能体)在**每个 Agent
|
|
780
|
+
// 预设**下到不到得了模型。回答的是"面板标着已注入,模型真的看得到吗"——本插件的
|
|
781
|
+
// 注入由它自己在每步发出(极简这类预设下默认不发,可用上面的「注入」块强制),
|
|
782
|
+
// 而全局提示词与技能目录还要看官方那两条行挂没挂(极简都没挂)。单独一块:取不到时
|
|
783
|
+
// 这一节不显示,不影响上面的结论。
|
|
528
784
|
//
|
|
529
785
|
// 排版口径(用户裁定 2026-09-15):每格一枚描边标签,**绿框=能用、红框=不能用、
|
|
530
786
|
// 灰框=判断不了**;不再出现 ✓/✗ 符号,也不再有任何整行的解释文字 —— 预设损坏与
|
|
@@ -540,21 +796,31 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
540
796
|
return React.createElement('span', { className: 'dsm-reach-chip dsm-reach-' + kind, key: label }, label)
|
|
541
797
|
}
|
|
542
798
|
const tags = []
|
|
799
|
+
if (row.suppressing) tags.push(React.createElement('span', { className: 'dsm-tag dsm-tag-off', key: 'suppressing' }, t('compat.reach.suppressing')))
|
|
543
800
|
if (row.broken) tags.push(React.createElement('span', { className: 'dsm-tag dsm-tag-off', key: 'broken' }, t('compat.reach.broken')))
|
|
544
801
|
else if (row.reason) tags.push(React.createElement('span', { className: 'dsm-tag', key: 'reason' }, t('compat.reach.whyUnknown')))
|
|
545
802
|
return React.createElement('div', { className: 'dsm-compat-mod-row', key: row.presetId },
|
|
546
803
|
React.createElement('span', { className: 'dsm-compat-name dsm-reach-name' },
|
|
547
804
|
React.createElement('span', null, (row.name || row.presetId) + (row.isDefault ? ' · ' + t('compat.reach.default') : '')),
|
|
548
805
|
tags.length ? React.createElement('span', { className: 'dsm-tags' }, tags) : null),
|
|
806
|
+
// 芯片顺序与「注入」块的勾选顺序一致(用户裁定 2026-09-16):
|
|
807
|
+
// 场景和记忆 → MCP → 技能 → 子智能体 → 提示词。
|
|
549
808
|
React.createElement('span', { className: 'dsm-compat-reach-marks' },
|
|
550
809
|
chip(t('compat.reach.mark.memory'), row.memory),
|
|
551
|
-
chip(t('compat.reach.mark.agentsMd'), row.agentsMd),
|
|
552
|
-
chip(t('compat.reach.mark.skill'), row.skillCatalog),
|
|
553
810
|
chip(t('compat.reach.mark.mcp'), row.mcp),
|
|
554
|
-
chip(t('compat.reach.mark.
|
|
811
|
+
chip(t('compat.reach.mark.skill'), row.skillCatalog),
|
|
812
|
+
chip(t('compat.reach.mark.subagent'), row.subagent),
|
|
813
|
+
chip(t('compat.reach.mark.agentsMd'), row.agentsMd)))
|
|
555
814
|
}))))
|
|
556
815
|
|
|
557
|
-
|
|
816
|
+
// 页脚用「标题 + 条目」清单,与导入弹窗的「文件要求」同款结构与样式(用户裁定
|
|
817
|
+
// 2026-09-16 第二版:条目要短、不列路径与内部机制,使用者知道"有这回事"就够)。
|
|
818
|
+
push(React.createElement('div', { className: 'dsm-upload-requirements' },
|
|
819
|
+
React.createElement('div', { className: 'dsm-label' }, t('compat.reach.tools.title')),
|
|
820
|
+
React.createElement('ul', null,
|
|
821
|
+
[1, 2, 3].map(function (n) {
|
|
822
|
+
return React.createElement('li', { key: n }, t('compat.reach.tools.' + n))
|
|
823
|
+
}))))
|
|
558
824
|
}
|
|
559
825
|
|
|
560
826
|
return React.createElement('div', { className: 'dsm-compat' },
|
|
@@ -564,8 +830,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
564
830
|
React.createElement('h2', { className: 'dsm-title' }, t('compat.title'))),
|
|
565
831
|
React.createElement('p', { className: 'dsm-desc' }, t('compat.desc')))),
|
|
566
832
|
React.createElement('div', { className: 'dsm-actions' },
|
|
567
|
-
|
|
568
|
-
busy ? t('btn.refreshing') : t('btn.refresh'))),
|
|
833
|
+
refreshButton(t, busy, { className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: reload })),
|
|
569
834
|
body)
|
|
570
835
|
}
|
|
571
836
|
|
|
@@ -580,7 +845,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
580
845
|
const DICT = {
|
|
581
846
|
zh: {
|
|
582
847
|
"title": "技能", "desc": "管理技能:启停、导入、创建、回收站。", "link.project": "GitHub", "link.feedback": "问题反馈",
|
|
583
|
-
"btn.create": "新增技能", "btn.import": "导入技能", "btn.refresh": "刷新", "btn.refreshing": "刷新中…", "btn.cancel": "取消", "btn.close": "关闭", "export.hint": "导出为 zip,写入你指定的目录;只读源文件,不改动任何数据。", "export.outDir": "导出目录", "export.outDir.placeholder": "例如 D:////backup", "export.pickDir": "选择导出目录", "export.submit": "导出", "export.busy": "导出中…", "export.done": "已导出 {count} 个文件到 {path}", "scenes.lock.lock": "锁定", "scenes.lock.unlock": "解锁", "scenes.lock.tag": "已锁定", "scenes.lock.hint": "锁定后五个管理域(MCP / 技能 / 子智能体 / 记忆 / 提示词)整体只读;场景启停不受影响", "scenes.lock.blockedEdit": "场景已锁定:先解锁再修改", "scenes.lock.notActive": "场景未启动:先启动再锁定", "scenes.lock.blockedExit": "场景已锁定:先解锁再关闭", "lock.banner": "有场景处于锁定状态:MCP / 技能 / 子智能体 / 记忆 / 提示词已整体冻结,先到场景页解锁再修改。", "export.empty": "没有可导出的条目", "export.pickAll": "全选", "export.pickNone": "
|
|
848
|
+
"btn.create": "新增技能", "btn.import": "导入技能", "btn.refresh": "刷新", "btn.refreshing": "刷新中…", "btn.cancel": "取消", "btn.close": "关闭", "export.hint": "导出为 zip,写入你指定的目录;只读源文件,不改动任何数据。", "export.outDir": "导出目录", "export.outDir.placeholder": "例如 D:////backup", "export.pickDir": "选择导出目录", "export.submit": "导出", "export.busy": "导出中…", "export.done": "已导出 {count} 个文件到 {path}", "scenes.lock.lock": "锁定", "scenes.lock.unlock": "解锁", "scenes.lock.tag": "已锁定", "scenes.lock.hint": "锁定后五个管理域(MCP / 技能 / 子智能体 / 记忆 / 提示词)整体只读;场景启停不受影响", "scenes.lock.blockedEdit": "场景已锁定:先解锁再修改", "scenes.lock.notActive": "场景未启动:先启动再锁定", "scenes.lock.blockedExit": "场景已锁定:先解锁再关闭", "lock.banner": "有场景处于锁定状态:MCP / 技能 / 子智能体 / 记忆 / 提示词已整体冻结,先到场景页解锁再修改。", "scene.switch.banner": "当前处于场景「{scene}」:这里的开关照常可用,改动会同步写进该场景的档案(改完立即生效、下次进这个场景照旧生效;退出场景仍按进场景前的状态还原)。", "scene.sync.failed": "注意:改动已生效,但没能同步写进场景档案({error})——本次进场景期间有效,重新进这个场景时会按旧档案应用。", "agm.apply.rebindScene": "场景「{scene}」接管中:应用这一份会把该场景改绑到它(同步写入场景档案)", "export.empty": "没有可导出的条目", "export.pickAll": "全选", "export.pickNone": "取消全选", "export.group.count": "{count} 项", "export.missing": ";跳过 {count} 项(文件不存在):{names}", "export.skills": "导出技能", "export.subagents": "导出子智能体", "export.presets": "导出提示词", "export.memories": "导出记忆", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用", "btn.open.editor": "用系统编辑器打开", "btn.activate": "启用这个", "btn.activate.title": "把当前来源的版本设为同名技能的首选并启用(不改动任何源文件)", "btn.unprefer": "取消首选", "btn.unprefer.title": "取消同名首选,回到按来源优先级自动选择",
|
|
584
849
|
"btn.custom.add": "添加目录", "btn.custom.remove": "移除", "btn.openDir": "选择",
|
|
585
850
|
"btn.source.remove": "移除来源", "btn.source.remove.title": "不再读取这个来源(源文件不动,之后可在回收站里恢复)", "btn.source.restore": "恢复", "btn.source.forget": "永久删除",
|
|
586
851
|
"trash.section.dirs": "目录", "trash.section.skills": "技能", "trash.section.dirs.sub": "只是不再读取,磁盘文件一个字节都不动", "trash.section.dirs.empty": "没有被移除的来源", "trash.section.skills.sub": "文件真的被移走了:恢复能还原,永久删除会删掉文件", "trash.section.presets.sub": "预设目录已移走:恢复能还原,永久删除会删掉文件", "trash.section.scenes.sub": "场景记录、档案与它的记忆都在这里:恢复会整条放回", "trash.section.agents.sub": "人设文件已移走:恢复能还原,永久删除会删掉文件",
|
|
@@ -606,7 +871,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
606
871
|
"empty.search": "没有匹配的技能", "empty.source": "该来源不存在或暂无技能", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
|
|
607
872
|
"source.external.note": "只读接入,启停不改写源文件", "source.dsh.note": "可创建、导入、移到回收站;默认来源必须读取,不能移除或停用",
|
|
608
873
|
"detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "元数据", "detail.noFrontmatter": "该技能未提供元数据。", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
|
|
609
|
-
"create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话里
|
|
874
|
+
"create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话里 skill_manager_create 建的是用户级技能",
|
|
610
875
|
"import.title": "导入技能", "upload.drop.title": "点击或拖入此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.requirement.persona.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.persona.2": "一个 .md = 一个人设,文件名即人设名", "upload.requirement.persona.3": "同名自动跳过,绝不覆盖原有文件", "upload.requirement.memory.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.memory.2": "一个 .md = 一条记忆,文件名即记忆名;带 SKILL.md 的目录 = bundle 记忆(附件一起进来)", "upload.requirement.memory.3": "zip 里的目录名即场景;没有目录时用下面「导入到场景」的值(留空 = 全局)", "upload.requirement.prompt.1": "支持 .md(可多选、可拖入)", "upload.requirement.prompt.2": "一个 .md = 一份预设,文件名即 id(目录名)", "upload.requirement.prompt.3": "同名已存在则跳过,绝不覆盖", "upload.requirement.session.1": "支持 .jsonl / .json / .md / .txt(一次一个)", "upload.requirement.session.2": "Claude Code / Cursor / Codex 的转录文件,或任意文本", "upload.requirement.session.3": "项目目录可留空 = 归入未分组;留空不影响导入", "upload.importing": "正在导入…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md", "select.folder.invalid": "该文件夹里没有 SKILL.md", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
|
|
611
876
|
"trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
|
|
612
877
|
"trash.btn.open": "回收站", "trash.items.count": "{count} 项", "trash.purge.confirm": "永久删除?",
|
|
@@ -632,9 +897,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
632
897
|
"mcp.btn.new": "新增 MCP", "mcp.btn.add": "添加", "mcp.btn.save": "保存", "mcp.btn.detail": "详情", "mcp.btn.edit": "编辑", "mcp.btn.restart": "重启", "mcp.btn.remove": "删除",
|
|
633
898
|
"mcp.btn.enableAll": "全部启用", "mcp.btn.disableAll": "全部停用",
|
|
634
899
|
"mcp.loading": "正在加载 MCP 服务…", "mcp.empty": "暂无 MCP,点「新增 MCP」添加", "mcp.empty.search": "没有匹配的服务。",
|
|
635
|
-
"mcp.servers.count": "{count} 个服务", "mcp.tools.count": "{count} 个工具", "mcp.duplicate": "重复 id",
|
|
900
|
+
"mcp.servers.count": "{count} 个服务", "mcp.tools.count": "{count} 个工具", "mcp.tools.countPartial": "{enabled}/{total} 个工具", "mcp.duplicate": "重复 id",
|
|
636
901
|
"mcp.table.name": "服务名称与地址", "mcp.table.transport": "传输与工具", "mcp.table.status": "运行状态",
|
|
637
|
-
"mcp.live.notLoaded": "未加载", "mcp.live.failed": "启动失败", "mcp.live.stopped": "未运行", "mcp.live.loading": "加载中", "mcp.live.noTools": "无工具", "mcp.live.running": "
|
|
902
|
+
"mcp.live.notLoaded": "未加载", "mcp.live.failed": "启动失败", "mcp.live.stopped": "未运行", "mcp.live.loading": "加载中", "mcp.live.noTools": "无工具", "mcp.live.running": "已运行",
|
|
638
903
|
"mcp.live.failedHint": "启动失败,检查配置后点「重启」重试", "mcp.live.noToolsHint": "已连接但没有工具:服务端可能未就绪",
|
|
639
904
|
"mcp.note.prefix": "备注:", "mcp.toggleServer": "启停服务", "mcp.toggleTool": "启停工具",
|
|
640
905
|
"mcp.msg.ok": "操作成功", "mcp.msg.failed": "操作失败", "mcp.msg.loadFailed": "加载失败", "mcp.msg.warn": "操作完成,但加载器有提示:{warning}",
|
|
@@ -653,8 +918,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
653
918
|
"mcp.btn.compact": "整理补丁", "mcp.btn.compact.title": "清掉补丁文件里已经不起作用的旧开关记录(每台服务的启用/停用状态不变)",
|
|
654
919
|
"mcp.compact.title": "整理补丁文件", "mcp.compact.desc": "配置里会积累一些已经不起作用的旧开关记录(同一个服务被反复启用/停用留下的)。",
|
|
655
920
|
"mcp.compact.confirm": "确认整理", "mcp.compact.done": "补丁文件已整理:清掉 {count} 条失效的旧开关记录,服务的启用/停用状态未变", "mcp.compact.clean": "补丁文件已经很干净,没有可清理的旧记录",
|
|
656
|
-
"mcp.detail.entryId": "条目 ID", "mcp.detail.status": "运行状态", "mcp.detail.
|
|
657
|
-
"mcp.
|
|
921
|
+
"mcp.detail.entryId": "条目 ID", "mcp.detail.status": "运行状态", "mcp.detail.loader": "Loader 登记", "mcp.detail.loader.yes": "已登记", "mcp.detail.loader.no": "未登记",
|
|
922
|
+
"mcp.tools.stale": "上次运行时", "mcp.tools.expand": "展开", "mcp.tools.collapse": "收起",
|
|
923
|
+
"mcp.detail.note": "备注(随上下文注入发给模型;压制型预设下不注入时,用 mcp_manager_list 仍能读到)", "mcp.detail.note.placeholder": "例如:A 不可用时改用 B 兜底", "mcp.detail.note.save": "保存备注",
|
|
658
924
|
"mcp.detail.tools": "工具({count})",
|
|
659
925
|
"mcp.field.headersShort": "请求头", "mcp.field.envShort": "环境变量",
|
|
660
926
|
"mcp.tools.loading": "正在获取工具列表…", "mcp.tools.loadFailed": "加载工具失败:", "mcp.tools.none": "该服务暂无已注册工具。",
|
|
@@ -682,11 +948,29 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
682
948
|
"compat.fallback.native": "改走原生入口", "compat.fallback.blocked": "相关按钮已禁用",
|
|
683
949
|
"compat.modules": "模块实体(插件 vs 宿主)", "compat.modules.hint": "同一份模块才谈得上适配", "compat.module.same": "同一份模块", "compat.module.separate": "两份拷贝(需修复)", "compat.module.unknown": "无法比较",
|
|
684
950
|
"compat.blockers": "阻塞项(按此修复)",
|
|
685
|
-
"compat.reach": "预设注入边界", "compat.reach.hint": "
|
|
686
|
-
"compat.reach.
|
|
951
|
+
"compat.reach": "预设注入边界", "compat.reach.hint": "每个预设下这些内容到不到得了模型",
|
|
952
|
+
"compat.reach.suppressing": "压制型",
|
|
953
|
+
"compat.inject": "注入", "compat.inject.hint": "本插件注入给模型的内容(不变不重发)",
|
|
954
|
+
"compat.inject.force": "极简模式也注入", "compat.inject.force.desc": "默认关(跟随预设)",
|
|
955
|
+
"compat.inject.domains": "注入内容",
|
|
956
|
+
"compat.inject.domain.memory": "场景和记忆", "compat.inject.domain.mcp": "MCP",
|
|
957
|
+
"compat.inject.domain.subagents": "子智能体", "compat.inject.domain.prompt": "提示词",
|
|
958
|
+
"compat.inject.domain.skills": "技能",
|
|
959
|
+
"compat.live.title": "注入实况", "compat.live.hint": "模型当前实际看到的内容(只读)",
|
|
960
|
+
"compat.live.never": "本次运行还没有投递", "compat.live.delivered": "已投递 {count} 条 · 最近 {time}",
|
|
961
|
+
"compat.live.copy": "复制全文", "compat.live.copied": "已复制", "compat.live.noAgent": "还没有会话:发一条消息后再来看",
|
|
962
|
+
"compat.live.state.inContext": "在上下文中", "compat.live.state.absent": "未投递", "compat.live.state.cleared": "已清空",
|
|
963
|
+
"compat.live.state.off": "已关闭", "compat.live.state.unknown": "没有会话",
|
|
964
|
+
"compat.live.state.official": "官方注入", "compat.live.state.official.title": "提示词与技能目录在标准类预设下由官方通道送达,本插件不重复注入(极简这类官方没挂的预设才由本插件兜底)",
|
|
965
|
+
"compat.live.state.empty": "无内容", "compat.live.est": "≈{count} token",
|
|
966
|
+
"compat.live.state.absent.title": "本插件负责这个域,但当前不在上下文里(可能刚被压缩,下一步会自动补发)",
|
|
967
|
+
"compat.reach.mark.memory": "场景和记忆", "compat.reach.mark.agentsMd": "提示词", "compat.reach.mark.skill": "技能", "compat.reach.mark.mcp": "MCP", "compat.reach.mark.subagent": "子智能体",
|
|
687
968
|
"compat.reach.default": "默认",
|
|
688
969
|
"compat.reach.whyUnknown": "判断不了", "compat.reach.broken": "预设损坏",
|
|
689
|
-
"compat.reach.tools": "
|
|
970
|
+
"compat.reach.tools.title": "工具能用 ≠ 模型知道",
|
|
971
|
+
"compat.reach.tools.1": "工具由本插件注册,任何预设下都能调用;怎么送到模型由预设决定。",
|
|
972
|
+
"compat.reach.tools.2": "场景和记忆 / MCP / 技能 / 子智能体 / 提示词 —— 由本插件按上面的「注入」块补齐。",
|
|
973
|
+
"compat.reach.tools.3": "提示词与技能官方自己也会送;官方有的不重复送,官方缺的(如极简)由本插件补上。",
|
|
690
974
|
"compat.hint.doctor": "命令行体检:",
|
|
691
975
|
"scenes.title": "场景", "scenes.desc": "管理场景:预设 MCP、技能、子智能体与记忆的组合,一键切换。",
|
|
692
976
|
"scenes.stat.total": "个场景", "scenes.stat.active": "个已启用", "scenes.stat.archives": "个有档案",
|
|
@@ -695,25 +979,32 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
695
979
|
"scenes.empty": "还没有专门设置的场景;全局记忆不需要场景就能注入,需要切换 MCP / 技能 / 人设时再新建。",
|
|
696
980
|
"scenes.field.desc.limit": "最多 {count} 字;超出部分在卡片上省略。",
|
|
697
981
|
"scenes.profile.mcp": "MCP {count} 台", "scenes.profile.skills": "技能 {count} 个", "scenes.profile.subagents": "子智能体 {count} 个", "scenes.profile.memories": "记忆 {count} 条",
|
|
698
|
-
"scenes.mcp.hint": "添加「MCP
|
|
699
|
-
"scenes.seg.
|
|
700
|
-
"
|
|
982
|
+
"scenes.mcp.hint": "添加「MCP 工具集」后,勾选要启用的服务器(含未运行的);「选工具」可细化到具体工具。没勾的、以及整段没建的,进场景时一律停用(含停掉正在运行的进程)。",
|
|
983
|
+
"scenes.seg.checked": "{checked}/{total} 已勾选",
|
|
984
|
+
"bulk.selectAll": "全选", "bulk.unselectAll": "取消全选",
|
|
985
|
+
"bulk.head.title": "对当前筛选后可见、且可切换的技能批量启停(被同名覆盖的副本不算)",
|
|
986
|
+
"source.bulk.title": "对这个目录下当前可见、且可切换的技能批量启停(被同名覆盖的副本不算)",
|
|
987
|
+
"bulk.enabled": "已启用", "bulk.disabled": "已停用",
|
|
988
|
+
"bulk.done.skills": "{action} {count} 个技能(失败 {failed})", "bulk.done.memories": "{action} {count} 条记忆(失败 {failed})", "memory.bulk.partial": "{action} {count} 条记忆后中止:{error}",
|
|
989
|
+
"scenes.archive.sectionOff": "未定义 = 全部停用",
|
|
701
990
|
"scenes.archive.summary": "档案:MCP {mcp} 台 · 技能 {skills} 个 · 子智能体 {subagents} 个",
|
|
702
|
-
"scenes.archive.note": "
|
|
991
|
+
"scenes.archive.note": "勾选集语义:勾的启用、没勾的停用;段未定义 = 一个都没勾 = 该域全部停用。MCP 会真的启停服务器进程,技能会启停它所在的来源。退出场景时按进场景前的状态精确还原。",
|
|
703
992
|
"scenes.mcp.noServers": "还没有可选的 MCP 服务器", "scenes.mcp.toolCount": "{count} 个工具", "scenes.skills.empty": "还没有可选的技能",
|
|
704
993
|
"scenes.mcp.allTools": "全部工具", "scenes.mcp.pickedCount": "指定 {count} 个工具", "scenes.mcp.notRunning": "未运行", "scenes.mcp.edit": "编辑", "scenes.mcp.done": "完成", "scenes.mcp.noteLabel": "场景备注", "scenes.mcp.notePlaceholder": "进入该场景时覆盖全局备注;退出后恢复", "scenes.mcp.hasNote": "有备注",
|
|
705
|
-
"scenes.mcp.toolsOf": "选择工具", "scenes.mcp.probeTools": "启动服务器读取工具", "scenes.mcp.unknownCount": "工具数未知 · 点「编辑」读取", "scenes.mcp.noteHint": "此备注随场景保存;仅在该场景本段**勾选**了这台服务器时,进入场景才会覆盖「MCP」页与系统提示词里的备注,退出后自动恢复。", "scenes.mcp.noTools": "该服务器当前没有可列出的工具(从未运行过,或已配置的工具尚未被记录)", "scenes.mcp.drillHint": "勾选 = 该场景下启用;不勾 = 停用。「全选」=
|
|
706
|
-
"scenes.skills.hint": "
|
|
994
|
+
"scenes.mcp.toolsOf": "选择工具", "scenes.mcp.probeTools": "启动服务器读取工具", "scenes.mcp.unknownCount": "工具数未知 · 点「编辑」读取", "scenes.mcp.noteHint": "此备注随场景保存;仅在该场景本段**勾选**了这台服务器时,进入场景才会覆盖「MCP」页与系统提示词里的备注,退出后自动恢复。", "scenes.mcp.noTools": "该服务器当前没有可列出的工具(从未运行过,或已配置的工具尚未被记录)", "scenes.mcp.drillHint": "勾选 = 该场景下启用;不勾 = 停用。「全选」= 全部工具(再点一次成「取消全选」= 全部停用)。",
|
|
995
|
+
"scenes.skills.hint": "添加「技能集」后,勾选该场景下启用的技能。没勾的、以及整段没建的,进场景时一律停用(退出按进场景前的开关还原)。被同名技能覆盖的副本与结构不完整的技能不能勾——它们在这个场景里不可能生效。", "scenes.subagents.hint": "勾选本场景要打开的人设;没勾的、以及整段没建的,进场景时一律关闭(退出按进场景前的开关还原)。", "scenes.subagents.empty": "还没有人设——到「子智能体」页创建。",
|
|
996
|
+
"scenes.skills.shadowed.hint": "被同名技能覆盖,在这个场景里不可能生效,因此不能勾选。要改用这一份,请到「技能」页点它的「启用这个」。",
|
|
997
|
+
"scenes.skills.invalid.hint": "技能结构不完整(缺 frontmatter / 名字非法 / 描述为空),无法启停,因此不能勾选。",
|
|
707
998
|
"scenes.filter.skills": "筛选技能/目录名称", "scenes.filter.subagents": "筛选人设(名称或描述)", "scenes.filter.servers": "筛选服务器",
|
|
708
999
|
|
|
709
1000
|
"scenes.mem.title": "记忆", "scenes.mem.search": "在本场景内筛选记忆(名称或描述)",
|
|
710
1001
|
"scenes.mem.noMatch": "没有匹配项", "scenes.mem.emptyScene": "该场景还没有记忆",
|
|
711
1002
|
"memory.mode.current": "当前模式", "memory.mode.exit": "退出模式",
|
|
712
1003
|
"memory.archive.edit": "档案", "memory.archive.title": "场景档案",
|
|
713
|
-
"memory.archive.tools": "MCP 工具集", "memory.archive.skills": "技能集", "memory.archive.subagents": "子智能体绑定", "memory.archive.memories": "记忆",
|
|
1004
|
+
"memory.archive.tools": "MCP 工具集", "memory.archive.skills": "技能集", "memory.archive.subagents": "子智能体绑定", "memory.archive.memories": "记忆", "memory.archive.applyFailed": "运行时未能应用:{reason}",
|
|
714
1005
|
"memory.archive.removeSection": "移除段", "memory.archive.save": "保存到场景",
|
|
715
1006
|
"memory.archive.addTools": "+ 添加 MCP 工具集", "memory.archive.addSkills": "+ 添加技能集", "memory.archive.addSubagents": "+ 添加子智能体绑定",
|
|
716
|
-
"memory.archive.stale": "
|
|
1007
|
+
"memory.archive.stale": "已跳过 {items}(已不存在,或被同名技能覆盖 / 结构不完整——本就不生效)",
|
|
717
1008
|
"memory.result.archiveSaved": "已保存场景档案:{name}", "memory.result.modeSet": "已进入模式:{name}", "memory.result.modeSwitched": "已切换 {mcp} 台服务器 / {skills} 个来源", "memory.result.modeExited": "已退出模式",
|
|
718
1009
|
"memory.desc": "管理记忆:启用的记忆进入系统提示词,可按场景分别启用。",
|
|
719
1010
|
"memory.btn.refresh": "刷新", "memory.btn.new": "新增记忆", "memory.btn.create": "创建", "memory.btn.save": "保存",
|
|
@@ -728,7 +1019,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
728
1019
|
"memory.scene.enable": "启用", "memory.scene.enableAll": "全部启用", "memory.scene.off": "未启用", "memory.scene.shared": "常开", "memory.scene.global.tag": "常驻",
|
|
729
1020
|
"memory.scene.global": "全局", "memory.scene.count": "{count} 条记忆", "memory.scene.new": "新建记忆", "memory.scene.browse": "选择已有",
|
|
730
1021
|
"memory.scene.empty": "该场景暂无记忆",
|
|
731
|
-
"memory.scene.orphan": "未归属场景", "memory.scene.orphan.tag": "不会注入", "memory.scene.orphan.hint": "这些记忆直接放在 memories/
|
|
1022
|
+
"memory.scene.orphan": "未归属场景", "memory.scene.orphan.tag": "不会注入", "memory.scene.orphan.hint": "这些记忆直接放在 memories/ 根层,没有归属场景,因此不会被注入上下文。请把它们移入某个场景目录(或放到 memories/global/ 作为「全局」记忆)。",
|
|
732
1023
|
"memory.scene.edit": "改描述", "memory.scene.editTitle": "修改场景", "memory.scene.editHelp": "名称就是它的记忆目录名;改名会连目录、档案与绑定一起改,记忆正文不动。",
|
|
733
1024
|
"memory.scene.field.name": "场景名", "memory.scene.field.name.placeholder": "例如 办公",
|
|
734
1025
|
"memory.scene.field.name.hint": "名称就是它的一级目录名,之后仍可修改。",
|
|
@@ -747,7 +1038,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
747
1038
|
"memory.field.body": "正文(Markdown)", "memory.field.body.placeholder": "写下要记住的约定、流程和边界…",
|
|
748
1039
|
"memory.field.form": "形态", "memory.field.attach": "附件",
|
|
749
1040
|
"memory.attach.add": "添加附件", "memory.attach.empty": "暂无附件", "memory.attach.pending": "待上传", "memory.attach.remove": "移除附件",
|
|
750
|
-
"memory.attach.hint": "附件随记忆存放,不会进入提示词",
|
|
1041
|
+
"memory.attach.hint": "附件随记忆存放,不会进入提示词",
|
|
1042
|
+
"memory.attach.count.one": "{count} 个附件", "memory.attach.count.other": "{count} 个附件",
|
|
1043
|
+
"memory.attach.tip": "点击管理附件:{count} 个 · 共 {size} —— {names}",
|
|
1044
|
+
"memory.attach.tip.empty": "还没有附件——点击添加",
|
|
1045
|
+
"memory.attach.more": ",另 {count} 个", "memory.attach.sep": "、", "memory.attach.flat": "flat 是单文件,不能带附件", "memory.attach.tooLarge": "附件过大:{name}(上限 {limit} MB)",
|
|
751
1046
|
"memory.delete.title": "移入回收站?", "memory.delete.desc": "「{name}」将移入回收站,可恢复",
|
|
752
1047
|
"memory.result.created": "已创建记忆:{name}", "memory.result.updated": "已保存记忆:{name}", "memory.result.removed": "已移入回收站:{name}", "memory.result.toggled": "已更新记忆状态:{name}", "memory.result.active": "已更新启用场景",
|
|
753
1048
|
"memory.result.restored": "已恢复记忆:{name}", "memory.result.trashRemoved": "已永久删除:{name}",
|
|
@@ -761,8 +1056,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
761
1056
|
"memory.deleteScene.title": "删除场景?", "memory.deleteScene.desc": "把「{name}」连同它下面全部记忆(含未启用的)一起移入回收站,可在回收站整条恢复;正在使用的场景不能删。",
|
|
762
1057
|
"memory.result.sceneCreated": "已创建场景:{name}", "memory.result.sceneUpdated": "已保存场景:{name}", "memory.result.sceneRemoved": "已删除场景:{name}", "memory.result.sceneRemoved.withMemories": "已删除场景:{name}(连同 {count} 项记忆,可在回收站整条恢复)",
|
|
763
1058
|
"memory.table.name": "记忆名称与描述", "memory.table.tags": "标记", "memory.table.status": "状态",
|
|
764
|
-
"error.rules.invalidArgs": "缺少参数:需要 all:true(回到全部启用)或 scenes:[...](显式收窄)", "error.rules.invalidGroup": "场景/分组名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.invalidName": "记忆名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.descriptionRequired": "描述不能为空", "error.rules.descriptionTooLong": "描述过长(不能超过 500 字符)", "error.rules.bodyRequired": "正文不能为空", "error.rules.tooLarge": "规则内容过大", "error.rules.shadowed": "规则被同名 bundle 遮蔽,无法写入", "error.rules.notFound": "规则不存在", "error.rules.budgetExceeded": "场景记忆段超出预算", "error.rules.ioFailed": "文件操作失败", "error.rules.nameTaken": "目标名称已被占用", "error.rules.sceneNotEmpty": "场景不为空,无法删除", "error.rules.notBundle": "该记忆是 flat(单文件),不能带附件", "error.rules.noFiles": "没有选择附件", "error.rules.emptyFile": "附件内容为空", "error.rules.fileTooLarge": "附件过大(单个上限 {limit} MB)", "error.rules.tooManyFiles": "一次最多 {limit} 个附件", "error.rules.sceneInMode": "该场景正处在当前模式,请先退出模式再删除",
|
|
765
|
-
"memory.archive.emptySection": "
|
|
1059
|
+
"error.rules.invalidArgs": "缺少参数:需要 all:true(回到全部启用)或 scenes:[...](显式收窄)", "error.rules.invalidGroup": "场景/分组名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.invalidName": "记忆名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.descriptionRequired": "描述不能为空", "error.rules.descriptionTooLong": "描述过长(不能超过 500 字符)", "error.rules.bodyRequired": "正文不能为空", "error.rules.tooLarge": "规则内容过大", "error.rules.shadowed": "规则被同名 bundle 遮蔽,无法写入", "error.rules.exists": "同名记忆已存在,无法写入", "error.rules.notFound": "规则不存在", "error.rules.budgetExceeded": "场景记忆段超出预算", "error.rules.ioFailed": "文件操作失败", "error.rules.nameTaken": "目标名称已被占用", "error.rules.sceneNotEmpty": "场景不为空,无法删除", "error.rules.notBundle": "该记忆是 flat(单文件),不能带附件", "error.rules.noFiles": "没有选择附件", "error.rules.emptyFile": "附件内容为空", "error.rules.fileTooLarge": "附件过大(单个上限 {limit} MB)", "error.rules.tooManyFiles": "一次最多 {limit} 个附件", "error.rules.sceneInMode": "该场景正处在当前模式,请先退出模式再删除",
|
|
1060
|
+
"memory.archive.emptySection": "没有勾选任何条目 = 全部停用(整段没建也一样)", "memory.archive.emptySubagents": "没有勾选任何条目 = 本场景不启用任何人设(整段没建也一样)",
|
|
766
1061
|
"subagents.title": "子智能体", "subagents.stat.total": "个子智能体", "subagents.stat.limited": "个有工具限制", "subagents.desc": "管理子智能体:新建、导入人设,正文即子代理的系统提示词。",
|
|
767
1062
|
"prompts.desc": "管理提示词:预设全局指令基线,应用后写入 AGENTS.md,下一轮对话生效。",
|
|
768
1063
|
"sessions.desc": "管理已归档会话:可恢复或永久删除,超保留期自动清理。",
|
|
@@ -781,6 +1076,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
781
1076
|
"subagents.mode.denyEmpty": "一个都没勾 = 不限制(该模式的全部工具都能用)",
|
|
782
1077
|
"subagents.mode.noTools": "读不到这个模式的工具清单",
|
|
783
1078
|
"subagents.mode.broken": "这个预设已损坏,读不到工具清单",
|
|
1079
|
+
"subagents.mode.toggleHint": "展开 / 收起这个模式的名单设置(右侧按钮负责启用 / 停用)",
|
|
784
1080
|
"subagents.mode.noPresets": "宿主没有回传 Agent 预设名单",
|
|
785
1081
|
"subagents.legacy.note": "这个文件里还有一份旧写法的工具限制(白名单 {allow} 个 / 黑名单 {deny} 个),它对所有模式都生效。",
|
|
786
1082
|
"subagents.legacy.convert": "转换到按模式设置",
|
|
@@ -792,7 +1088,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
792
1088
|
"subagents.field.body": "人设提示词", "subagents.field.body.placeholder": "写下这个人设的身份、职责与工作方式…",
|
|
793
1089
|
"subagents.result.saved": "已保存人设:{name}", "subagents.result.deleted": "已删除人设:{name}",
|
|
794
1090
|
"subagents.delete.title": "删除人设?", "subagents.delete.desc": "将把「{name}」的人设文件移入回收站(子智能体页的「回收站」里可以恢复)。",
|
|
795
|
-
"subagents.import": "导入子智能体", "subagents.import.title": "导入子智能体", "subagents.toggle": "启用子智能体", "subagents.disabled": "已停用", "subagents.disabled.hint": "
|
|
1091
|
+
"subagents.import": "导入子智能体", "subagents.import.title": "导入子智能体", "subagents.toggle": "启用子智能体", "subagents.disabled": "已停用", "subagents.disabled.hint": "停用后不注入上下文,subagent_manager_list / subagent_manager_run 也看不到;文件保留,随时可再打开。",
|
|
796
1092
|
"subagents.result.imported": "已导入 {count} 个人设:{names}",
|
|
797
1093
|
"memory.import": "导入记忆", "memory.import.title": "导入记忆", "memory.import.scene": "导入到场景",
|
|
798
1094
|
"memory.result.imported": "已导入 {count} 条记忆:{names}",
|
|
@@ -802,11 +1098,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
802
1098
|
"import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
|
|
803
1099
|
"error.import.noFiles": "没有选择要导入的文件",
|
|
804
1100
|
"nav.title": "工具",
|
|
805
|
-
"agm.stat.total": "个提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.btn.delete.blocked": "
|
|
1101
|
+
"agm.stat.total": "个提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.btn.delete.blocked.refs": "不能删除:{refs}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。", "agm.ref.sep": ";", "agm.ref.scene": "场景「{scene}」绑定了它", "agm.ref.sceneActive": "场景「{scene}」(已启用)绑定了它", "agm.ref.file": "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.btn.reapply": "重新应用", "agm.btn.apply": "应用", "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.copyFrom": "或从现有预设复制", "agm.field.copyFrom.none": "不复制(空白模板)", "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 代)", "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": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "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": "收敛为单选", "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.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "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.btn.archiveSelected": "归档所选", "hist.btn.archiveSelected.title": "把选中的会话收进 History,纳入保留期管理", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
|
|
806
1102
|
},
|
|
807
1103
|
en: {
|
|
808
1104
|
"title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
|
|
809
|
-
"btn.create": "New skill", "btn.import": "Import skills", "btn.refresh": "Refresh", "btn.refreshing": "Refreshing…", "btn.cancel": "Cancel", "btn.close": "Close", "export.hint": "Exports a zip into the directory you choose; source files are read-only and nothing is modified.", "export.outDir": "Output directory", "export.outDir.placeholder": "e.g. D:////backup", "export.pickDir": "Choose output directory", "export.submit": "Export", "export.busy": "Exporting…", "export.done": "Exported {count} file(s) to {path}", "scenes.lock.lock": "Lock", "scenes.lock.unlock": "Unlock", "scenes.lock.tag": "Locked", "scenes.lock.hint": "Locking makes all five domains (MCP / skills / subagents / memories / prompts) read-only; starting or stopping scenes still works", "scenes.lock.blockedEdit": "Scene is locked — unlock it to make changes", "scenes.lock.notActive": "Scene is not active — start it before locking", "scenes.lock.blockedExit": "Scene is locked — unlock it before turning it off", "lock.banner": "A scene is locked: MCP, skills, subagents, memories and prompts are frozen. Unlock it on the Scenes page to make changes.", "export.empty": "Nothing to export", "export.pickAll": "Select all", "export.pickNone": "
|
|
1105
|
+
"btn.create": "New skill", "btn.import": "Import skills", "btn.refresh": "Refresh", "btn.refreshing": "Refreshing…", "btn.cancel": "Cancel", "btn.close": "Close", "export.hint": "Exports a zip into the directory you choose; source files are read-only and nothing is modified.", "export.outDir": "Output directory", "export.outDir.placeholder": "e.g. D:////backup", "export.pickDir": "Choose output directory", "export.submit": "Export", "export.busy": "Exporting…", "export.done": "Exported {count} file(s) to {path}", "scenes.lock.lock": "Lock", "scenes.lock.unlock": "Unlock", "scenes.lock.tag": "Locked", "scenes.lock.hint": "Locking makes all five domains (MCP / skills / subagents / memories / prompts) read-only; starting or stopping scenes still works", "scenes.lock.blockedEdit": "Scene is locked — unlock it to make changes", "scenes.lock.notActive": "Scene is not active — start it before locking", "scenes.lock.blockedExit": "Scene is locked — unlock it before turning it off", "lock.banner": "A scene is locked: MCP, skills, subagents, memories and prompts are frozen. Unlock it on the Scenes page to make changes.", "scene.switch.banner": "Scene “{scene}” is active: switches here work as usual and are written into that scene’s profile too (they take effect immediately and are kept for the next visit; leaving the scene still restores the state from before).", "scene.sync.failed": "Heads-up: the change took effect, but it could not be written into the scene profile ({error}) — it holds for this visit only; re-entering this scene will apply the older profile.", "agm.apply.rebindScene": "Scene “{scene}” is driving the baseline: applying this preset rebinds that scene to it (written into the scene profile as well)", "export.empty": "Nothing to export", "export.pickAll": "Select all", "export.pickNone": "Deselect all", "export.group.count": "{count}", "export.missing": "; skipped {count} (missing on disk): {names}", "export.skills": "Export skills", "export.subagents": "Export subagents", "export.presets": "Export prompts", "export.memories": "Export memories", "btn.detail": "View details", "btn.trash": "Move to trash", "btn.restore": "Restore", "btn.delete.forever": "Delete forever", "btn.file.pick": "Choose file", "btn.folder.pick": "Choose folder", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable", "btn.open.editor": "Open in editor", "btn.activate": "Use this one", "btn.activate.title": "Make this source's copy the active one for this shared name and enable it (source files are never touched)", "btn.unprefer": "Auto again", "btn.unprefer.title": "Clear the manual choice and go back to automatic source priority",
|
|
810
1106
|
"btn.custom.add": "Add folder",
|
|
811
1107
|
"btn.custom.remove": "Remove",
|
|
812
1108
|
"btn.source.remove": "Remove source", "btn.source.remove.title": "Stop reading this source (files stay on disk; restore it later from the trash)", "btn.source.restore": "Restore", "btn.source.forget": "Delete forever",
|
|
@@ -840,7 +1136,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
840
1136
|
"empty.search": "No matching skills", "empty.source": "This source is missing or has no skills", "loading": "Loading skills…", "note.missing": "No description provided", "source.toggle": "Toggle source", "skill.toggle": "Toggle skill",
|
|
841
1137
|
"source.external.note": "Read-only; toggles never rewrite source files", "source.dsh.note": "Create, import, trash; a default source, always read — it cannot be removed or toggled off",
|
|
842
1138
|
"detail.title": "Skill details", "detail.body": "Body", "detail.frontmatter": "Metadata", "detail.noFrontmatter": "This skill provides no metadata.", "detail.diagnostics": "Diagnostics", "detail.path": "Source file", "detail.noIssues": "No diagnostic issues found.",
|
|
843
|
-
"create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "
|
|
1139
|
+
"create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "skill_manager_create in chat creates user-level skills",
|
|
844
1140
|
"import.title": "Import skill", "upload.drop.title": "Click or drop here", "upload.drop.copy": "Supported: .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.requirement.persona.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.persona.2": "One .md = one persona; the file name is the persona name", "upload.requirement.persona.3": "Same names are skipped, never overwritten", "upload.requirement.memory.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.memory.2": "One .md = one memory; a folder with SKILL.md becomes a bundle memory (attachments included)", "upload.requirement.memory.3": "Folder names inside the zip are scenes; otherwise the scene field below applies (blank = global)", "upload.requirement.prompt.1": "Accepts .md (multi-select or drag in)", "upload.requirement.prompt.2": "One .md = one preset; the file name is the id", "upload.requirement.prompt.3": "A preset with the same name is skipped, never overwritten", "upload.requirement.session.1": "Accepts .jsonl / .json / .md / .txt (one file at a time)", "upload.requirement.session.2": "Transcripts from Claude Code / Cursor / Codex, or any plain text", "upload.requirement.session.3": "The project directory may be left blank (the session goes to Ungrouped)", "upload.importing": "Importing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
|
|
845
1141
|
"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}",
|
|
846
1142
|
"trash.btn.open": "Trash", "trash.items.count": "{count} item(s)", "trash.purge.confirm": "Delete forever?",
|
|
@@ -866,7 +1162,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
866
1162
|
"mcp.btn.new": "New MCP", "mcp.btn.add": "Add", "mcp.btn.save": "Save", "mcp.btn.detail": "Details", "mcp.btn.edit": "Edit", "mcp.btn.restart": "Restart", "mcp.btn.remove": "Delete",
|
|
867
1163
|
"mcp.btn.enableAll": "Enable all", "mcp.btn.disableAll": "Disable all",
|
|
868
1164
|
"mcp.loading": "Loading MCP servers…", "mcp.empty": "No MCP servers yet — click “New MCP”.", "mcp.empty.search": "No matching server.",
|
|
869
|
-
"mcp.servers.count": "{count} server(s)", "mcp.tools.count": "{count} tool(s)", "mcp.duplicate": "duplicate id",
|
|
1165
|
+
"mcp.servers.count": "{count} server(s)", "mcp.tools.count": "{count} tool(s)", "mcp.tools.countPartial": "{enabled}/{total} tool(s)", "mcp.duplicate": "duplicate id",
|
|
870
1166
|
"mcp.table.name": "Server name and URL", "mcp.table.transport": "Transport and tools", "mcp.table.status": "Status",
|
|
871
1167
|
"mcp.live.notLoaded": "not loaded", "mcp.live.failed": "start failed", "mcp.live.stopped": "stopped", "mcp.live.loading": "loading", "mcp.live.noTools": "no tools", "mcp.live.running": "running",
|
|
872
1168
|
"mcp.live.failedHint": "Start failed — check the configuration and click “Restart” to retry", "mcp.live.noToolsHint": "Connected but with no tools: the server may not be ready yet",
|
|
@@ -887,8 +1183,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
887
1183
|
"mcp.btn.compact": "Tidy patch", "mcp.btn.compact.title": "Clear the stale on/off records left in the patch file (each server's enabled/disabled state is unchanged)",
|
|
888
1184
|
"mcp.compact.title": "Tidy the patch file", "mcp.compact.desc": "The config accumulates stale on/off records — leftovers from switching the same server on and off over time. This clears them so the file stays short and readable.",
|
|
889
1185
|
"mcp.compact.confirm": "Tidy now", "mcp.compact.done": "Patch file tidied: cleared {count} stale on/off record(s); no server changed state", "mcp.compact.clean": "The patch file is already clean — nothing stale to clear",
|
|
890
|
-
"mcp.detail.entryId": "Entry id", "mcp.detail.status": "Status", "mcp.detail.
|
|
891
|
-
"mcp.
|
|
1186
|
+
"mcp.detail.entryId": "Entry id", "mcp.detail.status": "Status", "mcp.detail.loader": "Loader registration", "mcp.detail.loader.yes": "Registered", "mcp.detail.loader.no": "Not registered",
|
|
1187
|
+
"mcp.tools.stale": "from last run", "mcp.tools.expand": "Show more", "mcp.tools.collapse": "Show less",
|
|
1188
|
+
"mcp.detail.note": "Note (sent to the model with the injected runtime snapshot; when a suppressing preset keeps it out, mcp_manager_list still reads it back)", "mcp.detail.note.placeholder": "e.g. fall back to B when A is unavailable", "mcp.detail.note.save": "Save note",
|
|
892
1189
|
"mcp.detail.tools": "Tools ({count})",
|
|
893
1190
|
"mcp.field.headersShort": "Headers", "mcp.field.envShort": "Environment",
|
|
894
1191
|
"mcp.tools.loading": "Fetching the tool list…", "mcp.tools.loadFailed": "Could not load tools: ", "mcp.tools.none": "This server has no registered tools.",
|
|
@@ -916,11 +1213,29 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
916
1213
|
"compat.fallback.native": "Falls back to native entry", "compat.fallback.blocked": "Affected buttons disabled",
|
|
917
1214
|
"compat.modules": "Module identity (plugin vs host)", "compat.modules.hint": "adapting only works on one shared module", "compat.module.same": "same module", "compat.module.separate": "separate copy (repair needed)", "compat.module.unknown": "not comparable",
|
|
918
1215
|
"compat.blockers": "Blockers (fix these)",
|
|
919
|
-
"compat.reach": "Preset injection reach", "compat.reach.hint": "
|
|
920
|
-
"compat.reach.
|
|
1216
|
+
"compat.reach": "Preset injection reach", "compat.reach.hint": "whether these contents reach the model under each preset",
|
|
1217
|
+
"compat.reach.suppressing": "suppressing",
|
|
1218
|
+
"compat.inject": "Injection", "compat.inject.hint": "what this plugin injects for the model (unchanged text is not resent)",
|
|
1219
|
+
"compat.inject.force": "Also inject in Minimal mode", "compat.inject.force.desc": "Off by default (follow the preset)",
|
|
1220
|
+
"compat.inject.domains": "What to inject",
|
|
1221
|
+
"compat.inject.domain.memory": "Scene + memory", "compat.inject.domain.mcp": "MCP",
|
|
1222
|
+
"compat.inject.domain.subagents": "Subagents", "compat.inject.domain.prompt": "Prompt",
|
|
1223
|
+
"compat.inject.domain.skills": "Skills",
|
|
1224
|
+
"compat.live.title": "Live injection", "compat.live.hint": "What the model actually sees (read-only)",
|
|
1225
|
+
"compat.live.never": "nothing delivered yet this run", "compat.live.delivered": "{count} delivered · last {time}",
|
|
1226
|
+
"compat.live.copy": "Copy all", "compat.live.copied": "Copied", "compat.live.noAgent": "No session yet — send a message first",
|
|
1227
|
+
"compat.live.state.inContext": "In context", "compat.live.state.absent": "Not delivered", "compat.live.state.cleared": "Cleared",
|
|
1228
|
+
"compat.live.state.off": "Off", "compat.live.state.unknown": "No session",
|
|
1229
|
+
"compat.live.state.official": "Official", "compat.live.state.official.title": "The official carrier delivers these under standard presets; the plugin does not duplicate (it only fills in for Minimal-like presets)",
|
|
1230
|
+
"compat.live.state.empty": "Nothing to send", "compat.live.est": "≈{count} tokens",
|
|
1231
|
+
"compat.live.state.absent.title": "This plugin owns this domain but it is not in context right now (it may have just been compacted; the next step re-sends it)",
|
|
1232
|
+
"compat.reach.mark.memory": "Scene + memory", "compat.reach.mark.agentsMd": "Prompt", "compat.reach.mark.skill": "Skills", "compat.reach.mark.mcp": "MCP", "compat.reach.mark.subagent": "Subagents",
|
|
921
1233
|
"compat.reach.default": "default",
|
|
922
1234
|
"compat.reach.whyUnknown": "cannot tell", "compat.reach.broken": "preset broken",
|
|
923
|
-
"compat.reach.tools": "
|
|
1235
|
+
"compat.reach.tools.title": "Tools work ≠ the model knows",
|
|
1236
|
+
"compat.reach.tools.1": "The tools come from this plugin and are callable under any preset; how they reach the model is up to the preset.",
|
|
1237
|
+
"compat.reach.tools.2": "Scene + memory / MCP / skills / subagents / prompt — filled in by this plugin according to the Injection block above.",
|
|
1238
|
+
"compat.reach.tools.3": "The prompt and the skill catalog also come from the official rows; this plugin only fills in what they miss (e.g. under Minimal).",
|
|
924
1239
|
"compat.hint.doctor": "Command-line check:",
|
|
925
1240
|
"scenes.title": "Scenes", "scenes.desc": "Manage scenes: preset combinations of MCP, skills, subagents and memories, switch with one click.",
|
|
926
1241
|
"scenes.stat.total": "scene(s)", "scenes.stat.active": "enabled", "scenes.stat.archives": "with a profile",
|
|
@@ -929,25 +1244,32 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
929
1244
|
"scenes.empty": "No dedicated scenes yet — global memories are injected without one; create a scene when you need to switch MCP servers, skills or personas.",
|
|
930
1245
|
"scenes.field.desc.limit": "Up to {count} characters; longer text is clipped on the card.",
|
|
931
1246
|
"scenes.profile.mcp": "{count} MCP", "scenes.profile.skills": "{count} skill(s)", "scenes.profile.subagents": "{count} subagent(s)", "scenes.profile.memories": "{count} memor(ies)",
|
|
932
|
-
"scenes.mcp.hint": "Add the MCP section first, then check the servers to enable (stopped ones included); use Pick tools to narrow to specific tools.",
|
|
933
|
-
"scenes.seg.
|
|
934
|
-
"
|
|
1247
|
+
"scenes.mcp.hint": "Add the MCP section first, then check the servers to enable (stopped ones included); use Pick tools to narrow to specific tools. Unchecked servers — and scenes with no section at all — are turned off on entry, running processes included.",
|
|
1248
|
+
"scenes.seg.checked": "{checked}/{total} selected",
|
|
1249
|
+
"bulk.selectAll": "Select all", "bulk.unselectAll": "Deselect all",
|
|
1250
|
+
"bulk.head.title": "Enable or disable every skill that is visible after filtering and can be toggled (copies shadowed by a same-name skill are not counted)",
|
|
1251
|
+
"source.bulk.title": "Enable or disable every visible, toggleable skill in this directory (copies shadowed by a same-name skill are not counted)",
|
|
1252
|
+
"bulk.enabled": "Enabled", "bulk.disabled": "Disabled",
|
|
1253
|
+
"bulk.done.skills": "{action} {count} skill(s) (failed: {failed})", "bulk.done.memories": "{action} {count} memor(ies) (failed: {failed})", "memory.bulk.partial": "{action} {count} memor(ies), then aborted: {error}",
|
|
1254
|
+
"scenes.archive.sectionOff": "not defined = all off",
|
|
935
1255
|
"scenes.archive.summary": "Profile: {mcp} MCP server(s) · {skills} skill(s) · {subagents} subagent(s)",
|
|
936
|
-
"scenes.archive.note": "
|
|
1256
|
+
"scenes.archive.note": "Checked means enabled, unchecked means disabled — and an undefined section means nothing is checked, so that whole domain is off. MCP really starts/stops server processes; skills toggle the source they live in. Leaving the scene restores the exact state from before.",
|
|
937
1257
|
"scenes.mcp.noServers": "No MCP servers to pick from yet", "scenes.mcp.toolCount": "{count} tool(s)", "scenes.skills.empty": "No skills to pick from yet",
|
|
938
1258
|
"scenes.mcp.allTools": "all tools", "scenes.mcp.pickedCount": "{count} tools picked", "scenes.mcp.notRunning": "not running", "scenes.mcp.edit": "Edit", "scenes.mcp.done": "Done", "scenes.mcp.noteLabel": "Scene note", "scenes.mcp.notePlaceholder": "Overrides the global note in this scene; restored on exit", "scenes.mcp.hasNote": "has note",
|
|
939
|
-
"scenes.mcp.toolsOf": "Pick tools", "scenes.mcp.probeTools": "Start the server to load tools", "scenes.mcp.unknownCount": "unknown tool count — open Edit to load", "scenes.mcp.noteHint": "Saved with the scene. It overrides the note shown on the MCP page and in the system prompt only while this scene is entered — and only for servers checked in this section. Restored on exit.", "scenes.mcp.noTools": "No tools listed for this server (it has never run, or its tools were never recorded)", "scenes.mcp.drillHint": "Checked = enabled in this scene; unchecked = disabled. “
|
|
940
|
-
"scenes.skills.hint": "Add the skills section first, then check the skills enabled in this scene.
|
|
1259
|
+
"scenes.mcp.toolsOf": "Pick tools", "scenes.mcp.probeTools": "Start the server to load tools", "scenes.mcp.unknownCount": "unknown tool count — open Edit to load", "scenes.mcp.noteHint": "Saved with the scene. It overrides the note shown on the MCP page and in the system prompt only while this scene is entered — and only for servers checked in this section. Restored on exit.", "scenes.mcp.noTools": "No tools listed for this server (it has never run, or its tools were never recorded)", "scenes.mcp.drillHint": "Checked = enabled in this scene; unchecked = disabled. “Select all” = every tool (click again for “Deselect all” = none).",
|
|
1260
|
+
"scenes.skills.hint": "Add the skills section first, then check the skills enabled in this scene. Unchecked skills — and scenes with no section at all — are disabled on entry, and restored to their previous switch when you exit. Copies shadowed by a same-name skill and structurally broken skills cannot be checked — they can never take effect in this scene.",
|
|
1261
|
+
"scenes.skills.shadowed.hint": "Shadowed by a same-name skill, so it can never take effect in this scene and cannot be checked. To use this copy instead, click “Use this one” for it on the Skills page.",
|
|
1262
|
+
"scenes.skills.invalid.hint": "Incomplete skill (missing frontmatter / invalid name / empty description): it cannot be enabled or disabled, so it cannot be checked.", "scenes.subagents.hint": "Check the personas this scene should turn on; anything unchecked (or with no section at all) is turned off when you enter, and restored to its previous switch when you exit.", "scenes.subagents.empty": "No personas yet — create one on the Subagents page.",
|
|
941
1263
|
"scenes.filter.skills": "Filter skills / directories", "scenes.filter.subagents": "Filter personas (name or description)", "scenes.filter.servers": "Filter servers",
|
|
942
1264
|
|
|
943
1265
|
"scenes.mem.title": "Memories", "scenes.mem.search": "Filter memories in this scene (name or description)",
|
|
944
1266
|
"scenes.mem.noMatch": "Nothing matches", "scenes.mem.emptyScene": "This scene has no memories yet",
|
|
945
1267
|
"memory.mode.current": "Active mode", "memory.mode.exit": "Exit mode",
|
|
946
1268
|
"memory.archive.edit": "Profile", "memory.archive.title": "Scene profile",
|
|
947
|
-
"memory.archive.tools": "MCP tools", "memory.archive.skills": "Skills", "memory.archive.subagents": "Subagent binding", "memory.archive.memories": "Memories",
|
|
1269
|
+
"memory.archive.tools": "MCP tools", "memory.archive.skills": "Skills", "memory.archive.subagents": "Subagent binding", "memory.archive.memories": "Memories", "memory.archive.applyFailed": "Could not apply at runtime: {reason}",
|
|
948
1270
|
"memory.archive.addTools": "+ Tools", "memory.archive.addSkills": "+ Skills", "memory.archive.addSubagents": "+ Subagents",
|
|
949
1271
|
"memory.archive.removeSection": "Remove section", "memory.archive.save": "Save to scene",
|
|
950
|
-
"memory.archive.emptySection": "
|
|
1272
|
+
"memory.archive.emptySection": "Nothing checked = all disabled (a missing section means the same)", "memory.archive.emptySubagents": "Nothing checked = no personas enabled in this scene (a missing section means the same)", "memory.archive.stale": "Skipped {items} (missing, shadowed by a same-name skill, or structurally broken — it could never take effect)",
|
|
951
1273
|
"memory.result.archiveSaved": "Scene profile saved: {name}", "memory.result.modeSet": "Entered mode: {name}", "memory.result.modeSwitched": "{mcp} server(s) / {skills} source(s) switched", "memory.result.modeExited": "Exited mode",
|
|
952
1274
|
"memory.desc": "Manage memories: enabled memories enter the system prompt; enable them per scene.",
|
|
953
1275
|
"memory.btn.refresh": "Refresh", "memory.btn.new": "New memory", "memory.btn.create": "Create", "memory.btn.save": "Save",
|
|
@@ -962,7 +1284,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
962
1284
|
"memory.scene.enable": "Enabled", "memory.scene.enableAll": "Enable all", "memory.scene.off": "Off", "memory.scene.shared": "always on", "memory.scene.global.tag": "always injected",
|
|
963
1285
|
"memory.scene.global": "Global", "memory.scene.count": "{count} memories", "memory.scene.new": "New memory", "memory.scene.browse": "Pick existing",
|
|
964
1286
|
"memory.scene.empty": "No memories in this scene yet",
|
|
965
|
-
"memory.scene.orphan": "No scene", "memory.scene.orphan.tag": "never injected", "memory.scene.orphan.hint": "These memories sit directly in the memories/ root and belong to no scene, so they never
|
|
1287
|
+
"memory.scene.orphan": "No scene", "memory.scene.orphan.tag": "never injected", "memory.scene.orphan.hint": "These memories sit directly in the memories/ root and belong to no scene, so they are never injected into the context. Move them into a scene directory (or into memories/global/ to make them “Global”).",
|
|
966
1288
|
"memory.scene.edit": "Edit", "memory.scene.editTitle": "Edit scene", "memory.scene.editHelp": "The name is its memory folder name; renaming moves the folder, profile and bindings along. Memory files stay untouched.",
|
|
967
1289
|
"memory.scene.field.name": "Scene name", "memory.scene.field.name.placeholder": "e.g. office",
|
|
968
1290
|
"memory.scene.field.name.hint": "The name is its top-level folder name; you can still change it later.",
|
|
@@ -981,7 +1303,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
981
1303
|
"memory.field.body": "Body (Markdown)", "memory.field.body.placeholder": "Write down the conventions, steps, and boundaries to remember…",
|
|
982
1304
|
"memory.field.form": "Form", "memory.field.attach": "Attachments",
|
|
983
1305
|
"memory.attach.add": "Add files", "memory.attach.empty": "No attachments yet", "memory.attach.pending": "pending", "memory.attach.remove": "Remove attachment",
|
|
984
|
-
"memory.attach.hint": "Stored with the memory; never injected into the prompt",
|
|
1306
|
+
"memory.attach.hint": "Stored with the memory; never injected into the prompt",
|
|
1307
|
+
"memory.attach.count.one": "{count} attachment", "memory.attach.count.other": "{count} attachments",
|
|
1308
|
+
"memory.attach.tip": "Click to manage attachments: {count} · {size} total — {names}",
|
|
1309
|
+
"memory.attach.tip.empty": "No files yet — click to add",
|
|
1310
|
+
"memory.attach.more": ", +{count} more", "memory.attach.sep": ", ", "memory.attach.flat": "The flat form is a single file and cannot carry attachments", "memory.attach.tooLarge": "File too large: {name} (max {limit} MB)",
|
|
985
1311
|
"memory.delete.title": "Move to trash?", "memory.delete.desc": "“{name}” moves to trash and can be restored",
|
|
986
1312
|
"memory.result.created": "Created memory: {name}", "memory.result.updated": "Saved memory: {name}", "memory.result.removed": "Moved to trash: {name}", "memory.result.toggled": "Memory state updated: {name}", "memory.result.active": "Enabled scenes updated",
|
|
987
1313
|
"memory.result.restored": "Restored memory: {name}", "memory.result.trashRemoved": "Permanently deleted: {name}",
|
|
@@ -995,7 +1321,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
995
1321
|
"memory.deleteScene.title": "Delete scene?", "memory.deleteScene.desc": "Moves “{name}” and every memory under it (including disabled ones) to the trash, restorable as one entry; a scene in use cannot be deleted.",
|
|
996
1322
|
"memory.result.sceneCreated": "Scene created: {name}", "memory.result.sceneUpdated": "Scene saved: {name}", "memory.result.sceneRemoved": "Scene deleted: {name}", "memory.result.sceneRemoved.withMemories": "Scene deleted: {name} (with {count} memory file(s); restorable as one entry from the trash)",
|
|
997
1323
|
"memory.table.name": "Memory name and description", "memory.table.tags": "Tags", "memory.table.status": "Status",
|
|
998
|
-
"error.rules.invalidArgs": "Missing arguments: pass all:true (enable everything) or scenes:[...] (explicit narrowing)", "error.rules.invalidGroup": "Invalid scene/group name (non-empty, ≤64 chars, no path separators or < > : \" | ? *, must not start with a dot)", "error.rules.invalidName": "Invalid memory name (non-empty, ≤64 chars, no / \\ < > : \" | ? *, must not start with a dot)", "error.rules.descriptionRequired": "Description is required", "error.rules.descriptionTooLong": "Description is too long (max 500 chars)", "error.rules.bodyRequired": "Body is required", "error.rules.tooLarge": "Rule content is too large", "error.rules.shadowed": "Rule is shadowed by a bundle of the same name", "error.rules.notFound": "Rule not found", "error.rules.budgetExceeded": "Scene memory section exceeds its budget", "error.rules.ioFailed": "File operation failed", "error.rules.nameTaken": "That name is already taken", "error.rules.sceneNotEmpty": "Scene is not empty; cannot be deleted", "error.rules.notBundle": "This memory is flat (a single file) and cannot carry attachments", "error.rules.noFiles": "No files selected", "error.rules.emptyFile": "Empty file", "error.rules.fileTooLarge": "File too large (max {limit} MB each)", "error.rules.tooManyFiles": "At most {limit} files per upload", "error.rules.sceneInMode": "This scene is the active mode; exit the mode before deleting it",
|
|
1324
|
+
"error.rules.invalidArgs": "Missing arguments: pass all:true (enable everything) or scenes:[...] (explicit narrowing)", "error.rules.invalidGroup": "Invalid scene/group name (non-empty, ≤64 chars, no path separators or < > : \" | ? *, must not start with a dot)", "error.rules.invalidName": "Invalid memory name (non-empty, ≤64 chars, no / \\ < > : \" | ? *, must not start with a dot)", "error.rules.descriptionRequired": "Description is required", "error.rules.descriptionTooLong": "Description is too long (max 500 chars)", "error.rules.bodyRequired": "Body is required", "error.rules.tooLarge": "Rule content is too large", "error.rules.shadowed": "Rule is shadowed by a bundle of the same name", "error.rules.exists": "A memory with this name already exists", "error.rules.notFound": "Rule not found", "error.rules.budgetExceeded": "Scene memory section exceeds its budget", "error.rules.ioFailed": "File operation failed", "error.rules.nameTaken": "That name is already taken", "error.rules.sceneNotEmpty": "Scene is not empty; cannot be deleted", "error.rules.notBundle": "This memory is flat (a single file) and cannot carry attachments", "error.rules.noFiles": "No files selected", "error.rules.emptyFile": "Empty file", "error.rules.fileTooLarge": "File too large (max {limit} MB each)", "error.rules.tooManyFiles": "At most {limit} files per upload", "error.rules.sceneInMode": "This scene is the active mode; exit the mode before deleting it",
|
|
999
1325
|
"subagents.title": "Subagents", "subagents.stat.total": " subagents", "subagents.stat.limited": " tool-restricted", "subagents.desc": "Manage subagents: create and import personas; a persona's body is the subagent's system prompt.",
|
|
1000
1326
|
"prompts.desc": "Manage prompts: preset the global instruction baseline; applying writes AGENTS.md and takes effect on the next turn.",
|
|
1001
1327
|
"sessions.desc": "Manage archived sessions: restore or delete them permanently; expired ones are cleaned up automatically.",
|
|
@@ -1014,6 +1340,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1014
1340
|
"subagents.mode.denyEmpty": "Nothing picked = no restriction (every tool of this preset stays available)",
|
|
1015
1341
|
"subagents.mode.noTools": "Could not read this preset's tool list",
|
|
1016
1342
|
"subagents.mode.broken": "This preset is broken; its tool list is unreadable",
|
|
1343
|
+
"subagents.mode.toggleHint": "Expand / collapse this preset's list settings (the buttons on the right enable / disable it)",
|
|
1017
1344
|
"subagents.mode.noPresets": "The host returned no agent-preset roster",
|
|
1018
1345
|
"subagents.legacy.note": "This file still carries the old-style tool limits (allowlist {allow} / denylist {deny}); they apply to every preset.",
|
|
1019
1346
|
"subagents.legacy.convert": "Convert into a preset row",
|
|
@@ -1025,7 +1352,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1025
1352
|
"subagents.field.body": "Persona prompt", "subagents.field.body.placeholder": "Describe the persona's role, responsibilities, and working style…",
|
|
1026
1353
|
"subagents.result.saved": "Persona saved: {name}", "subagents.result.deleted": "Persona deleted: {name}",
|
|
1027
1354
|
"subagents.delete.title": "Delete persona?", "subagents.delete.desc": "Moves the persona file “{name}” to the trash (restorable from the Trash button on the Subagents page).",
|
|
1028
|
-
"subagents.import": "Import subagents", "subagents.import.title": "Import subagents", "subagents.toggle": "Toggle subagent", "subagents.disabled": "Disabled", "subagents.disabled.hint": "
|
|
1355
|
+
"subagents.import": "Import subagents", "subagents.import.title": "Import subagents", "subagents.toggle": "Toggle subagent", "subagents.disabled": "Disabled", "subagents.disabled.hint": "Disabled personas are not injected into the context and hidden from subagent_manager_list / subagent_manager_run; the file is kept and can be re-enabled anytime.",
|
|
1029
1356
|
"subagents.result.imported": "Imported {count} persona(s): {names}",
|
|
1030
1357
|
"memory.import": "Import memories", "memory.import.title": "Import memories", "memory.import.scene": "Import into scene",
|
|
1031
1358
|
"memory.result.imported": "Imported {count} memory/memories: {names}",
|
|
@@ -1035,7 +1362,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1035
1362
|
"import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
|
|
1036
1363
|
"error.import.noFiles": "No files selected",
|
|
1037
1364
|
"nav.title": "Tools",
|
|
1038
|
-
"agm.stat.total": " prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.btn.delete.blocked": "
|
|
1365
|
+
"agm.stat.total": " prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "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.btn.reapply": "Apply again", "agm.btn.apply": "Apply", "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.copyFrom": "Or copy from an existing preset", "agm.field.copyFrom.none": "Do not copy (empty template)", "agm.field.desc": "Description (for humans only)", "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.field.prompt.hint": "While this scene is enabled, its preset is the active global baseline. Defaults to the one currently in effect. While the scene is on, applying another preset on the Prompts page rebinds this scene to that one, and the binding is written into the scene profile.", "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", "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); click “Apply again” 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": "Project directory (optional)", "hist.import.cwd.placeholder": "Absolute path, optional (the session goes to Ungrouped)", "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.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.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.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",
|
|
1039
1366
|
}
|
|
1040
1367
|
};
|
|
1041
1368
|
|
|
@@ -1139,8 +1466,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1139
1466
|
const [compactConfirm, setCompactConfirm] = React.useState(false)
|
|
1140
1467
|
const [, setTick] = React.useState(0)
|
|
1141
1468
|
|
|
1142
|
-
const refresh = (withReveal, onRows) => {
|
|
1469
|
+
const refresh = (withReveal, onRows, silent) => {
|
|
1143
1470
|
const useReveal = withReveal === undefined ? reveal : withReveal === true
|
|
1471
|
+
// 手动刷新(含首次加载)先把按钮切到「刷新中…」并禁用 —— 点完毫无反馈等于没点。
|
|
1472
|
+
// 轮询不算(定时器传 silent=true):它每几秒跑一次,切文案会让按钮一直闪。
|
|
1473
|
+
if (!silent) setState((prev) => Object.assign({}, prev, { loading: true }))
|
|
1144
1474
|
// 明文视图走独立的 mcpm-reveal op(host 端要求带对的访问令牌),
|
|
1145
1475
|
// mcpm-list 始终脱敏、无令牌也能渲染页面。
|
|
1146
1476
|
const applyRows = (res, revealError) => {
|
|
@@ -1154,6 +1484,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1154
1484
|
warnings: (res && res.warnings) || [],
|
|
1155
1485
|
revealError: revealError || null,
|
|
1156
1486
|
locked: (res && res.anyLocked) === true,
|
|
1487
|
+
// 场景内开关由档案定义:页面上这些开关置灰,并说明去哪儿改。
|
|
1488
|
+
scene: (res && res.activeScene) || null,
|
|
1157
1489
|
})
|
|
1158
1490
|
if (onRows) onRows(rows)
|
|
1159
1491
|
}
|
|
@@ -1181,7 +1513,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1181
1513
|
React.useEffect(() => ctx.interval(() => setTick((t) => t + 1), 1000), [])
|
|
1182
1514
|
React.useEffect(() => {
|
|
1183
1515
|
const ms = (settings && settings.pollIntervalMs) || 5000
|
|
1184
|
-
return ctx.interval(() => refreshRef.current(), ms)
|
|
1516
|
+
return ctx.interval(() => refreshRef.current(undefined, null, true), ms)
|
|
1185
1517
|
}, [settings])
|
|
1186
1518
|
React.useEffect(() => {
|
|
1187
1519
|
if (!msg) return undefined
|
|
@@ -1193,7 +1525,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1193
1525
|
setBusy(label)
|
|
1194
1526
|
apiCall(method, args).then((res) => {
|
|
1195
1527
|
if (res && res.ok) {
|
|
1196
|
-
|
|
1528
|
+
// 场景内改开关:档案没跟上的话要说出来,别只报「成功」(详见 sceneSyncWarn)。
|
|
1529
|
+
const warn = sceneSyncWarn(mt, res)
|
|
1530
|
+
setMsg(warn ? { kind: 'warn', text: opMsg(res) + ' ' + warn } : opMsg(res))
|
|
1197
1531
|
refresh()
|
|
1198
1532
|
if (onOk) onOk()
|
|
1199
1533
|
} else setMsg({ kind: 'err', text: (res && res.error) || mt('mcp.msg.failed') })
|
|
@@ -1344,10 +1678,20 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1344
1678
|
return [row.serverName, row.id, row.url, row.command, row.transport].some((value) => String(value == null ? '' : value).toLowerCase().indexOf(normalizedQuery) >= 0)
|
|
1345
1679
|
}
|
|
1346
1680
|
const visibleRows = rows.filter(matchesRow)
|
|
1681
|
+
// 工具数两个口径:total = 已知总数;enabled = 可用数(停用表扣减后,与详情页
|
|
1682
|
+
// 每行的开关一致)。两者不同时显示「可用/总数」,不让全部数量冒充可用数。
|
|
1683
|
+
const toolTotals = rows.reduce((acc, row) => {
|
|
1684
|
+
const total = typeof row.toolCount === 'number' ? row.toolCount : 0
|
|
1685
|
+
const enabled = typeof row.enabledToolCount === 'number' ? row.enabledToolCount : total
|
|
1686
|
+
acc.total += total
|
|
1687
|
+
acc.enabled += enabled
|
|
1688
|
+
return acc
|
|
1689
|
+
}, { total: 0, enabled: 0 })
|
|
1347
1690
|
const summary = {
|
|
1348
1691
|
total: rows.length,
|
|
1349
1692
|
enabled: rows.filter((row) => !row.disabled).length,
|
|
1350
|
-
tools:
|
|
1693
|
+
tools: toolTotals.enabled,
|
|
1694
|
+
toolsTotal: toolTotals.total,
|
|
1351
1695
|
}
|
|
1352
1696
|
const profilePath = state.paths && state.paths.profile ? 'profile: ' + state.paths.profile : null
|
|
1353
1697
|
const groups = levelFilter === 'loader'
|
|
@@ -1372,6 +1716,18 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1372
1716
|
if (row.live.phase === 'active' && typeof row.toolCount === 'number' && row.toolCount === 0) return mt('mcp.live.noToolsHint')
|
|
1373
1717
|
return null
|
|
1374
1718
|
}
|
|
1719
|
+
// 状态标签配色:绿=能用、红=坏了、橙=能连但没工具、灰=没在跑 / 还没定。
|
|
1720
|
+
const pillKind = (cls) => cls === 'dsm-enabled' ? 'ok' : cls === 'dsm-failed' ? 'bad' : cls === 'dsm-disabled' ? 'warn' : 'muted'
|
|
1721
|
+
|
|
1722
|
+
// 工具数标签:有被停用的工具时给「可用/总数」(场景档案收窄与手动逐工具开关
|
|
1723
|
+
// 写的是同一张停用表),否则就是总数 —— 与详情页每个工具的开关状态一致。
|
|
1724
|
+
const toolCountTag = (row) => {
|
|
1725
|
+
if (typeof row.toolCount !== 'number' || row.toolCount <= 0) return null
|
|
1726
|
+
const enabled = typeof row.enabledToolCount === 'number' ? row.enabledToolCount : row.toolCount
|
|
1727
|
+
return enabled < row.toolCount
|
|
1728
|
+
? React.createElement('span', { className: 'dsm-tag' }, mt('mcp.tools.countPartial', { enabled: enabled, total: row.toolCount }))
|
|
1729
|
+
: React.createElement('span', { className: 'dsm-tag' }, mt('mcp.tools.count', { count: row.toolCount }))
|
|
1730
|
+
}
|
|
1375
1731
|
|
|
1376
1732
|
const renderRow = (row) => {
|
|
1377
1733
|
const status = liveStatus(row)
|
|
@@ -1384,11 +1740,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1384
1740
|
row.notes ? React.createElement('div', { className: 'dsm-note dsm-note-user', title: row.notes }, mt('mcp.note.prefix') + row.notes) : null),
|
|
1385
1741
|
React.createElement('div', { className: 'dsm-tags' },
|
|
1386
1742
|
(levelFilter === 'loader' && row.level && row.level !== 'loader') ? React.createElement('span', { className: 'dsm-tag' }, mt('mcp.level.' + row.level)) : null,
|
|
1387
|
-
(
|
|
1743
|
+
toolCountTag(row),
|
|
1388
1744
|
row.duplicate ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, mt('mcp.duplicate')) : null),
|
|
1389
1745
|
React.createElement('div', { className: 'dsm-status ' + status.cls }, status.text),
|
|
1390
1746
|
React.createElement('div', { className: 'dsm-row-actions' },
|
|
1391
|
-
editable ? React.createElement(Switch, { on: !row.disabled, disabled: busy !== null, label: mt('mcp.toggleServer') + ' ' + row.serverName, onClick: () => toggleRow(row) }) : null,
|
|
1747
|
+
editable ? React.createElement(Switch, { on: !row.disabled, disabled: busy !== null || state.locked === true, label: mt('mcp.toggleServer') + ' ' + row.serverName, onClick: () => toggleRow(row) }) : null,
|
|
1392
1748
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', onClick: () => openDetail(row) }, mt('mcp.btn.detail')),
|
|
1393
1749
|
editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () => openEdit(row) }, mt('mcp.btn.edit')) : null,
|
|
1394
1750
|
editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () => restartRow(row) }, mt('mcp.btn.restart')) : null,
|
|
@@ -1431,8 +1787,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1431
1787
|
React.createElement('div', { className: 'dsm-tools' }, (detail.tools || []).map((tool) => React.createElement('div', { className: 'dsm-tool' + (tool.enabled === false ? ' dsm-tool-off' : ''), key: tool.name },
|
|
1432
1788
|
React.createElement('div', { className: 'dsm-tool-name-row' },
|
|
1433
1789
|
React.createElement('div', { className: 'dsm-tool-name' }, tool.name),
|
|
1790
|
+
// 描述来自「已知工具」缓存(服务器未运行)→ 标明是旧数据,不假装实时。
|
|
1791
|
+
tool.stale ? React.createElement('span', { className: 'dsm-tag' }, mt('mcp.tools.stale')) : null,
|
|
1434
1792
|
React.createElement(Switch, { on: tool.enabled !== false, disabled: busy !== null || state.locked === true, label: mt('mcp.toggleTool') + ' ' + tool.name, onClick: () => toggleTool(detail.row, tool) })),
|
|
1435
|
-
tool.description ? React.createElement(
|
|
1793
|
+
tool.description ? React.createElement(ToolDesc, { text: tool.description, expandLabel: mt('mcp.tools.expand'), collapseLabel: mt('mcp.tools.collapse') }) : null,
|
|
1436
1794
|
(tool.parameters && tool.parameters.length > 0) ? React.createElement('div', { className: 'dsm-tool-params' }, tool.parameters.map((param) => React.createElement('div', { className: 'dsm-tool-param', key: param.key },
|
|
1437
1795
|
React.createElement('span', { className: 'dsm-tool-param-key' }, param.key + (param.required ? ' *' : '')),
|
|
1438
1796
|
React.createElement('span', { className: 'dsm-tool-param-type' }, param.type || 'any'),
|
|
@@ -1498,7 +1856,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1498
1856
|
const detailNode = detail && React.createElement(Modal, {
|
|
1499
1857
|
key: 'mcp-detail',
|
|
1500
1858
|
wide: true,
|
|
1501
|
-
|
|
1859
|
+
// 运行状态做成标题右侧的描边标签(用户裁定 2026-09-16):状态是「一眼」信息,
|
|
1860
|
+
// 不该占一整块,也不该跟配置事实(是否登记在 Loader)用冒号拼成一句话。
|
|
1861
|
+
title: React.createElement('span', { className: 'dsm-modal-title-row' },
|
|
1862
|
+
mt('mcp.detail.title') + detail.row.serverName,
|
|
1863
|
+
React.createElement('span', { className: 'dsm-pill dsm-pill-' + pillKind(detailStatus.cls) }, detailStatus.text)),
|
|
1502
1864
|
closeLabel: mt('btn.close'),
|
|
1503
1865
|
onClose: () => setDetail(null),
|
|
1504
1866
|
},
|
|
@@ -1510,14 +1872,16 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1510
1872
|
configRow(mt('mcp.detail.entryId'), detail.row.id),
|
|
1511
1873
|
configRow(mt('mcp.field.level'), mt('mcp.level.' + detail.row.level)),
|
|
1512
1874
|
configRow(mt('mcp.field.transport'), detail.row.transport || '—'),
|
|
1875
|
+
configRow(mt('mcp.detail.loader'), detail.row.live ? mt('mcp.detail.loader.yes') : mt('mcp.detail.loader.no')),
|
|
1513
1876
|
detail.row.url ? configRow(mt('mcp.field.url'), detail.row.url) : null,
|
|
1514
1877
|
detail.row.command ? configRow(mt('mcp.field.command'), detail.row.command + ((detail.row.args && detail.row.args.length) ? ' ' + detail.row.args.join(' ') : '')) : null,
|
|
1515
1878
|
(detail.row.headers && Object.keys(detail.row.headers).length > 0) ? configRow(mt('mcp.field.headersShort'), Object.keys(detail.row.headers).map((k) => k + ': ' + maskSecret(detail.row.headers[k])).join('\n')) : null,
|
|
1516
1879
|
(detail.row.env && Object.keys(detail.row.env).length > 0) ? configRow(mt('mcp.field.envShort'), Object.keys(detail.row.env).map((k) => k + ' = ' + maskSecret(detail.row.env[k])).join('\n')) : null)),
|
|
1517
|
-
|
|
1880
|
+
// 运行状态一节只在**有可操作信息**时出现(启动失败 / 连上了但没工具)——
|
|
1881
|
+
// 状态本身已在标题标签里,再复述一遍就是噪音。
|
|
1882
|
+
detailHint ? React.createElement('div', { className: 'dsm-detail-section' },
|
|
1518
1883
|
React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.status')),
|
|
1519
|
-
React.createElement('div', { className: 'dsm-feedback' + (detailStatus.cls === 'dsm-failed' ? ' dsm-error' :
|
|
1520
|
-
detailStatus.text + ':' + (detailHint || mt('mcp.detail.registered')))),
|
|
1884
|
+
React.createElement('div', { className: 'dsm-feedback' + (detailStatus.cls === 'dsm-failed' ? ' dsm-error' : ' dsm-warning') }, detailHint)) : null,
|
|
1521
1885
|
React.createElement('div', { className: 'dsm-detail-section' },
|
|
1522
1886
|
React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.note')),
|
|
1523
1887
|
React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: noteDraft, maxLength: 200, placeholder: mt('mcp.detail.note.placeholder'), onChange: (ev) => setNoteDraft(ev.target.value) }),
|
|
@@ -1555,16 +1919,20 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1555
1919
|
React.createElement('h2', { className: 'dsm-title' }, 'MCP')),
|
|
1556
1920
|
React.createElement('p', { className: 'dsm-desc' }, mt('mcp.desc'))),
|
|
1557
1921
|
React.createElement('div', { className: 'dsm-actions' },
|
|
1558
|
-
|
|
1922
|
+
refreshButton(mt, busy !== null || state.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.loading, onClick: () => refresh() }),
|
|
1559
1923
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: state.locked === true, onClick: openAdd }, mt('mcp.btn.new')),
|
|
1560
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary dsm-btn-danger', disabled: busy !== null || state.loading, title: mt('mcp.btn.compact.title'),
|
|
1924
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary dsm-btn-danger', disabled: busy !== null || state.loading || state.locked === true, title: mt('mcp.btn.compact.title'), onClick: () => setCompactConfirm(true) }, mt('mcp.btn.compact')),
|
|
1925
|
+
// 批量启停与页头其余动作同栏(用户 2026-09-17 裁定)。场景内**照常可用**:
|
|
1926
|
+
// 改动会同步写进当前场景的档案(宿主 syncSwitchToScene),只有场景锁定才冻结。
|
|
1927
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.loading || !summary.total, onClick: () => run('mcpm-set-all', { enabled: summary.enabled < summary.total }, 'setall') }, mt(summary.total > 0 && summary.enabled === summary.total ? 'mcp.btn.disableAll' : 'mcp.btn.enableAll')))),
|
|
1561
1928
|
state.locked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, mt('lock.banner')) : null,
|
|
1929
|
+
// 场景未锁定时开关照常可用:说清「改动会同步写进该场景的档案」,别让用户担心白改。
|
|
1930
|
+
state.scene ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, mt('scene.switch.banner', { scene: state.scene })) : null,
|
|
1562
1931
|
React.createElement('div', { className: 'dsm-stat-row' },
|
|
1563
1932
|
React.createElement('div', { className: 'dsm-summary' },
|
|
1564
|
-
[['total', summary.total, mt('mcp.stat.total')], ['enabled', summary.enabled, mt('mcp.stat.enabled')], ['tools', summary.tools, mt('mcp.stat.tools')]].map((item) =>
|
|
1933
|
+
[['total', summary.total, mt('mcp.stat.total')], ['enabled', summary.enabled, mt('mcp.stat.enabled')], ['tools', summary.tools < summary.toolsTotal ? summary.tools + '/' + summary.toolsTotal : summary.tools, mt('mcp.stat.tools')]].map((item) =>
|
|
1565
1934
|
React.createElement('div', { key: item[0], className: 'dsm-stat' },
|
|
1566
|
-
React.createElement('strong', null, item[1]), item[2]))),
|
|
1567
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.loading || !summary.total, onClick: () => run('mcpm-set-all', { enabled: summary.enabled < summary.total }, 'setall') }, mt(summary.total > 0 && summary.enabled === summary.total ? 'mcp.btn.disableAll' : 'mcp.btn.enableAll'))),
|
|
1935
|
+
React.createElement('strong', null, item[1]), item[2])))),
|
|
1568
1936
|
React.createElement('div', { className: 'dsm-filters' },
|
|
1569
1937
|
React.createElement('input', { className: 'dsm-control dsm-search', value: query, 'aria-label': mt('mcp.search'), placeholder: mt('mcp.search.placeholder'), onChange: (ev) => setQuery(ev.target.value) }),
|
|
1570
1938
|
React.createElement('div', { className: 'dsm-source-filter' },
|
|
@@ -1595,7 +1963,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1595
1963
|
|
|
1596
1964
|
// ---------- AGENTS.md 预设页:多套全局指令基线,应用=写入 ~/.dsh/AGENTS.md ----------
|
|
1597
1965
|
function AgentsMdPage() {
|
|
1598
|
-
|
|
1966
|
+
// deny = 一次性「拒绝」说明(如:被引用的预设不能删)。它**不替换列表**——
|
|
1967
|
+
// 复用 error 会让整页列表消失,用户删不动之后连列表都看不到了。
|
|
1968
|
+
var st = React.useState({ loading: true, error: null, presets: [], current: null, deny: null })
|
|
1599
1969
|
var state = st[0], setState = st[1]
|
|
1600
1970
|
var busyState = React.useState(null), busy = busyState[0], setBusy = busyState[1]
|
|
1601
1971
|
var em = React.useState(null), editModal = em[0], setEditModal = em[1]
|
|
@@ -1667,10 +2037,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1667
2037
|
})
|
|
1668
2038
|
}
|
|
1669
2039
|
|
|
1670
|
-
function refresh() {
|
|
2040
|
+
function refresh(silent) {
|
|
2041
|
+
// 手动刷新(含首次加载)要把按钮切到「刷新中…」——点完毫无反馈等于没点。
|
|
2042
|
+
// 页面内部那些「操作完顺手重读」的调用传 silent,免得按钮每次都闪一下。
|
|
2043
|
+
if (!silent) setState(function (s) { return Object.assign({}, s, { loading: true }) })
|
|
1671
2044
|
apiCall('agentsmd-list', {}).then(function (res) {
|
|
1672
|
-
|
|
1673
|
-
|
|
2045
|
+
// 刷新即清掉上一次的「拒绝」说明(它解释的是那一次点击,不是常驻状态)。
|
|
2046
|
+
setState(function (s) { return { loading: false, error: res && res.ok ? null : ((res && res.error) || mt('mcp.msg.loadFailed')), presets: (res && res.presets) || [], current: s.current, scenePrompt: (res && res.scenePrompt) || null, anyLocked: (res && res.anyLocked) === true, deny: res && res.ok ? null : s.deny } })
|
|
2047
|
+
}).catch(function (e) { setState({ loading: false, error: String((e && e.message) || e), presets: [], current: null, scenePrompt: null, deny: null }) })
|
|
1674
2048
|
apiCall('agentsmd-get-current', {}).then(function (res) {
|
|
1675
2049
|
if (res && res.ok) setState(function (s) { return Object.assign({}, s, { current: res }) })
|
|
1676
2050
|
}).catch(function () {})
|
|
@@ -1678,6 +2052,19 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1678
2052
|
React.useEffect(function () { refresh() }, [])
|
|
1679
2053
|
|
|
1680
2054
|
var cur = state.current
|
|
2055
|
+
// 场景接管基线时(scenePrompt 指向某个启用场景的绑定),提示词页的「应用」只对
|
|
2056
|
+
// 场景绑定的那一份可用:应用别的预设会绕过场景绑定,造成「场景页显示 A、实际注入 B、
|
|
2057
|
+
// A 不能删而 B 能删」这类各说各话的状态(用户实测)。同一份 = 「重新应用」
|
|
2058
|
+
// (把被手改的 AGENTS.md 写回场景绑定的内容)→ 仍放行。
|
|
2059
|
+
var sceneDriver = state.scenePrompt && state.scenePrompt.scene && state.scenePrompt.presetId && state.scenePrompt.missing !== true ? state.scenePrompt : null
|
|
2060
|
+
var sceneDriverLabel = sceneDriver ? (sceneDriver.label || sceneDriver.scene) : ''
|
|
2061
|
+
/** 引用说明:判定在宿主(scene-prompt-sync),这里只负责把它说成人话。 */
|
|
2062
|
+
function refText(r) {
|
|
2063
|
+
if (r.kind === 'scene') return t(r.active ? 'agm.ref.sceneActive' : 'agm.ref.scene', { scene: r.label || r.scene })
|
|
2064
|
+
if (r.kind === 'restore') return t('agm.ref.restore')
|
|
2065
|
+
return t('agm.ref.file')
|
|
2066
|
+
}
|
|
2067
|
+
function refsOf(p) { return (p.refs || []).map(refText) }
|
|
1681
2068
|
// 改名结果提示:短暂显示后自动消失(与场景页的结果条同一节奏)。
|
|
1682
2069
|
React.useEffect(function () {
|
|
1683
2070
|
if (!state.notice) return undefined
|
|
@@ -1740,7 +2127,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1740
2127
|
}).catch(function () {})
|
|
1741
2128
|
}
|
|
1742
2129
|
|
|
1743
|
-
|
|
2130
|
+
// 只有「还没有任何内容」时才让加载占位顶掉列表:手动点刷新时列表不该凭空消失,
|
|
2131
|
+
// 反馈由刷新按钮自己的「刷新中…」承担(与记忆页 / 技能页同一口径)。
|
|
2132
|
+
var body = state.loading && !state.presets.length
|
|
1744
2133
|
? React.createElement('div', { className: 'dsm-empty' }, t('agm.loading'))
|
|
1745
2134
|
: state.error
|
|
1746
2135
|
? React.createElement(Notice, { kind: 'err', text: state.error })
|
|
@@ -1750,21 +2139,35 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1750
2139
|
// 「生效中」= 当前真正在起作用的基线:启用的场景绑了它,就是它(用户裁定);
|
|
1751
2140
|
// 否则看文件比对。p.fileApplied 单独留着,说明「AGENTS.md 里确实是它」。
|
|
1752
2141
|
var viaScene = p.activeVia === 'scene'
|
|
1753
|
-
var
|
|
2142
|
+
var refs = p.refs || []
|
|
2143
|
+
var restoreRef = refs.some(function (r) { return r.kind === 'restore' })
|
|
2144
|
+
var refTexts = refsOf(p)
|
|
2145
|
+
// 场景绑的就是它,但 AGENTS.md 里的内容不是它(被手改过?)→ 说清楚,
|
|
2146
|
+
// 并留「重新应用」这条路写回。
|
|
2147
|
+
var fileMismatch = !!sceneDriver && sceneDriver.presetId === p.id && !p.fileApplied
|
|
2148
|
+
// 场景接管的是**别的**预设时,应用这一份 = 把该场景改绑到它(用户裁定
|
|
2149
|
+
// 2026-09-17:未锁定时切换要可用,并且同步写进场景档案),所以不再拦截;
|
|
2150
|
+
// 只在 title 里把后果说出来,用户点了才知道会发生什么。
|
|
2151
|
+
var applyRebinds = !!sceneDriver && sceneDriver.presetId !== p.id
|
|
1754
2152
|
return React.createElement('div', { key: p.id, className: 'dsm-source' },
|
|
1755
2153
|
React.createElement('div', { className: 'dsm-source-head' },
|
|
1756
2154
|
React.createElement('div', { className: 'dsm-source-head-main' },
|
|
1757
2155
|
React.createElement('span', { className: 'dsm-source-title', title: p.id }, p.id),
|
|
1758
|
-
p.active ? React.createElement('span', { className: 'dsm-tag dsm-tag-on', title: viaScene ? t('agm.active.hint.scene', { scene: (state.scenePrompt && state.scenePrompt.scene) || '' }) : t('agm.active.hint.file') }, t('agm.active')) : null,
|
|
2156
|
+
p.active ? React.createElement('span', { className: 'dsm-tag dsm-tag-on', title: viaScene ? t('agm.active.hint.scene', { scene: (state.scenePrompt && state.scenePrompt.label) || (state.scenePrompt && state.scenePrompt.scene) || '' }) : t('agm.active.hint.file') }, t('agm.active')) : null,
|
|
1759
2157
|
!p.active && p.fileApplied ? React.createElement('span', { className: 'dsm-tag', title: t('agm.file.applied.hint') }, t('agm.file.applied')) : null,
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2158
|
+
fileMismatch ? React.createElement('span', { className: 'dsm-tag dsm-tag-off', title: t('agm.file.mismatch.hint') }, t('agm.file.mismatch')) : null,
|
|
2159
|
+
// 场景绑定**不在这里标标签**(用户裁定):多场景绑同一份时标签会把行挤爆;
|
|
2160
|
+
// 谁在引用它,点「删除」时会逐条说出来(`agm.btn.delete.blocked.refs`)。
|
|
2161
|
+
restoreRef ? React.createElement('span', { className: 'dsm-tag', title: t('agm.restore.tag.hint') }, t('agm.restore.tag')) : null),
|
|
1764
2162
|
React.createElement('div', { className: 'dsm-source-actions' },
|
|
1765
2163
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: state.anyLocked === true, onClick: function () { openEdit(p) } }, t('agm.btn.edit')),
|
|
1766
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.anyLocked === true, onClick: function () { setApplyConfirm({ id: p.id }) }, title: t('agm.apply.hint') }, p.fileApplied ? t('agm.btn.reapply') : t('agm.btn.apply')),
|
|
1767
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null ||
|
|
2164
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.anyLocked === true, onClick: function () { setApplyConfirm({ id: p.id }) }, title: applyRebinds ? t('agm.apply.rebindScene', { scene: sceneDriverLabel }) : t('agm.apply.hint') }, p.fileApplied ? t('agm.btn.reapply') : t('agm.btn.apply')),
|
|
2165
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || state.anyLocked === true, title: refTexts.length > 0 ? t('agm.btn.delete.blocked.refs', { refs: refTexts.join(t('agm.ref.sep')) }) : t('scenes.lock.blockedEdit'), onClick: function () {
|
|
2166
|
+
// 被引用的预设**点得动但删不掉**:当面说出「谁在用」,而不是让用户对着
|
|
2167
|
+
// 禁用按钮猜(disabled 的按钮连 title 都不弹)。宿主侧同样拒绝,这里只是先说。
|
|
2168
|
+
if (refTexts.length > 0) { setState(function (s) { return Object.assign({}, s, { deny: t('agm.btn.delete.blocked.refs', { refs: refTexts.join(t('agm.ref.sep')) }) }) }); return }
|
|
2169
|
+
setApplyConfirm({ id: p.id, remove: true })
|
|
2170
|
+
} }, t('agm.btn.delete')))), p.description ? React.createElement("div", { key: "desc", className: "dsm-source-note", title: p.description }, p.description) : null)
|
|
1768
2171
|
}))
|
|
1769
2172
|
|
|
1770
2173
|
return React.createElement('section', { className: 'dsm-section' },
|
|
@@ -1774,12 +2177,16 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1774
2177
|
React.createElement('h2', { className: 'dsm-title' }, t('tabs.prompts'))),
|
|
1775
2178
|
React.createElement('p', { className: 'dsm-desc' }, t('prompts.desc'))),
|
|
1776
2179
|
React.createElement('div', { className: 'dsm-actions' },
|
|
1777
|
-
|
|
2180
|
+
refreshButton(t, state.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: state.loading, onClick: function () { refresh() } }),
|
|
1778
2181
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: state.anyLocked === true, onClick: function () { setCreateModal({ id: '', from: '', content: '', description: '', error: null }) } }, t('agm.btn.new')),
|
|
1779
2182
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.anyLocked === true, onClick: function () { setImportResult(null); setImportOpen(true) } }, t('agm.btn.import')),
|
|
1780
2183
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || !state.presets.length, onClick: function () { setExportState({ busy: false, result: null }) } }, t('export.presets')),
|
|
1781
2184
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
|
|
1782
2185
|
state.anyLocked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('lock.banner')) : null,
|
|
2186
|
+
// 场景接管时把「为什么应用按钮点不动」直接写出来,而不是让用户对着置灰的按钮猜。
|
|
2187
|
+
sceneDriver ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('agm.sceneLock.notice', { scene: sceneDriverLabel, id: sceneDriver.presetId })) : null,
|
|
2188
|
+
// 一次性拒绝说明(被引用的预设删不掉):不替换列表,只加一条横幅。
|
|
2189
|
+
state.deny ? React.createElement(Notice, { kind: 'warn', text: state.deny }) : null,
|
|
1783
2190
|
React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '2' } },
|
|
1784
2191
|
[['total', state.presets.length, t('agm.stat.total')], ['active', state.presets.filter(function (p) { return p.active }).length, t('agm.stat.active')]].map(function (item) {
|
|
1785
2192
|
return React.createElement('div', { key: item[0], className: 'dsm-stat' },
|
|
@@ -1982,8 +2389,24 @@ function callApi(path, options) {
|
|
|
1982
2389
|
if (options && typeof options.body === "string") { try { args = JSON.parse(options.body) || {}; } catch (e) { args = {}; } }
|
|
1983
2390
|
return apiCall(op, args).then(function (payload) {
|
|
1984
2391
|
if (!payload || payload.ok !== true) throw payload || { error: "request failed" };
|
|
2392
|
+
// 场景内改开关时宿主会顺手把改动同步写进当前场景档案;同步没跟上(写盘失败 /
|
|
2393
|
+
// 场景已被删)时它会挂一个 `sceneSyncError`。这里必须把它带出去 —— 否则页面只会
|
|
2394
|
+
// 显示「已更新」,而运行时与档案已经对不上了(v0.9.1 之前正是静默的,用户只能靠肉眼
|
|
2395
|
+
// 去档案里比对才发现)。
|
|
2396
|
+
if (payload.sceneSyncError) {
|
|
2397
|
+
return Object.assign({}, (payload.data && typeof payload.data === "object") ? payload.data : {}, { sceneSyncError: payload.sceneSyncError });
|
|
2398
|
+
}
|
|
1985
2399
|
return payload.data;
|
|
1986
2400
|
});
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* 「改动已生效,但没能同步写进场景档案」的提示文案(没有就返回空串)。
|
|
2404
|
+
*
|
|
2405
|
+
* 入参可以是原始响应信封,也可以是上面 callApi 贴过字段的 data —— 两种都认。
|
|
2406
|
+
*/
|
|
2407
|
+
function sceneSyncWarn(t, res) {
|
|
2408
|
+
var err = res && res.sceneSyncError;
|
|
2409
|
+
return err ? t("scene.sync.failed", { error: String(err) }) : "";
|
|
1987
2410
|
}
|
|
1988
2411
|
function isSkillEnabled(skill) { if (skill.enabled !== undefined) return skill.enabled === true; return skill.invocationPolicyValid && skill.modelInvocable && skill.userInvocable && skill.managerEnabled !== false; }
|
|
1989
2412
|
function countKey(key, count) { return key + (Number(count) === 1 ? ".one" : ".other"); }
|
|
@@ -2061,13 +2484,49 @@ function callApi(path, options) {
|
|
|
2061
2484
|
h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", "aria-haspopup": "listbox", "aria-expanded": open, disabled: props.disabled || !options.length, onClick: function () { setOpen(!open); } }, props.browseLabel)),
|
|
2062
2485
|
open && options.length ? h("div", { className: "dsm-select-menu", role: "listbox" }, options.map(function (o) { return h("button", { key: o.value, type: "button", role: "option", className: "dsm-option", "aria-selected": o.value === props.value, onClick: function () { props.onChange(o.value); setOpen(false); } }, o.label); })) : null);
|
|
2063
2486
|
}
|
|
2064
|
-
function Switch(props) { return h("button", { type: "button", className: "dsm-switch" + (props.on ? " dsm-switch-on" : ""), role: "switch", "aria-checked": props.on, "aria-label": props.label, disabled: props.disabled, onClick: props.onClick }); }
|
|
2487
|
+
function Switch(props) { return h("button", { type: "button", className: "dsm-switch" + (props.on ? " dsm-switch-on" : ""), role: "switch", "aria-checked": props.on, "aria-label": props.label, title: props.title, disabled: props.disabled, onClick: props.onClick }); }
|
|
2065
2488
|
function GithubMark16() { return h("svg", { viewBox: "0 0 16 16", width: 16, height: 16, "aria-hidden": true, focusable: "false" }, h("path", { fill: "currentColor", d: "M8 0a8 8 0 0 0-2.53 15.59c.4.074.547-.173.547-.385 0-.19-.007-.693-.01-1.36-2.226.484-2.695-1.073-2.695-1.073-.364-.924-.89-1.17-.89-1.17-.726-.496.055-.486.055-.486.803.056 1.225.824 1.225.824.714 1.223 1.872.87 2.328.665.072-.517.28-.87.508-1.07-1.777-.202-3.645-.888-3.645-3.956 0-.874.31-1.588.823-2.148-.083-.202-.357-1.017.078-2.12 0 0 .672-.215 2.2.82A7.65 7.65 0 0 1 8 4.8c.68.003 1.365.092 2.004.27 1.527-1.035 2.197-.82 2.197-.82.437 1.103.162 1.918.08 2.12.513.56.822 1.274.822 2.148 0 3.076-1.872 3.752-3.654 3.95.288.248.544.735.544 1.482 0 1.07-.01 1.932-.01 2.195 0 .214.144.463.55.384A8.001 8.001 0 0 0 8 0Z" })); }
|
|
2066
2489
|
/**
|
|
2067
2490
|
* 弹窗原语。`list: true` 时走「列表类」三段式:固定头 / 滚动体 / 固定底,
|
|
2068
2491
|
* 配 `.dsm-modal-list`(宽高都固定),滚动只发生在 body 这一层 —— 条目数变化时
|
|
2069
2492
|
* 弹窗不跳、也不会出现嵌套滚动条。普通表单/确认类**不传 list**,结构保持原样。
|
|
2070
2493
|
*/
|
|
2494
|
+
/**
|
|
2495
|
+
* 工具描述:超过两行才默认只显示两行,点「展开」看全文。
|
|
2496
|
+
*
|
|
2497
|
+
* 为什么需要:MCP 工具描述动辄几百字(官方 server 的说明普遍很长),全展开会把详情弹窗
|
|
2498
|
+
* 撑成一面墙,用户反而找不到工具名。截断 + 展开把「扫一遍有哪些工具」和「细读某一个」
|
|
2499
|
+
* 分成两步。纯 CSS 行数裁剪(`-webkit-line-clamp`),不做字符截断 —— 展开时给的是原文,
|
|
2500
|
+
* 所以宿主的 `toolDescriptionMaxLength` 设置不影响这里(默认 0 = 不截断)。
|
|
2501
|
+
*
|
|
2502
|
+
* 按钮只按**真实溢出**显示:短的(哪怕字数不少、两行放得下)不给按钮 —— 字符数阈值
|
|
2503
|
+
* 会造出「点了没任何变化」的死按钮。收起态才量(展开后不裁剪,scrollHeight 不再代表
|
|
2504
|
+
* 被裁掉的高度);没有 DOM(测试里的假 dispatcher)时 ref 为空,静默跳过。
|
|
2505
|
+
*/
|
|
2506
|
+
function ToolDesc(props) {
|
|
2507
|
+
var openState = react.useState(false)
|
|
2508
|
+
var open = openState[0]
|
|
2509
|
+
var setOpen = openState[1]
|
|
2510
|
+
var overState = react.useState(false)
|
|
2511
|
+
var overflow = overState[0]
|
|
2512
|
+
var setOverflow = overState[1]
|
|
2513
|
+
var ref = react.useRef(null)
|
|
2514
|
+
var text = String(props.text || '')
|
|
2515
|
+
react.useEffect(function () {
|
|
2516
|
+
if (open) return
|
|
2517
|
+
var el = ref.current
|
|
2518
|
+
if (!el || typeof el.scrollHeight !== "number" || typeof el.clientHeight !== "number") return
|
|
2519
|
+
setOverflow(el.scrollHeight > el.clientHeight + 1)
|
|
2520
|
+
}, [text, open])
|
|
2521
|
+
return h("div", { className: "dsm-tool-desc-wrap" },
|
|
2522
|
+
h("div", { ref: ref, className: "dsm-tool-desc" + (open ? "" : " dsm-tool-desc-clamp") }, text),
|
|
2523
|
+
overflow ? h("button", {
|
|
2524
|
+
type: "button",
|
|
2525
|
+
className: "dsm-tool-desc-toggle",
|
|
2526
|
+
onClick: function () { setOpen(!open) },
|
|
2527
|
+
}, open ? props.collapseLabel : props.expandLabel) : null)
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2071
2530
|
function Modal(props) { var ref = react.useRef(null); react.useEffect(function () { if (ref.current) ref.current.focus(); }, []); var body = props.list ? h("div", { className: "dsm-modal-body" }, props.children) : props.children; return h("div", { className: "dsm-mask", onMouseDown: function (e) { if (e.target === e.currentTarget) props.onClose(); } }, h("div", { ref: ref, tabIndex: -1, className: "dsm-modal" + (props.wide ? " dsm-modal-wide" : "") + (props.className ? " " + props.className : ""), role: "dialog", "aria-modal": "true", onKeyDown: function (e) { if (!handleModalEscape(e, props.onClose)) trapModalFocus(e.currentTarget, e); } }, h("div", { className: "dsm-modal-head" }, h("h3", { className: "dsm-modal-title" }, props.title), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: props.onClose }, props.closeLabel)), body)); }
|
|
2072
2531
|
/**
|
|
2073
2532
|
* 全插件唯一的「操作结果」呈现方式:成功 = 右下角浮层(固定定位,不改变页面高度),
|
|
@@ -2172,6 +2631,16 @@ function callApi(path, options) {
|
|
|
2172
2631
|
h('span', null, label),
|
|
2173
2632
|
h('span', { className: 'dsm-seg-group-line' }))
|
|
2174
2633
|
}
|
|
2634
|
+
/**
|
|
2635
|
+
* 「全选 ↔ 取消全选」二合一按钮的标签对 —— fixedLabelPair 的语义化别名(同一套定宽机制)。
|
|
2636
|
+
*
|
|
2637
|
+
* 两种文案宽度不同(中文「全选」2 字 vs「取消全选」4 字;英文 Select all vs
|
|
2638
|
+
* Deselect all 差得更多),只渲染当前那一份的话,按钮宽度会随点击变来变去 ——
|
|
2639
|
+
* 用户实测「点了『全选』变成『取消全选』,整颗按钮换到第二行」。
|
|
2640
|
+
*/
|
|
2641
|
+
function bulkPair(label, other) {
|
|
2642
|
+
return fixedLabelPair(label, other)
|
|
2643
|
+
}
|
|
2175
2644
|
/** 段头公共动作:全选 / 清空 / 移除段(未定义时改为「添加」)。 */
|
|
2176
2645
|
/**
|
|
2177
2646
|
* 段头动作。**批量动作合二为一**(D11):同一时刻只有一个方向是合理的 ——
|
|
@@ -2180,7 +2649,7 @@ function callApi(path, options) {
|
|
|
2180
2649
|
*/
|
|
2181
2650
|
function segActions(props) {
|
|
2182
2651
|
if (!props.defined) return [h('button', { key: 'add', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy, onClick: props.onAdd }, props.addLabel)]
|
|
2183
|
-
var out = [h('button', { key: 'all', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy, onClick: props.allChecked ? props.onClear : props.onAll }, props.allChecked ? props.clearLabel : props.selectAllLabel)]
|
|
2652
|
+
var out = [h('button', { key: 'all', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-bulk', disabled: props.busy, onClick: props.allChecked ? props.onClear : props.onAll }, bulkPair(props.allChecked ? props.clearLabel : props.selectAllLabel, props.allChecked ? props.selectAllLabel : props.clearLabel))]
|
|
2184
2653
|
// 常驻段(记忆,P5 单一真相源)没有「段定义」可移除 → 不传 onRemove 即不渲染。
|
|
2185
2654
|
if (props.onRemove) out.push(h('button', { key: 'rm', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy, onClick: props.onRemove }, props.removeLabel))
|
|
2186
2655
|
return out
|
|
@@ -2305,7 +2774,7 @@ function callApi(path, options) {
|
|
|
2305
2774
|
})) : h('div', { className: 'dsm-pick-empty' }, t('export.empty')),
|
|
2306
2775
|
props.result ? h('div', { className: 'dsm-feedback' + (props.result.ok ? '' : ' dsm-error'), role: 'alert' }, props.result.text) : null),
|
|
2307
2776
|
h('div', { className: 'dsm-modal-actions' },
|
|
2308
|
-
h('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy || !items.length, onClick: function () { setPicked(allPicked ? [] : allKeys.slice()) } }, allPicked ? t('export.pickNone') : t('export.pickAll')),
|
|
2777
|
+
h('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-bulk', disabled: props.busy || !items.length, onClick: function () { setPicked(allPicked ? [] : allKeys.slice()) } }, bulkPair(allPicked ? t('export.pickNone') : t('export.pickAll'), allPicked ? t('export.pickAll') : t('export.pickNone'))),
|
|
2309
2778
|
h('button', { type: 'button', className: 'dsm-btn', disabled: props.busy || !picked.length || !outDir.trim(), onClick: function () { props.onSubmit(picked, outDir.trim()) } }, props.busy ? t('export.busy') : t('export.submit'))),
|
|
2310
2779
|
picking ? h(DirPickerModal, { key: 'dir', title: t('export.pickDir'), initial: outDir, closeLabel: t('btn.close'), onClose: function () { setPicking(false) }, onPick: function (p) { setOutDir(p); setPicking(false) } }) : null)
|
|
2311
2780
|
}
|
|
@@ -2379,7 +2848,7 @@ function callApi(path, options) {
|
|
|
2379
2848
|
var pickerOpenRef = react.useRef(false);
|
|
2380
2849
|
function refresh(silent) { if (!silent) setSnapshot({ loading: true, error: null, data: snapshot.data }); return callApi("/state").then(function (data) { setSnapshot({ loading: false, error: null, data: data }); return data; }).catch(function (error) { setSnapshot({ loading: false, error: translateError(t, error), data: snapshot.data }); }); }
|
|
2381
2850
|
react.useEffect(function () { refresh(false); }, []);
|
|
2382
|
-
function post(path, body, successKey, successParams) { if (inflightRef.current) return Promise.reject({ error: "operation already in progress" }); inflightRef.current = true; setBusy(true); setResult(null); return callApi(path, { body: JSON.stringify(body || {}) }).then(function (data) { setResult({ ok:
|
|
2851
|
+
function post(path, body, successKey, successParams) { if (inflightRef.current) return Promise.reject({ error: "operation already in progress" }); inflightRef.current = true; setBusy(true); setResult(null); return callApi(path, { body: JSON.stringify(body || {}) }).then(function (data) { var warn = sceneSyncWarn(t, data); setResult({ ok: !warn, warning: !!warn, text: (successKey ? t(successKey, successParams || data || {}) : t("result.updated")) + (warn ? " " + warn : "") }); return refresh(true).then(function () { return data; }); }).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); throw error; }).finally(function () { inflightRef.current = false; setBusy(false); }); }
|
|
2383
2852
|
function openDetail(root, skill) { setBusy(true); setDetail(null); setModal("detail"); callApi("/detail", { body: JSON.stringify({ root: root.key, name: skill.name }) }).then(setDetail).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); setModal(null); }).finally(function () { setBusy(false); }); }
|
|
2384
2853
|
function openSource() { if (!detail || !detail.path) return; setBusy(true); setResult(null); apiCall("skill-open", { path: detail.path }).then(function (res) { if (res && res.ok) setResult({ ok: true, text: t("result.opened", { path: detail.path }) }); else setResult({ ok: false, text: t("error.action", { error: translateError(t, res) }) }); }).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); }).finally(function () { setBusy(false); }); }
|
|
2385
2854
|
function updateForm(key, value) { setForm(Object.assign({}, form, { [key]: value })); }
|
|
@@ -2445,6 +2914,8 @@ function callApi(path, options) {
|
|
|
2445
2914
|
}
|
|
2446
2915
|
function submitImport(selection) { var sel = selection || upload; if (!sel) return; buildUploadPayload(sel).then(function (payload) { return post("/upload", payload, null); }).then(function (data) { var summary = summarizeImportResult(t, data); setResult({ ok: summary.ok, warning: summary.warning, text: summary.text }); if (summary.imported) { setModal(null); setUpload(null); } }).catch(function () {}); }
|
|
2447
2916
|
var anyLocked = !!(snapshot.data && snapshot.data.anyLocked === true);
|
|
2917
|
+
// 场景内开关由档案定义:技能开关 / 来源开关 / 首选都置灰(用户裁定)。
|
|
2918
|
+
var sceneName = (snapshot.data && snapshot.data.activeScene) || null;
|
|
2448
2919
|
var data = snapshot.data || { roots: [], trash: [], summary: { total: 0, enabled: 0, disabled: 0, issues: 0 } }, allRoots = data.roots || [], roots = visibleSkillRoots(allRoots), removedRoots = removedSkillRoots(allRoots), activeSource = roots.some(function (root) { return root.key === source; }) ? source : "";
|
|
2449
2920
|
var createRoots = allRoots.filter(function (root) { return root.mutable === true; }), createOptions = createRoots.map(function (root) { return { value: root.key, label: rootDisplayName(t, root) }; }); if (!createOptions.length) createOptions.push({ value: "hub", label: t("root.hub") });
|
|
2450
2921
|
// 新建技能的默认落点:hub(`tool-management/skills/`)。
|
|
@@ -2498,11 +2969,96 @@ function callApi(path, options) {
|
|
|
2498
2969
|
if (activeSource && activeSource !== root.key) return null;
|
|
2499
2970
|
var displayName = rootDisplayName(t, root), filtered = root.skills.filter(function (skill) { return matchSkillQuery(Object.assign({}, skill, { rootKey: root.key, rootLabel: displayName }), query); }); if (query && !filtered.length) return null; var open = !!expanded[root.key] || !!query;
|
|
2500
2971
|
var rootCount = root.count == null ? root.skills.length : root.count;
|
|
2501
|
-
return h("section", { key: root.key, className: "dsm-source" }, h("div", { className: "dsm-source-head" }, h("button", { type: "button", className: "dsm-source-head-main", "aria-expanded": open, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: !open })); } }, h("span", { className: "dsm-source-title", title: displayName }, displayName), h("span", { className: "dsm-count" }, t(countKey("summary.group", rootCount), { count: rootCount })), h("span", { className: "dsm-tag " + (root.scope === "project" ? "dsm-tag-on" : root.defaultSource === true ? "" : root.enabled ? "dsm-tag-on" : "dsm-tag-off") }, root.scope === "project" ? t("status.project") : root.defaultSource === true ? t("status.manageable") : t(root.enabled ? "status.source.on" : "status.source.off")), root.scope === "project" ? h("span", { className: "dsm-tag" }, t("status.rank", { rank: root.rank })) : null, h("span", { className: "dsm-path", title: root.path }, root.path)),
|
|
2972
|
+
return h("section", { key: root.key, className: "dsm-source" }, h("div", { className: "dsm-source-head" }, h("button", { type: "button", className: "dsm-source-head-main", "aria-expanded": open, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: !open })); } }, h("span", { className: "dsm-source-title", title: displayName }, displayName), h("span", { className: "dsm-count" }, t(countKey("summary.group", rootCount), { count: rootCount })), h("span", { className: "dsm-tag " + (root.scope === "project" ? "dsm-tag-on" : root.defaultSource === true ? "" : root.enabled ? "dsm-tag-on" : "dsm-tag-off") }, root.scope === "project" ? t("status.project") : root.defaultSource === true ? t("status.manageable") : t(root.enabled ? "status.source.on" : "status.source.off")), root.scope === "project" ? h("span", { className: "dsm-tag" }, t("status.rank", { rank: root.rank })) : null, h("span", { className: "dsm-path", title: root.path }, root.path)), (function () {
|
|
2973
|
+
// 目录级「全选 / 取消全选」(用户裁定 2026-09-17):只作用于**这个目录下当前
|
|
2974
|
+
// 可见且可切换**的技能 —— 与页头那颗同一套目标集逻辑(bulkTargets 传 rootKey),
|
|
2975
|
+
// 同名技能只算生效的那一个(被覆盖的副本跳过)。
|
|
2976
|
+
// 默认来源(可管理 / DSH 技能)没有来源开关,这颗按钮就独占右侧位置。
|
|
2977
|
+
var bulk = bulkState(root.key);
|
|
2978
|
+
var showSwitch = root.scope !== "project" && root.defaultSource !== true && root.toggleable !== false;
|
|
2979
|
+
var acts = [];
|
|
2980
|
+
// 点这颗按钮**顺带把卡片展开**(用户裁定 2026-09-17):批量开关的结果就在下面这几行里,
|
|
2981
|
+
// 卡片收着的话点完只剩按钮文案从「全选」变成「取消全选」,到底改了哪几个技能看不见。
|
|
2982
|
+
// 只展开、不收起 —— 两个方向(全选 / 取消全选)都要能看见结果,收起是点卡片头的事。
|
|
2983
|
+
if (bulk.targets.length) acts.push(h("button", { key: "all", type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-bulk", disabled: busy || anyLocked, title: t("source.bulk.title"), onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: true })); submitBulk(bulk.targets, !bulk.on); } }, bulkPair(bulk.on ? t("bulk.unselectAll") : t("bulk.selectAll"), bulk.on ? t("bulk.selectAll") : t("bulk.unselectAll"))));
|
|
2984
|
+
if (showSwitch) {
|
|
2985
|
+
// 「移除来源」只留图标(用户裁定 2026-09-17):它是低频的破坏性操作,却和「全选」
|
|
2986
|
+
// 并列、还带红框描边 —— 于是整行的视觉重心落在它身上,四字宽度也占地方。
|
|
2987
|
+
// 图标按钮必须自带 title + aria-label(没有可见文字);点击照旧弹确认弹窗,
|
|
2988
|
+
// 那里仍是带文字的红色「移除来源」,破坏性操作的说明没有丢。
|
|
2989
|
+
if (root.removable === true) acts.push(h("button", { key: "rm", type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger dsm-btn-icon", disabled: busy, title: t("btn.source.remove.title"), "aria-label": t("btn.source.remove.title"), onClick: function () { setModal({ type: "source-remove-confirm", key: root.key, name: displayName }); } }, h(TrashIcon, { size: 14 })));
|
|
2990
|
+
// 开关来源时**顺带把卡片展开**(用户裁定 2026-09-17),与旁边那颗「全选」同口径:
|
|
2991
|
+
// 启停的结果就是下面这些技能行(开启 = 整目录可用了,关闭 = 逐行变停用),卡片收着
|
|
2992
|
+
// 的话点完只剩标签从「已停用」变「已启用」,目录里到底有什么、哪些真的可用了看不见。
|
|
2993
|
+
// 只展开、不收起 —— 两个方向都要能看见结果,收起仍然是点卡片头那一行的事。
|
|
2994
|
+
acts.push(h(Switch, { key: "sw", on: root.enabled, disabled: busy || anyLocked, label: t("source.toggle") + " " + displayName, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: true })); post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } }));
|
|
2995
|
+
}
|
|
2996
|
+
return acts.length ? h("span", { className: "dsm-source-actions" }, acts) : null;
|
|
2997
|
+
})()), open ? h("div", { className: "dsm-source-body" }, filtered.length ? h(react.Fragment, null, h("div", { className: "dsm-table-head" }, h("span", null, t("table.skill")), h("span", null, t("filter.source")), h("span", null, t("table.status")), h("span", null, "")), filtered.map(function (skill) { return renderSkill(root, skill); })) : h("div", { className: "dsm-empty" }, query ? t("empty.search") : t("empty.source"))) : null);
|
|
2502
2998
|
}
|
|
2503
2999
|
|
|
2504
3000
|
var summary = data.summary || { total: 0, enabled: 0, disabled: 0, issues: 0 };
|
|
2505
|
-
|
|
3001
|
+
/**
|
|
3002
|
+
* 批量启停的目标集 = 当前**可见**(搜索 + 来源筛选命中)且**可切换**的技能。
|
|
3003
|
+
* 条件与单条 Switch 逐字一致(来源可切换、来源没被停用、技能可加载、不是被覆盖的副本),
|
|
3004
|
+
* 否则会出现「全选说 12 个、实际只改了 9 个」的假回执。
|
|
3005
|
+
*
|
|
3006
|
+
* 不传 rootKey → 页头那颗按钮的作用集(全部可见来源);传 rootKey → 某个目录卡片上
|
|
3007
|
+
* 那颗按钮的作用集(只看这一个目录)。同名技能只算**生效的那一个**(被覆盖的副本
|
|
3008
|
+
* `shadowedBy` 跳过)——这正是「全选后同名技能只有被启用的那个被勾」的由来。
|
|
3009
|
+
*/
|
|
3010
|
+
function bulkTargets(rootKey) {
|
|
3011
|
+
var out = [];
|
|
3012
|
+
for (var i = 0; i < roots.length; i++) {
|
|
3013
|
+
var root = roots[i];
|
|
3014
|
+
if (rootKey && root.key !== rootKey) continue;
|
|
3015
|
+
if (activeSource && activeSource !== root.key) continue;
|
|
3016
|
+
if (root.toggleable === false || root.enabled === false) continue;
|
|
3017
|
+
var list = root.skills || [];
|
|
3018
|
+
var rootLabel = rootDisplayName(t, root);
|
|
3019
|
+
for (var j = 0; j < list.length; j++) {
|
|
3020
|
+
var sk = list[j];
|
|
3021
|
+
if (sk.loadable === false || sk.shadowedBy) continue;
|
|
3022
|
+
if (!matchSkillQuery(Object.assign({}, sk, { rootKey: root.key, rootLabel: rootLabel }), query)) continue;
|
|
3023
|
+
out.push({ root: root.key, name: sk.name, on: isSkillEnabled(sk) });
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
return out;
|
|
3027
|
+
}
|
|
3028
|
+
/** 一组批量目标 + 是否已全开(二合一按钮的两个入参)。 */
|
|
3029
|
+
function bulkState(rootKey) {
|
|
3030
|
+
var targets = bulkTargets(rootKey);
|
|
3031
|
+
return { targets: targets, on: targets.length > 0 && targets.every(function (x) { return x.on; }) };
|
|
3032
|
+
}
|
|
3033
|
+
/** 批量启停:主要看「当前是不是全开」,是就发停用,否则发启用(二合一按钮的语义)。 */
|
|
3034
|
+
function submitBulk(items, enabled) {
|
|
3035
|
+
if (!items || !items.length) return;
|
|
3036
|
+
setBusy(true); setResult(null);
|
|
3037
|
+
apiCall("skill-set-all", {
|
|
3038
|
+
items: items.map(function (x) { return { root: x.root, name: x.name }; }),
|
|
3039
|
+
enabled: enabled,
|
|
3040
|
+
}).then(function (res) {
|
|
3041
|
+
if (res && res.ok) {
|
|
3042
|
+
var payload = (res && res.data) || {};
|
|
3043
|
+
var failed = (payload.failed || []).length;
|
|
3044
|
+
// 场景内批量开关同样会同步写档案;没跟上就说出来(详见 sceneSyncWarn)。
|
|
3045
|
+
var syncWarn = sceneSyncWarn(t, res);
|
|
3046
|
+
setResult({
|
|
3047
|
+
ok: failed === 0 && !syncWarn,
|
|
3048
|
+
warning: failed > 0 || !!syncWarn,
|
|
3049
|
+
text: t("bulk.done.skills", {
|
|
3050
|
+
action: t(enabled ? "bulk.enabled" : "bulk.disabled"),
|
|
3051
|
+
count: payload.changed == null ? items.length : payload.changed,
|
|
3052
|
+
failed: failed,
|
|
3053
|
+
}) + (syncWarn ? " " + syncWarn : ""),
|
|
3054
|
+
});
|
|
3055
|
+
refresh(true);
|
|
3056
|
+
} else setResult({ ok: false, text: translateError(t, res) });
|
|
3057
|
+
}).catch(function (e) { setResult({ ok: false, text: String((e && e.message) || e) }); }).finally(function () { setBusy(false); });
|
|
3058
|
+
}
|
|
3059
|
+
var bulkAll = bulkState();
|
|
3060
|
+
var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dsm-head" }, h("div", { className: "dsm-title-block" }, h("div", { className: "dsm-title-row" }, h("h2", { className: "dsm-title" }, t("title"))), h("p", { className: "dsm-desc" }, t("desc"))), h("div", { className: "dsm-actions" }, refreshButton(t, busy || snapshot.loading, { className: "dsm-btn dsm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: anyLocked, onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: anyLocked, onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: function () { setResult(null); setModal("export"); } }, t("export.skills")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: function () { setResult(null); setCustomForm({ path: "", label: "" }); setModal("custom-add"); } }, t("btn.custom.add")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal("trash"); } }, t("trash.btn.open")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary dsm-btn-bulk", disabled: busy || anyLocked || !bulkAll.targets.length, title: t("bulk.head.title"), onClick: function () { submitBulk(bulkAll.targets, !bulkAll.on) } }, bulkPair(bulkAll.on ? t("bulk.unselectAll") : t("bulk.selectAll"), bulkAll.on ? t("bulk.selectAll") : t("bulk.unselectAll"))))), anyLocked ? h("div", { key: "lockbanner", className: "dsm-feedback dsm-warning", role: "status" }, t("lock.banner")) : null,
|
|
3061
|
+
sceneName ? h("div", { key: "scenebanner", className: "dsm-feedback dsm-warning", role: "status" }, t("scene.switch.banner", { scene: sceneName })) : null,
|
|
2506
3062
|
h("div", { key: "summary", className: "dsm-summary" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.issues, "summary.issues"]].map(function (item) { return h("div", { key: item[1], className: "dsm-stat" }, h("strong", null, item[0]), t(countKey(item[1], item[0]), { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dsm-filters" }, h("input", { className: "dsm-control dsm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dsm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), result && modal !== "import" ? h(Notice, { key: "result", kind: result.warning ? "warn" : result.ok ? "ok" : "err", text: result.text }) : null].concat((data.warnings || []).map(function (warning, index) { return h(Notice, { key: "warning-" + index, kind: "warn", text: translateError(t, warning) }); }), [snapshot.error ? h(Notice, { key: "error", kind: "err", text: snapshot.error }) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dsm-empty" }, t("loading")) : h("div", { key: "sources", className: "dsm-sources" }, roots.map(renderRoot))]);
|
|
2507
3063
|
|
|
2508
3064
|
if (modal === "create") content.push(h(Modal, { key: "create", title: t("create.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dsm-form" }, h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("create.target")), h(SourceSelect, { value: form.root, options: createOptions, onChange: function (value) { updateForm("root", value); } })), [["name", "create.name", "create.name.placeholder"], ["description", "create.description", "create.description.placeholder"]].map(function (field) { return h("label", { key: field[0], className: "dsm-field" }, h("span", { className: "dsm-label" }, t(field[1])), h("input", { className: "dsm-control", value: form[field[0]], placeholder: t(field[2]), onChange: function (e) { updateForm(field[0], e.target.value); } })); }), h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("create.body")), h("textarea", { className: "dsm-control", value: form.body, placeholder: t("create.body.placeholder"), onChange: function (e) { updateForm("body", e.target.value); } })), h("p", { className: "dsm-help" }, t("create.chat.note"))), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
|
|
@@ -2612,7 +3168,10 @@ function callApi(path, options) {
|
|
|
2612
3168
|
var noticeState = React.useState(null)
|
|
2613
3169
|
var notice = noticeState[0], setNotice = noticeState[1]
|
|
2614
3170
|
|
|
2615
|
-
function refresh() {
|
|
3171
|
+
function refresh(silent) {
|
|
3172
|
+
// 手动刷新(含首次加载)要把按钮切到「刷新中…」——点完毫无反馈等于没点。
|
|
3173
|
+
// 页面内部那些「操作完顺手重读」的调用传 silent,免得按钮每次都闪一下。
|
|
3174
|
+
if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
|
|
2616
3175
|
apiCall('history-list', {}).then(function (r) {
|
|
2617
3176
|
if (r && r.ok) {
|
|
2618
3177
|
var items = r.items || []
|
|
@@ -2625,6 +3184,10 @@ function callApi(path, options) {
|
|
|
2625
3184
|
return next
|
|
2626
3185
|
})
|
|
2627
3186
|
} else setData({ loading: false, items: [], workspaces: {}, groups: [], retentionDays: 0, error: (r && r.error) || t('hist.err.load') })
|
|
3187
|
+
}).catch(function (e) {
|
|
3188
|
+
// 请求被拒 / 网络异常:必须把 loading 收回来,否则按钮会永远停在「刷新中…」
|
|
3189
|
+
// 且一直禁用(这一支原来根本没有 —— 失败时页面停在旧数据上,什么都没有说)。
|
|
3190
|
+
setData(function (prev) { return Object.assign({}, prev, { loading: false, error: String((e && e.message) || e) }) })
|
|
2628
3191
|
})
|
|
2629
3192
|
}
|
|
2630
3193
|
React.useEffect(function () { refresh() }, [])
|
|
@@ -3006,9 +3569,12 @@ function callApi(path, options) {
|
|
|
3006
3569
|
React.createElement('h2', { className: 'dsm-title' }, t('tabs.sessions'))),
|
|
3007
3570
|
React.createElement('p', { className: 'dsm-desc' }, t('sessions.desc'))),
|
|
3008
3571
|
React.createElement('div', { className: 'dsm-actions' },
|
|
3009
|
-
|
|
3572
|
+
refreshButton(t, data.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: data.loading, onClick: function () { refresh() } }),
|
|
3010
3573
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openImportPicker }, t('hist.btn.import')),
|
|
3011
3574
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openExportPicker }, t('hist.btn.export')),
|
|
3575
|
+
// 「全选 / 取消全选」紧挨导出(用户 2026-09-17 裁定):它是列表级动作,
|
|
3576
|
+
// 与其余页头动作同栏,不再和统计数字挤在一行。
|
|
3577
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary dsm-btn-bulk', disabled: !filtered.length, onClick: function () { toggleAllVisible(visibleIds) } }, bulkPair(allVisible ? t('hist.deselectAll') : t('hist.selectAll'), allVisible ? t('hist.selectAll') : t('hist.deselectAll'))),
|
|
3012
3578
|
selected.size > 0 ? React.createElement('div', { className: 'dsm-hist-batch' },
|
|
3013
3579
|
React.createElement('span', { className: 'dsm-hist-batch-count' }, t('hist.selected', { count: selected.size })),
|
|
3014
3580
|
React.createElement('div', { className: 'dsm-hist-batch-actions' },
|
|
@@ -3019,8 +3585,7 @@ function callApi(path, options) {
|
|
|
3019
3585
|
[['archived', data.items.length, t('hist.stat.archived')], ['projects', grouped ? groups.length : '—', t('hist.stat.projects')], ['retention', data.retentionDays ? String(data.retentionDays) : '∞', t('hist.stat.retention')]].map(function (item) {
|
|
3020
3586
|
return React.createElement('div', { key: item[0], className: 'dsm-stat' },
|
|
3021
3587
|
React.createElement('strong', null, item[1]), item[2])
|
|
3022
|
-
})),
|
|
3023
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: !filtered.length, onClick: function () { toggleAllVisible(visibleIds) } }, allVisible ? t('hist.deselectAll') : t('hist.selectAll'))),
|
|
3588
|
+
}))),
|
|
3024
3589
|
React.createElement('div', { className: 'dsm-filters' },
|
|
3025
3590
|
React.createElement('input', { className: 'dsm-control dsm-search', type: 'text', placeholder: t('hist.search.placeholder'), value: query, onChange: function (e) { setQuery(e.target.value) } }),
|
|
3026
3591
|
React.createElement('div', { className: 'dsm-source-filter' },
|
|
@@ -3029,7 +3594,7 @@ function callApi(path, options) {
|
|
|
3029
3594
|
}))),
|
|
3030
3595
|
data.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(data.error)) : null,
|
|
3031
3596
|
notice ? React.createElement(Notice, { kind: notice.kind, text: notice.text }) : null,
|
|
3032
|
-
data.loading ? React.createElement('div', { className: 'dsm-empty' }, t('hist.loading'))
|
|
3597
|
+
data.loading && !data.items.length ? React.createElement('div', { className: 'dsm-empty' }, t('hist.loading'))
|
|
3033
3598
|
: filtered.length === 0 ? React.createElement('div', { className: 'dsm-empty' }, query ? t('hist.empty.search') : t('hist.empty.none'))
|
|
3034
3599
|
: React.createElement('div', { className: 'dsm-sources' },
|
|
3035
3600
|
grouped ? groups.map(renderGroup) : React.createElement('div', { className: 'dsm-source' }, filtered.map(renderRow))),
|
|
@@ -3471,9 +4036,9 @@ function callApi(path, options) {
|
|
|
3471
4036
|
*
|
|
3472
4037
|
* 取 2026-09-13 前的「预勾当前运行时状态」是错的方向:用户点「添加」只是想开始配,
|
|
3473
4038
|
* 不是想把自己现有的启用状态抄进档案。代价要讲清楚——勾选集语义是「勾 = 启用」,
|
|
3474
|
-
* 空段 =
|
|
4039
|
+
* 空段 = 全部停用(未定义段也是同一个意思,见 archive-engine 的四域同口径),
|
|
4040
|
+
* 所以「添加 MCP 工具集」当下就等价于「该场景下全部 MCP 停用」,
|
|
3475
4041
|
* 段脚注(`memory.archive.emptySection`)与弹窗顶部说明会同时把这句话显示出来。
|
|
3476
|
-
* 子智能体段例外:空段 = 不限制(全部人设可用,见 filterBySceneBinding)。
|
|
3477
4042
|
*/
|
|
3478
4043
|
function emptyMcpPreset() { return {} }
|
|
3479
4044
|
/** 全选:列出全部服务器(含未运行的)并各勾「全部工具」。 */
|
|
@@ -3492,7 +4057,7 @@ function callApi(path, options) {
|
|
|
3492
4057
|
function archiveSegActions(defined, addLabel, onAdd, onRemove, onAll, onClear, allChecked) {
|
|
3493
4058
|
return segActions({
|
|
3494
4059
|
defined: defined, busy: busy, addLabel: addLabel,
|
|
3495
|
-
selectAllLabel: t('
|
|
4060
|
+
selectAllLabel: t('bulk.selectAll'), clearLabel: t('bulk.unselectAll'), removeLabel: t('memory.archive.removeSection'),
|
|
3496
4061
|
onAdd: onAdd, onRemove: onRemove, onAll: onAll, onClear: onClear, allChecked: allChecked,
|
|
3497
4062
|
})
|
|
3498
4063
|
}
|
|
@@ -3556,7 +4121,7 @@ function callApi(path, options) {
|
|
|
3556
4121
|
count: t('scenes.seg.checked', { checked: enabledCount, total: own.length }),
|
|
3557
4122
|
actions: segActions({
|
|
3558
4123
|
defined: true, busy: busy,
|
|
3559
|
-
selectAllLabel: t('
|
|
4124
|
+
selectAllLabel: t('bulk.selectAll'), clearLabel: t('bulk.unselectAll'),
|
|
3560
4125
|
onAll: function () { setAllMemories(true) },
|
|
3561
4126
|
onClear: function () { setAllMemories(false) },
|
|
3562
4127
|
allChecked: own.length > 0 && enabledCount === own.length,
|
|
@@ -3592,7 +4157,9 @@ function callApi(path, options) {
|
|
|
3592
4157
|
function () { setSections(Object.assign({}, modalSections(), { mcp: emptyMcpPreset() })) },
|
|
3593
4158
|
function () { var s = Object.assign({}, modalSections()); delete s.mcp; setSections(s) },
|
|
3594
4159
|
mcpSelectAll,
|
|
3595
|
-
function () { setSections(Object.assign({}, modalSections(), { mcp: {} })) }
|
|
4160
|
+
function () { setSections(Object.assign({}, modalSections(), { mcp: {} })) },
|
|
4161
|
+
// 「全选」按钮的全选态:档案里勾了全部服务器(含没在过滤结果里的)。
|
|
4162
|
+
defined && servers.length > 0 && servers.every(function (x) { return sections.mcp[x.name] !== undefined })),
|
|
3596
4163
|
body: defined
|
|
3597
4164
|
? React.createElement('div', { className: 'dsm-seg-body' },
|
|
3598
4165
|
segFilter(modal.mcpQuery, function (v) { setModal(Object.assign({}, modal, { mcpQuery: v })) }, t('scenes.filter.servers')),
|
|
@@ -3640,6 +4207,9 @@ function callApi(path, options) {
|
|
|
3640
4207
|
var noteVal = (sections.mcpNotes || {})[server] || ''
|
|
3641
4208
|
var known = drillTools
|
|
3642
4209
|
var checkedCount = spec === '*' ? (known || []).length : (Array.isArray(spec) ? spec.length : 0)
|
|
4210
|
+
// 工具级的「全选 / 取消全选」也是二合一:全勾了就该给「取消全选」,
|
|
4211
|
+
// 并列两个按钮必然有一个是废话(与段头同一口径)。
|
|
4212
|
+
var allToolsChecked = checkedCount > 0 && (known || []).length > 0 && checkedCount === (known || []).length
|
|
3643
4213
|
return React.createElement('div', { className: 'dsm-mask', key: 'mcp-edit' },
|
|
3644
4214
|
React.createElement('div', { className: 'dsm-modal' },
|
|
3645
4215
|
React.createElement('div', { className: 'dsm-modal-head' },
|
|
@@ -3654,8 +4224,7 @@ function callApi(path, options) {
|
|
|
3654
4224
|
React.createElement('div', { className: 'dsm-mcp-edit-tools-head' },
|
|
3655
4225
|
React.createElement('label', { className: 'dsm-label' }, t('scenes.mcp.toolsOf')),
|
|
3656
4226
|
React.createElement('div', { className: 'dsm-mcp-edit-tools-actions' },
|
|
3657
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !known, onClick: function () { mcpDrillSpec('*') } }, t('
|
|
3658
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { mcpDrillSpec([]) } }, t('scenes.seg.clear')))),
|
|
4227
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-bulk', disabled: busy || !known, onClick: function () { mcpDrillSpec(allToolsChecked ? [] : '*') } }, bulkPair(allToolsChecked ? t('bulk.unselectAll') : t('bulk.selectAll'), allToolsChecked ? t('bulk.selectAll') : t('bulk.unselectAll'))))),
|
|
3659
4228
|
known === null
|
|
3660
4229
|
? React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('memory.loading')))
|
|
3661
4230
|
: (known || []).length
|
|
@@ -3706,6 +4275,14 @@ function callApi(path, options) {
|
|
|
3706
4275
|
var sections = modalSections()
|
|
3707
4276
|
var defined = !!sections.skills
|
|
3708
4277
|
var allItems = modal.inventory.skills || []
|
|
4278
|
+
/**
|
|
4279
|
+
* 可勾选集:**被同名技能覆盖的副本**与**结构不完整的技能**都不算。
|
|
4280
|
+
* 它们勾了也不生效(运行时那一步本来就跳过),后端保存时还会按同一口径当失效项丢掉
|
|
4281
|
+
* —— 留着只会造成「档案说开着、技能页说没启动」(用户实测:启动一个目录后档案里
|
|
4282
|
+
* 多出被覆盖的那个技能)。计数、「全选」、行内开关全部按这一份来。
|
|
4283
|
+
*/
|
|
4284
|
+
var pickable = function (x) { return !x.shadowed && x.loadable !== false }
|
|
4285
|
+
var pickableItems = allItems.filter(pickable)
|
|
3709
4286
|
var items = (function () {
|
|
3710
4287
|
var q = String(modal.skillQuery || '').trim().toLowerCase()
|
|
3711
4288
|
if (!q) return allItems
|
|
@@ -3715,30 +4292,35 @@ function callApi(path, options) {
|
|
|
3715
4292
|
return (d.name + ' ' + d.source + ' ' + d.key).toLowerCase().indexOf(q) >= 0
|
|
3716
4293
|
})
|
|
3717
4294
|
})()
|
|
4295
|
+
var pickedCount = pickableItems.filter(function (x) { return sections.skills && sections.skills.indexOf(x.key) >= 0 }).length
|
|
3718
4296
|
return seg({
|
|
3719
4297
|
title: t('memory.archive.skills'),
|
|
3720
|
-
count: defined ? t('scenes.seg.checked', { checked:
|
|
4298
|
+
count: defined ? t('scenes.seg.checked', { checked: pickedCount, total: pickableItems.length }) : t('scenes.archive.sectionOff'),
|
|
3721
4299
|
actions: archiveSegActions(defined, t('memory.archive.addSkills'),
|
|
3722
4300
|
function () { setSections(Object.assign({}, modalSections(), { skills: [] })) },
|
|
3723
4301
|
function () { var s = Object.assign({}, modalSections()); delete s.skills; setSections(s) },
|
|
3724
|
-
function () { setSections(Object.assign({}, modalSections(), { skills:
|
|
3725
|
-
function () { setSections(Object.assign({}, modalSections(), { skills: [] })) }
|
|
4302
|
+
function () { setSections(Object.assign({}, modalSections(), { skills: pickableItems.map(function (x) { return x.key }) })) },
|
|
4303
|
+
function () { setSections(Object.assign({}, modalSections(), { skills: [] })) },
|
|
4304
|
+
defined && pickableItems.length > 0 && pickableItems.every(function (x) { return sections.skills.indexOf(x.key) >= 0 })),
|
|
3726
4305
|
body: defined
|
|
3727
4306
|
? React.createElement('div', { className: 'dsm-seg-body' },
|
|
3728
4307
|
segFilter(modal.skillQuery, function (v) { setModal(Object.assign({}, modal, { skillQuery: v })) }, t('scenes.filter.skills')),
|
|
3729
4308
|
items.length
|
|
3730
4309
|
? React.createElement('div', null, items.map(function (item) {
|
|
3731
4310
|
var d = skillDisplay(item)
|
|
4311
|
+
var offered = pickable(item)
|
|
3732
4312
|
return pickRow({
|
|
3733
|
-
|
|
4313
|
+
disabled: busy || !offered,
|
|
3734
4314
|
key: item.key,
|
|
3735
|
-
checked: sections.skills.indexOf(item.key) >= 0,
|
|
4315
|
+
checked: offered && sections.skills.indexOf(item.key) >= 0,
|
|
3736
4316
|
name: d.name,
|
|
3737
4317
|
desc: d.source || null,
|
|
3738
4318
|
descTitle: d.key,
|
|
3739
4319
|
meta: item.shadowed
|
|
3740
|
-
? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('status.shadowed'))
|
|
3741
|
-
: (item.
|
|
4320
|
+
? React.createElement('span', { className: 'dsm-tag dsm-tag-off', title: t('scenes.skills.shadowed.hint') }, t('status.shadowed'))
|
|
4321
|
+
: (item.loadable === false
|
|
4322
|
+
? React.createElement('span', { className: 'dsm-tag', title: t('scenes.skills.invalid.hint') }, t('status.invalid'))
|
|
4323
|
+
: (item.enabled === false ? React.createElement('span', { className: 'dsm-tag' }, t('memory.scene.off')) : null)),
|
|
3742
4324
|
onChange: function () {
|
|
3743
4325
|
var list = sections.skills.slice(); var i = list.indexOf(item.key)
|
|
3744
4326
|
if (i >= 0) list.splice(i, 1); else list.push(item.key)
|
|
@@ -3748,7 +4330,7 @@ function callApi(path, options) {
|
|
|
3748
4330
|
}))
|
|
3749
4331
|
: React.createElement('div', { className: 'dsm-pick-empty' }, modal.skillQuery ? t('scenes.mem.noMatch') : t('scenes.skills.empty')))
|
|
3750
4332
|
: React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.skills.hint'))),
|
|
3751
|
-
foot: archiveSegFoot(defined, defined ?
|
|
4333
|
+
foot: archiveSegFoot(defined, defined ? pickedCount : 0),
|
|
3752
4334
|
})
|
|
3753
4335
|
}
|
|
3754
4336
|
/** 段 3:子智能体绑定。 */
|
|
@@ -3798,7 +4380,8 @@ function callApi(path, options) {
|
|
|
3798
4380
|
function () { setSections(Object.assign({}, modalSections(), { subagents: [] })) },
|
|
3799
4381
|
function () { var s = Object.assign({}, modalSections()); delete s.subagents; setSections(s) },
|
|
3800
4382
|
function () { setSections(Object.assign({}, modalSections(), { subagents: allItems.map(function (x) { return x.name }) })) },
|
|
3801
|
-
function () { setSections(Object.assign({}, modalSections(), { subagents: [] })) }
|
|
4383
|
+
function () { setSections(Object.assign({}, modalSections(), { subagents: [] })) },
|
|
4384
|
+
defined && allItems.length > 0 && allItems.every(function (x) { return sections.subagents.indexOf(x.name) >= 0 })),
|
|
3802
4385
|
body: defined
|
|
3803
4386
|
? React.createElement('div', { className: 'dsm-seg-body' },
|
|
3804
4387
|
segFilter(modal.subQuery, function (v) { setModal(Object.assign({}, modal, { subQuery: v })) }, t('scenes.filter.subagents')),
|
|
@@ -3885,7 +4468,9 @@ function callApi(path, options) {
|
|
|
3885
4468
|
setBusy(false)
|
|
3886
4469
|
if (res && res.ok) {
|
|
3887
4470
|
setModal(null)
|
|
3888
|
-
|
|
4471
|
+
// 当前模式的档案保存后**就地生效**;运行时没应用成功要如实说(别假装生效了)。
|
|
4472
|
+
var applyNote = res.applyError ? ' · ' + t('memory.archive.applyFailed', { reason: res.applyError }) : ''
|
|
4473
|
+
setResult({ ok: !res.applyError, text: t('memory.result.archiveSaved', { name: modal.name }) + (res.stale && res.stale.length ? ' · ' + t('memory.archive.stale', { items: res.stale.join('、') }) : '') + applyNote })
|
|
3889
4474
|
refresh(true)
|
|
3890
4475
|
} else setResult({ ok: false, text: translateError(t, res) })
|
|
3891
4476
|
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
|
|
@@ -3974,7 +4559,7 @@ function callApi(path, options) {
|
|
|
3974
4559
|
React.createElement('h2', { className: 'dsm-title' }, t('scenes.title'))),
|
|
3975
4560
|
React.createElement('p', { className: 'dsm-desc' }, t('scenes.desc'))),
|
|
3976
4561
|
React.createElement('div', { className: 'dsm-actions' },
|
|
3977
|
-
|
|
4562
|
+
refreshButton(t, busy || data.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }),
|
|
3978
4563
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openCreateScene }, t('memory.btn.newScene')),
|
|
3979
4564
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
|
|
3980
4565
|
React.createElement('div', { key: 'stats', className: 'dsm-summary' },
|
|
@@ -4001,7 +4586,7 @@ function callApi(path, options) {
|
|
|
4001
4586
|
React.createElement('span', null, t('scenes.legacyAll', { count: legacyAllScenes.length })),
|
|
4002
4587
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { enterMode(legacyAllScenes[0].name) } }, t('scenes.legacyAll.fix'))) : null,
|
|
4003
4588
|
data.error ? React.createElement('div', { key: 'gerr', className: 'dsm-feedback dsm-error' }, String(data.error)) : null,
|
|
4004
|
-
data.loading ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
4589
|
+
data.loading && !presetScenes.length ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
4005
4590
|
: presetScenes.length ? React.createElement('div', { key: 'scenes', className: 'dsm-scenes' }, presetScenes.map(function (scene) {
|
|
4006
4591
|
var name = scene.name
|
|
4007
4592
|
var label = sceneLabel(data.scenes, name) || name
|
|
@@ -4018,6 +4603,9 @@ function callApi(path, options) {
|
|
|
4018
4603
|
var boundPrompt = scene.prompt || ''
|
|
4019
4604
|
var promptLive = !!(boundPrompt && data.scenePrompt && data.scenePrompt.scene === name)
|
|
4020
4605
|
var promptMissing = promptLive && data.scenePrompt.missing === true
|
|
4606
|
+
// 生效中但 AGENTS.md 里不是它(被手动改过?)——「显示必须等于实际注入」,
|
|
4607
|
+
// 不一致就如实标出来,别让用户以为文件里就是它。
|
|
4608
|
+
var promptMismatch = promptLive && !promptMissing && data.scenePrompt.duplicate !== true
|
|
4021
4609
|
return React.createElement('article', { key: 's:' + name, className: 'dsm-scene-tile' + (modeScene === name ? ' dsm-scene-tile-mode' : '') },
|
|
4022
4610
|
React.createElement('div', { className: 'dsm-scene-tile-head' },
|
|
4023
4611
|
React.createElement('span', { className: 'dsm-scene-tile-name', title: label }, label),
|
|
@@ -4026,13 +4614,14 @@ function callApi(path, options) {
|
|
|
4026
4614
|
locked ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.shared')) : null,
|
|
4027
4615
|
modeScene === name ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.mode.current')) : null,
|
|
4028
4616
|
sceneLocked ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('scenes.lock.tag')) : null,
|
|
4029
|
-
!locked && scene.active === false ? React.createElement('span', { className: 'dsm-tag
|
|
4617
|
+
!locked && scene.active === false ? React.createElement('span', { className: 'dsm-tag' }, t('memory.scene.off')) : null,
|
|
4030
4618
|
boundPrompt ? React.createElement('span', {
|
|
4031
4619
|
className: 'dsm-tag' + (promptMissing ? ' dsm-tag-off' : promptLive ? ' dsm-tag-on' : ''),
|
|
4032
|
-
title: t('scenes.prompt.tag.hint'),
|
|
4620
|
+
title: promptMismatch ? t('scenes.prompt.mismatch.hint', { id: boundPrompt }) : t('scenes.prompt.tag.hint'),
|
|
4033
4621
|
}, promptMissing
|
|
4034
4622
|
? t('scenes.prompt.missing', { id: boundPrompt })
|
|
4035
4623
|
: promptLive ? t('scenes.prompt.live', { id: boundPrompt }) : t('scenes.prompt.bound', { id: boundPrompt })) : null,
|
|
4624
|
+
promptMismatch ? React.createElement('span', { className: 'dsm-tag dsm-tag-off', title: t('scenes.prompt.mismatch.hint', { id: boundPrompt }) }, t('agm.file.mismatch')) : null,
|
|
4036
4625
|
locked ? null : React.createElement('span', {
|
|
4037
4626
|
className: 'dsm-scene-tile-switch',
|
|
4038
4627
|
title: blockedByOther ? t('scenes.enable.blocked', { name: sceneLabel(data.scenes, data.activeScene) || data.activeScene || '' }) : (sceneLocked && scene.active === true ? t('scenes.lock.blockedExit') : ''),
|
|
@@ -4113,6 +4702,8 @@ function callApi(path, options) {
|
|
|
4113
4702
|
var modal = ms[0], setModal = ms[1]
|
|
4114
4703
|
// 场景锁定(v0.8):任一场景锁定 = 五个域整体冻结;本页全部写控件禁用。
|
|
4115
4704
|
var anyLocked = data.anyLocked === true
|
|
4705
|
+
// 场景内开关由档案定义:人设开关置灰(要改就去场景页的档案编辑器里改)。
|
|
4706
|
+
var sceneName = data.activeScene || null
|
|
4116
4707
|
// 回收站:删除人设 = 移入回收站(宿主侧 subagent-delete),这里列出/恢复/永久删除。
|
|
4117
4708
|
var ts = React.useState(null)
|
|
4118
4709
|
var trash = ts[0], setTrash = ts[1]
|
|
@@ -4151,7 +4742,7 @@ function callApi(path, options) {
|
|
|
4151
4742
|
}
|
|
4152
4743
|
React.useEffect(function () { refresh() }, [])
|
|
4153
4744
|
/**
|
|
4154
|
-
* 子智能体开关(与记忆页同一套交互):停用 = 不注入目录段、
|
|
4745
|
+
* 子智能体开关(与记忆页同一套交互):停用 = 不注入目录段、subagent_manager_list/run
|
|
4155
4746
|
* 不可见;人设文件不动。开关联动全局状态,立即提交。
|
|
4156
4747
|
*/
|
|
4157
4748
|
function togglePersona(p) {
|
|
@@ -4159,8 +4750,12 @@ function callApi(path, options) {
|
|
|
4159
4750
|
setBusy(true)
|
|
4160
4751
|
apiCall('subagent-toggle', { name: p.name, enabled: next }).then(function (r) {
|
|
4161
4752
|
setBusy(false)
|
|
4162
|
-
if (r && r.ok)
|
|
4163
|
-
|
|
4753
|
+
if (r && r.ok) {
|
|
4754
|
+
// 场景内改开关:人设改了、档案没跟上时说清楚(详见 sceneSyncWarn)。
|
|
4755
|
+
var warn = sceneSyncWarn(t, r)
|
|
4756
|
+
setResult(warn ? { ok: false, warning: true, text: warn } : null)
|
|
4757
|
+
refresh(true)
|
|
4758
|
+
} else if (r) setResult({ ok: false, text: translateError(t, r) })
|
|
4164
4759
|
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
|
|
4165
4760
|
}
|
|
4166
4761
|
/**
|
|
@@ -4204,7 +4799,7 @@ function callApi(path, options) {
|
|
|
4204
4799
|
}
|
|
4205
4800
|
function openEditor(name) {
|
|
4206
4801
|
if (!name) {
|
|
4207
|
-
setModal({ type: 'editor', mode: 'create', advanced: false, openMode: null, modeQuery: '', legacyTarget: '', form: { name: '', description: '', provider: '', model: '', tools: [], toolsDeny: [], toolsByPreset: {}, body: '', error: null } })
|
|
4802
|
+
setModal({ type: 'editor', mode: 'create', advanced: false, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: { name: '', description: '', provider: '', model: '', tools: [], toolsDeny: [], toolsByPreset: {}, body: '', error: null } })
|
|
4208
4803
|
return
|
|
4209
4804
|
}
|
|
4210
4805
|
setBusy(true)
|
|
@@ -4212,11 +4807,15 @@ function callApi(path, options) {
|
|
|
4212
4807
|
setBusy(false)
|
|
4213
4808
|
if (res && res.ok) {
|
|
4214
4809
|
var p = res.persona || {}
|
|
4215
|
-
|
|
4810
|
+
var advanced = initialAdvanced(p)
|
|
4811
|
+
setModal({ type: 'editor', mode: 'edit', originalName: String(p.name || name), advanced: advanced, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: {
|
|
4216
4812
|
name: p.name || name, description: p.description || '', provider: p.provider || '', model: p.model || '',
|
|
4217
4813
|
tools: (p.tools || []).slice(), toolsDeny: (p.toolsDeny || []).slice(),
|
|
4218
4814
|
toolsByPreset: cloneRules(p.toolsByPreset), body: p.body || '', error: null,
|
|
4219
4815
|
} })
|
|
4816
|
+
// 已配过限制的人设**一打开就是展开的**(initialAdvanced)→ 候选数据必须在这里也拉,
|
|
4817
|
+
// 否则四行模式先亮"宿主没有回传预设名单",非得点两次「高级选项」才补上(用户实测)。
|
|
4818
|
+
if (advanced) loadCandidates()
|
|
4220
4819
|
} else setResult({ ok: false, text: translateError(t, res) })
|
|
4221
4820
|
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
|
|
4222
4821
|
}
|
|
@@ -4282,7 +4881,9 @@ function callApi(path, options) {
|
|
|
4282
4881
|
* **已勾选的名字保留**(只换语义),这样"改成黑名单"不用从头再勾一遍。
|
|
4283
4882
|
*/
|
|
4284
4883
|
function startMode(id, mode) {
|
|
4285
|
-
|
|
4884
|
+
// 刚被「关闭」掉的名字记在 stoppedRules 里:重新启用时带回,免得"关一下再开"把
|
|
4885
|
+
// 已勾好的名单清空(用户实测踩到:以为「关闭白名单」是收起设置,再启用就空了)。
|
|
4886
|
+
var prev = modeRule(id) || (modal.stoppedRules || {})[id]
|
|
4286
4887
|
var names = prev && Array.isArray(prev.names) ? prev.names.slice() : []
|
|
4287
4888
|
setModal(Object.assign({}, modal, {
|
|
4288
4889
|
openMode: id, modeQuery: '',
|
|
@@ -4291,7 +4892,18 @@ function callApi(path, options) {
|
|
|
4291
4892
|
}),
|
|
4292
4893
|
}))
|
|
4293
4894
|
}
|
|
4294
|
-
function stopMode(id) {
|
|
4895
|
+
function stopMode(id) {
|
|
4896
|
+
var prev = modeRule(id)
|
|
4897
|
+
var stopped = Object.assign({}, modal.stoppedRules || {})
|
|
4898
|
+
if (prev) stopped[id] = { mode: prev.mode, names: (prev.names || []).slice() }
|
|
4899
|
+
var next = Object.assign({}, modal.form.toolsByPreset || {})
|
|
4900
|
+
delete next[id]
|
|
4901
|
+
setModal(Object.assign({}, modal, {
|
|
4902
|
+
stoppedRules: stopped,
|
|
4903
|
+
openMode: modal.openMode === id ? null : modal.openMode,
|
|
4904
|
+
form: Object.assign({}, modal.form, { toolsByPreset: next }),
|
|
4905
|
+
}))
|
|
4906
|
+
}
|
|
4295
4907
|
function toggleModeTool(id, name) {
|
|
4296
4908
|
var rule = modeRule(id)
|
|
4297
4909
|
if (!rule) return
|
|
@@ -4314,6 +4926,8 @@ function callApi(path, options) {
|
|
|
4314
4926
|
var q = String(modal.modeQuery || '').trim().toLowerCase()
|
|
4315
4927
|
var list = q ? all.filter(function (n) { return n.toLowerCase().indexOf(q) >= 0 }) : all
|
|
4316
4928
|
var picked = rule.names || []
|
|
4929
|
+
// 与其余「全选」同一口径:全勾了就只给「取消全选」(二合一,不并列)。
|
|
4930
|
+
var allPickedAll = all.length > 0 && all.every(function (n) { return picked.indexOf(n) >= 0 })
|
|
4317
4931
|
return React.createElement('div', { className: 'dsm-mode-body' },
|
|
4318
4932
|
picked.length
|
|
4319
4933
|
? React.createElement('div', { className: 'dsm-tools-chips' }, picked.map(function (name) {
|
|
@@ -4329,8 +4943,7 @@ function callApi(path, options) {
|
|
|
4329
4943
|
placeholder: t('subagents.tools.filter'),
|
|
4330
4944
|
onChange: function (e) { setModal(Object.assign({}, modal, { modeQuery: e.target.value })) },
|
|
4331
4945
|
}),
|
|
4332
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { setModeRule(id, { mode: rule.mode, names: all.slice() }) } }, t('
|
|
4333
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !picked.length, onClick: function () { setModeRule(id, { mode: rule.mode, names: [] }) } }, t('scenes.seg.clear'))),
|
|
4946
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet 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')))),
|
|
4334
4947
|
list.length
|
|
4335
4948
|
? React.createElement('div', { className: 'dsm-tools-grid' }, list.map(function (name) {
|
|
4336
4949
|
return pickRow({ key: 'm:' + id + ':' + name, disabled: busy, checked: picked.indexOf(name) >= 0, name: name, onChange: function () { toggleModeTool(id, name) } })
|
|
@@ -4366,15 +4979,29 @@ function callApi(path, options) {
|
|
|
4366
4979
|
? (isDeny ? 'subagents.mode.stopDeny' : 'subagents.mode.stopAllow')
|
|
4367
4980
|
: (isDeny ? 'subagents.mode.startDeny' : 'subagents.mode.startAllow')))
|
|
4368
4981
|
}
|
|
4982
|
+
var open = modal.openMode === id
|
|
4369
4983
|
return React.createElement('div', { className: 'dsm-mode-row' + (rule ? ' dsm-mode-on' : ''), key: id },
|
|
4370
4984
|
React.createElement('div', { className: 'dsm-mode-head' },
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
React.createElement('
|
|
4985
|
+
// 行首按钮 = 展开 / 收起名单(用户实测:已配好的模式只有「关闭白名单」可点,
|
|
4986
|
+
// 点下去是把模式停掉、名单设置再也进不去 —— 查看/编辑与启用/停用必须分开)。
|
|
4987
|
+
React.createElement('button', {
|
|
4988
|
+
type: 'button',
|
|
4989
|
+
className: 'dsm-mode-head-main',
|
|
4990
|
+
disabled: !rule,
|
|
4991
|
+
title: rule ? t('subagents.mode.toggleHint') : '',
|
|
4992
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
4993
|
+
onClick: function () {
|
|
4994
|
+
setModal(Object.assign({}, modal, { openMode: open ? null : id, modeQuery: '' }))
|
|
4995
|
+
},
|
|
4996
|
+
},
|
|
4997
|
+
React.createElement('span', { className: 'dsm-mode-caret', 'aria-hidden': 'true' }, rule ? (open ? '▾' : '▸') : ''),
|
|
4998
|
+
React.createElement('span', { className: 'dsm-mode-name' }, presetLabel(p)),
|
|
4999
|
+
p.broken ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('compat.reach.broken')) : null,
|
|
5000
|
+
React.createElement('span', { className: 'dsm-mode-sum ' + summary.cls }, summary.text)),
|
|
4374
5001
|
React.createElement('span', { className: 'dsm-mode-actions' },
|
|
4375
5002
|
startButton('allow', false),
|
|
4376
5003
|
startButton('deny', true))),
|
|
4377
|
-
|
|
5004
|
+
open ? modeEditor(p) : null)
|
|
4378
5005
|
})),
|
|
4379
5006
|
React.createElement('p', { className: 'dsm-help' }, t('subagents.field.modes.hint')))
|
|
4380
5007
|
}
|
|
@@ -4469,21 +5096,22 @@ function callApi(path, options) {
|
|
|
4469
5096
|
React.createElement('h2', { className: 'dsm-title' }, t('subagents.title'))),
|
|
4470
5097
|
React.createElement('p', { className: 'dsm-desc' }, t('subagents.desc'))),
|
|
4471
5098
|
React.createElement('div', { className: 'dsm-actions' },
|
|
4472
|
-
|
|
5099
|
+
refreshButton(t, busy || data.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }),
|
|
4473
5100
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { openEditor(null) } }, t('subagents.new')),
|
|
4474
5101
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { setResult(null); setModal({ type: 'import' }) } }, t('subagents.import')),
|
|
4475
5102
|
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')),
|
|
4476
5103
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
|
|
4477
5104
|
anyLocked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('lock.banner')) : null,
|
|
5105
|
+
sceneName ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('scene.switch.banner', { scene: sceneName })) : null,
|
|
4478
5106
|
React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '2' } },
|
|
4479
5107
|
[['total', data.subagents.length, t('subagents.stat.total')],
|
|
4480
5108
|
['limited', data.subagents.filter(function (p) { return (p.tools && p.tools.length) || (p.toolsDeny && p.toolsDeny.length) || (p.toolsByPreset && Object.keys(p.toolsByPreset).length) }).length, t('subagents.stat.limited')]].map(function (item) {
|
|
4481
5109
|
return React.createElement('div', { key: item[0], className: 'dsm-stat' },
|
|
4482
5110
|
React.createElement('strong', null, item[1]), item[2])
|
|
4483
5111
|
})),
|
|
4484
|
-
React.createElement(Notice, { kind: result && result.ok ? 'ok' : 'err', text: result && result.text }),
|
|
5112
|
+
React.createElement(Notice, { kind: result && result.warning ? 'warn' : result && result.ok ? 'ok' : 'err', text: result && result.text }),
|
|
4485
5113
|
data.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(data.error)) : null,
|
|
4486
|
-
data.loading ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
5114
|
+
data.loading && !data.subagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
4487
5115
|
: data.subagents.length ? React.createElement('div', { className: 'dsm-sources' }, data.subagents.map(function (p) {
|
|
4488
5116
|
return React.createElement('div', { key: p.name, className: 'dsm-source' },
|
|
4489
5117
|
React.createElement('div', { className: 'dsm-source-head' },
|
|
@@ -4638,6 +5266,18 @@ function callApi(path, options) {
|
|
|
4638
5266
|
var attachRef = React.useRef(null)
|
|
4639
5267
|
var ATTACH_MAX_MB = 8
|
|
4640
5268
|
var ATTACH_MAX_BYTES = ATTACH_MAX_MB << 20
|
|
5269
|
+
// 从「N 个附件」标签进来的那一次,要把附件区滚进视野(否则那颗标签就只是块装饰:
|
|
5270
|
+
// 打开弹窗后还得自己往下找)。用回调 ref 会随每次 render 重跑 → 一打字就把页面拽回去,
|
|
5271
|
+
// 所以走 effect + 一次性标志,滚完立刻把标志清掉。
|
|
5272
|
+
var attachFieldRef = React.useRef(null)
|
|
5273
|
+
React.useEffect(function () {
|
|
5274
|
+
if (!editor || editor.focusAttach !== true) return
|
|
5275
|
+
var node = attachFieldRef.current
|
|
5276
|
+
if (node && typeof node.scrollIntoView === 'function') {
|
|
5277
|
+
try { node.scrollIntoView({ block: 'center' }) } catch (e) { /* 滚不动不影响使用 */ }
|
|
5278
|
+
}
|
|
5279
|
+
setEditor(function (prev) { return prev && prev.focusAttach ? Object.assign({}, prev, { focusAttach: false }) : prev })
|
|
5280
|
+
}, [editor])
|
|
4641
5281
|
|
|
4642
5282
|
/** 场景名 = 分组路径的第一段(`web/frontend` 属于场景 `web`)。 */
|
|
4643
5283
|
function sceneOf(group) {
|
|
@@ -4690,14 +5330,14 @@ function callApi(path, options) {
|
|
|
4690
5330
|
setEditor({ mode: 'create', id: '', group: sceneName || '', name: '', description: '', body: '', form: 'flat', path: '', error: null, files: [], attachments: [] })
|
|
4691
5331
|
setModal({ type: 'editor' })
|
|
4692
5332
|
}
|
|
4693
|
-
function openEditor(rule) {
|
|
5333
|
+
function openEditor(rule, focusAttach) {
|
|
4694
5334
|
if (busy) return
|
|
4695
5335
|
setBusy(true); setResult(null)
|
|
4696
5336
|
apiCall('rules-read', { id: rule.id }).then(function (res) {
|
|
4697
5337
|
setBusy(false)
|
|
4698
5338
|
if (res && res.ok && res.rule) {
|
|
4699
5339
|
var rd = res.rule
|
|
4700
|
-
setEditor({ mode: 'edit', id: rd.id, group: rd.group, name: rd.name, description: rd.description || '', body: rd.body || '', form: rd.form === 'bundle' ? 'bundle' : 'flat', path: rd.path || '', error: null, files: [], attachments: rd.attachments || [] })
|
|
5340
|
+
setEditor({ mode: 'edit', id: rd.id, group: rd.group, name: rd.name, description: rd.description || '', body: rd.body || '', form: rd.form === 'bundle' ? 'bundle' : 'flat', path: rd.path || '', error: null, files: [], attachments: rd.attachments || [], focusAttach: focusAttach === true })
|
|
4701
5341
|
setModal({ type: 'editor' })
|
|
4702
5342
|
} else setResult({ ok: false, text: translateError(t, res) })
|
|
4703
5343
|
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
|
|
@@ -4819,6 +5459,47 @@ function callApi(path, options) {
|
|
|
4819
5459
|
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
|
|
4820
5460
|
}
|
|
4821
5461
|
|
|
5462
|
+
/**
|
|
5463
|
+
* 批量写 `rules[*].enabled`(记忆页页头、每个场景卡片共用的「全选 / 取消全选」)。
|
|
5464
|
+
*
|
|
5465
|
+
* 顺序提交而不是 Promise.all:宿主侧每次 toggle 都要重写索引文件,并发提交只是排队,
|
|
5466
|
+
* 徒增失败时的书写顺序;串行还能「失败即停」并如实报出中止位置。
|
|
5467
|
+
*/
|
|
5468
|
+
function setMemories(ids, on) {
|
|
5469
|
+
var list = (ids || []).filter(function (id) { return !!id })
|
|
5470
|
+
if (!list.length) return
|
|
5471
|
+
setBusy(true); setResult(null)
|
|
5472
|
+
var done = 0
|
|
5473
|
+
var failed = null
|
|
5474
|
+
var chain = Promise.resolve()
|
|
5475
|
+
list.forEach(function (id) {
|
|
5476
|
+
chain = chain.then(function () {
|
|
5477
|
+
if (failed) return null
|
|
5478
|
+
return apiCall('rules-toggle', { id: id, enabled: on }).then(function (res) {
|
|
5479
|
+
if (res && res.ok) done += 1
|
|
5480
|
+
else failed = translateError(t, res)
|
|
5481
|
+
}).catch(function (e) { failed = String((e && e.message) || e) })
|
|
5482
|
+
})
|
|
5483
|
+
})
|
|
5484
|
+
chain.then(function () {
|
|
5485
|
+
setBusy(false)
|
|
5486
|
+
var action = t(on ? 'bulk.enabled' : 'bulk.disabled')
|
|
5487
|
+
setResult(failed
|
|
5488
|
+
? { ok: false, text: t('memory.bulk.partial', { action: action, count: done, error: failed }) }
|
|
5489
|
+
: { ok: true, text: t('bulk.done.memories', { action: action, count: done, failed: 0 }) })
|
|
5490
|
+
refresh(true)
|
|
5491
|
+
})
|
|
5492
|
+
}
|
|
5493
|
+
/** 可批量切换的记忆 = 非被同名 bundle 覆盖的(与行内 Switch 同一条件)。 */
|
|
5494
|
+
function toggleableIds(rules) {
|
|
5495
|
+
return (rules || []).filter(function (r) { return r.shadowed !== true }).map(function (r) { return String(r.id) })
|
|
5496
|
+
}
|
|
5497
|
+
/** 传进来的一组记忆是否全部开着(决定二合一按钮显示哪一个文案)。 */
|
|
5498
|
+
function allMemoriesOn(rules) {
|
|
5499
|
+
var rows = (rules || []).filter(function (r) { return r.shadowed !== true })
|
|
5500
|
+
return rows.length > 0 && rows.every(function (r) { return r.enabled !== false })
|
|
5501
|
+
}
|
|
5502
|
+
|
|
4822
5503
|
/** 记忆存好后统一收尾:上传附件 → 关弹窗 → 报结果(附件失败不回滚记忆)。 */
|
|
4823
5504
|
function finishEditor(id, name, pending, successKey, attachedKey) {
|
|
4824
5505
|
return flushAttachments(id, pending).then(function (att) {
|
|
@@ -4951,6 +5632,38 @@ function callApi(path, options) {
|
|
|
4951
5632
|
return buckets[n].rules.length > 0 || sceneFilter === n
|
|
4952
5633
|
})
|
|
4953
5634
|
var hasAnyRule = data.rules.length > 0
|
|
5635
|
+
/** 页头「全选 / 取消全选」的作用集 = 当前筛选后可见的、可切换的记忆。 */
|
|
5636
|
+
var bulkRules = []
|
|
5637
|
+
visibleBuckets.forEach(function (n) { bulkRules = bulkRules.concat(buckets[n].rules) })
|
|
5638
|
+
var bulkIds = toggleableIds(bulkRules)
|
|
5639
|
+
var bulkAllOn = allMemoriesOn(bulkRules)
|
|
5640
|
+
|
|
5641
|
+
/**
|
|
5642
|
+
* bundle 记忆的「N 个附件」标签(用户裁定 2026-09-17)。
|
|
5643
|
+
*
|
|
5644
|
+
* 形态归「bundle」那颗标签,**内容归这一颗** —— 两者语义不同,挤进一个框里中文下会到 7 个字。
|
|
5645
|
+
* 数量是宿主在 rules-list 里补的(`r.attach`,口径与注入给模型的附件清单逐字一致)。
|
|
5646
|
+
* 0 个附件也显示(灰色),这样"哪些 bundle 是空壳"一眼可见;两种状态都能点,
|
|
5647
|
+
* 点了直接开编辑弹窗并**滚到附件区**(不然这颗标签就只是块装饰)。
|
|
5648
|
+
* `r.attach` 缺失 = 宿主没读到这个目录(索引残留)→ 什么都不显示,不谎报 0。
|
|
5649
|
+
*/
|
|
5650
|
+
function attachTag(r, shadowed) {
|
|
5651
|
+
if (r.form !== 'bundle' || !r.attach) return null
|
|
5652
|
+
var n = r.attach.count
|
|
5653
|
+
var names = r.attach.names || []
|
|
5654
|
+
var extra = n > names.length ? t('memory.attach.more', { count: n - names.length }) : ''
|
|
5655
|
+
var title = n === 0
|
|
5656
|
+
? t('memory.attach.tip.empty')
|
|
5657
|
+
: t('memory.attach.tip', { count: n, size: fmtBytes(r.attach.bytes), names: names.join(t('memory.attach.sep')) + extra })
|
|
5658
|
+
return React.createElement('button', {
|
|
5659
|
+
type: 'button',
|
|
5660
|
+
className: 'dsm-tag dsm-tag-attach' + (n === 0 ? ' dsm-tag-attach-empty' : ''),
|
|
5661
|
+
disabled: busy || shadowed || anyLocked,
|
|
5662
|
+
title: title,
|
|
5663
|
+
'aria-label': title,
|
|
5664
|
+
onClick: function () { openEditor(r, true) },
|
|
5665
|
+
}, React.createElement(PaperclipIcon, { size: 11 }), t(countKey('memory.attach.count', n), { count: n }))
|
|
5666
|
+
}
|
|
4954
5667
|
|
|
4955
5668
|
function renderRuleRow(r, sceneActive) {
|
|
4956
5669
|
var shadowed = r.shadowed === true
|
|
@@ -4969,6 +5682,7 @@ function callApi(path, options) {
|
|
|
4969
5682
|
React.createElement('div', { className: 'dsm-tags' },
|
|
4970
5683
|
r.group && r.group.indexOf('/') >= 0 ? React.createElement('span', { className: 'dsm-tag' }, r.group) : null,
|
|
4971
5684
|
r.form === 'bundle' ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.form.bundle')) : null,
|
|
5685
|
+
attachTag(r, shadowed),
|
|
4972
5686
|
r.descriptionDerived ? React.createElement('span', { className: 'dsm-tag' }, t('memory.derived')) : null),
|
|
4973
5687
|
React.createElement('div', { className: 'dsm-status ' + statusCls }, t(statusKey)),
|
|
4974
5688
|
React.createElement('div', { className: 'dsm-row-actions' },
|
|
@@ -4984,8 +5698,10 @@ function callApi(path, options) {
|
|
|
4984
5698
|
var isGlobal = meta.global === true || name === 'global'
|
|
4985
5699
|
var isOrphan = name === '' && !bucket.meta
|
|
4986
5700
|
var open = !collapsed['s:' + name]
|
|
4987
|
-
//
|
|
4988
|
-
|
|
5701
|
+
// 两种「不进上下文」要分开:游离记忆(没有归属场景)是**真问题**,用错误色;
|
|
5702
|
+
// 场景只是没启用是**正常状态**(同时只有一个场景在用),整块灰色置灰即可 ——
|
|
5703
|
+
// 此前两者共用一个红类,关着的场景一片红,读起来像出了错。
|
|
5704
|
+
return React.createElement('div', { key: 's:' + name, className: 'dsm-source' + (isOrphan ? ' dsm-rule-shadowed' : meta.active === false ? ' dsm-rule-off' : '') },
|
|
4989
5705
|
React.createElement('div', { className: 'dsm-source-head' },
|
|
4990
5706
|
React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: function () { toggleCollapse('s:' + name) } },
|
|
4991
5707
|
React.createElement('span', { className: 'dsm-source-title', title: sceneLabel(data.scenes, name) }, sceneLabel(data.scenes, name)),
|
|
@@ -4993,9 +5709,12 @@ function callApi(path, options) {
|
|
|
4993
5709
|
isShared ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.shared')) : null,
|
|
4994
5710
|
isGlobal ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.global.tag')) : null,
|
|
4995
5711
|
isOrphan ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('memory.scene.orphan.tag')) : null,
|
|
4996
|
-
meta.active === false ? React.createElement('span', { className: 'dsm-tag
|
|
5712
|
+
meta.active === false ? React.createElement('span', { className: 'dsm-tag' }, t('memory.scene.off')) : null,
|
|
4997
5713
|
isOrphan ? null : React.createElement('span', { className: 'dsm-note' }, meta.description || '')),
|
|
4998
5714
|
React.createElement('div', { className: 'dsm-source-actions' },
|
|
5715
|
+
// 每个场景一张卡:卡内的「全选 / 取消全选」只作用于**这张卡里的**记忆
|
|
5716
|
+
// —— 用户说的「记忆的不同场景都缺少全选清空」。
|
|
5717
|
+
isOrphan || !bucket.rules.length ? null : React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-bulk', disabled: busy || anyLocked || !toggleableIds(bucket.rules).length, onClick: function () { setMemories(toggleableIds(bucket.rules), !allMemoriesOn(bucket.rules)) } }, bulkPair(allMemoriesOn(bucket.rules) ? t('bulk.unselectAll') : t('bulk.selectAll'), allMemoriesOn(bucket.rules) ? t('bulk.selectAll') : t('bulk.unselectAll'))),
|
|
4999
5718
|
isOrphan ? null : React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { openCreate(name) } }, t('memory.scene.new')))),
|
|
5000
5719
|
open ? React.createElement('div', { className: 'dsm-source-body' },
|
|
5001
5720
|
isOrphan ? React.createElement('div', { className: 'dsm-source-note' }, t('memory.scene.orphan.hint')) : null,
|
|
@@ -5040,11 +5759,13 @@ function callApi(path, options) {
|
|
|
5040
5759
|
React.createElement('h2', { className: 'dsm-title' }, t('memory.title'))),
|
|
5041
5760
|
React.createElement('p', { className: 'dsm-desc' }, t('memory.desc'))),
|
|
5042
5761
|
React.createElement('div', { className: 'dsm-actions' },
|
|
5043
|
-
|
|
5762
|
+
refreshButton(t, busy || data.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }),
|
|
5044
5763
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { openCreate(sceneFilter || '') } }, t('memory.btn.new')),
|
|
5045
5764
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { setResult(null); setImportScene(''); setModal({ type: 'import' }) } }, t('memory.import')),
|
|
5046
5765
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || !exportableRules.length, onClick: function () { setResult(null); setExportState({ busy: false, result: null }) } }, t('export.memories')),
|
|
5047
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openTrash }, t('memory.trash.open'))
|
|
5766
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openTrash }, t('memory.trash.open')),
|
|
5767
|
+
// 列表级动作与页头其余动作同栏(用户 2026-09-17 裁定):作用集 = 当前筛选后的可见记忆。
|
|
5768
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary dsm-btn-bulk', disabled: busy || anyLocked || !bulkIds.length, onClick: function () { setMemories(bulkIds, !bulkAllOn) } }, bulkPair(bulkAllOn ? t('bulk.unselectAll') : t('bulk.selectAll'), bulkAllOn ? t('bulk.selectAll') : t('bulk.unselectAll'))))),
|
|
5048
5769
|
React.createElement('div', { className: 'dsm-summary' },
|
|
5049
5770
|
React.createElement('div', { key: 'total', className: 'dsm-stat' }, React.createElement('strong', null, stats.total || 0), t('memory.stat.total')),
|
|
5050
5771
|
React.createElement('div', { key: 'scenes', className: 'dsm-stat' }, React.createElement('strong', null, (data.scenes || []).length), t('memory.stat.sceneCount')),
|
|
@@ -5124,7 +5845,7 @@ function callApi(path, options) {
|
|
|
5124
5845
|
React.createElement('span', { className: 'dsm-label' }, t('memory.field.form')),
|
|
5125
5846
|
React.createElement(SourceSelect, { options: RULE_FORM_OPTIONS, value: editor.form || 'flat', onChange: function (v) { setEditor(Object.assign({}, editor, { form: v })) } }),
|
|
5126
5847
|
editor.form === 'bundle'
|
|
5127
|
-
? React.createElement('div', { className: 'dsm-field' },
|
|
5848
|
+
? React.createElement('div', { className: 'dsm-field', ref: attachFieldRef },
|
|
5128
5849
|
React.createElement('span', { className: 'dsm-label' }, t('memory.field.attach')),
|
|
5129
5850
|
React.createElement('input', { ref: attachRef, type: 'file', multiple: true, className: 'dsm-hidden-input', onChange: onAttachPicked }),
|
|
5130
5851
|
(editor.attachments || []).length || (editor.files || []).length
|