dsh-plugin-tool-management 0.1.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -11,9 +11,7 @@ window.__ModuleLoader__.load({
11
11
  const React = require('react')
12
12
 
13
13
  const CSS =
14
- '.dsm-summary.dsm-summary-4{grid-template-columns:repeat(4,minmax(0,1fr))}' +
15
- '@media(max-width:760px){.dsm-summary.dsm-summary-4{grid-template-columns:repeat(2,minmax(0,1fr))}}' +
16
- '@container(max-width:520px){.dsm-summary.dsm-summary-4{grid-template-columns:1fr}}' +
14
+ '.dsm-summary.dsm-summary-3{grid-template-columns:repeat(3,minmax(0,1fr))}' +
17
15
  '.dsm-version{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;letter-spacing:.3px}' +
18
16
  '.dsm-failed{color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:nowrap}' +
19
17
  '.dsm-row-hint{grid-column:1/-1;margin:-4px 0 9px;color:#d49245;font-size:11px;line-height:16px}' +
@@ -29,10 +27,12 @@ window.__ModuleLoader__.load({
29
27
  '.dsm-tool-param-type{color:var(--dsw-alias-label-tertiary);min-width:56px}' +
30
28
  '.dsm-tool-param-desc{flex:1;color:var(--dsw-alias-label-secondary);word-break:break-all}' +
31
29
  '.dsm-modal textarea.dsm-control.dsm-textarea-sm{min-height:88px}' +
30
+ '.dsm-modal textarea.dsm-control.dsm-textarea-md{min-height:200px}' +
31
+ '.dsm-modal textarea.dsm-control.dsm-textarea-lg{min-height:300px}' +
32
32
  '.dsm-detail-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
33
33
  '.dsm-note-user{color:var(--dsw-alias-label-tertiary)}' +
34
34
  `
35
- .dsm-tabs{display:flex;gap:4px;border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:14px}.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;align-items:center;gap:4px;flex-wrap:wrap}.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;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px}.dsm-actions{display:flex;flex-wrap:wrap;gap:8px;margin-left:0;flex:none}
35
+ .dsm-tabs{display:flex;gap:4px;border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:14px}.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;align-items:center;gap:4px;flex-wrap:wrap}.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;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px}.dsm-actions{display:flex;flex-wrap:wrap;gap:8px;margin-left:0;flex:none}
36
36
  .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,.dsm-btn-quiet{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary)}.dsm-btn-quiet{min-height:28px;padding:0 9px;color:var(--dsw-alias-label-secondary);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: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}
37
37
  .dsm-summary{display:grid;grid-template-columns:repeat(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{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}
38
38
  .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)}
@@ -43,6 +43,63 @@ window.__ModuleLoader__.load({
43
43
  .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}
44
44
  @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}}@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}}
45
45
  .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-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}
46
+ .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)}
47
+ .dsm-combo-row{display:flex;gap:8px}.dsm-combo-row .dsm-control{flex:1}
48
+ .dsm-attach-list{display:flex;flex-direction:column;gap:6px}.dsm-attach-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
49
+ .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}}
50
+ /* ── 勾选段(场景档案编辑器 / 任何「多选一组条目」的地方共用同一套排版)────────────
51
+ .dsm-seg 一段(标题栏 + 固定高度滚动体 + 脚注);高度固定,勾选内容增减不会让弹窗跳动。
52
+ .dsm-pick 一行选项(复选框 + 名称/说明 + 右侧指标 + 可选行内动作)。 */
53
+ .dsm-seg{display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
54
+ .dsm-seg-head{display:flex;min-height:42px;align-items:center;gap:10px;padding:0 12px;border-bottom:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}
55
+ .dsm-seg-title{min-width:0;font-size:13px;font-weight:620;white-space:nowrap}
56
+ .dsm-seg-count{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}
57
+ .dsm-seg-actions{display:flex;align-items:center;gap:6px;margin-left:auto;flex:none}
58
+ .dsm-seg-body{display:flex;height:216px;padding:6px;overflow:auto;flex-direction:column}
59
+ .dsm-seg-foot{display:flex;align-items:center;gap:8px;padding:7px 12px;border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
60
+ .dsm-pick{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:7px}
61
+ .dsm-pick:hover{background:var(--dsw-alias-interactive-bg-hover)}
62
+ .dsm-pick input[type=checkbox]{flex:none;width:15px;height:15px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
63
+ .dsm-pick-main{min-width:0;flex:1}
64
+ .dsm-pick-name{overflow:hidden;font-size:13px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}
65
+ .dsm-pick-desc{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;text-overflow:ellipsis;white-space:nowrap}
66
+ .dsm-pick-meta{display:flex;align-items:center;gap:6px;flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}
67
+ .dsm-pick-actions{display:flex;align-items:center;gap:6px;flex:none}
68
+ .dsm-pick-empty{padding:18px 12px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}
69
+ .dsm-archive-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:12px}
70
+ /* 档案弹窗固定高度:加/删段、进出「选工具」子视图都不改变弹窗尺寸。 */
71
+ .dsm-modal-archive{height:min(720px,calc(100vh - 48px))}
72
+ .dsm-modal-archive .dsm-form{min-height:0;gap:10px;overflow:auto}
73
+ /* 段体高度:min-height 给下限、flex-shrink:0 保证下限优先(四项都在时也各占 300px),
74
+ 装不下由 .dsm-form 整体滚动。列表自身 overflow:auto,永远是它出滚动条。
75
+ 注意不要写成 flex:1 平分——那样四项分 720px 每段只剩约 155px,比原来的固定 216px 还矮。 */
76
+ .dsm-modal-archive .dsm-seg{min-height:300px;flex:0 0 auto}
77
+ .dsm-modal-archive .dsm-seg-body{height:auto;flex:1 1 auto}
78
+ /* ── 段内筛选(场景/记忆等条目多的选段用;条目少时不必显示)────────────────────
79
+ .dsm-seg-filter 段体顶部的搜索行;.dsm-seg-cards 场景卡片列表(比纯勾选行信息量大)。 */
80
+ .dsm-seg-filter{display:flex;align-items:center;gap:8px;padding:6px 6px 0}
81
+ .dsm-seg-filter .dsm-control{min-height:30px;font-size:12px}
82
+ .dsm-seg-group{display:flex;align-items:center;gap:8px;padding:7px 8px 3px;color:var(--dsw-alias-label-tertiary);font-size:11px}
83
+ .dsm-seg-group-line{height:1px;flex:1;background:var(--dsw-alias-border-l1)}
84
+ .dsm-seg-sub{padding-left:22px}
85
+ .dsm-seg-more{padding:6px 8px 2px;color:var(--dsw-alias-label-tertiary);font-size:11px}
86
+ .dsm-scene-card{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)}
87
+ .dsm-scene-card + .dsm-scene-card{margin-top:6px}
88
+ .dsm-scene-card-head{display:flex;min-height:36px;align-items:center;gap:9px;padding:6px 9px}
89
+ .dsm-scene-card-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
90
+ .dsm-scene-card-body{border-top:1px solid var(--dsw-alias-border-l1);padding:4px 6px}
91
+ .dsm-tools-grid{display:flex;max-height:180px;padding:6px;overflow:auto;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}
92
+ .dsm-tools-chips{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
93
+ .dsm-chip{display:inline-flex;min-height:22px;align-items:center;gap:5px;padding:0 7px;border:1px solid var(--dsw-alias-border-l3);border-radius:6px;color:var(--dsw-alias-label-secondary);font-size:11px}
94
+ .dsm-chip-deny{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
95
+ .dsm-chip button{padding:0;border:0;background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1;cursor:pointer}
96
+ /* ── 高级选项折叠区(人设表单;默认收起,展开后才拉候选数据)────────────────── */
97
+ .dsm-adv{border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1)}
98
+ .dsm-adv-head{display:flex;width:100%;min-height:40px;align-items:center;gap:8px;padding:0 11px;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-align:left;cursor:pointer}
99
+ .dsm-adv-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
100
+ .dsm-adv-caret{color:var(--dsw-alias-label-tertiary);font-size:10px}
101
+ .dsm-adv-body{display:flex;flex-direction:column;gap:12px;padding:11px;border-top:1px solid var(--dsw-alias-border-l1)}
102
+ .dsm-adv-note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
46
103
  `
47
104
 
48
105
  function ensureCss() {
@@ -96,22 +153,411 @@ window.__ModuleLoader__.load({
96
153
  return v ? React.createElement('span', { className: 'dsm-version' }, 'v' + v) : null
97
154
  }
98
155
 
156
+ // 曾经把导出写成 apply 方法体的最后两条语句(`module.exports.DICT = ...` /
157
+ // `module.exports._pages = ...`),而 apply 开头是
158
+ // `const slots = ctx.get('slots'); if (slots === undefined) return`。
159
+ // 对象字面量在 factory 求值时就定了型,方法体里的追加却可能永远不执行——
160
+ // 于是导出里查无 _pages。**任何导出都必须在 factory 作用域落地。**
161
+ const _pages = {}
162
+ // 中英两份词典。声明在 factory 作用域:apply 里的 ctx.locale 只是把它交给宿主,
163
+ // 契约测试则直接从导出里读它(apply 提前返回也必须拿得到)。
164
+ const DICT = {
165
+ zh: {
166
+ "title": "技能", "desc": "管理本机技能:启停、导入、创建与回收站;同名技能可指定哪个版本生效。", "link.project": "GitHub", "link.feedback": "问题反馈",
167
+ "btn.create": "创建技能", "btn.import": "导入", "btn.refresh": "刷新", "btn.cancel": "取消", "btn.close": "关闭", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.import.now": "安装", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用", "btn.open.editor": "用系统编辑器打开", "btn.activate": "启用这个", "btn.activate.title": "把当前来源的版本设为同名技能的首选并启用(不改动任何源文件)", "btn.unprefer": "取消首选", "btn.unprefer.title": "取消同名首选,回到按来源优先级自动选择",
168
+ "btn.custom.add": "添加目录", "btn.custom.remove": "移除", "btn.openDir": "选择",
169
+ "btn.source.remove": "移除来源", "btn.source.remove.title": "不再读取这个来源(源文件不动,可恢复)", "btn.source.restore": "恢复读取",
170
+ "removed.title": "已移除的来源", "removed.count": "{count} 个", "removed.hint": "插件不再读取这些目录;源文件与技能都还在磁盘上,点「恢复读取」即可重新纳入",
171
+ "confirm.source.remove.title": "移除来源?", "confirm.source.remove.desc": "「{name}」将不再被读取:它下面的技能不再出现在本页,也不再参与同名优先级;已启用/停用的选择会保留。",
172
+ "confirm.source.remove.hint": "不删除任何文件。想恢复时点这一组的「恢复读取」。",
173
+ "result.sourceRemoved": "已移除来源:{name}(不再读取)", "result.sourceRestored": "已恢复读取:{name}",
174
+ "custom.add.title": "添加自定义技能目录", "custom.add.submit": "添加",
175
+ "custom.add.path": "目录绝对路径", "custom.add.path.placeholder": "例如 D:\\skills\\my-skills",
176
+ "custom.add.openDir.title": "浏览并选择文件夹", "custom.add.picker.title": "选择文件夹",
177
+ "custom.add.label": "显示名称(可选)", "custom.add.label.placeholder": "自定义目录",
178
+ "custom.add.help": "只读接入该目录;目录与源文件都不会被修改",
179
+ "confirm.custom.remove.title": "移除自定义目录",
180
+ "confirm.custom.remove.desc": "移除后该目录的技能不再列出(文件不受影响):{name}",
181
+ "result.custom.added": "已添加自定义目录:{path}", "result.custom.removed": "已移除自定义目录:{name}",
182
+ "status.enabled": "已启用", "status.disabled": "已停用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.shadowed.hint": "同名技能「{name}」正在生效;点「启用这个」可改用当前来源的版本", "status.preferred": "同名首选", "status.readonly": "源文件只读", "status.notDeletable": "不可删除", "status.notDeletable.hint": "用户级来源的技能只能停用,不能删除(删除只对项目级来源开放)", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已启用", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
183
+ "summary.total.one": "{count} 个技能", "summary.total.other": "{count} 个技能", "summary.enabled.one": "{count} 个已启用", "summary.enabled.other": "{count} 个已启用", "summary.disabled.one": "{count} 个已停用", "summary.disabled.other": "{count} 个已停用", "summary.issues.one": "{count} 个诊断项", "summary.issues.other": "{count} 个诊断项", "summary.group.one": "{count} 个技能", "summary.group.other": "{count} 个技能", "table.skill": "技能名称与描述", "table.status": "调用状态",
184
+ "filter.source": "来源", "filter.all": "全部来源", "filter.option": "{name}({count})", "search": "搜索", "search.placeholder": "搜索技能名称或描述", "search.clear": "清除搜索",
185
+ "empty.search": "没有匹配的技能", "empty.source": "该来源不存在或暂无技能", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
186
+ "source.external.note": "只读接入,启停不改写源文件", "source.dsh.note": "可创建、导入、移到回收站;启停只改管理器状态",
187
+ "detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "元数据", "detail.noFrontmatter": "该技能未提供元数据。", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
188
+ "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": "对话里 create_skill 建的是用户级技能",
189
+ "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.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}",
190
+ "trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
191
+ "confirm.trash.title": "移到回收站?", "confirm.trash.desc": "「{name}」将移入回收站,可恢复", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "「{name}」将永久删除,无法恢复",
192
+ "result.created": "已创建技能:{name}", "result.imported": "导入完成:{names}", "result.importPartial": "已导入:{imported};已跳过同名技能:{skipped}", "result.importSkipped": "未导入任何技能;已跳过同名技能:{names}", "result.importEmpty": "未导入任何技能。", "result.importWarnings": "{result};警告:{warnings}", "result.restored": "已恢复技能:{name}", "result.trashed": "已移到回收站:{name}", "result.deleted": "已永久删除:{name}", "result.updated": "状态已更新。", "result.preferred": "已改为生效:{name}({source})", "result.unpreferred": "已取消同名首选:{name}", "result.opened": "已用系统默认程序打开:{path}", "error.action": "操作失败:{error}",
193
+ "warning.scan.truncated": "技能目录较大或嵌套过深,部分技能未显示:{path}", "warning.state.invalid": "状态文件不可读,已安全停用所有技能:{path}", "warning.backupUncleaned": "旧版本备份未清理:{path}({error})", "warning.project.unavailable": "无法从宿主读取活动工作区,项目技能未显示:{path}",
194
+ "error.root.readonly": "该来源不允许{action}", "error.root.disabled": "「{name}」这个来源已整体停用,请先在该来源行启用它", "error.root.unknown": "未知技能来源:{root}", "error.root.unsafe": "项目技能目录不安全,拒绝写入:{path}", "error.skill.notFound": "技能不存在: {name}", "error.skill.noFrontmatter": "缺少 frontmatter,无法{action}:{name}", "error.skill.notLoadable": "技能结构不完整,无法{action}: {name}", "error.skill.notDeletable": "该来源的技能不能删除(技能只能停用):删除只在项目级来源(<项目>/.dsh/skills)提供",
195
+ "error.source.notFound": "路径不存在: {path}", "error.source.symlink": "不支持符号链接来源:{path}", "error.source.unrecognized": "无法识别的 skill 来源: {path}", "error.source.tooDeep": "skill 来源目录层级超过 {depth} 层: {path}",
196
+ "error.import.overlap": "该目录与 DSH 技能目录重叠", "error.import.emptySource": "目录下没有 skill 条目:{path}", "error.import.invalidName": "名称不合法:{name}", "error.import.duplicateName": "批量来源中存在多个同名技能: {name}", "error.import.failed": "导入失败", "error.import.rollbackFailed": "覆盖导入回滚失败,备份保留在: {path}({error})",
197
+ "error.upload.path": "上传内容包含非法路径:{path}", "error.upload.encoding": "上传内容编码无效", "error.upload.empty": "上传内容为空", "error.upload.tooMany": "上传文件过多,最多 {limit} 个", "error.upload.tooLarge": "上传内容过大,限制为 {limit} 字节", "error.upload.archiveTooLarge": "ZIP 压缩包过大,限制为 {limit} 字节", "error.upload.duplicate": "上传内容包含重复路径:{path}", "error.upload.zipInvalid": "ZIP 压缩包无法解压",
198
+ "error.trash.notFound": "回收站条目不存在: {id}", "error.trash.conflict": "无法恢复,同名技能已存在: {name}", "error.trash.invalid": "回收站条目路径非法: {id}", "error.trash.projectUnavailable": "原项目当前不在活动工作区中,无法恢复:{path}", "error.trash.rollbackFailed": "移入回收站回滚失败,未恢复内容保留在: {path}({error})",
199
+ "error.state.invalid": "技能管理器状态文件不可读,已拒绝覆盖:{path}",
200
+ "error.create.descriptionRequired": "技能简介不能为空", "error.create.bodyRequired": "技能正文不能为空", "error.create.tooLarge": "技能内容过长", "error.create.conflict": "同名技能已存在: {name}",
201
+ "error.proto.forbidden": "禁止的修改请求(缺少客户端标记)", "error.proto.forbiddenHost": "禁止的请求来源(非法 Host)", "error.proto.contentType": "请求体必须是 application/json", "error.proto.method": "不支持的请求方法", "error.proto.unknownAction": "未知操作", "error.proto.bodyTooLarge": "请求体过大", "error.proto.invalidJson": "请求体不是合法 JSON", "error.proto.nonJson": "服务端返回非 JSON 响应(HTTP {status})",
202
+ "diagnostic.frontmatter.missing": "缺少完整 YAML frontmatter", "diagnostic.name.missing": "frontmatter 缺少 name", "diagnostic.name.invalid": "技能名需 kebab-case:{name}", "diagnostic.description.missing": "frontmatter 缺少 description", "diagnostic.invocation.invalid": "调用策略字段值无效", "diagnostic.shadowed": "被更高优先级来源 {root} 覆盖",
203
+ "action.enable": "启用", "action.disable": "停用", "action.create": "创建", "action.delete": "删除", "action.restore": "恢复", "action.toggle": "启用或停用",
204
+ "mcp.desc": "管理本机 MCP 服务:新增、启停、重启与工具级开关。",
205
+ "mcp.level.all": "全部级别", "mcp.level.project": "Profile 级", "mcp.level.global": "全局", "mcp.level.loader": "已加载",
206
+ "mcp.stat.total": "个服务", "mcp.stat.enabled": "个已启用", "mcp.stat.tools": "个工具",
207
+ "mcp.search": "搜索服务", "mcp.search.placeholder": "搜索服务名称、地址或命令",
208
+ "mcp.btn.new": "新增服务", "mcp.btn.add": "添加", "mcp.btn.detail": "详情", "mcp.btn.edit": "编辑", "mcp.btn.restart": "重启", "mcp.btn.remove": "删除",
209
+ "mcp.btn.enableAll": "全部启用", "mcp.btn.disableAll": "全部停用",
210
+ "mcp.loading": "正在加载 MCP 服务…", "mcp.empty": "暂无 MCP 服务,点「新增服务」添加", "mcp.empty.search": "没有匹配的服务。",
211
+ "mcp.servers.count": "{count} 个服务", "mcp.tools.count": "{count} 个工具", "mcp.duplicate": "重复 id",
212
+ "mcp.table.name": "服务名称与地址", "mcp.table.transport": "传输与工具", "mcp.table.status": "运行状态",
213
+ "mcp.live.notLoaded": "未加载", "mcp.live.failed": "启动失败", "mcp.live.stopped": "未运行", "mcp.live.loading": "加载中", "mcp.live.noTools": "无工具", "mcp.live.running": "运行中",
214
+ "mcp.live.failedHint": "启动失败,检查配置后点「重启」重试", "mcp.live.noToolsHint": "已连接但没有工具:服务端可能未就绪",
215
+ "mcp.note.prefix": "备注:", "mcp.toggleServer": "启停服务", "mcp.toggleTool": "启停工具",
216
+ "mcp.msg.ok": "操作成功", "mcp.msg.failed": "操作失败", "mcp.msg.loadFailed": "加载失败", "mcp.msg.warn": "操作完成,但加载器有提示:{warning}",
217
+ "mcp.msg.toolOn": "已启用工具:{name}", "mcp.msg.toolOff": "已停用工具:{name}",
218
+ "mcp.form.addTitle": "新增 MCP 服务", "mcp.form.editTitle": "编辑 MCP 服务:",
219
+ "mcp.field.serverName": "服务名称 serverName", "mcp.field.serverName.hint": "1-32 位 [A-Za-z0-9_-],补丁里按此名注册",
220
+ "mcp.field.transport": "传输方式", "mcp.field.level": "级别",
221
+ "mcp.field.level.project": "Profile 级(本应用:{path})", "mcp.field.level.global": "全局(跨 Profile:{path})",
222
+ "mcp.field.url": "服务 URL", "mcp.field.url.hint": "需以 http(s):// 开头",
223
+ "mcp.field.command": "启动命令", "mcp.field.args": "参数(空格或换行分隔)",
224
+ "mcp.field.env": "环境变量(每行 key=value)", "mcp.field.env.hint": "路径按系统路径写法填(Windows 用 \\,macOS/Linux 用 /)。",
225
+ "mcp.field.headers": "请求头(每行 key=value)",
226
+ "mcp.detail.title": "服务详情:", "mcp.detail.config": "配置", "mcp.detail.showSecret": "显示密钥", "mcp.detail.hideSecret": "隐藏密钥",
227
+ "mcp.detail.entryId": "条目 ID", "mcp.detail.status": "运行状态", "mcp.detail.registered": "该服务已登记在 Loader 中。",
228
+ "mcp.detail.note": "备注(仅本机可见)", "mcp.detail.note.placeholder": "例如:A 不可用时改用 B 兜底", "mcp.detail.note.save": "保存备注",
229
+ "mcp.detail.tools": "工具({count})",
230
+ "mcp.field.headersShort": "请求头", "mcp.field.envShort": "环境变量",
231
+ "mcp.tools.loading": "正在获取工具列表…", "mcp.tools.loadFailed": "加载工具失败:", "mcp.tools.none": "该服务暂无已注册工具。",
232
+ "mcp.tools.note": "停用的工具对模型不可见,改动即时生效",
233
+ "mcp.remove.title": "删除 MCP 服务", "mcp.remove.desc": "确定要删除「{name}」?配置将从补丁文件移除、工具立即下线,不可撤销",
234
+ "mcp.restarting": "重启中… {name}(已等待 {seconds} 秒,完成后自动刷新)", "mcp.err.warnings": "读取补丁告警:",
235
+ "root.dsh": "DSH 技能", "root.hub": "导入技能", "root.agents": "公共 Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "项目 DSH", "root.projectAgents": "项目 Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor",
236
+ "memory.title": "记忆",
237
+ "tabs.scenes": "场景", "tabs.skills": "技能", "tabs.subagents": "子智能体", "tabs.prompts": "提示词", "tabs.memory": "记忆", "tabs.sessions": "会话",
238
+ "scenes.title": "场景", "scenes.desc": "管理场景:可以预设不同的使用场景,包含 MCP、Skills、子智能体、记忆",
239
+ "scenes.stat.total": "个场景", "scenes.stat.active": "个已启用", "scenes.stat.archives": "个有档案",
240
+ "scenes.noDesc": "还没有描述(点「改描述」补一句)", "scenes.memCount": "{count} 条记忆",
241
+ "scenes.profile.summary": "已配:{parts}", "scenes.profile.empty": "档案为空",
242
+ "scenes.profile.mcp": "MCP {count} 台", "scenes.profile.skills": "技能 {count} 个", "scenes.profile.subagents": "子智能体 {count} 个", "scenes.profile.memories": "记忆 {count} 条",
243
+ "scenes.hasArchive": "档案", "scenes.mcp.hint": "添加「MCP 工具集」后,勾选要启用的服务器(含未运行的);「选工具」可细化到具体工具。",
244
+ "scenes.seg.selectAll": "全选", "scenes.seg.clear": "清空", "scenes.seg.checked": "{checked}/{total} 已勾选",
245
+ "scenes.archive.sectionOff": "未定义",
246
+ "scenes.archive.summary": "档案:MCP {mcp} 台 · 技能 {skills} 个 · 子智能体 {subagents} 个 · 记忆 {memories} 条",
247
+ "scenes.archive.note": "段未定义 = 不改动该域;段已定义但一项不勾 = 全部停用(记忆段只影响注入,不会删文件)。",
248
+ "scenes.mcp.noServers": "还没有可选的 MCP 服务器", "scenes.mcp.toolCount": "{count} 个工具", "scenes.skills.empty": "还没有可选的技能",
249
+ "scenes.mcp.allTools": "全部工具", "scenes.mcp.pickedCount": "指定 {count} 个工具", "scenes.mcp.notRunning": "未运行", "scenes.mcp.pickTools": "选工具",
250
+ "scenes.mcp.toolsOf": "工具明细", "scenes.mcp.back": "返回", "scenes.mcp.noTools": "该服务器当前没有可列出的工具(未运行或无工具)", "scenes.mcp.drillHint": "勾选 = 该场景下启用;不勾 = 停用。整台勾选时默认全部工具。",
251
+ "scenes.skills.hint": "添加「技能集」后,勾选该场景下启用的技能。", "scenes.subagents.hint": "添加「子智能体绑定」后,勾选本场景可调用的人设。", "scenes.subagents.empty": "还没有人设——到「子智能体」页创建。",
252
+ "scenes.filter.skills": "筛选技能(名称)", "scenes.filter.subagents": "筛选人设(名称或描述)", "scenes.filter.servers": "筛选服务器",
253
+ "scenes.mem.hint": "添加「记忆」段后,勾选本场景要注入的记忆;不勾的记忆不会进系统提示词(文件保留)。",
254
+ "scenes.mem.title": "记忆", "scenes.mem.search": "在场景内筛选记忆(名称或描述)", "scenes.mem.searchScene": "筛选场景(名称、描述或记忆名)",
255
+ "scenes.mem.noMatch": "没有匹配项", "scenes.mem.noScenes": "还没有场景——到「场景」页创建。", "scenes.mem.emptyScene": "该场景还没有记忆",
256
+ "scenes.mem.pick": "选记忆", "scenes.mem.drill": "记忆明细", "scenes.mem.drillHint": "勾选 = 该场景下注入这条记忆;不勾 = 不注入(文件与内容都不动)。",
257
+ "scenes.mem.sceneCount": "{checked}/{total} 条已勾选", "scenes.mem.alwaysOn": "始终注入",
258
+ "memory.mode.current": "当前模式", "memory.mode.set": "设为当前模式", "memory.mode.exit": "退出模式",
259
+ "memory.archive.edit": "档案", "memory.archive.title": "场景档案",
260
+ "memory.archive.tools": "MCP 工具集", "memory.archive.skills": "技能集", "memory.archive.subagents": "子智能体绑定", "memory.archive.memories": "记忆",
261
+ "memory.archive.removeSection": "移除段", "memory.archive.save": "保存到场景",
262
+ "memory.archive.addTools": "+ 添加 MCP 工具集", "memory.archive.addSkills": "+ 添加技能集", "memory.archive.addSubagents": "+ 添加子智能体绑定", "memory.archive.addMemories": "+ 添加记忆",
263
+ "memory.archive.stale": "失效项(已不存在,已跳过): {items}",
264
+ "memory.result.archiveSaved": "已保存场景档案:{name}", "memory.result.modeSet": "已进入模式:{name}", "memory.result.modeExited": "已退出模式",
265
+ "memory.desc": "管理场景记忆:启用场景里的记忆自动进入系统提示词;场景留空 = 全局注入。",
266
+ "memory.btn.refresh": "刷新", "memory.btn.new": "新建记忆", "memory.btn.create": "创建", "memory.btn.save": "保存",
267
+ "memory.btn.diagnose": "体检", "memory.btn.delete.confirm": "移入回收站",
268
+ "memory.diagnose.title": "记忆体检", "memory.diagnose.summary": "{rules} 条记忆 · {scenes} 个场景 · {issues} 个问题", "memory.diagnose.clean": "未发现问题,一切正常。",
269
+ "memory.diag.error": "错误", "memory.diag.warning": "警告", "memory.diag.info": "提示",
270
+ "memory.stat.total": "条记忆", "memory.stat.scenes": "个启用场景", "memory.stat.sceneCount": "个场景", "memory.status.sceneOff": "场景未启用", "memory.status.off": "本条已停用",
271
+ "memory.budget.label": "注入预算", "memory.budget.bytes": "{used} / {max} 字节",
272
+ "memory.budget.over": "已超限",
273
+ "memory.budget.truncated": "超出预算,段尾已列出未注入的记忆",
274
+ "memory.budget.dropped": "本次未注入:{names}",
275
+ "memory.scene.enable": "启用", "memory.scene.enableAll": "全部启用", "memory.scene.off": "未启用", "memory.scene.shared": "常开", "memory.scene.global.tag": "任何对话都注入",
276
+ "memory.scene.global": "全局", "memory.scene.count": "{count} 条记忆", "memory.scene.new": "新建记忆", "memory.scene.browse": "选择已有",
277
+ "memory.scene.empty": "该场景暂无记忆",
278
+ "memory.scene.orphan": "未归属场景", "memory.scene.orphan.tag": "不会注入", "memory.scene.orphan.hint": "这些记忆直接放在 memories/ 根层,没有归属场景,因此不会进入系统提示词。请把它们移入某个场景目录(或放到 memories/global/ 作为「全局」记忆)。",
279
+ "memory.scene.edit": "改描述", "memory.scene.editTitle": "修改场景", "memory.scene.editHelp": "只改场景的名称显示与描述,记忆文件不受影响。",
280
+ "memory.scene.field.name": "场景名", "memory.scene.field.name.placeholder": "例如 办公",
281
+ "memory.scene.field.name.hint": "创建后不可改名。",
282
+ "memory.scene.field.name.lock": "场景名创建后不可修改",
283
+ "memory.scene.field.desc": "描述(可选)", "memory.scene.field.desc.placeholder": "一句话说明这个场景是干什么的",
284
+ "memory.search.placeholder": "搜索名称 / 描述 / 场景", "memory.filter.all": "全部场景",
285
+ "memory.empty": "还没有记忆,点「新建记忆」", "memory.empty.search": "没有匹配的记忆",
286
+ "memory.loading": "正在加载…",
287
+ "memory.enable": "启用", "memory.edit": "编辑", "memory.delete": "删除",
288
+ "memory.form.flat": "flat", "memory.form.bundle": "bundle", "memory.derived": "派生", "memory.fill.frontmatter": "补齐 frontmatter",
289
+ "memory.shadowed.hint": "同名 bundle 存在,本条不加载",
290
+ "memory.create.title": "新建记忆", "memory.edit.title": "编辑记忆", "memory.edit.group.lock": "编辑时场景不可修改",
291
+ "memory.field.group": "场景", "memory.field.group.placeholder": "例如 办公", "memory.field.group.hint": "留空 = 全局",
292
+ "memory.field.name": "名称", "memory.field.name.placeholder": "例如 站会流程", "memory.name.invalid": "名称不合法(非空、≤64 字符、不含 / \\ < > : \" | ? *、不以 . 开头)",
293
+ "memory.field.description": "描述", "memory.field.description.placeholder": "一句话说明用途(留空取正文首行)",
294
+ "memory.field.body": "正文(Markdown)", "memory.field.body.placeholder": "写下要记住的约定、流程和边界…",
295
+ "memory.field.form": "形态", "memory.field.attach": "附件",
296
+ "memory.attach.add": "添加附件", "memory.attach.empty": "暂无附件", "memory.attach.pending": "待上传", "memory.attach.remove": "移除附件",
297
+ "memory.attach.hint": "附件随记忆存放,不会进入提示词", "memory.attach.flat": "flat 是单文件,不能带附件", "memory.attach.tooLarge": "附件过大:{name}(上限 {limit} MB)",
298
+ "memory.delete.title": "移入回收站?", "memory.delete.desc": "「{name}」将移入回收站,可恢复",
299
+ "memory.result.created": "已创建记忆:{name}", "memory.result.updated": "已保存记忆:{name}", "memory.result.removed": "已移入回收站:{name}", "memory.result.toggled": "已更新记忆状态:{name}", "memory.result.active": "已更新启用场景",
300
+ "memory.result.restored": "已恢复记忆:{name}", "memory.result.trashRemoved": "已永久删除:{name}",
301
+ "memory.result.createdAttached": "已创建记忆:{name}(含 {count} 个附件)", "memory.result.updatedAttached": "已保存记忆:{name}(含 {count} 个附件)", "memory.result.attachFailed": "记忆已保存,但附件失败:{error}",
302
+ "memory.trash.open": "回收站", "memory.trash.title": "记忆回收站", "memory.trash.empty": "回收站为空",
303
+ "memory.trash.count": "{count} 条已删除的记忆", "memory.trash.deletedAt": "删除于 {time}",
304
+ "memory.trash.restore": "恢复", "memory.trash.purge": "永久删除",
305
+ "memory.trash.confirmTitle": "永久删除?", "memory.trash.confirmDesc": "「{name}」将永久删除,无法恢复",
306
+ "memory.btn.newScene": "新建场景", "memory.btn.deleteScene": "删除场景", "memory.btn.saveScene": "保存修改",
307
+ "memory.scene.createTitle": "新建场景",
308
+ "memory.deleteScene.title": "删除场景?", "memory.deleteScene.desc": "将删除「{name}」的场景记录与空目录(里面还有记忆或正处在当前模式时不能删;「全局」是保留场景,不可删除)",
309
+ "memory.result.sceneCreated": "已创建场景:{name}", "memory.result.sceneUpdated": "已保存场景:{name}", "memory.result.sceneRemoved": "已删除场景:{name}",
310
+ "memory.table.name": "记忆名称与描述", "memory.table.tags": "标记", "memory.table.status": "状态",
311
+ "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.sceneNotEmpty": "场景不为空,无法删除", "error.rules.notBundle": "该记忆是 flat(单文件),不能带附件", "error.rules.noFiles": "没有选择附件", "error.rules.emptyFile": "附件内容为空", "error.rules.fileTooLarge": "附件过大(单个上限 {limit} MB)", "error.rules.tooManyFiles": "一次最多 {limit} 个附件", "error.rules.sceneInMode": "该场景正处在当前模式,请先退出模式再删除",
312
+ "memory.archive.emptySection": "该段已定义但没有勾选任何条目 = 全部停用",
313
+ "subagents.title": "子智能体", "subagents.desc": "管理人设:一个人设一个文件,正文即子代理的系统提示词。",
314
+ "prompts.desc": "管理全局指令基线:应用后写入 AGENTS.md,新会话生效。",
315
+ "sessions.desc": "管理已归档会话:可恢复或永久删除,超保留期自动清理。",
316
+ "subagents.new": "新建人设", "subagents.empty": "还没有人设——点「新建人设」创建第一个。",
317
+ "subagents.create": "新建人设", "subagents.edit": "编辑人设",
318
+ "subagents.field.name": "人设名",
319
+ "subagents.field.description": "描述", "subagents.field.description.placeholder": "例如 擅长 Java 后端实现与重构",
320
+ "subagents.field.model": "模型", "subagents.field.model.placeholder": "自定义模型 id", "subagents.field.model.hint": "留空继承主会话", "subagents.field.provider": "模型来源", "subagents.field.provider.placeholder": "sensenova", "subagents.field.provider.hint": "「模型来源」与「模型」是一对:只填模型会落在主会话的来源上,跨来源会解析失败。",
321
+ "subagents.model.inherit": "继承主会话(不指定)", "subagents.model.customOption": "自定义 / 目录里没有…",
322
+ "subagents.field.tools": "工具白名单", "subagents.field.tools.hint": "勾选 = 子代理只保留这些工具;一个都不勾 = 不限制(基础组合全集)。",
323
+ "subagents.field.toolsDeny": "工具黑名单", "subagents.field.toolsDeny.hint": "勾选 = 从子代理可见的工具里移除这些(优先级高于白名单)。适合「除了 bash 什么都能用」这类配置。",
324
+ "subagents.tools.chosen": "未选择 = 不限制", "subagents.tools.filter": "筛选工具名", "subagents.tools.empty": "宿主没有回传任何可选工具(模型可能不支持工具目录枚举)",
325
+ "subagents.tools.unavailable": "当前会话不可见", "subagents.tools.current": "当前可用", "subagents.tools.groupCurrent": "当前会话可用", "subagents.tools.groupOther": "其它 Agent 预设里可用",
326
+ "subagents.tools.count": "已选 {count} 个", "subagents.tools.remove": "移除",
327
+ "subagents.adv.title": "高级选项", "subagents.adv.inherit": "继承主会话", "subagents.adv.summary": "模型 {model} · 白名单 {allow} · 黑名单 {deny}", "subagents.adv.note": "候选工具是全部 Agent 预设工具名的并集:人设可能在任何预设下被复用,只列当前会话的工具会让换预设后的子代理启动失败。", "subagents.adv.loadFailed": "读取候选数据失败(模型目录 / 工具清单);仍可手动填写。",
328
+ "subagents.field.body": "人设提示词", "subagents.field.body.placeholder": "写下这个人设的身份、职责与工作方式…",
329
+ "subagents.result.saved": "已保存人设:{name}", "subagents.result.deleted": "已删除人设:{name}",
330
+ "subagents.delete.title": "删除人设?", "subagents.delete.desc": "将删除「{name}」的人设文件,不可恢复",
331
+ "subagents.import": "导入", "subagents.import.title": "导入人设", "subagents.import.hint": "支持 .md 或 .zip(可多选、可拖入):一个 .md = 一个人设,文件名即人设名;同名自动跳过。",
332
+ "subagents.result.imported": "已导入 {count} 个人设:{names}",
333
+ "memory.import": "导入记忆", "memory.import.title": "导入记忆", "memory.import.scene": "导入到场景",
334
+ "memory.import.sceneHint": "留空 = 全局;zip 内的目录名会当作场景名。",
335
+ "memory.import.hint": "支持 .md 或 .zip(可多选、可拖入):一个 .md = 一条记忆;zip 内 <场景>/<名>/SKILL.md 按 bundle 导入,同层文件作附件;同名自动跳过。",
336
+ "memory.result.imported": "已导入 {count} 条记忆:{names}",
337
+ "memory.import.scenesCreated": "为此新建了场景:{names}",
338
+ "memory.scene.global.hint": "全局",
339
+ "import.pick": "选择文件", "import.selected": "已选 {count} 个文件", "import.clear": "清空选择", "import.submit": "安装",
340
+ "import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
341
+ "error.import.noFiles": "没有选择要导入的文件"
342
+ },
343
+ en: {
344
+ "title": "Skills", "desc": "Manage skills on this machine: enable, import, create and trash; pick which copy of a shared name is active.", "link.project": "GitHub", "link.feedback": "Issues",
345
+ "btn.create": "Create skill", "btn.import": "Import", "btn.refresh": "Refresh", "btn.cancel": "Cancel", "btn.close": "Close", "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.import.now": "Install", "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",
346
+ "btn.custom.add": "Add folder",
347
+ "btn.custom.remove": "Remove",
348
+ "btn.source.remove": "Remove source", "btn.source.remove.title": "Stop reading this source (files stay on disk, reversible)", "btn.source.restore": "Read again",
349
+ "removed.title": "Removed sources", "removed.count": "{count}", "removed.hint": "The plugin no longer reads these directories; every file and skill is still on disk — click “Read again” to bring one back",
350
+ "confirm.source.remove.title": "Remove this source?", "confirm.source.remove.desc": "“{name}” will no longer be read: its skills disappear from this page and stop taking part in name resolution; your enable/disable choices are kept.",
351
+ "confirm.source.remove.hint": "No file is deleted. Use “Read again” in this group to restore it.",
352
+ "result.sourceRemoved": "Source removed: {name} (no longer read)", "result.sourceRestored": "Reading again: {name}",
353
+ "btn.openDir": "Browse",
354
+ "custom.add.title": "Add custom skill folder",
355
+ "custom.add.submit": "Add",
356
+ "custom.add.path": "Absolute folder path",
357
+ "custom.add.path.placeholder": "e.g. D:\\skills\\my-skills",
358
+ "custom.add.openDir.title": "Browse and pick a folder",
359
+ "custom.add.picker.title": "Pick a folder",
360
+ "custom.add.label": "Display name (optional)",
361
+ "custom.add.label.placeholder": "Custom folder",
362
+ "custom.add.help": "Read-only: neither the folder nor its files are modified",
363
+ "confirm.custom.remove.title": "Remove custom folder?",
364
+ "confirm.custom.remove.desc": "Its skills stop being listed (files are untouched): {name}",
365
+ "result.custom.added": "Custom folder added: {path}",
366
+ "result.custom.removed": "Custom folder removed: {name}",
367
+ "result.opened": "Opened with the system default app: {path}",
368
+ "status.enabled": "Enabled", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.shadowed.hint": "The copy “{name}” is the active one; click “Use this one” to switch to this source instead", "status.preferred": "Manually chosen", "status.readonly": "Source read-only", "status.notDeletable": "Cannot delete", "status.notDeletable.hint": "Skills from a user-level source can only be disabled, not deleted (deletion is available for project-level sources only)", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Enabled", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
369
+ "summary.total.one": "{count} skill", "summary.total.other": "{count} skills", "summary.enabled.one": "{count} enabled", "summary.enabled.other": "{count} enabled", "summary.disabled.one": "{count} disabled", "summary.disabled.other": "{count} disabled", "summary.issues.one": "{count} diagnostic", "summary.issues.other": "{count} diagnostics", "summary.group.one": "{count} skill", "summary.group.other": "{count} skills", "table.skill": "Skill name and description", "table.status": "Invocation status",
370
+ "filter.source": "Source", "filter.all": "All sources", "filter.option": "{name} ({count})", "search": "Search", "search.placeholder": "Search skill names or descriptions", "search.clear": "Clear search",
371
+ "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",
372
+ "source.external.note": "Read-only; toggles never rewrite source files", "source.dsh.note": "Create, import, trash; toggles only change manager state",
373
+ "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.",
374
+ "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": "create_skill in chat creates user-level skills",
375
+ "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.importing": "Installing…", "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}",
376
+ "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}",
377
+ "confirm.trash.title": "Move to trash?", "confirm.trash.desc": "“{name}” moves to trash and can be restored", "confirm.delete.title": "Delete forever?", "confirm.delete.desc": "“{name}” is deleted forever and cannot be recovered",
378
+ "result.created": "Created skill: {name}", "result.imported": "Import complete: {names}", "result.importPartial": "Imported: {imported}; skipped existing skills: {skipped}", "result.importSkipped": "No skills were imported; existing skills were skipped: {names}", "result.importEmpty": "No skills were imported.", "result.importWarnings": "{result}; warnings: {warnings}", "result.restored": "Restored skill: {name}", "result.trashed": "Moved to trash: {name}", "result.deleted": "Permanently deleted: {name}", "result.updated": "Status updated.", "result.preferred": "Now active: {name} ({source})", "result.unpreferred": "Cleared the manual choice: {name}", "error.action": "Action failed: {error}",
379
+ "warning.scan.truncated": "Some skills were not shown because the directory is too large or deeply nested: {path}", "warning.state.invalid": "The manager state file could not be read; all skills are disabled and state writes are blocked until it is repaired: {path}", "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})", "warning.project.unavailable": "The active workspace could not be read from the host, so its project skills are hidden: {path}",
380
+ "error.root.readonly": "This source does not allow {action}", "error.root.disabled": "The “{name}” source is disabled as a whole — enable that source first", "error.root.unknown": "Unknown skill source: {root}", "error.root.unsafe": "The project skill directory is unsafe, so the write was refused: {path}", "error.skill.notFound": "Skill not found: {name}", "error.skill.noFrontmatter": "Skill lacks complete frontmatter, cannot {action}: {name}", "error.skill.notLoadable": "Skill structure is incomplete, cannot {action}: {name}", "error.skill.notDeletable": "Skills from this source cannot be deleted (disable them instead); deletion is available for project-level sources only (<project>/.dsh/skills)",
381
+ "error.source.notFound": "Path does not exist: {path}", "error.source.symlink": "Symlinked sources are not supported: {path}", "error.source.unrecognized": "Unrecognized skill source: {path}", "error.source.tooDeep": "Skill source directory depth exceeds {depth} levels: {path}",
382
+ "error.import.overlap": "This folder overlaps the DSH skills folder", "error.import.emptySource": "No skill entries in this folder: {path}", "error.import.invalidName": "Invalid name: {name}", "error.import.duplicateName": "Batch source contains duplicate skill names: {name}", "error.import.failed": "Import failed", "error.import.rollbackFailed": "Overwrite import rollback failed; backups kept at: {path} ({error})",
383
+ "error.upload.path": "Upload contains an invalid path: {path}", "error.upload.encoding": "Upload encoding is invalid", "error.upload.empty": "Upload is empty", "error.upload.tooMany": "Too many uploaded files; maximum {limit}", "error.upload.tooLarge": "Upload is too large; limit {limit} bytes", "error.upload.archiveTooLarge": "ZIP archive is too large; limit {limit} bytes", "error.upload.duplicate": "Upload contains a duplicate path: {path}", "error.upload.zipInvalid": "ZIP archive could not be extracted",
384
+ "error.trash.notFound": "Trash item not found: {id}", "error.trash.conflict": "Cannot restore because a skill with the same name exists: {name}", "error.trash.invalid": "Invalid trash item path: {id}", "error.trash.projectUnavailable": "The original project is not an active workspace, so this skill cannot be restored: {path}", "error.trash.rollbackFailed": "Move-to-trash rollback failed; unrecovered content was kept at: {path} ({error})",
385
+ "error.state.invalid": "The manager state file could not be read, so overwriting it was refused: {path}",
386
+ "error.create.descriptionRequired": "Skill description is required", "error.create.bodyRequired": "Skill body is required", "error.create.tooLarge": "Skill content is too large", "error.create.conflict": "A skill with the same name already exists: {name}",
387
+ "error.proto.forbidden": "Forbidden mutation request (missing client marker)", "error.proto.forbiddenHost": "Forbidden request origin (invalid host)", "error.proto.contentType": "Content type must be application/json", "error.proto.method": "Method not allowed", "error.proto.unknownAction": "Unknown action", "error.proto.bodyTooLarge": "Request body too large", "error.proto.invalidJson": "Invalid JSON request body", "error.proto.nonJson": "Server returned a non-JSON response (HTTP {status})",
388
+ "diagnostic.frontmatter.missing": "Missing complete YAML frontmatter", "diagnostic.name.missing": "Frontmatter is missing name", "diagnostic.name.invalid": "Skill name must be kebab-case: {name}", "diagnostic.description.missing": "Frontmatter is missing description", "diagnostic.invocation.invalid": "Invocation policy value is invalid", "diagnostic.shadowed": "Shadowed by higher-priority source {root}",
389
+ "action.enable": "enable", "action.disable": "disable", "action.create": "create", "action.delete": "delete", "action.restore": "restore", "action.toggle": "enabling or disabling",
390
+ "mcp.desc": "Manage this machine's MCP servers: add, enable/disable, restart, and per-tool switches.",
391
+ "mcp.level.all": "All levels", "mcp.level.project": "Profile level", "mcp.level.global": "Global", "mcp.level.loader": "Loaded",
392
+ "mcp.stat.total": "server(s)", "mcp.stat.enabled": "enabled", "mcp.stat.tools": "tool(s)",
393
+ "mcp.search": "Search servers", "mcp.search.placeholder": "Search a server name, URL or command",
394
+ "mcp.btn.new": "Add server", "mcp.btn.add": "Add", "mcp.btn.detail": "Details", "mcp.btn.edit": "Edit", "mcp.btn.restart": "Restart", "mcp.btn.remove": "Delete",
395
+ "mcp.btn.enableAll": "Enable all", "mcp.btn.disableAll": "Disable all",
396
+ "mcp.loading": "Loading MCP servers…", "mcp.empty": "No MCP servers yet — click “Add server”.", "mcp.empty.search": "No matching server.",
397
+ "mcp.servers.count": "{count} server(s)", "mcp.tools.count": "{count} tool(s)", "mcp.duplicate": "duplicate id",
398
+ "mcp.table.name": "Server name and URL", "mcp.table.transport": "Transport and tools", "mcp.table.status": "Status",
399
+ "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",
400
+ "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",
401
+ "mcp.note.prefix": "Note: ", "mcp.toggleServer": "Toggle server", "mcp.toggleTool": "Toggle tool",
402
+ "mcp.msg.ok": "Done", "mcp.msg.failed": "Operation failed", "mcp.msg.loadFailed": "Load failed", "mcp.msg.warn": "Done, but the loader reported: {warning}",
403
+ "mcp.msg.toolOn": "Enabled tool: {name}", "mcp.msg.toolOff": "Disabled tool: {name}",
404
+ "mcp.form.addTitle": "Add MCP server", "mcp.form.editTitle": "Edit MCP server: ",
405
+ "mcp.field.serverName": "Server name (serverName)", "mcp.field.serverName.hint": "1-32 chars of [A-Za-z0-9_-]; the patch registers it under this name",
406
+ "mcp.field.transport": "Transport", "mcp.field.level": "Level",
407
+ "mcp.field.level.project": "Profile level (this app: {path})", "mcp.field.level.global": "Global (across profiles: {path})",
408
+ "mcp.field.url": "Server URL", "mcp.field.url.hint": "Must start with http(s)://",
409
+ "mcp.field.command": "Command", "mcp.field.args": "Arguments (space or newline separated)",
410
+ "mcp.field.env": "Environment (one key=value per line)", "mcp.field.env.hint": "Use the platform's own path syntax (backslashes on Windows, / on macOS/Linux).",
411
+ "mcp.field.headers": "Headers (one key=value per line)",
412
+ "mcp.detail.title": "Server details: ", "mcp.detail.config": "Configuration", "mcp.detail.showSecret": "Reveal secrets", "mcp.detail.hideSecret": "Hide secrets",
413
+ "mcp.detail.entryId": "Entry id", "mcp.detail.status": "Status", "mcp.detail.registered": "This server is registered in the loader.",
414
+ "mcp.detail.note": "Note (local only)", "mcp.detail.note.placeholder": "e.g. fall back to B when A is unavailable", "mcp.detail.note.save": "Save note",
415
+ "mcp.detail.tools": "Tools ({count})",
416
+ "mcp.field.headersShort": "Headers", "mcp.field.envShort": "Environment",
417
+ "mcp.tools.loading": "Fetching the tool list…", "mcp.tools.loadFailed": "Could not load tools: ", "mcp.tools.none": "This server has no registered tools.",
418
+ "mcp.tools.note": "Disabled tools are invisible to the model; changes take effect immediately",
419
+ "mcp.remove.title": "Delete MCP server", "mcp.remove.desc": "Delete “{name}”? Its configuration is removed from the patch file and the tools go offline at once; this cannot be undone",
420
+ "mcp.restarting": "Restarting… {name} (waited {seconds}s; the list refreshes when it finishes)", "mcp.err.warnings": "Patch warnings: ",
421
+ "root.dsh": "DSH skills", "root.hub": "Imported skills", "root.agents": "Shared Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "Project DSH", "root.projectAgents": "Project Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor",
422
+ "memory.title": "Memories",
423
+ "tabs.scenes": "Scenes", "tabs.skills": "Skills", "tabs.subagents": "Subagents", "tabs.prompts": "Prompts", "tabs.memory": "Memories", "tabs.sessions": "Sessions",
424
+ "scenes.title": "Scenes", "scenes.desc": "Manage scenes: preset different usage scenes combining MCP servers, skills, subagents and memories",
425
+ "scenes.stat.total": "scene(s)", "scenes.stat.active": "enabled", "scenes.stat.archives": "with a profile",
426
+ "scenes.noDesc": "No description yet (use Edit to add one)", "scenes.memCount": "{count} memor(ies)",
427
+ "scenes.profile.summary": "Configured: {parts}", "scenes.profile.empty": "empty profile",
428
+ "scenes.profile.mcp": "{count} MCP", "scenes.profile.skills": "{count} skill(s)", "scenes.profile.subagents": "{count} subagent(s)", "scenes.profile.memories": "{count} memor(ies)",
429
+ "scenes.hasArchive": "profile", "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.",
430
+ "scenes.seg.selectAll": "Select all", "scenes.seg.clear": "Clear", "scenes.seg.checked": "{checked}/{total} selected",
431
+ "scenes.archive.sectionOff": "not defined",
432
+ "scenes.archive.summary": "Profile: {mcp} MCP server(s) · {skills} skill(s) · {subagents} subagent(s) · {memories} memory item(s)",
433
+ "scenes.archive.note": "An undefined section leaves that domain untouched; a defined-but-empty section disables everything in it (the memory section only affects injection — no file is deleted).",
434
+ "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",
435
+ "scenes.mcp.allTools": "all tools", "scenes.mcp.pickedCount": "{count} tools picked", "scenes.mcp.notRunning": "not running", "scenes.mcp.pickTools": "Pick tools",
436
+ "scenes.mcp.toolsOf": "Tool details", "scenes.mcp.back": "Back", "scenes.mcp.noTools": "No tools listed for this server (not running or no tools)", "scenes.mcp.drillHint": "Checked = enabled in this scene; unchecked = disabled. A whole-server check defaults to all tools.",
437
+ "scenes.skills.hint": "Add the skills section first, then check the skills enabled in this scene.", "scenes.subagents.hint": "Add the subagent section first, then check the personas callable in this scene.", "scenes.subagents.empty": "No personas yet — create one on the Subagents page.",
438
+ "scenes.filter.skills": "Filter skills (name)", "scenes.filter.subagents": "Filter personas (name or description)", "scenes.filter.servers": "Filter servers",
439
+ "scenes.mem.hint": "Add the memory section, then check the memories injected for this scene; unchecked memories never reach the system prompt (their files stay).",
440
+ "scenes.mem.title": "Memories", "scenes.mem.search": "Filter memories in this scene (name or description)", "scenes.mem.searchScene": "Filter scenes (name, description or memory name)",
441
+ "scenes.mem.noMatch": "Nothing matches", "scenes.mem.noScenes": "No scenes yet — create one on the Scenes page.", "scenes.mem.emptyScene": "This scene has no memories yet",
442
+ "scenes.mem.pick": "Pick memories", "scenes.mem.drill": "Memory details", "scenes.mem.drillHint": "Checked = this memory is injected for the scene; unchecked = not injected (file and content untouched).",
443
+ "scenes.mem.sceneCount": "{checked}/{total} checked", "scenes.mem.alwaysOn": "always injected",
444
+ "memory.mode.current": "Active mode", "memory.mode.set": "Set as active mode", "memory.mode.exit": "Exit mode",
445
+ "memory.archive.edit": "Profile", "memory.archive.title": "Scene profile",
446
+ "memory.archive.tools": "MCP tools", "memory.archive.skills": "Skills", "memory.archive.subagents": "Subagent binding", "memory.archive.memories": "Memories",
447
+ "memory.archive.addTools": "+ Tools", "memory.archive.addSkills": "+ Skills", "memory.archive.addSubagents": "+ Subagents", "memory.archive.addMemories": "+ Memories",
448
+ "memory.archive.removeSection": "Remove section", "memory.archive.save": "Save to scene",
449
+ "memory.archive.emptySection": "Section defined with nothing checked = all disabled", "memory.archive.stale": "Stale entries (no longer exist, skipped): {items}",
450
+ "memory.result.archiveSaved": "Scene profile saved: {name}", "memory.result.modeSet": "Entered mode: {name}", "memory.result.modeExited": "Exited mode",
451
+ "memory.desc": "Manage scene memories: memories in enabled scenes are injected into the system prompt automatically, and a blank scene means global injection.",
452
+ "memory.btn.refresh": "Refresh", "memory.btn.new": "New memory", "memory.btn.create": "Create", "memory.btn.save": "Save",
453
+ "memory.btn.diagnose": "Check", "memory.btn.delete.confirm": "Move to trash",
454
+ "memory.diagnose.title": "Memory checkup", "memory.diagnose.summary": "{rules} memories · {scenes} scenes · {issues} issues", "memory.diagnose.clean": "No issues found.",
455
+ "memory.diag.error": "Error", "memory.diag.warning": "Warning", "memory.diag.info": "Info",
456
+ "memory.stat.total": "memories", "memory.stat.scenes": "enabled scenes", "memory.stat.sceneCount": "scenes", "memory.status.sceneOff": "scene off", "memory.status.off": "this memory is off",
457
+ "memory.budget.label": "Injection budget", "memory.budget.bytes": "{used} / {max} bytes",
458
+ "memory.budget.over": "Over budget",
459
+ "memory.budget.truncated": "Over budget — the memories left out are listed at the end",
460
+ "memory.budget.dropped": "Not injected this time: {names}",
461
+ "memory.scene.enable": "Enabled", "memory.scene.enableAll": "Enable all", "memory.scene.off": "Off", "memory.scene.shared": "always on", "memory.scene.global.tag": "injected everywhere",
462
+ "memory.scene.global": "Global", "memory.scene.count": "{count} memories", "memory.scene.new": "New memory", "memory.scene.browse": "Pick existing",
463
+ "memory.scene.empty": "No memories in this scene yet",
464
+ "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 reach the system prompt. Move them into a scene directory (or into memories/global/ to make them “Global”).",
465
+ "memory.scene.edit": "Edit", "memory.scene.editTitle": "Edit scene", "memory.scene.editHelp": "Only the display name and description change; memory files are untouched.",
466
+ "memory.scene.field.name": "Scene name", "memory.scene.field.name.placeholder": "e.g. office",
467
+ "memory.scene.field.name.hint": "Cannot be renamed after creation.",
468
+ "memory.scene.field.name.lock": "The scene name cannot be changed after creation",
469
+ "memory.scene.field.desc": "Description (optional)", "memory.scene.field.desc.placeholder": "One line on what this scene is for",
470
+ "memory.search.placeholder": "Search name / description / scene", "memory.filter.all": "All scenes",
471
+ "memory.empty": "No memories yet — click “New memory”", "memory.empty.search": "No matching memories",
472
+ "memory.loading": "Loading…",
473
+ "memory.enable": "Enabled", "memory.edit": "Edit", "memory.delete": "Delete",
474
+ "memory.form.flat": "flat", "memory.form.bundle": "bundle", "memory.derived": "derived", "memory.fill.frontmatter": "Fill frontmatter",
475
+ "memory.shadowed.hint": "A bundle with the same name exists, so this one is not loaded",
476
+ "memory.create.title": "New memory", "memory.edit.title": "Edit memory", "memory.edit.group.lock": "The scene cannot be changed while editing",
477
+ "memory.field.group": "Scene", "memory.field.group.placeholder": "e.g. office", "memory.field.group.hint": "Blank = global",
478
+ "memory.field.name": "Name", "memory.field.name.placeholder": "e.g. 站会流程", "memory.name.invalid": "Invalid name (non-empty, ≤64 chars, no / \\ < > : \" | ? *, must not start with a dot)",
479
+ "memory.field.description": "Description", "memory.field.description.placeholder": "One line on what it is for (blank = first body line)",
480
+ "memory.field.body": "Body (Markdown)", "memory.field.body.placeholder": "Write down the conventions, steps, and boundaries to remember…",
481
+ "memory.field.form": "Form", "memory.field.attach": "Attachments",
482
+ "memory.attach.add": "Add files", "memory.attach.empty": "No attachments yet", "memory.attach.pending": "pending", "memory.attach.remove": "Remove attachment",
483
+ "memory.attach.hint": "Stored with the memory; never injected into the prompt", "memory.attach.flat": "The flat form is a single file and cannot carry attachments", "memory.attach.tooLarge": "File too large: {name} (max {limit} MB)",
484
+ "memory.delete.title": "Move to trash?", "memory.delete.desc": "“{name}” moves to trash and can be restored",
485
+ "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",
486
+ "memory.result.restored": "Restored memory: {name}", "memory.result.trashRemoved": "Permanently deleted: {name}",
487
+ "memory.result.createdAttached": "Created memory: {name} ({count} attachments)", "memory.result.updatedAttached": "Saved memory: {name} ({count} attachments)", "memory.result.attachFailed": "Memory saved, but attachments failed: {error}",
488
+ "memory.trash.open": "Trash", "memory.trash.title": "Memory trash", "memory.trash.empty": "Trash is empty",
489
+ "memory.trash.count": "{count} deleted memories", "memory.trash.deletedAt": "Deleted {time}",
490
+ "memory.trash.restore": "Restore", "memory.trash.purge": "Delete forever",
491
+ "memory.trash.confirmTitle": "Delete forever?", "memory.trash.confirmDesc": "“{name}” is deleted forever and cannot be recovered",
492
+ "memory.btn.newScene": "New scene", "memory.btn.deleteScene": "Delete scene", "memory.btn.saveScene": "Save changes",
493
+ "memory.scene.createTitle": "New scene",
494
+ "memory.deleteScene.title": "Delete scene?", "memory.deleteScene.desc": "Deletes the scene record and its empty folder “{name}” (refused while it still holds memories or is the active mode; “Global” is reserved and cannot be deleted)",
495
+ "memory.result.sceneCreated": "Scene created: {name}", "memory.result.sceneUpdated": "Scene saved: {name}", "memory.result.sceneRemoved": "Scene deleted: {name}",
496
+ "memory.table.name": "Memory name and description", "memory.table.tags": "Tags", "memory.table.status": "Status",
497
+ "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.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",
498
+ "subagents.title": "Subagents", "subagents.desc": "Manage personas: one persona per file, its body is the subagent's system prompt.",
499
+ "prompts.desc": "Manage the global instruction baseline: applying writes AGENTS.md; it takes effect in new sessions.",
500
+ "sessions.desc": "Manage archived sessions: restore or delete them permanently; expired ones are cleaned up automatically.",
501
+ "subagents.new": "New persona", "subagents.empty": "No personas yet — click “New persona” to create the first one.",
502
+ "subagents.create": "New persona", "subagents.edit": "Edit persona",
503
+ "subagents.field.name": "Persona name",
504
+ "subagents.field.description": "Description", "subagents.field.description.placeholder": "e.g. Senior Java engineer",
505
+ "subagents.field.model": "Model", "subagents.field.model.placeholder": "Custom model id", "subagents.field.model.hint": "Blank inherits the main session", "subagents.field.provider": "Model provider", "subagents.field.provider.placeholder": "sensenova", "subagents.field.provider.hint": "Provider and model are a pair: a model alone resolves against the main session's provider and fails across providers.",
506
+ "subagents.model.inherit": "Inherit the main session (unspecified)", "subagents.model.customOption": "Custom / not in the catalogue…",
507
+ "subagents.field.tools": "Tool allowlist", "subagents.field.tools.hint": "Checked = the subagent keeps only these tools; nothing checked = no restriction (the base tool set).",
508
+ "subagents.field.toolsDeny": "Tool denylist", "subagents.field.toolsDeny.hint": "Checked = remove these from the subagent's visible tools (takes precedence over the allowlist). Use it for “everything except bash” style setups.",
509
+ "subagents.tools.chosen": "Nothing selected = no restriction", "subagents.tools.filter": "Filter tool names", "subagents.tools.empty": "The host returned no selectable tools (the model may not support catalogue enumeration)",
510
+ "subagents.tools.unavailable": "not visible here", "subagents.tools.current": "available now", "subagents.tools.groupCurrent": "Available in this session", "subagents.tools.groupOther": "Available in other agent presets",
511
+ "subagents.tools.count": "{count} selected", "subagents.tools.remove": "Remove",
512
+ "subagents.adv.title": "Advanced options", "subagents.adv.inherit": "inherit", "subagents.adv.summary": "model {model} · allow {allow} · deny {deny}", "subagents.adv.note": "Candidates are the UNION of tool names across all agent presets: a persona can be reused under any preset, and listing only this session's tools would make the child fail to start after a preset switch.", "subagents.adv.loadFailed": "Could not read the candidate data (model catalogue / tool list); you can still fill the fields by hand.",
513
+ "subagents.field.body": "Persona prompt", "subagents.field.body.placeholder": "Describe the persona's role, responsibilities, and working style…",
514
+ "subagents.result.saved": "Persona saved: {name}", "subagents.result.deleted": "Persona deleted: {name}",
515
+ "subagents.delete.title": "Delete persona?", "subagents.delete.desc": "Deletes the persona file “{name}”; this cannot be undone",
516
+ "subagents.import": "Import", "subagents.import.title": "Import personas", "subagents.import.hint": "Accepts .md or .zip (multi-select or drag in): one .md = one persona, the file name is the persona name; existing names are skipped.",
517
+ "subagents.result.imported": "Imported {count} persona(s): {names}",
518
+ "memory.import": "Import memories", "memory.import.title": "Import memories", "memory.import.scene": "Import into scene",
519
+ "memory.import.sceneHint": "Blank = global; directory names inside the zip become scene names.",
520
+ "memory.import.hint": "Accepts .md or .zip (multi-select or drag in): one .md = one memory; <scene>/<name>/SKILL.md inside a zip imports as a bundle with sibling files as attachments; existing names are skipped.",
521
+ "memory.result.imported": "Imported {count} memory/memories: {names}",
522
+ "memory.import.scenesCreated": "Scenes created for them: {names}",
523
+ "memory.scene.global.hint": "Global",
524
+ "import.pick": "Choose files", "import.selected": "{count} file(s) selected", "import.clear": "Clear", "import.submit": "Install",
525
+ "import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
526
+ "error.import.noFiles": "No files selected"
527
+ }
528
+ };
529
+
99
530
  module.exports = {
100
531
  name: 'dsh-plugin-tool-management-client',
101
532
  inject: ['timer'],
533
+ // 词典也导出:让 Node 契约测试能断言「bundle 里每个 t('键') 在两份词典里都存在」。
534
+ // 浏览器 UI 的交互验收仍要真浏览器;这一条挡的是「界面直接显示原始键名」这类缺陷。
535
+ dict: DICT,
536
+ // 页面组件(契约测试用):由 apply 末尾填充。必须在 factory 作用域先落地——
537
+ // 写在 apply 方法体里的导出会随 apply 提前返回而静默消失(见下方注释)。
538
+ pages: _pages,
102
539
  apply(ctx) {
103
540
  ensureCss()
104
541
  const slots = ctx.get('slots')
105
542
  if (slots === undefined) return
106
543
 
107
- const LEVEL_LABEL = { project: 'Profile 级', global: '全局', loader: '已加载' }
544
+ /** MCP 页的本地取词:模块作用域的 t 在 apply 里被 locale 覆盖,这里每次取值都用最新的那个。 */
545
+ const mt = (key, params) => t(key, params)
546
+ /** MCP 保存后统一的操作反馈:成功 / 成功但有 loader 告警 / 失败。 */
547
+ const opMsg = (res) => (
548
+ res && res.warning
549
+ ? { kind: 'warn', text: mt('mcp.msg.warn', { warning: res.warning }) }
550
+ : { kind: 'ok', text: mt('mcp.msg.ok') }
551
+ )
108
552
  const emptyForm = () => ({ serverName: '', transport: 'streamable-http', url: '', command: '', args: '', headers: '', env: '', level: 'project' })
109
553
  const kvToLines = (obj) => (obj ? Object.keys(obj).map((k) => k + '=' + obj[k]).join('\n') : '')
110
- const MCP_LEVEL_OPTIONS = [
111
- { value: '', label: '全部级别' },
112
- { value: 'project', label: 'Profile 级' },
113
- { value: 'global', label: '全局' },
114
- { value: 'loader', label: '已加载' },
554
+ // 级别筛选选项:必须在**渲染时**取词。原来写成模块级常量,apply() 早于 locale
555
+ // 注册,导致英文界面下这四项永远显示中文(且再也翻不过来)。
556
+ const mcpLevelOptions = () => [
557
+ { value: '', label: mt('mcp.level.all') },
558
+ { value: 'project', label: mt('mcp.level.project') },
559
+ { value: 'global', label: mt('mcp.level.global') },
560
+ { value: 'loader', label: mt('mcp.level.loader') },
115
561
  ]
116
562
  const MCP_TRANSPORT_OPTIONS = [
117
563
  { value: 'streamable-http', label: 'streamable-http' },
@@ -154,7 +600,7 @@ window.__ModuleLoader__.load({
154
600
  const rows = (res && res.rows) || []
155
601
  setState({
156
602
  loading: false,
157
- error: res && res.ok ? null : ((res && res.error) || '加载失败'),
603
+ error: res && res.ok ? null : ((res && res.error) || mt('mcp.msg.loadFailed')),
158
604
  rows,
159
605
  paths: (res && res.paths) || null,
160
606
  errors: (res && res.errors) || [],
@@ -189,10 +635,10 @@ window.__ModuleLoader__.load({
189
635
  setBusy(label)
190
636
  apiCall(method, args).then((res) => {
191
637
  if (res && res.ok) {
192
- setMsg(res.warning ? { kind: 'warn', text: '操作完成,但加载器有提示:' + res.warning } : { kind: 'ok', text: '操作成功' })
638
+ setMsg(opMsg(res))
193
639
  refresh()
194
640
  if (onOk) onOk()
195
- } else setMsg({ kind: 'err', text: (res && res.error) || '操作失败' })
641
+ } else setMsg({ kind: 'err', text: (res && res.error) || mt('mcp.msg.failed') })
196
642
  }).catch((e) => setMsg({ kind: 'err', text: String((e && e.message) || e) })).then(() => { setBusy(null); setRestartInfo(null) })
197
643
  }
198
644
 
@@ -245,10 +691,10 @@ window.__ModuleLoader__.load({
245
691
  setBusy(row.id + ':toggle')
246
692
  apiCall('mcpm-set-enabled', { id: row.id, level: row.level, enabled: row.disabled }).then((res) => {
247
693
  if (res && !res.ok) {
248
- setMsg({ kind: 'err', text: (res && res.error) || '操作失败' })
694
+ setMsg({ kind: 'err', text: (res && res.error) || mt('mcp.msg.failed') })
249
695
  refresh()
250
696
  } else if (res && res.warning) {
251
- setMsg({ kind: 'warn', text: '操作完成,但加载器有提示:' + res.warning })
697
+ setMsg({ kind: 'warn', text: mt('mcp.msg.warn', { warning: res.warning }) })
252
698
  }
253
699
  }).catch((e) => {
254
700
  setMsg({ kind: 'err', text: String((e && e.message) || e) })
@@ -288,7 +734,7 @@ window.__ModuleLoader__.load({
288
734
  setDetail({ row, loading: true, error: null, tools: [] })
289
735
  apiCall('mcpm-tools', { serverName: row.serverName }).then((res) => {
290
736
  if (res && res.ok) setDetail({ row, loading: false, error: null, tools: res.tools || [] })
291
- else setDetail({ row, loading: false, error: (res && res.error) || '加载工具失败', tools: [] })
737
+ else setDetail({ row, loading: false, error: (res && res.error) || mt('mcp.tools.loadFailed'), tools: [] })
292
738
  }).catch((e) => setDetail({ row, loading: false, error: String((e && e.message) || e), tools: [] }))
293
739
  }
294
740
 
@@ -299,8 +745,8 @@ window.__ModuleLoader__.load({
299
745
  apiCall('mcpm-tool-enabled', { serverName: row.serverName, tool: tool.name, enabled: nextEnabled }).then((res) => {
300
746
  if (res && res.ok) {
301
747
  setDetail(Object.assign({}, detail, { tools: (detail.tools || []).map((item) => (item.name === tool.name ? Object.assign({}, item, { enabled: nextEnabled }) : item)) }))
302
- setMsg({ kind: 'ok', text: '已' + (nextEnabled ? '启用' : '停用') + '工具:' + tool.name })
303
- } else setMsg({ kind: 'err', text: (res && res.error) || '操作失败' })
748
+ setMsg({ kind: 'ok', text: mt(nextEnabled ? 'mcp.msg.toolOn' : 'mcp.msg.toolOff', { name: tool.name }) })
749
+ } else setMsg({ kind: 'err', text: (res && res.error) || mt('mcp.msg.failed') })
304
750
  }).catch((e) => setMsg({ kind: 'err', text: String((e && e.message) || e) })).then(() => setBusy(null))
305
751
  }
306
752
 
@@ -318,30 +764,29 @@ window.__ModuleLoader__.load({
318
764
  const summary = {
319
765
  total: rows.length,
320
766
  enabled: rows.filter((row) => !row.disabled).length,
321
- running: rows.filter((row) => row.live && row.live.enabled && row.live.phase === 'active').length,
322
767
  tools: rows.reduce((sum, row) => sum + (typeof row.toolCount === 'number' ? row.toolCount : 0), 0),
323
768
  }
324
769
  const profilePath = state.paths && state.paths.profile ? 'profile: ' + state.paths.profile : null
325
770
  const groups = levelFilter === 'loader'
326
- ? [{ key: 'live', title: '已加载', path: profilePath, match: isRunning }]
771
+ ? [{ key: 'live', title: mt('mcp.level.loader'), path: profilePath, match: isRunning }]
327
772
  : [
328
- { key: 'project', title: 'Profile 级', path: state.paths ? state.paths.project : null, match: (row) => row.level === 'project' },
329
- { key: 'global', title: '全局', path: state.paths ? state.paths.global : null, match: (row) => row.level === 'global' },
330
- { key: 'loader', title: '已加载', path: profilePath, match: (row) => row.level === 'loader' && isRunning(row) },
773
+ { key: 'project', title: mt('mcp.level.project'), path: state.paths ? state.paths.project : null, match: (row) => row.level === 'project' },
774
+ { key: 'global', title: mt('mcp.level.global'), path: state.paths ? state.paths.global : null, match: (row) => row.level === 'global' },
775
+ { key: 'loader', title: mt('mcp.level.loader'), path: profilePath, match: (row) => row.level === 'loader' && isRunning(row) },
331
776
  ]
332
777
 
333
778
  const liveStatus = (row) => {
334
- if (!row.live) return { text: '未加载', cls: 'dsm-shadowed' }
335
- if (row.live.phase === 'failed') return { text: '启动失败', cls: 'dsm-failed' }
336
- if (!row.live.enabled) return { text: '未运行', cls: 'dsm-shadowed' }
337
- if (row.live.phase && row.live.phase !== 'active') return { text: '加载中', cls: 'dsm-shadowed' }
338
- if (typeof row.toolCount === 'number' && row.toolCount === 0) return { text: '无工具', cls: 'dsm-disabled' }
339
- return { text: '运行中', cls: 'dsm-enabled' }
779
+ if (!row.live) return { text: mt('mcp.live.notLoaded'), cls: 'dsm-shadowed' }
780
+ if (row.live.phase === 'failed') return { text: mt('mcp.live.failed'), cls: 'dsm-failed' }
781
+ if (!row.live.enabled) return { text: mt('mcp.live.stopped'), cls: 'dsm-shadowed' }
782
+ if (row.live.phase && row.live.phase !== 'active') return { text: mt('mcp.live.loading'), cls: 'dsm-shadowed' }
783
+ if (typeof row.toolCount === 'number' && row.toolCount === 0) return { text: mt('mcp.live.noTools'), cls: 'dsm-disabled' }
784
+ return { text: mt('mcp.live.running'), cls: 'dsm-enabled' }
340
785
  }
341
786
  const liveHint = (row) => {
342
787
  if (!row.live) return null
343
- if (row.live.phase === 'failed') return 'loader 启动失败:请检查 URL / 命令 / 凭证 / 网络,然后点「重启」重试'
344
- if (row.live.phase === 'active' && typeof row.toolCount === 'number' && row.toolCount === 0) return '已连接但未注册任何工具:服务端可能未就绪或工具列表为空'
788
+ if (row.live.phase === 'failed') return mt('mcp.live.failedHint')
789
+ if (row.live.phase === 'active' && typeof row.toolCount === 'number' && row.toolCount === 0) return mt('mcp.live.noToolsHint')
345
790
  return null
346
791
  }
347
792
 
@@ -352,18 +797,18 @@ window.__ModuleLoader__.load({
352
797
  return React.createElement('div', { key: row.id, className: 'dsm-row' },
353
798
  React.createElement('div', { className: 'dsm-main' },
354
799
  React.createElement('div', { className: 'dsm-name' }, row.serverName),
355
- row.notes ? React.createElement('div', { className: 'dsm-note dsm-note-user', title: row.notes }, '备注:' + row.notes) : null),
800
+ row.notes ? React.createElement('div', { className: 'dsm-note dsm-note-user', title: row.notes }, mt('mcp.note.prefix') + row.notes) : null),
356
801
  React.createElement('div', { className: 'dsm-tags' },
357
- (levelFilter === 'loader' && row.level && row.level !== 'loader') ? React.createElement('span', { className: 'dsm-tag' }, LEVEL_LABEL[row.level] || row.level) : null,
358
- (typeof row.toolCount === 'number' && row.toolCount > 0) ? React.createElement('span', { className: 'dsm-tag' }, row.toolCount + ' 个工具') : null,
359
- row.duplicate ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, '重复 id') : null),
802
+ (levelFilter === 'loader' && row.level && row.level !== 'loader') ? React.createElement('span', { className: 'dsm-tag' }, mt('mcp.level.' + row.level)) : null,
803
+ (typeof row.toolCount === 'number' && row.toolCount > 0) ? React.createElement('span', { className: 'dsm-tag' }, mt('mcp.tools.count', { count: row.toolCount })) : null,
804
+ row.duplicate ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, mt('mcp.duplicate')) : null),
360
805
  React.createElement('div', { className: 'dsm-status ' + status.cls }, status.text),
361
806
  React.createElement('div', { className: 'dsm-row-actions' },
362
- editable ? React.createElement(Switch, { on: !row.disabled, disabled: busy !== null, label: '启停服务 ' + row.serverName, onClick: () => toggleRow(row) }) : null,
363
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', onClick: () => openDetail(row) }, '详情'),
364
- editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () => openEdit(row) }, '编辑') : null,
365
- editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () => restartRow(row) }, '重启') : null,
366
- editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null, onClick: () => setConfirmRow(row) }, '删除') : null),
807
+ editable ? React.createElement(Switch, { on: !row.disabled, disabled: busy !== null, label: mt('mcp.toggleServer') + ' ' + row.serverName, onClick: () => toggleRow(row) }) : null,
808
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', onClick: () => openDetail(row) }, mt('mcp.btn.detail')),
809
+ editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () => openEdit(row) }, mt('mcp.btn.edit')) : null,
810
+ editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: () => restartRow(row) }, mt('mcp.btn.restart')) : null,
811
+ editable ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null, onClick: () => setConfirmRow(row) }, mt('mcp.btn.remove')) : null),
367
812
  hint ? React.createElement('div', { className: 'dsm-row-hint' }, '⚠ ' + hint) : null)
368
813
  }
369
814
 
@@ -375,14 +820,14 @@ window.__ModuleLoader__.load({
375
820
  React.createElement('div', { className: 'dsm-source-head' },
376
821
  React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: () => setCollapsed(Object.assign({}, collapsed, { [group.key]: !open })) },
377
822
  React.createElement('span', { className: 'dsm-source-title' }, group.title),
378
- React.createElement('span', { className: 'dsm-count' }, groupRows.length + ' 个服务'),
823
+ React.createElement('span', { className: 'dsm-count' }, mt('mcp.servers.count', { count: groupRows.length })),
379
824
  group.path ? React.createElement('span', { className: 'dsm-path', title: group.path }, group.path) : null)),
380
825
  open ? React.createElement('div', { className: 'dsm-source-body' },
381
826
  React.createElement(React.Fragment, null,
382
827
  React.createElement('div', { className: 'dsm-table-head' },
383
- React.createElement('span', null, '服务名称与地址'),
384
- React.createElement('span', null, '传输与工具'),
385
- React.createElement('span', null, '运行状态'),
828
+ React.createElement('span', null, mt('mcp.table.name')),
829
+ React.createElement('span', null, mt('mcp.table.transport')),
830
+ React.createElement('span', null, mt('mcp.table.status')),
386
831
  React.createElement('span', null, '')),
387
832
  groupRows.map(renderRow))) : null)
388
833
  }
@@ -392,17 +837,17 @@ window.__ModuleLoader__.load({
392
837
  React.createElement('div', { className: 'dsm-fm-val' }, value))
393
838
 
394
839
  const toolListNode = detail && (detail.loading
395
- ? React.createElement('div', { className: 'dsm-help' }, '正在获取工具列表…')
840
+ ? React.createElement('div', { className: 'dsm-help' }, mt('mcp.tools.loading'))
396
841
  : detail.error
397
- ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, '加载工具失败:' + detail.error)
842
+ ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, mt('mcp.tools.loadFailed') + detail.error)
398
843
  : (detail.tools || []).length === 0
399
- ? React.createElement('div', { className: 'dsm-help' }, '该服务暂无已注册工具。')
844
+ ? React.createElement('div', { className: 'dsm-help' }, mt('mcp.tools.none'))
400
845
  : React.createElement(React.Fragment, null,
401
- React.createElement('div', { className: 'dsm-help' }, '停用的工具对模型不可见且不可调用,改动即时生效。'),
846
+ React.createElement('div', { className: 'dsm-help' }, mt('mcp.tools.note')),
402
847
  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 },
403
848
  React.createElement('div', { className: 'dsm-tool-name-row' },
404
849
  React.createElement('div', { className: 'dsm-tool-name' }, tool.name),
405
- React.createElement(Switch, { on: tool.enabled !== false, disabled: busy !== null, label: '启停工具 ' + tool.name, onClick: () => toggleTool(detail.row, tool) })),
850
+ React.createElement(Switch, { on: tool.enabled !== false, disabled: busy !== null, label: mt('mcp.toggleTool') + ' ' + tool.name, onClick: () => toggleTool(detail.row, tool) })),
406
851
  tool.description ? React.createElement('div', { className: 'dsm-tool-desc' }, tool.description) : null,
407
852
  (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 },
408
853
  React.createElement('span', { className: 'dsm-tool-param-key' }, param.key + (param.required ? ' *' : '')),
@@ -411,104 +856,104 @@ window.__ModuleLoader__.load({
411
856
 
412
857
  const anyGroupVisible = groups.some((group) => visibleRows.some(group.match))
413
858
  const groupsNode = (state.loading && rows.length === 0)
414
- ? React.createElement('div', { className: 'dsm-empty' }, '正在加载 MCP 服务…')
859
+ ? React.createElement('div', { className: 'dsm-empty' }, mt('mcp.loading'))
415
860
  : anyGroupVisible
416
861
  ? React.createElement('div', { className: 'dsm-sources' }, groups.map(renderGroup))
417
- : React.createElement('div', { className: 'dsm-empty' }, (normalizedQuery || levelFilter) ? '没有匹配的服务。' : '暂无 MCP 服务。点击「新增服务」添加。')
862
+ : React.createElement('div', { className: 'dsm-empty' }, (normalizedQuery || levelFilter) ? mt('mcp.empty.search') : mt('mcp.empty'))
418
863
 
419
864
  const formModalNode = formModal && React.createElement(Modal, {
420
865
  key: 'mcp-form',
421
- title: formModal.mode === 'edit' ? '编辑 MCP 服务:' + formModal.id : '新增 MCP 服务',
422
- closeLabel: '关闭',
866
+ title: formModal.mode === 'edit' ? mt('mcp.form.editTitle') + formModal.id : mt('mcp.form.addTitle'),
867
+ closeLabel: mt('btn.close'),
423
868
  onClose: closeForm,
424
869
  },
425
870
  React.createElement('div', { className: 'dsm-form' },
426
871
  React.createElement('label', { className: 'dsm-field' },
427
- React.createElement('span', { className: 'dsm-label' }, '服务名称 serverName'),
872
+ React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.serverName')),
428
873
  React.createElement('input', { className: 'dsm-control', value: formModal.serverName, placeholder: 'e.g. github', onChange: setFormField('serverName') }),
429
- React.createElement('span', { className: 'dsm-help' }, '唯一标识,1-32 位 [A-Za-z0-9_-];补丁中按该名称注册工具。')),
874
+ React.createElement('span', { className: 'dsm-help' }, mt('mcp.field.serverName.hint'))),
430
875
  React.createElement('label', { className: 'dsm-field' },
431
- React.createElement('span', { className: 'dsm-label' }, '传输方式'),
876
+ React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.transport')),
432
877
  React.createElement(SourceSelect, { value: formModal.transport, options: MCP_TRANSPORT_OPTIONS, onChange: (value) => setFormValue('transport', value) })),
433
878
  React.createElement('label', { className: 'dsm-field' },
434
- React.createElement('span', { className: 'dsm-label' }, '级别'),
879
+ React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.level')),
435
880
  React.createElement(SourceSelect, {
436
881
  value: formModal.level,
437
882
  options: [
438
- { value: 'project', label: 'Profile 级(本应用:' + (state.paths ? state.paths.project : 'profiles/*/cordis.patch.yml') + ')' },
439
- { value: 'global', label: '全局(跨 Profile:' + (state.paths ? state.paths.global : '~/.dsh/cordis.patch.yml') + ')' },
883
+ { value: 'project', label: mt('mcp.field.level.project', { path: state.paths ? state.paths.project : 'profiles/*/cordis.patch.yml' }) },
884
+ { value: 'global', label: mt('mcp.field.level.global', { path: state.paths ? state.paths.global : '~/.dsh/cordis.patch.yml' }) },
440
885
  ],
441
886
  onChange: (value) => setFormValue('level', value),
442
887
  })),
443
888
  formModal.transport === 'streamable-http'
444
889
  ? React.createElement('label', { className: 'dsm-field' },
445
- React.createElement('span', { className: 'dsm-label' }, '服务 URL'),
890
+ React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.url')),
446
891
  React.createElement('input', { className: 'dsm-control', value: formModal.url, placeholder: 'https://host/mcp', onChange: setFormField('url') }),
447
- React.createElement('span', { className: 'dsm-help' }, '需以 http(s):// 开头。'))
892
+ React.createElement('span', { className: 'dsm-help' }, mt('mcp.field.url.hint')))
448
893
  : React.createElement(React.Fragment, null,
449
894
  React.createElement('label', { className: 'dsm-field' },
450
- React.createElement('span', { className: 'dsm-label' }, '启动命令'),
895
+ React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.command')),
451
896
  React.createElement('input', { className: 'dsm-control', value: formModal.command, placeholder: 'npx -y @modelcontextprotocol/server-github', onChange: setFormField('command') })),
452
897
  React.createElement('label', { className: 'dsm-field' },
453
- React.createElement('span', { className: 'dsm-label' }, '参数(空格或换行分隔)'),
898
+ React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.args')),
454
899
  React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.args, placeholder: '-y\n@modelcontextprotocol/server-github', onChange: setFormField('args') })),
455
900
  React.createElement('label', { className: 'dsm-field' },
456
- React.createElement('span', { className: 'dsm-label' }, '环境变量(每行 key=value)'),
457
- React.createElement('span', { className: 'dsm-help' }, '路径按系统路径写法填(Windows 用 \\,macOS/Linux 用 /)。'),
901
+ React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.env')),
902
+ React.createElement('span', { className: 'dsm-help' }, mt('mcp.field.env.hint')),
458
903
  React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.env, placeholder: 'GITHUB_TOKEN=xxx', onChange: setFormField('env') }))),
