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/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
+ }
@@ -0,0 +1,3 @@
1
+ agent.cordis.yml 改编自 DeepSeek Harness 内置 standard preset(MIT)。
2
+ 仅 persona 段改写为「主协调 Agent」身份;其余行与原 standard 预设一致。
3
+ 原始 DeepSeek 版权与 MIT 许可声明见开源仓库。