dsh-subagent-profile 0.1.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
@@ -1,11 +1,11 @@
1
1
  /**
2
- * dsh-subagent-profile — browser half: a read-only `dispatch` tool-call card
2
+ * dsh-subagent-profile — browser side: a read-only `dispatch` tool-call card
3
3
  * plus a settings page that manages the profile registry.
4
4
  *
5
5
  * Hand-written `__ModuleLoader__` factory (no build step), modeled on
6
6
  * @dsh-external/dsh-persona-ref: React comes from the shared module host, every
7
7
  * data operation goes through the loopback /subagent-profiles/* HTTP routes
8
- * served by the host half (index.mjs), and slots register through the shared
8
+ * served by the host side (index.mjs), and slots register through the shared
9
9
  * `slots` service. The card lives on the keyed `tool.call.toolview` seat (key
10
10
  * `dispatch`); the settings page lives on the `settings.section` list.
11
11
  *
@@ -16,526 +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 half 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),
152
+ }
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]
46
214
  }
47
- if (!data.ok) throw new Error(data.error || '请求失败')
48
- return data
215
+ out.continuable = parsed.continuable === true
216
+ out.runInBackground = parsed.run_in_background === true
49
217
  }
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-meta{flex-wrap:wrap;gap:6px 14px;display:flex}',
69
- '.sap-metaItem{display:inline-flex;align-items:baseline;gap:6px;font-size:12px;line-height:18px}',
70
- '.sap-metaLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}',
71
- '.sap-metaValue{color:var(--dsw-alias-label-primary)}',
72
- '.sap-metaValueDefault{color:var(--dsw-alias-label-tertiary)}',
73
- '.sap-addBlock{margin-top:4px}',
74
- '.sap-addActions{flex-wrap:wrap;gap:10px;display:flex}',
75
- '.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}',
76
- '.sap-addButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
77
- '.sap-addCard{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;display:flex}',
78
- '.sap-editorHeader{align-items:baseline;gap:8px;display:flex}',
79
- '.sap-editorTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}',
80
- '.sap-field{flex-direction:column;gap:6px;display:flex}',
81
- '.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}',
82
- '.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}',
83
- '.sap-input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}',
84
- '.sap-input::placeholder{color:var(--dsw-alias-label-dimmed)}',
85
- '.sap-select{appearance:auto}',
86
- '.sap-customized{border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px}',
87
- '.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}',
88
- '.sap-customizedSummary::-webkit-details-marker{display:none}',
89
- '.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)}',
90
- '.sap-customized[open]>.sap-customizedSummary:before{transform:rotate(45deg)translate(-1px,-1px)}',
91
- '.sap-customizedSummary:hover{color:var(--dsw-alias-label-primary)}',
92
- '.sap-customizedBody{flex-direction:column;gap:12px;padding-top:12px;display:flex}',
93
- '.sap-editorActions{justify-content:flex-end;gap:8px;display:flex}',
94
- '.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}',
95
- '.sap-primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}',
96
- '.sap-primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}',
97
- '.sap-secondaryButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0}',
98
- '.sap-secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
99
- '.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}',
100
- '.sap-dangerButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}',
101
- '.sap-primaryButton:disabled,.sap-secondaryButton:disabled,.sap-dangerButton:disabled,.sap-addButton:disabled{opacity:.4;cursor:default}',
102
- '.sap-titleRow{display:flex;align-items:center;justify-content:space-between;gap:12px}',
103
- '.sap-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);user-select:none}',
104
- '.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}',
105
- '.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)}',
106
- '.sap-switch input:checked{background:var(--dsw-alias-brand-primary)}',
107
- '.sap-switch input:checked::before{left:18px}',
108
- '.sap-toolview{display:flex;align-items:baseline;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);padding:4px 8px;white-space:pre-wrap;word-break:break-word}',
109
- '.sap-toolModes{flex-wrap:wrap;gap:4px 16px;display:flex}',
110
- '.sap-toolGroups{flex-direction:column;gap:8px;display:flex}',
111
- '.sap-toolLayerCard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px}',
112
- '.sap-toolLayerBody{flex-direction:column;gap:6px;padding-top:6px;display:flex}',
113
- '.sap-toolGroup{flex-direction:column;gap:4px;display:flex}',
114
- '.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}',
115
- '.sap-toolActions{flex-wrap:wrap;gap:6px;display:flex}',
116
- '.sap-toolList{flex-wrap:wrap;gap:2px 16px;display:flex}',
117
- '.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}',
118
- '.sap-toolItem input{margin:0;accent-color:var(--dsw-alias-brand-primary)}',
119
- ].join('\n')
120
-
121
- function ensureStyles() {
122
- const tagId = 'dsh-subagent-profile/ProfilesSection.css'
123
- if (document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') !== null) return
124
- const style = document.createElement('style')
125
- style.dataset.plugin = 'dsh-subagent-profile'
126
- style.dataset.pluginCss = tagId
127
- style.textContent = CSS
128
- document.head.appendChild(style)
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 !== '')
129
261
  }
130
-
131
- // ── dispatch tool-call card ─────────────────────────────────────────────
132
-
133
- function DispatchToolview(props) {
134
- const block = props && props.block
135
- let args = {}
136
- if (block && typeof block === 'object') {
137
- const done = 'kind' in block
138
- const argsRaw = (done ? (block.call && block.call.argsRaw) : block.argsRaw) ?? ''
139
- if (typeof argsRaw === 'string' && argsRaw !== '') {
140
- try {
141
- const parsed = JSON.parse(argsRaw)
142
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) args = parsed
143
- } catch {
144
- // unparsable argsRaw: fall through to the default field rendering
145
- }
146
- }
147
- }
148
- const field = (key, fallback) => {
149
- const value = args[key]
150
- return typeof value === 'string' && value !== '' ? value : fallback
151
- }
152
- const profile = field('profile', '内联')
153
- const preset = field('preset', '继承父')
154
- const provider = field('provider', '继承父')
155
- const model = field('model', '继承父')
156
- const rawEffort = field('reasoningEffort', '继承父')
157
- const reasoningEffort = EFFORT_ZH[rawEffort] ?? rawEffort
158
- const continuable = args.continuable === true
159
- const text = `派发子 Agent:方案=${profile} · 预设=${preset} · 提供方=${provider} · 模型=${model} · 推理强度=${reasoningEffort}${continuable ? ' · 可续跑' : ''}`
160
- return el('div', { className: 'sap-toolview' }, text)
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
161
278
  }
162
-
163
- // ── settings page ───────────────────────────────────────────────────────
164
-
165
- const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', toolMode: 'none', toolList: [] }
166
- const META_DEFS = [
167
- ['预设', 'preset'],
168
- ['提供方', 'provider'],
169
- ['模型', 'model'],
170
- ['推理强度', 'reasoningEffort'],
171
- ]
172
- const EFFORT_ZH = { off: '关闭', high: '高', max: '最大' }
173
- const effortLabel = (effort) => {
174
- const id = effort && effort.id
175
- return EFFORT_ZH[id] ? `${EFFORT_ZH[id]}(${id})` : (effort && effort.name) || id
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
176
317
  }
177
-
178
- function ProfilesSection() {
179
- const [profiles, setProfiles] = useState([])
180
- const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] })
181
- const [enabled, setEnabled] = useState(true)
182
- const [adding, setAdding] = useState(false)
183
- const [editingId, setEditingId] = useState(null)
184
- const [error, setError] = useState('')
185
- const [savedNotice, setSavedNotice] = useState('')
186
- const [form, setForm] = useState(EMPTY_FORM)
187
- const [tools, setTools] = useState([])
188
-
189
- const refresh = useCallback(() => {
190
- api('/list')
191
- .then((data) => setProfiles(Array.isArray(data.profiles) ? data.profiles : []))
192
- .catch((err) => setError(String((err && err.message) || err)))
193
- }, [])
194
-
195
- const loadOptions = useCallback(() => {
196
- api('/options')
197
- .then((data) => {
198
- setEnabled(data.enabled !== false)
199
- setOptions({
200
- models: Array.isArray(data.models) ? data.models : [],
201
- efforts: data.efforts && typeof data.efforts === 'object' ? data.efforts : {},
202
- presets: Array.isArray(data.presets) ? data.presets : []
203
- })
204
- setTools(Array.isArray(data.tools) ? data.tools : [])
205
- })
206
- .catch(() => {})
207
- }, [])
208
-
209
- useEffect(() => {
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
399
+ }
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}`)
210
506
  refresh()
211
- loadOptions()
212
- }, [refresh, loadOptions])
213
-
214
- const toggleEnabled = () => {
215
- const next = !enabled
216
- setEnabled(next)
217
- setError('')
218
- api('/set-enabled', { enabled: next })
219
- .catch((err) => {
220
- setEnabled(!next)
221
- setError(String((err && err.message) || err))
222
- })
223
- }
224
-
225
- const setField = (key) => (event) => {
226
- const value = event && event.target ? event.target.value : ''
227
- setForm((prev) => {
228
- const next = { ...prev, [key]: value }
229
- if (key === 'model') next.reasoningEffort = '' // 换模型时重置推理强度
230
- return next
231
- })
232
- }
233
-
234
- const setToolMode = (mode) => () => setForm((prev) => ({ ...prev, toolMode: mode }))
235
- const toggleTool = (name) => () => {
236
- setForm((prev) => {
237
- const cur = Array.isArray(prev.toolList) ? prev.toolList : []
238
- const next = cur.includes(name) ? cur.filter((n) => n !== name) : [...cur, name]
239
- return { ...prev, toolList: next }
240
- })
241
- }
242
- const selectLayer = (layer) => () => setForm((prev) => {
243
- const layerNames = tools.filter((t) => t.layer === layer).map((t) => t.name)
244
- return { ...prev, toolList: [...new Set([...(Array.isArray(prev.toolList) ? prev.toolList : []), ...layerNames])] }
245
507
  })
246
- const clearLayer = (layer) => () => setForm((prev) => {
247
- const layerSet = new Set(tools.filter((t) => t.layer === layer).map((t) => t.name))
248
- 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))
249
522
  })
250
- const invertLayer = (layer) => () => setForm((prev) => {
251
- const layerNames = tools.filter((t) => t.layer === layer).map((t) => t.name)
252
- const cur = Array.isArray(prev.toolList) ? prev.toolList : []
253
- const toAdd = layerNames.filter((n) => !cur.includes(n))
254
- const toRemove = new Set(layerNames.filter((n) => cur.includes(n)))
255
- 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()
256
544
  })
257
-
258
- const openAdd = () => {
259
- setError('')
260
- setSavedNotice('')
261
- setForm(EMPTY_FORM)
262
- setEditingId(null)
263
- setAdding(true)
264
- }
265
-
266
- const openEdit = (profile) => () => {
267
- setError(''); setSavedNotice('')
268
- const tf = profile.toolFilter && typeof profile.toolFilter === 'object' ? profile.toolFilter : {}
269
- const allowArr = Array.isArray(tf.allow) ? tf.allow.slice() : []
270
- const denyArr = Array.isArray(tf.deny) ? tf.deny.slice() : []
271
- const toolMode = allowArr.length > 0 ? 'allow' : (denyArr.length > 0 ? 'deny' : 'none')
272
- setForm({
273
- id: profile.id,
274
- description: typeof profile.description === 'string' ? profile.description : '',
275
- preset: typeof profile.preset === 'string' ? profile.preset : '',
276
- provider: typeof profile.provider === 'string' ? profile.provider : '',
277
- model: typeof profile.model === 'string' ? profile.model : '',
278
- reasoningEffort: typeof profile.reasoningEffort === 'string' ? profile.reasoningEffort : '',
279
- toolMode,
280
- toolList: toolMode === 'allow' ? allowArr : denyArr
281
- })
282
- setEditingId(profile.id)
283
- setAdding(true)
284
- }
285
-
286
- const closeAdd = () => {
287
- setAdding(false)
288
- setEditingId(null)
289
- setError('')
290
- setForm(EMPTY_FORM)
291
- }
292
-
293
- const submit = () => {
294
- const payload = { id: typeof form.id === 'string' ? form.id.trim() : '' }
295
- // 字符串字段总是发送(含空串):后端 merge 语义里空串 = 清除该字段,
296
- // 否则把字段清回“继承父/默认”会被静默忽略(保留旧值)。
297
- const add = (key, value) => { if (typeof value === 'string') payload[key] = value.trim() }
298
- add('description', form.description)
299
- add('preset', form.preset)
300
- add('provider', form.provider)
301
- add('model', form.model)
302
- add('reasoningEffort', form.reasoningEffort)
303
- const list = Array.isArray(form.toolList) ? form.toolList : []
304
- if (form.toolMode === 'allow' && list.length > 0) payload.toolFilter = { allow: list }
305
- else if (form.toolMode === 'deny' && list.length > 0) payload.toolFilter = { deny: list }
306
- else payload.toolFilter = {} // 不限制:空对象 = 清除(后端会 delete)
307
- if (typeof payload.id !== 'string' || payload.id === '') { setError('profile id 不能为空'); return }
308
- // 编辑:保留原 name 和 enabled 状态
309
- const existing = profiles.find((p) => p.id === form.id)
310
- if (existing) {
311
- if (typeof existing.name === 'string' && existing.name !== '') payload.name = existing.name
312
- if (existing.enabled === false) payload.enabled = false
313
- }
314
- api('/add', payload)
315
- .then(() => {
316
- setForm(EMPTY_FORM); setAdding(false); setEditingId(null); setError('')
317
- setSavedNotice(`已保存 ${payload.id}`)
318
- refresh()
319
- })
320
- .catch((err) => setError(String((err && err.message) || err)))
321
- }
322
-
323
- const setProfileEnabled = (profile, next) => () => {
324
- setError('')
325
- api('/set-profile-enabled', { id: profile.id, enabled: next })
326
- .then(() => refresh())
327
- .catch((err) => setError(String((err && err.message) || err)))
328
- }
329
-
330
- const resetAll = () => {
331
- if (!window.confirm('将所有内置方案重置为默认配置?已删除的内置也会恢复。')) return
332
- setError('')
333
- api('/reset-all')
334
- .then(() => { setSavedNotice('已重置所有内置方案'); refresh() })
335
- .catch((err) => setError(String((err && err.message) || err)))
336
- }
337
-
338
- const remove = (profile) => () => {
339
- if (!window.confirm(`删除方案「${profile.name || profile.id}」?${profile.builtin === true ? '内置方案删除后可在上方「重置内置方案」恢复' : ''}`)) return
340
- api('/remove', { id: profile.id })
341
- .then(() => {
342
- setError('')
343
- setSavedNotice(`已删除 ${profile.id}`)
344
- refresh()
345
- })
346
- .catch((err) => setError(String((err && err.message) || err)))
347
- }
348
-
349
- // Distinct providers, derived from the model list.
350
- const providers = []
351
- const seenProvider = new Set()
352
- for (const model of options.models) {
353
- if (seenProvider.has(model.provider)) continue
354
- seenProvider.add(model.provider)
355
- providers.push({ id: model.provider, name: model.providerName || model.provider })
356
- }
357
-
358
- const effortsForModel = form.model ? (options.efforts[form.model] ?? []) : []
359
-
360
- const rows = profiles.map((profile) => {
361
- const builtin = profile.builtin === true
362
- const isDisabled = profile.enabled === false
363
- const hasName = typeof profile.name === 'string' && profile.name !== '' && profile.name !== String(profile.id)
364
- const metaItems = META_DEFS.map(([label, key]) => {
365
- const value = profile[key]
366
- const has = typeof value === 'string' && value !== ''
367
- return el('span', { key: label, className: 'sap-metaItem' },
368
- el('span', { className: 'sap-metaLabel' }, `${label} `),
369
- el('span', { className: has ? 'sap-metaValue' : 'sap-metaValueDefault' }, has ? value : '继承父')
370
- )
371
- })
372
- return el('li', { key: String(profile.id), className: 'sap-rowCard' },
373
- el('div', { className: 'sap-rowHead' },
374
- el('span', { className: 'sap-rowIdentity' },
375
- el('span', { className: 'sap-rowName' }, hasName ? profile.name : String(profile.id)),
376
- hasName ? el('span', { className: 'sap-rowId' }, String(profile.id)) : null,
377
- builtin ? el('span', { className: 'sap-rowTag' }, '内置') : null,
378
- isDisabled ? el('span', { className: 'sap-rowTag' }, '已禁用') : null
379
- ),
380
- el('span', { className: 'sap-rowActions' },
381
- el('label', { className: 'sap-switch', title: '启用/禁用' },
382
- el('input', { type: 'checkbox', checked: !isDisabled, onChange: setProfileEnabled(profile, isDisabled) }),
383
- el('span', null, isDisabled ? '已禁用' : '已启用')
384
- ),
385
- el('button', { className: 'sap-secondaryButton', style: { height: '28px', padding: '0 10px', fontSize: '12px', borderRadius: '14px' }, onClick: openEdit(profile) }, '编辑'),
386
- el('button', { className: 'sap-dangerButton', onClick: remove(profile) }, '删除')
387
- )
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 ? '已禁用' : '已启用')
388
672
  ),
389
- typeof profile.description === 'string' && profile.description !== ''
390
- ? el('p', { className: 'sap-desc' }, profile.description)
391
- : null,
392
- el('div', { className: 'sap-meta' }, metaItems)
393
- )
394
- })
395
-
396
- const selectField = (label, key, optionsList, renderOption) => el('div', { className: 'sap-field' },
397
- el('span', { className: 'sap-fieldLabel' }, label),
398
- el('select', { className: 'sap-input sap-select', value: form[key], onChange: setField(key) },
399
- el('option', { value: '' }, '继承父(不指定)'),
400
- optionsList.map(renderOption)
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) }, '删除')
401
675
  )
402
- )
403
-
404
- const renderEffortOption = (effort) => el('option', { key: effort.id, value: effort.id }, effortLabel(effort))
405
-
406
- const smallBtn = { height: '24px', padding: '0 8px', fontSize: '12px', borderRadius: '12px' }
407
- const TOOL_MODES = [
408
- ['none', '不限制(继承父的全部工具,自动剔除 run_code)'],
409
- ['allow', '白名单(只允许勾选的工具)'],
410
- ['deny', '黑名单(排除勾选的工具)'],
411
- ]
412
- const CORE_ORDER = ['文件', '终端', '网络', '任务', '子 Agent', '工作流', '交互', '图片', 'Cordis', '计划']
413
- const LAYER_TITLES = { core: '核心预设', plugin: '第三方插件', custom: '自建预设' }
414
- const buildLayerGroups = (layer) => {
415
- const layerTools = tools.filter((t) => t.layer === layer)
416
- const groups = new Map()
417
- for (const t of layerTools) {
418
- const g = typeof t.group === 'string' && t.group !== '' ? t.group : '其他'
419
- if (!groups.has(g)) groups.set(g, [])
420
- groups.get(g).push(t)
421
- }
422
- const keys = layer === 'core'
423
- ? [...CORE_ORDER.filter((c) => groups.has(c)), ...new Set([...groups.keys()].filter((k) => !CORE_ORDER.includes(k)))]
424
- : [...groups.keys()]
425
- return keys.map((k) => [k, groups.get(k)])
426
- }
427
- const renderTool = (t) => {
428
- const checked = Array.isArray(form.toolList) && form.toolList.includes(t.name)
429
- return el('label', { key: t.name, className: 'sap-toolItem' },
430
- el('input', { type: 'checkbox', checked, onChange: toggleTool(t.name) }),
431
- el('span', null, t.zh ? `${t.zh} ${t.name}` : t.name)
432
- )
433
- }
434
- const toolSection = el('div', { className: 'sap-field' },
435
- el('span', { className: 'sap-fieldLabel' }, '工具限制'),
436
- el('div', { className: 'sap-toolModes' },
437
- TOOL_MODES.map(([mode, label]) => el('label', { key: mode, className: 'sap-toolItem' },
438
- el('input', { type: 'radio', name: 'sap-toolMode', checked: form.toolMode === mode, onChange: setToolMode(mode) }),
439
- el('span', null, label)
440
- ))
441
- ),
442
- form.toolMode === 'none'
443
- ? null
444
- : (tools.length === 0
445
- ? el('span', { className: 'sap-metaValueDefault' }, '(无可用工具列表)')
446
- : el('div', { className: 'sap-toolGroups' },
447
- ['core', 'plugin', 'custom']
448
- .filter((layer) => tools.some((t) => t.layer === layer))
449
- .map((layer) => {
450
- const groups = buildLayerGroups(layer)
451
- const layerTools = tools.filter((t) => t.layer === layer)
452
- const count = layerTools.length
453
- const selectedCount = layerTools.filter((t) => Array.isArray(form.toolList) && form.toolList.includes(t.name)).length
454
- const summaryText = selectedCount > 0
455
- ? `${LAYER_TITLES[layer]}(${count})· 已选 ${selectedCount}`
456
- : `${LAYER_TITLES[layer]}(${count})`
457
- return el('details', { key: layer, className: 'sap-toolLayerCard' },
458
- el('summary', { className: 'sap-customizedSummary' }, summaryText),
459
- el('div', { className: 'sap-toolLayerBody' },
460
- el('div', { className: 'sap-toolActions', style: { marginTop: '2px' } },
461
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: selectLayer(layer) }, '全选'),
462
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: clearLayer(layer) }, '清空'),
463
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: invertLayer(layer) }, '反选')
464
- ),
465
- groups.map(([g, list]) => el('div', { key: g, className: 'sap-toolGroup' },
466
- el('div', { className: 'sap-toolGroupTitle' }, `${g}(${list.length})`),
467
- el('div', { className: 'sap-toolList' }, list.map(renderTool))
468
- ))
469
- )
470
- )
471
- })
472
- )
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
+ )
473
716
  )
717
+ : buildSelectField(el, s, actions.setField, '推理强度(思考深度)', 'reasoningEffort', effortsForModel, renderEffortOption),
718
+ toolSection
474
719
  )
475
-
476
- return el('div', { className: 'sap-section' },
477
- el('div', { className: 'sap-titleRow' },
478
- el('h2', { className: 'sap-title' }, '子 Agent 方案'),
479
- el('label', { className: 'sap-switch' },
480
- el('input', { type: 'checkbox', checked: enabled, onChange: toggleEnabled }),
481
- el('span', null, enabled ? '已启用' : '已禁用')
482
- )
483
- ),
484
- el('p', { className: 'sap-intro' }, '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可点下方按钮批量重置回默认;每个方案可单独启用/禁用。'),
485
- el('div', { className: 'sap-addActions', style: { margin: '4px 0' } },
486
- el('button', { type: 'button', className: 'sap-secondaryButton', style: { height: '28px', padding: '0 12px', fontSize: '12px', borderRadius: '14px' }, onClick: resetAll }, '重置所有内置方案')
487
- ),
488
- !enabled ? el('p', { className: 'sap-notice' }, '插件已禁用:「派发子 Agent」工具已从模型工具列表中移除,打开开关即可恢复。') : null,
489
- error !== '' ? el('p', { className: 'sap-notice' }, error) : null,
490
- savedNotice !== '' ? el('p', { className: 'sap-savedNotice', role: 'status' }, savedNotice) : null,
491
- el('ul', { className: 'sap-rows' }, rows),
492
- el('div', { className: 'sap-addBlock' },
493
- adding
494
- ? el('div', { className: 'sap-addCard' },
495
- el('div', { className: 'sap-editorHeader' },
496
- el('span', { className: 'sap-editorTitle' }, editingId !== null ? '编辑 profile' : '新增 profile')
497
- ),
498
- el('div', { className: 'sap-field' },
499
- el('span', { className: 'sap-fieldLabel' }, 'profile id(唯一标识,必填)'),
500
- el('input', { className: 'sap-input', value: form.id, placeholder: '小写字母/数字/连字符,如 my-focus', disabled: editingId !== null, onChange: setField('id') })
501
- ),
502
- el('div', { className: 'sap-field' },
503
- el('span', { className: 'sap-fieldLabel' }, '描述(一句话定位)'),
504
- el('input', { className: 'sap-input', value: form.description, placeholder: '说明这个 profile 适合什么场景,帮助模型选择', onChange: setField('description') })
505
- ),
506
- selectField('模型(子 agent 使用的模型)', 'model', options.models,
507
- (m) => el('option', { key: m.id, value: m.id }, m.name && m.name !== m.id ? `${m.name}(${m.providerName || m.provider})` : m.id)),
508
- el('details', { className: 'sap-customized' },
509
- el('summary', { className: 'sap-customizedSummary' }, '高级选项'),
510
- el('div', { className: 'sap-customizedBody' },
511
- selectField('目标预设(切换子 agent 使用的 Agent 预设)', 'preset', options.presets,
512
- (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
513
- selectField('提供方(模型服务商)', 'provider', providers,
514
- (p) => el('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? `${p.name}(${p.id})` : p.id)),
515
- form.model === ''
516
- ? el('div', { className: 'sap-field' },
517
- el('span', { className: 'sap-fieldLabel' }, '推理强度(思考深度)'),
518
- el('select', { className: 'sap-input sap-select', value: '', disabled: true },
519
- el('option', { value: '' }, '先选择模型')
520
- )
521
- )
522
- : selectField('推理强度(思考深度)', 'reasoningEffort', effortsForModel, renderEffortOption),
523
- toolSection
524
- )
525
- ),
526
- el('div', { className: 'sap-editorActions' },
527
- el('button', { className: 'sap-secondaryButton', onClick: closeAdd }, '取消'),
528
- el('button', { className: 'sap-primaryButton', onClick: submit }, '保存')
529
- )
530
- )
531
- : el('div', { className: 'sap-addActions' },
532
- el('button', { className: 'sap-addButton', onClick: openAdd }, '+ 新增 profile')
533
- )
534
- )
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 ? '已启用' : '已禁用')
535
735
  )
536
- }
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']
537
810
 
538
- // ── plugin ──────────────────────────────────────────────────────────────
811
+ const chip = makeChip(el)
812
+ const DispatchToolview = makeDispatchToolview(el, chip)
813
+ const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback)
539
814
 
540
815
  function apply(ctx) {
541
816
  ensureStyles()