459
904
  formModal.transport === 'streamable-http'
460
905
  ? React.createElement('label', { className: 'dsm-field' },
461
- React.createElement('span', { className: 'dsm-label' }, '请求头(每行 key=value)'),
906
+ React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.headers')),
462
907
  React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.headers, placeholder: 'Authorization=Bearer xxx', onChange: setFormField('headers') }))
463
908
  : null),
464
909
  React.createElement('div', { className: 'dsm-modal-actions' },
465
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: closeForm }, '取消'),
466
- React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy === 'form' || !formModal.serverName.trim(), onClick: submitForm }, formModal.mode === 'edit' ? '保存' : '添加')))
910
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: closeForm }, mt('btn.cancel')),
911
+ React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy === 'form' || !formModal.serverName.trim(), onClick: submitForm }, formModal.mode === 'edit' ? mt('btn.save') : mt('mcp.btn.add'))))
467
912
 
468
913
  const detailStatus = detail ? liveStatus(detail.row) : null
469
914
  const detailHint = detail ? liveHint(detail.row) : null
470
915
  const detailNode = detail && React.createElement(Modal, {
471
916
  key: 'mcp-detail',
472
917
  wide: true,
473
- title: '服务详情:' + detail.row.serverName,
474
- closeLabel: '关闭',
918
+ title: mt('mcp.detail.title') + detail.row.serverName,
919
+ closeLabel: mt('btn.close'),
475
920
  onClose: () => setDetail(null),
476
921
  },
477
922
  React.createElement('div', { className: 'dsm-detail-section' },
478
923
  React.createElement('div', { className: 'dsm-detail-title dsm-detail-title-row' },
479
- '配置',
480
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: toggleReveal }, reveal ? '隐藏密钥' : '显示密钥')),
924
+ mt('mcp.detail.config'),
925
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: toggleReveal }, reveal ? mt('mcp.detail.hideSecret') : mt('mcp.detail.showSecret'))),
481
926
  React.createElement('div', { className: 'dsm-fm' },
482
- configRow('条目 ID', detail.row.id),
483
- configRow('级别', LEVEL_LABEL[detail.row.level] || detail.row.level),
484
- configRow('传输方式', detail.row.transport || '—'),
485
- detail.row.url ? configRow('服务 URL', detail.row.url) : null,
486
- detail.row.command ? configRow('启动命令', detail.row.command + ((detail.row.args && detail.row.args.length) ? ' ' + detail.row.args.join(' ') : '')) : null,
487
- (detail.row.headers && Object.keys(detail.row.headers).length > 0) ? configRow('请求头', Object.keys(detail.row.headers).map((k) => k + ': ' + maskSecret(detail.row.headers[k])).join('\n')) : null,
488
- (detail.row.env && Object.keys(detail.row.env).length > 0) ? configRow('环境变量', Object.keys(detail.row.env).map((k) => k + ' = ' + maskSecret(detail.row.env[k])).join('\n')) : null)),
927
+ configRow(mt('mcp.field.entryId'), detail.row.id),
928
+ configRow(mt('mcp.field.level'), mt('mcp.level.' + detail.row.level)),
929
+ configRow(mt('mcp.field.transport'), detail.row.transport || '—'),
930
+ detail.row.url ? configRow(mt('mcp.field.url'), detail.row.url) : null,
931
+ detail.row.command ? configRow(mt('mcp.field.command'), detail.row.command + ((detail.row.args && detail.row.args.length) ? ' ' + detail.row.args.join(' ') : '')) : null,
932
+ (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,
933
+ (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)),
489
934
  React.createElement('div', { className: 'dsm-detail-section' },
490
- React.createElement('div', { className: 'dsm-detail-title' }, '运行状态'),
935
+ React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.status')),
491
936
  React.createElement('div', { className: 'dsm-feedback' + (detailStatus.cls === 'dsm-failed' ? ' dsm-error' : detailStatus.cls === 'dsm-disabled' ? ' dsm-warning' : '') },
492
- detailStatus.text + ':' + (detailHint || '该服务已登记在 Loader 中。'))),
937
+ detailStatus.text + ':' + (detailHint || mt('mcp.detail.registered')))),
493
938
  React.createElement('div', { className: 'dsm-detail-section' },
494
- React.createElement('div', { className: 'dsm-detail-title' }, '备注(仅本机可见,不会被配置更新覆盖)'),
495
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: noteDraft, placeholder: '例如:A 不可用时改用 B 兜底', onChange: (ev) => setNoteDraft(ev.target.value) }),
939
+ React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.note')),
940
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: noteDraft, placeholder: mt('mcp.detail.note.placeholder'), onChange: (ev) => setNoteDraft(ev.target.value) }),
496
941
  React.createElement('div', { className: 'dsm-modal-actions' },
497
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || noteDraft.trim() === String(detail.row.notes || ''), onClick: saveNote }, '保存备注'))),
942
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || noteDraft.trim() === String(detail.row.notes || ''), onClick: saveNote }, mt('mcp.detail.note.save')))),
498
943
  React.createElement('div', { className: 'dsm-detail-section' },
499
- React.createElement('div', { className: 'dsm-detail-title' }, '工具(' + (detail.loading ? '…' : (detail.tools || []).length) + ')'),
944
+ React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.tools', { count: detail.loading ? '…' : (detail.tools || []).length })),
500
945
  toolListNode))
