dsh-subagent-profile 0.2.0 → 0.3.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
@@ -16,720 +16,801 @@
16
16
  * permanently-visible input row. Dropdown options (preset / provider / model /
17
17
  * reasoning effort) are served by the host `/options` route from the live
18
18
  * presets roster and llm directory.
19
+ *
20
+ * 结构:纯数据(CSS/ZH 文案表)与纯函数(api/解析/文案映射等)驻留模块作用域
21
+ * (load 调用之前的顶层声明);依赖 React 的组件经参数注入的 maker 函数装配
22
+ * (makeChip / makeDispatchToolview / makeProfilesSection),factory 只做组装。
19
23
  */
20
- window.__ModuleLoader__.load({
21
- id: 'dsh-subagent-profile',
22
- factory: (require) => {
23
- const React = require('react')
24
- const { useEffect, useState, useCallback } = React
25
- const el = React.createElement
26
24
 
27
- /** Required cordis services: the slot registry only. */
28
- const inject = ['slots']
29
-
30
- // ── api (loopback HTTP, host side serves /subagent-profiles/*) ──────────
31
-
32
- async function api(path, payload) {
33
- const init = payload === undefined
34
- ? undefined
35
- : {
36
- method: 'POST',
37
- headers: { 'Content-Type': 'application/json' },
38
- body: JSON.stringify(payload),
39
- }
40
- const res = await fetch(`/subagent-profiles${path}`, init)
41
- let data
42
- try {
43
- data = await res.json()
44
- } catch {
45
- throw new Error(`宿主返回了非 JSON 响应(HTTP ${res.status})`)
25
+ // ── 纯数据:样式(tokens 对齐 shipped「模型」设置节)────────────────────────
26
+
27
+ const CSS = [
28
+ '.sap-section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}',
29
+ '.sap-title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}',
30
+ '.sap-intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}',
31
+ '.sap-notice{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}',
32
+ '.sap-savedNotice{color:var(--dsw-alias-state-success-primary);margin:0;font-size:12px;line-height:18px}',
33
+ '.sap-rows{flex-direction:column;gap:8px;margin:12px 0 0;padding:0;list-style:none;display:flex}',
34
+ '.sap-rowCard{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:10px;padding:12px 14px;display:flex}',
35
+ '.sap-rowHead{align-items:center;gap:10px;display:flex}',
36
+ '.sap-rowIdentity{align-items:center;gap:6px;min-width:0;display:inline-flex}',
37
+ '.sap-rowName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}',
38
+ '.sap-rowId{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-family:monospace}',
39
+ '.sap-rowTag{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}',
40
+ '.sap-rowActions{align-items:center;gap:4px;margin-left:auto;display:inline-flex}',
41
+ '.sap-desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
42
+ '.sap-warn{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}',
43
+ '.sap-chips{flex-wrap:wrap;gap:6px;display:flex}',
44
+ '.sap-chip{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:6px;flex:none;align-items:center;gap:5px;padding:2px 8px;font-size:12px;line-height:18px;display:inline-flex}',
45
+ '.sap-chipLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
46
+ '.sap-chipValue{color:var(--dsw-alias-label-primary)}',
47
+ '.sap-chipValueDim{color:var(--dsw-alias-label-tertiary)}',
48
+ '.sap-chipWarn{background:var(--dsw-alias-state-warn-tertiary);border-color:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);font-weight:500}',
49
+ '.sap-chipSuccess{color:var(--dsw-alias-state-success-primary);font-weight:500}',
50
+ '.sap-chipDisabled{color:var(--dsw-alias-state-warn-label);font-weight:500}',
51
+ '.sap-metaValueDefault{color:var(--dsw-alias-label-tertiary)}',
52
+ '.sap-addBlock{margin-top:4px}',
53
+ '.sap-addActions{flex-wrap:wrap;gap:10px;display:flex}',
54
+ '.sap-addButton{box-sizing:border-box;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;flex:1 1 0;gap:6px;min-width:180px;height:44px;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;background:0 0;cursor:pointer;justify-content:center;align-items:center;display:inline-flex}',
55
+ '.sap-addButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
56
+ '.sap-addCard{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;display:flex}',
57
+ '.sap-editorHeader{align-items:baseline;gap:8px;display:flex}',
58
+ '.sap-editorTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}',
59
+ '.sap-field{flex-direction:column;gap:6px;display:flex}',
60
+ '.sap-fieldLabel{color:var(--dsw-alias-label-secondary);align-items:center;gap:10px;font-size:12px;font-weight:500;line-height:18px;display:inline-flex}',
61
+ '.sap-input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px;line-height:22px}',
62
+ '.sap-input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}',
63
+ '.sap-input::placeholder{color:var(--dsw-alias-label-dimmed)}',
64
+ '.sap-select{appearance:auto}',
65
+ '.sap-customized{border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px}',
66
+ '.sap-customizedSummary{cursor:pointer;width:fit-content;color:var(--dsw-alias-label-secondary);border-radius:6px;align-items:center;gap:6px;margin-left:-4px;padding:2px 4px;font-size:12px;font-weight:500;line-height:18px;list-style:none;display:flex}',
67
+ '.sap-customizedSummary::-webkit-details-marker{display:none}',
68
+ '.sap-customizedSummary:before{content:"";border-bottom:1.5px solid;border-right:1.5px solid;width:5px;height:5px;transition:transform .12s;transform:rotate(-45deg)translate(-1px,-1px)}',
69
+ '.sap-customized[open]>.sap-customizedSummary:before{transform:rotate(45deg)translate(-1px,-1px)}',
70
+ '.sap-customizedSummary:hover{color:var(--dsw-alias-label-primary)}',
71
+ '.sap-customizedBody{flex-direction:column;gap:12px;padding-top:12px;display:flex}',
72
+ '.sap-editorActions{justify-content:flex-end;gap:8px;display:flex}',
73
+ '.sap-primaryButton,.sap-secondaryButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border:none;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}',
74
+ '.sap-primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}',
75
+ '.sap-primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}',
76
+ '.sap-secondaryButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0}',
77
+ '.sap-secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
78
+ '.sap-dangerButton{box-sizing:border-box;height:28px;color:var(--dsw-alias-state-error-primary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:14px;justify-content:center;align-items:center;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}',
79
+ '.sap-dangerButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}',
80
+ '.sap-primaryButton:disabled,.sap-secondaryButton:disabled,.sap-dangerButton:disabled,.sap-addButton:disabled{opacity:.4;cursor:default}',
81
+ '.sap-titleRow{display:flex;align-items:center;justify-content:space-between;gap:12px}',
82
+ '.sap-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);user-select:none}',
83
+ '.sap-switch input{appearance:none;width:36px;height:20px;border-radius:10px;background:var(--dsw-alias-border-l2);position:relative;cursor:pointer;margin:0;transition:background .2s;flex:none}',
84
+ '.sap-switch input::before{content:"";position:absolute;width:16px;height:16px;border-radius:50%;background:#fff;top:2px;left:2px;transition:left .2s;box-shadow:0 1px 2px rgba(0,0,0,.2)}',
85
+ '.sap-switch input:checked{background:var(--dsw-alias-brand-primary)}',
86
+ '.sap-switch input:checked::before{left:18px}',
87
+ '.sap-toolview{flex-direction:column;align-items:flex-start;gap:6px;padding:4px 8px;display:flex}',
88
+ '.sap-toolText{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word}',
89
+ '.sap-toolModes{flex-wrap:wrap;gap:4px 16px;display:flex}',
90
+ '.sap-toolGroups{flex-direction:column;gap:8px;display:flex}',
91
+ '.sap-toolLayerCard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px}',
92
+ '.sap-toolLayerBody{flex-direction:column;gap:6px;padding-top:6px;display:flex}',
93
+ '.sap-toolGroup{flex-direction:column;gap:4px;display:flex}',
94
+ '.sap-toolGroupTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px}',
95
+ '.sap-toolActions{flex-wrap:wrap;gap:6px;display:flex}',
96
+ '.sap-toolList{flex-wrap:wrap;gap:2px 16px;display:flex}',
97
+ '.sap-toolItem{align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;line-height:20px;user-select:none;display:inline-flex}',
98
+ '.sap-toolItem input{margin:0;accent-color:var(--dsw-alias-brand-primary)}',
99
+ ].join('\n')
100
+
101
+ // ── 纯数据:dispatch 卡片面向用户固定文案 ──────────────────────────────────
102
+
103
+ // 面向用户的固定文案。注意:此表仅集中 dispatch 卡片文案;设置页文案仍为
104
+ // 散落硬编码,属待迁移债(迁移时一次性并入此表)。
105
+ const ZH = {
106
+ inlineProfile: '内联',
107
+ inherit: '继承父',
108
+ effortDefault: '默认(继承父)',
109
+ chipProfile: '方案',
110
+ chipPreset: '预设',
111
+ chipProvider: '提供方',
112
+ chipModel: '模型',
113
+ chipEffort: '推理强度',
114
+ chipTier: '档位',
115
+ chipEnabled: '已启用',
116
+ chipDisabled: '已禁用',
117
+ ignoredMark: '已忽略',
118
+ toolLabel: '派发子 Agent',
119
+ cont: 'continuable 子 Agent',
120
+ canContinue: '可续跑',
121
+ toolBackground: '后台任务',
122
+ toolForeground: '前台',
123
+ toolRunning: '运行中',
124
+ tierZh: { cheap: '省 token(cheap)', balanced: '均衡(balanced)', premium: '高成本(premium)' },
125
+ persistFail: '已保存但未持久化',
126
+ persistFailHint: '(磁盘写入未成功,重启 dsh web 后该改动可能丢失)'
127
+ }
128
+
129
+ // ── 纯数据:设置页常量 ─────────────────────────────────────────────────────
130
+
131
+ const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', toolMode: 'none', toolList: [] }
132
+ const EFFORT_ZH = { off: '关闭', high: '高', max: '最大' }
133
+ const TOOL_MODES = [
134
+ ['none', '不限制(继承父的全部工具,自动剔除 run_code)'],
135
+ ['allow', '白名单(只允许勾选的工具)'],
136
+ ['deny', '黑名单(排除勾选的工具)'],
137
+ ]
138
+ const CORE_ORDER = ['文件', '终端', '网络', '任务', '子 Agent', '工作流', '交互', '图片', 'Cordis', '计划']
139
+ const LAYER_TITLES = { core: '核心预设', plugin: '第三方插件', custom: '自建预设' }
140
+ const smallBtn = { height: '24px', padding: '0 8px', fontSize: '12px', borderRadius: '12px' }
141
+
142
+ // ── 纯函数:api / 样式注入 / 文案映射 ──────────────────────────────────────
143
+
144
+ // loopback HTTP,host 侧服务 /subagent-profiles/*。
145
+ async function api(path, payload) {
146
+ const init = payload === undefined
147
+ ? undefined
148
+ : {
149
+ method: 'POST',
150
+ headers: { 'Content-Type': 'application/json' },
151
+ body: JSON.stringify(payload),
46
152
  }
47
- if (!data.ok) throw new Error(data.error || '请求失败')
48
- return data
49
- }
50
-
51
- // ── styles (tokens mirror the shipped "模型" settings section) ──────────
52
-
53
- const CSS = [
54
- '.sap-section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}',
55
- '.sap-title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}',
56
- '.sap-intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}',
57
- '.sap-notice{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}',
58
- '.sap-savedNotice{color:var(--dsw-alias-state-success-primary);margin:0;font-size:12px;line-height:18px}',
59
- '.sap-rows{flex-direction:column;gap:8px;margin:12px 0 0;padding:0;list-style:none;display:flex}',
60
- '.sap-rowCard{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:10px;padding:12px 14px;display:flex}',
61
- '.sap-rowHead{align-items:center;gap:10px;display:flex}',
62
- '.sap-rowIdentity{align-items:center;gap:6px;min-width:0;display:inline-flex}',
63
- '.sap-rowName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}',
64
- '.sap-rowId{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-family:monospace}',
65
- '.sap-rowTag{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}',
66
- '.sap-rowActions{align-items:center;gap:4px;margin-left:auto;display:inline-flex}',
67
- '.sap-desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
68
- '.sap-warn{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}',
69
- '.sap-chips{flex-wrap:wrap;gap:6px;display:flex}',
70
- '.sap-chip{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:6px;flex:none;align-items:center;gap:5px;padding:2px 8px;font-size:12px;line-height:18px;display:inline-flex}',
71
- '.sap-chipLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
72
- '.sap-chipValue{color:var(--dsw-alias-label-primary)}',
73
- '.sap-chipValueDim{color:var(--dsw-alias-label-tertiary)}',
74
- '.sap-chipWarn{background:var(--dsw-alias-state-warn-tertiary);border-color:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);font-weight:500}',
75
- '.sap-chipSuccess{color:var(--dsw-alias-state-success-primary);font-weight:500}',
76
- '.sap-chipDisabled{color:var(--dsw-alias-state-warn-label);font-weight:500}',
77
- '.sap-metaValueDefault{color:var(--dsw-alias-label-tertiary)}',
78
- '.sap-addBlock{margin-top:4px}',
79
- '.sap-addActions{flex-wrap:wrap;gap:10px;display:flex}',
80
- '.sap-addButton{box-sizing:border-box;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;flex:1 1 0;gap:6px;min-width:180px;height:44px;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;background:0 0;cursor:pointer;justify-content:center;align-items:center;display:inline-flex}',
81
- '.sap-addButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
82
- '.sap-addCard{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;display:flex}',
83
- '.sap-editorHeader{align-items:baseline;gap:8px;display:flex}',
84
- '.sap-editorTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}',
85
- '.sap-field{flex-direction:column;gap:6px;display:flex}',
86
- '.sap-fieldLabel{color:var(--dsw-alias-label-secondary);align-items:center;gap:10px;font-size:12px;font-weight:500;line-height:18px;display:inline-flex}',
87
- '.sap-input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px;line-height:22px}',
88
- '.sap-input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}',
89
- '.sap-input::placeholder{color:var(--dsw-alias-label-dimmed)}',
90
- '.sap-select{appearance:auto}',
91
- '.sap-customized{border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px}',
92
- '.sap-customizedSummary{cursor:pointer;width:fit-content;color:var(--dsw-alias-label-secondary);border-radius:6px;align-items:center;gap:6px;margin-left:-4px;padding:2px 4px;font-size:12px;font-weight:500;line-height:18px;list-style:none;display:flex}',
93
- '.sap-customizedSummary::-webkit-details-marker{display:none}',
94
- '.sap-customizedSummary:before{content:"";border-bottom:1.5px solid;border-right:1.5px solid;width:5px;height:5px;transition:transform .12s;transform:rotate(-45deg)translate(-1px,-1px)}',
95
- '.sap-customized[open]>.sap-customizedSummary:before{transform:rotate(45deg)translate(-1px,-1px)}',
96
- '.sap-customizedSummary:hover{color:var(--dsw-alias-label-primary)}',
97
- '.sap-customizedBody{flex-direction:column;gap:12px;padding-top:12px;display:flex}',
98
- '.sap-editorActions{justify-content:flex-end;gap:8px;display:flex}',
99
- '.sap-primaryButton,.sap-secondaryButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border:none;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}',
100
- '.sap-primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}',
101
- '.sap-primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}',
102
- '.sap-secondaryButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0}',
103
- '.sap-secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
104
- '.sap-dangerButton{box-sizing:border-box;height:28px;color:var(--dsw-alias-state-error-primary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:14px;justify-content:center;align-items:center;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}',
105
- '.sap-dangerButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}',
106
- '.sap-primaryButton:disabled,.sap-secondaryButton:disabled,.sap-dangerButton:disabled,.sap-addButton:disabled{opacity:.4;cursor:default}',
107
- '.sap-titleRow{display:flex;align-items:center;justify-content:space-between;gap:12px}',
108
- '.sap-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);user-select:none}',
109
- '.sap-switch input{appearance:none;width:36px;height:20px;border-radius:10px;background:var(--dsw-alias-border-l2);position:relative;cursor:pointer;margin:0;transition:background .2s;flex:none}',
110
- '.sap-switch input::before{content:"";position:absolute;width:16px;height:16px;border-radius:50%;background:#fff;top:2px;left:2px;transition:left .2s;box-shadow:0 1px 2px rgba(0,0,0,.2)}',
111
- '.sap-switch input:checked{background:var(--dsw-alias-brand-primary)}',
112
- '.sap-switch input:checked::before{left:18px}',
113
- '.sap-toolview{flex-direction:column;align-items:flex-start;gap:6px;padding:4px 8px;display:flex}',
114
- '.sap-toolText{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word}',
115
- '.sap-toolModes{flex-wrap:wrap;gap:4px 16px;display:flex}',
116
- '.sap-toolGroups{flex-direction:column;gap:8px;display:flex}',
117
- '.sap-toolLayerCard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px}',
118
- '.sap-toolLayerBody{flex-direction:column;gap:6px;padding-top:6px;display:flex}',
119
- '.sap-toolGroup{flex-direction:column;gap:4px;display:flex}',
120
- '.sap-toolGroupTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px}',
121
- '.sap-toolActions{flex-wrap:wrap;gap:6px;display:flex}',
122
- '.sap-toolList{flex-wrap:wrap;gap:2px 16px;display:flex}',
123
- '.sap-toolItem{align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;line-height:20px;user-select:none;display:inline-flex}',
124
- '.sap-toolItem input{margin:0;accent-color:var(--dsw-alias-brand-primary)}',
125
- ].join('\n')
126
-
127
- function ensureStyles() {
128
- const tagId = 'dsh-subagent-profile/ProfilesSection.css'
129
- if (document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') !== null) return
130
- const style = document.createElement('style')
131
- style.dataset.plugin = 'dsh-subagent-profile'
132
- style.dataset.pluginCss = tagId
133
- style.textContent = CSS
134
- document.head.appendChild(style)
135
- }
136
-
137
- // ── dispatch tool-call card ─────────────────────────────────────────────
138
-
139
- // 面向用户的固定文案(G1)。注意:此表仅集中 dispatch 卡片文案;设置页文案
140
- // 仍为 v0.1.0 散落硬编码,属 G1 待迁移债(V2.0 中期一次性迁入)。
141
- const ZH = {
142
- inlineProfile: '内联',
143
- inherit: '继承父',
144
- effortDefault: '默认(继承父)',
145
- chipProfile: '方案',
146
- chipPreset: '预设',
147
- chipProvider: '提供方',
148
- chipModel: '模型',
149
- chipEffort: '推理强度',
150
- chipTier: '档位',
151
- chipEnabled: '已启用',
152
- chipDisabled: '已禁用',
153
- ignoredMark: '已忽略',
154
- toolLabel: '派发子 Agent',
155
- cont: 'continuable 子 Agent',
156
- canContinue: '可续跑',
157
- toolBackground: '后台任务',
158
- toolForeground: '前台',
159
- toolRunning: '运行中',
160
- tierZh: { cheap: '省 token(cheap)', balanced: '均衡(balanced)', premium: '高成本(premium)' },
161
- persistFail: '已保存但未持久化',
162
- persistFailHint: '(磁盘写入未成功,重启 dsh web 后该改动可能丢失)'
163
- }
164
-
165
- const effortZh = (id) => {
166
- if (id === undefined || id === null || id === '') return ZH.inherit
167
- if (id === '(default)') return ZH.effortDefault
168
- if (id === '(parent)' || id === 'undefined') return ZH.inherit
169
- return EFFORT_ZH[id] ? `${EFFORT_ZH[id]}(${id})` : id
170
- }
171
- const presetZh = (v) => (v === 'inherit' ? ZH.inherit : (typeof v === 'string' && v !== '' ? v : ZH.inherit))
172
- const profileZh = (v) => (v === undefined || v === '' || v === '(inline)' ? ZH.inlineProfile : v)
173
- const rawOrInherit = (v) => (v === '(parent)' ? ZH.inherit : (typeof v === 'string' && v !== '' ? v : ZH.inherit))
174
-
175
- /** 统一 chip 元素:label + value,dim=继承/默认值弱显。 */
176
- const chip = (key, label, value, opts) => {
177
- const valueCls = opts && opts.dim ? 'sap-chipValueDim' : 'sap-chipValue'
178
- return el('span', {
179
- key,
180
- className: 'sap-chip',
181
- ...(opts && typeof opts.title === 'string' && opts.title !== '' ? { title: opts.title } : {})
182
- },
183
- el('span', { className: 'sap-chipLabel' }, label),
184
- el('span', { className: valueCls }, value)
185
- )
186
- }
187
-
188
- // 请求值:argsRaw(运行中 / 已结算两种 block 形态都有)。
189
- function readDispatchRequest(block) {
190
- const out = { profile: '', preset: '', provider: '', model: '', reasoningEffort: '', continuable: false, runInBackground: false }
191
- if (!block || typeof block !== 'object') return out
192
- const done = 'kind' in block
193
- const argsRaw = (done ? (block.call && block.call.argsRaw) : block.argsRaw) ?? ''
194
- if (typeof argsRaw !== 'string' || argsRaw === '') return out
195
- try {
196
- const parsed = JSON.parse(argsRaw)
197
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
198
- for (const key of ['profile', 'preset', 'provider', 'model', 'reasoningEffort']) {
199
- if (typeof parsed[key] === 'string' && parsed[key] !== '') out[key] = parsed[key]
200
- }
201
- out.continuable = parsed.continuable === true
202
- out.runInBackground = parsed.run_in_background === true
203
- }
204
- } catch {
205
- // unparsable argsRaw:保持默认(继承/内联口径)。
153
+ const res = await fetch(`/subagent-profiles${path}`, init)
154
+ let data
155
+ try {
156
+ data = await res.json()
157
+ } catch {
158
+ throw new Error(`宿主返回了非 JSON 响应(HTTP ${res.status})`)
159
+ }
160
+ if (!data.ok) throw new Error(data.error || '请求失败')
161
+ return data
162
+ }
163
+
164
+ function ensureStyles() {
165
+ const tagId = 'dsh-subagent-profile/ProfilesSection.css'
166
+ if (document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') !== null) return
167
+ const style = document.createElement('style')
168
+ style.dataset.plugin = 'dsh-subagent-profile'
169
+ style.dataset.pluginCss = tagId
170
+ style.textContent = CSS
171
+ document.head.appendChild(style)
172
+ }
173
+
174
+ const effortZh = (id) => {
175
+ if (id === undefined || id === null || id === '') return ZH.inherit
176
+ if (id === '(default)') return ZH.effortDefault
177
+ if (id === '(parent)' || id === 'undefined') return ZH.inherit
178
+ return EFFORT_ZH[id] ? `${EFFORT_ZH[id]}(${id})` : id
179
+ }
180
+ const presetZh = (v) => (v === 'inherit' ? ZH.inherit : (typeof v === 'string' && v !== '' ? v : ZH.inherit))
181
+ const profileZh = (v) => (v === undefined || v === '' || v === '(inline)' ? ZH.inlineProfile : v)
182
+ const rawOrInherit = (v) => (v === '(parent)' ? ZH.inherit : (typeof v === 'string' && v !== '' ? v : ZH.inherit))
183
+ const effortLabel = (effort) => {
184
+ const id = effort && effort.id
185
+ return EFFORT_ZH[id] ? `${EFFORT_ZH[id]}(${id})` : (effort && effort.name) || id
186
+ }
187
+
188
+ // Distinct providers, derived from the model list.
189
+ function deriveProviders(models) {
190
+ const providers = []
191
+ const seenProvider = new Set()
192
+ for (const model of models) {
193
+ if (seenProvider.has(model.provider)) continue
194
+ seenProvider.add(model.provider)
195
+ providers.push({ id: model.provider, name: model.providerName || model.provider })
196
+ }
197
+ return providers
198
+ }
199
+
200
+ // ── 纯函数:dispatch 块解析 ────────────────────────────────────────────────
201
+
202
+ // 请求值:argsRaw(运行中 / 已结算两种 block 形态都有)。
203
+ function readDispatchRequest(block) {
204
+ const out = { profile: '', preset: '', provider: '', model: '', reasoningEffort: '', continuable: false, runInBackground: false }
205
+ if (!block || typeof block !== 'object') return out
206
+ const done = 'kind' in block
207
+ const argsRaw = (done ? (block.call && block.call.argsRaw) : block.argsRaw) ?? ''
208
+ if (typeof argsRaw !== 'string' || argsRaw === '') return out
209
+ try {
210
+ const parsed = JSON.parse(argsRaw)
211
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
212
+ for (const key of ['profile', 'preset', 'provider', 'model', 'reasoningEffort']) {
213
+ if (typeof parsed[key] === 'string' && parsed[key] !== '') out[key] = parsed[key]
206
214
  }
207
- return out
215
+ out.continuable = parsed.continuable === true
216
+ out.runInBackground = parsed.run_in_background === true
208
217
  }
209
-
210
- // 结果 meta(生效值):宿主 render 行("[dispatch] …")是持久回放的唯一
211
- // 生效值来源——结构化 value 只存在于执行局部(dsh-tools 约定),且宿主未
212
- // 定义 output.presentationMeta(block.meta 为空)。解析成功返回
213
- // {kind, id, fields, ignored};任何一步不符预期返回 null,调用方回退请求值。
214
- const DISPATCH_RESULT_KEYS = ['profile', 'preset', 'provider', 'model', 'reasoningEffort', 'tokenTier']
215
- function parseDispatchText(text) {
216
- if (typeof text !== 'string') return null
217
- const firstLine = text.split('\n')[0] ?? ''
218
- if (!firstLine.startsWith('[dispatch] ')) return null
219
- let rest = firstLine.slice('[dispatch] '.length)
220
- let kind = 'foreground'
221
- let id = ''
222
- if (rest.startsWith('background job ')) {
223
- kind = 'background'
224
- rest = rest.slice('background job '.length)
225
- } else if (rest.startsWith('started subagent ')) {
226
- kind = 'continuable'
227
- rest = rest.slice('started subagent '.length)
228
- }
229
- if (kind !== 'foreground') {
230
- const sep = rest.indexOf(' · ')
231
- id = sep >= 0 ? rest.slice(0, sep) : rest
232
- rest = sep >= 0 ? rest.slice(sep + 3) : ''
233
- }
234
- const fields = {}
235
- let ignored = []
236
- let known = 0
237
- for (const seg of rest.split(' · ')) {
238
- const eq = seg.indexOf('=')
239
- if (eq <= 0) continue
240
- const key = seg.slice(0, eq)
241
- if (!DISPATCH_RESULT_KEYS.includes(key)) continue
242
- let value = seg.slice(eq + 1)
243
- const m = /^(.+)\(ignored:\s*([^)]*)\)$/.exec(value)
244
- if (m !== null) {
245
- value = m[1]
246
- ignored = m[2].split(',').map((s) => s.trim()).filter((s) => s !== '')
247
- }
248
- if (value === 'undefined') value = '' // v0.1.0 渲染该字段为字面 undefined
249
- fields[key] = value
250
- known += 1
251
- }
252
- if (known <= 0) return null
253
- return { kind, id, fields, ignored }
218
+ } catch {
219
+ // unparsable argsRaw:保持默认(继承/内联口径)。
220
+ }
221
+ return out
222
+ }
223
+
224
+ // 结果 meta(生效值):宿主 render 行("[dispatch] …")是持久回放的唯一
225
+ // 生效值来源——结构化 value 只存在于执行局部(dsh-tools 约定),且宿主未
226
+ // 定义 output.presentationMeta(block.meta 为空)。解析成功返回
227
+ // {kind, id, fields, ignored};任何一步不符预期返回 null,调用方回退请求值。
228
+ const DISPATCH_RESULT_KEYS = ['profile', 'preset', 'provider', 'model', 'reasoningEffort', 'tokenTier']
229
+ function parseDispatchText(text) {
230
+ if (typeof text !== 'string') return null
231
+ const firstLine = text.split('\n')[0] ?? ''
232
+ if (!firstLine.startsWith('[dispatch] ')) return null
233
+ let rest = firstLine.slice('[dispatch] '.length)
234
+ let kind = 'foreground'
235
+ let id = ''
236
+ if (rest.startsWith('background job ')) {
237
+ kind = 'background'
238
+ rest = rest.slice('background job '.length)
239
+ } else if (rest.startsWith('started subagent ')) {
240
+ kind = 'continuable'
241
+ rest = rest.slice('started subagent '.length)
242
+ }
243
+ if (kind !== 'foreground') {
244
+ const sep = rest.indexOf(' · ')
245
+ id = sep >= 0 ? rest.slice(0, sep) : rest
246
+ rest = sep >= 0 ? rest.slice(sep + 3) : ''
247
+ }
248
+ const fields = {}
249
+ let ignored = []
250
+ let known = 0
251
+ for (const seg of rest.split(' · ')) {
252
+ const eq = seg.indexOf('=')
253
+ if (eq <= 0) continue
254
+ const key = seg.slice(0, eq)
255
+ if (!DISPATCH_RESULT_KEYS.includes(key)) continue
256
+ let value = seg.slice(eq + 1)
257
+ const m = /^(.+)\(ignored:\s*([^)]*)\)$/.exec(value)
258
+ if (m !== null) {
259
+ value = m[1]
260
+ ignored = m[2].split(',').map((s) => s.trim()).filter((s) => s !== '')
254
261
  }
255
-
256
- function readDispatchResult(block) {
257
- if (!block || typeof block !== 'object' || !('kind' in block)) return null
258
- const content = block.content
259
- if (!Array.isArray(content)) return null
260
- for (const item of content) {
261
- if (item && typeof item === 'object' && typeof item.text === 'string') {
262
- const parsed = parseDispatchText(item.text)
263
- if (parsed !== null) return parsed
264
- }
265
- }
266
- return null
262
+ if (value === 'undefined') value = '' // 旧版把该字段渲染为字面 undefined,此处归一为空
263
+ fields[key] = value
264
+ known += 1
265
+ }
266
+ if (known <= 0) return null
267
+ return { kind, id, fields, ignored }
268
+ }
269
+
270
+ function readDispatchResult(block) {
271
+ if (!block || typeof block !== 'object' || !('kind' in block)) return null
272
+ const content = block.content
273
+ if (!Array.isArray(content)) return null
274
+ for (const item of content) {
275
+ if (item && typeof item === 'object' && typeof item.text === 'string') {
276
+ const parsed = parseDispatchText(item.text)
277
+ if (parsed !== null) return parsed
267
278
  }
268
-
269
- function DispatchToolview(props) {
270
- const block = props && props.block
271
- const request = readDispatchRequest(block)
272
- const result = readDispatchResult(block)
273
- const settled = !!(block && typeof block === 'object' && 'kind' in block)
274
- // 生效值:结果解析成功用结果(优先),字段缺失回退请求值。
275
- const eff = {
276
- profile: result ? (result.fields.profile || request.profile) : request.profile,
277
- preset: result ? (result.fields.preset || request.preset) : request.preset,
278
- provider: result ? (result.fields.provider || request.provider) : request.provider,
279
- model: result ? (result.fields.model || request.model) : request.model,
280
- reasoningEffort: result ? (result.fields.reasoningEffort || request.reasoningEffort) : request.reasoningEffort
281
- }
282
- // ignored 列表:已结算读结果;运行中(无结果)按已知降级规则预判——
283
- // continuable 恒忽略 preset 换用与 reasoningEffort(与结果同口径)。
284
- const ignored = result ? result.ignored.slice() : []
285
- if (!settled && request.continuable) {
286
- if (request.preset !== '' && request.preset !== 'inherit' && !ignored.includes('preset')) ignored.push('preset')
287
- if (request.reasoningEffort !== '' && !ignored.includes('reasoningEffort')) ignored.push('reasoningEffort')
288
- }
289
- // P2(请求≠生效):被忽略字段的生效值 = 确定性降级值。
290
- if (ignored.includes('preset')) eff.preset = 'inherit'
291
- if (ignored.includes('reasoningEffort')) eff.reasoningEffort = '(default)'
292
-
293
- let kindLabel
294
- const kindPrefix = request.continuable ? ZH.cont : (request.runInBackground ? ZH.toolBackground : ZH.toolForeground)
295
- // 运行/结算标签判据 = settled(`'kind' in block`):已结算块即使 render
296
- // 旧格式不可解析(result===null)也绝不落入「运行中」分支。
297
- if (settled) {
298
- if (result) {
299
- if (result.kind === 'background') kindLabel = `${ZH.toolBackground} #${result.id}`
300
- else if (result.kind === 'continuable') kindLabel = `${ZH.cont} #${result.id}(${ZH.canContinue})`
301
- else kindLabel = ZH.toolForeground
302
- } else {
303
- // 已结算但结果不可解析:kind 由请求值推导,不带「运行中」。
304
- kindLabel = kindPrefix
305
- }
306
- } else {
307
- kindLabel = `${kindPrefix}(${ZH.toolRunning})`
308
- }
309
-
310
- const chips = []
311
- chips.push(chip('profile', ZH.chipProfile, profileZh(eff.profile)))
312
- // preset 继承值('inherit' 或未请求即空串,均渲染为「继承父」)与
313
- // provider·model / effort 一样弱显。
314
- chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: eff.preset === 'inherit' || eff.preset === '' }))
315
- const p = rawOrInherit(eff.provider)
316
- const m = rawOrInherit(eff.model)
317
- chips.push(chip('providerModel', `${ZH.chipProvider}·${ZH.chipModel}`, p === ZH.inherit && m === ZH.inherit ? ZH.inherit : `${p} / ${m}`, { dim: p === ZH.inherit && m === ZH.inherit }))
318
- chips.push(chip('effort', ZH.chipEffort, effortZh(eff.reasoningEffort), { dim: eff.reasoningEffort === '' || eff.reasoningEffort === '(default)' }))
319
- // tokenTier:宿主当前结果里未产出(§8.3 属中期切片);字段出现即渲染(向前兼容)。
320
- let tier = ''
321
- if (result && result.fields && typeof result.fields.tokenTier === 'string' && result.fields.tokenTier !== '') {
322
- tier = result.fields.tokenTier
323
- } else if (block && block.meta && typeof block.meta === 'object' && typeof block.meta.tokenTier === 'string' && block.meta.tokenTier !== '') {
324
- tier = block.meta.tokenTier
325
- }
326
- if (tier !== '') chips.push(chip('tier', ZH.chipTier, ZH.tierZh[tier] || tier))
327
- // ignored 项:警示色、置于 chip 行末尾(BP-03 根治:非静默)。
328
- for (const key of ignored) {
329
- const reqVal = request[key] || ''
330
- chips.push(el('span', {
331
- key: `ignored-${key}`,
332
- className: 'sap-chip sap-chipWarn',
333
- title: reqVal !== '' ? `请求值 "${reqVal}" 已忽略` : '请求值已忽略'
334
- }, `⬆${key}(${ZH.ignoredMark})`))
335
- }
336
-
337
- const text = `${ZH.toolLabel}:${kindLabel} · 方案=${profileZh(eff.profile)} · 预设=${presetZh(eff.preset)} · 提供方=${p} · 模型=${m} · 推理强度=${effortZh(eff.reasoningEffort)}`
338
- return el('div', { className: 'sap-toolview' },
339
- el('div', { className: 'sap-chips' }, chips),
340
- el('div', { className: 'sap-toolText' }, text)
341
- )
279
+ }
280
+ return null
281
+ }
282
+
283
+ // ── dispatch 工具卡(React 依赖经参数注入)─────────────────────────────────
284
+
285
+ // 生效值:结果解析成功用结果(优先),字段缺失回退请求值。
286
+ function resolveDispatchState(request, result, settled) {
287
+ const eff = {
288
+ profile: result ? (result.fields.profile || request.profile) : request.profile,
289
+ preset: result ? (result.fields.preset || request.preset) : request.preset,
290
+ provider: result ? (result.fields.provider || request.provider) : request.provider,
291
+ model: result ? (result.fields.model || request.model) : request.model,
292
+ reasoningEffort: result ? (result.fields.reasoningEffort || request.reasoningEffort) : request.reasoningEffort
293
+ }
294
+ // ignored 列表:已结算读结果;运行中(无结果)按已知降级规则预判——
295
+ // continuable 恒忽略 preset 换用与 reasoningEffort(与结果同口径)。
296
+ const ignored = result ? result.ignored.slice() : []
297
+ if (!settled && request.continuable) {
298
+ if (request.preset !== '' && request.preset !== 'inherit' && !ignored.includes('preset')) ignored.push('preset')
299
+ if (request.reasoningEffort !== '' && !ignored.includes('reasoningEffort')) ignored.push('reasoningEffort')
300
+ }
301
+ // 请求≠生效:被忽略字段的生效值 = 确定性降级值。
302
+ if (ignored.includes('preset')) eff.preset = 'inherit'
303
+ if (ignored.includes('reasoningEffort')) eff.reasoningEffort = '(default)'
304
+
305
+ let kindLabel
306
+ const kindPrefix = request.continuable ? ZH.cont : (request.runInBackground ? ZH.toolBackground : ZH.toolForeground)
307
+ // 运行/结算标签判据 = settled(`'kind' in block`):已结算块即使 render 为
308
+ // 旧格式不可解析(result===null)也绝不落入「运行中」分支。
309
+ if (settled) {
310
+ if (result) {
311
+ if (result.kind === 'background') kindLabel = `${ZH.toolBackground} #${result.id}`
312
+ else if (result.kind === 'continuable') kindLabel = `${ZH.cont} #${result.id}(${ZH.canContinue})`
313
+ else kindLabel = ZH.toolForeground
314
+ } else {
315
+ // 已结算但结果不可解析:kind 由请求值推导,不带「运行中」。
316
+ kindLabel = kindPrefix
342
317
  }
343
-
344
- // ── settings page ───────────────────────────────────────────────────────
345
-
346
- const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', toolMode: 'none', toolList: [] }
347
- const EFFORT_ZH = { off: '关闭', high: '高', max: '最大' }
348
- const effortLabel = (effort) => {
349
- const id = effort && effort.id
350
- return EFFORT_ZH[id] ? `${EFFORT_ZH[id]}(${id})` : (effort && effort.name) || id
318
+ } else {
319
+ kindLabel = `${kindPrefix}(${ZH.toolRunning})`
320
+ }
321
+ return { eff, ignored, kindLabel }
322
+ }
323
+
324
+ function buildDispatchChips(el, chip, eff, result, block, ignored, request) {
325
+ const chips = []
326
+ chips.push(chip('profile', ZH.chipProfile, profileZh(eff.profile)))
327
+ // preset 继承值('inherit' 或未请求即空串,均渲染为「继承父」)与
328
+ // provider·model / effort 一样弱显。
329
+ chips.push(chip('preset', ZH.chipPreset, presetZh(eff.preset), { dim: eff.preset === 'inherit' || eff.preset === '' }))
330
+ const p = rawOrInherit(eff.provider)
331
+ const m = rawOrInherit(eff.model)
332
+ chips.push(chip('providerModel', `${ZH.chipProvider}·${ZH.chipModel}`, p === ZH.inherit && m === ZH.inherit ? ZH.inherit : `${p} / ${m}`, { dim: p === ZH.inherit && m === ZH.inherit }))
333
+ chips.push(chip('effort', ZH.chipEffort, effortZh(eff.reasoningEffort), { dim: eff.reasoningEffort === '' || eff.reasoningEffort === '(default)' }))
334
+ // tokenTier:宿主当前渲染结果尚未产出该字段;字段一旦出现即渲染(向前兼容)。
335
+ let tier = ''
336
+ if (result && result.fields && typeof result.fields.tokenTier === 'string' && result.fields.tokenTier !== '') {
337
+ tier = result.fields.tokenTier
338
+ } else if (block && block.meta && typeof block.meta === 'object' && typeof block.meta.tokenTier === 'string' && block.meta.tokenTier !== '') {
339
+ tier = block.meta.tokenTier
340
+ }
341
+ if (tier !== '') chips.push(chip('tier', ZH.chipTier, ZH.tierZh[tier] || tier))
342
+ // ignored 项:警示色、置于 chip 行末尾(非静默)。
343
+ for (const key of ignored) {
344
+ const reqVal = request[key] || ''
345
+ chips.push(el('span', {
346
+ key: `ignored-${key}`,
347
+ className: 'sap-chip sap-chipWarn',
348
+ title: reqVal !== '' ? `请求值 "${reqVal}" 已忽略` : '请求值已忽略'
349
+ }, `⬆${key}(${ZH.ignoredMark})`))
350
+ }
351
+ return chips
352
+ }
353
+
354
+ /** 统一 chip 元素:label + value,dim=继承/默认值弱显。 */
355
+ function makeChip(el) {
356
+ const chip = (key, label, value, opts) => {
357
+ const valueCls = opts && opts.dim ? 'sap-chipValueDim' : 'sap-chipValue'
358
+ return el('span', {
359
+ key,
360
+ className: 'sap-chip',
361
+ ...(opts && typeof opts.title === 'string' && opts.title !== '' ? { title: opts.title } : {})
362
+ },
363
+ el('span', { className: 'sap-chipLabel' }, label),
364
+ el('span', { className: valueCls }, value)
365
+ )
366
+ }
367
+ return chip
368
+ }
369
+
370
+ function makeDispatchToolview(el, chip) {
371
+ function DispatchToolview(props) {
372
+ const block = props && props.block
373
+ const request = readDispatchRequest(block)
374
+ const result = readDispatchResult(block)
375
+ const settled = !!(block && typeof block === 'object' && 'kind' in block)
376
+ const { eff, ignored, kindLabel } = resolveDispatchState(request, result, settled)
377
+ const chips = buildDispatchChips(el, chip, eff, result, block, ignored, request)
378
+ const p = rawOrInherit(eff.provider)
379
+ const m = rawOrInherit(eff.model)
380
+ const text = `${ZH.toolLabel}:${kindLabel} · 方案=${profileZh(eff.profile)} · 预设=${presetZh(eff.preset)} · 提供方=${p} · 模型=${m} · 推理强度=${effortZh(eff.reasoningEffort)}`
381
+ return el('div', { className: 'sap-toolview' },
382
+ el('div', { className: 'sap-chips' }, chips),
383
+ el('div', { className: 'sap-toolText' }, text)
384
+ )
385
+ }
386
+ return DispatchToolview
387
+ }
388
+
389
+ // ── 设置页:事件处理工厂(state 统一经 s 注入)─────────────────────────────
390
+
391
+ // 写路径契约:HTTP 200 但 persisted:false 时 host 附 persistWarning
392
+ // 「已保存但未持久化」——这里改为 amber 警示(非绿色成功态)。
393
+ function makeApplyWrite(s) {
394
+ const applyWrite = (data, successNotice) => {
395
+ if (data && data.persisted === false) {
396
+ s.setSavedNotice('')
397
+ s.setPersistWarning(typeof data.persistWarning === 'string' && data.persistWarning !== '' ? data.persistWarning : ZH.persistFail)
398
+ return
351
399
  }
352
-
353
- function ProfilesSection() {
354
- const [profiles, setProfiles] = useState([])
355
- const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] })
356
- const [enabled, setEnabled] = useState(true)
357
- const [adding, setAdding] = useState(false)
358
- const [editingId, setEditingId] = useState(null)
359
- const [error, setError] = useState('')
360
- const [savedNotice, setSavedNotice] = useState('')
361
- const [persistWarning, setPersistWarning] = useState('')
362
- const [form, setForm] = useState(EMPTY_FORM)
363
- const [tools, setTools] = useState([])
364
-
365
- const refresh = useCallback(() => {
366
- api('/list')
367
- .then((data) => setProfiles(Array.isArray(data.profiles) ? data.profiles : []))
368
- .catch((err) => setError(String((err && err.message) || err)))
369
- }, [])
370
-
371
- const loadOptions = useCallback(() => {
372
- api('/options')
373
- .then((data) => {
374
- setEnabled(data.enabled !== false)
375
- setOptions({
376
- models: Array.isArray(data.models) ? data.models : [],
377
- efforts: data.efforts && typeof data.efforts === 'object' ? data.efforts : {},
378
- presets: Array.isArray(data.presets) ? data.presets : []
379
- })
380
- setTools(Array.isArray(data.tools) ? data.tools : [])
381
- })
382
- .catch(() => {})
383
- }, [])
384
-
385
- useEffect(() => {
400
+ s.setPersistWarning('')
401
+ if (typeof successNotice === 'string' && successNotice !== '') s.setSavedNotice(successNotice)
402
+ else s.setSavedNotice('')
403
+ }
404
+ return applyWrite
405
+ }
406
+
407
+ function makeFormActions(s) {
408
+ const setField = (key) => (event) => {
409
+ const value = event && event.target ? event.target.value : ''
410
+ s.setForm((prev) => {
411
+ const next = { ...prev, [key]: value }
412
+ if (key === 'model') next.reasoningEffort = '' // 换模型时重置推理强度
413
+ return next
414
+ })
415
+ }
416
+ const setToolMode = (mode) => () => s.setForm((prev) => ({ ...prev, toolMode: mode }))
417
+ const toggleTool = (name) => () => {
418
+ s.setForm((prev) => {
419
+ const cur = Array.isArray(prev.toolList) ? prev.toolList : []
420
+ const next = cur.includes(name) ? cur.filter((n) => n !== name) : [...cur, name]
421
+ return { ...prev, toolList: next }
422
+ })
423
+ }
424
+ const selectLayer = (layer) => () => s.setForm((prev) => {
425
+ const layerNames = s.tools.filter((t) => t.layer === layer).map((t) => t.name)
426
+ return { ...prev, toolList: [...new Set([...(Array.isArray(prev.toolList) ? prev.toolList : []), ...layerNames])] }
427
+ })
428
+ const clearLayer = (layer) => () => s.setForm((prev) => {
429
+ const layerSet = new Set(s.tools.filter((t) => t.layer === layer).map((t) => t.name))
430
+ return { ...prev, toolList: (Array.isArray(prev.toolList) ? prev.toolList : []).filter((n) => !layerSet.has(n)) }
431
+ })
432
+ const invertLayer = (layer) => () => s.setForm((prev) => {
433
+ const layerNames = s.tools.filter((t) => t.layer === layer).map((t) => t.name)
434
+ const cur = Array.isArray(prev.toolList) ? prev.toolList : []
435
+ const toAdd = layerNames.filter((n) => !cur.includes(n))
436
+ const toRemove = new Set(layerNames.filter((n) => cur.includes(n)))
437
+ return { ...prev, toolList: [...cur.filter((n) => !toRemove.has(n)), ...toAdd] }
438
+ })
439
+ return { setField, setToolMode, toggleTool, selectLayer, clearLayer, invertLayer }
440
+ }
441
+
442
+ function makeEditActions(s) {
443
+ const openAdd = () => {
444
+ s.setError('')
445
+ s.setSavedNotice('')
446
+ s.setPersistWarning('')
447
+ s.setForm(EMPTY_FORM)
448
+ s.setEditingId(null)
449
+ s.setAdding(true)
450
+ }
451
+ const openEdit = (profile) => () => {
452
+ s.setError(''); s.setSavedNotice(''); s.setPersistWarning('')
453
+ const tf = profile.toolFilter && typeof profile.toolFilter === 'object' ? profile.toolFilter : {}
454
+ const allowArr = Array.isArray(tf.allow) ? tf.allow.slice() : []
455
+ const denyArr = Array.isArray(tf.deny) ? tf.deny.slice() : []
456
+ const toolMode = allowArr.length > 0 ? 'allow' : (denyArr.length > 0 ? 'deny' : 'none')
457
+ s.setForm({
458
+ id: profile.id,
459
+ description: typeof profile.description === 'string' ? profile.description : '',
460
+ preset: typeof profile.preset === 'string' ? profile.preset : '',
461
+ provider: typeof profile.provider === 'string' ? profile.provider : '',
462
+ model: typeof profile.model === 'string' ? profile.model : '',
463
+ reasoningEffort: typeof profile.reasoningEffort === 'string' ? profile.reasoningEffort : '',
464
+ toolMode,
465
+ toolList: toolMode === 'allow' ? allowArr : denyArr
466
+ })
467
+ s.setEditingId(profile.id)
468
+ s.setAdding(true)
469
+ }
470
+ const closeAdd = () => {
471
+ s.setAdding(false)
472
+ s.setEditingId(null)
473
+ s.setError('')
474
+ s.setPersistWarning('')
475
+ s.setForm(EMPTY_FORM)
476
+ }
477
+ return { openAdd, openEdit, closeAdd }
478
+ }
479
+
480
+ function makeSubmitActions(api, s, refresh, applyWrite) {
481
+ const submit = () => {
482
+ const payload = { id: typeof s.form.id === 'string' ? s.form.id.trim() : '' }
483
+ // 字符串字段总是发送(含空串):后端 merge 语义里空串 = 清除该字段,
484
+ // 否则把字段清回“继承父/默认”会被静默忽略(保留旧值)。
485
+ const add = (key, value) => { if (typeof value === 'string') payload[key] = value.trim() }
486
+ add('description', s.form.description)
487
+ add('preset', s.form.preset)
488
+ add('provider', s.form.provider)
489
+ add('model', s.form.model)
490
+ add('reasoningEffort', s.form.reasoningEffort)
491
+ const list = Array.isArray(s.form.toolList) ? s.form.toolList : []
492
+ if (s.form.toolMode === 'allow' && list.length > 0) payload.toolFilter = { allow: list }
493
+ else if (s.form.toolMode === 'deny' && list.length > 0) payload.toolFilter = { deny: list }
494
+ else payload.toolFilter = {} // 不限制:空对象 = 清除(后端会 delete)
495
+ if (typeof payload.id !== 'string' || payload.id === '') { s.setError('profile id 不能为空'); return }
496
+ // 编辑:保留原 name 和 enabled 状态
497
+ const existing = s.profiles.find((p) => p.id === s.form.id)
498
+ if (existing) {
499
+ if (typeof existing.name === 'string' && existing.name !== '') payload.name = existing.name
500
+ if (existing.enabled === false) payload.enabled = false
501
+ }
502
+ api('/add', payload)
503
+ .then((data) => {
504
+ s.setForm(EMPTY_FORM); s.setAdding(false); s.setEditingId(null); s.setError('')
505
+ applyWrite(data, `已保存 ${payload.id}`)
386
506
  refresh()
387
- loadOptions()
388
- }, [refresh, loadOptions])
389
-
390
- const toggleEnabled = () => {
391
- const next = !enabled
392
- setEnabled(next)
393
- setError('')
394
- api('/set-enabled', { enabled: next })
395
- .catch((err) => {
396
- setEnabled(!next)
397
- setError(String((err && err.message) || err))
398
- })
399
- }
400
-
401
- const setField = (key) => (event) => {
402
- const value = event && event.target ? event.target.value : ''
403
- setForm((prev) => {
404
- const next = { ...prev, [key]: value }
405
- if (key === 'model') next.reasoningEffort = '' // 换模型时重置推理强度
406
- return next
407
- })
408
- }
409
-
410
- const setToolMode = (mode) => () => setForm((prev) => ({ ...prev, toolMode: mode }))
411
- const toggleTool = (name) => () => {
412
- setForm((prev) => {
413
- const cur = Array.isArray(prev.toolList) ? prev.toolList : []
414
- const next = cur.includes(name) ? cur.filter((n) => n !== name) : [...cur, name]
415
- return { ...prev, toolList: next }
416
- })
417
- }
418
- const selectLayer = (layer) => () => setForm((prev) => {
419
- const layerNames = tools.filter((t) => t.layer === layer).map((t) => t.name)
420
- return { ...prev, toolList: [...new Set([...(Array.isArray(prev.toolList) ? prev.toolList : []), ...layerNames])] }
421
507
  })
422
- const clearLayer = (layer) => () => setForm((prev) => {
423
- const layerSet = new Set(tools.filter((t) => t.layer === layer).map((t) => t.name))
424
- return { ...prev, toolList: (Array.isArray(prev.toolList) ? prev.toolList : []).filter((n) => !layerSet.has(n)) }
508
+ .catch((err) => s.setError(String((err && err.message) || err)))
509
+ }
510
+ return { submit }
511
+ }
512
+
513
+ function makeRowActions(api, s, refresh, applyWrite) {
514
+ const toggleEnabled = () => {
515
+ const next = !s.enabled
516
+ s.setEnabled(next)
517
+ s.setError('')
518
+ api('/set-enabled', { enabled: next })
519
+ .catch((err) => {
520
+ s.setEnabled(!next)
521
+ s.setError(String((err && err.message) || err))
425
522
  })
426
- const invertLayer = (layer) => () => setForm((prev) => {
427
- const layerNames = tools.filter((t) => t.layer === layer).map((t) => t.name)
428
- const cur = Array.isArray(prev.toolList) ? prev.toolList : []
429
- const toAdd = layerNames.filter((n) => !cur.includes(n))
430
- const toRemove = new Set(layerNames.filter((n) => cur.includes(n)))
431
- return { ...prev, toolList: [...cur.filter((n) => !toRemove.has(n)), ...toAdd] }
523
+ }
524
+ const setProfileEnabled = (profile, next) => () => {
525
+ s.setError('')
526
+ api('/set-profile-enabled', { id: profile.id, enabled: next })
527
+ .then((data) => { applyWrite(data, ''); refresh() })
528
+ .catch((err) => s.setError(String((err && err.message) || err)))
529
+ }
530
+ const resetAll = () => {
531
+ if (!window.confirm('将所有内置方案重置为默认配置?已删除的内置也会恢复。')) return
532
+ s.setError('')
533
+ api('/reset-all')
534
+ .then((data) => { applyWrite(data, '已重置所有内置方案'); refresh() })
535
+ .catch((err) => s.setError(String((err && err.message) || err)))
536
+ }
537
+ const remove = (profile) => () => {
538
+ if (!window.confirm(`删除方案「${profile.name || profile.id}」?${profile.builtin === true ? '内置方案删除后可在上方「重置内置方案」恢复' : ''}`)) return
539
+ api('/remove', { id: profile.id })
540
+ .then((data) => {
541
+ s.setError('')
542
+ applyWrite(data, `已删除 ${profile.id}`)
543
+ refresh()
432
544
  })
433
-
434
- // D5/B1 写路径契约:HTTP 200 但 persisted:false 时 host 附
435
- // persistWarning「已保存但未持久化」——这里改为 amber 警示(非绿色成功态)。
436
- const applyWrite = (data, successNotice) => {
437
- if (data && data.persisted === false) {
438
- setSavedNotice('')
439
- setPersistWarning(typeof data.persistWarning === 'string' && data.persistWarning !== '' ? data.persistWarning : ZH.persistFail)
440
- return
441
- }
442
- setPersistWarning('')
443
- if (typeof successNotice === 'string' && successNotice !== '') setSavedNotice(successNotice)
444
- else setSavedNotice('')
445
- }
446
-
447
- const openAdd = () => {
448
- setError('')
449
- setSavedNotice('')
450
- setPersistWarning('')
451
- setForm(EMPTY_FORM)
452
- setEditingId(null)
453
- setAdding(true)
454
- }
455
-
456
- const openEdit = (profile) => () => {
457
- setError(''); setSavedNotice(''); setPersistWarning('')
458
- const tf = profile.toolFilter && typeof profile.toolFilter === 'object' ? profile.toolFilter : {}
459
- const allowArr = Array.isArray(tf.allow) ? tf.allow.slice() : []
460
- const denyArr = Array.isArray(tf.deny) ? tf.deny.slice() : []
461
- const toolMode = allowArr.length > 0 ? 'allow' : (denyArr.length > 0 ? 'deny' : 'none')
462
- setForm({
463
- id: profile.id,
464
- description: typeof profile.description === 'string' ? profile.description : '',
465
- preset: typeof profile.preset === 'string' ? profile.preset : '',
466
- provider: typeof profile.provider === 'string' ? profile.provider : '',
467
- model: typeof profile.model === 'string' ? profile.model : '',
468
- reasoningEffort: typeof profile.reasoningEffort === 'string' ? profile.reasoningEffort : '',
469
- toolMode,
470
- toolList: toolMode === 'allow' ? allowArr : denyArr
471
- })
472
- setEditingId(profile.id)
473
- setAdding(true)
474
- }
475
-
476
- const closeAdd = () => {
477
- setAdding(false)
478
- setEditingId(null)
479
- setError('')
480
- setPersistWarning('')
481
- setForm(EMPTY_FORM)
482
- }
483
-
484
- const submit = () => {
485
- const payload = { id: typeof form.id === 'string' ? form.id.trim() : '' }
486
- // 字符串字段总是发送(含空串):后端 merge 语义里空串 = 清除该字段,
487
- // 否则把字段清回“继承父/默认”会被静默忽略(保留旧值)。
488
- const add = (key, value) => { if (typeof value === 'string') payload[key] = value.trim() }
489
- add('description', form.description)
490
- add('preset', form.preset)
491
- add('provider', form.provider)
492
- add('model', form.model)
493
- add('reasoningEffort', form.reasoningEffort)
494
- const list = Array.isArray(form.toolList) ? form.toolList : []
495
- if (form.toolMode === 'allow' && list.length > 0) payload.toolFilter = { allow: list }
496
- else if (form.toolMode === 'deny' && list.length > 0) payload.toolFilter = { deny: list }
497
- else payload.toolFilter = {} // 不限制:空对象 = 清除(后端会 delete)
498
- if (typeof payload.id !== 'string' || payload.id === '') { setError('profile id 不能为空'); return }
499
- // 编辑:保留原 name enabled 状态
500
- const existing = profiles.find((p) => p.id === form.id)
501
- if (existing) {
502
- if (typeof existing.name === 'string' && existing.name !== '') payload.name = existing.name
503
- if (existing.enabled === false) payload.enabled = false
504
- }
505
- api('/add', payload)
506
- .then((data) => {
507
- setForm(EMPTY_FORM); setAdding(false); setEditingId(null); setError('')
508
- applyWrite(data, `已保存 ${payload.id}`)
509
- refresh()
510
- })
511
- .catch((err) => setError(String((err && err.message) || err)))
512
- }
513
-
514
- const setProfileEnabled = (profile, next) => () => {
515
- setError('')
516
- api('/set-profile-enabled', { id: profile.id, enabled: next })
517
- .then((data) => { applyWrite(data, ''); refresh() })
518
- .catch((err) => setError(String((err && err.message) || err)))
519
- }
520
-
521
- const resetAll = () => {
522
- if (!window.confirm('将所有内置方案重置为默认配置?已删除的内置也会恢复。')) return
523
- setError('')
524
- api('/reset-all')
525
- .then((data) => { applyWrite(data, '已重置所有内置方案'); refresh() })
526
- .catch((err) => setError(String((err && err.message) || err)))
527
- }
528
-
529
- const remove = (profile) => () => {
530
- if (!window.confirm(`删除方案「${profile.name || profile.id}」?${profile.builtin === true ? '内置方案删除后可在上方「重置内置方案」恢复' : ''}`)) return
531
- api('/remove', { id: profile.id })
532
- .then((data) => {
533
- setError('')
534
- applyWrite(data, `已删除 ${profile.id}`)
535
- refresh()
536
- })
537
- .catch((err) => setError(String((err && err.message) || err)))
538
- }
539
-
540
- // Distinct providers, derived from the model list.
541
- const providers = []
542
- const seenProvider = new Set()
543
- for (const model of options.models) {
544
- if (seenProvider.has(model.provider)) continue
545
- seenProvider.add(model.provider)
546
- providers.push({ id: model.provider, name: model.providerName || model.provider })
547
- }
548
-
549
- const effortsForModel = form.model ? (options.efforts[form.model] ?? []) : []
550
-
551
- const rows = profiles.map((profile) => {
552
- const builtin = profile.builtin === true
553
- const isDisabled = profile.enabled === false
554
- const hasName = typeof profile.name === 'string' && profile.name !== '' && profile.name !== String(profile.id)
555
- const hasPreset = typeof profile.preset === 'string' && profile.preset !== ''
556
- const hasModel = typeof profile.model === 'string' && profile.model !== ''
557
- const hasEffort = typeof profile.reasoningEffort === 'string' && profile.reasoningEffort !== ''
558
- const chipItems = [
559
- chip('preset', ZH.chipPreset, presetZh(profile.preset), { dim: !hasPreset }),
560
- chip('model', ZH.chipModel, rawOrInherit(profile.model), { dim: !hasModel }),
561
- chip('effort', ZH.chipEffort, effortZh(profile.reasoningEffort), { dim: !hasEffort }),
562
- isDisabled
563
- ? el('span', { key: 'state', className: 'sap-chip sap-chipDisabled' }, ZH.chipDisabled)
564
- : el('span', { key: 'state', className: 'sap-chip sap-chipSuccess' }, ZH.chipEnabled)
565
- ]
566
- return el('li', { key: String(profile.id), className: 'sap-rowCard' },
567
- el('div', { className: 'sap-rowHead' },
568
- el('span', { className: 'sap-rowIdentity' },
569
- el('span', { className: 'sap-rowName' }, hasName ? profile.name : String(profile.id)),
570
- hasName ? el('span', { className: 'sap-rowId' }, String(profile.id)) : null,
571
- builtin ? el('span', { className: 'sap-rowTag' }, '内置') : null
572
- ),
573
- el('span', { className: 'sap-rowActions' },
574
- el('label', { className: 'sap-switch', title: '启用/禁用' },
575
- el('input', { type: 'checkbox', checked: !isDisabled, onChange: setProfileEnabled(profile, isDisabled) }),
576
- el('span', null, isDisabled ? '已禁用' : '已启用')
577
- ),
578
- el('button', { className: 'sap-secondaryButton', style: { height: '28px', padding: '0 10px', fontSize: '12px', borderRadius: '14px' }, onClick: openEdit(profile) }, '编辑'),
579
- el('button', { className: 'sap-dangerButton', onClick: remove(profile) }, '删除')
580
- )
545
+ .catch((err) => s.setError(String((err && err.message) || err)))
546
+ }
547
+ return { toggleEnabled, setProfileEnabled, resetAll, remove }
548
+ }
549
+
550
+ function makeProfilesActions(api, s, refresh, applyWrite) {
551
+ return {
552
+ ...makeFormActions(s),
553
+ ...makeEditActions(s),
554
+ ...makeSubmitActions(api, s, refresh, applyWrite),
555
+ ...makeRowActions(api, s, refresh, applyWrite),
556
+ }
557
+ }
558
+
559
+ // ── 设置页:渲染 helpers ────────────────────────────────────────────────────
560
+
561
+ function buildSelectField(el, s, setField, label, key, optionsList, renderOption) {
562
+ return el('div', { className: 'sap-field' },
563
+ el('span', { className: 'sap-fieldLabel' }, label),
564
+ el('select', { className: 'sap-input sap-select', value: s.form[key], onChange: setField(key) },
565
+ el('option', { value: '' }, '继承父(不指定)'),
566
+ optionsList.map(renderOption)
567
+ )
568
+ )
569
+ }
570
+
571
+ function renderTool(el, s, actions, t) {
572
+ const checked = Array.isArray(s.form.toolList) && s.form.toolList.includes(t.name)
573
+ return el('label', { key: t.name, className: 'sap-toolItem' },
574
+ el('input', { type: 'checkbox', checked, onChange: actions.toggleTool(t.name) }),
575
+ el('span', null, t.zh ? `${t.zh} ${t.name}` : t.name)
576
+ )
577
+ }
578
+
579
+ function buildLayerGroups(tools, layer) {
580
+ const layerTools = tools.filter((t) => t.layer === layer)
581
+ const groups = new Map()
582
+ for (const t of layerTools) {
583
+ const g = typeof t.group === 'string' && t.group !== '' ? t.group : '其他'
584
+ if (!groups.has(g)) groups.set(g, [])
585
+ groups.get(g).push(t)
586
+ }
587
+ const keys = layer === 'core'
588
+ ? [...CORE_ORDER.filter((c) => groups.has(c)), ...new Set([...groups.keys()].filter((k) => !CORE_ORDER.includes(k)))]
589
+ : [...groups.keys()]
590
+ return keys.map((k) => [k, groups.get(k)])
591
+ }
592
+
593
+ function buildToolModePicker(el, s, actions) {
594
+ return el('div', { className: 'sap-toolModes' },
595
+ TOOL_MODES.map(([mode, label]) => el('label', { key: mode, className: 'sap-toolItem' },
596
+ el('input', { type: 'radio', name: 'sap-toolMode', checked: s.form.toolMode === mode, onChange: actions.setToolMode(mode) }),
597
+ el('span', null, label)
598
+ ))
599
+ )
600
+ }
601
+
602
+ function buildToolLayerCard(el, s, actions, layer, groups) {
603
+ const layerTools = s.tools.filter((t) => t.layer === layer)
604
+ const count = layerTools.length
605
+ const selectedCount = layerTools.filter((t) => Array.isArray(s.form.toolList) && s.form.toolList.includes(t.name)).length
606
+ const summaryText = selectedCount > 0
607
+ ? `${LAYER_TITLES[layer]}(${count})· 已选 ${selectedCount}`
608
+ : `${LAYER_TITLES[layer]}(${count})`
609
+ return el('details', { key: layer, className: 'sap-toolLayerCard' },
610
+ el('summary', { className: 'sap-customizedSummary' }, summaryText),
611
+ el('div', { className: 'sap-toolLayerBody' },
612
+ el('div', { className: 'sap-toolActions', style: { marginTop: '2px' } },
613
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.selectLayer(layer) }, '全选'),
614
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.clearLayer(layer) }, '清空'),
615
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.invertLayer(layer) }, '反选')
616
+ ),
617
+ groups.map(([g, list]) => el('div', { key: g, className: 'sap-toolGroup' },
618
+ el('div', { className: 'sap-toolGroupTitle' }, `${g}(${list.length})`),
619
+ el('div', { className: 'sap-toolList' }, list.map((t) => renderTool(el, s, actions, t)))
620
+ ))
621
+ )
622
+ )
623
+ }
624
+
625
+ function buildToolLayers(el, s, actions) {
626
+ return el('div', { className: 'sap-toolGroups' },
627
+ ['core', 'plugin', 'custom']
628
+ .filter((layer) => s.tools.some((t) => t.layer === layer))
629
+ .map((layer) => buildToolLayerCard(el, s, actions, layer, buildLayerGroups(s.tools, layer)))
630
+ )
631
+ }
632
+
633
+ function buildToolSection(el, s, actions) {
634
+ return el('div', { className: 'sap-field' },
635
+ el('span', { className: 'sap-fieldLabel' }, '工具限制'),
636
+ buildToolModePicker(el, s, actions),
637
+ s.form.toolMode === 'none'
638
+ ? null
639
+ : (s.tools.length === 0
640
+ ? el('span', { className: 'sap-metaValueDefault' }, '(无可用工具列表)')
641
+ : buildToolLayers(el, s, actions))
642
+ )
643
+ }
644
+
645
+ function buildProfileRows(el, chip, s, actions) {
646
+ return s.profiles.map((profile) => {
647
+ const builtin = profile.builtin === true
648
+ const isDisabled = profile.enabled === false
649
+ const hasName = typeof profile.name === 'string' && profile.name !== '' && profile.name !== String(profile.id)
650
+ const hasPreset = typeof profile.preset === 'string' && profile.preset !== ''
651
+ const hasModel = typeof profile.model === 'string' && profile.model !== ''
652
+ const hasEffort = typeof profile.reasoningEffort === 'string' && profile.reasoningEffort !== ''
653
+ const chipItems = [
654
+ chip('preset', ZH.chipPreset, presetZh(profile.preset), { dim: !hasPreset }),
655
+ chip('model', ZH.chipModel, rawOrInherit(profile.model), { dim: !hasModel }),
656
+ chip('effort', ZH.chipEffort, effortZh(profile.reasoningEffort), { dim: !hasEffort }),
657
+ isDisabled
658
+ ? el('span', { key: 'state', className: 'sap-chip sap-chipDisabled' }, ZH.chipDisabled)
659
+ : el('span', { key: 'state', className: 'sap-chip sap-chipSuccess' }, ZH.chipEnabled)
660
+ ]
661
+ return el('li', { key: String(profile.id), className: 'sap-rowCard' },
662
+ el('div', { className: 'sap-rowHead' },
663
+ el('span', { className: 'sap-rowIdentity' },
664
+ el('span', { className: 'sap-rowName' }, hasName ? profile.name : String(profile.id)),
665
+ hasName ? el('span', { className: 'sap-rowId' }, String(profile.id)) : null,
666
+ builtin ? el('span', { className: 'sap-rowTag' }, '内置') : null
667
+ ),
668
+ el('span', { className: 'sap-rowActions' },
669
+ el('label', { className: 'sap-switch', title: '启用/禁用' },
670
+ el('input', { type: 'checkbox', checked: !isDisabled, onChange: actions.setProfileEnabled(profile, isDisabled) }),
671
+ el('span', null, isDisabled ? '已禁用' : '已启用')
581
672
  ),
582
- el('div', { className: 'sap-chips' }, chipItems),
583
- typeof profile.description === 'string' && profile.description !== ''
584
- ? el('p', { className: 'sap-desc' }, profile.description)
585
- : null
586
- )
587
- })
588
-
589
- const selectField = (label, key, optionsList, renderOption) => el('div', { className: 'sap-field' },
590
- el('span', { className: 'sap-fieldLabel' }, label),
591
- el('select', { className: 'sap-input sap-select', value: form[key], onChange: setField(key) },
592
- el('option', { value: '' }, '继承父(不指定)'),
593
- optionsList.map(renderOption)
594
- )
595
- )
596
-
597
- const renderEffortOption = (effort) => el('option', { key: effort.id, value: effort.id }, effortLabel(effort))
598
-
599
- const smallBtn = { height: '24px', padding: '0 8px', fontSize: '12px', borderRadius: '12px' }
600
- const TOOL_MODES = [
601
- ['none', '不限制(继承父的全部工具,自动剔除 run_code)'],
602
- ['allow', '白名单(只允许勾选的工具)'],
603
- ['deny', '黑名单(排除勾选的工具)'],
604
- ]
605
- const CORE_ORDER = ['文件', '终端', '网络', '任务', '子 Agent', '工作流', '交互', '图片', 'Cordis', '计划']
606
- const LAYER_TITLES = { core: '核心预设', plugin: '第三方插件', custom: '自建预设' }
607
- const buildLayerGroups = (layer) => {
608
- const layerTools = tools.filter((t) => t.layer === layer)
609
- const groups = new Map()
610
- for (const t of layerTools) {
611
- const g = typeof t.group === 'string' && t.group !== '' ? t.group : '其他'
612
- if (!groups.has(g)) groups.set(g, [])
613
- groups.get(g).push(t)
614
- }
615
- const keys = layer === 'core'
616
- ? [...CORE_ORDER.filter((c) => groups.has(c)), ...new Set([...groups.keys()].filter((k) => !CORE_ORDER.includes(k)))]
617
- : [...groups.keys()]
618
- return keys.map((k) => [k, groups.get(k)])
619
- }
620
- const renderTool = (t) => {
621
- const checked = Array.isArray(form.toolList) && form.toolList.includes(t.name)
622
- return el('label', { key: t.name, className: 'sap-toolItem' },
623
- el('input', { type: 'checkbox', checked, onChange: toggleTool(t.name) }),
624
- el('span', null, t.zh ? `${t.zh} ${t.name}` : t.name)
673
+ el('button', { className: 'sap-secondaryButton', style: { height: '28px', padding: '0 10px', fontSize: '12px', borderRadius: '14px' }, onClick: actions.openEdit(profile) }, '编辑'),
674
+ el('button', { className: 'sap-dangerButton', onClick: actions.remove(profile) }, '删除')
625
675
  )
626
- }
627
- const toolSection = el('div', { className: 'sap-field' },
628
- el('span', { className: 'sap-fieldLabel' }, '工具限制'),
629
- el('div', { className: 'sap-toolModes' },
630
- TOOL_MODES.map(([mode, label]) => el('label', { key: mode, className: 'sap-toolItem' },
631
- el('input', { type: 'radio', name: 'sap-toolMode', checked: form.toolMode === mode, onChange: setToolMode(mode) }),
632
- el('span', null, label)
633
- ))
634
- ),
635
- form.toolMode === 'none'
636
- ? null
637
- : (tools.length === 0
638
- ? el('span', { className: 'sap-metaValueDefault' }, '(无可用工具列表)')
639
- : el('div', { className: 'sap-toolGroups' },
640
- ['core', 'plugin', 'custom']
641
- .filter((layer) => tools.some((t) => t.layer === layer))
642
- .map((layer) => {
643
- const groups = buildLayerGroups(layer)
644
- const layerTools = tools.filter((t) => t.layer === layer)
645
- const count = layerTools.length
646
- const selectedCount = layerTools.filter((t) => Array.isArray(form.toolList) && form.toolList.includes(t.name)).length
647
- const summaryText = selectedCount > 0
648
- ? `${LAYER_TITLES[layer]}(${count})· 已选 ${selectedCount}`
649
- : `${LAYER_TITLES[layer]}(${count})`
650
- return el('details', { key: layer, className: 'sap-toolLayerCard' },
651
- el('summary', { className: 'sap-customizedSummary' }, summaryText),
652
- el('div', { className: 'sap-toolLayerBody' },
653
- el('div', { className: 'sap-toolActions', style: { marginTop: '2px' } },
654
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: selectLayer(layer) }, '全选'),
655
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: clearLayer(layer) }, '清空'),
656
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: invertLayer(layer) }, '反选')
657
- ),
658
- groups.map(([g, list]) => el('div', { key: g, className: 'sap-toolGroup' },
659
- el('div', { className: 'sap-toolGroupTitle' }, `${g}(${list.length})`),
660
- el('div', { className: 'sap-toolList' }, list.map(renderTool))
661
- ))
662
- )
663
- )
664
- })
665
- )
676
+ ),
677
+ el('div', { className: 'sap-chips' }, chipItems),
678
+ typeof profile.description === 'string' && profile.description !== ''
679
+ ? el('p', { className: 'sap-desc' }, profile.description)
680
+ : null
681
+ )
682
+ })
683
+ }
684
+
685
+ function buildAddCard(el, s, actions, toolSection) {
686
+ const providers = deriveProviders(s.options.models)
687
+ const effortsForModel = s.form.model ? (s.options.efforts[s.form.model] ?? []) : []
688
+ const renderEffortOption = (effort) => el('option', { key: effort.id, value: effort.id }, effortLabel(effort))
689
+ return el('div', { className: 'sap-addCard' },
690
+ el('div', { className: 'sap-editorHeader' },
691
+ el('span', { className: 'sap-editorTitle' }, s.editingId !== null ? '编辑 profile' : '新增 profile')
692
+ ),
693
+ el('div', { className: 'sap-field' },
694
+ el('span', { className: 'sap-fieldLabel' }, 'profile id(唯一标识,必填)'),
695
+ el('input', { className: 'sap-input', value: s.form.id, placeholder: '小写字母/数字/连字符,如 my-focus', disabled: s.editingId !== null, onChange: actions.setField('id') })
696
+ ),
697
+ el('div', { className: 'sap-field' },
698
+ el('span', { className: 'sap-fieldLabel' }, '描述(一句话定位)'),
699
+ el('input', { className: 'sap-input', value: s.form.description, placeholder: '说明这个 profile 适合什么场景,帮助模型选择', onChange: actions.setField('description') })
700
+ ),
701
+ buildSelectField(el, s, actions.setField, '模型(子 agent 使用的模型)', 'model', s.options.models,
702
+ (m) => el('option', { key: m.id, value: m.id }, m.name && m.name !== m.id ? `${m.name}(${m.providerName || m.provider})` : m.id)),
703
+ el('details', { className: 'sap-customized' },
704
+ el('summary', { className: 'sap-customizedSummary' }, '高级选项'),
705
+ el('div', { className: 'sap-customizedBody' },
706
+ buildSelectField(el, s, actions.setField, '目标预设(切换子 agent 使用的 Agent 预设)', 'preset', s.options.presets,
707
+ (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
708
+ buildSelectField(el, s, actions.setField, '提供方(模型服务商)', 'provider', providers,
709
+ (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
710
+ s.form.model === ''
711
+ ? el('div', { className: 'sap-field' },
712
+ el('span', { className: 'sap-fieldLabel' }, '推理强度(思考深度)'),
713
+ el('select', { className: 'sap-input sap-select', value: '', disabled: true },
714
+ el('option', { value: '' }, '先选择模型')
715
+ )
666
716
  )
717
+ : buildSelectField(el, s, actions.setField, '推理强度(思考深度)', 'reasoningEffort', effortsForModel, renderEffortOption),
718
+ toolSection
667
719
  )
668
-
669
- return el('div', { className: 'sap-section' },
670
- el('div', { className: 'sap-titleRow' },
671
- el('h2', { className: 'sap-title' }, '子 Agent 方案'),
672
- el('label', { className: 'sap-switch' },
673
- el('input', { type: 'checkbox', checked: enabled, onChange: toggleEnabled }),
674
- el('span', null, enabled ? '已启用' : '已禁用')
675
- )
676
- ),
677
- el('p', { className: 'sap-intro' }, '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可点下方按钮批量重置回默认;每个方案可单独启用/禁用。'),
678
- el('div', { className: 'sap-addActions', style: { margin: '4px 0' } },
679
- el('button', { type: 'button', className: 'sap-secondaryButton', style: { height: '28px', padding: '0 12px', fontSize: '12px', borderRadius: '14px' }, onClick: resetAll }, '重置所有内置方案')
680
- ),
681
- !enabled ? el('p', { className: 'sap-notice' }, '插件已禁用:「派发子 Agent」工具已从模型工具列表中移除,打开开关即可恢复。') : null,
682
- error !== '' ? el('p', { className: 'sap-notice' }, error) : null,
683
- savedNotice !== '' ? el('p', { className: 'sap-savedNotice', role: 'status' }, savedNotice) : null,
684
- persistWarning !== '' ? el('p', { className: 'sap-warn', role: 'alert' }, `${persistWarning}${ZH.persistFailHint}`) : null,
685
- el('ul', { className: 'sap-rows' }, rows),
686
- el('div', { className: 'sap-addBlock' },
687
- adding
688
- ? el('div', { className: 'sap-addCard' },
689
- el('div', { className: 'sap-editorHeader' },
690
- el('span', { className: 'sap-editorTitle' }, editingId !== null ? '编辑 profile' : '新增 profile')
691
- ),
692
- el('div', { className: 'sap-field' },
693
- el('span', { className: 'sap-fieldLabel' }, 'profile id(唯一标识,必填)'),
694
- el('input', { className: 'sap-input', value: form.id, placeholder: '小写字母/数字/连字符,如 my-focus', disabled: editingId !== null, onChange: setField('id') })
695
- ),
696
- el('div', { className: 'sap-field' },
697
- el('span', { className: 'sap-fieldLabel' }, '描述(一句话定位)'),
698
- el('input', { className: 'sap-input', value: form.description, placeholder: '说明这个 profile 适合什么场景,帮助模型选择', onChange: setField('description') })
699
- ),
700
- selectField('模型(子 agent 使用的模型)', 'model', options.models,
701
- (m) => el('option', { key: m.id, value: m.id }, m.name && m.name !== m.id ? `${m.name}(${m.providerName || m.provider})` : m.id)),
702
- el('details', { className: 'sap-customized' },
703
- el('summary', { className: 'sap-customizedSummary' }, '高级选项'),
704
- el('div', { className: 'sap-customizedBody' },
705
- selectField('目标预设(切换子 agent 使用的 Agent 预设)', 'preset', options.presets,
706
- (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
707
- selectField('提供方(模型服务商)', 'provider', providers,
708
- (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
709
- form.model === ''
710
- ? el('div', { className: 'sap-field' },
711
- el('span', { className: 'sap-fieldLabel' }, '推理强度(思考深度)'),
712
- el('select', { className: 'sap-input sap-select', value: '', disabled: true },
713
- el('option', { value: '' }, '先选择模型')
714
- )
715
- )
716
- : selectField('推理强度(思考深度)', 'reasoningEffort', effortsForModel, renderEffortOption),
717
- toolSection
718
- )
719
- ),
720
- el('div', { className: 'sap-editorActions' },
721
- el('button', { className: 'sap-secondaryButton', onClick: closeAdd }, '取消'),
722
- el('button', { className: 'sap-primaryButton', onClick: submit }, '保存')
723
- )
724
- )
725
- : el('div', { className: 'sap-addActions' },
726
- el('button', { className: 'sap-addButton', onClick: openAdd }, '+ 新增 profile')
727
- )
728
- )
720
+ ),
721
+ el('div', { className: 'sap-editorActions' },
722
+ el('button', { className: 'sap-secondaryButton', onClick: actions.closeAdd }, '取消'),
723
+ el('button', { className: 'sap-primaryButton', onClick: actions.submit }, '保存')
724
+ )
725
+ )
726
+ }
727
+
728
+ function buildSectionReturn(el, s, actions, rows, addCard) {
729
+ return el('div', { className: 'sap-section' },
730
+ el('div', { className: 'sap-titleRow' },
731
+ el('h2', { className: 'sap-title' }, ' Agent 方案'),
732
+ el('label', { className: 'sap-switch' },
733
+ el('input', { type: 'checkbox', checked: s.enabled, onChange: actions.toggleEnabled }),
734
+ el('span', null, s.enabled ? '已启用' : '已禁用')
729
735
  )
730
- }
736
+ ),
737
+ el('p', { className: 'sap-intro' }, '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可点下方按钮批量重置回默认;每个方案可单独启用/禁用。'),
738
+ el('div', { className: 'sap-addActions', style: { margin: '4px 0' } },
739
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: { height: '28px', padding: '0 12px', fontSize: '12px', borderRadius: '14px' }, onClick: actions.resetAll }, '重置所有内置方案')
740
+ ),
741
+ !s.enabled ? el('p', { className: 'sap-notice' }, '插件已禁用:「派发子 Agent」工具已从模型工具列表中移除,打开开关即可恢复。') : null,
742
+ s.error !== '' ? el('p', { className: 'sap-notice' }, s.error) : null,
743
+ s.savedNotice !== '' ? el('p', { className: 'sap-savedNotice', role: 'status' }, s.savedNotice) : null,
744
+ s.persistWarning !== '' ? el('p', { className: 'sap-warn', role: 'alert' }, `${s.persistWarning}${ZH.persistFailHint}`) : null,
745
+ el('ul', { className: 'sap-rows' }, rows),
746
+ el('div', { className: 'sap-addBlock' },
747
+ s.adding ? addCard : el('div', { className: 'sap-addActions' },
748
+ el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
749
+ )
750
+ )
751
+ )
752
+ }
753
+
754
+ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback) {
755
+ function ProfilesSection() {
756
+ const [profiles, setProfiles] = useState([])
757
+ const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] })
758
+ const [enabled, setEnabled] = useState(true)
759
+ const [adding, setAdding] = useState(false)
760
+ const [editingId, setEditingId] = useState(null)
761
+ const [error, setError] = useState('')
762
+ const [savedNotice, setSavedNotice] = useState('')
763
+ const [persistWarning, setPersistWarning] = useState('')
764
+ const [form, setForm] = useState(EMPTY_FORM)
765
+ const [tools, setTools] = useState([])
766
+ const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools,
767
+ setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools }
768
+ const refresh = useCallback(() => {
769
+ api('/list')
770
+ .then((data) => setProfiles(Array.isArray(data.profiles) ? data.profiles : []))
771
+ .catch((err) => setError(String((err && err.message) || err)))
772
+ }, [])
773
+ const loadOptions = useCallback(() => {
774
+ api('/options')
775
+ .then((data) => {
776
+ setEnabled(data.enabled !== false)
777
+ setOptions({
778
+ models: Array.isArray(data.models) ? data.models : [],
779
+ efforts: data.efforts && typeof data.efforts === 'object' ? data.efforts : {},
780
+ presets: Array.isArray(data.presets) ? data.presets : []
781
+ })
782
+ setTools(Array.isArray(data.tools) ? data.tools : [])
783
+ })
784
+ .catch(() => {})
785
+ }, [])
786
+ useEffect(() => {
787
+ refresh()
788
+ loadOptions()
789
+ }, [refresh, loadOptions])
790
+ const applyWrite = makeApplyWrite(s)
791
+ const actions = makeProfilesActions(api, s, refresh, applyWrite)
792
+ const rows = buildProfileRows(el, chip, s, actions)
793
+ const toolSection = buildToolSection(el, s, actions)
794
+ return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection))
795
+ }
796
+ return ProfilesSection
797
+ }
798
+
799
+ // ── plugin 装配 ─────────────────────────────────────────────────────────────
800
+
801
+ window.__ModuleLoader__.load({
802
+ id: 'dsh-subagent-profile',
803
+ factory: (require) => {
804
+ const React = require('react')
805
+ const { useEffect, useState, useCallback } = React
806
+ const el = React.createElement
807
+
808
+ /** Required cordis services: the slot registry only. */
809
+ const inject = ['slots']
731
810
 
732
- // ── plugin ──────────────────────────────────────────────────────────────
811
+ const chip = makeChip(el)
812
+ const DispatchToolview = makeDispatchToolview(el, chip)
813
+ const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback)
733
814
 
734
815
  function apply(ctx) {
735
816
  ensureStyles()