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