501
946
 
502
947
  const confirmNode = confirmRow && React.createElement(Modal, {
503
948
  key: 'mcp-remove',
504
- title: '删除 MCP 服务',
505
- closeLabel: '关闭',
949
+ title: mt('mcp.remove.title'),
950
+ closeLabel: mt('btn.close'),
506
951
  onClose: () => setConfirmRow(null),
507
952
  },
508
- React.createElement('p', { className: 'dsm-desc' }, '确定要删除「' + confirmRow.serverName + '」吗?删除后该服务的配置将从补丁文件中移除,相关工具立即下线,此操作不可撤销。'),
953
+ React.createElement('p', { className: 'dsm-desc' }, mt('mcp.remove.desc', { name: confirmRow.serverName })),
509
954
  React.createElement('div', { className: 'dsm-modal-actions' },
510
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: () => setConfirmRow(null) }, '取消'),
511
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', onClick: confirmRemove }, '删除')))
955
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: () => setConfirmRow(null) }, mt('btn.cancel')),
956
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', onClick: confirmRemove }, mt('mcp.btn.remove'))))
512
957
 
513
958
  return React.createElement('section', { className: 'dsm-section' },
514
959
  React.createElement('div', { className: 'dsm-head' },
@@ -516,25 +961,25 @@ window.__ModuleLoader__.load({
516
961
  React.createElement('div', { className: 'dsm-title-row' },
517
962
  React.createElement('h2', { className: 'dsm-title' }, 'MCP'),
518
963
  React.createElement(VersionBadge, null)),
519
- React.createElement('p', { className: 'dsm-desc' }, '统一管理本机的 MCP 服务与运行状态。')),
964
+ React.createElement('p', { className: 'dsm-desc' }, mt('mcp.desc'))),
520
965
  React.createElement('div', { className: 'dsm-actions' },
521
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.loading, onClick: () => refresh() }, '刷新'),
522
- React.createElement('button', { type: 'button', className: 'dsm-btn', onClick: openAdd }, '新增服务'),
523
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.loading, onClick: () => run('mcpm-set-all', { enabled: true }, 'setall') }, '全部启用'),
524
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.loading, onClick: () => run('mcpm-set-all', { enabled: false }, 'setall') }, '全部停用'))),
525
- React.createElement('div', { className: 'dsm-summary dsm-summary-4' },
526
- [[summary.total, '个服务'], [summary.enabled, '个已启用'], [summary.running, '个运行中'], [summary.tools, '个工具']].map((item) =>
966
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.loading, onClick: () => refresh() }, mt('btn.refresh')),
967
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openAdd }, mt('mcp.btn.new')),
968
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.loading, onClick: () => run('mcpm-set-all', { enabled: true }, 'setall') }, mt('mcp.btn.enableAll')),
969
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.loading, onClick: () => run('mcpm-set-all', { enabled: false }, 'setall') }, mt('mcp.btn.disableAll')))),
970
+ React.createElement('div', { className: 'dsm-summary dsm-summary-3' },
971
+ [[summary.total, mt('mcp.stat.total')], [summary.enabled, mt('mcp.stat.enabled')], [summary.tools, mt('mcp.stat.tools')]].map((item) =>
527
972
  React.createElement('div', { key: item[1], className: 'dsm-stat' },
528
973
  React.createElement('strong', null, item[0]), item[1]))),
529
974
  React.createElement('div', { className: 'dsm-filters' },
530
- React.createElement('input', { className: 'dsm-control dsm-search', value: query, 'aria-label': '搜索服务', placeholder: '搜索服务名称、地址或命令', onChange: (ev) => setQuery(ev.target.value) }),
975
+ 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) }),
531
976
  React.createElement('div', { className: 'dsm-source-filter' },
532
- React.createElement(SourceSelect, { value: levelFilter, options: MCP_LEVEL_OPTIONS, onChange: setLevelFilter }))),
533
- restartInfo ? React.createElement('div', { className: 'dsm-feedback' }, '重启中… ' + restartInfo.name + '(已等待 ' + Math.max(0, Math.floor((Date.now() - restartInfo.startedAt) / 1000)) + ' 秒,完成后自动刷新)') : null,
534
- msg ? React.createElement('div', { className: 'dsm-feedback' + (msg.kind === 'err' ? ' dsm-error' : msg.kind === 'warn' ? ' dsm-warning' : ''), role: msg.kind === 'err' ? 'alert' : undefined }, msg.text) : null,
535
- state.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, state.error) : null,
536
- (state.errors && state.errors.length > 0) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'alert' }, '读取补丁告警:' + state.errors.join(';')) : null,
537
- (state.warnings && state.warnings.length > 0) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'alert' }, state.warnings.join(';')) : null,
977
+ React.createElement(SourceSelect, { value: levelFilter, options: mcpLevelOptions(), onChange: setLevelFilter }))),
978
+ restartInfo ? React.createElement('div', { className: 'dsm-feedback' }, mt('mcp.restarting', { name: restartInfo.name, seconds: Math.max(0, Math.floor((Date.now() - restartInfo.startedAt) / 1000)) })) : null,
979
+ React.createElement(Notice, { kind: msg && msg.kind, text: msg && msg.text }),
980
+ React.createElement(Notice, { kind: 'err', text: state.error }),
981
+ React.createElement(Notice, { kind: 'warn', text: (state.errors && state.errors.length > 0) ? mt('mcp.err.warnings') + state.errors.join(';') : null }),
982
+ React.createElement(Notice, { kind: 'warn', text: (state.warnings && state.warnings.length > 0) ? state.warnings.join(';') : null }),
538
983
  groupsNode,
539
984
  formModalNode,
540
985
  detailNode,
@@ -567,7 +1012,7 @@ window.__ModuleLoader__.load({
567
1012
 
568
1013
  function refresh() {
569
1014
  apiCall('agentsmd-list', {}).then(function (res) {
570
- setState(function (s) { return { loading: false, error: res && res.ok ? null : ((res && res.error) || '加载失败'), presets: (res && res.presets) || [], current: s.current } })
1015
+ 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 } })
571
1016
  }).catch(function (e) { setState({ loading: false, error: String((e && e.message) || e), presets: [], current: null }) })
