@menghuan1918/dsh-apollo 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +232 -2
- package/cordis.patch.yml +78 -0
- package/docs/DEVELOPMENT.md +629 -0
- package/package.json +75 -4
- package/presets/eng/NOTICE +2 -0
- package/presets/eng/plugins/exit-guard/index.js +111 -0
- package/presets/eng/plugins/exit-guard/package.json +7 -0
- package/presets/eng/plugins/mode-instructions/index.js +48 -0
- package/presets/eng/plugins/mode-instructions/package.json +7 -0
- package/presets/eng/plugins/wait-subagent/index.js +228 -0
- package/presets/eng/plugins/wait-subagent/package.json +7 -0
- package/presets/eng-noask/NOTICE +2 -0
- package/presets/eng-noask/plugins/exit-guard/index.js +111 -0
- package/presets/eng-noask/plugins/exit-guard/package.json +7 -0
- package/presets/eng-noask/plugins/mode-instructions/index.js +48 -0
- package/presets/eng-noask/plugins/mode-instructions/package.json +7 -0
- package/presets/eng-noask/plugins/wait-subagent/index.js +228 -0
- package/presets/eng-noask/plugins/wait-subagent/package.json +7 -0
- package/presets/eng-noask.patch.yml +341 -0
- package/presets/eng.patch.yml +341 -0
- package/skills/brainstorming/SKILL.md +90 -0
- package/skills/brainstorming/spec-document-reviewer-prompt.md +50 -0
- package/skills/code-review/SKILL.md +90 -0
- package/skills/dev/SKILL.md +43 -0
- package/skills/diagnosing-bugs/SKILL.md +132 -0
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +55 -0
- package/skills/exec/SKILL.md +91 -0
- package/skills/handoff/SKILL.md +23 -0
- package/skills/main-agent-driven-development/SKILL.md +64 -0
- package/skills/tdd/SKILL.md +34 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/tests.md +77 -0
- package/src/cli/claude.js +166 -0
- package/src/cli/codex.js +164 -0
- package/src/cli/jsonl.js +216 -0
- package/src/cli-adapter.js +198 -0
- package/src/cli-sessions.js +69 -0
- package/src/client.js +815 -0
- package/src/config.js +301 -0
- package/src/dsh-home.js +38 -0
- package/src/index.js +181 -0
- package/src/panel.js +452 -0
- package/src/settings.js +191 -0
- package/src/subagents.js +199 -0
- package/third-party/LICENSE-MIT-dsh-standard-preset +21 -0
- package/third-party/NOTICE-mattpocock-skills +21 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-apollo (npm: @menghuan1918/dsh-apollo) — 浏览器半边:官方 Plugins 页的行配置表单。
|
|
3
|
+
*
|
|
4
|
+
* 宿主行导出正式 volatile Config,dsh-settings 把它投影成行配置表单;本文件注册
|
|
5
|
+
* `plugins.row.config`(键 `<npm 包名>#apollo-skills`,与 src/config.js 的
|
|
6
|
+
* ROW_CONFIG_KEY/LEGACY_ROW_CONFIG_KEY 同步),自画紧凑表单并只在保存时一次提交全部改动。零构建:
|
|
7
|
+
* closure factory,只 require('react'),不 import 任何 DSH client 包;样式随组件
|
|
8
|
+
* 树内一个 <style> 渲染(不写 DOM)。注册 id 必须等于 npm 包名(client closure id)。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
window.__ModuleLoader__.load({
|
|
12
|
+
id: '@menghuan1918/dsh-apollo',
|
|
13
|
+
factory: (require) => {
|
|
14
|
+
var module = { exports: {} };
|
|
15
|
+
var exports = module.exports;
|
|
16
|
+
const { createElement: h, useEffect, useState } = require('react');
|
|
17
|
+
|
|
18
|
+
const NS = 'apollo';
|
|
19
|
+
// 新键是 Plugins 页按“bundle 依赖名 + 行 id”算出的(= npm 包名 + #apollo-skills);
|
|
20
|
+
// 旧键并排注册,只为仍把依赖槽记成旧 unscoped 名的 profile 保留配置入口。
|
|
21
|
+
const ROW_KEYS = ['@menghuan1918/dsh-apollo#apollo-skills', 'dsh-apollo#apollo-skills'];
|
|
22
|
+
const CAPABILITIES_URL = '/apollo/api/capabilities';
|
|
23
|
+
|
|
24
|
+
/** Codex CLI 权限档(与 src/config.js 的 CODEX_PERMISSION_MODES 同步)。 */
|
|
25
|
+
const CODEX = {
|
|
26
|
+
cli: 'codex',
|
|
27
|
+
executable: 'codex',
|
|
28
|
+
permission: 'read-only',
|
|
29
|
+
permissions: ['read-only', 'workspace-write', 'danger-full-access'],
|
|
30
|
+
profile: true,
|
|
31
|
+
};
|
|
32
|
+
/** Claude Code CLI 权限档(与 src/config.js 的 CLAUDE_PERMISSION_MODES 同步)。 */
|
|
33
|
+
const CLAUDE = {
|
|
34
|
+
cli: 'claude',
|
|
35
|
+
executable: 'claude',
|
|
36
|
+
permission: 'plan',
|
|
37
|
+
permissions: ['plan', 'manual', 'dontAsk', 'acceptEdits', 'auto', 'bypassPermissions'],
|
|
38
|
+
profile: false,
|
|
39
|
+
};
|
|
40
|
+
const BACKENDS = [CODEX, CLAUDE];
|
|
41
|
+
|
|
42
|
+
const zh = {
|
|
43
|
+
summary: '工程模式运行时设置:退出守卫、子代理 CLI 后端与接入范围',
|
|
44
|
+
title: '工程模式运行时设置',
|
|
45
|
+
intro: '保存后即时生效,无需重启。配置写入当前 profile 的行配置(cordis.patch.yml)。',
|
|
46
|
+
loading: '正在读取配置…',
|
|
47
|
+
waitingForm: '正在等待宿主提供这一行的配置快照…',
|
|
48
|
+
noFormHint: '长时间停在这里说明宿主没有为这一行提供可编辑配置:本部署没组合 dsh-settings/config-editor,或本行的 Config 未被识别。',
|
|
49
|
+
unavailable: '当前连接下该配置不可用:请通过 loopback 地址访问,或在行配置里把该权威加入 trustedHosts。',
|
|
50
|
+
readonly: '当前连接只读(设置镜像为进程内 memory),无法写入宿主配置。',
|
|
51
|
+
exitGuardLabel: '退出守卫',
|
|
52
|
+
exitGuardHint: '回合结束时若仍有运行中的子代理或后台命令,注入提醒强制继续处理;用户中断(Esc)始终放行。',
|
|
53
|
+
scopeTitle: '接入范围',
|
|
54
|
+
scopeEng: '工程模式预设',
|
|
55
|
+
scopeEngHint: 'eng / eng-noask 会话可以看到下面这些 CLI 子代理路由。',
|
|
56
|
+
scopeGlobal: '全部预设',
|
|
57
|
+
scopeGlobalHint: '所有预设的会话都能看到这些路由(影响面更大)。',
|
|
58
|
+
scopeNone: '两个范围都关闭时,这些 CLI 路由不会出现在任何会话里。',
|
|
59
|
+
codexTitle: 'Codex 后端',
|
|
60
|
+
claudeTitle: 'Claude 后端',
|
|
61
|
+
backendEnable: '启用',
|
|
62
|
+
executableLabel: '可执行文件',
|
|
63
|
+
executableHint: '只写命令名则从 PATH 查找;也可手输绝对路径或 ~ 开头的路径。',
|
|
64
|
+
profileLabel: 'Profile',
|
|
65
|
+
profileHint: '名称来自 CODEX_HOME 下的 <name>.config.toml 与旧式 [profiles.*] 段。',
|
|
66
|
+
profileNone: '(不指定)',
|
|
67
|
+
permissionLabel: '权限档',
|
|
68
|
+
permissionHint: '完全由这里的设置决定,不继承会话的权限状态。',
|
|
69
|
+
defaultModelLabel: '默认模型',
|
|
70
|
+
defaultModelNone: '(未指定)',
|
|
71
|
+
modelsLabel: '模型与 effort',
|
|
72
|
+
modelsHint: '每行一个模型:id + 允许的 effort(逗号分隔)+ 默认 effort。',
|
|
73
|
+
modelsEmpty: '还没有配置模型;启用后端前至少加一个模型。',
|
|
74
|
+
modelIdPlaceholder: '模型 id',
|
|
75
|
+
modelEffortsPlaceholder: 'low, medium, high',
|
|
76
|
+
modelDefaultEffort: '默认 effort',
|
|
77
|
+
modelEffortNone: '(无默认)',
|
|
78
|
+
modelAdd: '添加模型',
|
|
79
|
+
modelRemove: '移除',
|
|
80
|
+
probing: '正在探测本机可执行文件…',
|
|
81
|
+
detected: '已检测到 {path}({version})',
|
|
82
|
+
detectedNoVersion: '已检测到 {path}(版本未知)',
|
|
83
|
+
missing: 'PATH 上没找到 {executable},可直接填绝对路径。',
|
|
84
|
+
capabilitiesError: '读取本机探测信息失败(不影响保存)。',
|
|
85
|
+
save: '保存',
|
|
86
|
+
saving: '保存中…',
|
|
87
|
+
reset: '放弃改动',
|
|
88
|
+
saved: '已保存,即时生效。',
|
|
89
|
+
conflict: '宿主拒绝了这次写入:配置可能已被其他改动更新或被更高优先级配置覆盖,已重新读取宿主值。',
|
|
90
|
+
saveFailed: '保存失败:',
|
|
91
|
+
invalidEmptyId: '{backend}里有模型缺少 id,请补齐后再保存。',
|
|
92
|
+
invalidDuplicateId: '{backend}的模型 id 不能重复。',
|
|
93
|
+
invalidNoModels: '{backend}已启用,但还没有配置任何模型。',
|
|
94
|
+
invalidDefaultModel: '{backend}已启用,请把默认模型设为模型列表里的一个。',
|
|
95
|
+
invalidDefaultEffort: '{backend}的默认模型需要 effort 列表,并指定一个属于它的默认 effort。',
|
|
96
|
+
readonlyNote: '只读:当前无法写入宿主配置。',
|
|
97
|
+
unavailableNote: '不可写:当前连接下该配置不可用。',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const en = {
|
|
101
|
+
summary: 'Eng mode runtime settings: exit guard, CLI subagent backends, and scope',
|
|
102
|
+
title: 'Eng mode runtime settings',
|
|
103
|
+
intro: 'Saving applies immediately, no restart. Values persist in the active profile row config (cordis.patch.yml).',
|
|
104
|
+
loading: 'Loading configuration…',
|
|
105
|
+
waitingForm: 'Waiting for the host to provide this row’s configuration snapshot…',
|
|
106
|
+
noFormHint: 'Staying here means the host exposes no editable config for this row: dsh-settings/config-editor is not composed, or this row’s Config was not recognized.',
|
|
107
|
+
unavailable: 'This configuration is unavailable on the current connection: use a loopback address, or list this authority in the row config trustedHosts.',
|
|
108
|
+
readonly: 'This connection is read-only (settings mirror is process-local memory); host configuration cannot be written.',
|
|
109
|
+
exitGuardLabel: 'Exit guard',
|
|
110
|
+
exitGuardHint: 'Blocks turn end while subagents or background jobs are still running; interrupt (Esc) always passes through.',
|
|
111
|
+
scopeTitle: 'Scope',
|
|
112
|
+
scopeEng: 'Eng preset',
|
|
113
|
+
scopeEngHint: 'eng / eng-noask sessions see the CLI subagent routes below.',
|
|
114
|
+
scopeGlobal: 'All presets',
|
|
115
|
+
scopeGlobalHint: 'Every preset’s sessions see these routes (broader impact).',
|
|
116
|
+
scopeNone: 'With both scopes off, these CLI routes appear in no session.',
|
|
117
|
+
codexTitle: 'Codex backend',
|
|
118
|
+
claudeTitle: 'Claude backend',
|
|
119
|
+
backendEnable: 'Enabled',
|
|
120
|
+
executableLabel: 'Executable',
|
|
121
|
+
executableHint: 'A bare command name is resolved from PATH; an absolute or ~ path is also accepted.',
|
|
122
|
+
profileLabel: 'Profile',
|
|
123
|
+
profileHint: 'Names come from <name>.config.toml under CODEX_HOME and from legacy [profiles.*] sections.',
|
|
124
|
+
profileNone: '(none)',
|
|
125
|
+
permissionLabel: 'Permission',
|
|
126
|
+
permissionHint: 'Decided entirely by this setting; the session’s permission state is never inherited.',
|
|
127
|
+
defaultModelLabel: 'Default model',
|
|
128
|
+
defaultModelNone: '(unset)',
|
|
129
|
+
modelsLabel: 'Models and efforts',
|
|
130
|
+
modelsHint: 'One model per row: id + allowed efforts (comma separated) + default effort.',
|
|
131
|
+
modelsEmpty: 'No model yet; add at least one before enabling the backend.',
|
|
132
|
+
modelIdPlaceholder: 'model id',
|
|
133
|
+
modelEffortsPlaceholder: 'low, medium, high',
|
|
134
|
+
modelDefaultEffort: 'Default effort',
|
|
135
|
+
modelEffortNone: '(none)',
|
|
136
|
+
modelAdd: 'Add model',
|
|
137
|
+
modelRemove: 'Remove',
|
|
138
|
+
probing: 'Probing local executables…',
|
|
139
|
+
detected: 'Found {path} ({version})',
|
|
140
|
+
detectedNoVersion: 'Found {path} (version unknown)',
|
|
141
|
+
missing: '{executable} was not found on PATH; type an absolute path if needed.',
|
|
142
|
+
capabilitiesError: 'Could not read local probe data (saving still works).',
|
|
143
|
+
save: 'Save',
|
|
144
|
+
saving: 'Saving…',
|
|
145
|
+
reset: 'Discard',
|
|
146
|
+
saved: 'Saved and applied.',
|
|
147
|
+
conflict: 'The host refused this write: the configuration changed elsewhere or a higher layer overrides it. Host values were reloaded.',
|
|
148
|
+
saveFailed: 'Save failed: ',
|
|
149
|
+
invalidEmptyId: 'Every model in {backend} needs an id before saving.',
|
|
150
|
+
invalidDuplicateId: 'Model ids in {backend} must be unique.',
|
|
151
|
+
invalidNoModels: '{backend} is enabled but has no model configured.',
|
|
152
|
+
invalidDefaultModel: '{backend} is enabled: set the default model to one of its model ids.',
|
|
153
|
+
invalidDefaultEffort: '{backend}’s default model needs an effort list and a default effort from that list.',
|
|
154
|
+
readonlyNote: 'Read-only: host configuration cannot be written.',
|
|
155
|
+
unavailableNote: 'Not writable: this configuration is unavailable on the current connection.',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// ── 文案(locale 服务缺失时保持中文默认)───────────────────────────────
|
|
159
|
+
|
|
160
|
+
function interpolate(text, params) {
|
|
161
|
+
if (params === undefined || params === null) return text;
|
|
162
|
+
return text.replace(/\{(\w+)\}/g, (match, key) => (key in params ? String(params[key]) : match));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let t = (key, params) => interpolate(zh[key] ?? key, params);
|
|
166
|
+
|
|
167
|
+
// ── 样式(组件树内的 <style>;复用 DSH 设计令牌,与官方设置页同观感)──────────
|
|
168
|
+
|
|
169
|
+
const STYLE_ID = '@menghuan1918/dsh-apollo/settings.css';
|
|
170
|
+
const CSS = [
|
|
171
|
+
'.apollo-cfg{display:flex;flex-direction:column;gap:12px;max-width:760px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary)}',
|
|
172
|
+
'.apollo-note{margin:0;padding:8px 10px;border-radius:var(--dsw-radius-md);font-size:12.5px;line-height:18px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}',
|
|
173
|
+
'.apollo-note[data-tone=error]{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary)}',
|
|
174
|
+
'.apollo-note[data-tone=warn]{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-label-secondary)}',
|
|
175
|
+
'.apollo-note[data-tone=ok]{color:var(--dsw-alias-state-success-primary)}',
|
|
176
|
+
'.apollo-pulse{animation:apollo-pulse 2s cubic-bezier(.36,0,.64,1) infinite}',
|
|
177
|
+
'@keyframes apollo-pulse{0%{opacity:1}40%{opacity:.6}80%,to{opacity:1}}',
|
|
178
|
+
'@media (prefers-reduced-motion:reduce){.apollo-pulse{animation:none}}',
|
|
179
|
+
'.apollo-title{margin:0;font-size:14px;font-weight:500;line-height:22px}',
|
|
180
|
+
'.apollo-hint{margin:0;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary)}',
|
|
181
|
+
'.apollo-intro{margin:0;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
|
|
182
|
+
'.apollo-section{display:flex;flex-direction:column;gap:2px;padding-top:12px;border-top:.5px solid var(--dsw-alias-border-l2)}',
|
|
183
|
+
'.apollo-sectionHead{display:flex;flex-direction:column;gap:2px;padding-bottom:4px}',
|
|
184
|
+
'.apollo-field{display:grid;grid-template-columns:minmax(120px,168px) minmax(0,1fr);gap:2px 12px;padding:6px 0;align-items:start}',
|
|
185
|
+
'.apollo-fieldLabel{padding-top:5px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}',
|
|
186
|
+
'.apollo-fieldBody{display:flex;flex-direction:column;gap:4px;min-width:0}',
|
|
187
|
+
'.apollo-input,.apollo-select{box-sizing:border-box;width:100%;min-width:0;height:28px;padding:0 8px;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md)}',
|
|
188
|
+
'.apollo-select{appearance:none;background-image:none;padding-right:24px}',
|
|
189
|
+
'.apollo-input::placeholder{color:var(--dsw-alias-label-dimmed)}',
|
|
190
|
+
'.apollo-input:focus,.apollo-select:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}',
|
|
191
|
+
'.apollo-input:focus-visible,.apollo-select:focus-visible,.apollo-switch:focus-visible,.apollo-button:focus-visible,.apollo-iconButton:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}',
|
|
192
|
+
'.apollo-input:disabled,.apollo-select:disabled{opacity:.4;cursor:not-allowed}',
|
|
193
|
+
'.apollo-input[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary)}',
|
|
194
|
+
'.apollo-switchRow{display:flex;align-items:flex-start;gap:12px;padding:6px 0}',
|
|
195
|
+
'.apollo-switchMain{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}',
|
|
196
|
+
'.apollo-switchLabel{font-size:13px;font-weight:500}',
|
|
197
|
+
'.apollo-switch{box-sizing:border-box;position:relative;flex:none;width:36px;height:20px;margin:2px 0 0;padding:2px;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer;appearance:none}',
|
|
198
|
+
'.apollo-switch[aria-checked=true]{background:var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary))}',
|
|
199
|
+
'.apollo-switch:disabled{opacity:.4;cursor:not-allowed}',
|
|
200
|
+
'.apollo-switchThumb{display:block;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary-foreground);transition:transform 120ms ease}',
|
|
201
|
+
'.apollo-switch[aria-checked=false] .apollo-switchThumb{background:var(--dsw-alias-switch-thumb,var(--dsw-alias-label-primary-foreground))}',
|
|
202
|
+
'.apollo-switch[aria-checked=true] .apollo-switchThumb{transform:translateX(16px)}',
|
|
203
|
+
'@media (prefers-reduced-motion:reduce){.apollo-switchThumb{transition:none}}',
|
|
204
|
+
'.apollo-card{display:flex;flex-direction:column;gap:2px;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1)}',
|
|
205
|
+
'.apollo-cardOff{opacity:.72}',
|
|
206
|
+
'.apollo-cardHead{display:flex;align-items:flex-start;gap:12px}',
|
|
207
|
+
'.apollo-cardTitle{flex:1;min-width:0;font-size:13.5px;font-weight:500}',
|
|
208
|
+
'.apollo-models{display:flex;flex-direction:column;gap:6px}',
|
|
209
|
+
'.apollo-model{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) minmax(0,132px) auto;gap:6px;align-items:center}',
|
|
210
|
+
'.apollo-modelHead{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) minmax(0,132px) auto;gap:6px;font-size:11.5px;color:var(--dsw-alias-label-tertiary)}',
|
|
211
|
+
'@media (max-width:640px){.apollo-field,.apollo-model,.apollo-modelHead{grid-template-columns:minmax(0,1fr)}.apollo-fieldLabel{padding-top:0}}',
|
|
212
|
+
'.apollo-button{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:28px;padding:0 10px;border:0;border-radius:var(--dsw-radius-sm);font:inherit;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);background:transparent;cursor:pointer}',
|
|
213
|
+
'.apollo-button:disabled{opacity:.4;cursor:not-allowed}',
|
|
214
|
+
'.apollo-button[data-variant=primary]{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-label-primary));color:var(--dsw-alias-label-primary-foreground,var(--dsw-alias-bg-layer-3))}',
|
|
215
|
+
'.apollo-button[data-variant=primary]:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-label-primary))}',
|
|
216
|
+
'.apollo-button[data-variant=outline]{border:.5px solid var(--dsw-alias-border-l3)}',
|
|
217
|
+
'.apollo-button[data-variant=outline]:hover:not(:disabled),.apollo-button[data-variant=ghost]:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
|
|
218
|
+
'.apollo-iconButton{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;font:inherit;font-size:14px;line-height:1}',
|
|
219
|
+
'.apollo-iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
220
|
+
'.apollo-iconButton:disabled{opacity:.4;cursor:not-allowed}',
|
|
221
|
+
'.apollo-footer{display:flex;align-items:center;gap:8px;padding-top:12px;border-top:.5px solid var(--dsw-alias-border-l2)}',
|
|
222
|
+
'.apollo-status{flex:1;min-width:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}',
|
|
223
|
+
'.apollo-status[data-tone=ok]{color:var(--dsw-alias-state-success-primary)}',
|
|
224
|
+
'.apollo-status[data-tone=error]{color:var(--dsw-alias-state-error-primary)}',
|
|
225
|
+
'.apollo-status[data-tone=warn]{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-label-secondary))}',
|
|
226
|
+
].join('\n');
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
// ── 取值/形状(与 src/config.js 的默认形状一致;浏览器半边不能 import 宿主模块)──
|
|
230
|
+
|
|
231
|
+
function isRecord(value) {
|
|
232
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function stringOr(value, fallback) {
|
|
236
|
+
return typeof value === 'string' ? value : fallback;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function modelList(value) {
|
|
240
|
+
if (!Array.isArray(value)) return [];
|
|
241
|
+
return value.filter(isRecord).map((entry) => {
|
|
242
|
+
const efforts = Array.isArray(entry.efforts) ? entry.efforts.filter((item) => typeof item === 'string') : [];
|
|
243
|
+
const defaultEffort = stringOr(entry.defaultEffort, '');
|
|
244
|
+
return {
|
|
245
|
+
id: stringOr(entry.id, ''),
|
|
246
|
+
efforts,
|
|
247
|
+
// 与 src/config.js 同规则:默认 effort 必须属于该模型的 effort 列表。
|
|
248
|
+
defaultEffort: efforts.indexOf(defaultEffort) >= 0 ? defaultEffort : '',
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function backendShape(value, spec) {
|
|
254
|
+
const source = isRecord(value) ? value : {};
|
|
255
|
+
const backend = {
|
|
256
|
+
enabled: source.enabled === true,
|
|
257
|
+
executable: typeof source.executable === 'string' && source.executable.trim() !== ''
|
|
258
|
+
? source.executable.trim()
|
|
259
|
+
: spec.executable,
|
|
260
|
+
};
|
|
261
|
+
if (spec.profile) backend.profile = stringOr(source.profile, '');
|
|
262
|
+
backend.permissionMode = spec.permissions.indexOf(source.permissionMode) >= 0
|
|
263
|
+
? source.permissionMode
|
|
264
|
+
: spec.permission;
|
|
265
|
+
backend.defaultModel = stringOr(source.defaultModel, '');
|
|
266
|
+
backend.models = modelList(source.models);
|
|
267
|
+
return backend;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 把宿主快照值(或 undefined)规整成表单可安全读写的形状。 */
|
|
271
|
+
function shape(value) {
|
|
272
|
+
const source = isRecord(value) ? value : {};
|
|
273
|
+
const subagents = isRecord(source.subagents) ? source.subagents : {};
|
|
274
|
+
const result = {
|
|
275
|
+
exitGuard: source.exitGuard === true,
|
|
276
|
+
subagents: {
|
|
277
|
+
eng: subagents.eng !== false,
|
|
278
|
+
global: subagents.global === true,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
for (const spec of BACKENDS) result.subagents[spec.cli] = backendShape(subagents[spec.cli], spec);
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** 逗号分隔的 effort 文本 → 去重后的字符串数组。 */
|
|
286
|
+
function parseEfforts(text) {
|
|
287
|
+
const result = [];
|
|
288
|
+
for (const part of String(text ?? '').split(',')) {
|
|
289
|
+
const trimmed = part.trim();
|
|
290
|
+
if (trimmed === '' || result.indexOf(trimmed) >= 0) continue;
|
|
291
|
+
result.push(trimmed);
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 只在用户按下保存时才写入:把差异摊成一组 set 操作(数组整段替换)。 */
|
|
297
|
+
function diffOps(host, current) {
|
|
298
|
+
const ops = [];
|
|
299
|
+
if (host.exitGuard !== current.exitGuard) {
|
|
300
|
+
ops.push({ op: 'set', path: ['exitGuard'], value: current.exitGuard });
|
|
301
|
+
}
|
|
302
|
+
for (const key of ['eng', 'global']) {
|
|
303
|
+
if (host.subagents[key] !== current.subagents[key]) {
|
|
304
|
+
ops.push({ op: 'set', path: ['subagents', key], value: current.subagents[key] });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (const spec of BACKENDS) {
|
|
308
|
+
const before = host.subagents[spec.cli];
|
|
309
|
+
const after = current.subagents[spec.cli];
|
|
310
|
+
for (const field of Object.keys(after)) {
|
|
311
|
+
if (JSON.stringify(before[field]) === JSON.stringify(after[field])) continue;
|
|
312
|
+
ops.push({ op: 'set', path: ['subagents', spec.cli, field], value: after[field] });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return ops;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 前端校验(宿主 schema 管不到跨字段关联,这里补齐):
|
|
320
|
+
* 模型 id 非空且唯一;**启用的**后端必须有默认模型,且该模型的 effort
|
|
321
|
+
* 列表与默认 effort 齐备——CLI 路由按这三元组选档,缺一项运行时就报错。
|
|
322
|
+
* @returns `{ key, titleKey }`(key 用 `{backend}` 参数渲染后端名),或 null。
|
|
323
|
+
*/
|
|
324
|
+
function validate(current) {
|
|
325
|
+
for (const spec of BACKENDS) {
|
|
326
|
+
const backend = current.subagents[spec.cli];
|
|
327
|
+
const titleKey = spec.cli === 'codex' ? 'codexTitle' : 'claudeTitle';
|
|
328
|
+
const fail = (key) => ({ key, titleKey });
|
|
329
|
+
const ids = backend.models.map((model) => model.id.trim());
|
|
330
|
+
if (ids.indexOf('') >= 0) return fail('invalidEmptyId');
|
|
331
|
+
if (new Set(ids).size !== ids.length) return fail('invalidDuplicateId');
|
|
332
|
+
if (!backend.enabled) continue;
|
|
333
|
+
if (backend.models.length === 0) return fail('invalidNoModels');
|
|
334
|
+
const selected = backend.models.find((model) => model.id.trim() === backend.defaultModel.trim());
|
|
335
|
+
if (selected === undefined) return fail('invalidDefaultModel');
|
|
336
|
+
if (selected.efforts.length === 0) return fail('invalidDefaultEffort');
|
|
337
|
+
if (selected.defaultEffort === '' || selected.efforts.indexOf(selected.defaultEffort) < 0) {
|
|
338
|
+
return fail('invalidDefaultEffort');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── 能力探针(host 半边的只读接口)─────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
/** 拉取 `/apollo/api/capabilities`;失败只降级显示,不阻塞表单。 */
|
|
347
|
+
function useCapabilities() {
|
|
348
|
+
const [state, setState] = useState({ status: 'loading' });
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
let active = true;
|
|
351
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : undefined;
|
|
352
|
+
const done = (next) => {
|
|
353
|
+
if (active) setState(next);
|
|
354
|
+
};
|
|
355
|
+
void (async () => {
|
|
356
|
+
try {
|
|
357
|
+
const res = await fetch(CAPABILITIES_URL, controller === undefined ? undefined : { signal: controller.signal });
|
|
358
|
+
const body = await res.json().catch(() => undefined);
|
|
359
|
+
if (!res.ok || body?.ok !== true) throw new Error('HTTP ' + String(res.status));
|
|
360
|
+
done({ status: 'ready', value: body.value });
|
|
361
|
+
} catch (error) {
|
|
362
|
+
done({ status: 'error', message: String(error?.message ?? error) });
|
|
363
|
+
}
|
|
364
|
+
})();
|
|
365
|
+
return () => {
|
|
366
|
+
active = false;
|
|
367
|
+
controller?.abort();
|
|
368
|
+
};
|
|
369
|
+
}, []);
|
|
370
|
+
return state;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** 一行探针结论:已检测到 <路径>(版本)/ 未找到。 */
|
|
374
|
+
function detectionOf(capabilities, spec, backend, translate) {
|
|
375
|
+
if (capabilities.status === 'loading') return translate('probing');
|
|
376
|
+
if (capabilities.status !== 'ready') return translate('capabilitiesError');
|
|
377
|
+
const info = isRecord(capabilities.value?.executables) ? capabilities.value.executables[spec.cli] : undefined;
|
|
378
|
+
if (typeof info?.path === 'string' && info.path !== '') {
|
|
379
|
+
return typeof info.version === 'string' && info.version !== ''
|
|
380
|
+
? translate('detected', { path: info.path, version: info.version })
|
|
381
|
+
: translate('detectedNoVersion', { path: info.path });
|
|
382
|
+
}
|
|
383
|
+
return translate('missing', { executable: backend.executable });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── 基础控件 ───────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
/** 标签 + 控件 + 提示的紧凑两列字段。 */
|
|
389
|
+
function Field({ id, label, hint, children }) {
|
|
390
|
+
return h('div', { className: 'apollo-field' },
|
|
391
|
+
h('label', id === undefined ? { className: 'apollo-fieldLabel' } : { className: 'apollo-fieldLabel', htmlFor: id }, label),
|
|
392
|
+
h('div', { className: 'apollo-fieldBody' },
|
|
393
|
+
children,
|
|
394
|
+
hint === undefined ? null : h('p', { className: 'apollo-hint' }, hint)));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** 开关行(role=switch + aria-checked,与官方 Switch 同观感)。 */
|
|
398
|
+
function SwitchRow({ id, label, hint, checked, disabled, onChange }) {
|
|
399
|
+
const labelId = id + '-label';
|
|
400
|
+
return h('div', { className: 'apollo-switchRow' },
|
|
401
|
+
h('div', { className: 'apollo-switchMain' },
|
|
402
|
+
h('span', { className: 'apollo-switchLabel', id: labelId }, label),
|
|
403
|
+
hint === undefined ? null : h('p', { className: 'apollo-hint' }, hint)),
|
|
404
|
+
h('button', {
|
|
405
|
+
type: 'button',
|
|
406
|
+
id,
|
|
407
|
+
role: 'switch',
|
|
408
|
+
className: 'apollo-switch',
|
|
409
|
+
'aria-checked': checked === true,
|
|
410
|
+
'aria-labelledby': labelId,
|
|
411
|
+
disabled: disabled === true,
|
|
412
|
+
onClick: () => onChange(checked !== true),
|
|
413
|
+
}, h('span', { className: 'apollo-switchThumb' })));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** 下拉选项:保留当前值(外部写入的值不在候选里时也不能被静默改掉)。 */
|
|
417
|
+
function optionNodes(values, current, noneLabel) {
|
|
418
|
+
const nodes = [];
|
|
419
|
+
if (noneLabel !== undefined) nodes.push(h('option', { key: '__none', value: '' }, noneLabel));
|
|
420
|
+
const seen = new Set();
|
|
421
|
+
for (const value of values) {
|
|
422
|
+
if (typeof value !== 'string' || value === '' || seen.has(value)) continue;
|
|
423
|
+
seen.add(value);
|
|
424
|
+
nodes.push(h('option', { key: value, value }, value));
|
|
425
|
+
}
|
|
426
|
+
if (typeof current === 'string' && current !== '' && !seen.has(current)) {
|
|
427
|
+
nodes.push(h('option', { key: current, value: current }, current));
|
|
428
|
+
}
|
|
429
|
+
return nodes;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** 一个模型的编辑行:effort 用本地文本缓冲(直接回填解析结果会吃掉未输完的逗号)。 */
|
|
433
|
+
function ModelRow({ model, index, disabled, t: translate, onPatch, onRemove }) {
|
|
434
|
+
const [effortsText, setEffortsText] = useState(model.efforts.join(', '));
|
|
435
|
+
return h('div', { className: 'apollo-model' },
|
|
436
|
+
h('input', {
|
|
437
|
+
className: 'apollo-input',
|
|
438
|
+
type: 'text',
|
|
439
|
+
value: model.id,
|
|
440
|
+
placeholder: translate('modelIdPlaceholder'),
|
|
441
|
+
disabled,
|
|
442
|
+
'aria-label': translate('modelIdPlaceholder') + ' ' + String(index + 1),
|
|
443
|
+
onChange: (event) => onPatch({ id: event.target.value }),
|
|
444
|
+
}),
|
|
445
|
+
h('input', {
|
|
446
|
+
className: 'apollo-input',
|
|
447
|
+
type: 'text',
|
|
448
|
+
value: effortsText,
|
|
449
|
+
placeholder: translate('modelEffortsPlaceholder'),
|
|
450
|
+
disabled,
|
|
451
|
+
'aria-label': translate('modelsLabel') + ' ' + String(index + 1),
|
|
452
|
+
onChange: (event) => {
|
|
453
|
+
const efforts = parseEfforts(event.target.value);
|
|
454
|
+
setEffortsText(event.target.value);
|
|
455
|
+
// 默认 effort 不属于新的列表时清空(宿主读回同规则)。
|
|
456
|
+
onPatch({
|
|
457
|
+
efforts,
|
|
458
|
+
defaultEffort: efforts.indexOf(model.defaultEffort) >= 0 ? model.defaultEffort : '',
|
|
459
|
+
});
|
|
460
|
+
},
|
|
461
|
+
}),
|
|
462
|
+
h('select', {
|
|
463
|
+
className: 'apollo-select',
|
|
464
|
+
value: model.defaultEffort,
|
|
465
|
+
disabled,
|
|
466
|
+
'aria-label': translate('modelDefaultEffort') + ' ' + String(index + 1),
|
|
467
|
+
onChange: (event) => {
|
|
468
|
+
const next = event.target.value;
|
|
469
|
+
if (next !== '' && model.efforts.indexOf(next) < 0) return;
|
|
470
|
+
onPatch({ defaultEffort: next });
|
|
471
|
+
},
|
|
472
|
+
}, optionNodes(model.efforts, model.defaultEffort, translate('modelEffortNone'))),
|
|
473
|
+
h('button', {
|
|
474
|
+
type: 'button',
|
|
475
|
+
className: 'apollo-iconButton',
|
|
476
|
+
title: translate('modelRemove'),
|
|
477
|
+
'aria-label': translate('modelRemove'),
|
|
478
|
+
disabled,
|
|
479
|
+
onClick: onRemove,
|
|
480
|
+
}, '−'));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** 一个 CLI 后端的配置卡片。 */
|
|
484
|
+
function BackendCard({ spec, backend, disabled, capabilities, t: translate, onPatch, onModelPatch, onModelAdd, onModelRemove, generation }) {
|
|
485
|
+
const title = translate(spec.cli === 'codex' ? 'codexTitle' : 'claudeTitle');
|
|
486
|
+
const profileNames = capabilities.status === 'ready' && Array.isArray(capabilities.value?.codexProfiles)
|
|
487
|
+
? capabilities.value.codexProfiles.filter((name) => typeof name === 'string')
|
|
488
|
+
: [];
|
|
489
|
+
const modelIds = backend.models.map((model) => model.id.trim()).filter((id) => id !== '');
|
|
490
|
+
const cards = [];
|
|
491
|
+
cards.push(h(SwitchRow, {
|
|
492
|
+
key: 'enable',
|
|
493
|
+
id: 'apollo-' + spec.cli + '-enabled',
|
|
494
|
+
label: translate('backendEnable'),
|
|
495
|
+
checked: backend.enabled,
|
|
496
|
+
disabled,
|
|
497
|
+
onChange: (next) => onPatch({ enabled: next }),
|
|
498
|
+
}));
|
|
499
|
+
cards.push(h(Field, {
|
|
500
|
+
key: 'executable',
|
|
501
|
+
id: 'apollo-' + spec.cli + '-executable',
|
|
502
|
+
label: translate('executableLabel'),
|
|
503
|
+
hint: translate('executableHint'),
|
|
504
|
+
},
|
|
505
|
+
h('input', {
|
|
506
|
+
className: 'apollo-input',
|
|
507
|
+
type: 'text',
|
|
508
|
+
id: 'apollo-' + spec.cli + '-executable',
|
|
509
|
+
value: backend.executable,
|
|
510
|
+
spellCheck: false,
|
|
511
|
+
autoComplete: 'off',
|
|
512
|
+
disabled,
|
|
513
|
+
onChange: (event) => onPatch({ executable: event.target.value }),
|
|
514
|
+
}),
|
|
515
|
+
h('p', { className: 'apollo-hint' }, detectionOf(capabilities, spec, backend, translate))));
|
|
516
|
+
if (spec.profile) {
|
|
517
|
+
cards.push(h(Field, {
|
|
518
|
+
key: 'profile',
|
|
519
|
+
id: 'apollo-' + spec.cli + '-profile',
|
|
520
|
+
label: translate('profileLabel'),
|
|
521
|
+
hint: translate('profileHint'),
|
|
522
|
+
},
|
|
523
|
+
h('select', {
|
|
524
|
+
className: 'apollo-select',
|
|
525
|
+
id: 'apollo-' + spec.cli + '-profile',
|
|
526
|
+
value: backend.profile,
|
|
527
|
+
disabled,
|
|
528
|
+
onChange: (event) => onPatch({ profile: event.target.value }),
|
|
529
|
+
}, optionNodes(profileNames, backend.profile, translate('profileNone')))));
|
|
530
|
+
}
|
|
531
|
+
cards.push(h(Field, {
|
|
532
|
+
key: 'permission',
|
|
533
|
+
id: 'apollo-' + spec.cli + '-permission',
|
|
534
|
+
label: translate('permissionLabel'),
|
|
535
|
+
hint: translate('permissionHint'),
|
|
536
|
+
},
|
|
537
|
+
h('select', {
|
|
538
|
+
className: 'apollo-select',
|
|
539
|
+
id: 'apollo-' + spec.cli + '-permission',
|
|
540
|
+
value: backend.permissionMode,
|
|
541
|
+
disabled,
|
|
542
|
+
onChange: (event) => onPatch({ permissionMode: event.target.value }),
|
|
543
|
+
}, optionNodes(spec.permissions, backend.permissionMode))));
|
|
544
|
+
cards.push(h(Field, {
|
|
545
|
+
key: 'defaultModel',
|
|
546
|
+
id: 'apollo-' + spec.cli + '-default-model',
|
|
547
|
+
label: translate('defaultModelLabel'),
|
|
548
|
+
},
|
|
549
|
+
h('select', {
|
|
550
|
+
className: 'apollo-select',
|
|
551
|
+
id: 'apollo-' + spec.cli + '-default-model',
|
|
552
|
+
value: backend.defaultModel,
|
|
553
|
+
disabled,
|
|
554
|
+
onChange: (event) => onPatch({ defaultModel: event.target.value }),
|
|
555
|
+
}, optionNodes(modelIds, backend.defaultModel, translate('defaultModelNone')))));
|
|
556
|
+
cards.push(h(Field, {
|
|
557
|
+
key: 'models',
|
|
558
|
+
label: translate('modelsLabel'),
|
|
559
|
+
hint: translate('modelsHint'),
|
|
560
|
+
},
|
|
561
|
+
backend.models.length === 0
|
|
562
|
+
? h('p', { className: 'apollo-hint' }, translate('modelsEmpty'))
|
|
563
|
+
: h('div', { className: 'apollo-models' },
|
|
564
|
+
h('div', { className: 'apollo-modelHead' },
|
|
565
|
+
h('span', null, translate('modelIdPlaceholder')),
|
|
566
|
+
h('span', null, translate('modelEffortsPlaceholder')),
|
|
567
|
+
h('span', null, translate('modelDefaultEffort')),
|
|
568
|
+
h('span', null, '')),
|
|
569
|
+
backend.models.map((model, index) => h(ModelRow, {
|
|
570
|
+
key: generation + ':' + index,
|
|
571
|
+
model,
|
|
572
|
+
index,
|
|
573
|
+
disabled,
|
|
574
|
+
t: translate,
|
|
575
|
+
onPatch: (patch) => onModelPatch(index, patch),
|
|
576
|
+
onRemove: () => onModelRemove(index),
|
|
577
|
+
}))),
|
|
578
|
+
h('div', null,
|
|
579
|
+
h('button', {
|
|
580
|
+
type: 'button',
|
|
581
|
+
className: 'apollo-button',
|
|
582
|
+
'data-variant': 'outline',
|
|
583
|
+
disabled,
|
|
584
|
+
onClick: onModelAdd,
|
|
585
|
+
}, translate('modelAdd')))));
|
|
586
|
+
return h('div', { className: backend.enabled ? 'apollo-card' : 'apollo-card apollo-cardOff' },
|
|
587
|
+
h('div', { className: 'apollo-cardHead' },
|
|
588
|
+
h('span', { className: 'apollo-cardTitle' }, title)),
|
|
589
|
+
cards);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** 行配置页面主体。 */
|
|
593
|
+
function ApolloConfigPage({ form, t: translate }) {
|
|
594
|
+
const capabilities = useCapabilities();
|
|
595
|
+
const state = form?.state;
|
|
596
|
+
const revision = state?.revision;
|
|
597
|
+
const [draft, setDraft] = useState(null);
|
|
598
|
+
const [generation, setGeneration] = useState(0);
|
|
599
|
+
const [busy, setBusy] = useState(false);
|
|
600
|
+
const [notice, setNotice] = useState(null);
|
|
601
|
+
|
|
602
|
+
const host = state?.status === 'ready' ? shape(state.value) : null;
|
|
603
|
+
const live = draft !== null && draft.revision === revision ? draft.value : host;
|
|
604
|
+
const writable = state?.status === 'ready' && state?.writable === true && state?.mode !== 'memory';
|
|
605
|
+
const dirty = live !== null && host !== null && JSON.stringify(live) !== JSON.stringify(host);
|
|
606
|
+
const invalid = live === null ? null : validate(live);
|
|
607
|
+
|
|
608
|
+
if (state === undefined) {
|
|
609
|
+
// 官方页面在镜像读到该命名空间前不传 form:这是正常加载窗口,
|
|
610
|
+
// 长期停在这里才说明本部署缺少 settings/config-editor。
|
|
611
|
+
return h('div', { className: 'apollo-cfg' },
|
|
612
|
+
h('p', { className: 'apollo-note apollo-pulse', role: 'status' }, translate('waitingForm')),
|
|
613
|
+
h('p', { className: 'apollo-hint' }, translate('noFormHint')));
|
|
614
|
+
}
|
|
615
|
+
if (state.status === 'loading') {
|
|
616
|
+
return h('div', { className: 'apollo-cfg' },
|
|
617
|
+
h('p', { className: 'apollo-note apollo-pulse', role: 'status' }, translate('loading')));
|
|
618
|
+
}
|
|
619
|
+
if (host === null) {
|
|
620
|
+
return h('div', { className: 'apollo-cfg' },
|
|
621
|
+
h('p', { className: 'apollo-note', 'data-tone': 'warn' }, translate('unavailable')));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** 记一次草稿修改(函数式叠加:连续编辑不丢改动;换 revision 即失效)。 */
|
|
625
|
+
const edit = (mutate) => {
|
|
626
|
+
setDraft((previous) => {
|
|
627
|
+
const base = previous !== null && previous.revision === revision ? previous.value : live;
|
|
628
|
+
return { revision, value: mutate(base) };
|
|
629
|
+
});
|
|
630
|
+
setNotice(null);
|
|
631
|
+
};
|
|
632
|
+
const patchBackend = (spec, patch) => edit((current) => ({
|
|
633
|
+
...current,
|
|
634
|
+
subagents: {
|
|
635
|
+
...current.subagents,
|
|
636
|
+
[spec.cli]: { ...current.subagents[spec.cli], ...patch },
|
|
637
|
+
},
|
|
638
|
+
}));
|
|
639
|
+
/** 按 index 做函数式补丁:不依赖触发那一帧的数组快照。 */
|
|
640
|
+
const patchModel = (spec, index, patch) => edit((current) => {
|
|
641
|
+
const backend = current.subagents[spec.cli];
|
|
642
|
+
const models = backend.models.map((model, at) => (at === index ? { ...model, ...patch } : model));
|
|
643
|
+
return { ...current, subagents: { ...current.subagents, [spec.cli]: { ...backend, models } } };
|
|
644
|
+
});
|
|
645
|
+
const addModel = (spec) => edit((current) => {
|
|
646
|
+
const backend = current.subagents[spec.cli];
|
|
647
|
+
return {
|
|
648
|
+
...current,
|
|
649
|
+
subagents: {
|
|
650
|
+
...current.subagents,
|
|
651
|
+
[spec.cli]: { ...backend, models: [...backend.models, { id: '', efforts: [], defaultEffort: '' }] },
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
});
|
|
655
|
+
const removeModel = (spec, index) => edit((current) => {
|
|
656
|
+
const backend = current.subagents[spec.cli];
|
|
657
|
+
return {
|
|
658
|
+
...current,
|
|
659
|
+
subagents: {
|
|
660
|
+
...current.subagents,
|
|
661
|
+
[spec.cli]: { ...backend, models: backend.models.filter((model, at) => at !== index) },
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
});
|
|
665
|
+
const save = async () => {
|
|
666
|
+
if (!dirty || invalid !== null || !writable) return;
|
|
667
|
+
const ops = diffOps(host, live);
|
|
668
|
+
if (ops.length === 0) return;
|
|
669
|
+
setBusy(true);
|
|
670
|
+
setNotice(null);
|
|
671
|
+
try {
|
|
672
|
+
const accepted = await form.mutate(ops, revision);
|
|
673
|
+
if (accepted) {
|
|
674
|
+
// 草稿保留:mirror 折叠后 revision 前进、草稿自动失效(避免回显旧值闪烁)。
|
|
675
|
+
setNotice({ tone: 'ok', text: translate('saved') });
|
|
676
|
+
} else {
|
|
677
|
+
setDraft(null);
|
|
678
|
+
setGeneration((value) => value + 1);
|
|
679
|
+
setNotice({ tone: 'warn', text: translate('conflict') });
|
|
680
|
+
}
|
|
681
|
+
} catch (error) {
|
|
682
|
+
setNotice({ tone: 'error', text: translate('saveFailed') + String(error?.message ?? error) });
|
|
683
|
+
} finally {
|
|
684
|
+
setBusy(false);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
const reset = () => {
|
|
688
|
+
setDraft(null);
|
|
689
|
+
setGeneration((value) => value + 1);
|
|
690
|
+
setNotice(null);
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const status = notice !== null
|
|
694
|
+
? h('span', { className: 'apollo-status', 'data-tone': notice.tone, role: notice.tone === 'error' ? 'alert' : 'status' }, notice.text)
|
|
695
|
+
: invalid !== null
|
|
696
|
+
? h('span', { className: 'apollo-status', 'data-tone': 'error', role: 'alert' },
|
|
697
|
+
translate(invalid.key, { backend: translate(invalid.titleKey) }))
|
|
698
|
+
: h('span', { className: 'apollo-status', role: 'status' }, writable
|
|
699
|
+
? (state.mode === 'host' ? '' : '')
|
|
700
|
+
: (state.status === 'unavailable' ? translate('unavailableNote') : translate('readonlyNote')));
|
|
701
|
+
|
|
702
|
+
return h('div', { className: 'apollo-cfg' },
|
|
703
|
+
h('div', { className: 'apollo-sectionHead' },
|
|
704
|
+
h('h3', { className: 'apollo-title' }, translate('title')),
|
|
705
|
+
h('p', { className: 'apollo-intro' }, translate('intro'))),
|
|
706
|
+
writable ? null : h('p', { className: 'apollo-note', 'data-tone': 'warn' },
|
|
707
|
+
state.status === 'unavailable' ? translate('unavailable') : translate('readonly')),
|
|
708
|
+
h('div', { className: 'apollo-section' },
|
|
709
|
+
h(SwitchRow, {
|
|
710
|
+
id: 'apollo-exit-guard',
|
|
711
|
+
label: translate('exitGuardLabel'),
|
|
712
|
+
hint: translate('exitGuardHint'),
|
|
713
|
+
checked: live.exitGuard,
|
|
714
|
+
disabled: !writable || busy,
|
|
715
|
+
onChange: (next) => edit((current) => ({ ...current, exitGuard: next })),
|
|
716
|
+
})),
|
|
717
|
+
h('div', { className: 'apollo-section' },
|
|
718
|
+
h('div', { className: 'apollo-sectionHead' },
|
|
719
|
+
h('h3', { className: 'apollo-title' }, translate('scopeTitle'))),
|
|
720
|
+
h(SwitchRow, {
|
|
721
|
+
id: 'apollo-scope-eng',
|
|
722
|
+
label: translate('scopeEng'),
|
|
723
|
+
hint: translate('scopeEngHint'),
|
|
724
|
+
checked: live.subagents.eng,
|
|
725
|
+
disabled: !writable || busy,
|
|
726
|
+
onChange: (next) => edit((current) => ({ ...current, subagents: { ...current.subagents, eng: next } })),
|
|
727
|
+
}),
|
|
728
|
+
h(SwitchRow, {
|
|
729
|
+
id: 'apollo-scope-global',
|
|
730
|
+
label: translate('scopeGlobal'),
|
|
731
|
+
hint: translate('scopeGlobalHint'),
|
|
732
|
+
checked: live.subagents.global,
|
|
733
|
+
disabled: !writable || busy,
|
|
734
|
+
onChange: (next) => edit((current) => ({ ...current, subagents: { ...current.subagents, global: next } })),
|
|
735
|
+
}),
|
|
736
|
+
live.subagents.eng || live.subagents.global
|
|
737
|
+
? null
|
|
738
|
+
: h('p', { className: 'apollo-note', 'data-tone': 'warn' }, translate('scopeNone'))),
|
|
739
|
+
...BACKENDS.map((spec) => h(BackendCard, {
|
|
740
|
+
key: spec.cli,
|
|
741
|
+
spec,
|
|
742
|
+
backend: live.subagents[spec.cli],
|
|
743
|
+
disabled: !writable || busy,
|
|
744
|
+
capabilities,
|
|
745
|
+
t: translate,
|
|
746
|
+
generation,
|
|
747
|
+
onPatch: (patch) => patchBackend(spec, patch),
|
|
748
|
+
onModelPatch: (index, patch) => patchModel(spec, index, patch),
|
|
749
|
+
onModelAdd: () => addModel(spec),
|
|
750
|
+
onModelRemove: (index) => removeModel(spec, index),
|
|
751
|
+
})),
|
|
752
|
+
h('div', { className: 'apollo-footer' },
|
|
753
|
+
h('button', {
|
|
754
|
+
type: 'button',
|
|
755
|
+
className: 'apollo-button',
|
|
756
|
+
'data-variant': 'primary',
|
|
757
|
+
disabled: !writable || busy || !dirty || invalid !== null,
|
|
758
|
+
onClick: () => void save(),
|
|
759
|
+
}, busy ? translate('saving') : translate('save')),
|
|
760
|
+
h('button', {
|
|
761
|
+
type: 'button',
|
|
762
|
+
className: 'apollo-button',
|
|
763
|
+
'data-variant': 'outline',
|
|
764
|
+
disabled: busy || !dirty,
|
|
765
|
+
onClick: reset,
|
|
766
|
+
}, translate('reset')),
|
|
767
|
+
status));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* 行配置入口:`summary` 只回一行描述;`page` 渲染样式表 + 表单主体。
|
|
772
|
+
* 样式随组件树内的 <style> 挂载一次(不写 DOM,重复渲染不重复注入)。
|
|
773
|
+
*/
|
|
774
|
+
function ApolloPanel(props) {
|
|
775
|
+
if (props.view === 'summary') return h('span', null, t('summary'));
|
|
776
|
+
return h('div', { className: 'apollo-root' },
|
|
777
|
+
h('style', { 'data-plugin-css': STYLE_ID }, CSS),
|
|
778
|
+
h(ApolloConfigPage, { form: props.form, t: props.t ?? t }));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const inject = ['slots', 'locale'];
|
|
782
|
+
|
|
783
|
+
function apply(ctx) {
|
|
784
|
+
try {
|
|
785
|
+
const dispose = ctx.locale.register(NS, { zh, en });
|
|
786
|
+
t = ctx.locale.bind(NS);
|
|
787
|
+
ctx.effect(() => dispose, '@menghuan1918/dsh-apollo: dictionaries');
|
|
788
|
+
} catch {
|
|
789
|
+
// locale 服务缺失时保持中文默认。
|
|
790
|
+
}
|
|
791
|
+
ctx.effect(() => ctx.slots.inject('plugins.row.config', () => {
|
|
792
|
+
const disposers = [];
|
|
793
|
+
for (const key of ROW_KEYS) {
|
|
794
|
+
try {
|
|
795
|
+
const dispose = ctx.slots.register({
|
|
796
|
+
name: 'plugins.row.config',
|
|
797
|
+
key,
|
|
798
|
+
locale: NS,
|
|
799
|
+
}, ApolloPanel);
|
|
800
|
+
if (typeof dispose === 'function') disposers.push(dispose);
|
|
801
|
+
} catch {
|
|
802
|
+
// 一个键被槽账本拒绝时,另一个键仍应注册。
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return () => {
|
|
806
|
+
for (const dispose of disposers) dispose();
|
|
807
|
+
};
|
|
808
|
+
}), '@menghuan1918/dsh-apollo: row config page');
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
exports.inject = inject;
|
|
812
|
+
exports.apply = apply;
|
|
813
|
+
return module.exports;
|
|
814
|
+
},
|
|
815
|
+
});
|