dsh-subagent-profile 0.2.0 → 0.3.1

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