572
1017
  apiCall('agentsmd-get-current', {}).then(function (res) {
573
1018
  if (res && res.ok) setState(function (s) { return Object.assign({}, s, { current: res }) })
@@ -610,9 +1055,9 @@ window.__ModuleLoader__.load({
610
1055
  var body = state.loading
611
1056
  ? React.createElement('div', { className: 'dsm-empty' }, '加载中…')
612
1057
  : state.error
613
- ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, state.error)
1058
+ ? React.createElement(Notice, { kind: 'err', text: state.error })
614
1059
  : !state.presets.length
615
- ? React.createElement('div', { className: 'dsm-empty' }, '暂无预设,点「新建预设」创建。')
1060
+ ? React.createElement('div', { className: 'dsm-empty' }, '暂无预设,点「新建预设」创建')
616
1061
  : React.createElement('div', { className: 'dsm-sources' }, state.presets.map(function (p) {
617
1062
  return React.createElement('div', { key: p.id, className: 'dsm-source' },
618
1063
  React.createElement('div', { className: 'dsm-source-head' },
@@ -629,15 +1074,15 @@ window.__ModuleLoader__.load({
629
1074
  React.createElement('div', { className: 'dsm-head' },
630
1075
  React.createElement('div', { className: 'dsm-title-block' },
631
1076
  React.createElement('div', { className: 'dsm-title-row' },
632
- React.createElement('h2', { className: 'dsm-title' }, 'AGENTS.md'),
1077
+ React.createElement('h2', { className: 'dsm-title' }, t('tabs.prompts')),
633
1078
  React.createElement(VersionBadge, null)),
634
- React.createElement('p', { className: 'dsm-desc' }, '管理多套全局指令基线;「应用」写入 ~/.dsh/AGENTS.md,新会话生效,当前会话不变。')),
1079
+ React.createElement('p', { className: 'dsm-desc' }, t('prompts.desc'))),
635
1080
  React.createElement('div', { className: 'dsm-actions' },
636
- React.createElement('button', { type: 'button', className: 'dsm-btn', onClick: function () { setCreateModal({ id: '', from: '', error: null }) } }, '新建预设'),
1081
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { setCreateModal({ id: '', from: '', error: null }) } }, '新建预设'),
637
1082
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null, onClick: function () { if (importInput.current) importInput.current.click() } }, '导入'))),
638
1083
  body,
639
1084
  editModal ? React.createElement(Modal, { title: '编辑预设 · ' + editModal.id, closeLabel: '取消', onClose: function () { setEditModal(null) } },
640
- React.createElement('textarea', { className: 'dsm-control', style: { minHeight: '320px' }, value: editModal.content, onChange: function (e) { setEditModal(Object.assign({}, editModal, { content: e.target.value })) } }),
1085
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-lg', value: editModal.content, onChange: function (e) { setEditModal(Object.assign({}, editModal, { content: e.target.value })) } }),
641
1086
  React.createElement('div', { className: 'dsm-modal-actions' },
642
1087
  React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null, onClick: doUpdate }, '保存'))) : null,
643
1088
  createModal ? React.createElement(Modal, { title: '新建预设', closeLabel: '取消', onClose: function () { setCreateModal(null) } },
@@ -655,31 +1100,37 @@ window.__ModuleLoader__.load({
655
1100
  ? React.createElement('p', { className: 'dsm-help' }, '删除预设 ' + applyConfirm.id + ' ?此操作不可恢复。')
656
1101
  : React.createElement('div', null,
657
1102
  React.createElement('p', { className: 'dsm-help' }, '将把 ' + applyConfirm.id + ' 的内容写入 ~/.dsh/AGENTS.md。'),
658
- React.createElement('div', { className: 'dsm-feedback dsm-warning' }, '新会话生效,当前会话不变;当前内容已自动备份到 __last-applied__。')),
1103
+ React.createElement('div', { className: 'dsm-feedback dsm-warning' }, '新会话生效;当前内容已备份到 __last-applied__')),
659
1104
  React.createElement('div', { className: 'dsm-modal-actions' },
660
1105
  React.createElement('button', { type: 'button', className: 'dsm-btn ' + (applyConfirm.remove ? 'dsm-btn-danger' : ''), disabled: busy !== null, onClick: function () { if (applyConfirm.remove) doRemove({ id: applyConfirm.id }); else doApply(applyConfirm.id) } }, applyConfirm.remove ? '确认删除' : '确认应用'))) : null,
661
1106
  React.createElement('input', { ref: importInput, type: 'file', accept: '.md,.markdown,.txt', className: 'dsm-hidden-input', onChange: function (e) { if (e.target.files && e.target.files[0]) doImport(e.target.files[0]); e.target.value = ''; } }))
662
1107
  }
663
1108
 
664
- // TOOLS 设置页:一个侧栏项,内部 tab 切换 MCP / Skills / AGENTS.md / History。
1109
+ // 「工具」设置页:一个侧栏项,内部 tab 切换 场景 / MCP / 技能 / 子智能体 / 提示词 / 记忆 / 会话。
665
1110
  function ToolsSection() {
666
- var tabState = React.useState('mcp')
1111
+ var tabState = React.useState('scenes')
667
1112
  var active = tabState[0], setActive = tabState[1]
668
- var page = active === 'mcp' ? React.createElement(MCPPage)
1113
+ var page = active === 'scenes' ? React.createElement(ScenesPage)
1114
+ : active === 'mcp' ? React.createElement(MCPPage)
669
1115
  : active === 'skills' ? React.createElement(SkillManagerSection, { t: t })
670
- : active === 'agents-md' ? React.createElement(AgentsMdPage)
1116
+ : active === 'subagents' ? React.createElement(SubagentsPage)
1117
+ : active === 'prompts' ? React.createElement(AgentsMdPage)
1118
+ : active === 'memory' ? React.createElement(MemoryPage)
671
1119
  : React.createElement(HistoryPage)
672
1120
  return React.createElement('section', { className: 'dsm-section' },
673
1121
  React.createElement('div', { className: 'dsm-tabs' },
1122
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'scenes' ? ' dsm-tab-active' : ''), onClick: function () { setActive('scenes') } }, t('tabs.scenes')),
674
1123
  React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'mcp' ? ' dsm-tab-active' : ''), onClick: function () { setActive('mcp') } }, 'MCP'),
675
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'skills' ? ' dsm-tab-active' : ''), onClick: function () { setActive('skills') } }, 'Skills'),
676
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'agents-md' ? ' dsm-tab-active' : ''), onClick: function () { setActive('agents-md') } }, 'AGENTS.md'),
677
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'history' ? ' dsm-tab-active' : ''), onClick: function () { setActive('history') } }, 'History'),
678
- React.createElement('div', { style: { marginLeft: 'auto' } }, React.createElement(FeedbackLinks, null))),
1124
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'skills' ? ' dsm-tab-active' : ''), onClick: function () { setActive('skills') } }, t('tabs.skills')),
1125
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'subagents' ? ' dsm-tab-active' : ''), onClick: function () { setActive('subagents') } }, t('tabs.subagents')),
1126
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'prompts' ? ' dsm-tab-active' : ''), onClick: function () { setActive('prompts') } }, t('tabs.prompts')),
1127
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'memory' ? ' dsm-tab-active' : ''), onClick: function () { setActive('memory') } }, t('tabs.memory')),
1128
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'sessions' ? ' dsm-tab-active' : ''), onClick: function () { setActive('sessions') } }, t('tabs.sessions')),
1129
+ React.createElement('div', { className: 'dsm-tabs-end' }, React.createElement(FeedbackLinks, null))),
679
1130
  page)
680
1131
  }
681
1132
  slots.inject('settings.section', () => slots.register(
682
- { name: 'settings.section', id: 'dsm-tools', order: 16, label: 'TOOLS' },
1133
+ { name: 'settings.section', id: 'dsm-tools', order: 16, label: '工具' },
683
1134
  () => React.createElement(ToolsSection)
684
1135
  ))
685
1136
 
@@ -714,71 +1165,6 @@ window.__ModuleLoader__.load({
714
1165
  }))
715
1166
  }
716
1167
 
717
- var DICT = {
718
- zh: {
719
- "title": "Skills", "desc": "统一加载和管理本机 Agent Skills。", "link.project": "GitHub", "link.feedback": "问题反馈",
720
- "btn.create": "创建技能", "btn.import": "导入", "btn.refresh": "刷新", "btn.cancel": "取消", "btn.close": "关闭", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.import.now": "安装", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用", "btn.open.editor": "用系统编辑器打开",
721
- "btn.custom.add": "添加目录", "btn.custom.remove": "移除", "btn.openDir": "选择",
722
- "custom.add.title": "添加自定义技能目录", "custom.add.submit": "添加",
723
- "custom.add.path": "目录绝对路径", "custom.add.path.placeholder": "例如 D:\\skills\\my-skills 或 /home/me/skills",
724
- "custom.add.openDir.title": "浏览并选择文件夹", "custom.add.picker.title": "选择文件夹",
725
- "custom.add.label": "显示名称(可选)", "custom.add.label.placeholder": "自定义目录",
726
- "custom.add.help": "以只读方式接入该目录:自动发现 SKILL.md 子目录与顶层 .md 单文件,可在来源卡片上启停整个目录或单个技能;与现有来源重叠的目录会被拒绝,目录与源文件都不会被修改。",
727
- "confirm.custom.remove.title": "移除自定义目录",
728
- "confirm.custom.remove.desc": "移除后该目录下的技能将不再出现在列表中(目录与技能文件本身不受影响,可随时重新添加):{name}",
729
- "result.custom.added": "已添加自定义目录:{path}", "result.custom.removed": "已移除自定义目录",
730
- "status.enabled": "已启用", "status.disabled": "已停用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.readonly": "源文件只读", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已启用", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
731
- "summary.total.one": "{count} 个技能", "summary.total.other": "{count} 个技能", "summary.enabled.one": "{count} 个已启用", "summary.enabled.other": "{count} 个已启用", "summary.disabled.one": "{count} 个已停用", "summary.disabled.other": "{count} 个已停用", "summary.issues.one": "{count} 个诊断项", "summary.issues.other": "{count} 个诊断项", "summary.group.one": "{count} 个技能", "summary.group.other": "{count} 个技能", "table.skill": "技能名称与描述", "table.status": "调用状态",
732
- "filter.source": "来源", "filter.all": "全部来源", "filter.option": "{name}({count})", "search": "搜索", "search.placeholder": "搜索技能名称或描述", "search.clear": "清除搜索",
733
- "empty.search": "没有匹配的技能。", "empty.source": "该来源目录不存在或暂时没有技能。", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
734
- "source.external.note": "通过 Skills Manager 接入,启停不会改写源文件。", "source.dsh.note": "DSH 本地技能可创建、导入和移到回收站;启停只更新管理器状态。",
735
- "detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "元数据", "detail.noFrontmatter": "该技能未提供元数据。", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
736
- "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": "对话中的 create_skill 仍创建用户级 DSH Skill;项目 Skill 可在这里选择活动项目后创建。",
737
- "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.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}",
738
- "trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空。", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
739
- "confirm.trash.title": "移到回收站?", "confirm.trash.desc": "“{name}”将从当前技能来源移入回收站,之后可以恢复到原位置。", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "“{name}”将从回收站永久删除,无法恢复。",
740
- "result.created": "已创建技能:{name}", "result.imported": "导入完成:{names}", "result.importPartial": "已导入:{imported};已跳过同名技能:{skipped}", "result.importSkipped": "未导入任何技能;已跳过同名技能:{names}", "result.importEmpty": "未导入任何技能。", "result.importWarnings": "{result};警告:{warnings}", "result.restored": "已恢复技能:{name}", "result.trashed": "已移到回收站:{name}", "result.deleted": "已永久删除:{name}", "result.updated": "状态已更新。", "result.opened": "已用系统默认程序打开:{path}", "error.action": "操作失败:{error}",
741
- "warning.scan.truncated": "技能目录较大或嵌套过深,部分技能未显示:{path}", "warning.state.invalid": "技能管理器状态文件不可读;所有技能已安全停用,修复文件前不会覆盖状态:{path}", "warning.backupUncleaned": "旧版本备份未清理:{path}({error})", "warning.project.unavailable": "无法从宿主读取活动工作区,项目技能未显示:{path}",
742
- "error.root.readonly": "该来源不允许{action}", "error.root.unknown": "未知技能来源:{root}", "error.root.unsafe": "项目技能目录不安全,拒绝写入:{path}", "error.skill.notFound": "技能不存在: {name}", "error.skill.noFrontmatter": "技能缺少完整 frontmatter,无法{action}: {name}", "error.skill.notLoadable": "技能结构不完整,无法{action}: {name}",
743
- "error.source.notFound": "路径不存在: {path}", "error.source.symlink": "不支持包含符号链接的 skill 来源: {path}", "error.source.unrecognized": "无法识别的 skill 来源: {path}", "error.source.tooDeep": "skill 来源目录层级超过 {depth} 层: {path}",
744
- "error.import.overlap": "导入来源不能与 DSH 技能目录相同、包含或位于其中", "error.import.emptySource": "目录下未找到任何 skill 条目: {path}", "error.import.invalidName": "无法生成合法 kebab-case 名称(原始名: {name})", "error.import.duplicateName": "批量来源中存在多个同名技能: {name}", "error.import.failed": "导入失败", "error.import.rollbackFailed": "覆盖导入回滚失败,备份保留在: {path}({error})",
745
- "error.upload.path": "上传内容包含非法路径:{path}", "error.upload.encoding": "上传内容编码无效", "error.upload.empty": "上传内容为空", "error.upload.tooMany": "上传文件过多,最多 {limit} 个", "error.upload.tooLarge": "上传内容过大,限制为 {limit} 字节", "error.upload.archiveTooLarge": "ZIP 压缩包过大,限制为 {limit} 字节", "error.upload.duplicate": "上传内容包含重复路径:{path}", "error.upload.zipInvalid": "ZIP 压缩包无法解压",
746
- "error.trash.notFound": "回收站条目不存在: {id}", "error.trash.conflict": "无法恢复,同名技能已存在: {name}", "error.trash.invalid": "回收站条目路径非法: {id}", "error.trash.projectUnavailable": "原项目当前不在活动工作区中,无法恢复:{path}", "error.trash.rollbackFailed": "移入回收站回滚失败,未恢复内容保留在: {path}({error})",
747
- "error.state.invalid": "技能管理器状态文件不可读,已拒绝覆盖:{path}",
748
- "error.create.descriptionRequired": "技能简介不能为空", "error.create.bodyRequired": "技能正文不能为空", "error.create.tooLarge": "技能内容过长", "error.create.conflict": "同名技能已存在: {name}",
749
- "error.proto.forbidden": "禁止的修改请求(缺少客户端标记)", "error.proto.forbiddenHost": "禁止的请求来源(非法 Host)", "error.proto.contentType": "请求体必须是 application/json", "error.proto.method": "不支持的请求方法", "error.proto.unknownAction": "未知操作", "error.proto.bodyTooLarge": "请求体过大", "error.proto.invalidJson": "请求体不是合法 JSON", "error.proto.nonJson": "服务端返回非 JSON 响应(HTTP {status})",
750
- "diagnostic.frontmatter.missing": "缺少完整 YAML frontmatter", "diagnostic.name.missing": "frontmatter 缺少 name", "diagnostic.name.invalid": "技能名称不是合法 kebab-case:{name}", "diagnostic.description.missing": "frontmatter 缺少 description", "diagnostic.invocation.invalid": "调用策略字段值无效", "diagnostic.shadowed": "被更高优先级来源 {root} 覆盖",
751
- "action.enable": "启用", "action.disable": "停用", "action.create": "创建", "action.delete": "删除", "action.restore": "恢复", "action.toggle": "启用或停用",
752
- "root.dsh": "DSH 技能", "root.agents": "公共 Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "项目 DSH", "root.projectAgents": "项目 Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor"
753
- },
754
- en: {
755
- "title": "Skills", "desc": "Load and manage Agent Skills on this computer in one place.", "link.project": "GitHub", "link.feedback": "Issues",
756
- "btn.create": "Create skill", "btn.import": "Import", "btn.refresh": "Refresh", "btn.cancel": "Cancel", "btn.close": "Close", "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.import.now": "Install", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable", "btn.open.editor": "Open in editor",
757
- "status.enabled": "Enabled", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.readonly": "Source read-only", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Enabled", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
758
- "summary.total.one": "{count} skill", "summary.total.other": "{count} skills", "summary.enabled.one": "{count} enabled", "summary.enabled.other": "{count} enabled", "summary.disabled.one": "{count} disabled", "summary.disabled.other": "{count} disabled", "summary.issues.one": "{count} diagnostic", "summary.issues.other": "{count} diagnostics", "summary.group.one": "{count} skill", "summary.group.other": "{count} skills", "table.skill": "Skill name and description", "table.status": "Invocation status",
759
- "filter.source": "Source", "filter.all": "All sources", "filter.option": "{name} ({count})", "search": "Search", "search.placeholder": "Search skill names or descriptions", "search.clear": "Clear search",
760
- "empty.search": "No matching skills.", "empty.source": "This source does not exist or has no skills yet.", "loading": "Loading skills…", "note.missing": "No description provided", "source.toggle": "Toggle source", "skill.toggle": "Toggle skill",
761
- "source.external.note": "Managed through Skills Manager; toggles never rewrite source files.", "source.dsh.note": "Local DSH skills can be created, imported, and moved to trash; toggles update manager state only.",
762
- "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.",
763
- "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": "The conversational create_skill tool still creates a user-level DSH Skill; choose an active project here for a project Skill.",
764
- "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.importing": "Installing…", "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}",
765
- "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}",
766
- "confirm.trash.title": "Move to trash?", "confirm.trash.desc": "“{name}” will move out of its current skill source and can be restored to the same location later.", "confirm.delete.title": "Delete forever?", "confirm.delete.desc": "“{name}” will be permanently deleted from trash and cannot be recovered.",
767
- "result.created": "Created skill: {name}", "result.imported": "Import complete: {names}", "result.importPartial": "Imported: {imported}; skipped existing skills: {skipped}", "result.importSkipped": "No skills were imported; existing skills were skipped: {names}", "result.importEmpty": "No skills were imported.", "result.importWarnings": "{result}; warnings: {warnings}", "result.restored": "Restored skill: {name}", "result.trashed": "Moved to trash: {name}", "result.deleted": "Permanently deleted: {name}", "result.updated": "Status updated.", "error.action": "Action failed: {error}",
768
- "warning.scan.truncated": "Some skills were not shown because the directory is too large or deeply nested: {path}", "warning.state.invalid": "The manager state file could not be read; all skills are disabled and state writes are blocked until it is repaired: {path}", "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})", "warning.project.unavailable": "The active workspace could not be read from the host, so its project skills are hidden: {path}",
769
- "error.root.readonly": "This source does not allow {action}", "error.root.unknown": "Unknown skill source: {root}", "error.root.unsafe": "The project skill directory is unsafe, so the write was refused: {path}", "error.skill.notFound": "Skill not found: {name}", "error.skill.noFrontmatter": "Skill lacks complete frontmatter, cannot {action}: {name}", "error.skill.notLoadable": "Skill structure is incomplete, cannot {action}: {name}",
770
- "error.source.notFound": "Path does not exist: {path}", "error.source.symlink": "Skill sources containing symbolic links are not supported: {path}", "error.source.unrecognized": "Unrecognized skill source: {path}", "error.source.tooDeep": "Skill source directory depth exceeds {depth} levels: {path}",
771
- "error.import.overlap": "Import source cannot be the same as, contain, or be inside the DSH skills directory", "error.import.emptySource": "No skill entries found in the directory: {path}", "error.import.invalidName": "Cannot generate a valid kebab-case name (original: {name})", "error.import.duplicateName": "Batch source contains duplicate skill names: {name}", "error.import.failed": "Import failed", "error.import.rollbackFailed": "Overwrite import rollback failed; backups kept at: {path} ({error})",
772
- "error.upload.path": "Upload contains an invalid path: {path}", "error.upload.encoding": "Upload encoding is invalid", "error.upload.empty": "Upload is empty", "error.upload.tooMany": "Too many uploaded files; maximum {limit}", "error.upload.tooLarge": "Upload is too large; limit {limit} bytes", "error.upload.archiveTooLarge": "ZIP archive is too large; limit {limit} bytes", "error.upload.duplicate": "Upload contains a duplicate path: {path}", "error.upload.zipInvalid": "ZIP archive could not be extracted",
773
- "error.trash.notFound": "Trash item not found: {id}", "error.trash.conflict": "Cannot restore because a skill with the same name exists: {name}", "error.trash.invalid": "Invalid trash item path: {id}", "error.trash.projectUnavailable": "The original project is not an active workspace, so this skill cannot be restored: {path}", "error.trash.rollbackFailed": "Move-to-trash rollback failed; unrecovered content was kept at: {path} ({error})",
774
- "error.state.invalid": "The manager state file could not be read, so overwriting it was refused: {path}",
775
- "error.create.descriptionRequired": "Skill description is required", "error.create.bodyRequired": "Skill body is required", "error.create.tooLarge": "Skill content is too large", "error.create.conflict": "A skill with the same name already exists: {name}",
776
- "error.proto.forbidden": "Forbidden mutation request (missing client marker)", "error.proto.forbiddenHost": "Forbidden request origin (invalid host)", "error.proto.contentType": "Content type must be application/json", "error.proto.method": "Method not allowed", "error.proto.unknownAction": "Unknown action", "error.proto.bodyTooLarge": "Request body too large", "error.proto.invalidJson": "Invalid JSON request body", "error.proto.nonJson": "Server returned a non-JSON response (HTTP {status})",
777
- "diagnostic.frontmatter.missing": "Missing complete YAML frontmatter", "diagnostic.name.missing": "Frontmatter is missing name", "diagnostic.name.invalid": "Skill name is not valid kebab-case: {name}", "diagnostic.description.missing": "Frontmatter is missing description", "diagnostic.invocation.invalid": "Invocation policy value is invalid", "diagnostic.shadowed": "Shadowed by higher-priority source {root}",
778
- "action.enable": "enable", "action.disable": "disable", "action.create": "create", "action.delete": "delete", "action.restore": "restore", "action.toggle": "enabling or disabling",
779
- "root.dsh": "DSH skills", "root.agents": "Shared Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "Project DSH", "root.projectAgents": "Project Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor"
780
- }
781
- };
782
1168
 
783
1169
 
784
1170
  function translateOrFallback(t, key, fallback, params) { var value = t(key, params); return typeof value === "string" && value !== key ? value : fallback; }
@@ -792,6 +1178,7 @@ window.__ModuleLoader__.load({
792
1178
  }
793
1179
  function parseApiResponse(response) { return response.json().catch(function () { var error = new Error("non-json response"); error.code = "error.proto.nonJson"; error.params = { status: response.status }; throw error; }).then(function (payload) { if (!response.ok || payload.ok === false) throw payload; return payload.data; }); }
794
1180
  var OP_BY_PATH = { "/state": "skill-state", "/enable": "skill-enable", "/disable": "skill-disable",
1181
+ "/prefer": "skill-prefer", "/unprefer": "skill-unprefer",
795
1182
  "/source-enable": "skill-source-enable", "/source-disable": "skill-source-disable",
796
1183
  "/delete": "skill-delete", "/trash-restore": "skill-trash-restore", "/trash-delete": "skill-trash-delete",
797
1184
  "/detail": "skill-detail", "/create": "skill-create", "/import": "skill-import",
@@ -831,7 +1218,11 @@ function callApi(path, options) {
831
1218
  function normalizeSkillQuery(query) { return String(query == null ? "" : query).trim().toLowerCase(); }
832
1219
  function matchSkillQuery(skill, query) { var q = normalizeSkillQuery(query); if (!q) return true; return [skill.name, skill.declaredName, skill.description, skill.kind, skill.kindLabel, skill.statusLabel, skill.rootKey, skill.rootLabel].some(function (value) { return String(value == null ? "" : value).toLowerCase().includes(q); }); }
833
1220
  function filterSkills(list, options) { var rootKey = options && options.rootKey != null ? options.rootKey : ""; return list.filter(function (skill) { return (!rootKey || skill.rootKey === rootKey) && matchSkillQuery(skill, options && options.query); }); }
834
- function visibleSkillRoots(roots) { return (roots || []).filter(function (root) { return root.scope !== "project" || (root.skills || []).length > 0; }); }
1221
+ // 技能页主列表:去掉没有技能的项目级空壳,以及**已被移除**的来源(它们单独一组显示,
1222
+ // 在那里可以「恢复读取」)。项目根与已移除根都不参与「全部来源」筛选。
1223
+ function visibleSkillRoots(roots) { return (roots || []).filter(function (root) { if (root.removed === true) return false; return root.scope !== "project" || (root.skills || []).length > 0; }); }
1224
+ /** 已移除的来源(不在主列表里,但要让用户看得到并恢复)。 */
1225
+ function removedSkillRoots(roots) { return (roots || []).filter(function (root) { return root.removed === true; }); }
835
1226
  var MAX_UPLOAD_ARCHIVE_BYTES = 32 << 20, MAX_UPLOAD_ENTRY_BYTES = 32 << 20, MAX_UPLOAD_TOTAL_BYTES = 64 << 20, MAX_UPLOAD_ENTRIES = 1000;
836
1227
  function uploadFilePath(file) { return String(file && (file._dssmPath || file.webkitRelativePath || file.name) || "").replace(/\\/g, "/"); }
837
1228
  function inspectUploadSelection(files) {
@@ -866,48 +1257,157 @@ function callApi(path, options) {
866
1257
  react.useEffect(function () { if (!open) return undefined; function close(event) { if (!ref.current || !ref.current.contains(event.target)) setOpen(false); } document.addEventListener("pointerdown", close); return function () { document.removeEventListener("pointerdown", close); }; }, [open]);
867
1258
  return h("div", { className: "dsm-select", ref: ref }, h("button", { type: "button", className: "dsm-select-trigger", "aria-haspopup": "listbox", "aria-expanded": open, onClick: function () { setOpen(!open); } }, h("span", null, selected.label)), open ? h("div", { className: "dsm-select-menu", role: "listbox" }, props.options.map(function (o) { return h("button", { key: o.value || "all", type: "button", role: "option", className: "dsm-option", "aria-selected": o.value === props.value, onClick: function () { props.onChange(o.value); setOpen(false); } }, o.label); })) : null);
868
1259
  }
1260
+ // 场景输入(combobox):既能从已有场景里挑,也能直接手输一个新场景名——
1261
+ // 宿主 rules-create 用 mkdir(recursive) 落盘,所以「自定义场景」无需先建目录。
1262
+ function SceneCombo(props) {
1263
+ var state = react.useState(false), open = state[0], setOpen = state[1], ref = react.useRef(null);
1264
+ react.useEffect(function () { if (!open) return undefined; function close(event) { if (!ref.current || !ref.current.contains(event.target)) setOpen(false); } document.addEventListener("pointerdown", close); return function () { document.removeEventListener("pointerdown", close); }; }, [open]);
1265
+ var options = props.options || [];
1266
+ return h("div", { className: "dsm-select", ref: ref },
1267
+ h("div", { className: "dsm-combo-row" },
1268
+ h("input", { className: "dsm-control" + (props.invalid ? " dsm-rule-invalid" : ""), type: "text", value: props.value || "", placeholder: props.placeholder, "aria-label": props.label, onChange: function (e) { props.onChange(e.target.value); } }),
1269
+ 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)),
1270
+ 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);
1271
+ }
869
1272
  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 }); }
870
1273
  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" })); }
871
- function Modal(props) { var ref = react.useRef(null); react.useEffect(function () { if (ref.current) ref.current.focus(); }, []); 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)), props.children)); }
872
-
873
- // 目录选择器:经 host 的 dir-list 逐级浏览目录树,选中后把绝对路径回填给调用方。
874
- // 标准浏览器不暴露所选文件夹的绝对路径(File.path 仅 Electron 可用),故不走
875
- // webkitdirectory,改由宿主端列目录 + 前端目录树完成「选择文件夹」。
876
- function DirPickerModal(props) {
877
- var stState = react.useState({ loading: true, error: null, current: '', parent: null, entries: [] })
878
- var st = stState[0], setSt = stState[1]
879
- var txState = react.useState(props.initial || '')
880
- var text = txState[0], setText = txState[1]
881
- function load(dir) {
882
- setSt({ loading: true, error: null, current: dir || '', parent: null, entries: [] })
883
- apiCall('dir-list', { dir: dir || '' }).then(function (res) {
884
- if (res && res.ok) setSt({ loading: false, error: null, current: res.current || '', parent: res.parent || null, entries: res.entries || [] })
885
- else setSt({ loading: false, error: (res && res.error) || '无法读取目录', current: dir || '', parent: null, entries: [] })
886
- }).catch(function () {
887
- setSt({ loading: false, error: '无法读取目录', current: dir || '', parent: null, entries: [] })
888
- })
889
- }
890
- react.useEffect(function () { load(props.initial || '') }, [])
891
- function jump() { var v = String(text || '').trim(); if (v) load(v) }
892
- function goUp() { if (!st.parent) return; setText(st.parent); load(st.parent) }
893
- function enter(path) { setText(path); load(path) }
894
- function pick() { var target = String(st.current || '').trim() || String(text || '').trim(); if (target && props.onPick) props.onPick(target) }
895
- return h(Modal, { title: props.title || '选择文件夹', closeLabel: props.closeLabel || '关闭', onClose: props.onClose },
896
- h("div", { className: "dsm-form" },
897
- h("label", { className: "dsm-field" },
898
- h("span", { className: "dsm-label" }, '当前路径'),
899
- h("div", { className: "dsm-dir-row" },
900
- h("input", { className: "dsm-control", value: text, placeholder: '输入目录路径,回车跳转', onChange: function (e) { setText(e.target.value); }, onKeyDown: function (e) { if (e.key === 'Enter') { e.preventDefault(); jump(); } } }),
901
- h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: st.loading || !String(text || '').trim(), onClick: jump }, '跳转'))),
902
- st.error ? h("div", { className: "dsm-feedback dsm-error", role: "alert" }, st.error) : null,
903
- h("div", { className: "dsm-dir-list" },
904
- st.loading ? h("div", { className: "dsm-empty" }, '加载中…')
905
- : st.entries.length ? st.entries.map(function (e) { return h("button", { key: e.path, type: "button", className: "dsm-dir-item", title: e.path, onClick: function () { enter(e.path); } }, e.name); })
906
- : h("div", { className: "dsm-empty" }, '(当前目录没有子文件夹)')),
907
- h("div", { className: "dsm-modal-actions" },
908
- h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: st.loading || !st.parent, onClick: goUp }, '上级'),
909
- h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: props.onClose }, '取消'),
910
- h("button", { type: "button", className: "dsm-btn", disabled: st.loading || !(String(st.current || '').trim() || String(text || '').trim()), onClick: pick }, '选择此文件夹'))))
1274
+ function Modal(props) { var ref = react.useRef(null); react.useEffect(function () { if (ref.current) ref.current.focus(); }, []); 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)), props.children)); }
1275
+ /**
1276
+ * 全插件唯一的「操作结果」呈现方式:成功 = 右下角浮层(固定定位,不改变页面高度),
1277
+ * 警告 / 错误 = 文档流内横幅(需要用户处理,常驻到下一次操作)。所有页面共用,
1278
+ * 避免同一件事在不同页看起来不一样、也让成功反馈不再把下方内容顶来顶去。
1279
+ */
1280
+ function Notice(props) {
1281
+ var text = props && props.text
1282
+ if (!text) return null
1283
+ if (props.kind === "ok") return h("div", { className: "dsm-toast", role: "status", "aria-live": "polite" }, text)
1284
+ return h("div", { className: "dsm-feedback" + (props.kind === "warn" ? " dsm-warning" : " dsm-error"), role: "alert" }, text)
1285
+ }
1286
+ /* ── 勾选列表原语(场景档案编辑器 / 人设工具选择器 / 任何「多选一组条目」的地方共用)────
1287
+ 原先这套排版只在 ScenesPage 内部,人设的工具选择器要么复制一份、要么写成裸 input;
1288
+ 现在提到模块作用域,页面之间才可能真正长得一样。 */
1289
+ /** 段卡片:标题栏 + 固定高度滚动体 + 脚注(高度不随内容增减变化,弹窗不跳)。 */
1290
+ function seg(props) {
1291
+ return h('div', { className: 'dsm-seg' },
1292
+ h('div', { className: 'dsm-seg-head' },
1293
+ h('span', { className: 'dsm-seg-title' }, props.title),
1294
+ h('span', { className: 'dsm-seg-count' }, props.count),
1295
+ h('div', { className: 'dsm-seg-actions' }, props.actions)),
1296
+ props.body,
1297
+ props.foot || null)
1298
+ }
1299
+ /** 勾选行:复选框 + 名称/说明 + 右侧指标 + 可选行内动作。 */
1300
+ function pickRow(props) {
1301
+ return h('label', { key: props.key, className: 'dsm-pick' },
1302
+ h('input', { type: 'checkbox', checked: props.checked === true, disabled: props.disabled === true, onChange: props.onChange }),
1303
+ h('span', { className: 'dsm-pick-main' },
1304
+ h('span', { className: 'dsm-pick-name' }, props.name),
1305
+ props.desc ? h('span', { className: 'dsm-pick-desc' }, props.desc) : null),
1306
+ props.meta ? h('span', { className: 'dsm-pick-meta' }, props.meta) : null,
1307
+ props.actions ? h('span', { className: 'dsm-pick-actions' }, props.actions) : null)
1308
+ }
1309
+ /** 段体顶部的筛选行(条目多的段用;勾选状态不受筛选影响)。 */
1310
+ function segFilter(value, onChange, placeholder) {
1311
+ return h('div', { className: 'dsm-seg-filter' },
1312
+ h('input', { className: 'dsm-control', value: value || '', placeholder: placeholder, onChange: function (e) { onChange(e.target.value) } }))
1313
+ }
1314
+ /** 分组小标题(长列表里给用户地标,避免「一屏看不出层次」)。 */
1315
+ function segGroup(label) {
1316
+ return h('div', { className: 'dsm-seg-group' },
1317
+ h('span', null, label),
1318
+ h('span', { className: 'dsm-seg-group-line' }))
1319
+ }
1320
+ /** 段头公共动作:全选 / 清空 / 移除段(未定义时改为「添加」)。 */
1321
+ function segActions(props) {
1322
+ if (!props.defined) return [h('button', { key: 'add', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy, onClick: props.onAdd }, props.addLabel)]
1323
+ return [
1324
+ h('button', { key: 'all', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy, onClick: props.onAll }, props.selectAllLabel),
1325
+ h('button', { key: 'clear', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy, onClick: props.onClear }, props.clearLabel),
1326
+ h('button', { key: 'rm', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy, onClick: props.onRemove }, props.removeLabel),
1327
+ ]
1328
+ }
1329
+ /** 段脚注:段已定义但一项未勾 = 该域全部停用(必须显式提示,否则像没保存上)。 */
1330
+ function segFoot(defined, count, emptyLabel) {
1331
+ if (!defined || count > 0) return null
1332
+ return h('div', { className: 'dsm-seg-foot' }, emptyLabel)
1333
+ }
1334
+ /** 固定高度滚动体内的条目列表(空态居中提示)。 */
1335
+ function segList(nodes, emptyText) {
1336
+ if (!nodes.length) return h('div', { className: 'dsm-seg-body' }, h('div', { className: 'dsm-pick-empty' }, emptyText))
1337
+ return h('div', { className: 'dsm-seg-body' }, nodes)
1338
+ }
1339
+
1340
+ // 通用导入弹窗(人设 / 记忆共用):拖放或点选 .md / .zip → 已选计数 → 交由调用方走自己的写 op。
1341
+ // t 由调用方注入(各页的 t 是局部函数,不在模块作用域);extra 用于注入「导入到场景」一类的额外字段。
1342
+ function ImportModal(props) {
1343
+ var st = react.useState([]); var files = st[0], setFiles = st[1]
1344
+ var ref = react.useRef(null)
1345
+ var t = props.t
1346
+ function add(list) {
1347
+ var picked = Array.prototype.slice.call(list || [])
1348
+ if (!picked.length) return
1349
+ Promise.all(picked.map(function (f) { return f.arrayBuffer().then(function (buf) { return { name: uploadFilePath(f), data: bytesToBase64(buf) } }) }))
1350
+ .then(function (entries) { setFiles(function (prev) { return prev.concat(entries) }) })
1351
+ .catch(function () {})
1352
+ }
1353
+ return h(Modal, { className: "dsm-modal-import", title: props.title, closeLabel: t("btn.cancel"), onClose: props.onClose },
1354
+ h("div", { className: "dsm-form" },
1355
+ props.extra || null,
1356
+ h("div", {
1357
+ className: "dsm-dropzone",
1358
+ onClick: function () { if (ref.current) ref.current.click() },
1359
+ onDragOver: function (e) { e.preventDefault(); if (e.currentTarget.classList) e.currentTarget.classList.add("dsm-dropzone-active") },
1360
+ onDragLeave: function (e) { if (e.currentTarget.classList) e.currentTarget.classList.remove("dsm-dropzone-active") },
1361
+ onDrop: function (e) { e.preventDefault(); if (e.currentTarget.classList) e.currentTarget.classList.remove("dsm-dropzone-active"); droppedFiles(e.dataTransfer).then(add) },
1362
+ },
1363
+ h("div", { className: "dsm-dropzone-title" }, t("import.pick")),
1364
+ h("div", { className: "dsm-dropzone-copy" }, props.hint),
1365
+ files.length ? h("div", { className: "dsm-help" }, t("import.selected", { count: files.length })) : null),
1366
+ h("input", { ref: ref, type: "file", multiple: true, accept: ".md,.zip", className: "dsm-hidden-input", onChange: function (e) { add(e.target.files); e.target.value = "" } })),
1367
+ h("div", { className: "dsm-modal-actions" },
1368
+ h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: props.busy, onClick: props.onClose }, t("btn.cancel")),
1369
+ files.length ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: props.busy, onClick: function () { setFiles([]) } }, t("import.clear")) : null,
1370
+ h("button", { type: "button", className: "dsm-btn dsm-btn-primary", disabled: props.busy || !files.length, onClick: function () { props.onSubmit(files) } }, props.busy ? t("upload.importing") : t("import.submit"))))
1371
+ }
1372
+
1373
+ // 目录选择器:经 host 的 dir-list 逐级浏览目录树,选中后把绝对路径回填给调用方。
1374
+ // 标准浏览器不暴露所选文件夹的绝对路径(File.path 仅 Electron 可用),故不走
1375
+ // webkitdirectory,改由宿主端列目录 + 前端目录树完成「选择文件夹」。
1376
+ function DirPickerModal(props) {
1377
+ var stState = react.useState({ loading: true, error: null, current: '', parent: null, entries: [] })
1378
+ var st = stState[0], setSt = stState[1]
1379
+ var txState = react.useState(props.initial || '')
1380
+ var text = txState[0], setText = txState[1]
1381
+ function load(dir) {
1382
+ setSt({ loading: true, error: null, current: dir || '', parent: null, entries: [] })
1383
+ apiCall('dir-list', { dir: dir || '' }).then(function (res) {
1384
+ if (res && res.ok) setSt({ loading: false, error: null, current: res.current || '', parent: res.parent || null, entries: res.entries || [] })
1385
+ else setSt({ loading: false, error: (res && res.error) || '无法读取目录', current: dir || '', parent: null, entries: [] })
1386
+ }).catch(function () {
1387
+ setSt({ loading: false, error: '无法读取目录', current: dir || '', parent: null, entries: [] })
1388
+ })
1389
+ }
1390
+ react.useEffect(function () { load(props.initial || '') }, [])
1391
+ function jump() { var v = String(text || '').trim(); if (v) load(v) }
1392
+ function goUp() { if (!st.parent) return; setText(st.parent); load(st.parent) }
1393
+ function enter(path) { setText(path); load(path) }
1394
+ function pick() { var target = String(st.current || '').trim() || String(text || '').trim(); if (target && props.onPick) props.onPick(target) }
1395
+ return h(Modal, { title: props.title || '选择文件夹', closeLabel: props.closeLabel || '关闭', onClose: props.onClose },
1396
+ h("div", { className: "dsm-form" },
1397
+ h("label", { className: "dsm-field" },
1398
+ h("span", { className: "dsm-label" }, '当前路径'),
1399
+ h("div", { className: "dsm-dir-row" },
1400
+ h("input", { className: "dsm-control", value: text, placeholder: '输入目录路径,回车跳转', onChange: function (e) { setText(e.target.value); }, onKeyDown: function (e) { if (e.key === 'Enter') { e.preventDefault(); jump(); } } }),
1401
+ h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: st.loading || !String(text || '').trim(), onClick: jump }, '跳转'))),
1402
+ st.error ? h("div", { className: "dsm-feedback dsm-error", role: "alert" }, st.error) : null,
1403
+ h("div", { className: "dsm-dir-list" },
1404
+ st.loading ? h("div", { className: "dsm-empty" }, '加载中…')
1405
+ : st.entries.length ? st.entries.map(function (e) { return h("button", { key: e.path, type: "button", className: "dsm-dir-item", title: e.path, onClick: function () { enter(e.path); } }, e.name); })
1406
+ : h("div", { className: "dsm-empty" }, '(当前目录没有子文件夹)')),
1407
+ h("div", { className: "dsm-modal-actions" },
1408
+ h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: st.loading || !st.parent, onClick: goUp }, '上级'),
1409
+ h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: props.onClose }, '取消'),
1410
+ h("button", { type: "button", className: "dsm-btn", disabled: st.loading || !(String(st.current || '').trim() || String(text || '').trim()), onClick: pick }, '选择此文件夹'))))
911
1411
  }
