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