dsh-plugin-admin 0.4.3 → 0.5.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 +15 -5
- package/lib/client.js +1531 -5
- package/lib/index.js +17 -2
- package/lib/subagent-admin.js +1878 -0
- package/lib/tool-seed.js +67 -0
- package/package.json +6 -3
package/lib/client.js
CHANGED
|
@@ -2278,12 +2278,1529 @@ function renderSessionCard(session, view, act, patch) {
|
|
|
2278
2278
|
return createElement('div', { key: session.id, className: 'card' }, cardChildren)
|
|
2279
2279
|
}
|
|
2280
2280
|
|
|
2281
|
+
/* ========================================================================== */
|
|
2282
|
+
/* Subagent Administration (merged from dsh-plugin-subagents) */
|
|
2283
|
+
/* ========================================================================== */
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
* dsh-plugin-subagents browser half: the 子智能体管理 settings section.
|
|
2287
|
+
*
|
|
2288
|
+
* Renders a management panel over the shell's `settings.section` slot (same
|
|
2289
|
+
* platform-shared React and --dsw-* design tokens as the admin plugin) and
|
|
2290
|
+
* drives the host's `subagentAdmin` remote over the /api RPC gateway. One
|
|
2291
|
+
* managed row in the profile cordis.patch.yml equals one named subagent: a
|
|
2292
|
+
* `@deepseek-ai/dsh-tool-subagent` instance whose config carries the agent
|
|
2293
|
+
* name (toolName), the persona prompt, the tool constraint lists, and the
|
|
2294
|
+
* model specification (agentOptions).
|
|
2295
|
+
*/
|
|
2296
|
+
|
|
2297
|
+
var TOOLNAME_RE = /^[a-z][a-z0-9_]{1,48}$/
|
|
2298
|
+
var TOOL_REF_RE = /^[a-z][a-z0-9_]*$/
|
|
2299
|
+
var ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/
|
|
2300
|
+
var RESERVED_TOOL_NAMES = ['subagent', 'subagent_fork', 'run_code']
|
|
2301
|
+
|
|
2302
|
+
var SA_CSS_TEXT = [
|
|
2303
|
+
'@keyframes dsh-sa-spin { to { transform: rotate(360deg); } }',
|
|
2304
|
+
'[data-dsh-sa-section] { display: flex; flex-direction: column; width: 100%; gap: 14px; padding: 2px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary, #222); overflow: visible; }',
|
|
2305
|
+
'[data-dsh-sa-section] * { box-sizing: border-box; }',
|
|
2306
|
+
'[data-dsh-sa-section] .tabs { display: flex; padding: 3px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.2)); border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.3)); gap: 3px; }',
|
|
2307
|
+
'[data-dsh-sa-section] .tab { flex: 1; border: 0; background: transparent; border-radius: 7px; padding: 7px 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 7px; font-size: 12px; font-weight: 500; color: var(--dsw-alias-label-secondary, #666); transition: all 0.15s ease; font-family: inherit; }',
|
|
2308
|
+
'[data-dsh-sa-section] .tab:hover:not(.active) { color: var(--dsw-alias-label-primary, #222); background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); }',
|
|
2309
|
+
'[data-dsh-sa-section] .tab.active { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); color: var(--dsw-alias-label-primary, #333); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); }',
|
|
2310
|
+
'[data-dsh-sa-section] .tab-count { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-alias-label-secondary, #555); }',
|
|
2311
|
+
'[data-dsh-sa-section] .tab.active .tab-count { background: var(--dsw-static-blue-500, #3b82f6); color: #fff; }',
|
|
2312
|
+
'[data-dsh-sa-section] .toolbar { display: flex; gap: 8px; align-items: center; padding: 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 12px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); flex-wrap: wrap; }',
|
|
2313
|
+
'[data-dsh-sa-section] .filterbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 12px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); }',
|
|
2314
|
+
'[data-dsh-sa-section] .filter-label { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #666); flex: none; margin-right: 2px; }',
|
|
2315
|
+
'[data-dsh-sa-section] .search-wrap { flex: 1; min-width: 150px; position: relative; display: flex; align-items: center; height: 32px; }',
|
|
2316
|
+
'[data-dsh-sa-section] .search-wrap .input { flex: 1; min-width: 0; padding-left: 30px; font-size: 13px; height: 32px; }',
|
|
2317
|
+
'[data-dsh-sa-section] .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 12px; opacity: 0.7; pointer-events: none; z-index: 1; }',
|
|
2318
|
+
'[data-dsh-sa-section] .input { width: 100%; padding: 7px 12px; border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); background: var(--dsw-alias-bg-base, transparent); color: inherit; font: inherit; outline: none; transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; }',
|
|
2319
|
+
'[data-dsh-sa-section] .input:hover { border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.6)); }',
|
|
2320
|
+
'[data-dsh-sa-section] .input:focus { border-color: var(--dsw-static-blue-500, #3b82f6); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16); }',
|
|
2321
|
+
'[data-dsh-sa-section] textarea.input { resize: vertical; min-height: 80px; font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 12px; line-height: 1.55; }',
|
|
2322
|
+
'[data-dsh-sa-section] select.input { height: 32px; padding: 0 8px; cursor: pointer; }',
|
|
2323
|
+
'[data-dsh-sa-section] .btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; height: 32px; padding: 0 14px; border-radius: 9px; font-size: 13px; font-weight: 500; cursor: pointer; font-family: inherit; transition: all 0.15s ease; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); background: var(--dsw-alias-interactive-bg-hover, rgba(240,240,245,0.6)); color: var(--dsw-alias-label-secondary, #555); white-space: nowrap; flex: none; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }',
|
|
2324
|
+
'[data-dsh-sa-section] .btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(220,220,230,0.7)); border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.65)); color: var(--dsw-alias-label-primary, #222); }',
|
|
2325
|
+
'[data-dsh-sa-section] .btn:active:not(:disabled) { transform: translateY(1px); }',
|
|
2326
|
+
'[data-dsh-sa-section] .btn:focus-visible { outline: 2px solid var(--dsw-static-blue-500, #3b82f6); outline-offset: 2px; }',
|
|
2327
|
+
'[data-dsh-sa-section] .btn:disabled { opacity: 0.45; cursor: not-allowed; }',
|
|
2328
|
+
'[data-dsh-sa-section] .btn.primary { background: var(--dsw-static-blue-500, #3b82f6); color: #fff; border-color: transparent; font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }',
|
|
2329
|
+
'[data-dsh-sa-section] .btn.primary:hover:not(:disabled) { background: var(--dsw-static-blue-400, #60a5fa); color: #fff; }',
|
|
2330
|
+
'[data-dsh-sa-section] .btn.danger { color: var(--dsw-alias-state-error-primary, #dc2626); border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); }',
|
|
2331
|
+
'[data-dsh-sa-section] .btn.danger:hover:not(:disabled) { background: rgba(239,68,68,0.1); border-color: rgba(220,38,38,0.35); color: #b91c1c; }',
|
|
2332
|
+
'[data-dsh-sa-section] .btn.danger-solid { background: var(--dsw-alias-state-error-primary, #dc2626); color: #fff; border-color: transparent; }',
|
|
2333
|
+
'[data-dsh-sa-section] .btn.sm { height: 28px; padding: 0 10px; font-size: 12px; border-radius: 6px; }',
|
|
2334
|
+
'[data-dsh-sa-section] .btn.sm.active { background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-static-blue-500, #3b82f6); font-weight: 600; border-color: currentColor; }',
|
|
2335
|
+
'[data-dsh-sa-section] .spinner { width: 12px; height: 12px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: dsh-sa-spin 0.7s linear infinite; display: inline-block; flex: none; }',
|
|
2336
|
+
'[data-dsh-sa-section] .list { display: flex; flex-direction: column; gap: 10px; flex: 1 1 auto; min-height: 0; max-height: 560px; overflow-y: auto; padding: 2px 4px 2px 2px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)) transparent; }',
|
|
2337
|
+
'[data-dsh-sa-section] .card { display: flex; flex-direction: column; gap: 7px; padding: 12px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); transition: all 0.15s ease; }',
|
|
2338
|
+
'[data-dsh-sa-section] .card:hover { transform: translateY(-1px); border-color: var(--dsw-alias-label-tertiary, rgba(180,180,195,0.6)); box-shadow: 0 8px 20px rgba(0,0,0,0.06); }',
|
|
2339
|
+
'[data-dsh-sa-section] .card-header { display: flex; align-items: center; gap: 8px; min-width: 0; }',
|
|
2340
|
+
'[data-dsh-sa-section] .card-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); display: flex; align-items: center; gap: 7px; min-width: 0; flex: 1; font-family: var(--dsw-font-mono, ui-monospace, monospace); }',
|
|
2341
|
+
'[data-dsh-sa-section] .card-actions { display: flex; gap: 5px; align-items: center; flex: none; }',
|
|
2342
|
+
'[data-dsh-sa-section] .persona-box { display: flex; align-items: flex-start; gap: 6px; padding: 6px 9px; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); border-left: 2px solid var(--dsw-static-blue-500, #3b82f6); font-size: 12px; line-height: 1.45; color: var(--dsw-alias-label-secondary, #555); }',
|
|
2343
|
+
'[data-dsh-sa-section] .persona-text { flex: 1; min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; word-break: break-word; white-space: pre-wrap; }',
|
|
2344
|
+
'[data-dsh-sa-section] .card-sub { font-size: 11px; color: var(--dsw-alias-label-secondary, #666); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }',
|
|
2345
|
+
'[data-dsh-sa-section] .tag { font-size: 10px; font-weight: 500; padding: 1px 6px; border-radius: 4px; flex: none; display: inline-flex; align-items: center; gap: 4px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); color: var(--dsw-alias-label-secondary, #555); }',
|
|
2346
|
+
'[data-dsh-sa-section] .tag.provider { background: rgba(139, 92, 246, 0.12); color: #7c3aed; }',
|
|
2347
|
+
'[data-dsh-sa-section] .tag.model { background: rgba(59, 130, 246, 0.12); color: var(--dsw-static-blue-500, #3b82f6); }',
|
|
2348
|
+
'[data-dsh-sa-section] .tag.live { background: rgba(16, 185, 129, 0.12); color: var(--dsw-alias-state-success-primary, #10b981); }',
|
|
2349
|
+
'[data-dsh-sa-section] .tag.warn { background: rgba(245, 158, 11, 0.14); color: #d97706; font-weight: 600; }',
|
|
2350
|
+
'[data-dsh-sa-section] .tag.dead { background: rgba(239, 68, 68, 0.12); color: var(--dsw-alias-state-error-primary, #dc2626); }',
|
|
2351
|
+
'[data-dsh-sa-section] .chips { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }',
|
|
2352
|
+
'[data-dsh-sa-section] .chip { font-size: 10px; font-family: var(--dsw-font-mono, ui-monospace, monospace); padding: 1px 6px; border-radius: 4px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); color: var(--dsw-alias-label-secondary, #555); }',
|
|
2353
|
+
'[data-dsh-sa-section] .chip.deny { background: rgba(239, 68, 68, 0.08); border-color: rgba(220,38,38,0.25); color: #b91c1c; }',
|
|
2354
|
+
'[data-dsh-sa-section] .chip.allow { background: rgba(16, 185, 129, 0.08); border-color: rgba(16,185,129,0.3); color: #047857; }',
|
|
2355
|
+
'[data-dsh-sa-section] .form { flex: 1 1 auto; min-height: 0; max-height: calc(100vh - 220px); overflow-y: auto; display: flex; flex-direction: column; gap: 10px; padding: 14px; border-radius: 12px; border: 1px solid var(--dsw-static-blue-500, #3b82f6); background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 4px 14px rgba(59,130,246,0.08); }',
|
|
2356
|
+
'[data-dsh-sa-section] .form-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); display: flex; align-items: center; gap: 6px; }',
|
|
2357
|
+
'[data-dsh-sa-section] .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }',
|
|
2358
|
+
'[data-dsh-sa-section] .form-grid .full { grid-column: 1 / -1; }',
|
|
2359
|
+
'[data-dsh-sa-section] .field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }',
|
|
2360
|
+
'[data-dsh-sa-section] .field-label { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary, #666); display: flex; align-items: center; gap: 5px; }',
|
|
2361
|
+
'[data-dsh-sa-section] .field-hint { font-size: 10px; color: var(--dsw-alias-label-tertiary, #999); line-height: 1.4; }',
|
|
2362
|
+
'[data-dsh-sa-section] .fieldset { border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 10px; padding: 10px; display: flex; flex-direction: column; gap: 8px; }',
|
|
2363
|
+
'[data-dsh-sa-section] .fieldset-legend { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary, #666); padding: 0 4px; }',
|
|
2364
|
+
'[data-dsh-sa-section] .tag-input { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 8px; border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); background: var(--dsw-alias-bg-base, transparent); min-height: 34px; align-items: center; cursor: text; }',
|
|
2365
|
+
'[data-dsh-sa-section] .tag-input:focus-within { border-color: var(--dsw-static-blue-500, #3b82f6); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16); }',
|
|
2366
|
+
'[data-dsh-sa-section] .tag-input input { border: 0; outline: none; background: transparent; color: inherit; font: inherit; font-size: 12px; flex: 1; min-width: 90px; }',
|
|
2367
|
+
'[data-dsh-sa-section] .tag-chip { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-family: var(--dsw-font-mono, ui-monospace, monospace); padding: 2px 4px 2px 8px; border-radius: 5px; }',
|
|
2368
|
+
'[data-dsh-sa-section] .tag-chip.allow { background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16,185,129,0.3); color: #047857; }',
|
|
2369
|
+
'[data-dsh-sa-section] .tag-chip.deny { background: rgba(239, 68, 68, 0.08); border: 1px solid rgba(220,38,38,0.25); color: #b91c1c; }',
|
|
2370
|
+
'[data-dsh-sa-section] .tag-chip button { border: 0; background: transparent; cursor: pointer; color: inherit; opacity: 0.65; font-size: 11px; padding: 0 3px; line-height: 1; }',
|
|
2371
|
+
'[data-dsh-sa-section] .tag-chip button:hover { opacity: 1; }',
|
|
2372
|
+
'[data-dsh-sa-section] .checkbox-row { display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--dsw-alias-label-secondary, #555); cursor: pointer; }',
|
|
2373
|
+
'[data-dsh-sa-section] .checkbox-row input { accent-color: var(--dsw-static-blue-500, #3b82f6); }',
|
|
2374
|
+
'[data-dsh-sa-section] .env-row { display: flex; gap: 6px; align-items: center; }',
|
|
2375
|
+
'[data-dsh-sa-section] .env-row .env-key { flex: 2; min-width: 0; }',
|
|
2376
|
+
'[data-dsh-sa-section] .env-row .env-value { flex: 3; min-width: 0; }',
|
|
2377
|
+
'[data-dsh-sa-section] .cli-toggle { border: 0; background: transparent; cursor: pointer; color: var(--dsw-alias-label-secondary, #555); font-size: 12px; line-height: 1; padding: 3px 6px; border-radius: 6px; }',
|
|
2378
|
+
'[data-dsh-sa-section] .cli-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); color: var(--dsw-alias-label-primary, #222); }',
|
|
2379
|
+
'[data-dsh-sa-section] .cli-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary, #222); display: inline-flex; align-items: center; flex: none; white-space: nowrap; font-family: var(--dsw-font-mono, ui-monospace, monospace); cursor: pointer; }',
|
|
2380
|
+
'[data-dsh-sa-section] .cli-tags { display: flex; flex-wrap: wrap; gap: 4px; flex: 1; min-width: 0; align-items: center; }',
|
|
2381
|
+
'[data-dsh-sa-section] .cli-detail { display: flex; flex-direction: column; gap: 8px; }',
|
|
2382
|
+
'[data-dsh-sa-section] .cli-scan-list { display: flex; flex-direction: column; gap: 6px; }',
|
|
2383
|
+
'[data-dsh-sa-section] .cli-scan-row { display: flex; gap: 8px; align-items: center; }',
|
|
2384
|
+
'[data-dsh-sa-section] .cli-scan-row .input { flex: 1; min-width: 0; }',
|
|
2385
|
+
'[data-dsh-sa-section] .cli-scan-hint { font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-secondary, #666); }',
|
|
2386
|
+
'[data-dsh-sa-section] .cli-scan-card { display: flex; gap: 8px; align-items: center; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.12)); }',
|
|
2387
|
+
'[data-dsh-sa-section] .cli-scan-card .btn { margin-left: auto; flex: none; }',
|
|
2388
|
+
'[data-dsh-sa-section] .error-strip { display: flex; align-items: flex-start; gap: 8px; padding: 9px 12px; border-radius: 9px; border: 1px solid rgba(220,38,38,0.35); background: rgba(239,68,68,0.08); color: var(--dsw-alias-state-error-primary, #dc2626); font-size: 12px; line-height: 1.45; word-break: break-word; }',
|
|
2389
|
+
'[data-dsh-sa-section] .warn-strip { display: flex; flex-direction: column; gap: 4px; padding: 9px 12px; border-radius: 9px; border: 1px solid rgba(217,119,6,0.35); background: rgba(245,158,11,0.08); color: #b45309; font-size: 12px; line-height: 1.5; word-break: break-word; }',
|
|
2390
|
+
'[data-dsh-sa-section] .warn-strip div { display: flex; gap: 6px; align-items: flex-start; }',
|
|
2391
|
+
'[data-dsh-sa-section] .empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 34px 12px; border: 1px dashed var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); border-radius: 12px; color: var(--dsw-alias-label-tertiary, #999); font-size: 12px; text-align: center; }',
|
|
2392
|
+
'[data-dsh-sa-section] .empty .big { font-size: 26px; }',
|
|
2393
|
+
'[data-dsh-sa-section] .toast { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); z-index: 10000; max-width: 520px; padding: 10px 16px; border-radius: 10px; font-size: 12px; line-height: 1.5; box-shadow: 0 8px 24px rgba(0,0,0,0.18); background: rgba(239,68,68,0.96); color: #fff; word-break: break-word; white-space: pre-line; }',
|
|
2394
|
+
'[data-dsh-sa-section] .history-row { display: flex; flex-direction: column; gap: 3px; padding: 9px 12px; border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); }',
|
|
2395
|
+
'[data-dsh-sa-section] .history-head { display: flex; align-items: center; gap: 8px; font-size: 12px; flex-wrap: wrap; }',
|
|
2396
|
+
'[data-dsh-sa-section] .history-time { font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 10px; color: var(--dsw-alias-label-tertiary, #999); margin-left: auto; }',
|
|
2397
|
+
'[data-dsh-sa-section] .history-detail { font-size: 11px; color: var(--dsw-alias-label-tertiary, #888); font-family: var(--dsw-font-mono, ui-monospace, monospace); word-break: break-all; max-height: 60px; overflow: hidden; }',
|
|
2398
|
+
'[data-dsh-sa-section] .footer-note { font-size: 10px; color: var(--dsw-alias-label-tertiary, #999); line-height: 1.5; padding: 0 2px; }',
|
|
2399
|
+
'[data-dsh-sa-section] .sa-picker { position: relative; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }',
|
|
2400
|
+
'[data-dsh-sa-section] .sa-picker-input { border: 0; outline: none; background: transparent; color: inherit; font: inherit; font-size: 12px; flex: 1; min-width: 120px; padding: 2px 4px; }',
|
|
2401
|
+
'[data-dsh-sa-section] .sa-picker-list { width: 100%; margin-top: 4px; max-height: 220px; overflow-y: auto; background: var(--dsh-alias-bg-elevated, #fff); border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); border-radius: 8px; box-shadow: 0 6px 18px rgba(0,0,0,0.14); padding: 3px; scrollbar-width: thin; }',
|
|
2402
|
+
'[data-dsh-sa-section] .sa-picker-opt { padding: 6px 9px; font-size: 12px; cursor: pointer; border-radius: 5px; color: var(--dsw-alias-label-primary, #222); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }',
|
|
2403
|
+
'[data-dsh-sa-section] .sa-picker-opt.active, [data-dsh-sa-section] .sa-picker-opt:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(59,130,246,0.14)); color: var(--dsw-static-blue-500, #3b82f6); }',
|
|
2404
|
+
'[data-dsh-sa-section] .sa-picker-empty { padding: 6px 9px; font-size: 11px; color: var(--dsw-alias-label-tertiary, #999); }',
|
|
2405
|
+
]
|
|
2406
|
+
|
|
2407
|
+
var SA_STYLE_SEEN = false
|
|
2408
|
+
function injectSaStyles() {
|
|
2409
|
+
if (SA_STYLE_SEEN) return
|
|
2410
|
+
var style = document.createElement('style')
|
|
2411
|
+
style.setAttribute('data-dsh-sa-styles', '')
|
|
2412
|
+
style.textContent = SA_CSS_TEXT.join('\n')
|
|
2413
|
+
document.head.appendChild(style)
|
|
2414
|
+
SA_STYLE_SEEN = true
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
/** The gateway wraps every remote result in an { ok, value | error } envelope. */
|
|
2418
|
+
function unwrap(result) {
|
|
2419
|
+
if (result && typeof result === 'object' && 'ok' in result) {
|
|
2420
|
+
if (result.ok) return result.value
|
|
2421
|
+
var detail = result.error && result.error.message ? result.error.message : String(result.error)
|
|
2422
|
+
throw new Error(detail)
|
|
2423
|
+
}
|
|
2424
|
+
return result
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
/* ========================================================================== */
|
|
2428
|
+
/* Small components */
|
|
2429
|
+
/* ========================================================================== */
|
|
2430
|
+
|
|
2431
|
+
function Spinner() {
|
|
2432
|
+
return createElement('span', { className: 'spinner' })
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
/** Two-click inline confirm; disarms after 3.2s. */
|
|
2436
|
+
function ConfirmButton(props) {
|
|
2437
|
+
var armed = useState(false)
|
|
2438
|
+
var isArmed = armed[0]
|
|
2439
|
+
var setArmed = armed[1]
|
|
2440
|
+
var timer = useRef(null)
|
|
2441
|
+
useEffect(function () {
|
|
2442
|
+
return function () { if (timer.current) clearTimeout(timer.current) }
|
|
2443
|
+
}, [])
|
|
2444
|
+
var label = isArmed ? (props.confirmLabel || '确认?') : props.label
|
|
2445
|
+
return createElement('button', {
|
|
2446
|
+
type: 'button',
|
|
2447
|
+
className: 'btn sm ' + (isArmed ? 'danger-solid' : 'danger'),
|
|
2448
|
+
disabled: props.disabled === true,
|
|
2449
|
+
onClick: function () {
|
|
2450
|
+
if (!isArmed) {
|
|
2451
|
+
setArmed(true)
|
|
2452
|
+
timer.current = setTimeout(function () { setArmed(false) }, 3200)
|
|
2453
|
+
return
|
|
2454
|
+
}
|
|
2455
|
+
if (timer.current) clearTimeout(timer.current)
|
|
2456
|
+
setArmed(false)
|
|
2457
|
+
props.onConfirm()
|
|
2458
|
+
},
|
|
2459
|
+
}, label)
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
/* ========================================================================== */
|
|
2463
|
+
/* Form draft handling */
|
|
2464
|
+
/* ========================================================================== */
|
|
2465
|
+
|
|
2466
|
+
function emptyDraft() {
|
|
2467
|
+
return {
|
|
2468
|
+
id: '',
|
|
2469
|
+
toolName: '',
|
|
2470
|
+
provider: 'spawn',
|
|
2471
|
+
persona: '',
|
|
2472
|
+
allow: [],
|
|
2473
|
+
deny: [],
|
|
2474
|
+
agentProvider: '',
|
|
2475
|
+
agentModel: '',
|
|
2476
|
+
maxTokens: '',
|
|
2477
|
+
maxDepthManaged: false,
|
|
2478
|
+
maxDepth: '3',
|
|
2479
|
+
backgroundMode: 'one-shot',
|
|
2480
|
+
enableRunInBackground: true,
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
function draftFromEntry(entry) {
|
|
2485
|
+
var config = entry.config || {}
|
|
2486
|
+
return {
|
|
2487
|
+
id: entry.id,
|
|
2488
|
+
toolName: config.toolName || '',
|
|
2489
|
+
provider: config.provider || 'spawn',
|
|
2490
|
+
persona: config.persona || '',
|
|
2491
|
+
allow: (config.toolFilter && config.toolFilter.allow) || [],
|
|
2492
|
+
deny: (config.toolFilter && config.toolFilter.deny) || [],
|
|
2493
|
+
agentProvider: (config.agentOptions && config.agentOptions.provider) || '',
|
|
2494
|
+
agentModel: (config.agentOptions && config.agentOptions.model) || '',
|
|
2495
|
+
maxTokens: config.agentOptions && config.agentOptions.maxTokens !== undefined ? String(config.agentOptions.maxTokens) : '',
|
|
2496
|
+
maxDepthManaged: config.maxDepth === 'provider-managed',
|
|
2497
|
+
maxDepth: typeof config.maxDepth === 'number' ? String(config.maxDepth) : '3',
|
|
2498
|
+
backgroundMode: config.backgroundMode || 'one-shot',
|
|
2499
|
+
enableRunInBackground: config.enableRunInBackground !== false,
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/** Client-side validation mirroring the host rules; returns an error string or null. */
|
|
2504
|
+
function validateDraft(draft, isCreate, otherToolNames, providerMeta, candidateNames) {
|
|
2505
|
+
if (isCreate && !ID_RE.test(draft.id)) return '实例 ID 只能包含字母、数字、下划线和中划线(字母或数字开头,最长 64 位)'
|
|
2506
|
+
if (!TOOLNAME_RE.test(draft.toolName)) return '子智能体名称必须是 2-48 位小写字母/数字/下划线且字母开头'
|
|
2507
|
+
if (RESERVED_TOOL_NAMES.indexOf(draft.toolName) !== -1) return '子智能体名称 "' + draft.toolName + '" 是保留名(内置预设已占用)'
|
|
2508
|
+
if (otherToolNames.indexOf(draft.toolName) !== -1) return '子智能体名称 "' + draft.toolName + '" 已被其他实例使用'
|
|
2509
|
+
var meta = providerMeta[draft.provider]
|
|
2510
|
+
if (!meta) return '未知的执行后端 "' + draft.provider + '"'
|
|
2511
|
+
if (draft.persona !== '' && meta.capabilities.persona === false) return '后端 "' + draft.provider + '" 不支持 persona(提示词)'
|
|
2512
|
+
if ((draft.allow.length > 0 || draft.deny.length > 0) && meta.capabilities.toolFilter === false) return '后端 "' + draft.provider + '" 不支持 toolFilter(工具约束)'
|
|
2513
|
+
if (!draft.maxDepthManaged) {
|
|
2514
|
+
if (draft.maxDepth !== '' && !/^\d+$/.test(draft.maxDepth)) return '最大委托深度必须是非负整数'
|
|
2515
|
+
if (draft.maxDepth !== '' && meta.capabilities.depthLimit === false) {
|
|
2516
|
+
return '后端 "' + draft.provider + '" 无法执行数值 maxDepth;请勾选「交由后端管理」或换后端'
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
if (draft.maxTokens !== '' && !/^\d+$/.test(draft.maxTokens)) return 'maxTokens 必须是正整数'
|
|
2520
|
+
var filterNames = (draft.allow || []).concat(draft.deny || [])
|
|
2521
|
+
for (var fi = 0; fi < filterNames.length; fi++) {
|
|
2522
|
+
var tname = filterNames[fi]
|
|
2523
|
+
if (RESERVED_TOOL_NAMES.indexOf(tname) !== -1) return '工具 "' + tname + '" 是保留名(' + RESERVED_TOOL_NAMES.join('/') + '),不能用于工具约束'
|
|
2524
|
+
if (candidateNames && candidateNames.length > 0 && candidateNames.indexOf(tname) === -1) return '工具 "' + tname + '" 不在可选清单中(来自运行中工具或内置名录),保存会被拒绝'
|
|
2525
|
+
}
|
|
2526
|
+
return null
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
function draftToPayload(draft) {
|
|
2530
|
+
var config = { provider: draft.provider, toolName: draft.toolName }
|
|
2531
|
+
if (draft.persona.trim() !== '') config.persona = draft.persona
|
|
2532
|
+
if (draft.allow.length > 0 || draft.deny.length > 0) {
|
|
2533
|
+
config.toolFilter = {}
|
|
2534
|
+
if (draft.allow.length > 0) config.toolFilter.allow = draft.allow.slice()
|
|
2535
|
+
if (draft.deny.length > 0) config.toolFilter.deny = draft.deny.slice()
|
|
2536
|
+
}
|
|
2537
|
+
var agentOptions = {}
|
|
2538
|
+
if (draft.agentProvider.trim() !== '') agentOptions.provider = draft.agentProvider.trim()
|
|
2539
|
+
if (draft.agentModel.trim() !== '') agentOptions.model = draft.agentModel.trim()
|
|
2540
|
+
if (draft.maxTokens.trim() !== '') agentOptions.maxTokens = Number(draft.maxTokens)
|
|
2541
|
+
if (Object.keys(agentOptions).length > 0) config.agentOptions = agentOptions
|
|
2542
|
+
if (draft.maxDepthManaged) config.maxDepth = 'provider-managed'
|
|
2543
|
+
else if (draft.maxDepth !== '') config.maxDepth = Number(draft.maxDepth)
|
|
2544
|
+
config.backgroundMode = draft.backgroundMode
|
|
2545
|
+
config.enableRunInBackground = draft.enableRunInBackground === true
|
|
2546
|
+
return { id: draft.id.trim(), config: config }
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
/* ========================================================================== */
|
|
2550
|
+
/* Searchable picker (single/multi) */
|
|
2551
|
+
/* ========================================================================== */
|
|
2552
|
+
|
|
2553
|
+
/**
|
|
2554
|
+
* Filterable picker used for the model/provider selects (single) and the
|
|
2555
|
+
* tool-constraint chips (multi). Renders a text input plus a scrollable,
|
|
2556
|
+
* filtered dropdown; click or Enter selects. Single mode also commits the
|
|
2557
|
+
* typed text (so arbitrary values still work); multi mode appends chips and
|
|
2558
|
+
* hides already-picked options from the list.
|
|
2559
|
+
*/
|
|
2560
|
+
function Picker(props) {
|
|
2561
|
+
var multi = props.multi === true
|
|
2562
|
+
var values = props.values || []
|
|
2563
|
+
var options = props.options || []
|
|
2564
|
+
var allowCustom = props.allowCustom !== false
|
|
2565
|
+
var inputRef = useRef(null)
|
|
2566
|
+
var textState = useState('')
|
|
2567
|
+
var text = textState[0]
|
|
2568
|
+
var setText = textState[1]
|
|
2569
|
+
var openState = useState(false)
|
|
2570
|
+
var open = openState[0]
|
|
2571
|
+
var setOpen = openState[1]
|
|
2572
|
+
var highlightState = useState(0)
|
|
2573
|
+
var highlight = highlightState[0]
|
|
2574
|
+
var setHighlight = highlightState[1]
|
|
2575
|
+
|
|
2576
|
+
var selected = {}
|
|
2577
|
+
for (var si = 0; si < values.length; si++) selected[values[si]] = true
|
|
2578
|
+
|
|
2579
|
+
var filtered = options.filter(function (o) {
|
|
2580
|
+
if (multi && selected[o.value]) return false
|
|
2581
|
+
var q = text.trim().toLowerCase()
|
|
2582
|
+
if (q === '') return true
|
|
2583
|
+
var label = (o.label || o.value).toLowerCase()
|
|
2584
|
+
return label.indexOf(q) !== -1 || o.value.toLowerCase().indexOf(q) !== -1
|
|
2585
|
+
})
|
|
2586
|
+
|
|
2587
|
+
function emit(next) { props.onChange(next) }
|
|
2588
|
+
|
|
2589
|
+
function pick(opt) {
|
|
2590
|
+
if (multi) {
|
|
2591
|
+
if (selected[opt.value] || !TOOL_REF_RE.test(opt.value)) return
|
|
2592
|
+
emit(values.concat([opt.value]))
|
|
2593
|
+
setText(''); setHighlight(0); setOpen(true)
|
|
2594
|
+
} else {
|
|
2595
|
+
emit([opt.value]); setText(''); setOpen(false)
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
function addTyped(v) {
|
|
2599
|
+
var val = (v || '').trim()
|
|
2600
|
+
if (val === '') return
|
|
2601
|
+
if (multi && (selected[val] || !TOOL_REF_RE.test(val))) return
|
|
2602
|
+
emit(multi ? values.concat([val]) : [val])
|
|
2603
|
+
setText(''); setHighlight(0)
|
|
2604
|
+
}
|
|
2605
|
+
function onInputChange(e) {
|
|
2606
|
+
var v = e.target.value
|
|
2607
|
+
setText(v); setOpen(true); setHighlight(0)
|
|
2608
|
+
if (!multi) emit([v])
|
|
2609
|
+
}
|
|
2610
|
+
function onKeyDown(e) {
|
|
2611
|
+
var live = (inputRef.current ? inputRef.current.value : text).trim()
|
|
2612
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); setHighlight(function (h) { return Math.min(h + 1, filtered.length - 1) }); return }
|
|
2613
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); setHighlight(function (h) { return Math.max(h - 1, 0) }); return }
|
|
2614
|
+
if (e.key === 'Enter') {
|
|
2615
|
+
e.preventDefault()
|
|
2616
|
+
if (filtered.length > 0 && highlight >= 0 && highlight < filtered.length) {
|
|
2617
|
+
var hl = filtered[highlight]
|
|
2618
|
+
if (live === '' || live === hl.value || live === (hl.label || hl.value)) { pick(hl); return }
|
|
2619
|
+
}
|
|
2620
|
+
if (allowCustom && live !== '') addTyped(live)
|
|
2621
|
+
return
|
|
2622
|
+
}
|
|
2623
|
+
if (e.key === 'Escape') setOpen(false)
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
var tokens = multi ? values.map(function (v) {
|
|
2627
|
+
return createElement('span', { className: 'tag-chip ' + (props.kind || 'allow'), key: 'tok-' + v },
|
|
2628
|
+
v,
|
|
2629
|
+
createElement('button', {
|
|
2630
|
+
type: 'button', 'aria-label': '移除 ' + v,
|
|
2631
|
+
onClick: function (ev) { ev.stopPropagation(); emit(values.filter(function (x) { return x !== v })) },
|
|
2632
|
+
}, '✕'))
|
|
2633
|
+
}) : null
|
|
2634
|
+
|
|
2635
|
+
var list = open
|
|
2636
|
+
? createElement('div', { className: 'sa-picker-list' },
|
|
2637
|
+
filtered.length === 0
|
|
2638
|
+
? createElement('div', { className: 'sa-picker-empty' }, allowCustom && text.trim() !== '' ? '回车添加:' + text.trim() : '无匹配')
|
|
2639
|
+
: filtered.map(function (o, idx) {
|
|
2640
|
+
return createElement('div', {
|
|
2641
|
+
key: o.value,
|
|
2642
|
+
className: 'sa-picker-opt' + (idx === highlight ? ' active' : ''),
|
|
2643
|
+
onMouseDown: function (ev) { ev.preventDefault(); pick(o) },
|
|
2644
|
+
onMouseEnter: function () { setHighlight(idx) },
|
|
2645
|
+
}, o.label || o.value)
|
|
2646
|
+
}))
|
|
2647
|
+
: null
|
|
2648
|
+
|
|
2649
|
+
return createElement('div', { className: 'sa-picker' + (multi ? ' tag-input' : '') },
|
|
2650
|
+
tokens,
|
|
2651
|
+
createElement('input', {
|
|
2652
|
+
ref: inputRef,
|
|
2653
|
+
className: 'sa-picker-input',
|
|
2654
|
+
value: multi ? text : (values[0] || ''),
|
|
2655
|
+
placeholder: props.placeholder || '',
|
|
2656
|
+
'aria-label': props.ariaLabel || props.placeholder || '',
|
|
2657
|
+
onChange: onInputChange,
|
|
2658
|
+
onKeyDown: onKeyDown,
|
|
2659
|
+
onFocus: function () { setOpen(true) },
|
|
2660
|
+
onBlur: function () { setOpen(false) },
|
|
2661
|
+
}),
|
|
2662
|
+
list,
|
|
2663
|
+
)
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
/* ========================================================================== */
|
|
2667
|
+
/* Subagents panel */
|
|
2668
|
+
/* ========================================================================== */
|
|
2669
|
+
|
|
2670
|
+
function SubagentsPanel(props) {
|
|
2671
|
+
var call = props.call
|
|
2672
|
+
|
|
2673
|
+
var viewState = useState({ loading: true, error: null, data: null })
|
|
2674
|
+
var view = viewState[0]
|
|
2675
|
+
var setView = viewState[1]
|
|
2676
|
+
|
|
2677
|
+
var formState = useState(null)
|
|
2678
|
+
var form = formState[0]
|
|
2679
|
+
var setForm = formState[1]
|
|
2680
|
+
|
|
2681
|
+
var needleState = useState('')
|
|
2682
|
+
var needle = needleState[0]
|
|
2683
|
+
var setNeedle = needleState[1]
|
|
2684
|
+
|
|
2685
|
+
var providerFilterState = useState('全部')
|
|
2686
|
+
var providerFilter = providerFilterState[0]
|
|
2687
|
+
var setProviderFilter = providerFilterState[1]
|
|
2688
|
+
|
|
2689
|
+
var noticeState = useState(null)
|
|
2690
|
+
var notice = noticeState[0]
|
|
2691
|
+
var setNotice = noticeState[1]
|
|
2692
|
+
|
|
2693
|
+
var toastState = useState(null)
|
|
2694
|
+
var toast = toastState[0]
|
|
2695
|
+
var setToast = toastState[1]
|
|
2696
|
+
|
|
2697
|
+
var reload = function () {
|
|
2698
|
+
setView(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
|
|
2699
|
+
call('subagentAdmin/list', {}).then(function (raw) {
|
|
2700
|
+
var result = unwrap(raw)
|
|
2701
|
+
setView({ loading: false, error: null, data: result })
|
|
2702
|
+
}).catch(function (error) {
|
|
2703
|
+
setView({ loading: false, error: String((error && error.message) || error), data: null })
|
|
2704
|
+
})
|
|
2705
|
+
}
|
|
2706
|
+
useEffect(reload, [])
|
|
2707
|
+
|
|
2708
|
+
useEffect(function () {
|
|
2709
|
+
if (!toast) return undefined
|
|
2710
|
+
var timer = setTimeout(function () { setToast(null) }, 4200)
|
|
2711
|
+
return function () { clearTimeout(timer) }
|
|
2712
|
+
}, [toast])
|
|
2713
|
+
|
|
2714
|
+
var data = view.data
|
|
2715
|
+
var entries = (data && data.entries) || []
|
|
2716
|
+
var meta = (data && data.meta) || { tools: [], providers: [], llmProviders: [], llmModels: {} }
|
|
2717
|
+
var candidateNames = meta.tools.map(function (tool) { return tool.name })
|
|
2718
|
+
var providerMeta = {}
|
|
2719
|
+
meta.providers.forEach(function (provider) { providerMeta[provider.name] = provider })
|
|
2720
|
+
var llmProviders = meta.llmProviders || []
|
|
2721
|
+
var llmModels = meta.llmModels || {}
|
|
2722
|
+
|
|
2723
|
+
var filtered = entries.filter(function (entry) {
|
|
2724
|
+
var config = entry.config || {}
|
|
2725
|
+
if (providerFilter !== '全部' && config.provider !== providerFilter) return false
|
|
2726
|
+
if (needle === '') return true
|
|
2727
|
+
var haystack = [entry.id, config.toolName, config.provider, config.persona, config.agentOptions && config.agentOptions.model]
|
|
2728
|
+
.filter(Boolean).join(' ').toLowerCase()
|
|
2729
|
+
return haystack.indexOf(needle.toLowerCase()) !== -1
|
|
2730
|
+
})
|
|
2731
|
+
|
|
2732
|
+
var openCreate = function () {
|
|
2733
|
+
setNotice(null)
|
|
2734
|
+
setForm({ draft: emptyDraft(), editing: false, saving: false, error: null })
|
|
2735
|
+
}
|
|
2736
|
+
var openEdit = function (entry) {
|
|
2737
|
+
setNotice(null)
|
|
2738
|
+
setForm({ draft: draftFromEntry(entry), editing: true, saving: false, error: null })
|
|
2739
|
+
}
|
|
2740
|
+
var closeForm = function () { setForm(null) }
|
|
2741
|
+
|
|
2742
|
+
var saveForm = function () {
|
|
2743
|
+
if (!form || form.saving) return
|
|
2744
|
+
var otherToolNames = entries
|
|
2745
|
+
.filter(function (entry) { return !form.editing || entry.id !== form.draft.id })
|
|
2746
|
+
.map(function (entry) { return (entry.config || {}).toolName })
|
|
2747
|
+
.filter(Boolean)
|
|
2748
|
+
var clientError = validateDraft(form.draft, !form.editing, otherToolNames, providerMeta, candidateNames)
|
|
2749
|
+
if (clientError) {
|
|
2750
|
+
setForm(Object.assign({}, form, { error: clientError }))
|
|
2751
|
+
return
|
|
2752
|
+
}
|
|
2753
|
+
var saving = Object.assign({}, form, { saving: true, error: null })
|
|
2754
|
+
setForm(saving)
|
|
2755
|
+
var payload = draftToPayload(saving.draft)
|
|
2756
|
+
call('subagentAdmin/upsert', { entry: payload }).then(function (raw) {
|
|
2757
|
+
var result = unwrap(raw)
|
|
2758
|
+
setView(function (prev) { return Object.assign({}, prev, { data: result, loading: false }) })
|
|
2759
|
+
setForm(null)
|
|
2760
|
+
setNotice(result.warnings && result.warnings.length > 0 ? result.warnings : null)
|
|
2761
|
+
}).catch(function (error) {
|
|
2762
|
+
var message = String((error && error.message) || error)
|
|
2763
|
+
setForm(Object.assign({}, saving, { saving: false, error: message }))
|
|
2764
|
+
setToast('保存失败:' + message)
|
|
2765
|
+
})
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
var removeEntry = function (entry) {
|
|
2769
|
+
call('subagentAdmin/remove', { id: entry.id }).then(function (raw) {
|
|
2770
|
+
var result = unwrap(raw)
|
|
2771
|
+
setView(function (prev) { return Object.assign({}, prev, { data: result, loading: false }) })
|
|
2772
|
+
setNotice(null)
|
|
2773
|
+
}).catch(function (error) {
|
|
2774
|
+
setToast('删除失败:' + String((error && error.message) || error))
|
|
2775
|
+
})
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
var patchDraft = function (patch) {
|
|
2779
|
+
setForm(function (prev) {
|
|
2780
|
+
return Object.assign({}, prev, { draft: Object.assign({}, prev.draft, patch) })
|
|
2781
|
+
})
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
var children = []
|
|
2785
|
+
|
|
2786
|
+
// Main toolbar: search + primary actions. Provider filters live on their own
|
|
2787
|
+
// row below so a growing backend list never squeezes the search box.
|
|
2788
|
+
children.push(createElement('div', { className: 'toolbar', key: 'toolbar' },
|
|
2789
|
+
createElement('div', { className: 'search-wrap', key: 'search' },
|
|
2790
|
+
createElement('span', { className: 'search-icon' }, '🔍'),
|
|
2791
|
+
createElement('input', {
|
|
2792
|
+
className: 'input',
|
|
2793
|
+
placeholder: '搜索子智能体(名称/ID/后端/提示词/模型)...',
|
|
2794
|
+
value: needle,
|
|
2795
|
+
onChange: function (event) { setNeedle(event.target.value) },
|
|
2796
|
+
})
|
|
2797
|
+
),
|
|
2798
|
+
createElement('button', { type: 'button', className: 'btn sm primary', key: 'create', onClick: openCreate }, '+ 新建子智能体'),
|
|
2799
|
+
createElement('button', { type: 'button', className: 'btn sm', key: 'refresh', onClick: reload, disabled: view.loading },
|
|
2800
|
+
view.loading ? createElement(Spinner, { key: 'spin' }) : '↻', '刷新')
|
|
2801
|
+
))
|
|
2802
|
+
|
|
2803
|
+
if (meta.providers.length > 1) {
|
|
2804
|
+
children.push(createElement('div', { className: 'filterbar', key: 'filterbar' },
|
|
2805
|
+
createElement('span', { className: 'filter-label' }, '执行后端'),
|
|
2806
|
+
createElement('button', {
|
|
2807
|
+
type: 'button', key: 'pill-all',
|
|
2808
|
+
className: 'btn sm' + (providerFilter === '全部' ? ' active' : ''),
|
|
2809
|
+
onClick: function () { setProviderFilter('全部') },
|
|
2810
|
+
}, '全部'),
|
|
2811
|
+
meta.providers.map(function (provider) {
|
|
2812
|
+
return createElement('button', {
|
|
2813
|
+
type: 'button', key: 'pill-' + provider.name,
|
|
2814
|
+
className: 'btn sm' + (providerFilter === provider.name ? ' active' : ''),
|
|
2815
|
+
onClick: function () { setProviderFilter(providerFilter === provider.name ? '全部' : provider.name) },
|
|
2816
|
+
}, provider.name)
|
|
2817
|
+
})
|
|
2818
|
+
))
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
if (form) {
|
|
2822
|
+
children.push(SubagentForm({
|
|
2823
|
+
form: form,
|
|
2824
|
+
meta: meta,
|
|
2825
|
+
candidateNames: candidateNames,
|
|
2826
|
+
providerMeta: providerMeta,
|
|
2827
|
+
llmProviders: llmProviders,
|
|
2828
|
+
llmModels: llmModels,
|
|
2829
|
+
entries: entries,
|
|
2830
|
+
onPatch: patchDraft,
|
|
2831
|
+
onClose: closeForm,
|
|
2832
|
+
onSave: saveForm,
|
|
2833
|
+
}))
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
if (notice) {
|
|
2837
|
+
children.push(createElement('div', { className: 'warn-strip', key: 'notice' },
|
|
2838
|
+
notice.map(function (warning, index) {
|
|
2839
|
+
return createElement('div', { key: index }, '⚠️ ', warning)
|
|
2840
|
+
})
|
|
2841
|
+
))
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
if (!form) {
|
|
2845
|
+
if (view.loading && entries.length === 0) {
|
|
2846
|
+
children.push(createElement('div', { className: 'empty', key: 'loading' },
|
|
2847
|
+
createElement('span', { className: 'big' }, '⏳'),
|
|
2848
|
+
'正在加载子智能体配置…'))
|
|
2849
|
+
} else if (view.error) {
|
|
2850
|
+
children.push(createElement('div', { className: 'error-strip', key: 'error' }, '⚠️ 加载失败:', view.error))
|
|
2851
|
+
} else if (filtered.length === 0) {
|
|
2852
|
+
children.push(createElement('div', { className: 'empty', key: 'empty' },
|
|
2853
|
+
createElement('span', { className: 'big' }, '🤖'),
|
|
2854
|
+
entries.length === 0
|
|
2855
|
+
? '还没有受管子智能体。点击「+ 新建子智能体」创建第一个:名称、提示词、工具约束、模型指定全部可配。'
|
|
2856
|
+
: '没有匹配当前搜索/筛选的子智能体。'))
|
|
2857
|
+
} else {
|
|
2858
|
+
children.push(createElement('div', { className: 'list', key: 'cards' }, filtered.map(function (entry) {
|
|
2859
|
+
return SubagentCard({
|
|
2860
|
+
entry: entry,
|
|
2861
|
+
onEdit: openEdit,
|
|
2862
|
+
onRemove: removeEntry,
|
|
2863
|
+
})
|
|
2864
|
+
})))
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
children.push(createElement('div', { className: 'footer-note', key: 'note' },
|
|
2869
|
+
'每张卡片对应 profile cordis.patch.yml 中一个 @deepseek-ai/dsh-tool-subagent 行;保存即写入该文件,由 Cordis HMR 热加载生效(无需重启),重启 dsh 后同样自动加载。首次修改前原文件自动备份为 cordis.patch.yml.bak-subagent-admin。'))
|
|
2870
|
+
|
|
2871
|
+
return createElement('div', null,
|
|
2872
|
+
toast ? createElement('div', { className: 'toast' }, toast) : null,
|
|
2873
|
+
children
|
|
2874
|
+
)
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
function SubagentForm(props) {
|
|
2878
|
+
var form = props.form
|
|
2879
|
+
var draft = form.draft
|
|
2880
|
+
var meta = props.meta
|
|
2881
|
+
var candidateNames = props.candidateNames
|
|
2882
|
+
var providerMeta = props.providerMeta
|
|
2883
|
+
var llmProviders = props.llmProviders || []
|
|
2884
|
+
var llmModels = props.llmModels || {}
|
|
2885
|
+
var entries = props.entries
|
|
2886
|
+
var onPatch = props.onPatch
|
|
2887
|
+
var onClose = props.onClose
|
|
2888
|
+
var onSave = props.onSave
|
|
2889
|
+
|
|
2890
|
+
var providerInfo = providerMeta[draft.provider]
|
|
2891
|
+
var capabilityHint = providerInfo
|
|
2892
|
+
? '能力:' + [
|
|
2893
|
+
providerInfo.capabilities.persona ? '✓ 提示词' : '✗ 提示词',
|
|
2894
|
+
providerInfo.capabilities.toolFilter ? '✓ 工具约束' : '✗ 工具约束',
|
|
2895
|
+
providerInfo.capabilities.depthLimit ? '✓ 深度上限' : '✗ 深度上限',
|
|
2896
|
+
providerInfo.continuable ? '✓ 可持续会话' : '仅一次性',
|
|
2897
|
+
].join(' / ')
|
|
2898
|
+
: ''
|
|
2899
|
+
// Model picker options: the selected provider's catalog, else every model.
|
|
2900
|
+
var modelOptions = draft.agentProvider && llmModels[draft.agentProvider]
|
|
2901
|
+
? llmModels[draft.agentProvider]
|
|
2902
|
+
: [].concat.apply([], Object.keys(llmModels).map(function (k) { return llmModels[k] }))
|
|
2903
|
+
|
|
2904
|
+
return createElement('div', { className: 'form', key: 'form' },
|
|
2905
|
+
createElement('div', { className: 'form-title' },
|
|
2906
|
+
form.editing ? '✏️ 编辑子智能体' : '✨ 新建子智能体',
|
|
2907
|
+
createElement('span', { style: { marginLeft: 'auto', display: 'flex', gap: '6px' } },
|
|
2908
|
+
createElement('button', { type: 'button', className: 'btn sm', onClick: onClose, disabled: form.saving }, '取消'),
|
|
2909
|
+
createElement('button', { type: 'button', className: 'btn sm primary', onClick: onSave, disabled: form.saving },
|
|
2910
|
+
form.saving ? createElement(Spinner, { key: 'spin' }) : null,
|
|
2911
|
+
'保存')
|
|
2912
|
+
)
|
|
2913
|
+
),
|
|
2914
|
+
form.error ? createElement('div', { className: 'error-strip' }, '⚠️ ', form.error) : null,
|
|
2915
|
+
createElement('div', { className: 'form-grid' },
|
|
2916
|
+
createElement('div', { className: 'field' },
|
|
2917
|
+
createElement('span', { className: 'field-label' }, '实例 ID'),
|
|
2918
|
+
createElement('input', {
|
|
2919
|
+
className: 'input', value: draft.id, disabled: form.editing || form.saving,
|
|
2920
|
+
placeholder: '如 researcher、code-reviewer',
|
|
2921
|
+
onChange: function (event) { onPatch({ id: event.target.value }) },
|
|
2922
|
+
}),
|
|
2923
|
+
createElement('span', { className: 'field-hint' }, '补丁行标识,创建后不可改;持久化在 profile 的 cordis.patch.yml')
|
|
2924
|
+
),
|
|
2925
|
+
createElement('div', { className: 'field' },
|
|
2926
|
+
createElement('span', { className: 'field-label' }, '子智能体名称(模型可见工具名)'),
|
|
2927
|
+
createElement('input', {
|
|
2928
|
+
className: 'input', value: draft.toolName, disabled: form.saving,
|
|
2929
|
+
placeholder: '如 web_researcher(模型用它发起委托)',
|
|
2930
|
+
onChange: function (event) { onPatch({ toolName: event.target.value }) },
|
|
2931
|
+
}),
|
|
2932
|
+
createElement('span', { className: 'field-hint' }, '不能用保留名 subagent / subagent_fork / run_code,且各实例间唯一')
|
|
2933
|
+
),
|
|
2934
|
+
createElement('div', { className: 'field' },
|
|
2935
|
+
createElement('span', { className: 'field-label' }, '执行后端(provider)'),
|
|
2936
|
+
createElement('select', {
|
|
2937
|
+
className: 'input', value: draft.provider, disabled: form.saving,
|
|
2938
|
+
onChange: function (event) { onPatch({ provider: event.target.value }) },
|
|
2939
|
+
},
|
|
2940
|
+
meta.providers.map(function (provider) {
|
|
2941
|
+
return createElement('option', { key: provider.name, value: provider.name }, provider.name)
|
|
2942
|
+
})
|
|
2943
|
+
),
|
|
2944
|
+
createElement('span', { className: 'field-hint' }, capabilityHint)
|
|
2945
|
+
),
|
|
2946
|
+
createElement('div', { className: 'field' },
|
|
2947
|
+
createElement('span', { className: 'field-label' }, '后台模式'),
|
|
2948
|
+
createElement('select', {
|
|
2949
|
+
className: 'input', value: draft.backgroundMode, disabled: form.saving,
|
|
2950
|
+
onChange: function (event) { onPatch({ backgroundMode: event.target.value }) },
|
|
2951
|
+
},
|
|
2952
|
+
createElement('option', { value: 'one-shot' }, 'one-shot(一次性任务)'),
|
|
2953
|
+
createElement('option', { value: 'continuable' }, 'continuable(可持续会话)')
|
|
2954
|
+
),
|
|
2955
|
+
createElement('label', { className: 'checkbox-row' },
|
|
2956
|
+
createElement('input', {
|
|
2957
|
+
type: 'checkbox', checked: draft.enableRunInBackground === true, disabled: form.saving,
|
|
2958
|
+
onChange: function (event) { onPatch({ enableRunInBackground: event.target.checked }) },
|
|
2959
|
+
}),
|
|
2960
|
+
'暴露 run_in_background 参数'
|
|
2961
|
+
)
|
|
2962
|
+
),
|
|
2963
|
+
createElement('div', { className: 'field full' },
|
|
2964
|
+
createElement('span', { className: 'field-label' }, '提示词(persona,留空继承部署默认)'),
|
|
2965
|
+
createElement('textarea', {
|
|
2966
|
+
className: 'input', value: draft.persona, rows: 4, disabled: form.saving,
|
|
2967
|
+
placeholder: '该子智能体的人设/职责说明…支持 {{model}} 与 {{cwd}} 模板变量',
|
|
2968
|
+
onChange: function (event) { onPatch({ persona: event.target.value }) },
|
|
2969
|
+
}),
|
|
2970
|
+
createElement('span', { className: 'field-hint' }, '保存后影子覆盖(shadow)部署级 persona,仅对该子智能体生效')
|
|
2971
|
+
),
|
|
2972
|
+
createElement('div', { className: 'fieldset full' },
|
|
2973
|
+
createElement('span', { className: 'fieldset-legend' }, '工具约束(toolFilter,留空则不限制;可选项来自 harness 已注册工具与内置工具名录,也可手填未知名)'),
|
|
2974
|
+
createElement('div', { className: 'field' },
|
|
2975
|
+
createElement('span', { className: 'field-label' }, '仅允许(allow 白名单)'),
|
|
2976
|
+
createElement(Picker, {
|
|
2977
|
+
multi: true, kind: 'allow', allowCustom: true,
|
|
2978
|
+
values: draft.allow, options: candidateNames.map(function (n) { return { value: n, label: n } }),
|
|
2979
|
+
placeholder: '输入或选择工具名,如 read / glob / grep', ariaLabel: '仅允许工具',
|
|
2980
|
+
onChange: function (next) { onPatch({ allow: next }) },
|
|
2981
|
+
}),
|
|
2982
|
+
createElement('span', { className: 'field-hint' }, '设置后子智能体只保留名单内工具,其余从提示词移除且拒绝执行')
|
|
2983
|
+
),
|
|
2984
|
+
createElement('div', { className: 'field' },
|
|
2985
|
+
createElement('span', { className: 'field-label' }, '禁止(deny 黑名单)'),
|
|
2986
|
+
createElement(Picker, {
|
|
2987
|
+
multi: true, kind: 'deny', allowCustom: true,
|
|
2988
|
+
values: draft.deny, options: candidateNames.map(function (n) { return { value: n, label: n } }),
|
|
2989
|
+
placeholder: '输入或选择工具名,如 bash / pwsh', ariaLabel: '禁止工具',
|
|
2990
|
+
onChange: function (next) { onPatch({ deny: next }) },
|
|
2991
|
+
}),
|
|
2992
|
+
createElement('span', { className: 'field-hint' }, '名单内工具对子智能体不可见;未知工具名保存时会被拒绝')
|
|
2993
|
+
)
|
|
2994
|
+
),
|
|
2995
|
+
createElement('div', { className: 'fieldset full' },
|
|
2996
|
+
createElement('span', { className: 'fieldset-legend' }, '模型指定(agentOptions,留空字段继承父代理当前路由)'),
|
|
2997
|
+
createElement('div', { className: 'form-grid' },
|
|
2998
|
+
createElement('div', { className: 'field' },
|
|
2999
|
+
createElement('span', { className: 'field-label' }, 'LLM provider'),
|
|
3000
|
+
createElement(Picker, {
|
|
3001
|
+
multi: false, allowCustom: true,
|
|
3002
|
+
values: draft.agentProvider ? [draft.agentProvider] : [],
|
|
3003
|
+
options: llmProviders.map(function (p) { return { value: p.id, label: p.name } }),
|
|
3004
|
+
placeholder: '留空继承,如 optirouter / deepseek-official', ariaLabel: 'LLM provider',
|
|
3005
|
+
onChange: function (next) { onPatch({ agentProvider: next[0] || '' }) },
|
|
3006
|
+
})
|
|
3007
|
+
),
|
|
3008
|
+
createElement('div', { className: 'field' },
|
|
3009
|
+
createElement('span', { className: 'field-label' }, '模型标识(model)'),
|
|
3010
|
+
createElement(Picker, {
|
|
3011
|
+
multi: false, allowCustom: true,
|
|
3012
|
+
values: draft.agentModel ? [draft.agentModel] : [],
|
|
3013
|
+
options: modelOptions.map(function (m) { return { value: m.id, label: m.name } }),
|
|
3014
|
+
placeholder: '留空继承,如 auto', ariaLabel: '模型标识',
|
|
3015
|
+
onChange: function (next) { onPatch({ agentModel: next[0] || '' }) },
|
|
3016
|
+
}),
|
|
3017
|
+
createElement('span', { className: 'field-hint' }, '从已配置模型中选择,或手填模型 id(需在该 provider 路由上注册)')
|
|
3018
|
+
),
|
|
3019
|
+
createElement('div', { className: 'field' },
|
|
3020
|
+
createElement('span', { className: 'field-label' }, 'maxTokens(单次回复上限)'),
|
|
3021
|
+
createElement('input', {
|
|
3022
|
+
className: 'input', value: draft.maxTokens, disabled: form.saving,
|
|
3023
|
+
placeholder: '留空使用默认', inputMode: 'numeric',
|
|
3024
|
+
onChange: function (event) { onPatch({ maxTokens: event.target.value.replace(/[^0-9]/g, '') }) },
|
|
3025
|
+
})
|
|
3026
|
+
),
|
|
3027
|
+
createElement('div', { className: 'field' },
|
|
3028
|
+
createElement('span', { className: 'field-label' }, '最大委托深度(maxDepth,0 = 禁止再委托)'),
|
|
3029
|
+
createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
|
|
3030
|
+
createElement('input', {
|
|
3031
|
+
className: 'input', value: draft.maxDepthManaged ? '' : draft.maxDepth,
|
|
3032
|
+
disabled: form.saving || draft.maxDepthManaged,
|
|
3033
|
+
placeholder: '3(默认)', inputMode: 'numeric',
|
|
3034
|
+
onChange: function (event) { onPatch({ maxDepth: event.target.value.replace(/[^0-9]/g, '') }) },
|
|
3035
|
+
}),
|
|
3036
|
+
createElement('label', { className: 'checkbox-row', style: { flex: 'none' } },
|
|
3037
|
+
createElement('input', {
|
|
3038
|
+
type: 'checkbox', checked: draft.maxDepthManaged, disabled: form.saving,
|
|
3039
|
+
onChange: function (event) { onPatch({ maxDepthManaged: event.target.checked }) },
|
|
3040
|
+
}),
|
|
3041
|
+
'交由后端管理'
|
|
3042
|
+
)
|
|
3043
|
+
)
|
|
3044
|
+
)
|
|
3045
|
+
)
|
|
3046
|
+
)
|
|
3047
|
+
)
|
|
3048
|
+
)
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
function SubagentCard(props) {
|
|
3052
|
+
var entry = props.entry
|
|
3053
|
+
var config = entry.config || {}
|
|
3054
|
+
var live = entry.live || {}
|
|
3055
|
+
var liveTag = live.providerPresent === false
|
|
3056
|
+
? createElement('span', { className: 'tag dead' }, '❌ 后端未注册')
|
|
3057
|
+
: live.toolRegistered
|
|
3058
|
+
? createElement('span', { className: 'tag live' }, '✅ 已挂载')
|
|
3059
|
+
: createElement('span', { className: 'tag warn' }, '⚠️ 工具未挂载')
|
|
3060
|
+
var hasModelRoute = config.agentOptions && (config.agentOptions.model || config.agentOptions.provider)
|
|
3061
|
+
var modelLabel = hasModelRoute
|
|
3062
|
+
? ((config.agentOptions.model || '') + (config.agentOptions.provider ? (config.agentOptions.model ? ' @ ' : '') + config.agentOptions.provider : ''))
|
|
3063
|
+
: '继承父代理'
|
|
3064
|
+
|
|
3065
|
+
var children = [
|
|
3066
|
+
createElement('div', { className: 'card-header', key: 'head' },
|
|
3067
|
+
createElement('span', { className: 'card-title' }, '🤖 ', config.toolName || entry.id),
|
|
3068
|
+
createElement('span', { className: 'card-actions' },
|
|
3069
|
+
createElement('button', { type: 'button', className: 'btn sm', onClick: function () { props.onEdit(entry) } }, '编辑'),
|
|
3070
|
+
createElement(ConfirmButton, {
|
|
3071
|
+
label: '删除', confirmLabel: '确认删除?',
|
|
3072
|
+
onConfirm: function () { props.onRemove(entry) },
|
|
3073
|
+
})
|
|
3074
|
+
)
|
|
3075
|
+
),
|
|
3076
|
+
createElement('div', { className: 'card-sub', key: 'tags' },
|
|
3077
|
+
createElement('span', { className: 'tag provider' }, '⚙ ' + (config.provider || '?')),
|
|
3078
|
+
createElement('span', { className: 'tag model' }, '🧠 ' + modelLabel),
|
|
3079
|
+
createElement('span', { className: 'tag' }, config.backgroundMode === 'continuable' ? '可持续' : '一次性'),
|
|
3080
|
+
typeof config.maxDepth === 'number' ? createElement('span', { className: 'tag' }, '深度≤' + config.maxDepth) : null,
|
|
3081
|
+
config.maxDepth === 'provider-managed' ? createElement('span', { className: 'tag' }, '深度=后端管理') : null,
|
|
3082
|
+
liveTag
|
|
3083
|
+
),
|
|
3084
|
+
]
|
|
3085
|
+
|
|
3086
|
+
if (config.persona) {
|
|
3087
|
+
children.push(createElement('div', { className: 'persona-box', key: 'persona' },
|
|
3088
|
+
createElement('span', null, '📝'),
|
|
3089
|
+
createElement('span', { className: 'persona-text' }, config.persona)
|
|
3090
|
+
))
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
var chips = []
|
|
3094
|
+
if (config.toolFilter && config.toolFilter.allow && config.toolFilter.allow.length > 0) {
|
|
3095
|
+
chips.push(createElement('span', { key: 'allow-label', style: { fontSize: '10px', color: '#047857', fontWeight: 600 } }, '仅允许'))
|
|
3096
|
+
config.toolFilter.allow.forEach(function (name) {
|
|
3097
|
+
chips.push(createElement('span', { key: 'a-' + name, className: 'chip allow' }, name))
|
|
3098
|
+
})
|
|
3099
|
+
}
|
|
3100
|
+
if (config.toolFilter && config.toolFilter.deny && config.toolFilter.deny.length > 0) {
|
|
3101
|
+
chips.push(createElement('span', { key: 'deny-label', style: { fontSize: '10px', color: '#b91c1c', fontWeight: 600 } }, '禁止'))
|
|
3102
|
+
config.toolFilter.deny.forEach(function (name) {
|
|
3103
|
+
chips.push(createElement('span', { key: 'd-' + name, className: 'chip deny' }, name))
|
|
3104
|
+
})
|
|
3105
|
+
}
|
|
3106
|
+
if (chips.length > 0) {
|
|
3107
|
+
children.push(createElement('div', { className: 'chips', key: 'chips' }, chips))
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
children.push(createElement('div', { className: 'card-sub', key: 'meta' },
|
|
3111
|
+
createElement('span', { className: 'tag', title: entry.id }, 'ID: ' + entry.id),
|
|
3112
|
+
config.toolFilter === undefined ? createElement('span', { className: 'tag' }, '工具不限制') : null
|
|
3113
|
+
))
|
|
3114
|
+
|
|
3115
|
+
return createElement('div', { className: 'card', key: entry.id }, children)
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
/* ========================================================================== */
|
|
3119
|
+
/* History panel */
|
|
3120
|
+
/* ========================================================================== */
|
|
3121
|
+
|
|
3122
|
+
function HistoryPanel(props) {
|
|
3123
|
+
var call = props.call
|
|
3124
|
+
var state = useState({ loading: true, error: null, data: null })
|
|
3125
|
+
var view = state[0]
|
|
3126
|
+
var setView = state[1]
|
|
3127
|
+
|
|
3128
|
+
var reload = function () {
|
|
3129
|
+
setView(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
|
|
3130
|
+
call('subagentAdmin/history', { limit: 200 }).then(function (raw) {
|
|
3131
|
+
setView({ loading: false, error: null, data: unwrap(raw) })
|
|
3132
|
+
}).catch(function (error) {
|
|
3133
|
+
setView({ loading: false, error: String((error && error.message) || error), data: null })
|
|
3134
|
+
})
|
|
3135
|
+
}
|
|
3136
|
+
useEffect(reload, [])
|
|
3137
|
+
|
|
3138
|
+
var actionLabels = { create: '新建', update: '更新', delete: '删除', corrupt: '损坏记录', mount: '挂载', unmount: '卸载', 'cli-update': 'CLI 配置' }
|
|
3139
|
+
var children = [
|
|
3140
|
+
createElement('div', { className: 'toolbar', key: 'bar' },
|
|
3141
|
+
createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #666)', flex: 1 } },
|
|
3142
|
+
'配置台账:每次新建/更新/删除都追加一条记录(JSONL),文件路径见下方。'),
|
|
3143
|
+
createElement('button', { type: 'button', className: 'btn sm', onClick: reload, disabled: view.loading },
|
|
3144
|
+
view.loading ? createElement(Spinner, { key: 'spin' }) : '↻', '刷新')
|
|
3145
|
+
),
|
|
3146
|
+
]
|
|
3147
|
+
|
|
3148
|
+
if (view.error) {
|
|
3149
|
+
children.push(createElement('div', { className: 'error-strip', key: 'err' }, '⚠️ 加载失败:', view.error))
|
|
3150
|
+
} else if (view.data && view.data.records && view.data.records.length > 0) {
|
|
3151
|
+
children.push(createElement('div', { className: 'footer-note', key: 'path' }, '台账文件:' + view.data.path))
|
|
3152
|
+
children.push(createElement('div', { className: 'list', key: 'rows' }, view.data.records.map(function (record, index) {
|
|
3153
|
+
return createElement('div', { className: 'history-row', key: index },
|
|
3154
|
+
createElement('div', { className: 'history-head' },
|
|
3155
|
+
createElement('span', {
|
|
3156
|
+
className: 'tag ' + (record.action === 'delete' || record.action === 'unmount' ? 'dead'
|
|
3157
|
+
: record.action === 'create' || record.action === 'mount' ? 'live' : 'warn'),
|
|
3158
|
+
}, actionLabels[record.action] || record.action),
|
|
3159
|
+
createElement('span', { style: { fontWeight: 600 } }, record.id || '—'),
|
|
3160
|
+
record.toolName ? createElement('span', { className: 'tag' }, record.toolName) : null,
|
|
3161
|
+
createElement('span', { className: 'history-time' }, record.at || '')
|
|
3162
|
+
),
|
|
3163
|
+
record.entry ? createElement('div', { className: 'history-detail' }, JSON.stringify(record.entry.config || record.entry)) : null
|
|
3164
|
+
)
|
|
3165
|
+
})))
|
|
3166
|
+
} else if (!view.loading) {
|
|
3167
|
+
children.push(createElement('div', { className: 'empty', key: 'empty' },
|
|
3168
|
+
createElement('span', { className: 'big' }, '📜'),
|
|
3169
|
+
'还没有变更记录。每次新建、编辑或删除子智能体后都会在这里留痕。'))
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
return createElement('div', null, children)
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
/* ========================================================================== */
|
|
3176
|
+
/* CLI backends */
|
|
3177
|
+
/* ========================================================================== */
|
|
3178
|
+
|
|
3179
|
+
/** Draft shape for one CLI backend's editable config. */
|
|
3180
|
+
function cliDraftFromConfig(config) {
|
|
3181
|
+
config = config || {}
|
|
3182
|
+
return {
|
|
3183
|
+
providerName: config.providerName !== undefined ? String(config.providerName) : '',
|
|
3184
|
+
permissionMode: config.permissionMode !== undefined ? String(config.permissionMode) : '',
|
|
3185
|
+
disposeGraceMs: config.disposeGraceMs !== undefined ? String(config.disposeGraceMs) : '3000',
|
|
3186
|
+
envPairs: Object.keys(config.env || {}).map(function (key) {
|
|
3187
|
+
return { key: key, value: String(config.env[key]) }
|
|
3188
|
+
}),
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
/** Convert a draft back into the wire config object. */
|
|
3193
|
+
function cliConfigFromDraft(draft) {
|
|
3194
|
+
var config = {
|
|
3195
|
+
providerName: draft.providerName.trim(),
|
|
3196
|
+
permissionMode: draft.permissionMode,
|
|
3197
|
+
disposeGraceMs: Number(draft.disposeGraceMs.trim()),
|
|
3198
|
+
env: {},
|
|
3199
|
+
}
|
|
3200
|
+
draft.envPairs.forEach(function (pair) {
|
|
3201
|
+
var key = pair.key.trim()
|
|
3202
|
+
if (key !== '') config.env[key] = pair.value
|
|
3203
|
+
})
|
|
3204
|
+
return config
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
/** Shared env key/value pair editor (used by every CLI backend card). */
|
|
3208
|
+
function EnvPairsEditor(props) {
|
|
3209
|
+
var pairs = props.pairs || []
|
|
3210
|
+
return createElement('div', { className: 'fieldset' },
|
|
3211
|
+
createElement('span', { className: 'fieldset-legend' }, 'env(传给 CLI 子进程的额外环境变量)'),
|
|
3212
|
+
pairs.map(function (pair, index) {
|
|
3213
|
+
return createElement('div', { className: 'env-row', key: index },
|
|
3214
|
+
createElement('input', {
|
|
3215
|
+
className: 'input env-key', value: pair.key,
|
|
3216
|
+
placeholder: '变量名(如 OPENAI_API_KEY)',
|
|
3217
|
+
onChange: function (event) { props.onPatchPair(index, { key: event.target.value }) },
|
|
3218
|
+
}),
|
|
3219
|
+
createElement('input', {
|
|
3220
|
+
className: 'input env-value', value: pair.value,
|
|
3221
|
+
placeholder: '变量值',
|
|
3222
|
+
onChange: function (event) { props.onPatchPair(index, { value: event.target.value }) },
|
|
3223
|
+
}),
|
|
3224
|
+
createElement('button', { type: 'button', className: 'btn sm', onClick: function () { props.onRemove(index) } }, '✕'),
|
|
3225
|
+
)
|
|
3226
|
+
}),
|
|
3227
|
+
createElement('button', { type: 'button', className: 'btn sm', onClick: props.onAdd }, '+ 添加变量'),
|
|
3228
|
+
)
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
/** One CLI backend card: collapsible status header + config form + mount/save/unmount. */
|
|
3232
|
+
function CliBackendCard(props) {
|
|
3233
|
+
var backend = props.backend
|
|
3234
|
+
var draft = props.draft
|
|
3235
|
+
var busy = props.busy === true
|
|
3236
|
+
var expanded = props.expanded === true
|
|
3237
|
+
var onPatch = props.onPatch
|
|
3238
|
+
var onPatchEnvPair = props.onPatchEnvPair
|
|
3239
|
+
var onAddEnvPair = props.onAddEnvPair
|
|
3240
|
+
var onRemoveEnvPair = props.onRemoveEnvPair
|
|
3241
|
+
var onToggle = props.onToggle
|
|
3242
|
+
var onSave = props.onSave
|
|
3243
|
+
var onUnmount = props.onUnmount
|
|
3244
|
+
|
|
3245
|
+
var packageOk = !!(backend.providerPackage && backend.providerPackage.ok)
|
|
3246
|
+
var runner = backend.runner || { ok: false, version: null }
|
|
3247
|
+
var cli = backend.cli || { ok: false, version: null }
|
|
3248
|
+
var modeValue = backend.permissionModes.indexOf(draft.permissionMode) !== -1
|
|
3249
|
+
? draft.permissionMode
|
|
3250
|
+
: backend.permissionModes[0]
|
|
3251
|
+
|
|
3252
|
+
var onToggle = props.onToggle
|
|
3253
|
+
var onSave = props.onSave
|
|
3254
|
+
var onInstall = props.onInstall
|
|
3255
|
+
var onUnmount = props.onUnmount
|
|
3256
|
+
|
|
3257
|
+
var packageOk = !!(backend.providerPackage && backend.providerPackage.ok)
|
|
3258
|
+
var runner = backend.runner || { ok: false, version: null }
|
|
3259
|
+
var cli = backend.cli || { ok: false, version: null }
|
|
3260
|
+
var missingPackages = backend.missing || []
|
|
3261
|
+
var depsMissing = missingPackages.length > 0
|
|
3262
|
+
var missingLabel = missingPackages.join(' + ')
|
|
3263
|
+
var modeValue = backend.permissionModes.indexOf(draft.permissionMode) !== -1
|
|
3264
|
+
? draft.permissionMode
|
|
3265
|
+
: backend.permissionModes[0]
|
|
3266
|
+
|
|
3267
|
+
return createElement('div', { className: 'card', key: backend.id },
|
|
3268
|
+
createElement('div', { className: 'card-header' },
|
|
3269
|
+
createElement('button', {
|
|
3270
|
+
type: 'button', className: 'cli-toggle',
|
|
3271
|
+
'aria-expanded': expanded,
|
|
3272
|
+
'aria-label': expanded ? '收起明细' : '展开明细',
|
|
3273
|
+
title: expanded ? '收起明细' : '展开明细',
|
|
3274
|
+
onClick: onToggle,
|
|
3275
|
+
}, expanded ? '▾' : '▸'),
|
|
3276
|
+
createElement('span', { className: 'cli-title', onClick: onToggle }, '🔌 ' + backend.label),
|
|
3277
|
+
createElement('span', { className: 'cli-tags' },
|
|
3278
|
+
createElement('span', { className: 'tag ' + (backend.mounted ? 'live' : '') }, backend.mounted ? '已挂载' : '未挂载'),
|
|
3279
|
+
createElement('span', { className: 'tag ' + (packageOk ? 'live' : 'dead') },
|
|
3280
|
+
packageOk
|
|
3281
|
+
? 'provider 包' + (backend.providerPackage.version ? ' v' + backend.providerPackage.version : ' ✓')
|
|
3282
|
+
: 'provider 包 ✗'),
|
|
3283
|
+
createElement('span', { className: 'tag ' + (runner.ok ? 'live' : 'warn') },
|
|
3284
|
+
runner.ok ? 'CLI 依赖 ✓' : 'CLI 依赖 ✗'),
|
|
3285
|
+
createElement('span', { className: 'tag ' + (cli.ok ? 'model' : 'warn') },
|
|
3286
|
+
cli.ok ? 'PATH ' + cli.version : 'PATH ✗ ' + backend.cliCommand),
|
|
3287
|
+
),
|
|
3288
|
+
createElement('span', { style: { display: 'flex', gap: '5px', flex: 'none' } },
|
|
3289
|
+
depsMissing
|
|
3290
|
+
? createElement('button', {
|
|
3291
|
+
type: 'button', className: 'btn sm',
|
|
3292
|
+
disabled: busy,
|
|
3293
|
+
title: 'npm install -g 全局安装缺失包:' + missingLabel,
|
|
3294
|
+
onClick: onInstall,
|
|
3295
|
+
}, busy ? createElement(Spinner, { key: 'spin' }) : null, busy ? '安装中…' : '安装依赖包')
|
|
3296
|
+
: null,
|
|
3297
|
+
backend.mounted
|
|
3298
|
+
? createElement('button', { type: 'button', className: 'btn sm primary', disabled: busy, onClick: onSave },
|
|
3299
|
+
busy ? createElement(Spinner, { key: 'spin' }) : null, '保存配置')
|
|
3300
|
+
: packageOk
|
|
3301
|
+
? createElement('button', { type: 'button', className: 'btn sm primary', disabled: busy, onClick: onSave },
|
|
3302
|
+
busy ? createElement(Spinner, { key: 'spin' }) : null, '挂载')
|
|
3303
|
+
: null,
|
|
3304
|
+
backend.mounted
|
|
3305
|
+
? createElement(ConfirmButton, { label: '卸载', confirmLabel: '确认卸载?', disabled: busy, onConfirm: onUnmount })
|
|
3306
|
+
: null,
|
|
3307
|
+
)
|
|
3308
|
+
),
|
|
3309
|
+
expanded ? createElement('div', { className: 'cli-detail', key: 'detail' },
|
|
3310
|
+
createElement('div', { className: 'form-grid' },
|
|
3311
|
+
createElement('div', { className: 'field' },
|
|
3312
|
+
createElement('span', { className: 'field-label' }, 'providerName(执行后端注册名)'),
|
|
3313
|
+
createElement('input', {
|
|
3314
|
+
className: 'input', value: draft.providerName,
|
|
3315
|
+
placeholder: '如 codex / claude-code(子智能体表单的执行后端选项)',
|
|
3316
|
+
onChange: function (event) { onPatch({ providerName: event.target.value }) },
|
|
3317
|
+
}),
|
|
3318
|
+
),
|
|
3319
|
+
createElement('div', { className: 'field' },
|
|
3320
|
+
createElement('span', { className: 'field-label' }, 'permissionMode(CLI 权限模式)'),
|
|
3321
|
+
createElement('select', {
|
|
3322
|
+
className: 'input', value: modeValue,
|
|
3323
|
+
onChange: function (event) { onPatch({ permissionMode: event.target.value }) },
|
|
3324
|
+
}, backend.permissionModes.map(function (mode) {
|
|
3325
|
+
return createElement('option', { key: mode, value: mode }, mode)
|
|
3326
|
+
})),
|
|
3327
|
+
createElement('span', { className: 'field-hint' }, '枚举来自该 provider 包的 Config schema')
|
|
3328
|
+
),
|
|
3329
|
+
createElement('div', { className: 'field' },
|
|
3330
|
+
createElement('span', { className: 'field-label' }, 'disposeGraceMs(进程树终止宽限,毫秒)'),
|
|
3331
|
+
createElement('input', {
|
|
3332
|
+
className: 'input', value: draft.disposeGraceMs,
|
|
3333
|
+
placeholder: '3000',
|
|
3334
|
+
onChange: function (event) { onPatch({ disposeGraceMs: event.target.value }) },
|
|
3335
|
+
}),
|
|
3336
|
+
),
|
|
3337
|
+
),
|
|
3338
|
+
createElement(EnvPairsEditor, {
|
|
3339
|
+
pairs: draft.envPairs,
|
|
3340
|
+
onPatchPair: onPatchEnvPair,
|
|
3341
|
+
onAdd: onAddEnvPair,
|
|
3342
|
+
onRemove: onRemoveEnvPair,
|
|
3343
|
+
}),
|
|
3344
|
+
) : null,
|
|
3345
|
+
)
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
/** Draft shape for one generic external-CLI backend's editable config. */
|
|
3349
|
+
function cliDraftFromGeneric(backend) {
|
|
3350
|
+
backend = backend || {}
|
|
3351
|
+
return {
|
|
3352
|
+
command: backend.command !== undefined ? String(backend.command) : '',
|
|
3353
|
+
argsText: Array.isArray(backend.args) ? backend.args.join(' ') : '{prompt}',
|
|
3354
|
+
providerName: backend.providerName !== undefined ? String(backend.providerName) : '',
|
|
3355
|
+
disposeGraceMs: backend.disposeGraceMs !== undefined ? String(backend.disposeGraceMs) : '3000',
|
|
3356
|
+
envPairs: Object.keys(backend.env || {}).map(function (key) {
|
|
3357
|
+
return { key: key, value: String(backend.env[key]) }
|
|
3358
|
+
}),
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
/** Convert a generic draft into the wire config object (args split on whitespace). */
|
|
3363
|
+
function cliConfigFromGenericDraft(draft) {
|
|
3364
|
+
var config = {
|
|
3365
|
+
command: draft.command.trim(),
|
|
3366
|
+
args: draft.argsText.trim().split(/\s+/).filter(Boolean),
|
|
3367
|
+
providerName: draft.providerName.trim(),
|
|
3368
|
+
disposeGraceMs: Number(draft.disposeGraceMs.trim()),
|
|
3369
|
+
env: {},
|
|
3370
|
+
}
|
|
3371
|
+
draft.envPairs.forEach(function (pair) {
|
|
3372
|
+
var key = pair.key.trim()
|
|
3373
|
+
if (key !== '') config.env[key] = pair.value
|
|
3374
|
+
})
|
|
3375
|
+
return config
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
/** One generic external-CLI backend card (served by this plugin's command provider). */
|
|
3379
|
+
function GenericCliCard(props) {
|
|
3380
|
+
var backend = props.backend
|
|
3381
|
+
var draft = props.draft
|
|
3382
|
+
var busy = props.busy === true
|
|
3383
|
+
var expanded = props.expanded === true
|
|
3384
|
+
var onToggle = props.onToggle
|
|
3385
|
+
var onPatch = props.onPatch
|
|
3386
|
+
var onPatchEnvPair = props.onPatchEnvPair
|
|
3387
|
+
var onAddEnvPair = props.onAddEnvPair
|
|
3388
|
+
var onRemoveEnvPair = props.onRemoveEnvPair
|
|
3389
|
+
var onSave = props.onSave
|
|
3390
|
+
var onUnmount = props.onUnmount
|
|
3391
|
+
|
|
3392
|
+
var cli = backend.cli || { ok: false, version: null }
|
|
3393
|
+
|
|
3394
|
+
return createElement('div', { className: 'card', key: backend.id },
|
|
3395
|
+
createElement('div', { className: 'card-header' },
|
|
3396
|
+
createElement('button', {
|
|
3397
|
+
type: 'button', className: 'cli-toggle',
|
|
3398
|
+
'aria-expanded': expanded,
|
|
3399
|
+
'aria-label': expanded ? '收起明细' : '展开明细',
|
|
3400
|
+
title: expanded ? '收起明细' : '展开明细',
|
|
3401
|
+
onClick: onToggle,
|
|
3402
|
+
}, expanded ? '▾' : '▸'),
|
|
3403
|
+
createElement('span', { className: 'cli-title', onClick: onToggle }, '⌨️ ' + backend.command),
|
|
3404
|
+
createElement('span', { className: 'cli-tags' },
|
|
3405
|
+
createElement('span', { className: 'tag live' }, '已挂载'),
|
|
3406
|
+
createElement('span', { className: 'tag ' + (backend.providerPresent ? 'live' : 'warn') },
|
|
3407
|
+
backend.providerPresent ? '后端在线 ✓' : '后端未注册'),
|
|
3408
|
+
createElement('span', { className: 'tag ' + (cli.ok ? 'model' : 'warn') },
|
|
3409
|
+
cli.ok ? 'PATH ' + cli.version : 'PATH ✗ ' + backend.command),
|
|
3410
|
+
),
|
|
3411
|
+
createElement('span', { style: { display: 'flex', gap: '5px', flex: 'none' } },
|
|
3412
|
+
createElement('button', { type: 'button', className: 'btn sm primary', disabled: busy, onClick: onSave },
|
|
3413
|
+
busy ? createElement(Spinner, { key: 'spin' }) : null, '保存配置'),
|
|
3414
|
+
createElement(ConfirmButton, { label: '卸载', confirmLabel: '确认卸载?', disabled: busy, onConfirm: onUnmount }),
|
|
3415
|
+
)
|
|
3416
|
+
),
|
|
3417
|
+
expanded ? createElement('div', { className: 'cli-detail', key: 'detail' },
|
|
3418
|
+
createElement('div', { className: 'form-grid' },
|
|
3419
|
+
createElement('div', { className: 'field' },
|
|
3420
|
+
createElement('span', { className: 'field-label' }, 'command(PATH 命令名或绝对路径)'),
|
|
3421
|
+
createElement('input', {
|
|
3422
|
+
className: 'input', value: draft.command,
|
|
3423
|
+
placeholder: '如 gemini / qwen / C:\\tools\\aider.exe',
|
|
3424
|
+
onChange: function (event) { onPatch({ command: event.target.value }) },
|
|
3425
|
+
}),
|
|
3426
|
+
),
|
|
3427
|
+
createElement('div', { className: 'field' },
|
|
3428
|
+
createElement('span', { className: 'field-label' }, 'providerName(执行后端注册名)'),
|
|
3429
|
+
createElement('input', {
|
|
3430
|
+
className: 'input', value: draft.providerName,
|
|
3431
|
+
placeholder: '如 cli-gemini(子智能体表单的执行后端选项)',
|
|
3432
|
+
onChange: function (event) { onPatch({ providerName: event.target.value }) },
|
|
3433
|
+
}),
|
|
3434
|
+
),
|
|
3435
|
+
createElement('div', { className: 'field full' },
|
|
3436
|
+
createElement('span', { className: 'field-label' }, 'args(空格分隔,{prompt} 占位提示词)'),
|
|
3437
|
+
createElement('input', {
|
|
3438
|
+
className: 'input', value: draft.argsText,
|
|
3439
|
+
placeholder: '-p {prompt}',
|
|
3440
|
+
onChange: function (event) { onPatch({ argsText: event.target.value }) },
|
|
3441
|
+
}),
|
|
3442
|
+
createElement('span', { className: 'field-hint' }, 'one-shot 纯文本:stdout 即委托结果,非零退出记为失败;prompt 经 {prompt} 传入'),
|
|
3443
|
+
),
|
|
3444
|
+
createElement('div', { className: 'field' },
|
|
3445
|
+
createElement('span', { className: 'field-label' }, 'disposeGraceMs(进程树终止宽限,毫秒)'),
|
|
3446
|
+
createElement('input', {
|
|
3447
|
+
className: 'input', value: draft.disposeGraceMs,
|
|
3448
|
+
placeholder: '3000',
|
|
3449
|
+
onChange: function (event) { onPatch({ disposeGraceMs: event.target.value }) },
|
|
3450
|
+
}),
|
|
3451
|
+
),
|
|
3452
|
+
),
|
|
3453
|
+
createElement(EnvPairsEditor, {
|
|
3454
|
+
pairs: draft.envPairs,
|
|
3455
|
+
onPatchPair: onPatchEnvPair,
|
|
3456
|
+
onAdd: onAddEnvPair,
|
|
3457
|
+
onRemove: onRemoveEnvPair,
|
|
3458
|
+
}),
|
|
3459
|
+
) : null,
|
|
3460
|
+
)
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
function CliPanel(props) {
|
|
3464
|
+
var call = props.call
|
|
3465
|
+
|
|
3466
|
+
var viewState = useState({ loading: true, error: null, data: null })
|
|
3467
|
+
var view = viewState[0]
|
|
3468
|
+
var setView = viewState[1]
|
|
3469
|
+
|
|
3470
|
+
var draftsState = useState({})
|
|
3471
|
+
var drafts = draftsState[0]
|
|
3472
|
+
var setDrafts = draftsState[1]
|
|
3473
|
+
|
|
3474
|
+
var busyState = useState({})
|
|
3475
|
+
var busy = busyState[0]
|
|
3476
|
+
var setBusy = busyState[1]
|
|
3477
|
+
|
|
3478
|
+
var customCommandState = useState('')
|
|
3479
|
+
var customCommand = customCommandState[0]
|
|
3480
|
+
var setCustomCommand = customCommandState[1]
|
|
3481
|
+
|
|
3482
|
+
var expandedState = useState({})
|
|
3483
|
+
var expanded = expandedState[0]
|
|
3484
|
+
var setExpanded = expandedState[1]
|
|
3485
|
+
|
|
3486
|
+
var toggleExpanded = function (backendId) {
|
|
3487
|
+
setExpanded(function (prev) {
|
|
3488
|
+
var next = Object.assign({}, prev)
|
|
3489
|
+
next[backendId] = prev[backendId] !== true
|
|
3490
|
+
return next
|
|
3491
|
+
})
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
var toastState = useState(null)
|
|
3495
|
+
var toast = toastState[0]
|
|
3496
|
+
var setToast = toastState[1]
|
|
3497
|
+
|
|
3498
|
+
useEffect(function () {
|
|
3499
|
+
if (!toast) return undefined
|
|
3500
|
+
var timer = setTimeout(function () { setToast(null) }, 4200)
|
|
3501
|
+
return function () { clearTimeout(timer) }
|
|
3502
|
+
}, [toast])
|
|
3503
|
+
|
|
3504
|
+
/** Adopt a detection payload: view data + reset drafts to the served configs. */
|
|
3505
|
+
var absorb = function (result) {
|
|
3506
|
+
setView({ loading: false, error: null, data: result })
|
|
3507
|
+
var next = {}
|
|
3508
|
+
;(result.backends || []).forEach(function (backend) {
|
|
3509
|
+
next[backend.id] = backend.kind === 'generic'
|
|
3510
|
+
? cliDraftFromGeneric(backend)
|
|
3511
|
+
: cliDraftFromConfig(backend.config)
|
|
3512
|
+
})
|
|
3513
|
+
setDrafts(next)
|
|
3514
|
+
setBusy({})
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
var reload = function () {
|
|
3518
|
+
setView(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
|
|
3519
|
+
call('subagentAdmin/cliList', {}).then(function (raw) {
|
|
3520
|
+
absorb(unwrap(raw))
|
|
3521
|
+
}).catch(function (error) {
|
|
3522
|
+
setView({ loading: false, error: String((error && error.message) || error), data: null })
|
|
3523
|
+
})
|
|
3524
|
+
}
|
|
3525
|
+
useEffect(reload, [])
|
|
3526
|
+
|
|
3527
|
+
var patchDraft = function (backendId, patch) {
|
|
3528
|
+
setDrafts(function (prev) {
|
|
3529
|
+
var current = prev[backendId] || cliDraftFromConfig(null)
|
|
3530
|
+
var next = Object.assign({}, prev)
|
|
3531
|
+
next[backendId] = Object.assign({}, current, patch)
|
|
3532
|
+
return next
|
|
3533
|
+
})
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
// Env-pair edits derive from the latest queued state (functional update), so
|
|
3537
|
+
// rapid consecutive edits can never overwrite each other via stale drafts.
|
|
3538
|
+
var patchEnvPair = function (backendId, index, patch) {
|
|
3539
|
+
setDrafts(function (prev) {
|
|
3540
|
+
var current = prev[backendId] || cliDraftFromConfig(null)
|
|
3541
|
+
var pairs = (current.envPairs || []).map(function (pair, i) {
|
|
3542
|
+
return i === index ? Object.assign({}, pair, patch) : pair
|
|
3543
|
+
})
|
|
3544
|
+
var next = Object.assign({}, prev)
|
|
3545
|
+
next[backendId] = Object.assign({}, current, { envPairs: pairs })
|
|
3546
|
+
return next
|
|
3547
|
+
})
|
|
3548
|
+
}
|
|
3549
|
+
var addEnvPair = function (backendId) {
|
|
3550
|
+
setDrafts(function (prev) {
|
|
3551
|
+
var current = prev[backendId] || cliDraftFromConfig(null)
|
|
3552
|
+
var next = Object.assign({}, prev)
|
|
3553
|
+
next[backendId] = Object.assign({}, current, { envPairs: (current.envPairs || []).concat([{ key: '', value: '' }]) })
|
|
3554
|
+
return next
|
|
3555
|
+
})
|
|
3556
|
+
}
|
|
3557
|
+
var removeEnvPair = function (backendId, index) {
|
|
3558
|
+
setDrafts(function (prev) {
|
|
3559
|
+
var current = prev[backendId] || cliDraftFromConfig(null)
|
|
3560
|
+
var next = Object.assign({}, prev)
|
|
3561
|
+
next[backendId] = Object.assign({}, current, { envPairs: (current.envPairs || []).filter(function (_, i) { return i !== index }) })
|
|
3562
|
+
return next
|
|
3563
|
+
})
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
var runUpsert = function (backend) {
|
|
3567
|
+
if (busy[backend.id]) return
|
|
3568
|
+
var draft = drafts[backend.id]
|
|
3569
|
+
if (!draft) return
|
|
3570
|
+
var payload
|
|
3571
|
+
if (backend.kind === 'generic') {
|
|
3572
|
+
if (!/^[^\s]+$/.test(draft.command.trim())) {
|
|
3573
|
+
setToast('command 不能包含空格(PATH 命令名或绝对路径)')
|
|
3574
|
+
return
|
|
3575
|
+
}
|
|
3576
|
+
var args = draft.argsText.trim().split(/\s+/).filter(Boolean)
|
|
3577
|
+
if (args.length === 0 || args.length > 20 || !args.every(function (arg) { return arg.length <= 256 })) {
|
|
3578
|
+
setToast('args 必须是 1-20 个非空片段(单条 ≤ 256 字符),用 {prompt} 占位提示词')
|
|
3579
|
+
return
|
|
3580
|
+
}
|
|
3581
|
+
if (args.indexOf('{prompt}') === -1) {
|
|
3582
|
+
setToast('args 必须包含 {prompt} 占位符(提示词将替换该占位符传入 CLI)')
|
|
3583
|
+
return
|
|
3584
|
+
}
|
|
3585
|
+
if (!/^[a-z][a-z0-9_-]{0,47}$/.test(draft.providerName.trim())) {
|
|
3586
|
+
setToast('providerName 必须是 1-48 位小写字母/数字/下划线/中划线且字母开头')
|
|
3587
|
+
return
|
|
3588
|
+
}
|
|
3589
|
+
if (!/^\d+$/.test(draft.disposeGraceMs.trim())) {
|
|
3590
|
+
setToast('disposeGraceMs 必须是非负整数(毫秒)')
|
|
3591
|
+
return
|
|
3592
|
+
}
|
|
3593
|
+
payload = { kind: 'generic', backendId: backend.id, config: cliConfigFromGenericDraft(draft) }
|
|
3594
|
+
} else {
|
|
3595
|
+
if (!/^[a-z][a-z0-9_-]{0,47}$/.test(draft.providerName.trim())) {
|
|
3596
|
+
setToast('providerName 必须是 1-48 位小写字母/数字/下划线/中划线且字母开头')
|
|
3597
|
+
return
|
|
3598
|
+
}
|
|
3599
|
+
if (!/^\d+$/.test(draft.disposeGraceMs.trim())) {
|
|
3600
|
+
setToast('disposeGraceMs 必须是非负整数(毫秒)')
|
|
3601
|
+
return
|
|
3602
|
+
}
|
|
3603
|
+
payload = { backendId: backend.id, config: cliConfigFromDraft(draft) }
|
|
3604
|
+
}
|
|
3605
|
+
var busyPatch = {}
|
|
3606
|
+
busyPatch[backend.id] = true
|
|
3607
|
+
setBusy(Object.assign({}, busy, busyPatch))
|
|
3608
|
+
call('subagentAdmin/cliUpsert', { payload: payload }).then(function (raw) {
|
|
3609
|
+
absorb(unwrap(raw))
|
|
3610
|
+
}).catch(function (error) {
|
|
3611
|
+
setBusy({})
|
|
3612
|
+
setToast('保存失败:' + String((error && error.message) || error))
|
|
3613
|
+
})
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
var runUnmount = function (backend) {
|
|
3617
|
+
if (busy[backend.id]) return
|
|
3618
|
+
var busyPatch = {}
|
|
3619
|
+
busyPatch[backend.id] = true
|
|
3620
|
+
setBusy(Object.assign({}, busy, busyPatch))
|
|
3621
|
+
// Generic backends are recognized server-side by their "cli-" id prefix.
|
|
3622
|
+
call('subagentAdmin/cliRemove', { id: backend.id }).then(function (raw) {
|
|
3623
|
+
absorb(unwrap(raw))
|
|
3624
|
+
}).catch(function (error) {
|
|
3625
|
+
setBusy({})
|
|
3626
|
+
setToast('卸载失败:' + String((error && error.message) || error))
|
|
3627
|
+
})
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
var runInstall = function (backend) {
|
|
3631
|
+
if (busy[backend.id]) return
|
|
3632
|
+
var busyPatch = {}
|
|
3633
|
+
busyPatch[backend.id] = true
|
|
3634
|
+
setBusy(Object.assign({}, busy, busyPatch))
|
|
3635
|
+
call('subagentAdmin/cliInstall', { backendId: backend.id }).then(function (raw) {
|
|
3636
|
+
var result = unwrap(raw)
|
|
3637
|
+
absorb(result)
|
|
3638
|
+
setToast((result && result.output ? result.output : '依赖包安装完成') + ',已重新检测')
|
|
3639
|
+
}).catch(function (error) {
|
|
3640
|
+
var message = String((error && error.message) || error)
|
|
3641
|
+
if (message.indexOf('404') !== -1) {
|
|
3642
|
+
message += '(宿主端未注册该接口:请重启 dsh 加载最新插件后重试)'
|
|
3643
|
+
}
|
|
3644
|
+
setBusy({})
|
|
3645
|
+
setToast('安装失败:' + message)
|
|
3646
|
+
})
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
var runGenericMount = function (command) {
|
|
3650
|
+
if (!/^[^\s]+$/.test(command)) {
|
|
3651
|
+
setToast('command 不能包含空格(PATH 命令名或绝对路径)')
|
|
3652
|
+
return
|
|
3653
|
+
}
|
|
3654
|
+
var busyPatch = { __scan__: true }
|
|
3655
|
+
setBusy(Object.assign({}, busy, busyPatch))
|
|
3656
|
+
call('subagentAdmin/cliUpsert', { payload: { kind: 'generic', config: { command: command } } }).then(function (raw) {
|
|
3657
|
+
absorb(unwrap(raw))
|
|
3658
|
+
setCustomCommand('')
|
|
3659
|
+
}).catch(function (error) {
|
|
3660
|
+
setBusy({})
|
|
3661
|
+
setToast('挂载失败:' + String((error && error.message) || error))
|
|
3662
|
+
})
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
var children = []
|
|
3666
|
+
|
|
3667
|
+
// 本机 CLI card sits on top: scan list + custom command + refresh; the
|
|
3668
|
+
// builtin codex/claude-code cards and mounted generic cards follow below.
|
|
3669
|
+
var backends = (view.data && view.data.backends) || []
|
|
3670
|
+
var others = (view.data && view.data.others) || []
|
|
3671
|
+
var busyAny = Object.keys(busy).some(function (key) { return busy[key] === true })
|
|
3672
|
+
var genericBackends = backends.filter(function (backend) { return backend.kind === 'generic' })
|
|
3673
|
+
|
|
3674
|
+
children.push(createElement('div', { className: 'card', key: 'local-cli' },
|
|
3675
|
+
createElement('div', { className: 'card-header' },
|
|
3676
|
+
createElement('span', { className: 'card-title' }, '🛰️ 本机 CLI'),
|
|
3677
|
+
createElement('span', { className: 'tag' }, '通用命令行后端 · one-shot 纯文本'),
|
|
3678
|
+
createElement('span', { style: { marginLeft: 'auto', display: 'flex' } },
|
|
3679
|
+
createElement('button', { type: 'button', className: 'btn sm', onClick: reload, disabled: view.loading },
|
|
3680
|
+
view.loading ? createElement(Spinner, { key: 'spin' }) : '⟳', '重新检测')
|
|
3681
|
+
)
|
|
3682
|
+
),
|
|
3683
|
+
createElement('div', { className: 'cli-scan-hint' },
|
|
3684
|
+
'检测并挂载 harness 内置的外部 CLI 后端(codex / claude-code)与本机其他命令行工具;挂载后即可在「子智能体」表单的执行后端下拉中选用。'),
|
|
3685
|
+
createElement('div', { className: 'cli-scan-list' },
|
|
3686
|
+
createElement('div', { className: 'cli-scan-row', key: 'custom' },
|
|
3687
|
+
createElement('input', {
|
|
3688
|
+
className: 'input', value: customCommand,
|
|
3689
|
+
placeholder: '添加自定义 CLI:输入命令名或绝对路径,如 aider(挂载为 one-shot 后端)',
|
|
3690
|
+
onChange: function (event) { setCustomCommand(event.target.value) },
|
|
3691
|
+
}),
|
|
3692
|
+
createElement('button', {
|
|
3693
|
+
type: 'button', className: 'btn sm',
|
|
3694
|
+
disabled: busyAny || customCommand.trim() === '',
|
|
3695
|
+
onClick: function () { runGenericMount(customCommand.trim()) },
|
|
3696
|
+
}, '挂载'),
|
|
3697
|
+
),
|
|
3698
|
+
(function () {
|
|
3699
|
+
var mountedCommands = new Set(genericBackends.map(function (backend) {
|
|
3700
|
+
return backend.command.toLowerCase()
|
|
3701
|
+
}))
|
|
3702
|
+
return others
|
|
3703
|
+
.filter(function (item) { return !!(item.cli && item.cli.ok) })
|
|
3704
|
+
.filter(function (item) { return !mountedCommands.has(item.name.toLowerCase()) })
|
|
3705
|
+
.map(function (item) {
|
|
3706
|
+
return createElement('div', { className: 'cli-scan-card', key: item.name },
|
|
3707
|
+
createElement('span', { className: 'cli-title' }, '⌨️ ' + item.name),
|
|
3708
|
+
createElement('span', { className: 'cli-tags' },
|
|
3709
|
+
createElement('span', { className: 'tag' }, '未挂载'),
|
|
3710
|
+
createElement('span', { className: 'tag live' }, 'PATH ' + item.cli.version),
|
|
3711
|
+
),
|
|
3712
|
+
createElement('button', {
|
|
3713
|
+
type: 'button', className: 'btn sm', disabled: busyAny,
|
|
3714
|
+
onClick: function () { runGenericMount(item.name) },
|
|
3715
|
+
}, '挂载'),
|
|
3716
|
+
)
|
|
3717
|
+
})
|
|
3718
|
+
})(),
|
|
3719
|
+
others.length > 0 && others.every(function (item) { return !(item.cli && item.cli.ok) })
|
|
3720
|
+
? createElement('span', { className: 'tag', key: 'none' }, '未检测到其他 agent CLI')
|
|
3721
|
+
: null,
|
|
3722
|
+
),
|
|
3723
|
+
))
|
|
3724
|
+
|
|
3725
|
+
if (view.error) {
|
|
3726
|
+
children.push(createElement('div', { className: 'error-strip', key: 'error' }, '⚠️ 加载失败:', view.error))
|
|
3727
|
+
} else {
|
|
3728
|
+
genericBackends.forEach(function (backend) {
|
|
3729
|
+
children.push(GenericCliCard({
|
|
3730
|
+
backend: backend,
|
|
3731
|
+
draft: drafts[backend.id] || cliDraftFromGeneric(backend),
|
|
3732
|
+
busy: busy[backend.id] === true,
|
|
3733
|
+
expanded: expanded[backend.id] === true,
|
|
3734
|
+
onToggle: function () { toggleExpanded(backend.id) },
|
|
3735
|
+
onPatch: function (patch) { patchDraft(backend.id, patch) },
|
|
3736
|
+
onPatchEnvPair: function (index, patch) { patchEnvPair(backend.id, index, patch) },
|
|
3737
|
+
onAddEnvPair: function () { addEnvPair(backend.id) },
|
|
3738
|
+
onRemoveEnvPair: function (index) { removeEnvPair(backend.id, index) },
|
|
3739
|
+
onSave: function () { runUpsert(backend) },
|
|
3740
|
+
onUnmount: function () { runUnmount(backend) },
|
|
3741
|
+
}))
|
|
3742
|
+
})
|
|
3743
|
+
backends.filter(function (backend) { return backend.kind !== 'generic' }).forEach(function (backend) {
|
|
3744
|
+
children.push(CliBackendCard({
|
|
3745
|
+
backend: backend,
|
|
3746
|
+
draft: drafts[backend.id] || cliDraftFromConfig(backend.config),
|
|
3747
|
+
busy: busy[backend.id] === true,
|
|
3748
|
+
expanded: expanded[backend.id] === true,
|
|
3749
|
+
onToggle: function () { toggleExpanded(backend.id) },
|
|
3750
|
+
onPatch: function (patch) { patchDraft(backend.id, patch) },
|
|
3751
|
+
onPatchEnvPair: function (index, patch) { patchEnvPair(backend.id, index, patch) },
|
|
3752
|
+
onAddEnvPair: function () { addEnvPair(backend.id) },
|
|
3753
|
+
onRemoveEnvPair: function (index) { removeEnvPair(backend.id, index) },
|
|
3754
|
+
onSave: function () { runUpsert(backend) },
|
|
3755
|
+
onInstall: function () { runInstall(backend) },
|
|
3756
|
+
onUnmount: function () { runUnmount(backend) },
|
|
3757
|
+
}))
|
|
3758
|
+
})
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
return createElement('div', null,
|
|
3762
|
+
toast ? createElement('div', { className: 'toast' }, toast) : null,
|
|
3763
|
+
children
|
|
3764
|
+
)
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
/* ========================================================================== */
|
|
3768
|
+
/* Section entrypoint */
|
|
3769
|
+
/* ========================================================================== */
|
|
3770
|
+
|
|
3771
|
+
function SubagentAdminSection(props) {
|
|
3772
|
+
var tabState = useState('subagents')
|
|
3773
|
+
var activeTab = tabState[0]
|
|
3774
|
+
var setActiveTab = tabState[1]
|
|
3775
|
+
var tabs = [
|
|
3776
|
+
{ id: 'subagents', label: '子智能体', component: SubagentsPanel },
|
|
3777
|
+
{ id: 'cli', label: 'CLI 后端', component: CliPanel },
|
|
3778
|
+
{ id: 'history', label: '变更记录', component: HistoryPanel },
|
|
3779
|
+
]
|
|
3780
|
+
var selected = tabs.find(function (tab) { return tab.id === activeTab }) || tabs[0]
|
|
3781
|
+
|
|
3782
|
+
return createElement('div', { 'data-dsh-sa-section': '' },
|
|
3783
|
+
createElement('div', { className: 'tabs', role: 'tablist', 'aria-label': '子智能体管理' },
|
|
3784
|
+
tabs.map(function (tab) {
|
|
3785
|
+
return createElement('button', {
|
|
3786
|
+
type: 'button', key: tab.id,
|
|
3787
|
+
className: 'tab' + (tab.id === selected.id ? ' active' : ''),
|
|
3788
|
+
role: 'tab', 'aria-selected': tab.id === selected.id,
|
|
3789
|
+
onClick: function () { setActiveTab(tab.id) },
|
|
3790
|
+
}, tab.label)
|
|
3791
|
+
})
|
|
3792
|
+
),
|
|
3793
|
+
createElement(selected.component, { key: selected.id, call: props.call })
|
|
3794
|
+
)
|
|
3795
|
+
}
|
|
3796
|
+
|
|
2281
3797
|
/* ========================================================================== */
|
|
2282
3798
|
/* Plugin Entrypoint */
|
|
2283
3799
|
/* ========================================================================== */
|
|
2284
3800
|
|
|
2285
3801
|
function apply(ctx) {
|
|
2286
3802
|
injectStyles()
|
|
3803
|
+
injectSaStyles()
|
|
2287
3804
|
|
|
2288
3805
|
var call = function (method, args) {
|
|
2289
3806
|
return ctx.connection.rpc.call('/api', method, { args: args })
|
|
@@ -2304,11 +3821,11 @@ function apply(ctx) {
|
|
|
2304
3821
|
} catch (e) { refreshSessions = null }
|
|
2305
3822
|
var disposeSidebar = ctx.effect(function () { return setupMenuInjection(call, refreshSessions) })
|
|
2306
3823
|
|
|
2307
|
-
//
|
|
2308
|
-
// the shell
|
|
2309
|
-
//
|
|
2310
|
-
//
|
|
2311
|
-
//
|
|
3824
|
+
// Four surfaces, each parked where the shell already renders its domain.
|
|
3825
|
+
// 插件管理 joins the shell-owned 插件 section as its third tab (插件配置 0,
|
|
3826
|
+
// 插件列表 10, 扩展插件 20); MCP / 子智能体 / 历史会话 become standalone
|
|
3827
|
+
// settings pages (MCP服务器 takes the 25 slot right after Agent 预设,
|
|
3828
|
+
// 子智能体 follows at 26, 历史会话 sorts last). inject() waits on each
|
|
2312
3829
|
// slot's declaration, so registration order never matters.
|
|
2313
3830
|
ctx.slots.inject('settings.plugins.tab', function () {
|
|
2314
3831
|
return ctx.slots.register({
|
|
@@ -2337,6 +3854,15 @@ function apply(ctx) {
|
|
|
2337
3854
|
inject: function () { return { call: call, refreshSessions: refreshSessions } },
|
|
2338
3855
|
}, SessionsSection)
|
|
2339
3856
|
})
|
|
3857
|
+
ctx.slots.inject('settings.section', function () {
|
|
3858
|
+
return ctx.slots.register({
|
|
3859
|
+
name: 'settings.section',
|
|
3860
|
+
id: 'subagent-admin',
|
|
3861
|
+
order: 26,
|
|
3862
|
+
label: '子智能体',
|
|
3863
|
+
inject: function () { return { call: call } },
|
|
3864
|
+
}, SubagentAdminSection)
|
|
3865
|
+
})
|
|
2340
3866
|
}
|
|
2341
3867
|
|
|
2342
3868
|
module.exports = { apply: apply, inject: ['slots', 'connection'] }
|