912
1412
 
913
1413
  function SkillManagerSection(props) {
@@ -958,25 +1458,51 @@ function callApi(path, options) {
958
1458
  function selectUploadFiles(files) { pickerOpenRef.current = false; var selected = inspectUploadSelection(files); if (!selected) return; if (selected.error) { setUpload(null); setResult({ ok: false, text: translateError(t, selected.error) }); return; } setUpload(selected); setResult(null); }
959
1459
  function openNativePicker(ref) { if (busy || pickerOpenRef.current || !ref.current) return; pickerOpenRef.current = true; ref.current.value = ""; function release() { setTimeout(function () { pickerOpenRef.current = false; }, 0); } window.addEventListener("focus", release, { once: true }); ref.current.click(); setTimeout(function () { pickerOpenRef.current = false; }, 30000); }
960
1460
  function submitImport() { if (!upload) return; buildUploadPayload(upload).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 () {}); }
961
- var data = snapshot.data || { roots: [], trash: [], summary: { total: 0, enabled: 0, disabled: 0, issues: 0 } }, allRoots = data.roots || [], roots = visibleSkillRoots(allRoots), activeSource = roots.some(function (root) { return root.key === source; }) ? source : "";
962
- 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: "dsh", label: t("root.dsh") });
963
- function openCreate() { var selectedRoot = createRoots.some(function (root) { return root.key === activeSource; }) ? activeSource : "dsh"; setForm(Object.assign({}, form, { root: selectedRoot })); setModal("create"); }
1461
+ 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 : "";
1462
+ 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") });
1463
+ // 新建技能的默认落点:hub(`tool-management/skills/`)。
1464
+ // 用户在技能页筛选了某个来源时,优先用那个来源;两者都不可用时退回 dsh。
1465
+ function defaultCreateRoot() {
1466
+ if (createRoots.some(function (root) { return root.key === activeSource; })) return activeSource;
1467
+ if (createRoots.some(function (root) { return root.key === "hub"; })) return "hub";
1468
+ return "dsh";
1469
+ }
1470
+ function openCreate() { setForm(Object.assign({}, form, { root: defaultCreateRoot() })); setModal("create"); }
964
1471
  function trashRootLabel(item) { return item.root && item.root.scope === "project" ? t("root.projectDsh") + " · " + (item.root.projectName || item.root.projectRoot) : t("root.dsh"); }
965
1472
  var options = [{ value: "", label: t("filter.all") }].concat(roots.map(function (root) { return { value: root.key, label: t("filter.option", { name: rootDisplayName(t, root), count: root.count == null ? root.skills.length : root.count }) }; }));
966
1473
 
1474
+ // 同名技能:默认按来源优先级取一个生效,其余显示为「被覆盖」。
1475
+ // 「启用这个」= 把这个来源设为同名首选 + 启用它(只写本地策略,不动任何源文件)。
1476
+ function activateShadowed(root, skill) {
1477
+ // 来源整体停用时,切首选 + 单点启用都不会真的生效:先如实说明,别给「已改为生效」的假回执。
1478
+ if (root.enabled === false) {
1479
+ setResult({ ok: false, text: t("error.root.disabled", { name: rootDisplayName(t, root) }) });
1480
+ return;
1481
+ }
1482
+ post("/prefer", { root: root.key, name: skill.name }, null)
1483
+ .then(function () { return post("/enable", { root: root.key, name: skill.name }, null); })
1484
+ .then(function () { setResult({ ok: true, text: t("result.preferred", { name: skill.declaredName || skill.name, source: rootDisplayName(t, root) }) }); })
1485
+ .catch(function () {});
1486
+ }
1487
+ function clearPreferred(root, skill) {
1488
+ post("/unprefer", { root: root.key, name: skill.name }, null)
1489
+ .then(function () { setResult({ ok: true, text: t("result.unpreferred", { name: skill.declaredName || skill.name }) }); })
1490
+ .catch(function () {});
1491
+ }
967
1492
  function renderSkill(root, skill) {
968
1493
  var enabled = isSkillEnabled(skill), key = skill.shadowedBy ? "status.shadowed" : skill.loadable === false ? "status.invalid" : enabled ? "status.enabled" : "status.disabled", cls = skill.shadowedBy ? "dsm-shadowed" : enabled ? "dsm-enabled" : "dsm-disabled";
969
- return h("div", { key: skill.name, className: "dsm-row" }, h("div", { className: "dsm-main" }, h("div", { className: "dsm-name" }, skill.declaredName || skill.name), h("div", { className: "dsm-note" }, skill.description || t("note.missing"))), h("div", { className: "dsm-tags" }, h("span", { className: "dsm-tag" }, rootDisplayName(t, root)), !root.mutable ? h("span", { className: "dsm-tag" }, t("status.readonly")) : null), h("div", { className: "dsm-status " + cls }, t(key)), h("div", { className: "dsm-row-actions" }, root.toggleable !== false ? h(Switch, { on: enabled, disabled: busy || root.enabled === false || !!skill.shadowedBy || skill.loadable === false, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.mutable ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
1494
+ var canToggle = root.toggleable !== false && skill.loadable !== false;
1495
+ return h("div", { key: skill.name, className: "dsm-row" }, h("div", { className: "dsm-main" }, h("div", { className: "dsm-name" }, skill.declaredName || skill.name), h("div", { className: "dsm-note" }, skill.description || t("note.missing")), skill.shadowedBy ? h("div", { className: "dsm-rule-hint" }, t("status.shadowed.hint", { name: skill.shadowedBy.name })) : null), h("div", { className: "dsm-tags" }, h("span", { className: "dsm-tag" }, rootDisplayName(t, root)), skill.preferred === true ? h("span", { className: "dsm-tag dsm-tag-on" }, t("status.preferred")) : null, !root.mutable ? h("span", { className: "dsm-tag" }, t("status.readonly")) : null, root.mutable && !root.deletable ? h("span", { className: "dsm-tag", title: t("status.notDeletable.hint") }, t("status.notDeletable")) : null), h("div", { className: "dsm-status " + cls }, t(key)), h("div", { className: "dsm-row-actions" }, canToggle && skill.shadowedBy ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, title: t("btn.activate.title"), onClick: function () { activateShadowed(root, skill); } }, t("btn.activate")) : null, canToggle && !skill.shadowedBy ? h(Switch, { on: enabled, disabled: busy || root.enabled === false, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, skill.preferred === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, title: t("btn.unprefer.title"), onClick: function () { clearPreferred(root, skill); } }, t("btn.unprefer")) : null, h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.deletable === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
970
1496
  }
971
1497
  function renderRoot(root) {
972
1498
  if (activeSource && activeSource !== root.key) return null;
973
1499
  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;
974
1500
  var rootCount = root.count == null ? root.skills.length : root.count;
975
- 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" }, 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.key === "dsh" ? "" : root.enabled ? "dsm-tag-on" : "dsm-tag-off") }, root.scope === "project" ? t("status.project") : root.key === "dsh" ? 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)), root.scope !== "project" && root.key !== "dsh" && root.toggleable !== false ? h("span", { className: "dsm-source-actions" }, root.key.indexOf("custom-") === 0 ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "custom-remove-confirm", key: root.key, name: displayName }); } }, t("btn.custom.remove")) : null, h(Switch, { on: root.enabled, disabled: busy, label: t("source.toggle") + " " + displayName, onClick: function () { post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } })) : null), 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);
1501
+ 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" }, 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.key === "dsh" ? "" : root.enabled ? "dsm-tag-on" : "dsm-tag-off") }, root.scope === "project" ? t("status.project") : root.key === "dsh" ? 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)), root.scope !== "project" && root.key !== "dsh" && root.toggleable !== false ? h("span", { className: "dsm-source-actions" }, root.removable === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy, title: t("btn.source.remove.title"), onClick: function () { setModal({ type: "source-remove-confirm", key: root.key, name: displayName }); } }, t("btn.source.remove")) : null, h(Switch, { on: root.enabled, disabled: busy, label: t("source.toggle") + " " + displayName, onClick: function () { post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } })) : null), 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);
976
1502
  }
977
1503
 
978
1504
  var summary = data.summary || { total: 0, enabled: 0, disabled: 0, issues: 0 };
979
- 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(VersionBadge, null)), h("p", { className: "dsm-desc" }, t("desc"))), h("div", { className: "dsm-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dsm-btn", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", 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); setCustomForm({ path: "", label: "" }); setModal("custom-add"); } }, t("btn.custom.add")))), h("div", { key: "summary", className: "dsm-summary dsm-summary-4" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.disabled, "summary.disabled"], [summary.issues || 0, "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("div", { key: "result", className: "dsm-feedback" + (result.warning ? " dsm-warning" : result.ok ? "" : " dsm-error") }, result.text) : null].concat((data.warnings || []).map(function (warning, index) { return h("div", { key: "warning-" + index, className: "dsm-feedback dsm-warning", role: "alert" }, translateError(t, warning)); }), [snapshot.error ? h("div", { key: "error", className: "dsm-feedback dsm-error" }, 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)), h("button", { key: "trash", type: "button", className: "dsm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dsm-trash-count" }, (data.trash || []).length))]);
1505
+ 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(VersionBadge, null)), h("p", { className: "dsm-desc" }, t("desc"))), h("div", { className: "dsm-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", 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); setCustomForm({ path: "", label: "" }); setModal("custom-add"); } }, t("btn.custom.add")))), h("div", { key: "summary", className: "dsm-summary dsm-summary-3" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.disabled, "summary.disabled"]].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)),removedRoots.length ? h("section", { key: "removed-sources", className: "dsm-source dsm-source-removed" }, h("div", { className: "dsm-source-head" }, h("div", { className: "dsm-source-head-main" }, h("span", { className: "dsm-source-title" }, t("removed.title")), h("span", { className: "dsm-count" }, t("removed.count", { count: removedRoots.length })), h("span", { className: "dsm-note" }, t("removed.hint")))), h("div", { className: "dsm-source-body" }, removedRoots.map(function (root) { var name = rootDisplayName(t, root); return h("div", { key: root.key, className: "dsm-row" }, h("div", { className: "dsm-main" }, h("div", { className: "dsm-name" }, name), h("div", { className: "dsm-note", title: root.path }, root.path)), h("div", { className: "dsm-row-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, onClick: function () { post("/source-restore", { root: root.key }, "result.sourceRestored", { name: name }); } }, t("btn.source.restore")))); }))) : null, h("button", { key: "trash", type: "button", className: "dsm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dsm-trash-count" }, (data.trash || []).length))]);
980
1506
 
981
1507
  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 dsm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dsm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
982
1508
  if (modal === "import") content.push(h(Modal, { key: "import", className: "dsm-modal-import", title: t("import.title"), closeLabel: t("btn.close"), onClose: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } },
@@ -994,6 +1520,7 @@ function callApi(path, options) {
994
1520
  if (modal && modal.type === "trash-confirm") content.push(h(Modal, { key: "trash-confirm", title: t("confirm.trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("p", { className: "dsm-desc" }, t("confirm.trash.desc", { name: modal.name })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dsm-btn", disabled: busy, onClick: function () { post("/delete", { root: modal.root, name: modal.name }, "result.trashed", { name: modal.name }).then(function () { setModal(null); }).catch(function () {}); } }, t("btn.trash")))));
995
1521
  if (modal && modal.type === "delete-confirm") content.push(h(Modal, { key: "delete-confirm", title: t("confirm.delete.title"), closeLabel: t("btn.close"), onClose: function () { setModal("trash"); } }, h("p", { className: "dsm-desc" }, t("confirm.delete.desc", { name: modal.name })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal("trash"); } }, t("btn.cancel")), h("button", { className: "dsm-btn dsm-btn-danger", disabled: busy, onClick: function () { post("/trash-delete", { id: modal.id }, "result.deleted", { name: modal.name }).then(function () { setModal("trash"); }).catch(function () {}); } }, t("btn.delete.forever")))));
996
1522
  if (modal === "custom-add") content.push(h(Modal, { key: "custom-add", title: t("custom.add.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("custom.add.path")), h("div", { className: "dsm-dir-row" }, h("input", { className: "dsm-control", value: customForm.path, placeholder: t("custom.add.path.placeholder"), onChange: function (e) { setCustomForm(Object.assign({}, customForm, { path: e.target.value })); } }), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: openCustomDir, title: t("custom.add.openDir.title") }, t("btn.openDir")))), h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("custom.add.label")), h("input", { className: "dsm-control", value: customForm.label, placeholder: t("custom.add.label.placeholder"), onChange: function (e) { setCustomForm(Object.assign({}, customForm, { label: e.target.value })); } })), h("p", { className: "dsm-help" }, t("custom.add.help")), result ? h("div", { className: "dsm-feedback" + (result.warning ? " dsm-warning" : result.ok ? "" : " dsm-error"), role: "alert" }, result.text) : null), customForm.picking ? h(DirPickerModal, { key: "custom-dir-picker", title: t("custom.add.picker.title"), initial: String(customForm.path || ""), closeLabel: t("btn.close"), onClose: function () { setCustomForm(Object.assign({}, customForm, { picking: false })); }, onPick: function (path) { setCustomForm(Object.assign({}, customForm, { path: path, picking: false })); } }) : null, h("div", { className: "dsm-modal-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { type: "button", className: "dsm-btn", disabled: busy || !String(customForm.path || "").trim(), onClick: submitCustomAdd }, t("custom.add.submit")))));
1523
+ if (modal && modal.type === "source-remove-confirm") content.push(h(Modal, { key: "source-remove-confirm", title: t("confirm.source.remove.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("p", { className: "dsm-desc" }, t("confirm.source.remove.desc", { name: modal.name })), h("p", { className: "dsm-help" }, t("confirm.source.remove.hint")), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dsm-btn dsm-btn-danger", disabled: busy, onClick: function () { post("/source-remove", { root: modal.key }, "result.sourceRemoved", { name: modal.name }).then(function () { setModal(null); }).catch(function () {}); } }, t("btn.source.remove")))));
997
1524
  if (modal && modal.type === "custom-remove-confirm") content.push(h(Modal, { key: "custom-remove-confirm", title: t("confirm.custom.remove.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("p", { className: "dsm-desc" }, t("confirm.custom.remove.desc", { name: modal.name })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dsm-btn dsm-btn-danger", disabled: busy, onClick: function () { submitCustomRemove(modal.key, modal.name); } }, t("btn.custom.remove")))));
998
1525
  return h("section", { className: "dsm-section" }, content);
999
1526
  }
@@ -1030,12 +1557,12 @@ function callApi(path, options) {
1030
1557
  // 导入对话弹窗:null = 关闭;{ busy, error, result, cwd } = 打开。
1031
1558
  var impState = React.useState(null)
1032
1559
  var imp = impState[0], setImp = impState[1]
1033
- var importInputRef = React.useRef(null)
1034
- // 导出对话弹窗:null = 关闭;{ busy, error, result, outDir, format, wsFilter, selectedIds } = 打开。
1035
- var expState = React.useState(null)
1036
- var exp = expState[0], setExp = expState[1]
1037
- // 分组卡片折叠状态:key → true(收起)。默认全部展开。
1038
- var collState = React.useState({})
1560
+ var importInputRef = React.useRef(null)
1561
+ // 导出对话弹窗:null = 关闭;{ busy, error, result, outDir, format, wsFilter, selectedIds } = 打开。
1562
+ var expState = React.useState(null)
1563
+ var exp = expState[0], setExp = expState[1]
1564
+ // 分组卡片折叠状态:key → true(收起)。默认全部展开。
1565
+ var collState = React.useState({})
1039
1566
  var collapsed = collState[0], setCollapsed = collState[1]
1040
1567
 
1041
1568
  function refresh() {
@@ -1096,9 +1623,9 @@ function callApi(path, options) {
1096
1623
  })
1097
1624
  }
1098
1625
 
1099
- // 「全选」状态:当前过滤结果是否全部选中(按钮文案切换为「取消全选」)。
1100
- var allVisible = filtered.length > 0 && visibleIds.every(function (id) { return selected.has(id) })
1101
-
1626
+ // 「全选」状态:当前过滤结果是否全部选中(按钮文案切换为「取消全选」)。
1627
+ var allVisible = filtered.length > 0 && visibleIds.every(function (id) { return selected.has(id) })
1628
+
1102
1629
  function doUnarchive(sessionId) {
1103
1630
  setBusy(true)
1104
1631
  apiCall('history-unarchive', { sessionId: sessionId }).then(function (r) {
@@ -1350,20 +1877,20 @@ function callApi(path, options) {
1350
1877
  React.createElement('div', { className: 'dsm-head' },
1351
1878
  React.createElement('div', { className: 'dsm-title-block' },
1352
1879
  React.createElement('div', { className: 'dsm-title-row' },
1353
- React.createElement('h2', { className: 'dsm-title' }, 'History'),
1880
+ React.createElement('h2', { className: 'dsm-title' }, t('tabs.sessions')),
1354
1881
  React.createElement(VersionBadge, null)),
1355
- React.createElement('p', { className: 'dsm-desc' }, '管理已归档会话:恢复、永久删除或批量操作;归档满保留期后自动清理。')),
1882
+ React.createElement('p', { className: 'dsm-desc' }, t('sessions.desc'))),
1356
1883
  React.createElement('div', { className: 'dsm-actions' },
1357
1884
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: !filtered.length, onClick: function () { toggleAllVisible(visibleIds) } }, allVisible ? '取消全选' : '全选'),
1358
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openImportPicker }, '导入对话'),
1885
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openImportPicker }, '导入对话'),
1359
1886
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openExportPicker }, '导出对话'),
1360
1887
  selected.size > 0 ? React.createElement('div', { className: 'dsm-hist-batch' },
1361
1888
  React.createElement('span', { className: 'dsm-hist-batch-count' }, '已选 ' + selected.size + ' 项'),
1362
1889
  React.createElement('div', { className: 'dsm-hist-batch-actions' },
1363
1890
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { doUnarchiveBatch() } }, '恢复所选 (' + selected.size + ')'),
1364
1891
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'deleteBatch', count: selected.size }) } }, '删除所选 (' + selected.size + ')'))) : null)),
1365
- React.createElement('div', { className: 'dsm-summary dsm-summary-4' },
1366
- [[data.items.length, '个归档'], [grouped ? groups.length : '—', '个项目'], [data.retentionDays === 0 ? '永久' : data.retentionDays + ' 天', '保留期'], [selected.size, '个已选']].map(function (item) {
1892
+ React.createElement('div', { className: 'dsm-summary dsm-summary-3' },
1893
+ [[data.items.length, '个归档'], [grouped ? groups.length : '—', '个项目'], [selected.size, '个已选']].map(function (item) {
1367
1894
  return React.createElement('div', { key: item[1], className: 'dsm-stat' },
1368
1895
  React.createElement('strong', null, item[0]), item[1])
1369
1896
  })),
@@ -1379,17 +1906,17 @@ function callApi(path, options) {
1379
1906
  : React.createElement('div', { className: 'dsm-sources' },
1380
1907
  grouped ? groups.map(renderGroup) : React.createElement('div', { className: 'dsm-source' }, filtered.map(renderRow))),
1381
1908
  modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'del', title: '永久删除?', closeLabel: '取消', onClose: function () { setModal(null) } },
1382
- React.createElement('p', { className: 'dsm-help' }, '将永久删除会话「' + modal.title + '」及其全部记录,此操作不可撤销。'),
1909
+ React.createElement('p', { className: 'dsm-help' }, '永久删除会话「' + modal.title + '」及其全部记录,不可恢复'),
1383
1910
  React.createElement('div', { className: 'dsm-modal-actions' },
1384
1911
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, '取消'),
1385
1912
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { doDelete(modal.sessionId) } }, '确认删除'))) : null,
1386
1913
  modal && modal.type === 'deleteBatch' ? React.createElement(Modal, { key: 'delb', title: '永久删除?', closeLabel: '取消', onClose: function () { setModal(null) } },
1387
- React.createElement('p', { className: 'dsm-help' }, '将永久删除所选 ' + modal.count + ' 个会话及其全部记录,此操作不可撤销。'),
1914
+ React.createElement('p', { className: 'dsm-help' }, '永久删除所选 ' + modal.count + ' 个会话及其全部记录,不可恢复'),
1388
1915
  React.createElement('div', { className: 'dsm-modal-actions' },
1389
1916
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, '取消'),
1390
1917
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { doDeleteBatch() } }, '确认删除'))) : null,
1391
1918
  imp ? React.createElement(Modal, { key: 'imp', className: 'dsm-modal-import', title: '导入对话', closeLabel: '关闭', onClose: function () { setImp(null) } },
1392
- React.createElement('p', { className: 'dsm-help' }, '从其他 Agent 导入对话记录,生成可在 DSH 中继续对话的新会话。支持 Claude Code / Cursor JSONL、Codex Markdown 与通用文本。'),
1919
+ React.createElement('p', { className: 'dsm-help' }, '导入其他 Agent 的对话记录,生成可继续对话的新会话'),
1393
1920
  React.createElement('div', { className: 'dsm-field' },
1394
1921
  React.createElement('label', { className: 'dsm-label' }, '项目目录(可留空)'),
1395
1922
  React.createElement('input', { className: 'dsm-control', type: 'text', placeholder: '绝对路径,可留空(会话归入未分组)', value: imp.cwd || '', onChange: function (e) { setImp(Object.assign({}, imp, { cwd: e.target.value })) } })),
@@ -1400,7 +1927,7 @@ function callApi(path, options) {
1400
1927
  imp.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(imp.error)) : null,
1401
1928
  imp.result ? React.createElement('div', { className: 'dsm-feedback' }, '已创建会话 ' + imp.result.sessionId + '(' + imp.result.count + ' 条消息),可在 DSH 会话列表中继续对话。') : null) : null,
1402
1929
  exp ? React.createElement(Modal, { key: 'exp', className: 'dsm-modal-import', title: '导出对话', closeLabel: '关闭', onClose: function () { setExp(null) } },
1403
- React.createElement('p', { className: 'dsm-help' }, '把选中的会话导出为转录文件,写入指定目录;文件可在「导入对话」中重新导入。'),
1930
+ React.createElement('p', { className: 'dsm-help' }, '导出选中会话为转录文件,可再次导入'),
1404
1931
  React.createElement('div', { className: 'dsm-field' },
1405
1932
  React.createElement('label', { className: 'dsm-label' }, '导出目录(绝对路径,自动创建)'),
1406
1933
  React.createElement('div', { className: 'dsm-dir-row' },
@@ -1445,13 +1972,1518 @@ function callApi(path, options) {
1445
1972
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { setModal(null) } }, '关闭'))) : null)
1446
1973
  }
1447
1974
 
1448
- module.exports.DICT = DICT
1449
- // Test-only export: page 组件 + i18n t,让 client 测试直接渲染 page(绕开
1450
- // ToolsSection 的子组件嵌套——minimal React 不支持子组件 hook 隔离)。
1451
- // 生产环境浏览器 React 会正确递归渲染 ToolsSection 内的 page。
1452
- module.exports._pages = { MCPPage: MCPPage, SkillManagerSection: SkillManagerSection, AgentsMdPage: AgentsMdPage, HistoryPage: HistoryPage, t: t }
1975
+ // ---------- Rules 页(v0.3 规则层:场景 = 一级目录,勾选即自动生效)----------
1976
+ // 场景/分组名校验:与宿主 src/rules/service.ts 的 isValidGroupSegment 保持一致。
1977
+ // 变更单 01 放宽为**任意 Unicode**(`办公` / `日常` 必须通过),只保留文件系统安全约束。
1978
+ var SCENE_RESERVED_RE = /[<>:"|?*\\/]/
1979
+ function isValidSceneSegment(segment) {
1980
+ var s = String(segment == null ? '' : segment)
1981
+ if (s === '' || s === '.' || s === '..') return false
1982
+ if (s.length > 64) return false
1983
+ if (s.charAt(0) === '.') return false
1984
+ if (SCENE_RESERVED_RE.test(s)) return false
1985
+ if (s !== s.trim()) return false
1986
+ if (/[.\s]$/.test(s)) return false
1987
+ return true
1988
+ }
1989
+ function isValidScenePath(group) {
1990
+ var s = String(group == null ? '' : group)
1991
+ if (s === '' || s.charAt(0) === '/' || s.charAt(s.length - 1) === '/') return false
1992
+ return s.split('/').every(isValidSceneSegment)
1993
+ }
1994
+ var RULE_FORM_OPTIONS = [
1995
+ { value: 'flat', label: 'flat' },
1996
+ { value: 'bundle', label: 'bundle' },
1997
+ ] /**
1998
+ * 场景显示名:宿主给出的 `label` 优先(保留场景 `global` 的 label 是「全局」,由宿主决定),
1999
+ * 界面不再自己拼中文;没有元数据的游离桶(`''`)用专门的说明文案。
2000
+ *
2001
+ * 定义在模块作用域、把 `scenes` 作为**参数**传入:它原本是 MemoryPage 内部的闭包,
2002
+ * 而 ScenesPage 也调用它——两个页面是各自独立的函数作用域,闭包不可能共享。
2003
+ * 那正是「工具」页整页白屏的原因(ScenesPage 渲染卡片时 ReferenceError: sceneLabel
2004
+ * is not defined,被 shell 的 slot 边界吞成一条日志,面板什么都不画)。
2005
+ */
2006
+ function sceneLabel(scenes, name) {
2007
+ if (name === '') return t('memory.scene.orphan')
2008
+ var row = (scenes || []).filter(function (s) { return s.name === name })[0]
2009
+ if (row && row.label) return row.label
2010
+ if (name === 'global') return t('memory.scene.global')
2011
+ return name
2012
+ }
2013
+
2014
+ // ---------- 场景记忆页(场景 = scene-memory/ 下的一级目录;内容自动生效)----------
2015
+ //
2016
+ // 变更单 01 之后 Rules 与 Scenes 合并为本页:**场景(一级目录)是分组维度,
2017
+ // 记忆(.md)是内容**;勾选启用后该目录树内所有 .md 正文自动进入系统提示词。
2018
+ // 原 Scenes 页的「agent preset ↔ 分组绑定」降级为页底的「高级」折叠区(可选)。
2019
+ // ---------- 场景页:场景清单 + 档案(MCP 工具集/技能集/子智能体绑定,三段自由搭配)+ 当前模式 ----------
2020
+ function ScenesPage() {
2021
+ var EMPTY_MODE = { scene: null, snapshot: null }
2022
+ var state = React.useState({ loading: true, error: null, scenes: [], activeMode: 'all', mode: EMPTY_MODE, archives: {} })
2023
+ var data = state[0], setData = state[1]
2024
+ var bs = React.useState(false)
2025
+ var busy = bs[0], setBusy = bs[1]
2026
+ var rs = React.useState(null)
2027
+ var result = rs[0], setResult = rs[1]
2028
+ var ms = React.useState(null)
2029
+ var modal = ms[0], setModal = ms[1]
2030
+ var sfs = React.useState({ name: '', error: null })
2031
+ var sceneForm = sfs[0], setSceneForm = sfs[1]
2032
+ var dts = React.useState(null)
2033
+ var drillTools = dts[0], setDrillTools = dts[1]
2034
+ React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
2035
+ function refresh(silent) {
2036
+ if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
2037
+ apiCall('scene-mode-get', {}).then(function (m) {
2038
+ if (m && m.ok) setData(function (prev) { return Object.assign({}, prev, { mode: m.mode || EMPTY_MODE, archives: m.archives || {} }) })
2039
+ }).catch(function () {})
2040
+ apiCall('rules-list', {}).then(function (r) {
2041
+ if (r && r.ok) setData(function (prev) { return Object.assign({}, prev, { loading: false, error: null, scenes: r.scenes || [], activeMode: r.activeMode === 'custom' ? 'custom' : 'all', stats: r.stats || prev.stats }) })
2042
+ else setData(function (prev) { return Object.assign({}, prev, { loading: false, error: translateError(t, r) }) })
2043
+ }).catch(function (e) { setData(function (prev) { return Object.assign({}, prev, { loading: false, error: String((e && e.message) || e) }) }) })
2044
+ }
2045
+ React.useEffect(function () { refresh() }, [])
2046
+ function isValidSceneName(name) {
2047
+ return name.length > 0 && name.length <= 64 && !name.startsWith('.') && !/[\\/<>:"|?*]/.test(name)
2048
+ }
2049
+ // ── 场景启用开关(记忆注入;勾满全部 = 默认全部启用)──
2050
+ function setActiveScenes(names) {
2051
+ if (busy) return
2052
+ setBusy(true); setResult(null)
2053
+ var real = (data.scenes || []).filter(function (s) { return !s.shared && !s.global }).map(function (s) { return s.name })
2054
+ var payload = real.length > 0 && real.every(function (n) { return names.indexOf(n) >= 0 }) ? { all: true } : { scenes: names }
2055
+ apiCall('rules-set-active', payload).then(function (res) {
2056
+ setBusy(false)
2057
+ if (res && res.ok) { setResult({ ok: true, text: t('memory.result.active') }); refresh(true) }
2058
+ else setResult({ ok: false, text: translateError(t, res) })
2059
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2060
+ }
2061
+ function toggleScene(scene) {
2062
+ if (busy || scene.shared || scene.global) return
2063
+ var names = (data.scenes || []).filter(function (s) { return s.active && !s.shared && !s.global }).map(function (s) { return s.name })
2064
+ var i = names.indexOf(scene.name)
2065
+ if (i >= 0) names.splice(i, 1)
2066
+ else names.push(scene.name)
2067
+ setActiveScenes(names)
2068
+ }
2069
+ // ── 场景建 / 改描述 / 删 ──
2070
+ function openCreateScene() { setSceneForm({ name: '', description: '', error: null }); setModal({ type: 'scene-create' }) }
2071
+ function openEditScene(scene) {
2072
+ setSceneForm({ name: scene.name, description: scene.description || '', error: null })
2073
+ setModal({ type: 'scene-edit', name: scene.name })
2074
+ }
2075
+ /** 新建与编辑共用一个表单:字段相同,只是分别走 rules-create-scene / rules-update-scene。 */
2076
+ function submitSceneForm() {
2077
+ var isEdit = modal && modal.type === 'scene-edit'
2078
+ var name = String(sceneForm.name || '').trim()
2079
+ if (!isValidSceneName(name)) { setSceneForm(Object.assign({}, sceneForm, { error: t('error.rules.invalidGroup') })); return }
2080
+ var description = String(sceneForm.description || '').trim()
2081
+ setBusy(true)
2082
+ apiCall(isEdit ? 'rules-update-scene' : 'rules-create-scene', isEdit ? { name: name, description: description } : { name: name, description: description }).then(function (res) {
2083
+ setBusy(false)
2084
+ if (res && res.ok) {
2085
+ setModal(null)
2086
+ setResult({ ok: true, text: t(isEdit ? 'memory.result.sceneUpdated' : 'memory.result.sceneCreated', { name: name }) })
2087
+ refresh(true)
2088
+ } else setSceneForm(Object.assign({}, sceneForm, { error: translateError(t, res) }))
2089
+ }).catch(function (e) { setBusy(false); setSceneForm(Object.assign({}, sceneForm, { error: String((e && e.message) || e) })) })
2090
+ }
2091
+ function submitDeleteScene(name) {
2092
+ setBusy(true)
2093
+ apiCall('rules-remove-scene', { name: name }).then(function (res) {
2094
+ setBusy(false); setModal(null)
2095
+ if (res && res.ok) { setResult({ ok: true, text: t('memory.result.sceneRemoved', { name: name }) }); refresh(true) }
2096
+ else setResult({ ok: false, text: translateError(t, res) })
2097
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2098
+ }
2099
+ // ── 档案(mcp 两级 / skills / subagents 三段自由搭配)──
2100
+ function openArchive(name) {
2101
+ setBusy(true)
2102
+ Promise.all([apiCall('scene-mode-get', {}), apiCall('scene-inventory', {})]).then(function (rs) {
2103
+ setBusy(false)
2104
+ var modeRes = rs[0], inv = rs[1]
2105
+ if (!(inv && inv.ok)) { setResult({ ok: false, text: translateError(t, inv) }); return }
2106
+ var archives = (modeRes && modeRes.ok ? modeRes.archives : null) || data.archives || {}
2107
+ var archive = archives[name] || {}
2108
+ setModal({ type: 'scene-archive', name: name, drill: null, memDrill: null, memQuery: '', skillQuery: '', subQuery: '', mcpQuery: '',
2109
+ sections: {
2110
+ mcp: archive.mcp ? Object.assign({}, archive.mcp) : null,
2111
+ skills: Array.isArray(archive.skills) ? archive.skills.slice() : null,
2112
+ subagents: Array.isArray(archive.subagents) ? archive.subagents.slice() : null,
2113
+ memories: Array.isArray(archive.memories) ? archive.memories.slice() : null,
2114
+ },
2115
+ inventory: {
2116
+ mcpServers: inv.mcpServers || [], skills: inv.skills || [], subagents: inv.subagents || [], tools: inv.tools || [],
2117
+ // 记忆段的数据源(宿主 scene-inventory 回传;老宿主缺失时退化为空列表而不是崩)。
2118
+ memScenes: inv.scenes || [], memories: inv.memories || [],
2119
+ } })
2120
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2121
+ }
2122
+ function modalSections() { return modal && modal.type === 'scene-archive' ? modal.sections : null }
2123
+ function setSections(sections) { if (modal && modal.type === 'scene-archive') setModal(Object.assign({}, modal, { sections: sections })) }
2124
+ function toggleMcpServer(server) {
2125
+ var sections = modalSections(); if (!sections) return
2126
+ var mcp = Object.assign({}, sections.mcp || {})
2127
+ if (mcp[server] !== undefined) delete mcp[server]
2128
+ else mcp[server] = '*'
2129
+ setSections(Object.assign({}, sections, { mcp: mcp }))
2130
+ }
2131
+ /**
2132
+ * 「添加 MCP 工具集」的初值 = 当前运行时启用状态(设计 §2.4「预勾当前启用状态」)。
2133
+ * 勾选集语义是「勾 = 启用」,空段 = 全部停用,所以绝不能拿 {} 当默认值——
2134
+ * 否则用户点一下「添加」就把所有 MCP 服务器关掉。
2135
+ */
2136
+ function mcpPreset() {
2137
+ var preset = {}
2138
+ var states = modal.inventory.tools || []
2139
+ ;(modal.inventory.mcpServers || []).forEach(function (server) {
2140
+ if (server.allToolsDisabled) return // 整台已停用 → 不勾(等价)
2141
+ var prefix = server.name + '/'
2142
+ var list = states.filter(function (x) { return x.key.indexOf(prefix) === 0 })
2143
+ var enabled = list.filter(function (x) { return x.enabled !== false })
2144
+ if (!list.length || enabled.length === list.length) { preset[server.name] = '*'; return }
2145
+ if (!enabled.length) return // 全停用 → 不勾(等价)
2146
+ preset[server.name] = enabled.map(function (x) { return x.key.slice(prefix.length) })
2147
+ })
2148
+ return preset
2149
+ }
2150
+ /** 全选:列出全部服务器(含未运行的)并各勾「全部工具」。 */
2151
+ function mcpSelectAll() {
2152
+ var all = {}
2153
+ ;(modal.inventory.mcpServers || []).forEach(function (server) { all[server.name] = '*' })
2154
+ setSections(Object.assign({}, modalSections(), { mcp: all }))
2155
+ }
2156
+ function mcpDrillSpec(value) {
2157
+ var sections = modalSections(); if (!sections) return
2158
+ var mcp = Object.assign({}, sections.mcp || {})
2159
+ mcp[modal.drill] = value
2160
+ setSections(Object.assign({}, sections, { mcp: mcp }))
2161
+ }
2162
+ /** 档案各段共用的段头动作(包装共享的 segActions,把 busy / 文案在这里补上)。 */
2163
+ function archiveSegActions(defined, addLabel, onAdd, onRemove, onAll, onClear) {
2164
+ return segActions({
2165
+ defined: defined, busy: busy, addLabel: addLabel,
2166
+ selectAllLabel: t('scenes.seg.selectAll'), clearLabel: t('scenes.seg.clear'), removeLabel: t('memory.archive.removeSection'),
2167
+ onAdd: onAdd, onRemove: onRemove, onAll: onAll, onClear: onClear,
2168
+ })
2169
+ }
2170
+ function archiveSegFoot(defined, count) {
2171
+ return segFoot(defined, count, t('memory.archive.emptySection'))
2172
+ }
2173
+
2174
+ // ── 段 4:记忆(v3)────────────────────────────────────────────────
2175
+ // 数据源是宿主的 scene-inventory(scenes + memories 两个扁平列表)。
2176
+ // 勾选语义与其余段一致:段已定义 → 没勾的记忆在该场景下不注入;段未定义 = 不碰。
2177
+ // 记忆文件不受影响(纯投影),所以这一段没有「运行时副作用」。
2178
+ function memGrouped() {
2179
+ var scenes = (modal.inventory.memScenes || []).slice()
2180
+ var all = modal.inventory.memories || []
2181
+ return scenes.map(function (s) {
2182
+ return {
2183
+ scene: s,
2184
+ items: all.filter(function (m) { return m.scene === s.name })
2185
+ .sort(function (a, b) { return String(a.name).localeCompare(String(b.name)) }),
2186
+ }
2187
+ })
2188
+ }
2189
+ /** 记忆段已定义时的初值 = 当前全部记忆(「添加」不改变现状,是最小惊讶的选择)。 */
2190
+ function memPreset() {
2191
+ return (modal.inventory.memories || []).map(function (m) { return String(m.id) })
2192
+ }
2193
+ function memCheckedCount() {
2194
+ var sections = modalSections()
2195
+ return sections.memories ? sections.memories.length : 0
2196
+ }
2197
+ function toggleMemory(id) {
2198
+ var sections = modalSections(); if (!sections) return
2199
+ var list = (sections.memories || []).slice()
2200
+ var i = list.indexOf(id)
2201
+ if (i >= 0) list.splice(i, 1); else list.push(id)
2202
+ setSections(Object.assign({}, sections, { memories: list }))
2203
+ }
2204
+ function memDrillList() {
2205
+ var all = (modal.inventory.memories || []).filter(function (m) { return m.scene === modal.memDrill })
2206
+ var q = String(modal.memQuery || '').trim().toLowerCase()
2207
+ var list = q ? all.filter(function (m) {
2208
+ return String(m.name).toLowerCase().indexOf(q) >= 0 || String(m.description || '').toLowerCase().indexOf(q) >= 0
2209
+ }) : all
2210
+ return list.sort(function (a, b) { return String(a.name).localeCompare(String(b.name)) })
2211
+ }
2212
+ function memoriesSeg() {
2213
+ var sections = modalSections()
2214
+ var defined = !!sections.memories
2215
+ var groups = memGrouped()
2216
+ var total = (modal.inventory.memories || []).length
2217
+ if (modal.memDrill) {
2218
+ // 场景内的记忆明细:与 MCP「选工具」同构的钻取视图,弹窗尺寸不变。
2219
+ var scene = (modal.inventory.memScenes || []).filter(function (s) { return s.name === modal.memDrill })[0] || { name: modal.memDrill, label: modal.memDrill, count: 0 }
2220
+ var items = memDrillList()
2221
+ var sceneIds = (modal.inventory.memories || []).filter(function (m) { return m.scene === modal.memDrill }).map(function (m) { return String(m.id) })
2222
+ var checkedInScene = sceneIds.filter(function (id) { return (sections.memories || []).indexOf(id) >= 0 }).length
2223
+ return seg({
2224
+ title: t('scenes.mem.drill') + ' · ' + (scene.label || scene.name),
2225
+ count: t('scenes.seg.checked', { checked: checkedInScene, total: sceneIds.length }),
2226
+ actions: [
2227
+ React.createElement('button', { key: 'back', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { setModal(Object.assign({}, modal, { memDrill: null, memQuery: '' })) } }, t('scenes.mcp.back')),
2228
+ React.createElement('button', { key: 'all', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () {
2229
+ var list = (sections.memories || []).slice()
2230
+ sceneIds.forEach(function (id) { if (list.indexOf(id) < 0) list.push(id) })
2231
+ setSections(Object.assign({}, sections, { memories: list }))
2232
+ } }, t('scenes.seg.selectAll')),
2233
+ React.createElement('button', { key: 'none', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () {
2234
+ setSections(Object.assign({}, sections, { memories: (sections.memories || []).filter(function (id) { return sceneIds.indexOf(id) < 0 }) }))
2235
+ } }, t('scenes.seg.clear')),
2236
+ ],
2237
+ body: React.createElement('div', { className: 'dsm-seg-body' },
2238
+ segFilter(modal.memQuery, function (v) { setModal(Object.assign({}, modal, { memQuery: v })) }, t('scenes.mem.search')),
2239
+ items.length
2240
+ ? React.createElement('div', null, items.map(function (m) {
2241
+ return pickRow({
2242
+ disabled: busy,
2243
+ key: m.id,
2244
+ checked: (sections.memories || []).indexOf(String(m.id)) >= 0,
2245
+ name: m.name,
2246
+ desc: m.description || null,
2247
+ onChange: function () { toggleMemory(String(m.id)) },
2248
+ })
2249
+ }))
2250
+ : React.createElement('div', { className: 'dsm-pick-empty' }, modal.memQuery ? t('scenes.mem.noMatch') : t('scenes.mem.emptyScene'))),
2251
+ foot: React.createElement('div', { className: 'dsm-seg-foot' }, t('scenes.mem.drillHint')),
2252
+ })
2253
+ }
2254
+ return seg({
2255
+ title: t('memory.archive.memories'),
2256
+ count: defined ? t('scenes.seg.checked', { checked: memCheckedCount(), total: total }) : t('scenes.archive.sectionOff'),
2257
+ actions: archiveSegActions(defined, t('memory.archive.addMemories'),
2258
+ function () { setSections(Object.assign({}, modalSections(), { memories: memPreset() })) },
2259
+ function () { var s = Object.assign({}, modalSections()); delete s.memories; setSections(s) },
2260
+ function () { setSections(Object.assign({}, modalSections(), { memories: memPreset() })) },
2261
+ function () { setSections(Object.assign({}, modalSections(), { memories: [] })) }),
2262
+ body: defined
2263
+ ? React.createElement('div', { className: 'dsm-seg-body' },
2264
+ segFilter(modal.memQuery, function (v) { setModal(Object.assign({}, modal, { memQuery: v })) }, t('scenes.mem.searchScene')),
2265
+ (function () {
2266
+ var q = String(modal.memQuery || '').trim().toLowerCase()
2267
+ var shown = q ? groups.filter(function (g) {
2268
+ return String(g.scene.label || g.scene.name).toLowerCase().indexOf(q) >= 0
2269
+ || String(g.scene.description || '').toLowerCase().indexOf(q) >= 0
2270
+ || g.items.some(function (m) { return String(m.name).toLowerCase().indexOf(q) >= 0 })
2271
+ }) : groups
2272
+ if (!shown.length) return React.createElement('div', { className: 'dsm-pick-empty' }, q ? t('scenes.mem.noMatch') : t('scenes.mem.noScenes'))
2273
+ // 场景卡片:一行就能看到「这是在管哪个场景的哪些记忆」,比纯勾选行信息量大。
2274
+ return React.createElement('div', null, shown.map(function (g) {
2275
+ var ids = g.items.map(function (m) { return String(m.id) })
2276
+ var on = ids.filter(function (id) { return (sections.memories || []).indexOf(id) >= 0 }).length
2277
+ return React.createElement('div', { key: 'mg:' + g.scene.name, className: 'dsm-scene-card' },
2278
+ React.createElement('div', { className: 'dsm-scene-card-head' },
2279
+ React.createElement('span', { className: 'dsm-pick-main' },
2280
+ React.createElement('span', { className: 'dsm-pick-name' }, g.scene.label || g.scene.name),
2281
+ React.createElement('span', { className: 'dsm-pick-desc' },
2282
+ (g.scene.description ? g.scene.description + ' · ' : '') + t('scenes.mem.sceneCount', { checked: on, total: ids.length }))),
2283
+ g.scene.global ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('scenes.mem.alwaysOn')) : null,
2284
+ React.createElement('span', { className: 'dsm-pick-actions' },
2285
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !g.items.length, onClick: function (e) { e.preventDefault(); e.stopPropagation(); setModal(Object.assign({}, modal, { memDrill: g.scene.name, memQuery: '' })) } }, t('scenes.mem.pick')))))
2286
+ }))
2287
+ })())
2288
+ : React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.mem.hint'))),
2289
+ foot: archiveSegFoot(defined, memCheckedCount()),
2290
+ })
2291
+ }
2292
+ /** 段 1:MCP 工具集(服务器级勾选 → 行内「选工具」进工具明细,明细留在同一段内)。 */
2293
+ function mcpSeg() {
2294
+ var sections = modalSections()
2295
+ var defined = !!sections.mcp
2296
+ var servers = modal.inventory.mcpServers || []
2297
+ if (modal.drill) {
2298
+ var spec = (sections.mcp || {})[modal.drill]
2299
+ var known = drillTools
2300
+ var checkedCount = spec === '*' ? (known || []).length : (Array.isArray(spec) ? spec.length : 0)
2301
+ return seg({
2302
+ title: t('scenes.mcp.toolsOf') + ' · ' + modal.drill,
2303
+ count: t('scenes.seg.checked', { checked: checkedCount, total: (known || []).length }),
2304
+ actions: [
2305
+ React.createElement('button', { key: 'back', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { setModal(Object.assign({}, modal, { drill: null })) } }, t('scenes.mcp.back')),
2306
+ React.createElement('button', { key: 'all', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !known, onClick: function () { mcpDrillSpec('*') } }, t('scenes.seg.selectAll')),
2307
+ React.createElement('button', { key: 'none', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { mcpDrillSpec([]) } }, t('scenes.seg.clear')),
2308
+ ],
2309
+ body: known === null
2310
+ ? React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('memory.loading')))
2311
+ : segList(known.map(function (item) {
2312
+ var on = spec === '*' ? true : (Array.isArray(spec) && spec.indexOf(item.short) >= 0)
2313
+ return pickRow({
2314
+ disabled: busy,
2315
+ key: item.key,
2316
+ checked: on,
2317
+ name: item.short,
2318
+ onChange: function () { toggleDrillTool(modal.drill, item.short) },
2319
+ })
2320
+ }), t('scenes.mcp.noTools')),
2321
+ foot: React.createElement('div', { className: 'dsm-seg-foot' }, t('scenes.mcp.drillHint')),
2322
+ })
2323
+ }
2324
+ return seg({
2325
+ title: t('memory.archive.tools'),
2326
+ count: defined ? t('scenes.seg.checked', { checked: Object.keys(sections.mcp).length, total: servers.length }) : t('scenes.archive.sectionOff'),
2327
+ actions: archiveSegActions(defined, t('memory.archive.addTools'),
2328
+ function () { setSections(Object.assign({}, modalSections(), { mcp: mcpPreset() })) },
2329
+ function () { var s = Object.assign({}, modalSections()); delete s.mcp; setSections(s) },
2330
+ mcpSelectAll,
2331
+ function () { setSections(Object.assign({}, modalSections(), { mcp: {} })) }),
2332
+ body: defined
2333
+ ? React.createElement('div', { className: 'dsm-seg-body' },
2334
+ segFilter(modal.mcpQuery, function (v) { setModal(Object.assign({}, modal, { mcpQuery: v })) }, t('scenes.filter.servers')),
2335
+ (function () {
2336
+ var q = String(modal.mcpQuery || '').trim().toLowerCase()
2337
+ var shown = q ? servers.filter(function (s) { return String(s.name).toLowerCase().indexOf(q) >= 0 }) : servers
2338
+ if (!shown.length) return React.createElement('div', { className: 'dsm-pick-empty' }, q ? t('scenes.mem.noMatch') : t('scenes.mcp.noServers'))
2339
+ return React.createElement('div', null, shown.map(function (server) {
2340
+ var selected = sections.mcp[server.name] !== undefined
2341
+ var spec2 = sections.mcp[server.name]
2342
+ var specText = !selected ? '' : spec2 === '*' ? t('scenes.mcp.allTools') : t('scenes.mcp.pickedCount', { count: spec2.length })
2343
+ return pickRow({
2344
+ disabled: busy,
2345
+ key: server.name,
2346
+ checked: selected,
2347
+ name: server.name,
2348
+ desc: server.toolCount === null || server.toolCount === undefined ? null : t('scenes.mcp.toolCount', { count: server.toolCount }),
2349
+ meta: [
2350
+ server.live ? null : React.createElement('span', { key: 'nr', className: 'dsm-tag dsm-tag-off' }, t('scenes.mcp.notRunning')),
2351
+ specText ? React.createElement('span', { key: 'spec' }, specText) : null,
2352
+ ],
2353
+ actions: selected ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function (e) { e.preventDefault(); e.stopPropagation(); openDrill(server.name) } }, t('scenes.mcp.pickTools')) : null,
2354
+ onChange: function () { toggleMcpServer(server.name) },
2355
+ })
2356
+ }))
2357
+ })())
2358
+ : React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.mcp.hint'))),
2359
+ foot: archiveSegFoot(defined, defined ? Object.keys(sections.mcp).length : 0),
2360
+ })
2361
+ }
2362
+ /** 段 2:技能集。 */
2363
+ function skillsSeg() {
2364
+ var sections = modalSections()
2365
+ var defined = !!sections.skills
2366
+ var items = (function () {
2367
+ var q = String(modal.skillQuery || '').trim().toLowerCase()
2368
+ var all = modal.inventory.skills || []
2369
+ return q ? all.filter(function (x) { return String(x.key).toLowerCase().indexOf(q) >= 0 }) : all
2370
+ })()
2371
+ var allItems = modal.inventory.skills || []
2372
+ return seg({
2373
+ title: t('memory.archive.skills'),
2374
+ count: defined ? t('scenes.seg.checked', { checked: sections.skills.length, total: allItems.length }) : t('scenes.archive.sectionOff'),
2375
+ actions: archiveSegActions(defined, t('memory.archive.addSkills'),
2376
+ function () { setSections(Object.assign({}, modalSections(), { skills: allItems.filter(function (x) { return x.enabled }).map(function (x) { return x.key }) })) },
2377
+ function () { var s = Object.assign({}, modalSections()); delete s.skills; setSections(s) },
2378
+ function () { setSections(Object.assign({}, modalSections(), { skills: allItems.map(function (x) { return x.key }) })) },
2379
+ function () { setSections(Object.assign({}, modalSections(), { skills: [] })) }),
2380
+ body: defined
2381
+ ? React.createElement('div', { className: 'dsm-seg-body' },
2382
+ segFilter(modal.skillQuery, function (v) { setModal(Object.assign({}, modal, { skillQuery: v })) }, t('scenes.filter.skills')),
2383
+ items.length
2384
+ ? React.createElement('div', null, items.map(function (item) {
2385
+ return pickRow({
2386
+ disabled: busy,
2387
+ key: item.key,
2388
+ checked: sections.skills.indexOf(item.key) >= 0,
2389
+ name: item.key,
2390
+ meta: item.enabled === false ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('memory.scene.off')) : null,
2391
+ onChange: function () {
2392
+ var list = sections.skills.slice(); var i = list.indexOf(item.key)
2393
+ if (i >= 0) list.splice(i, 1); else list.push(item.key)
2394
+ setSections(Object.assign({}, modalSections(), { skills: list }))
2395
+ },
2396
+ })
2397
+ }))
2398
+ : React.createElement('div', { className: 'dsm-pick-empty' }, modal.skillQuery ? t('scenes.mem.noMatch') : t('scenes.skills.empty')))
2399
+ : React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.skills.hint'))),
2400
+ foot: archiveSegFoot(defined, defined ? sections.skills.length : 0),
2401
+ })
2402
+ }
2403
+ /** 段 3:子智能体绑定。 */
2404
+ function subagentsSeg() {
2405
+ var sections = modalSections()
2406
+ var defined = !!sections.subagents
2407
+ var allItems = modal.inventory.subagents || []
2408
+ var items = (function () {
2409
+ var q = String(modal.subQuery || '').trim().toLowerCase()
2410
+ return q ? allItems.filter(function (x) {
2411
+ return String(x.name).toLowerCase().indexOf(q) >= 0 || String(x.description || '').toLowerCase().indexOf(q) >= 0
2412
+ }) : allItems
2413
+ })()
2414
+ return seg({
2415
+ title: t('memory.archive.subagents'),
2416
+ count: defined ? t('scenes.seg.checked', { checked: sections.subagents.length, total: allItems.length }) : t('scenes.archive.sectionOff'),
2417
+ actions: archiveSegActions(defined, t('memory.archive.addSubagents'),
2418
+ function () { setSections(Object.assign({}, modalSections(), { subagents: [] })) },
2419
+ function () { var s = Object.assign({}, modalSections()); delete s.subagents; setSections(s) },
2420
+ function () { setSections(Object.assign({}, modalSections(), { subagents: allItems.map(function (x) { return x.name }) })) },
2421
+ function () { setSections(Object.assign({}, modalSections(), { subagents: [] })) }),
2422
+ body: defined
2423
+ ? React.createElement('div', { className: 'dsm-seg-body' },
2424
+ segFilter(modal.subQuery, function (v) { setModal(Object.assign({}, modal, { subQuery: v })) }, t('scenes.filter.subagents')),
2425
+ items.length
2426
+ ? React.createElement('div', null, items.map(function (item) {
2427
+ return pickRow({
2428
+ disabled: busy,
2429
+ key: item.name,
2430
+ checked: sections.subagents.indexOf(item.name) >= 0,
2431
+ name: item.name,
2432
+ desc: item.description || null,
2433
+ onChange: function () {
2434
+ var list = sections.subagents.slice(); var i = list.indexOf(item.name)
2435
+ if (i >= 0) list.splice(i, 1); else list.push(item.name)
2436
+ setSections(Object.assign({}, modalSections(), { subagents: list }))
2437
+ },
2438
+ })
2439
+ }))
2440
+ : React.createElement('div', { className: 'dsm-pick-empty' }, modal.subQuery ? t('scenes.mem.noMatch') : t('scenes.subagents.empty')))
2441
+ : React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.subagents.hint'))),
2442
+ foot: archiveSegFoot(defined, defined ? sections.subagents.length : 0),
2443
+ })
2444
+ }
2445
+ /**
2446
+ * 档案编辑弹窗:顶部一行摘要(MCP/技能/子智能体/记忆各勾了多少)+ 四段 + 底部动作。
2447
+ * 结构固定 —— 加/删段、进出「选工具」/「选记忆」明细都不会改变弹窗尺寸或元素顺序。
2448
+ */
2449
+ function archiveNode() {
2450
+ if (!modal || modal.type !== 'scene-archive') return null
2451
+ return React.createElement(Modal, { key: 'sarch', wide: true, className: 'dsm-modal-archive', title: t('memory.archive.title') + ' · ' + (modal.name === '' ? t('memory.scene.global') : modal.name), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
2452
+ React.createElement('div', { className: 'dsm-form' },
2453
+ React.createElement('div', { className: 'dsm-archive-meta' },
2454
+ React.createElement('span', null, t('scenes.archive.summary', {
2455
+ mcp: modal.sections.mcp ? Object.keys(modal.sections.mcp).length : 0,
2456
+ skills: modal.sections.skills ? modal.sections.skills.length : 0,
2457
+ subagents: modal.sections.subagents ? modal.sections.subagents.length : 0,
2458
+ memories: modal.sections.memories ? modal.sections.memories.length : 0,
2459
+ })),
2460
+ React.createElement('span', { className: 'dsm-help' }, t('scenes.archive.note'))),
2461
+ mcpSeg(),
2462
+ skillsSeg(),
2463
+ subagentsSeg(),
2464
+ memoriesSeg(),
2465
+ React.createElement('div', { className: 'dsm-modal-actions' },
2466
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, t('btn.cancel')),
2467
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-primary', disabled: busy, onClick: submitArchive }, t('memory.archive.save')))))
2468
+ }
2469
+ function openDrill(server) {
2470
+ setModal(Object.assign({}, modal, { drill: server }))
2471
+ setDrillTools(null)
2472
+ apiCall('mcpm-tools', { serverName: server }).then(function (res) {
2473
+ if (res && res.ok) setDrillTools((res.tools || []).map(function (x) { return { key: server + '/' + x.name, short: x.name, enabled: x.enabled !== false } }))
2474
+ else setDrillTools([])
2475
+ }).catch(function () { setDrillTools([]) })
2476
+ }
2477
+ function toggleDrillTool(server, short) {
2478
+ var sections = modalSections(); if (!sections) return
2479
+ var mcp = Object.assign({}, sections.mcp || {})
2480
+ var cur = mcp[server] === '*' ? (drillTools || []).filter(function (t) { return t.enabled }).map(function (t) { return t.short }) : (mcp[server] || []).slice()
2481
+ var i = cur.indexOf(short)
2482
+ if (i >= 0) cur.splice(i, 1); else cur.push(short)
2483
+ mcp[server] = cur
2484
+ setSections(Object.assign({}, sections, { mcp: mcp }))
2485
+ }
2486
+ function submitArchive() {
2487
+ if (!modalSections()) return
2488
+ setBusy(true)
2489
+ apiCall('scene-archive-save', { scene: modal.name, archive: (function () { var payload = {}; if (modal.sections.mcp) payload.mcp = modal.sections.mcp; if (modal.sections.skills) payload.skills = modal.sections.skills; if (modal.sections.subagents) payload.subagents = modal.sections.subagents; if (modal.sections.memories) payload.memories = modal.sections.memories; return payload })() }).then(function (res) {
2490
+ setBusy(false)
2491
+ if (res && res.ok) {
2492
+ setModal(null)
2493
+ setResult({ ok: true, text: t('memory.result.archiveSaved', { name: modal.name }) + (res.stale && res.stale.length ? ' · ' + t('memory.archive.stale', { items: res.stale.join('、') }) : '') })
2494
+ refresh(true)
2495
+ } else setResult({ ok: false, text: translateError(t, res) })
2496
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2497
+ }
2498
+ function enterMode(name) {
2499
+ setBusy(true)
2500
+ apiCall('scene-mode-set', { scene: name }).then(function (res) {
2501
+ setBusy(false)
2502
+ if (res && res.ok) { setResult({ ok: true, text: t('memory.result.modeSet', { name: name }) + (res.stale && res.stale.length ? ' · ' + t('memory.archive.stale', { items: res.stale.join('、') }) : '') }); refresh(true) }
2503
+ else setResult({ ok: false, text: translateError(t, res) })
2504
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2505
+ }
2506
+ function exitMode() {
2507
+ setBusy(true)
2508
+ apiCall('scene-mode-set', { scene: null }).then(function (res) {
2509
+ setBusy(false)
2510
+ if (res && res.ok) { setResult({ ok: true, text: t('memory.result.modeExited') }); refresh(true) }
2511
+ else setResult({ ok: false, text: translateError(t, res) })
2512
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2513
+ }
2514
+ var modeScene = data.mode && data.mode.scene
2515
+ // 与 MCP / 技能页同构的三格统计(页面之间「头顶长什么样」保持一致)。
2516
+ var sceneStats = (function () {
2517
+ var own = (data.scenes || [])
2518
+ return {
2519
+ total: own.length,
2520
+ active: own.filter(function (s) { return s.active === true }).length,
2521
+ archives: Object.keys(data.archives || {}).length,
2522
+ }
2523
+ })()
2524
+ /**
2525
+ * 场景已绑定的东西,一行摘要(没绑的域不出现)。
2526
+ * 之前卡片上只有一个「档案」标签,用户点进去才知道里面配了什么。
2527
+ */
2528
+ function archiveSummary(archive) {
2529
+ if (!archive) return ''
2530
+ var parts = []
2531
+ if (archive.mcp) parts.push(t('scenes.profile.mcp', { count: Object.keys(archive.mcp).length }))
2532
+ if (Array.isArray(archive.skills)) parts.push(t('scenes.profile.skills', { count: archive.skills.length }))
2533
+ if (Array.isArray(archive.subagents)) parts.push(t('scenes.profile.subagents', { count: archive.subagents.length }))
2534
+ if (Array.isArray(archive.memories)) parts.push(t('scenes.profile.memories', { count: archive.memories.length }))
2535
+ return parts.join(' · ')
2536
+ }
2537
+ return React.createElement('section', { className: 'dsm-section' },
2538
+ React.createElement('div', { className: 'dsm-head' },
2539
+ React.createElement('div', { className: 'dsm-title-block' },
2540
+ React.createElement('div', { className: 'dsm-title-row' },
2541
+ React.createElement('h2', { className: 'dsm-title' }, t('scenes.title')),
2542
+ React.createElement(VersionBadge, null)),
2543
+ React.createElement('p', { className: 'dsm-desc' }, t('scenes.desc'))),
2544
+ React.createElement('div', { className: 'dsm-actions' },
2545
+ modeScene ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.mode.current') + ': ' + (sceneLabel(data.scenes, modeScene) || modeScene)) : null,
2546
+ modeScene ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy, onClick: exitMode }, t('memory.mode.exit')) : null,
2547
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }, t('memory.btn.refresh')),
2548
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openCreateScene }, t('memory.btn.newScene')))),
2549
+ React.createElement('div', { key: 'stats', className: 'dsm-summary dsm-summary-3' },
2550
+ [[sceneStats.total, t('scenes.stat.total')], [sceneStats.active, t('scenes.stat.active')], [sceneStats.archives, t('scenes.stat.archives')]].map(function (item) {
2551
+ return React.createElement('div', { key: item[1], className: 'dsm-stat' },
2552
+ React.createElement('strong', null, item[0]), item[1])
2553
+ })),
2554
+ React.createElement(Notice, { key: 'notice', kind: result && result.ok ? 'ok' : 'err', text: result && result.text }),
2555
+ data.error ? React.createElement('div', { key: 'gerr', className: 'dsm-feedback dsm-error' }, String(data.error)) : null,
2556
+ data.loading ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
2557
+ : React.createElement('div', { className: 'dsm-sources' }, (data.scenes || []).map(function (scene) {
2558
+ var name = scene.name
2559
+ var archive = data.archives[name]
2560
+ var hasModeSections = !!(archive && (archive.mcp || Array.isArray(archive.skills)))
2561
+ var summary = archiveSummary(archive)
2562
+ return React.createElement('div', { key: 's:' + name, className: 'dsm-source' },
2563
+ React.createElement('div', { className: 'dsm-source-head' },
2564
+ React.createElement('div', { className: 'dsm-source-head-main' },
2565
+ React.createElement('span', { className: 'dsm-source-title' }, sceneLabel(data.scenes, name) || name),
2566
+ scene.global === true ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.global.tag')) : null,
2567
+ scene.shared ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.shared')) : null,
2568
+ modeScene === name ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.mode.current')) : null,
2569
+ scene.active === false ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('memory.scene.off')) : null,
2570
+ // 描述 + 记忆条数 + 已绑定内容:卡片本身就把「这个场景是什么」说清楚。
2571
+ React.createElement('span', { className: 'dsm-note' },
2572
+ (scene.description ? scene.description : t('scenes.noDesc'))
2573
+ + ' · ' + t('scenes.memCount', { count: scene.count || 0 })
2574
+ + (summary ? ' · ' + t('scenes.profile.summary', { parts: summary })
2575
+ : (archive ? ' · ' + t('scenes.profile.empty') : ''))),
2576
+ // 场景名(磁盘上的目录名)与显示名可能不同——把真名放在路径位,方便对文件。
2577
+ React.createElement('span', { className: 'dsm-path' }, name)),
2578
+ React.createElement('div', { className: 'dsm-source-actions' },
2579
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { openArchive(name) } }, t('memory.archive.edit')),
2580
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { openEditScene(scene) } }, t('memory.scene.edit')),
2581
+ hasModeSections && modeScene !== name ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { enterMode(name) } }, t('memory.mode.set')) : null,
2582
+ scene.global === true || scene.shared ? null : React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'scene-delete', name: name }) } }, t('memory.btn.deleteScene')),
2583
+ scene.global === true || scene.shared ? null : React.createElement(Switch, { on: scene.active === true, disabled: busy, label: t('memory.scene.enable') + ' ' + name, onClick: function () { toggleScene(scene) } }))))
2584
+ })),
2585
+ modal && (modal.type === 'scene-create' || modal.type === 'scene-edit') ? React.createElement(Modal, { key: 'screate', title: modal.type === 'scene-create' ? t('memory.scene.createTitle') : t('memory.scene.editTitle'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
2586
+ React.createElement('div', { className: 'dsm-form' },
2587
+ React.createElement('label', { className: 'dsm-field' },
2588
+ React.createElement('span', { className: 'dsm-label' }, t('memory.scene.field.name')),
2589
+ React.createElement('input', {
2590
+ className: 'dsm-control' + (sceneForm.error ? ' dsm-rule-invalid' : ''),
2591
+ value: sceneForm.name || '',
2592
+ disabled: modal.type === 'scene-edit',
2593
+ placeholder: t('memory.scene.field.name.placeholder'),
2594
+ onChange: function (e) { setSceneForm(Object.assign({}, sceneForm, { name: e.target.value, error: null })) },
2595
+ }),
2596
+ React.createElement('p', { className: sceneForm.error ? 'dsm-rule-hint' : 'dsm-help' }, sceneForm.error || (modal.type === 'scene-create' ? t('memory.scene.field.name.hint') : t('memory.scene.field.name.lock')))),
2597
+ React.createElement('label', { className: 'dsm-field' },
2598
+ React.createElement('span', { className: 'dsm-label' }, t('memory.scene.field.desc')),
2599
+ React.createElement('input', {
2600
+ className: 'dsm-control',
2601
+ value: sceneForm.description || '',
2602
+ placeholder: t('memory.scene.field.desc.placeholder'),
2603
+ onChange: function (e) { setSceneForm(Object.assign({}, sceneForm, { description: e.target.value, error: null })) },
2604
+ })),
2605
+ React.createElement('div', { className: 'dsm-modal-actions' },
2606
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, t('btn.cancel')),
2607
+ React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || !String(sceneForm.name || '').trim(), onClick: submitSceneForm }, t(modal.type === 'scene-create' ? 'memory.btn.create' : 'memory.btn.saveScene'))))) : null,
2608
+ modal && modal.type === 'scene-delete' ? React.createElement(Modal, { key: 'sdel', title: t('memory.deleteScene.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
2609
+ React.createElement('p', { className: 'dsm-help' }, t('memory.deleteScene.desc', { name: modal.name })),
2610
+ React.createElement('div', { className: 'dsm-modal-actions' },
2611
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, t('btn.cancel')),
2612
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDeleteScene(modal.name) } }, t('memory.btn.deleteScene')))) : null,
2613
+ // 档案编辑器(见 archiveNode):三段共用「段卡片 + 勾选行」排版。
2614
+ archiveNode(),
2615
+ )
2616
+ }
2617
+ // ---------- 子智能体页:人设文件(~/.dsh/subagents/*.md)管理 ----------
2618
+ function SubagentsPage() {
2619
+ var state = React.useState({ loading: true, error: null, subagents: [] })
2620
+ var data = state[0], setData = state[1]
2621
+ var bs = React.useState(false)
2622
+ var busy = bs[0], setBusy = bs[1]
2623
+ var rs = React.useState(null)
2624
+ var result = rs[0], setResult = rs[1]
2625
+ var ms = React.useState(null)
2626
+ var modal = ms[0], setModal = ms[1]
2627
+ React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
2628
+ function refresh(silent) {
2629
+ if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
2630
+ apiCall('subagent-list', {}).then(function (r) {
2631
+ if (r && r.ok) setData({ loading: false, error: null, subagents: r.subagents || [] })
2632
+ else setData({ loading: false, error: translateError(t, r), subagents: [] })
2633
+ }).catch(function (e) { setData({ loading: false, error: String((e && e.message) || e), subagents: [] }) })
2634
+ }
2635
+ React.useEffect(function () { refresh() }, [])
2636
+ /**
2637
+ * 人设表单的候选数据(模型目录 / 全体预设工具并集)只在**首次展开高级选项**时拉取:
2638
+ * 宿主枚举预设需要为尚未挂载的预设建立 standing mount,不该在打开弹窗时就付这个代价。
2639
+ */
2640
+ var cands = React.useState({ loaded: false, loading: false, error: null, models: [], tools: [] })
2641
+ var cand = cands[0], setCand = cands[1]
2642
+ function loadCandidates() {
2643
+ if (cand.loaded || cand.loading) return
2644
+ setCand(Object.assign({}, cand, { loading: true, error: null }))
2645
+ Promise.all([apiCall('model-candidates', {}), apiCall('preset-tools', {})]).then(function (rs) {
2646
+ var mres = rs[0], tres = rs[1]
2647
+ var failed = (!mres || mres.ok === false) && (!tres || tres.ok === false)
2648
+ setCand({
2649
+ loaded: true, loading: false,
2650
+ error: failed ? t('subagents.adv.loadFailed') : null,
2651
+ models: (mres && mres.models) || [],
2652
+ tools: (tres && tres.tools) || [],
2653
+ })
2654
+ }).catch(function (e) {
2655
+ setCand({ loaded: true, loading: false, error: String((e && e.message) || e), models: [], tools: [] })
2656
+ })
2657
+ }
2658
+ /** 高级选项:默认收起;已经在用模型/工具限制的人设自动展开(否则用户看不见自己配了什么)。 */
2659
+ function initialAdvanced(p) {
2660
+ return !!((p && (p.model || p.provider)) || (p && ((p.tools || []).length || (p.toolsDeny || []).length)))
2661
+ }
2662
+ function openEditor(name) {
2663
+ if (!name) {
2664
+ setModal({ type: 'editor', mode: 'create', advanced: false, form: { name: '', description: '', provider: '', model: '', tools: [], toolsDeny: [], body: '', error: null } })
2665
+ return
2666
+ }
2667
+ setBusy(true)
2668
+ apiCall('subagent-get', { name: name }).then(function (res) {
2669
+ setBusy(false)
2670
+ if (res && res.ok) {
2671
+ var p = res.persona || {}
2672
+ setModal({ type: 'editor', mode: 'edit', advanced: initialAdvanced(p), form: {
2673
+ name: p.name || name, description: p.description || '', provider: p.provider || '', model: p.model || '',
2674
+ tools: (p.tools || []).slice(), toolsDeny: (p.toolsDeny || []).slice(), body: p.body || '', error: null,
2675
+ } })
2676
+ } else setResult({ ok: false, text: translateError(t, res) })
2677
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2678
+ }
2679
+ function setForm(patch) { if (modal && modal.type === 'editor') setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, patch) })) }
2680
+ /** 列表里增删一项(勾选语义:存在 = 勾上)。 */
2681
+ function toggled(list, name) {
2682
+ var next = (list || []).slice()
2683
+ var i = next.indexOf(name)
2684
+ if (i >= 0) next.splice(i, 1); else next.push(name)
2685
+ return next
2686
+ }
2687
+ /**
2688
+ * 模型下拉的当前值。
2689
+ * 宿主目录里有这对 (provider, model) → 用它;只有 model 没有 provider(跨来源手工填的)
2690
+ * 或目录里没有 → 走「自定义」,同时把输入框露出来,避免把用户已配好的值悄悄改掉。
2691
+ */
2692
+ function modelSelectValue() {
2693
+ if (!modal || modal.type !== 'editor') return ''
2694
+ var f = modal.form
2695
+ if (!f.model) return ''
2696
+ var hit = (cand.models || []).filter(function (m) { return m.provider === f.provider && m.id === f.model })[0]
2697
+ return hit ? (f.provider + '\u0000' + f.model) : '__custom__'
2698
+ }
2699
+ /**
2700
+ * 工具选择器(白名单 / 黑名单共用)。
2701
+ *
2702
+ * 候选 = 宿主回传的**全体 Agent 预设工具并集**,并标注「当前会话可见」:
2703
+ * 人设可能在任何预设下被子代理复用,只列当前会话的工具会让换预设后的子代理启动失败
2704
+ * (官方 toolFilter 对未知名直接拒绝启动)。因此这里宁可多列,并把手填入口留出来。
2705
+ */
2706
+ function toolPicker(props) {
2707
+ var all = cand.tools || []
2708
+ var q = String(modal.toolQuery || '').trim().toLowerCase()
2709
+ var list = q ? all.filter(function (x) { return String(x.name).toLowerCase().indexOf(q) >= 0 }) : all
2710
+ var picked = props.selected || []
2711
+ var current = list.filter(function (x) { return x.current })
2712
+ var others = list.filter(function (x) { return !x.current })
2713
+ function row(item) {
2714
+ return pickRow({
2715
+ disabled: busy,
2716
+ key: (props.deny ? 'd:' : 'a:') + item.name,
2717
+ checked: picked.indexOf(item.name) >= 0,
2718
+ name: item.name,
2719
+ meta: item.current
2720
+ ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, props.current)
2721
+ : React.createElement('span', { className: 'dsm-tag' }, props.unavailable),
2722
+ onChange: function () { props.onToggle(item.name) },
2723
+ })
2724
+ }
2725
+ return React.createElement('div', { className: 'dsm-field' },
2726
+ React.createElement('span', { className: 'dsm-label' },
2727
+ props.label + (picked.length ? ' · ' + t('subagents.tools.count', { count: picked.length }) : '')),
2728
+ picked.length
2729
+ ? React.createElement('div', { className: 'dsm-tools-chips' }, picked.map(function (name) {
2730
+ return React.createElement('span', { key: 'c:' + name, className: 'dsm-chip' + (props.deny ? ' dsm-chip-deny' : '') },
2731
+ name,
2732
+ React.createElement('button', { type: 'button', title: t('subagents.tools.remove'), onClick: function () { props.onToggle(name) } }, '×'))
2733
+ }))
2734
+ : React.createElement('span', { className: 'dsm-adv-note' }, props.chosen),
2735
+ React.createElement('div', { className: 'dsm-combo-row' },
2736
+ React.createElement('input', {
2737
+ className: 'dsm-control',
2738
+ value: modal.toolQuery || '',
2739
+ placeholder: props.filterPlaceholder,
2740
+ onChange: function (e) { setModal(Object.assign({}, modal, { toolQuery: e.target.value })) },
2741
+ }),
2742
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !all.length, onClick: props.onAll }, t('scenes.seg.selectAll')),
2743
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !picked.length, onClick: props.onClear }, t('scenes.seg.clear'))),
2744
+ React.createElement('div', { className: 'dsm-tools-grid' },
2745
+ list.length
2746
+ ? React.createElement('div', null,
2747
+ current.length ? segGroup(props.groupCurrent) : null,
2748
+ current.map(row),
2749
+ others.length ? segGroup(props.groupOther) : null,
2750
+ others.map(row))
2751
+ : React.createElement('div', { className: 'dsm-pick-empty' }, all.length ? t('scenes.mem.noMatch') : props.empty)),
2752
+ React.createElement('p', { className: 'dsm-help' }, props.hint))
2753
+ }
2754
+ function toggleAdvanced() {
2755
+ if (!modal || modal.type !== 'editor') return
2756
+ var next = !modal.advanced
2757
+ setModal(Object.assign({}, modal, { advanced: next }))
2758
+ if (next) loadCandidates()
2759
+ }
2760
+ function submitEditor() {
2761
+ if (!modal || modal.type !== 'editor') return
2762
+ setBusy(true)
2763
+ var op = modal.mode === 'create' ? 'subagent-create' : 'subagent-update'
2764
+ apiCall(op, {
2765
+ name: modal.form.name, description: modal.form.description,
2766
+ provider: modal.form.provider, model: modal.form.model,
2767
+ tools: modal.form.tools || [], toolsDeny: modal.form.toolsDeny || [],
2768
+ body: modal.form.body,
2769
+ }).then(function (res) {
2770
+ setBusy(false)
2771
+ if (res && res.ok) { setModal(null); setResult({ ok: true, text: t('subagents.result.saved', { name: modal.form.name }) }); refresh(true) }
2772
+ else setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, { error: translateError(t, res) }) }))
2773
+ }).catch(function (e) { setBusy(false); setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, { error: String((e && e.message) || e) }) })) })
2774
+ }
2775
+ function submitDelete(name) {
2776
+ setBusy(true)
2777
+ apiCall('subagent-delete', { name: name }).then(function (res) {
2778
+ setBusy(false); setModal(null)
2779
+ if (res && res.ok) { setResult({ ok: true, text: t('subagents.result.deleted', { name: name }) }); refresh(true) }
2780
+ else setResult({ ok: false, text: translateError(t, res) })
2781
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2782
+ }
2783
+ // ── 导入人设(.md / .zip,多选或拖入;同名跳过并报告)──
2784
+ function submitImport(files) {
2785
+ if (!files || !files.length) return
2786
+ setBusy(true)
2787
+ apiCall('subagent-import', { files: files }).then(function (res) {
2788
+ setBusy(false)
2789
+ if (res && res.ok) {
2790
+ setModal(null)
2791
+ var names = res.imported || [], skipped = res.skipped || []
2792
+ var text = names.length ? t('subagents.result.imported', { count: names.length, names: names.join('、') }) : t('import.none')
2793
+ if (skipped.length) text += ' · ' + t('import.skipped', { items: skipped.map(function (s) { return s.name + '(' + s.reason + ')' }).join(';') })
2794
+ setResult({ ok: true, text: text }); refresh(true)
2795
+ } else setResult({ ok: false, text: translateError(t, res) })
2796
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2797
+ }
2798
+ return React.createElement('section', { className: 'dsm-section' },
2799
+ React.createElement('div', { className: 'dsm-head' },
2800
+ React.createElement('div', { className: 'dsm-title-block' },
2801
+ React.createElement('div', { className: 'dsm-title-row' },
2802
+ React.createElement('h2', { className: 'dsm-title' }, t('subagents.title')),
2803
+ React.createElement(VersionBadge, null)),
2804
+ React.createElement('p', { className: 'dsm-desc' }, t('subagents.desc'))),
2805
+ React.createElement('div', { className: 'dsm-actions' },
2806
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }, t('memory.btn.refresh')),
2807
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setResult(null); setModal({ type: 'import' }) } }, t('subagents.import')),
2808
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { openEditor(null) } }, t('subagents.new')))),
2809
+ React.createElement(Notice, { kind: result && result.ok ? 'ok' : 'err', text: result && result.text }),
2810
+ data.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(data.error)) : null,
2811
+ data.loading ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
2812
+ : data.subagents.length ? React.createElement('div', { className: 'dsm-sources' }, data.subagents.map(function (p) {
2813
+ return React.createElement('div', { key: p.name, className: 'dsm-source' },
2814
+ React.createElement('div', { className: 'dsm-source-head' },
2815
+ React.createElement('div', { className: 'dsm-source-head-main' },
2816
+ React.createElement('span', { className: 'dsm-source-title' }, p.name),
2817
+ React.createElement('span', { className: 'dsm-note' }, p.description || '')),
2818
+ React.createElement('div', { className: 'dsm-source-actions' },
2819
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { openEditor(p.name) } }, t('memory.edit')),
2820
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'delete', name: p.name }) } }, t('memory.delete')))))
2821
+ })) : React.createElement('div', { className: 'dsm-empty' }, t('subagents.empty')),
2822
+ modal && modal.type === 'editor' ? React.createElement(Modal, { key: 'sedit', wide: true, title: modal.mode === 'create' ? t('subagents.create') : t('subagents.edit') + ' · ' + modal.form.name, closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
2823
+ React.createElement('div', { className: 'dsm-form' },
2824
+ React.createElement('label', { className: 'dsm-field' },
2825
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.name')),
2826
+ React.createElement('input', { className: 'dsm-control', value: modal.form.name || '', disabled: modal.mode === 'edit', placeholder: 'code-review', onChange: function (e) { setForm({ name: e.target.value }) } })),
2827
+ React.createElement('label', { className: 'dsm-field' },
2828
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.description')),
2829
+ React.createElement('input', { className: 'dsm-control', value: modal.form.description || '', placeholder: t('subagents.field.description.placeholder'), onChange: function (e) { setForm({ description: e.target.value }) } })),
2830
+ React.createElement('label', { className: 'dsm-field' },
2831
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.body')),
2832
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-md', value: modal.form.body || '', placeholder: t('subagents.field.body.placeholder'), onChange: function (e) { setForm({ body: e.target.value }) } })),
2833
+ // ── 高级选项(默认收起)──
2834
+ // 模型 / 工具限制是「少数人才改」的字段,但一旦改错代价高(跨来源模型、工具名打错
2835
+ // 会让子代理直接启动失败)。所以:收起来但**有值就自动展开**,并把候选做成选择器。
2836
+ React.createElement('div', { className: 'dsm-adv' },
2837
+ React.createElement('button', { type: 'button', className: 'dsm-adv-head', 'aria-expanded': modal.advanced === true, onClick: toggleAdvanced },
2838
+ React.createElement('span', { className: 'dsm-adv-caret' }, modal.advanced ? '▼' : '▶'),
2839
+ React.createElement('span', null, t('subagents.adv.title')),
2840
+ React.createElement('span', { className: 'dsm-adv-note' },
2841
+ modal.advanced ? '' : t('subagents.adv.summary', {
2842
+ model: modal.form.model ? (modal.form.provider ? modal.form.provider + '/' + modal.form.model : modal.form.model) : t('subagents.adv.inherit'),
2843
+ allow: (modal.form.tools || []).length,
2844
+ deny: (modal.form.toolsDeny || []).length,
2845
+ }))),
2846
+ modal.advanced ? React.createElement('div', { className: 'dsm-adv-body' },
2847
+ // 模型:宿主 LLM 目录里的 (provider, model) 对 + 自定义兜底。
2848
+ React.createElement('div', { className: 'dsm-field' },
2849
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.model')),
2850
+ React.createElement('div', { className: 'dsm-combo-row' },
2851
+ React.createElement('div', { className: 'dsm-select' },
2852
+ React.createElement('select', {
2853
+ className: 'dsm-control',
2854
+ value: modelSelectValue(),
2855
+ disabled: busy || cand.loading,
2856
+ onChange: function (e) {
2857
+ var v = e.target.value
2858
+ if (v === '') setForm({ provider: '', model: '' })
2859
+ else if (v !== '__custom__') {
2860
+ var parts = v.split('\u0000')
2861
+ setForm({ provider: parts[0], model: parts[1] })
2862
+ } else setForm({ model: modal.form.model || '' })
2863
+ },
2864
+ },
2865
+ React.createElement('option', { value: '' }, t('subagents.model.inherit')),
2866
+ (cand.models || []).map(function (m) {
2867
+ return React.createElement('option', { key: m.provider + '/' + m.id, value: m.provider + '\u0000' + m.id }, m.provider + ' · ' + m.name)
2868
+ }),
2869
+ React.createElement('option', { value: '__custom__' }, t('subagents.model.customOption')))),
2870
+ modelSelectValue() === '__custom__'
2871
+ ? React.createElement('input', { className: 'dsm-control', value: modal.form.model || '', placeholder: t('subagents.field.model.placeholder'), onChange: function (e) { setForm({ model: e.target.value }) } })
2872
+ : null),
2873
+ React.createElement('p', { className: 'dsm-help' }, cand.loading ? t('memory.loading') : t('subagents.field.model.hint'))),
2874
+ // provider 独立成一项:跨来源模型(如 sensenova)需要 provider+model 两个键同时给。
2875
+ React.createElement('label', { className: 'dsm-field' },
2876
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.provider')),
2877
+ React.createElement('input', { className: 'dsm-control', value: modal.form.provider || '', placeholder: t('subagents.field.provider.placeholder'), onChange: function (e) { setForm({ provider: e.target.value }) } }),
2878
+ React.createElement('p', { className: 'dsm-help' }, t('subagents.field.provider.hint'))),
2879
+ // 工具白名单:勾选 = 子代理只保留这些工具(不勾 = 基础组合全集)。
2880
+ toolPicker({
2881
+ key: 'allow',
2882
+ label: t('subagents.field.tools'),
2883
+ hint: t('subagents.field.tools.hint'),
2884
+ selected: modal.form.tools || [],
2885
+ onToggle: function (name) { setForm({ tools: toggled(modal.form.tools || [], name) }) },
2886
+ onClear: function () { setForm({ tools: [] }) },
2887
+ onAll: function () { setForm({ tools: (cand.tools || []).map(function (x) { return x.name }) }) },
2888
+ chosen: t('subagents.tools.chosen'),
2889
+ filterPlaceholder: t('subagents.tools.filter'),
2890
+ empty: t('subagents.tools.empty'),
2891
+ unavailable: t('subagents.tools.unavailable'),
2892
+ current: t('subagents.tools.current'),
2893
+ groupCurrent: t('subagents.tools.groupCurrent'),
2894
+ groupOther: t('subagents.tools.groupOther'),
2895
+ }),
2896
+ // 工具黑名单:勾选 = 从子代理可见集合里移除(官方 ToolRestriction.deny,优先级高于白名单)。
2897
+ toolPicker({
2898
+ key: 'deny',
2899
+ label: t('subagents.field.toolsDeny'),
2900
+ hint: t('subagents.field.toolsDeny.hint'),
2901
+ deny: true,
2902
+ selected: modal.form.toolsDeny || [],
2903
+ onToggle: function (name) { setForm({ toolsDeny: toggled(modal.form.toolsDeny || [], name) }) },
2904
+ onClear: function () { setForm({ toolsDeny: [] }) },
2905
+ onAll: function () { setForm({ toolsDeny: (cand.tools || []).map(function (x) { return x.name }) }) },
2906
+ chosen: t('subagents.tools.chosen'),
2907
+ filterPlaceholder: t('subagents.tools.filter'),
2908
+ empty: t('subagents.tools.empty'),
2909
+ unavailable: t('subagents.tools.unavailable'),
2910
+ current: t('subagents.tools.current'),
2911
+ groupCurrent: t('subagents.tools.groupCurrent'),
2912
+ groupOther: t('subagents.tools.groupOther'),
2913
+ }),
2914
+ cand.error ? React.createElement('div', { className: 'dsm-feedback dsm-warning' }, String(cand.error)) : null,
2915
+ React.createElement('p', { className: 'dsm-adv-note' }, t('subagents.adv.note')))
2916
+ : null),
2917
+ modal.form.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(modal.form.error)) : null),
2918
+ React.createElement('div', { className: 'dsm-modal-actions' },
2919
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, t('btn.cancel')),
2920
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-primary', disabled: busy || (modal.mode === 'create' && !String(modal.form.name || '').trim()) || !String(modal.form.body || '').trim(), onClick: submitEditor }, t('memory.btn.save')))) : null,
2921
+ modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'sdel2', title: t('subagents.delete.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
2922
+ React.createElement('p', { className: 'dsm-help' }, t('subagents.delete.desc', { name: modal.name })),
2923
+ React.createElement('div', { className: 'dsm-modal-actions' },
2924
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, t('btn.cancel')),
2925
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDelete(modal.name) } }, t('memory.btn.delete.confirm')))) : null,
2926
+ modal && modal.type === 'import' ? React.createElement(ImportModal, { key: 'simp', t: t, title: t('subagents.import.title'), hint: t('subagents.import.hint'), busy: busy, onClose: function () { setModal(null) }, onSubmit: submitImport }) : null)
2927
+ }
2928
+
2929
+ function MemoryPage() {
2930
+ var EMPTY_STATS = { total: 0, enabled: 0, scenes: 0 }
2931
+ var EMPTY_MEMORY = { usedBytes: 0, maxBytes: 0, truncated: false }
2932
+ var state = React.useState({
2933
+ loading: true, error: null,
2934
+ rules: [], scenes: [], activeMode: 'all',
2935
+ sceneMemory: EMPTY_MEMORY, stats: EMPTY_STATS,
2936
+ })
2937
+ var data = state[0], setData = state[1]
2938
+ var qs = React.useState('')
2939
+ var query = qs[0], setQuery = qs[1]
2940
+ var sfs = React.useState('')
2941
+ var sceneFilter = sfs[0], setSceneFilter = sfs[1]
2942
+ var cs = React.useState({})
2943
+ var collapsed = cs[0], setCollapsed = cs[1]
2944
+ var ms = React.useState(null)
2945
+ var modal = ms[0], setModal = ms[1]
2946
+ var rs = React.useState(null)
2947
+ var result = rs[0], setResult = rs[1]
2948
+ var bs = React.useState(false)
2949
+ var busy = bs[0], setBusy = bs[1]
2950
+ var es = React.useState(null)
2951
+ var editor = es[0], setEditor = es[1]
2952
+ var imps = React.useState('')
2953
+ var importScene = imps[0], setImportScene = imps[1]
2954
+ // 新建场景弹窗与场景建/删已移至「场景」页(ScenesPage)。
2955
+ // 记忆回收站:列表在打开弹窗时按需拉取(rules-trash-list)。
2956
+ var trs = React.useState({ loading: false, error: null, entries: [] })
2957
+ var trash = trs[0], setTrash = trs[1]
2958
+ // bundle 附件的隐藏文件选择器;上限与服务端 MAX_ATTACH_ENTRY_BYTES 对齐。
2959
+ var attachRef = React.useRef(null)
2960
+ var ATTACH_MAX_MB = 8
2961
+ var ATTACH_MAX_BYTES = ATTACH_MAX_MB << 20
2962
+
2963
+ /** 场景名 = 分组路径的第一段(`web/frontend` 属于场景 `web`)。 */
2964
+ function sceneOf(group) {
2965
+ var s = String(group == null ? '' : group)
2966
+ var i = s.indexOf('/')
2967
+ return i >= 0 ? s.slice(0, i) : s
2968
+ }
2969
+ function toggleCollapse(key) {
2970
+ setCollapsed(function (prev) {
2971
+ var next = Object.assign({}, prev)
2972
+ next[key] = !next[key]
2973
+ return next
2974
+ })
2975
+ }
2976
+
2977
+ function refresh(silent) {
2978
+ if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
2979
+ apiCall('rules-list', {}).then(function (r) {
2980
+ if (r && r.ok) {
2981
+ // 合并式更新:只覆盖本页由 rules-list 驱动的字段,其余 state 键保持不变。
2982
+ setData(function (prev) { return Object.assign({}, prev, {
2983
+ loading: false, error: null,
2984
+ rules: r.rules || [], scenes: r.scenes || [],
2985
+ activeMode: r.activeMode === 'custom' ? 'custom' : 'all',
2986
+ sceneMemory: r.sceneMemory || EMPTY_MEMORY,
2987
+ stats: r.stats || EMPTY_STATS,
2988
+ paths: r.paths || null,
2989
+ }) })
2990
+ } else {
2991
+ setData(function (prev) { return Object.assign({}, prev, { loading: false, error: translateError(t, r), rules: [], scenes: [], activeMode: 'all', sceneMemory: EMPTY_MEMORY, stats: EMPTY_STATS }) })
2992
+ }
2993
+ }).catch(function (e) {
2994
+ setData(function (prev) { return Object.assign({}, prev, { loading: false, error: String((e && e.message) || e), rules: [], scenes: [], activeMode: 'all', sceneMemory: EMPTY_MEMORY, stats: EMPTY_STATS }) })
2995
+ })
2996
+ }
2997
+ React.useEffect(function () { refresh() }, [])
2998
+
2999
+ // 成功提示自动消失(2.6s):浮层只是「刚做完什么」的短暂回执,不该长期占屏。
3000
+ // 错误(ok === false)不自动清理,一直留到下一次操作把它替换掉。
3001
+ React.useEffect(function () {
3002
+ if (!result || result.ok !== true) return undefined
3003
+ var timer = setTimeout(function () { setResult(null) }, 2600)
3004
+ return function () { clearTimeout(timer) }
3005
+ }, [result])
3006
+
3007
+ // ── 启用场景开关已移至「场景」页(ScenesPage)──
3008
+
3009
+ // ── 记忆 CRUD ──
3010
+ function openCreate(sceneName) {
3011
+ setEditor({ mode: 'create', id: '', group: sceneName || '', name: '', description: '', body: '', form: 'flat', path: '', error: null, files: [], attachments: [] })
3012
+ setModal({ type: 'editor' })
3013
+ }
3014
+ function openEditor(rule) {
3015
+ if (busy) return
3016
+ setBusy(true); setResult(null)
3017
+ apiCall('rules-read', { id: rule.id }).then(function (res) {
3018
+ setBusy(false)
3019
+ if (res && res.ok && res.rule) {
3020
+ var rd = res.rule
3021
+ 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 || [] })
3022
+ setModal({ type: 'editor' })
3023
+ } else setResult({ ok: false, text: translateError(t, res) })
3024
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
3025
+ }
3026
+ function closeEditor() { setModal(null) }
3027
+ // ── 导入记忆(.md / .zip;场景留空 = 保留场景「全局」)──
3028
+ function submitImport(files) {
3029
+ if (!files || !files.length) return
3030
+ setBusy(true)
3031
+ apiCall('rules-import', { scene: importScene, files: files }).then(function (res) {
3032
+ setBusy(false)
3033
+ if (res && res.ok) {
3034
+ setModal(null)
3035
+ var names = res.imported || [], skipped = res.skipped || []
3036
+ var text = names.length ? t('memory.result.imported', { count: names.length, names: names.join('、') }) : t('import.none')
3037
+ if (skipped.length) text += ' · ' + t('import.skipped', { items: skipped.map(function (s) { return s.name + '(' + s.reason + ')' }).join(';') })
3038
+ // 导入会为引用的目录名补出场景记录(宿主回传 scenes)——必须告诉用户,
3039
+ // 否则「我的场景列表怎么多了一个」就是静默造数据。
3040
+ if (res.scenes && res.scenes.length) text += ' · ' + t('memory.import.scenesCreated', { names: res.scenes.join('、') })
3041
+ setResult({ ok: true, text: text }); refresh(true)
3042
+ } else setResult({ ok: false, text: translateError(t, res) })
3043
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
3044
+ }
3045
+
3046
+ // ── bundle 附件(只有 bundle 有目录可放;flat 是单文件)──
3047
+ function fmtBytes(n) {
3048
+ var v = Number(n) || 0
3049
+ if (v < 1024) return v + ' B'
3050
+ if (v < 1024 * 1024) return Math.ceil(v / 1024) + ' KB'
3051
+ return (Math.round((v / (1024 * 1024)) * 10) / 10) + ' MB'
3052
+ }
3053
+ function pickAttach() { if (attachRef.current) attachRef.current.click() }
3054
+ function onAttachPicked(ev) {
3055
+ var picked = Array.prototype.slice.call((ev.target && ev.target.files) || [])
3056
+ if (ev.target) ev.target.value = '' // 允许连续选中同一个文件
3057
+ if (!picked.length) return
3058
+ Promise.all(picked.map(function (f) {
3059
+ return f.arrayBuffer().then(function (buf) {
3060
+ return { name: String(f.name || ''), size: buf.byteLength, data: bytesToBase64(buf) }
3061
+ })
3062
+ })).then(function (entries) {
3063
+ var tooBig = entries.filter(function (e) { return e.size > ATTACH_MAX_BYTES })
3064
+ var keep = entries.filter(function (e) { return e.size <= ATTACH_MAX_BYTES })
3065
+ setEditor(function (prev) {
3066
+ var next = Object.assign({}, prev, { files: (prev.files || []).concat(keep) })
3067
+ // 超限的当场拦下(服务端还会再拦一次),其余照常加入
3068
+ if (tooBig.length) next.error = t('memory.attach.tooLarge', { name: tooBig[0].name, limit: ATTACH_MAX_MB })
3069
+ return next
3070
+ })
3071
+ }).catch(function (e) { setEditor(function (prev) { return Object.assign({}, prev, { error: String((e && e.message) || e) }) }) })
3072
+ }
3073
+ function dropPending(index) {
3074
+ setEditor(function (prev) {
3075
+ var next = (prev.files || []).slice()
3076
+ next.splice(index, 1)
3077
+ return Object.assign({}, prev, { files: next })
3078
+ })
3079
+ }
3080
+ function removeAttachment(name) {
3081
+ if (busy || !editor) return
3082
+ setBusy(true)
3083
+ apiCall('rules-detach', { id: editor.id, name: name }).then(function (res) {
3084
+ setBusy(false)
3085
+ if (res && res.ok) setEditor(function (prev) { return Object.assign({}, prev, { attachments: res.attachments || [] }) })
3086
+ else setEditor(function (prev) { return Object.assign({}, prev, { error: translateError(t, res) }) })
3087
+ }).catch(function (e) { setBusy(false); setEditor(function (prev) { return Object.assign({}, prev, { error: String((e && e.message) || e) }) }) })
3088
+ }
3089
+ /** 附件与记忆本体分两步:先存记忆,再传附件;附件失败只如实报错,不回滚已保存的记忆。 */
3090
+ function flushAttachments(id, files) {
3091
+ if (!files || !files.length) return Promise.resolve({ ok: true, empty: true })
3092
+ return apiCall('rules-attach', { id: id, files: files.map(function (f) { return { path: f.name, data: f.data } }) })
3093
+ }
3094
+
3095
+ // (场景档案与当前模式的功能已移至 ScenesPage——「场景」页)
3096
+
3097
+ // (场景建/删也已移至 ScenesPage;场景启停开关同)
3098
+ // ── 记忆回收站(rules-remove 移入 / rules-restore 恢复 / rules-trash-remove 永久删除)──
3099
+ // 此前删除后只能看到一句"回收站 ID",界面上无法恢复;这里把入口补齐。
3100
+ function loadTrash() {
3101
+ setTrash({ loading: true, error: null, entries: [] })
3102
+ apiCall('rules-trash-list', {}).then(function (res) {
3103
+ if (res && res.ok) setTrash({ loading: false, error: null, entries: res.entries || [] })
3104
+ else setTrash({ loading: false, error: translateError(t, res), entries: [] })
3105
+ }).catch(function (e) { setTrash({ loading: false, error: String((e && e.message) || e), entries: [] }) })
3106
+ }
3107
+ function openTrash() { setModal({ type: 'trash' }); loadTrash() }
3108
+ function submitRestore(entry) {
3109
+ if (busy) return
3110
+ setBusy(true)
3111
+ apiCall('rules-restore', { trashId: entry.trashId }).then(function (res) {
3112
+ setBusy(false)
3113
+ if (res && res.ok) {
3114
+ setModal(null)
3115
+ setResult({ ok: true, text: t('memory.result.restored', { name: entry.name }) })
3116
+ refresh(true)
3117
+ } else setTrash(Object.assign({}, trash, { error: translateError(t, res) }))
3118
+ }).catch(function (e) { setBusy(false); setTrash(Object.assign({}, trash, { error: String((e && e.message) || e) })) })
3119
+ }
3120
+ function submitTrashDelete(entry) {
3121
+ if (busy) return
3122
+ setBusy(true)
3123
+ apiCall('rules-trash-remove', { trashId: entry.trashId }).then(function (res) {
3124
+ setBusy(false)
3125
+ if (res && res.ok) {
3126
+ setResult({ ok: true, text: t('memory.result.trashRemoved', { name: entry.name }) })
3127
+ setModal({ type: 'trash' })
3128
+ loadTrash()
3129
+ } else setTrash(Object.assign({}, trash, { error: translateError(t, res) }))
3130
+ }).catch(function (e) { setBusy(false); setTrash(Object.assign({}, trash, { error: String((e && e.message) || e) })) })
3131
+ }
3132
+ // ── 单条记忆启停(索引层开关,不改文件;停用的记忆仍留在磁盘上)──
3133
+ function toggleRule(rule) {
3134
+ if (busy) return
3135
+ setBusy(true); setResult(null)
3136
+ apiCall('rules-toggle', { id: rule.id, enabled: rule.enabled === false }).then(function (res) {
3137
+ setBusy(false)
3138
+ if (res && res.ok) { setResult({ ok: true, text: t('memory.result.toggled', { name: rule.name }) }); refresh(true) }
3139
+ else setResult({ ok: false, text: translateError(t, res) })
3140
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
3141
+ }
3142
+
3143
+ /** 记忆存好后统一收尾:上传附件 → 关弹窗 → 报结果(附件失败不回滚记忆)。 */
3144
+ function finishEditor(id, name, pending, successKey, attachedKey) {
3145
+ return flushAttachments(id, pending).then(function (att) {
3146
+ setBusy(false)
3147
+ setModal(null); setEditor(null)
3148
+ if (att && att.ok === false) setResult({ ok: false, text: t('memory.result.attachFailed', { error: translateError(t, att) }) })
3149
+ else if (pending.length) setResult({ ok: true, text: t(attachedKey, { name: name, count: pending.length }) })
3150
+ else setResult({ ok: true, text: t(successKey, { name: name }) })
3151
+ refresh(true)
3152
+ })
3153
+ }
3154
+ function submitCreate() {
3155
+ var payload = {
3156
+ group: String(editor.group || '').trim(),
3157
+ name: String(editor.name || '').trim(),
3158
+ description: String(editor.description || '').trim(),
3159
+ body: editor.body || '',
3160
+ form: editor.form || 'flat',
3161
+ }
3162
+ var pending = editor.files || []
3163
+ setBusy(true)
3164
+ apiCall('rules-create', payload).then(function (res) {
3165
+ if (!res || !res.ok) {
3166
+ setBusy(false)
3167
+ setEditor(Object.assign({}, editor, { error: translateError(t, res) }))
3168
+ return null
3169
+ }
3170
+ var id = (res.rule && res.rule.id) || (payload.group ? payload.group + '/' + payload.name : payload.name)
3171
+ var name = (res.rule && (res.rule.id || res.rule.name)) || payload.name
3172
+ return finishEditor(id, name, pending, 'memory.result.created', 'memory.result.createdAttached')
3173
+ }).catch(function (e) { setBusy(false); setEditor(Object.assign({}, editor, { error: String((e && e.message) || e) })) })
3174
+ }
3175
+ function submitUpdate() {
3176
+ var payload = {
3177
+ id: editor.id,
3178
+ name: String(editor.name || '').trim(),
3179
+ description: String(editor.description || '').trim(),
3180
+ body: editor.body || '',
3181
+ form: editor.form || 'flat',
3182
+ }
3183
+ var pending = editor.files || []
3184
+ setBusy(true)
3185
+ apiCall('rules-update', payload).then(function (res) {
3186
+ if (!res || !res.ok) {
3187
+ setBusy(false)
3188
+ setEditor(Object.assign({}, editor, { error: translateError(t, res) }))
3189
+ return null
3190
+ }
3191
+ var name = (res.rule && (res.rule.id || res.rule.name)) || payload.name
3192
+ return finishEditor(editor.id, name, pending, 'memory.result.updated', 'memory.result.updatedAttached')
3193
+ }).catch(function (e) { setBusy(false); setEditor(Object.assign({}, editor, { error: String((e && e.message) || e) })) })
3194
+ }
3195
+ function submitDelete(rule) {
3196
+ setBusy(true)
3197
+ apiCall('rules-remove', { id: rule.id }).then(function (res) {
3198
+ setBusy(false)
3199
+ if (res && res.ok) {
3200
+ setModal(null)
3201
+ setResult({ ok: true, text: t('memory.result.removed', { name: rule.name, trashId: res.trashId != null ? res.trashId : '' }) })
3202
+ refresh(true)
3203
+ } else setResult({ ok: false, text: translateError(t, res) })
3204
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
3205
+ }
3206
+
3207
+ // ── 渲染 ──
3208
+ var stats = data.stats || EMPTY_STATS
3209
+ // 注入预算:宿主 rules-list 一直在返回 sceneMemory,但此前从未渲染 ——
3210
+ // 于是"超出预算、部分记忆没进提示词"对用户完全不可见。
3211
+ var memInfo = data.sceneMemory || EMPTY_MEMORY
3212
+ var memMax = Number(memInfo.maxBytes) || 0
3213
+ var memUsed = Number(memInfo.usedBytes) || 0
3214
+ var budgetOver = memMax > 0 && memUsed > memMax
3215
+ var budgetPct = memMax > 0 ? Math.min(100, Math.round((memUsed / memMax) * 100)) : 0
3216
+ var droppedNames = (memInfo.dropped || []).map(function (d) { return (d.scene ? d.scene + '/' : '') + d.name })
3217
+ var q = String(query || '').toLowerCase()
3218
+ function matchRule(r) {
3219
+ if (sceneFilter !== '' && sceneOf(r.group) !== sceneFilter) return false
3220
+ if (!q) return true
3221
+ return (r.name || '').toLowerCase().indexOf(q) >= 0
3222
+ || (r.description || '').toLowerCase().indexOf(q) >= 0
3223
+ || (r.group || '').toLowerCase().indexOf(q) >= 0
3224
+ }
3225
+
3226
+ // 场景 → 记忆:场景元数据来自宿主 scenes(含**空场景**与保留场景 global),
3227
+ // 记忆按一级目录归位。没有归属场景的记忆(直接放在 memories/ 根层)也单独列出来,
3228
+ // 并在体检里报 noScene —— 不能让它既不在列表里、也不在提示词里。
3229
+ var buckets = {}
3230
+ var order = []
3231
+ function ensureBucket(name, meta) {
3232
+ if (!buckets[name]) { buckets[name] = { name: name, meta: meta, rules: [] }; order.push(name) }
3233
+ else if (meta && !buckets[name].meta) buckets[name].meta = meta
3234
+ return buckets[name]
3235
+ }
3236
+ ;(data.scenes || []).forEach(function (s) { ensureBucket(s.name, s) })
3237
+ data.rules.forEach(function (r) {
3238
+ var g = String(r.group == null ? '' : r.group)
3239
+ var b = ensureBucket(g === '' ? '' : sceneOf(g), null)
3240
+ if (matchRule(r)) b.rules.push(r)
3241
+ })
3242
+ order.sort(function (a, b) {
3243
+ if (a === b) return 0
3244
+ var am = buckets[a].meta, bm = buckets[b].meta
3245
+ // 宿主给的 order(保留场景 global = 0,恒在最前);无元数据的游离桶排最后。
3246
+ var ao = am && typeof am.order === 'number' ? am.order : 1e9
3247
+ var bo = bm && typeof bm.order === 'number' ? bm.order : 1e9
3248
+ if (ao !== bo) return ao - bo
3249
+ if (a === '') return 1
3250
+ if (b === '') return -1
3251
+ return String(a).localeCompare(String(b))
3252
+ })
3253
+ var sceneOptions = [{ value: '', label: t('memory.filter.all') }].concat(
3254
+ order.map(function (n) { return { value: n, label: sceneLabel(data.scenes, n) } }))
3255
+ // 场景选择:保留场景「全局」排最前(它是真实场景,不是「留空」);
3256
+ // 游离桶只在真的存在游离记忆时才出现。
3257
+ var sceneChoices = order.map(function (n) { return { value: n, label: sceneLabel(data.scenes, n) } })
3258
+ // 名称 = .md 文件名(单个路径段,不是路径):用 segment 校验器,与宿主逐字对齐。
3259
+ // 中文等任意 Unicode 都合法(`站会流程` → `站会流程.md`)。
3260
+ // 未输入时不标红/不报「不合法」——一打开弹窗就飘红会让人以为中文名被拒;
3261
+ // 空值改由提交按钮的 disabled 兜住(见下方 disabled 表达式)。
3262
+ var editorNameInvalid = editor ? String(editor.name || '').trim() !== '' && !isValidSceneSegment(String(editor.name || '').trim()) : false
3263
+ var editorGroupInvalid = editor && editor.mode === 'create' ? String(editor.group || '').trim() !== '' && !isValidScenePath(String(editor.group || '').trim()) : false
3264
+ var editorDescLen = editor ? String(editor.description || '').length : 0
3265
+ // 空场景(含刚「新建场景」建好的空目录)必须列出来:否则它在页面上直接消失,
3266
+ // 卡片上的「新建记忆」入口也就点不到(只能靠顶部按钮再手填场景名)。
3267
+ // 只在搜索 / 场景筛选生效时按命中收敛,避免空卡片噪音。
3268
+ var filtering = q !== '' || sceneFilter !== ''
3269
+ var visibleBuckets = order.filter(function (n) {
3270
+ if (sceneFilter !== '' && n !== sceneFilter) return false
3271
+ if (!filtering) return true
3272
+ return buckets[n].rules.length > 0 || sceneFilter === n
3273
+ })
3274
+ var hasAnyRule = data.rules.length > 0
3275
+
3276
+ function renderRuleRow(r, sceneActive) {
3277
+ var shadowed = r.shadowed === true
3278
+ // shadowed = 存在同名 bundle,本条 flat 不会被加载;与文件名是否 kebab-case 无关——
3279
+ // 记忆名就是 .md 文件名,中文名同样可以在本页编辑 / 删除。
3280
+ var enabled = r.enabled !== false
3281
+ // 单条停用(索引层开关)优先于场景状态:原来只有 已启用/被覆盖/场景未启用 三个分支,
3282
+ // 单条停用的记忆会被误显示为「已启用」。
3283
+ var statusKey = shadowed ? 'status.shadowed' : !enabled ? 'memory.status.off' : sceneActive === false ? 'memory.status.sceneOff' : 'status.enabled'
3284
+ var statusCls = shadowed ? 'dsm-shadowed' : (!enabled || sceneActive === false) ? 'dsm-disabled' : 'dsm-enabled'
3285
+ return React.createElement('div', { key: r.id, className: 'dsm-row' },
3286
+ React.createElement('div', { className: 'dsm-main' },
3287
+ React.createElement('div', { className: 'dsm-name' }, r.name),
3288
+ React.createElement('div', { className: 'dsm-note' }, r.description || ''),
3289
+ shadowed ? React.createElement('div', { className: 'dsm-rule-shadow-hint' }, t('memory.shadowed.hint')) : null),
3290
+ React.createElement('div', { className: 'dsm-tags' },
3291
+ r.group && r.group.indexOf('/') >= 0 ? React.createElement('span', { className: 'dsm-tag' }, r.group) : null,
3292
+ r.form === 'bundle' ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.form.bundle')) : null,
3293
+ r.descriptionDerived ? React.createElement('span', { className: 'dsm-tag' }, t('memory.derived')) : null),
3294
+ React.createElement('div', { className: 'dsm-status ' + statusCls }, t(statusKey)),
3295
+ React.createElement('div', { className: 'dsm-row-actions' },
3296
+ React.createElement(Switch, { on: enabled, disabled: busy || shadowed, label: t('memory.enable') + ' ' + r.name, onClick: function () { toggleRule(r) } }),
3297
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || shadowed, onClick: function () { openEditor(r) } }, t('memory.edit')),
3298
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy || shadowed, onClick: function () { setModal({ type: 'delete', rule: r }) } }, t('memory.delete'))))
3299
+ }
3300
+
3301
+ function renderSceneCard(name) {
3302
+ var bucket = buckets[name]
3303
+ var meta = bucket.meta || { name: name, count: 0, active: true, shared: false, label: null }
3304
+ var isShared = meta.shared === true
3305
+ var isGlobal = meta.global === true || name === 'global'
3306
+ var isOrphan = name === '' && !bucket.meta
3307
+ var open = !collapsed['s:' + name]
3308
+ // 游离记忆(没有归属场景)用错误色提示:它们不会进系统提示词。
3309
+ return React.createElement('div', { key: 's:' + name, className: 'dsm-source' + (meta.active === false || isOrphan ? ' dsm-rule-shadowed' : '') },
3310
+ React.createElement('div', { className: 'dsm-source-head' },
3311
+ React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: function () { toggleCollapse('s:' + name) } },
3312
+ React.createElement('span', { className: 'dsm-source-title' }, sceneLabel(data.scenes, name)),
3313
+ React.createElement('span', { className: 'dsm-count' }, t('memory.scene.count', { count: bucket.rules.length })),
3314
+ isShared ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.shared')) : null,
3315
+ isGlobal ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.global.tag')) : null,
3316
+ isOrphan ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('memory.scene.orphan.tag')) : null,
3317
+ meta.active === false ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('memory.scene.off')) : null,
3318
+ isOrphan ? null : React.createElement('span', { className: 'dsm-note' }, meta.description || '')),
3319
+ React.createElement('div', { className: 'dsm-source-actions' },
3320
+ isOrphan ? null : React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { openCreate(name) } }, t('memory.scene.new')))),
3321
+ open ? React.createElement('div', { className: 'dsm-source-body' },
3322
+ isOrphan ? React.createElement('div', { className: 'dsm-source-note' }, t('memory.scene.orphan.hint')) : null,
3323
+ bucket.rules.length
3324
+ ? React.createElement(React.Fragment, null,
3325
+ React.createElement('div', { className: 'dsm-table-head' },
3326
+ React.createElement('span', null, t('memory.table.name')),
3327
+ React.createElement('span', null, t('memory.table.tags')),
3328
+ React.createElement('span', null, t('memory.table.status')),
3329
+ React.createElement('span', null, '')),
3330
+ bucket.rules.map(function (r) { return renderRuleRow(r, meta.active !== false) }))
3331
+ : React.createElement('div', { className: 'dsm-empty' }, t('memory.scene.empty'))) : null)
3332
+ }
3333
+
3334
+ return React.createElement('section', { className: 'dsm-section' },
3335
+ React.createElement('div', { className: 'dsm-head' },
3336
+ React.createElement('div', { className: 'dsm-title-block' },
3337
+ React.createElement('div', { className: 'dsm-title-row' },
3338
+ React.createElement('h2', { className: 'dsm-title' }, t('memory.title')),
3339
+ React.createElement(VersionBadge, null)),
3340
+ React.createElement('p', { className: 'dsm-desc' }, t('memory.desc'))),
3341
+ React.createElement('div', { className: 'dsm-actions' },
3342
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }, t('memory.btn.refresh')),
3343
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openTrash }, t('memory.trash.open')),
3344
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setResult(null); setImportScene(''); setModal({ type: 'import' }) } }, t('memory.import')),
3345
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { openCreate(sceneFilter || '') } }, t('memory.btn.new')))),
3346
+ React.createElement('div', { className: 'dsm-summary dsm-summary-3' },
3347
+ React.createElement('div', { key: 'total', className: 'dsm-stat' }, React.createElement('strong', null, stats.total || 0), t('memory.stat.total')),
3348
+ React.createElement('div', { key: 'scenes', className: 'dsm-stat' }, React.createElement('strong', null, (data.scenes || []).length), t('memory.stat.sceneCount')),
3349
+ React.createElement('div', { key: 'active', className: 'dsm-stat' }, React.createElement('strong', null, stats.scenes || 0), t('memory.stat.scenes'))),
3350
+ memMax > 0 ? React.createElement('div', { className: 'dsm-rule-budget' },
3351
+ React.createElement('div', { className: 'dsm-budget-meta' },
3352
+ React.createElement('span', { className: 'dsm-label' }, t('memory.budget.label')),
3353
+ React.createElement('span', { className: budgetOver ? 'dsm-budget-over-text' : 'dsm-char-count' },
3354
+ t('memory.budget.bytes', { used: memUsed, max: memMax }) + (budgetOver ? ' · ' + t('memory.budget.over') : ''))),
3355
+ React.createElement('div', { className: 'dsm-budget-bar', role: 'progressbar', 'aria-valuenow': budgetPct, 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-label': t('memory.budget.label') },
3356
+ React.createElement('div', { className: 'dsm-budget-fill' + (budgetOver ? ' dsm-budget-over' : ''), style: { width: budgetPct + '%' } })),
3357
+ memInfo.truncated ? React.createElement('p', { className: 'dsm-rule-hint' }, t('memory.budget.truncated')) : null,
3358
+ droppedNames.length ? React.createElement('p', { className: 'dsm-rule-hint' }, t('memory.budget.dropped', { names: droppedNames.join('、') })) : null) : null,
3359
+ React.createElement('div', { className: 'dsm-filters' },
3360
+ React.createElement('input', { className: 'dsm-control dsm-search', type: 'text', placeholder: t('memory.search.placeholder'), value: query, onChange: function (e) { setQuery(e.target.value) } }),
3361
+ React.createElement('div', { className: 'dsm-source-filter' },
3362
+ React.createElement(SourceSelect, { options: sceneOptions, value: sceneFilter, onChange: setSceneFilter }))),
3363
+ // 成功 / 失败统一走 Notice(成功浮层 2.6 秒自动消失,见上方 result 清理 effect;
3364
+ // 错误留在文档流里不自动消失——错误需要一直看得见)。
3365
+ React.createElement(Notice, { key: 'notice', kind: result && result.ok ? 'ok' : 'err', text: result && result.text }),
3366
+ data.error ? React.createElement('div', { key: 'error', className: 'dsm-feedback dsm-error' }, String(data.error)) : null,
3367
+ data.loading && !hasAnyRule ? React.createElement('div', { key: 'loading', className: 'dsm-empty' }, t('memory.loading'))
3368
+ : visibleBuckets.length ? React.createElement('div', { key: 'scenes', className: 'dsm-sources' }, visibleBuckets.map(renderSceneCard))
3369
+ : React.createElement('div', { key: 'empty', className: 'dsm-empty' }, q || sceneFilter ? t('memory.empty.search') : t('memory.empty')),
3370
+ modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'del', title: t('memory.delete.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
3371
+ React.createElement('p', { className: 'dsm-help' }, t('memory.delete.desc', { name: modal.rule.name })),
3372
+ React.createElement('div', { className: 'dsm-modal-actions' },
3373
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal(null) } }, t('btn.cancel')),
3374
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDelete(modal.rule) } }, t('memory.btn.delete.confirm')))) : null,
3375
+ modal && modal.type === 'import' ? React.createElement(ImportModal, {
3376
+ key: 'mimp', t: t, title: t('memory.import.title'), hint: t('memory.import.hint'), busy: busy,
3377
+ extra: React.createElement('label', { className: 'dsm-field' },
3378
+ React.createElement('span', { className: 'dsm-label' }, t('memory.import.scene')),
3379
+ React.createElement('input', { className: 'dsm-control', value: importScene, placeholder: t('memory.scene.global.hint'), onChange: function (e) { setImportScene(e.target.value) } }),
3380
+ React.createElement('p', { className: 'dsm-help' }, t('memory.import.sceneHint'))),
3381
+ onClose: function () { setModal(null) },
3382
+ onSubmit: submitImport,
3383
+ }) : null,
3384
+ editor && modal && modal.type === 'editor' ? React.createElement(Modal, { key: 'editor', className: 'dsm-modal-wide', title: editor.mode === 'create' ? t('memory.create.title') : t('memory.edit.title'), closeLabel: t('btn.cancel'), onClose: closeEditor },
3385
+ React.createElement('div', { className: 'dsm-form' },
3386
+ React.createElement('label', { className: 'dsm-field' },
3387
+ React.createElement('span', { className: 'dsm-label' }, t('memory.field.group')),
3388
+ editor.mode === 'create'
3389
+ ? React.createElement(SceneCombo, {
3390
+ options: sceneChoices,
3391
+ value: editor.group || '',
3392
+ placeholder: t('memory.field.group.placeholder'),
3393
+ browseLabel: t('memory.scene.browse'),
3394
+ label: t('memory.field.group'),
3395
+ disabled: busy,
3396
+ invalid: editorGroupInvalid,
3397
+ onChange: function (v) { setEditor(Object.assign({}, editor, { group: v })) },
3398
+ })
3399
+ : React.createElement('input', { className: 'dsm-control', value: editor.group || '', disabled: true, onChange: function () {} }),
3400
+ editor.mode === 'create'
3401
+ ? React.createElement('p', { className: 'dsm-help' }, t('memory.field.group.hint'))
3402
+ : null,
3403
+ editorGroupInvalid ? React.createElement('p', { className: 'dsm-rule-hint' }, t('error.rules.invalidGroup')) : null),
3404
+ React.createElement('label', { className: 'dsm-field' },
3405
+ React.createElement('span', { className: 'dsm-label' }, t('memory.field.name')),
3406
+ React.createElement('input', { className: 'dsm-control' + (editorNameInvalid ? ' dsm-rule-invalid' : ''), value: editor.name || '', placeholder: t('memory.field.name.placeholder'), onChange: function (e) { setEditor(Object.assign({}, editor, { name: e.target.value })) } }),
3407
+ editorNameInvalid ? React.createElement('p', { className: 'dsm-rule-hint' }, t('memory.name.invalid')) : null),
3408
+ React.createElement('label', { className: 'dsm-field' },
3409
+ React.createElement('span', { className: 'dsm-label' }, t('memory.field.description')),
3410
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: editor.description || '', placeholder: t('memory.field.description.placeholder'), onChange: function (e) { setEditor(Object.assign({}, editor, { description: e.target.value })) } }),
3411
+ React.createElement('div', { className: 'dsm-char-count' + (editorDescLen > 500 ? ' dsm-char-over' : '') }, editorDescLen + ' / 500')),
3412
+ React.createElement('label', { className: 'dsm-field' },
3413
+ React.createElement('span', { className: 'dsm-label' }, t('memory.field.body')),
3414
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-md', value: editor.body || '', placeholder: t('memory.field.body.placeholder'), onChange: function (e) { setEditor(Object.assign({}, editor, { body: e.target.value })) } })),
3415
+ React.createElement('div', { className: 'dsm-field' },
3416
+ React.createElement('span', { className: 'dsm-label' }, t('memory.field.form')),
3417
+ React.createElement(SourceSelect, { options: RULE_FORM_OPTIONS, value: editor.form || 'flat', onChange: function (v) { setEditor(Object.assign({}, editor, { form: v })) } }),
3418
+ editor.form === 'bundle'
3419
+ ? React.createElement('div', { className: 'dsm-field' },
3420
+ React.createElement('span', { className: 'dsm-label' }, t('memory.field.attach')),
3421
+ React.createElement('input', { ref: attachRef, type: 'file', multiple: true, className: 'dsm-hidden-input', onChange: onAttachPicked }),
3422
+ (editor.attachments || []).length || (editor.files || []).length
3423
+ ? React.createElement('div', { className: 'dsm-attach-list' },
3424
+ (editor.attachments || []).map(function (a) {
3425
+ return React.createElement('div', { key: 'a:' + a.name, className: 'dsm-file', title: a.name },
3426
+ React.createElement('span', { className: 'dsm-file-kind', 'aria-hidden': 'true' }, 'FILE'),
3427
+ React.createElement('span', { className: 'dsm-file-name' }, a.name),
3428
+ React.createElement('span', { className: 'dsm-file-meta' }, fmtBytes(a.size)),
3429
+ React.createElement('button', { type: 'button', className: 'dsm-file-remove', disabled: busy, 'aria-label': t('memory.attach.remove') + ' ' + a.name, onClick: function () { removeAttachment(a.name) } }, '×'))
3430
+ }),
3431
+ (editor.files || []).map(function (f, i) {
3432
+ return React.createElement('div', { key: 'p:' + i + ':' + f.name, className: 'dsm-file', title: f.name },
3433
+ React.createElement('span', { className: 'dsm-file-kind', 'aria-hidden': 'true' }, 'NEW'),
3434
+ React.createElement('span', { className: 'dsm-file-name' }, f.name),
3435
+ React.createElement('span', { className: 'dsm-file-meta' }, fmtBytes(f.size) + ' · ' + t('memory.attach.pending')),
3436
+ React.createElement('button', { type: 'button', className: 'dsm-file-remove', disabled: busy, 'aria-label': t('memory.attach.remove') + ' ' + f.name, onClick: function () { dropPending(i) } }, '×'))
3437
+ }))
3438
+ : React.createElement('p', { className: 'dsm-help' }, t('memory.attach.empty')),
3439
+ React.createElement('div', { className: 'dsm-attach-actions' },
3440
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: pickAttach }, t('memory.attach.add')),
3441
+ React.createElement('span', { className: 'dsm-help' }, t('memory.attach.hint'))))
3442
+ : React.createElement('p', { className: 'dsm-help' }, t('memory.attach.flat'))),
3443
+ editor.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(editor.error)) : null),
3444
+ React.createElement('div', { className: 'dsm-modal-actions' },
3445
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: closeEditor }, t('btn.cancel')),
3446
+ React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || editorNameInvalid || editorGroupInvalid || editorDescLen > 500 || !String(editor.name || '').trim() || !String(editor.body || '').trim(), onClick: editor.mode === 'create' ? submitCreate : submitUpdate }, editor.mode === 'create' ? t('memory.btn.create') : t('memory.btn.save')))) : null,
3447
+ // 场景建/删/档案弹窗已移至「场景」页(ScenesPage)。
3448
+ modal && modal.type === 'trash' ? React.createElement(Modal, { key: 'trash', wide: true, title: t('memory.trash.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
3449
+ trash.loading ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
3450
+ : React.createElement(React.Fragment, null,
3451
+ trash.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(trash.error)) : null,
3452
+ trash.entries.length
3453
+ ? React.createElement(React.Fragment, null,
3454
+ React.createElement('div', { className: 'dsm-count' }, t('memory.trash.count', { count: trash.entries.length })),
3455
+ trash.entries.map(function (entry) {
3456
+ return React.createElement('div', { key: entry.trashId, className: 'dsm-trash-item' },
3457
+ React.createElement('div', { className: 'dsm-trash-main' },
3458
+ React.createElement('div', { className: 'dsm-name' }, entry.name),
3459
+ React.createElement('div', { className: 'dsm-note' },
3460
+ sceneLabel(data.scenes, entry.group) + ' · ' + t('memory.form.' + (entry.form === 'bundle' ? 'bundle' : 'flat')) + ' · ' + entry.bytes + ' B · '
3461
+ + t('memory.trash.deletedAt', { time: entry.deletedAt ? new Date(entry.deletedAt).toLocaleString() : '' }))),
3462
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { submitRestore(entry) } }, t('memory.trash.restore')),
3463
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'trash-delete', entry: entry }) } }, t('memory.trash.purge')))
3464
+ }))
3465
+ : React.createElement('div', { className: 'dsm-empty' }, t('memory.trash.empty')))) : null,
3466
+ modal && modal.type === 'trash-delete' ? React.createElement(Modal, { key: 'trash-delete', title: t('memory.trash.confirmTitle'), closeLabel: t('btn.cancel'), onClose: function () { setModal({ type: 'trash' }) } },
3467
+ React.createElement('p', { className: 'dsm-help' }, t('memory.trash.confirmDesc', { name: modal.entry.name })),
3468
+ React.createElement('div', { className: 'dsm-modal-actions' },
3469
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setModal({ type: 'trash' }) } }, t('btn.cancel')),
3470
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitTrashDelete(modal.entry) } }, t('memory.trash.purge')))) : null)
3471
+ }
3472
+
3473
+ // 页面组件填进 factory 作用域那个占位对象(导出对象求值时已指向它)。
3474
+ // 不要改回 `module.exports.X = ...`:apply 可能在本函数开头提前返回。
3475
+ _pages.MCPPage = MCPPage
3476
+ _pages.SkillManagerSection = SkillManagerSection
3477
+ _pages.AgentsMdPage = AgentsMdPage
3478
+ _pages.HistoryPage = HistoryPage
3479
+ _pages.ScenesPage = ScenesPage
3480
+ _pages.SubagentsPage = SubagentsPage
3481
+ _pages.MemoryPage = MemoryPage
3482
+ _pages.t = t
1453
3483
  },
1454
3484
  }
1455
3485
  return module.exports
1456
3486
  }
1457
3487
  })
3488
+
3489
+