@menghuan1918/dsh-apollo 0.4.1 → 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 +135 -131
- package/cordis.patch.yml +75 -42
- package/docs/DEVELOPMENT.md +280 -468
- package/package.json +7 -4
- package/presets/eng/plugins/exit-guard/index.js +39 -51
- package/presets/eng/plugins/mode-instructions/index.js +23 -59
- package/presets/eng-ptc.patch.yml +5 -9
- package/presets/eng.patch.yml +29 -36
- package/src/client.js +63 -912
- package/src/config.js +31 -10
- package/src/index.js +13 -7
- package/src/migrate.js +602 -0
- package/src/owns-host.js +66 -0
- package/src/settings.js +39 -35
- package/presets/eng/plugins/wait-subagent/index.js +0 -233
- package/presets/eng/plugins/wait-subagent/package.json +0 -7
- package/src/cli/claude.js +0 -166
- package/src/cli/codex.js +0 -164
- package/src/cli/jsonl.js +0 -216
- package/src/cli-adapter.js +0 -198
- package/src/cli-sessions.js +0 -69
- package/src/panel.js +0 -844
- package/src/subagents.js +0 -198
package/src/client.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-apollo (npm: @menghuan1918/dsh-apollo) — 浏览器半边:官方 Plugins
|
|
2
|
+
* dsh-apollo (npm: @menghuan1918/dsh-apollo) — 浏览器半边:官方 Plugins 页的最小行配置页。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* 0.5.0 起 Apollo 只保留两件事需要网页入口:
|
|
5
|
+
* - `exitGuard`:运行中**后台 job** 的退出守卫开关(volatile,保存即生效);
|
|
6
|
+
* - 迁移/弃用说明:CLI 子代理、模型分档、运行中子代理守卫与压缩设置分别由
|
|
7
|
+
* `@menghuan1918/dsh-better-subagent` 与 `dsh-better-compact` 的行拥有。
|
|
8
|
+
* Apollo 的旧字段仍能装载(供一次性迁移读取),但不再作为可编辑的子代理设置。
|
|
9
|
+
*
|
|
10
|
+
* 本文件注册 `plugins.row.config`(键 `<npm 包名>#apollo-skills`,与
|
|
11
|
+
* src/config.js 的 ROW_CONFIG_KEY/LEGACY_ROW_CONFIG_KEY 同步),自画紧凑表单并只在
|
|
12
|
+
* 保存时一次提交全部改动。零构建:closure factory,只 require('react'),不 import
|
|
13
|
+
* 任何 DSH client 包;样式随组件树内一个 <style> 渲染(不写 DOM)。注册 id 必须等于
|
|
14
|
+
* npm 包名(client closure id)。不注册新插件的页面键——那些由各自的包拥有。
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
17
|
window.__ModuleLoader__.load({
|
|
@@ -13,229 +19,63 @@ window.__ModuleLoader__.load({
|
|
|
13
19
|
factory: (require) => {
|
|
14
20
|
var module = { exports: {} };
|
|
15
21
|
var exports = module.exports;
|
|
16
|
-
const { createElement: h,
|
|
22
|
+
const { createElement: h, useState } = require('react');
|
|
17
23
|
|
|
18
24
|
const NS = 'apollo';
|
|
19
25
|
// 新键是 Plugins 页按“bundle 依赖名 + 行 id”算出的(= npm 包名 + #apollo-skills);
|
|
20
26
|
// 旧键并排注册,只为仍把依赖槽记成旧 unscoped 名的 profile 保留配置入口。
|
|
21
27
|
const ROW_KEYS = ['@menghuan1918/dsh-apollo#apollo-skills', 'dsh-apollo#apollo-skills'];
|
|
22
|
-
const CAPABILITIES_URL = '/apollo/api/capabilities';
|
|
23
|
-
const MODELS_URL = '/apollo/api/models';
|
|
24
|
-
const ROUTES_URL = '/apollo/api/routes';
|
|
25
|
-
/** 子代理模型分档(与 src/config.js 的 tiers 同步)。 */
|
|
26
|
-
const TIERS = ['fast', 'quality'];
|
|
27
|
-
const TIER_TITLE_KEYS = { fast: 'tierFast', quality: 'tierQuality' };
|
|
28
|
-
const TIER_HINT_KEYS = { fast: 'tierFastHint', quality: 'tierQualityHint' };
|
|
29
|
-
|
|
30
|
-
/** Codex CLI 权限档(与 src/config.js 的 CODEX_PERMISSION_MODES 同步)。 */
|
|
31
|
-
const CODEX = {
|
|
32
|
-
cli: 'codex',
|
|
33
|
-
executable: 'codex',
|
|
34
|
-
permission: 'read-only',
|
|
35
|
-
permissions: ['read-only', 'workspace-write', 'danger-full-access'],
|
|
36
|
-
profile: true,
|
|
37
|
-
};
|
|
38
|
-
/** Claude Code CLI 权限档(与 src/config.js 的 CLAUDE_PERMISSION_MODES 同步)。 */
|
|
39
|
-
const CLAUDE = {
|
|
40
|
-
cli: 'claude',
|
|
41
|
-
executable: 'claude',
|
|
42
|
-
permission: 'plan',
|
|
43
|
-
permissions: ['plan', 'manual', 'dontAsk', 'acceptEdits', 'auto', 'bypassPermissions'],
|
|
44
|
-
profile: false,
|
|
45
|
-
};
|
|
46
|
-
const BACKENDS = [CODEX, CLAUDE];
|
|
47
28
|
|
|
48
29
|
const zh = {
|
|
49
|
-
summary: '
|
|
50
|
-
title: '
|
|
51
|
-
intro: '
|
|
30
|
+
summary: 'Apollo 集成设置:后台任务退出守卫与迁移说明',
|
|
31
|
+
title: 'Apollo 集成设置',
|
|
32
|
+
intro: 'Apollo 负责技能、工程预设和独立插件的装配。这里只保留后台任务退出守卫;保存后即时生效,配置写入当前 profile 的行配置(cordis.patch.yml)。',
|
|
33
|
+
migratedTitle: '已迁移的能力',
|
|
34
|
+
migratedBody: 'CLI 子代理、wait_subagent、运行中子代理守卫、模型分档与子代理设置页由 @menghuan1918/dsh-better-subagent 拥有;压缩模式与预算由 dsh-better-compact 拥有。请到 Plugins 中对应的行配置。',
|
|
35
|
+
deprecatedBody: '旧 Apollo 行里的 tiers / subagents / trustedHosts 只为迁移旧 profile 保留,运行时不再生效;迁移在启动后自动补齐新插件的缺失值,目标行已有显式设置时不会被覆盖。',
|
|
36
|
+
reloadBody: '从 0.4.x 升级、或同时显式选中了独立 bundle 时,行可能显示为只读/不可寻址;请只保留一个显式入口并重载 profile。压缩设置的保存需要重载 profile 才会被引擎采用。',
|
|
52
37
|
loading: '正在读取配置…',
|
|
53
38
|
waitingForm: '正在等待宿主提供这一行的配置快照…',
|
|
54
39
|
noFormHint: '长时间停在这里说明宿主没有为这一行提供可编辑配置:本部署没组合 dsh-settings/config-editor,或本行的 Config 未被识别。',
|
|
55
|
-
unavailable: '当前连接下该配置不可用:请通过 loopback
|
|
40
|
+
unavailable: '当前连接下该配置不可用:请通过 loopback 地址访问,或在宿主部署层把该权威加入受信反代。',
|
|
56
41
|
readonly: '当前连接只读(设置镜像为进程内 memory),无法写入宿主配置。',
|
|
57
|
-
exitGuardLabel: '
|
|
58
|
-
exitGuardHint: '
|
|
59
|
-
tiersTitle: '子代理模型分档',
|
|
60
|
-
tiersIntro: '工程模式提示词按这里的路由告诉主代理两档各用哪个模型;候选来自「插件 → 子代理 → 模型选择」白名单。',
|
|
61
|
-
tierFast: '快速档',
|
|
62
|
-
tierQuality: '质量档',
|
|
63
|
-
tierFastHint: '快速档:探索、侦察、机械分析、批量排查。',
|
|
64
|
-
tierQualityHint: '质量档:复杂实现、调试、审查、设计。',
|
|
65
|
-
tierInherit: '沿用父会话路由',
|
|
66
|
-
tierEffortDefault: '模型默认',
|
|
67
|
-
tierRouteLabel: '{tier}路由',
|
|
68
|
-
tierEffortLabel: '{tier} effort',
|
|
69
|
-
tierOffList: '(不在白名单) {route}',
|
|
70
|
-
tiersInactive: '「插件 → 子代理 → 模型选择」未开启或白名单为空,分档不会生效。',
|
|
71
|
-
routesLoading: '正在读取子代理模型白名单…',
|
|
72
|
-
routesError: '读取子代理模型白名单失败(不影响保存)。',
|
|
73
|
-
invalidTierRoute: '{backend}所选路由不在子代理模型白名单内,请改选或改回沿用父会话路由。',
|
|
74
|
-
scopeTitle: '接入范围',
|
|
75
|
-
scopeEng: '工程模式预设',
|
|
76
|
-
scopeEngHint: 'eng / eng-ptc 会话可以看到下面这些 CLI 子代理路由。',
|
|
77
|
-
scopeGlobal: '全部预设',
|
|
78
|
-
scopeGlobalHint: '所有预设的会话都能看到这些路由(影响面更大)。',
|
|
79
|
-
scopeNone: '两个范围都关闭时,这些 CLI 路由不会出现在任何会话里。',
|
|
80
|
-
codexTitle: 'Codex 后端',
|
|
81
|
-
claudeTitle: 'Claude 后端',
|
|
82
|
-
backendEnable: '启用',
|
|
83
|
-
executableLabel: '可执行文件',
|
|
84
|
-
executableHint: '只写命令名则从 PATH 查找;也可手输绝对路径或 ~ 开头的路径。',
|
|
85
|
-
profileLabel: 'Profile',
|
|
86
|
-
profileHint: '名称来自 CODEX_HOME 下的 <name>.config.toml 与旧式 [profiles.*] 段。',
|
|
87
|
-
profileNone: '(不指定)',
|
|
88
|
-
permissionLabel: '权限档',
|
|
89
|
-
permissionHint: '完全由这里的设置决定,不继承会话的权限状态。',
|
|
90
|
-
defaultModelLabel: '默认模型',
|
|
91
|
-
defaultModelNone: '(未指定)',
|
|
92
|
-
modelsLabel: '模型与 effort',
|
|
93
|
-
modelsHint: '每行一个模型:id + 允许的 effort(逗号分隔)+ 默认 effort。',
|
|
94
|
-
modelsEmpty: '还没有配置模型;启用后端前至少加一个模型。',
|
|
95
|
-
discover: '自动发现模型',
|
|
96
|
-
discovering: '正在发现…',
|
|
97
|
-
discoverHint: '从本机 CLI 或 Anthropic 端点读取模型,只追加列表里还没有的条目。',
|
|
98
|
-
discoverDone: '已发现 {count} 个模型,新增 {added} 个。',
|
|
99
|
-
discoverFailed: '自动发现失败:{reason}',
|
|
100
|
-
discoverUnknown: '未知原因',
|
|
101
|
-
discoverReasonMissing: '没找到可执行文件',
|
|
102
|
-
discoverReasonFailed: 'CLI 命令执行失败',
|
|
103
|
-
discoverReasonNoCredentials: '没找到 ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY',
|
|
104
|
-
discoverReasonUnauthorized: '模型端点拒绝了凭据(HTTP {status})',
|
|
105
|
-
discoverReasonHttp: '模型端点返回 HTTP {status}',
|
|
106
|
-
discoverReasonNetwork: '无法连接模型端点',
|
|
107
|
-
discoverReasonParse: '返回内容不是可解析的模型清单',
|
|
108
|
-
discoverReasonEmpty: '模型清单为空',
|
|
109
|
-
discoverReasonTooLarge: '返回内容过大',
|
|
110
|
-
modelIdPlaceholder: '模型 id',
|
|
111
|
-
modelEffortsPlaceholder: 'low, medium, high',
|
|
112
|
-
modelDefaultEffort: '默认 effort',
|
|
113
|
-
modelEffortNone: '(无默认)',
|
|
114
|
-
modelAdd: '添加模型',
|
|
115
|
-
modelRemove: '移除',
|
|
116
|
-
probing: '正在探测本机可执行文件…',
|
|
117
|
-
detected: '已检测到 {path}({version})',
|
|
118
|
-
detectedNoVersion: '已检测到 {path}(版本未知)',
|
|
119
|
-
missing: 'PATH 上没找到 {executable},可直接填绝对路径。',
|
|
120
|
-
capabilitiesError: '读取本机探测信息失败(不影响保存)。',
|
|
42
|
+
exitGuardLabel: '后台任务退出守卫',
|
|
43
|
+
exitGuardHint: '回合结束时若本会话仍有运行中的后台命令,注入提醒强制继续处理;用户中断(Esc)始终放行。运行中子代理的拦截由 dsh-better-subagent 的 childExitGuard 单独控制。',
|
|
121
44
|
save: '保存',
|
|
122
45
|
saving: '保存中…',
|
|
123
46
|
reset: '放弃改动',
|
|
124
47
|
saved: '已保存,即时生效。',
|
|
125
48
|
conflict: '宿主拒绝了这次写入:配置可能已被其他改动更新或被更高优先级配置覆盖,已重新读取宿主值。',
|
|
126
49
|
saveFailed: '保存失败:',
|
|
127
|
-
invalidEmptyId: '{backend}里有模型缺少 id,请补齐后再保存。',
|
|
128
|
-
invalidDuplicateId: '{backend}的模型 id 不能重复。',
|
|
129
|
-
invalidNoModels: '{backend}已启用,但还没有配置任何模型。',
|
|
130
|
-
invalidDefaultModel: '{backend}已启用,请把默认模型设为模型列表里的一个。',
|
|
131
|
-
invalidDefaultEffort: '{backend}的默认模型需要 effort 列表,并指定一个属于它的默认 effort。',
|
|
132
50
|
readonlyNote: '只读:当前无法写入宿主配置。',
|
|
133
51
|
unavailableNote: '不可写:当前连接下该配置不可用。',
|
|
134
52
|
};
|
|
135
53
|
|
|
136
54
|
const en = {
|
|
137
|
-
summary: '
|
|
138
|
-
title: '
|
|
139
|
-
intro: '
|
|
55
|
+
summary: 'Apollo integration settings: background-job exit guard and migration notes',
|
|
56
|
+
title: 'Apollo integration settings',
|
|
57
|
+
intro: 'Apollo owns the skills, the engineering presets and the assembly of the independent plugins. Only the background-job exit guard remains here; saving applies immediately and persists in the active profile row config (cordis.patch.yml).',
|
|
58
|
+
migratedTitle: 'Migrated capabilities',
|
|
59
|
+
migratedBody: 'CLI subagents, wait_subagent, the running-child guard, model tiers and the subagent settings page are owned by @menghuan1918/dsh-better-subagent; compaction modes and budgets are owned by dsh-better-compact. Use their rows under Plugins.',
|
|
60
|
+
deprecatedBody: 'The old Apollo tiers / subagents / trustedHosts fields are kept only so 0.4.x profiles load; they are inert at runtime. After boot the migration fills the new plugins’ missing values and never overwrites an explicit target setting.',
|
|
61
|
+
reloadBody: 'Upgrading from 0.4.x, or selecting both the standalone and Apollo bundles explicitly, can make the row read-only/unaddressable: keep a single explicit entry and reload the profile. Saved compaction settings take effect only after the profile reloads.',
|
|
140
62
|
loading: 'Loading configuration…',
|
|
141
63
|
waitingForm: 'Waiting for the host to provide this row’s configuration snapshot…',
|
|
142
64
|
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.',
|
|
143
|
-
unavailable: 'This configuration is unavailable on the current connection: use a loopback address, or
|
|
65
|
+
unavailable: 'This configuration is unavailable on the current connection: use a loopback address, or trust this authority in the host deployment.',
|
|
144
66
|
readonly: 'This connection is read-only (settings mirror is process-local memory); host configuration cannot be written.',
|
|
145
|
-
exitGuardLabel: '
|
|
146
|
-
exitGuardHint: 'Blocks turn end while
|
|
147
|
-
tiersTitle: 'Subagent model tiers',
|
|
148
|
-
tiersIntro: 'The eng mode prompt tells the main agent which route each tier uses; candidates come from the Plugins → Subagent → Model selection allowlist.',
|
|
149
|
-
tierFast: 'Fast tier',
|
|
150
|
-
tierQuality: 'Quality tier',
|
|
151
|
-
tierFastHint: 'Fast tier: exploration, reconnaissance, mechanical analysis, batch checks.',
|
|
152
|
-
tierQualityHint: 'Quality tier: complex implementation, debugging, review, design.',
|
|
153
|
-
tierInherit: 'Inherit parent route',
|
|
154
|
-
tierEffortDefault: 'Model default',
|
|
155
|
-
tierRouteLabel: '{tier} route',
|
|
156
|
-
tierEffortLabel: '{tier} effort',
|
|
157
|
-
tierOffList: '(not allowlisted) {route}',
|
|
158
|
-
tiersInactive: 'Plugins → Subagent → Model selection is off or its allowlist is empty, so the tiers take no effect.',
|
|
159
|
-
routesLoading: 'Loading the subagent model allowlist…',
|
|
160
|
-
routesError: 'Could not read the subagent model allowlist (saving still works).',
|
|
161
|
-
invalidTierRoute: 'The {backend} route is not in the subagent model allowlist: pick another or inherit the parent route.',
|
|
162
|
-
scopeTitle: 'Scope',
|
|
163
|
-
scopeEng: 'Eng preset',
|
|
164
|
-
scopeEngHint: 'eng / eng-ptc sessions see the CLI subagent routes below.',
|
|
165
|
-
scopeGlobal: 'All presets',
|
|
166
|
-
scopeGlobalHint: 'Every preset’s sessions see these routes (broader impact).',
|
|
167
|
-
scopeNone: 'With both scopes off, these CLI routes appear in no session.',
|
|
168
|
-
codexTitle: 'Codex backend',
|
|
169
|
-
claudeTitle: 'Claude backend',
|
|
170
|
-
backendEnable: 'Enabled',
|
|
171
|
-
executableLabel: 'Executable',
|
|
172
|
-
executableHint: 'A bare command name is resolved from PATH; an absolute or ~ path is also accepted.',
|
|
173
|
-
profileLabel: 'Profile',
|
|
174
|
-
profileHint: 'Names come from <name>.config.toml under CODEX_HOME and from legacy [profiles.*] sections.',
|
|
175
|
-
profileNone: '(none)',
|
|
176
|
-
permissionLabel: 'Permission',
|
|
177
|
-
permissionHint: 'Decided entirely by this setting; the session’s permission state is never inherited.',
|
|
178
|
-
defaultModelLabel: 'Default model',
|
|
179
|
-
defaultModelNone: '(unset)',
|
|
180
|
-
modelsLabel: 'Models and efforts',
|
|
181
|
-
modelsHint: 'One model per row: id + allowed efforts (comma separated) + default effort.',
|
|
182
|
-
modelsEmpty: 'No model yet; add at least one before enabling the backend.',
|
|
183
|
-
discover: 'Discover models',
|
|
184
|
-
discovering: 'Discovering…',
|
|
185
|
-
discoverHint: 'Reads models from the local CLI or the Anthropic endpoint; appends only entries not already listed.',
|
|
186
|
-
discoverDone: 'Found {count} models, added {added}.',
|
|
187
|
-
discoverFailed: 'Discovery failed: {reason}',
|
|
188
|
-
discoverUnknown: 'unknown reason',
|
|
189
|
-
discoverReasonMissing: 'executable not found',
|
|
190
|
-
discoverReasonFailed: 'the CLI command failed',
|
|
191
|
-
discoverReasonNoCredentials: 'no ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY',
|
|
192
|
-
discoverReasonUnauthorized: 'the model endpoint rejected the credential (HTTP {status})',
|
|
193
|
-
discoverReasonHttp: 'the model endpoint answered HTTP {status}',
|
|
194
|
-
discoverReasonNetwork: 'the model endpoint could not be reached',
|
|
195
|
-
discoverReasonParse: 'the reply is not a parseable model list',
|
|
196
|
-
discoverReasonEmpty: 'the model list is empty',
|
|
197
|
-
discoverReasonTooLarge: 'the reply is too large',
|
|
198
|
-
modelIdPlaceholder: 'model id',
|
|
199
|
-
modelEffortsPlaceholder: 'low, medium, high',
|
|
200
|
-
modelDefaultEffort: 'Default effort',
|
|
201
|
-
modelEffortNone: '(none)',
|
|
202
|
-
modelAdd: 'Add model',
|
|
203
|
-
modelRemove: 'Remove',
|
|
204
|
-
probing: 'Probing local executables…',
|
|
205
|
-
detected: 'Found {path} ({version})',
|
|
206
|
-
detectedNoVersion: 'Found {path} (version unknown)',
|
|
207
|
-
missing: '{executable} was not found on PATH; type an absolute path if needed.',
|
|
208
|
-
capabilitiesError: 'Could not read local probe data (saving still works).',
|
|
67
|
+
exitGuardLabel: 'Background-job exit guard',
|
|
68
|
+
exitGuardHint: 'Blocks turn end while background commands are still running; interrupt (Esc) always passes through. The running-child guard is controlled separately by dsh-better-subagent’s childExitGuard.',
|
|
209
69
|
save: 'Save',
|
|
210
70
|
saving: 'Saving…',
|
|
211
71
|
reset: 'Discard',
|
|
212
72
|
saved: 'Saved and applied.',
|
|
213
73
|
conflict: 'The host refused this write: the configuration changed elsewhere or a higher layer overrides it. Host values were reloaded.',
|
|
214
74
|
saveFailed: 'Save failed: ',
|
|
215
|
-
invalidEmptyId: 'Every model in {backend} needs an id before saving.',
|
|
216
|
-
invalidDuplicateId: 'Model ids in {backend} must be unique.',
|
|
217
|
-
invalidNoModels: '{backend} is enabled but has no model configured.',
|
|
218
|
-
invalidDefaultModel: '{backend} is enabled: set the default model to one of its model ids.',
|
|
219
|
-
invalidDefaultEffort: '{backend}’s default model needs an effort list and a default effort from that list.',
|
|
220
75
|
readonlyNote: 'Read-only: host configuration cannot be written.',
|
|
221
76
|
unavailableNote: 'Not writable: this configuration is unavailable on the current connection.',
|
|
222
77
|
};
|
|
223
78
|
|
|
224
|
-
/** 宿主发现接口的 error.code → 文案键(未知 code 回退通用原因)。 */
|
|
225
|
-
const DISCOVERY_REASON_KEYS = {
|
|
226
|
-
missing: 'discoverReasonMissing',
|
|
227
|
-
failed: 'discoverReasonFailed',
|
|
228
|
-
'no-credentials': 'discoverReasonNoCredentials',
|
|
229
|
-
unauthorized: 'discoverReasonUnauthorized',
|
|
230
|
-
http: 'discoverReasonHttp',
|
|
231
|
-
network: 'discoverReasonNetwork',
|
|
232
|
-
parse: 'discoverReasonParse',
|
|
233
|
-
empty: 'discoverReasonEmpty',
|
|
234
|
-
'too-large': 'discoverReasonTooLarge',
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// ── 文案(locale 服务缺失时保持中文默认)───────────────────────────────
|
|
238
|
-
|
|
239
79
|
function interpolate(text, params) {
|
|
240
80
|
if (params === undefined || params === null) return text;
|
|
241
81
|
return text.replace(/\{(\w+)\}/g, (match, key) => (key in params ? String(params[key]) : match));
|
|
@@ -243,36 +83,20 @@ window.__ModuleLoader__.load({
|
|
|
243
83
|
|
|
244
84
|
let t = (key, params) => interpolate(zh[key] ?? key, params);
|
|
245
85
|
|
|
246
|
-
// ── 样式(组件树内的 <style>;复用 DSH 设计令牌,与官方设置页同观感)──────────
|
|
247
|
-
|
|
248
86
|
const STYLE_ID = '@menghuan1918/dsh-apollo/settings.css';
|
|
249
87
|
const CSS = [
|
|
250
88
|
'.apollo-cfg{display:flex;flex-direction:column;gap:12px;max-width:760px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary)}',
|
|
251
89
|
'.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)}',
|
|
252
90
|
'.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)}',
|
|
253
91
|
'.apollo-note[data-tone=warn]{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-label-secondary)}',
|
|
254
|
-
'.apollo-note[data-tone=ok]{color:var(--dsw-alias-state-success-primary)}',
|
|
255
92
|
'.apollo-pulse{animation:apollo-pulse 2s cubic-bezier(.36,0,.64,1) infinite}',
|
|
256
93
|
'@keyframes apollo-pulse{0%{opacity:1}40%{opacity:.6}80%,to{opacity:1}}',
|
|
257
94
|
'@media (prefers-reduced-motion:reduce){.apollo-pulse{animation:none}}',
|
|
258
95
|
'.apollo-title{margin:0;font-size:14px;font-weight:500;line-height:22px}',
|
|
259
96
|
'.apollo-hint{margin:0;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary)}',
|
|
260
|
-
'.apollo-hint[data-tone=error]{color:var(--dsw-alias-state-error-primary)}',
|
|
261
|
-
'.apollo-hint[data-tone=ok]{color:var(--dsw-alias-state-success-primary)}',
|
|
262
|
-
'.apollo-actions{display:flex;flex-wrap:wrap;gap:6px}',
|
|
263
97
|
'.apollo-intro{margin:0;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
|
|
264
98
|
'.apollo-section{display:flex;flex-direction:column;gap:2px;padding-top:12px;border-top:.5px solid var(--dsw-alias-border-l2)}',
|
|
265
99
|
'.apollo-sectionHead{display:flex;flex-direction:column;gap:2px;padding-bottom:4px}',
|
|
266
|
-
'.apollo-field{display:grid;grid-template-columns:minmax(120px,168px) minmax(0,1fr);gap:2px 12px;padding:6px 0;align-items:start}',
|
|
267
|
-
'.apollo-fieldLabel{padding-top:5px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}',
|
|
268
|
-
'.apollo-fieldBody{display:flex;flex-direction:column;gap:4px;min-width:0}',
|
|
269
|
-
'.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)}',
|
|
270
|
-
'.apollo-select{appearance:none;background-image:none;padding-right:24px}',
|
|
271
|
-
'.apollo-input::placeholder{color:var(--dsw-alias-label-dimmed)}',
|
|
272
|
-
'.apollo-input:focus,.apollo-select:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}',
|
|
273
|
-
'.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}',
|
|
274
|
-
'.apollo-input:disabled,.apollo-select:disabled{opacity:.4;cursor:not-allowed}',
|
|
275
|
-
'.apollo-input[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary)}',
|
|
276
100
|
'.apollo-switchRow{display:flex;align-items:flex-start;gap:12px;padding:6px 0}',
|
|
277
101
|
'.apollo-switchMain{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}',
|
|
278
102
|
'.apollo-switchLabel{font-size:13px;font-weight:500}',
|
|
@@ -280,27 +104,13 @@ window.__ModuleLoader__.load({
|
|
|
280
104
|
'.apollo-switch[aria-checked=true]{background:var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary))}',
|
|
281
105
|
'.apollo-switch:disabled{opacity:.4;cursor:not-allowed}',
|
|
282
106
|
'.apollo-switchThumb{display:block;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary-foreground);transition:transform 120ms ease}',
|
|
283
|
-
'.apollo-switch[aria-checked=false] .apollo-switchThumb{background:var(--dsw-alias-switch-thumb,var(--dsw-alias-label-primary-foreground))}',
|
|
284
107
|
'.apollo-switch[aria-checked=true] .apollo-switchThumb{transform:translateX(16px)}',
|
|
285
108
|
'@media (prefers-reduced-motion:reduce){.apollo-switchThumb{transition:none}}',
|
|
286
|
-
'.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)}',
|
|
287
|
-
'.apollo-cardOff{opacity:.72}',
|
|
288
|
-
'.apollo-cardHead{display:flex;align-items:flex-start;gap:12px}',
|
|
289
|
-
'.apollo-cardTitle{flex:1;min-width:0;font-size:13.5px;font-weight:500}',
|
|
290
|
-
'.apollo-models{display:flex;flex-direction:column;gap:6px}',
|
|
291
|
-
'.apollo-model{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) minmax(0,132px) auto;gap:6px;align-items:center}',
|
|
292
|
-
'.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)}',
|
|
293
|
-
'.apollo-tier{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(0,1fr);gap:6px}',
|
|
294
|
-
'@media (max-width:640px){.apollo-field,.apollo-model,.apollo-modelHead,.apollo-tier{grid-template-columns:minmax(0,1fr)}.apollo-fieldLabel{padding-top:0}}',
|
|
295
109
|
'.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}',
|
|
296
110
|
'.apollo-button:disabled{opacity:.4;cursor:not-allowed}',
|
|
297
111
|
'.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))}',
|
|
298
|
-
'.apollo-button[data-variant=primary]:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-label-primary))}',
|
|
299
112
|
'.apollo-button[data-variant=outline]{border:.5px solid var(--dsw-alias-border-l3)}',
|
|
300
|
-
'.apollo-button
|
|
301
|
-
'.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}',
|
|
302
|
-
'.apollo-iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
303
|
-
'.apollo-iconButton:disabled{opacity:.4;cursor:not-allowed}',
|
|
113
|
+
'.apollo-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
|
|
304
114
|
'.apollo-footer{display:flex;align-items:center;gap:8px;padding-top:12px;border-top:.5px solid var(--dsw-alias-border-l2)}',
|
|
305
115
|
'.apollo-status{flex:1;min-width:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}',
|
|
306
116
|
'.apollo-status[data-tone=ok]{color:var(--dsw-alias-state-success-primary)}',
|
|
@@ -308,524 +118,35 @@ window.__ModuleLoader__.load({
|
|
|
308
118
|
'.apollo-status[data-tone=warn]{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-label-secondary))}',
|
|
309
119
|
].join('\n');
|
|
310
120
|
|
|
311
|
-
|
|
312
|
-
// ── 取值/形状(与 src/config.js 的默认形状一致;浏览器半边不能 import 宿主模块)──
|
|
313
|
-
|
|
314
|
-
function isRecord(value) {
|
|
315
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function stringOr(value, fallback) {
|
|
319
|
-
return typeof value === 'string' ? value : fallback;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function modelList(value) {
|
|
323
|
-
if (!Array.isArray(value)) return [];
|
|
324
|
-
return value.filter(isRecord).map((entry) => {
|
|
325
|
-
const efforts = Array.isArray(entry.efforts) ? entry.efforts.filter((item) => typeof item === 'string') : [];
|
|
326
|
-
const defaultEffort = stringOr(entry.defaultEffort, '');
|
|
327
|
-
return {
|
|
328
|
-
id: stringOr(entry.id, ''),
|
|
329
|
-
efforts,
|
|
330
|
-
// 与 src/config.js 同规则:默认 effort 必须属于该模型的 effort 列表。
|
|
331
|
-
defaultEffort: efforts.indexOf(defaultEffort) >= 0 ? defaultEffort : '',
|
|
332
|
-
};
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function backendShape(value, spec) {
|
|
337
|
-
const source = isRecord(value) ? value : {};
|
|
338
|
-
const backend = {
|
|
339
|
-
enabled: source.enabled === true,
|
|
340
|
-
executable: typeof source.executable === 'string' && source.executable.trim() !== ''
|
|
341
|
-
? source.executable.trim()
|
|
342
|
-
: spec.executable,
|
|
343
|
-
};
|
|
344
|
-
if (spec.profile) backend.profile = stringOr(source.profile, '');
|
|
345
|
-
backend.permissionMode = spec.permissions.indexOf(source.permissionMode) >= 0
|
|
346
|
-
? source.permissionMode
|
|
347
|
-
: spec.permission;
|
|
348
|
-
backend.defaultModel = stringOr(source.defaultModel, '');
|
|
349
|
-
backend.models = modelList(source.models);
|
|
350
|
-
return backend;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** 一个分档:provider 或 model 为空即整档清空(与 src/config.js 同规则)。 */
|
|
354
|
-
function tierShape(value) {
|
|
355
|
-
const source = isRecord(value) ? value : {};
|
|
356
|
-
const provider = stringOr(source.provider, '').trim();
|
|
357
|
-
const model = stringOr(source.model, '').trim();
|
|
358
|
-
if (provider === '' || model === '') return { provider: '', model: '', reasoningEffort: '' };
|
|
359
|
-
return { provider, model, reasoningEffort: stringOr(source.reasoningEffort, '').trim() };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/** 把宿主快照值(或 undefined)规整成表单可安全读写的形状。 */
|
|
121
|
+
/** 宿主快照只投影这一个可编辑字段。 */
|
|
363
122
|
function shape(value) {
|
|
364
|
-
const source =
|
|
365
|
-
|
|
366
|
-
const tiers = isRecord(source.tiers) ? source.tiers : {};
|
|
367
|
-
const result = {
|
|
368
|
-
exitGuard: source.exitGuard === true,
|
|
369
|
-
tiers: {},
|
|
370
|
-
subagents: {
|
|
371
|
-
eng: subagents.eng !== false,
|
|
372
|
-
global: subagents.global === true,
|
|
373
|
-
},
|
|
374
|
-
};
|
|
375
|
-
for (const tier of TIERS) result.tiers[tier] = tierShape(tiers[tier]);
|
|
376
|
-
for (const spec of BACKENDS) result.subagents[spec.cli] = backendShape(subagents[spec.cli], spec);
|
|
377
|
-
return result;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** 逗号分隔的 effort 文本 → 去重后的字符串数组。 */
|
|
381
|
-
function parseEfforts(text) {
|
|
382
|
-
const result = [];
|
|
383
|
-
for (const part of String(text ?? '').split(',')) {
|
|
384
|
-
const trimmed = part.trim();
|
|
385
|
-
if (trimmed === '' || result.indexOf(trimmed) >= 0) continue;
|
|
386
|
-
result.push(trimmed);
|
|
387
|
-
}
|
|
388
|
-
return result;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/** 只在用户按下保存时才写入:把差异摊成一组 set 操作(数组整段替换)。 */
|
|
392
|
-
function diffOps(host, current) {
|
|
393
|
-
const ops = [];
|
|
394
|
-
if (host.exitGuard !== current.exitGuard) {
|
|
395
|
-
ops.push({ op: 'set', path: ['exitGuard'], value: current.exitGuard });
|
|
396
|
-
}
|
|
397
|
-
for (const tier of TIERS) {
|
|
398
|
-
if (JSON.stringify(host.tiers[tier]) === JSON.stringify(current.tiers[tier])) continue;
|
|
399
|
-
ops.push({ op: 'set', path: ['tiers', tier], value: tierShape(current.tiers[tier]) });
|
|
400
|
-
}
|
|
401
|
-
for (const key of ['eng', 'global']) {
|
|
402
|
-
if (host.subagents[key] !== current.subagents[key]) {
|
|
403
|
-
ops.push({ op: 'set', path: ['subagents', key], value: current.subagents[key] });
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
for (const spec of BACKENDS) {
|
|
407
|
-
const before = host.subagents[spec.cli];
|
|
408
|
-
const after = current.subagents[spec.cli];
|
|
409
|
-
for (const field of Object.keys(after)) {
|
|
410
|
-
if (JSON.stringify(before[field]) === JSON.stringify(after[field])) continue;
|
|
411
|
-
ops.push({ op: 'set', path: ['subagents', spec.cli, field], value: after[field] });
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return ops;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* 前端校验(宿主 schema 管不到跨字段关联,这里补齐):
|
|
419
|
-
* 模型 id 非空且唯一;**启用的**后端必须有默认模型,且该模型的 effort
|
|
420
|
-
* 列表与默认 effort 齐备——CLI 路由按这三元组选档,缺一项运行时就报错。
|
|
421
|
-
* 分档:白名单读取成功时,已选路由必须在白名单内(否则子代理派发会被策略拒绝)。
|
|
422
|
-
* @param current - 表单当前值。
|
|
423
|
-
* @param allowed - `routesOf()` 的结果;白名单未读到时为 null(不校验分档)。
|
|
424
|
-
* @returns `{ key, titleKey }`(key 用 `{backend}` 参数渲染后端/分档名),或 null。
|
|
425
|
-
*/
|
|
426
|
-
function validate(current, allowed) {
|
|
427
|
-
if (allowed !== null && allowed !== undefined) {
|
|
428
|
-
for (const tier of TIERS) {
|
|
429
|
-
const value = current.tiers[tier];
|
|
430
|
-
if (value.provider === '' || value.model === '') continue;
|
|
431
|
-
if (findRoute(allowed.routes, value) === undefined) {
|
|
432
|
-
return { key: 'invalidTierRoute', titleKey: TIER_TITLE_KEYS[tier] };
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
for (const spec of BACKENDS) {
|
|
437
|
-
const backend = current.subagents[spec.cli];
|
|
438
|
-
const titleKey = spec.cli === 'codex' ? 'codexTitle' : 'claudeTitle';
|
|
439
|
-
const fail = (key) => ({ key, titleKey });
|
|
440
|
-
const ids = backend.models.map((model) => model.id.trim());
|
|
441
|
-
if (ids.indexOf('') >= 0) return fail('invalidEmptyId');
|
|
442
|
-
if (new Set(ids).size !== ids.length) return fail('invalidDuplicateId');
|
|
443
|
-
if (!backend.enabled) continue;
|
|
444
|
-
if (backend.models.length === 0) return fail('invalidNoModels');
|
|
445
|
-
const selected = backend.models.find((model) => model.id.trim() === backend.defaultModel.trim());
|
|
446
|
-
if (selected === undefined) return fail('invalidDefaultModel');
|
|
447
|
-
if (selected.efforts.length === 0) return fail('invalidDefaultEffort');
|
|
448
|
-
if (selected.defaultEffort === '' || selected.efforts.indexOf(selected.defaultEffort) < 0) {
|
|
449
|
-
return fail('invalidDefaultEffort');
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// ── 能力探针(host 半边的只读接口)─────────────────────────────────────
|
|
456
|
-
|
|
457
|
-
/** 拉取一个 `/apollo/api/*` 只读接口;失败只降级显示,不阻塞表单。 */
|
|
458
|
-
function useEndpoint(url) {
|
|
459
|
-
const [state, setState] = useState({ status: 'loading' });
|
|
460
|
-
useEffect(() => {
|
|
461
|
-
let active = true;
|
|
462
|
-
const controller = typeof AbortController === 'function' ? new AbortController() : undefined;
|
|
463
|
-
const done = (next) => {
|
|
464
|
-
if (active) setState(next);
|
|
465
|
-
};
|
|
466
|
-
void (async () => {
|
|
467
|
-
try {
|
|
468
|
-
const res = await fetch(url, controller === undefined ? undefined : { signal: controller.signal });
|
|
469
|
-
const body = await res.json().catch(() => undefined);
|
|
470
|
-
if (!res.ok || body?.ok !== true) throw new Error('HTTP ' + String(res.status));
|
|
471
|
-
done({ status: 'ready', value: body.value });
|
|
472
|
-
} catch (error) {
|
|
473
|
-
done({ status: 'error', message: String(error?.message ?? error) });
|
|
474
|
-
}
|
|
475
|
-
})();
|
|
476
|
-
return () => {
|
|
477
|
-
active = false;
|
|
478
|
-
controller?.abort();
|
|
479
|
-
};
|
|
480
|
-
}, [url]);
|
|
481
|
-
return state;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* `/apollo/api/routes` 的结果 → `{ enabled, routes }`;未读到(加载中/失败)时 null。
|
|
486
|
-
* 每条路由 `{ provider, model, efforts, defaultEffort }`。
|
|
487
|
-
*/
|
|
488
|
-
function routesOf(state) {
|
|
489
|
-
if (state.status !== 'ready') return null;
|
|
490
|
-
const value = isRecord(state.value) ? state.value : {};
|
|
491
|
-
const routes = [];
|
|
492
|
-
for (const entry of Array.isArray(value.routes) ? value.routes : []) {
|
|
493
|
-
if (!isRecord(entry)) continue;
|
|
494
|
-
const provider = stringOr(entry.provider, '').trim();
|
|
495
|
-
const model = stringOr(entry.model, '').trim();
|
|
496
|
-
if (provider === '' || model === '') continue;
|
|
497
|
-
routes.push({
|
|
498
|
-
provider,
|
|
499
|
-
model,
|
|
500
|
-
efforts: Array.isArray(entry.efforts) ? entry.efforts.filter((item) => typeof item === 'string' && item !== '') : [],
|
|
501
|
-
defaultEffort: stringOr(entry.defaultEffort, ''),
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
return { enabled: value.enabled === true, routes };
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function findRoute(routes, tier) {
|
|
508
|
-
return routes.find((route) => route.provider === tier.provider && route.model === tier.model);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/** 路由下拉的值:`provider/model`(按第一个 `/` 切回;provider id 不含 `/`)。 */
|
|
512
|
-
function routeKey(route) {
|
|
513
|
-
return route.provider === '' || route.model === '' ? '' : route.provider + '/' + route.model;
|
|
123
|
+
const source = typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {};
|
|
124
|
+
return { exitGuard: source.exitGuard === true };
|
|
514
125
|
}
|
|
515
126
|
|
|
516
|
-
function parseRouteKey(key) {
|
|
517
|
-
const text = String(key ?? '');
|
|
518
|
-
const at = text.indexOf('/');
|
|
519
|
-
if (at <= 0 || at === text.length - 1) return { provider: '', model: '' };
|
|
520
|
-
return { provider: text.slice(0, at), model: text.slice(at + 1) };
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/** 一行探针结论:已检测到 <路径>(版本)/ 未找到。 */
|
|
524
|
-
function detectionOf(capabilities, spec, backend, translate) {
|
|
525
|
-
if (capabilities.status === 'loading') return translate('probing');
|
|
526
|
-
if (capabilities.status !== 'ready') return translate('capabilitiesError');
|
|
527
|
-
const info = isRecord(capabilities.value?.executables) ? capabilities.value.executables[spec.cli] : undefined;
|
|
528
|
-
if (typeof info?.path === 'string' && info.path !== '') {
|
|
529
|
-
return typeof info.version === 'string' && info.version !== ''
|
|
530
|
-
? translate('detected', { path: info.path, version: info.version })
|
|
531
|
-
: translate('detectedNoVersion', { path: info.path });
|
|
532
|
-
}
|
|
533
|
-
return translate('missing', { executable: backend.executable });
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// ── 基础控件 ───────────────────────────────────────────────────────────
|
|
537
|
-
|
|
538
|
-
/** 标签 + 控件 + 提示的紧凑两列字段。 */
|
|
539
|
-
function Field({ id, label, hint, children }) {
|
|
540
|
-
return h('div', { className: 'apollo-field' },
|
|
541
|
-
h('label', id === undefined ? { className: 'apollo-fieldLabel' } : { className: 'apollo-fieldLabel', htmlFor: id }, label),
|
|
542
|
-
h('div', { className: 'apollo-fieldBody' },
|
|
543
|
-
children,
|
|
544
|
-
hint === undefined ? null : h('p', { className: 'apollo-hint' }, hint)));
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/** 开关行(role=switch + aria-checked,与官方 Switch 同观感)。 */
|
|
548
127
|
function SwitchRow({ id, label, hint, checked, disabled, onChange }) {
|
|
549
|
-
const labelId = id + '-label';
|
|
550
128
|
return h('div', { className: 'apollo-switchRow' },
|
|
551
129
|
h('div', { className: 'apollo-switchMain' },
|
|
552
|
-
h('
|
|
130
|
+
h('label', { className: 'apollo-switchLabel', htmlFor: id }, label),
|
|
553
131
|
hint === undefined ? null : h('p', { className: 'apollo-hint' }, hint)),
|
|
554
132
|
h('button', {
|
|
555
133
|
type: 'button',
|
|
556
134
|
id,
|
|
557
|
-
role: 'switch',
|
|
558
135
|
className: 'apollo-switch',
|
|
559
|
-
'
|
|
560
|
-
'aria-
|
|
561
|
-
|
|
562
|
-
onClick: () => onChange(checked !== true),
|
|
563
|
-
}, h('span', { className: 'apollo-switchThumb' })));
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/** 下拉选项:保留当前值(外部写入的值不在候选里时也不能被静默改掉)。 */
|
|
567
|
-
function optionNodes(values, current, noneLabel) {
|
|
568
|
-
const nodes = [];
|
|
569
|
-
if (noneLabel !== undefined) nodes.push(h('option', { key: '__none', value: '' }, noneLabel));
|
|
570
|
-
const seen = new Set();
|
|
571
|
-
for (const value of values) {
|
|
572
|
-
if (typeof value !== 'string' || value === '' || seen.has(value)) continue;
|
|
573
|
-
seen.add(value);
|
|
574
|
-
nodes.push(h('option', { key: value, value }, value));
|
|
575
|
-
}
|
|
576
|
-
if (typeof current === 'string' && current !== '' && !seen.has(current)) {
|
|
577
|
-
nodes.push(h('option', { key: current, value: current }, current));
|
|
578
|
-
}
|
|
579
|
-
return nodes;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/** 一个模型的编辑行:effort 用本地文本缓冲(直接回填解析结果会吃掉未输完的逗号)。 */
|
|
583
|
-
function ModelRow({ model, index, disabled, t: translate, onPatch, onRemove }) {
|
|
584
|
-
const [effortsText, setEffortsText] = useState(model.efforts.join(', '));
|
|
585
|
-
return h('div', { className: 'apollo-model' },
|
|
586
|
-
h('input', {
|
|
587
|
-
className: 'apollo-input',
|
|
588
|
-
type: 'text',
|
|
589
|
-
value: model.id,
|
|
590
|
-
placeholder: translate('modelIdPlaceholder'),
|
|
591
|
-
disabled,
|
|
592
|
-
'aria-label': translate('modelIdPlaceholder') + ' ' + String(index + 1),
|
|
593
|
-
onChange: (event) => onPatch({ id: event.target.value }),
|
|
594
|
-
}),
|
|
595
|
-
h('input', {
|
|
596
|
-
className: 'apollo-input',
|
|
597
|
-
type: 'text',
|
|
598
|
-
value: effortsText,
|
|
599
|
-
placeholder: translate('modelEffortsPlaceholder'),
|
|
600
|
-
disabled,
|
|
601
|
-
'aria-label': translate('modelsLabel') + ' ' + String(index + 1),
|
|
602
|
-
onChange: (event) => {
|
|
603
|
-
const efforts = parseEfforts(event.target.value);
|
|
604
|
-
setEffortsText(event.target.value);
|
|
605
|
-
// 默认 effort 不属于新的列表时清空(宿主读回同规则)。
|
|
606
|
-
onPatch({
|
|
607
|
-
efforts,
|
|
608
|
-
defaultEffort: efforts.indexOf(model.defaultEffort) >= 0 ? model.defaultEffort : '',
|
|
609
|
-
});
|
|
610
|
-
},
|
|
611
|
-
}),
|
|
612
|
-
h('select', {
|
|
613
|
-
className: 'apollo-select',
|
|
614
|
-
value: model.defaultEffort,
|
|
615
|
-
disabled,
|
|
616
|
-
'aria-label': translate('modelDefaultEffort') + ' ' + String(index + 1),
|
|
617
|
-
onChange: (event) => {
|
|
618
|
-
const next = event.target.value;
|
|
619
|
-
if (next !== '' && model.efforts.indexOf(next) < 0) return;
|
|
620
|
-
onPatch({ defaultEffort: next });
|
|
621
|
-
},
|
|
622
|
-
}, optionNodes(model.efforts, model.defaultEffort, translate('modelEffortNone'))),
|
|
623
|
-
h('button', {
|
|
624
|
-
type: 'button',
|
|
625
|
-
className: 'apollo-iconButton',
|
|
626
|
-
title: translate('modelRemove'),
|
|
627
|
-
'aria-label': translate('modelRemove'),
|
|
628
|
-
disabled,
|
|
629
|
-
onClick: onRemove,
|
|
630
|
-
}, '−'));
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* 一个分档:路由下拉(白名单)+ effort(路由有 effort 列表时下拉,否则文本)。
|
|
635
|
-
* 已保存但不在白名单里的路由仍作为选项保留(白名单读到时标注出来),不被静默改掉。
|
|
636
|
-
*/
|
|
637
|
-
function TierRow({ tier, value, allowed, disabled, t: translate, onPatch }) {
|
|
638
|
-
const title = translate(TIER_TITLE_KEYS[tier]);
|
|
639
|
-
const routes = allowed === null ? [] : allowed.routes;
|
|
640
|
-
const current = routeKey(value);
|
|
641
|
-
const selected = findRoute(routes, value);
|
|
642
|
-
const routeOptions = [h('option', { key: '__inherit', value: '' }, translate('tierInherit'))];
|
|
643
|
-
for (const route of routes) {
|
|
644
|
-
routeOptions.push(h('option', { key: routeKey(route), value: routeKey(route) }, route.provider + ' / ' + route.model));
|
|
645
|
-
}
|
|
646
|
-
if (current !== '' && selected === undefined) {
|
|
647
|
-
const label = value.provider + ' / ' + value.model;
|
|
648
|
-
routeOptions.push(h('option', { key: current, value: current },
|
|
649
|
-
allowed === null ? label : translate('tierOffList', { route: label })));
|
|
650
|
-
}
|
|
651
|
-
const effortLabel = translate('tierEffortLabel', { tier: title });
|
|
652
|
-
const effort = selected !== undefined && selected.efforts.length > 0
|
|
653
|
-
? h('select', {
|
|
654
|
-
className: 'apollo-select',
|
|
655
|
-
id: 'apollo-tier-' + tier + '-effort',
|
|
656
|
-
'aria-label': effortLabel,
|
|
657
|
-
value: value.reasoningEffort,
|
|
658
|
-
disabled: disabled || current === '',
|
|
659
|
-
onChange: (event) => onPatch({ reasoningEffort: event.target.value }),
|
|
660
|
-
}, optionNodes(selected.efforts, value.reasoningEffort, translate('tierEffortDefault')))
|
|
661
|
-
: h('input', {
|
|
662
|
-
className: 'apollo-input',
|
|
663
|
-
type: 'text',
|
|
664
|
-
id: 'apollo-tier-' + tier + '-effort',
|
|
665
|
-
'aria-label': effortLabel,
|
|
666
|
-
value: value.reasoningEffort,
|
|
667
|
-
placeholder: translate('tierEffortDefault'),
|
|
668
|
-
spellCheck: false,
|
|
669
|
-
autoComplete: 'off',
|
|
670
|
-
disabled: disabled || current === '',
|
|
671
|
-
onChange: (event) => onPatch({ reasoningEffort: event.target.value.trim() }),
|
|
672
|
-
});
|
|
673
|
-
return h(Field, { id: 'apollo-tier-' + tier + '-route', label: title, hint: translate(TIER_HINT_KEYS[tier]) },
|
|
674
|
-
h('div', { className: 'apollo-tier' },
|
|
675
|
-
h('select', {
|
|
676
|
-
className: 'apollo-select',
|
|
677
|
-
id: 'apollo-tier-' + tier + '-route',
|
|
678
|
-
'aria-label': translate('tierRouteLabel', { tier: title }),
|
|
679
|
-
value: current,
|
|
680
|
-
disabled,
|
|
681
|
-
// 换路由即清空 effort(旧值未必属于新模型)。
|
|
682
|
-
onChange: (event) => onPatch({ ...parseRouteKey(event.target.value), reasoningEffort: '' }),
|
|
683
|
-
}, routeOptions),
|
|
684
|
-
effort));
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/** 一个 CLI 后端的配置卡片。 */
|
|
688
|
-
function BackendCard({ spec, backend, disabled, capabilities, discovery, t: translate, onPatch, onModelPatch, onModelAdd, onModelRemove, onDiscover, generation }) {
|
|
689
|
-
const title = translate(spec.cli === 'codex' ? 'codexTitle' : 'claudeTitle');
|
|
690
|
-
const status = discovery === undefined ? undefined : discovery[spec.cli];
|
|
691
|
-
const profileNames = capabilities.status === 'ready' && Array.isArray(capabilities.value?.codexProfiles)
|
|
692
|
-
? capabilities.value.codexProfiles.filter((name) => typeof name === 'string')
|
|
693
|
-
: [];
|
|
694
|
-
const modelIds = backend.models.map((model) => model.id.trim()).filter((id) => id !== '');
|
|
695
|
-
const cards = [];
|
|
696
|
-
cards.push(h(SwitchRow, {
|
|
697
|
-
key: 'enable',
|
|
698
|
-
id: 'apollo-' + spec.cli + '-enabled',
|
|
699
|
-
label: translate('backendEnable'),
|
|
700
|
-
checked: backend.enabled,
|
|
701
|
-
disabled,
|
|
702
|
-
onChange: (next) => onPatch({ enabled: next }),
|
|
703
|
-
}));
|
|
704
|
-
cards.push(h(Field, {
|
|
705
|
-
key: 'executable',
|
|
706
|
-
id: 'apollo-' + spec.cli + '-executable',
|
|
707
|
-
label: translate('executableLabel'),
|
|
708
|
-
hint: translate('executableHint'),
|
|
709
|
-
},
|
|
710
|
-
h('input', {
|
|
711
|
-
className: 'apollo-input',
|
|
712
|
-
type: 'text',
|
|
713
|
-
id: 'apollo-' + spec.cli + '-executable',
|
|
714
|
-
value: backend.executable,
|
|
715
|
-
spellCheck: false,
|
|
716
|
-
autoComplete: 'off',
|
|
717
|
-
disabled,
|
|
718
|
-
onChange: (event) => onPatch({ executable: event.target.value }),
|
|
719
|
-
}),
|
|
720
|
-
h('p', { className: 'apollo-hint' }, detectionOf(capabilities, spec, backend, translate))));
|
|
721
|
-
if (spec.profile) {
|
|
722
|
-
cards.push(h(Field, {
|
|
723
|
-
key: 'profile',
|
|
724
|
-
id: 'apollo-' + spec.cli + '-profile',
|
|
725
|
-
label: translate('profileLabel'),
|
|
726
|
-
hint: translate('profileHint'),
|
|
727
|
-
},
|
|
728
|
-
h('select', {
|
|
729
|
-
className: 'apollo-select',
|
|
730
|
-
id: 'apollo-' + spec.cli + '-profile',
|
|
731
|
-
value: backend.profile,
|
|
732
|
-
disabled,
|
|
733
|
-
onChange: (event) => onPatch({ profile: event.target.value }),
|
|
734
|
-
}, optionNodes(profileNames, backend.profile, translate('profileNone')))));
|
|
735
|
-
}
|
|
736
|
-
cards.push(h(Field, {
|
|
737
|
-
key: 'permission',
|
|
738
|
-
id: 'apollo-' + spec.cli + '-permission',
|
|
739
|
-
label: translate('permissionLabel'),
|
|
740
|
-
hint: translate('permissionHint'),
|
|
741
|
-
},
|
|
742
|
-
h('select', {
|
|
743
|
-
className: 'apollo-select',
|
|
744
|
-
id: 'apollo-' + spec.cli + '-permission',
|
|
745
|
-
value: backend.permissionMode,
|
|
746
|
-
disabled,
|
|
747
|
-
onChange: (event) => onPatch({ permissionMode: event.target.value }),
|
|
748
|
-
}, optionNodes(spec.permissions, backend.permissionMode))));
|
|
749
|
-
cards.push(h(Field, {
|
|
750
|
-
key: 'defaultModel',
|
|
751
|
-
id: 'apollo-' + spec.cli + '-default-model',
|
|
752
|
-
label: translate('defaultModelLabel'),
|
|
753
|
-
},
|
|
754
|
-
h('select', {
|
|
755
|
-
className: 'apollo-select',
|
|
756
|
-
id: 'apollo-' + spec.cli + '-default-model',
|
|
757
|
-
value: backend.defaultModel,
|
|
758
|
-
disabled,
|
|
759
|
-
onChange: (event) => onPatch({ defaultModel: event.target.value }),
|
|
760
|
-
}, optionNodes(modelIds, backend.defaultModel, translate('defaultModelNone')))));
|
|
761
|
-
cards.push(h(Field, {
|
|
762
|
-
key: 'models',
|
|
763
|
-
label: translate('modelsLabel'),
|
|
764
|
-
hint: translate('modelsHint'),
|
|
765
|
-
},
|
|
766
|
-
backend.models.length === 0
|
|
767
|
-
? h('p', { className: 'apollo-hint' }, translate('modelsEmpty'))
|
|
768
|
-
: h('div', { className: 'apollo-models' },
|
|
769
|
-
h('div', { className: 'apollo-modelHead' },
|
|
770
|
-
h('span', null, translate('modelIdPlaceholder')),
|
|
771
|
-
h('span', null, translate('modelEffortsPlaceholder')),
|
|
772
|
-
h('span', null, translate('modelDefaultEffort')),
|
|
773
|
-
h('span', null, '')),
|
|
774
|
-
backend.models.map((model, index) => h(ModelRow, {
|
|
775
|
-
key: generation + ':' + index,
|
|
776
|
-
model,
|
|
777
|
-
index,
|
|
778
|
-
disabled,
|
|
779
|
-
t: translate,
|
|
780
|
-
onPatch: (patch) => onModelPatch(index, patch),
|
|
781
|
-
onRemove: () => onModelRemove(index),
|
|
782
|
-
}))),
|
|
783
|
-
h('div', { className: 'apollo-actions' },
|
|
784
|
-
h('button', {
|
|
785
|
-
type: 'button',
|
|
786
|
-
id: 'apollo-' + spec.cli + '-discover',
|
|
787
|
-
className: 'apollo-button',
|
|
788
|
-
'data-variant': 'outline',
|
|
789
|
-
disabled: disabled || status?.status === 'loading',
|
|
790
|
-
onClick: onDiscover,
|
|
791
|
-
}, status?.status === 'loading' ? translate('discovering') : translate('discover')),
|
|
792
|
-
h('button', {
|
|
793
|
-
type: 'button',
|
|
794
|
-
className: 'apollo-button',
|
|
795
|
-
'data-variant': 'outline',
|
|
136
|
+
role: 'switch',
|
|
137
|
+
'aria-checked': checked,
|
|
138
|
+
'aria-label': label,
|
|
796
139
|
disabled,
|
|
797
|
-
onClick:
|
|
798
|
-
},
|
|
799
|
-
h('p', {
|
|
800
|
-
className: 'apollo-hint',
|
|
801
|
-
...(status?.status === 'error' ? { 'data-tone': 'error' }
|
|
802
|
-
: status?.status === 'ok' ? { 'data-tone': 'ok' } : {}),
|
|
803
|
-
}, status?.text ?? translate('discoverHint'))));
|
|
804
|
-
return h('div', { className: backend.enabled ? 'apollo-card' : 'apollo-card apollo-cardOff' },
|
|
805
|
-
h('div', { className: 'apollo-cardHead' },
|
|
806
|
-
h('span', { className: 'apollo-cardTitle' }, title)),
|
|
807
|
-
cards);
|
|
140
|
+
onClick: () => onChange(!checked),
|
|
141
|
+
}, h('span', { className: 'apollo-switchThumb' })));
|
|
808
142
|
}
|
|
809
143
|
|
|
810
144
|
/** 行配置页面主体。 */
|
|
811
145
|
function ApolloConfigPage({ form, t: translate }) {
|
|
812
|
-
const capabilities = useEndpoint(CAPABILITIES_URL);
|
|
813
|
-
const routesState = useEndpoint(ROUTES_URL);
|
|
814
|
-
const allowed = routesOf(routesState);
|
|
815
146
|
const state = form?.state;
|
|
816
|
-
const revision = state?.revision;
|
|
817
147
|
const [draft, setDraft] = useState(null);
|
|
818
|
-
const [generation, setGeneration] = useState(0);
|
|
819
148
|
const [busy, setBusy] = useState(false);
|
|
820
149
|
const [notice, setNotice] = useState(null);
|
|
821
|
-
/** 每个后端的发现状态:`{ status: 'loading' | 'ok' | 'error', text? }`。 */
|
|
822
|
-
const [discovery, setDiscovery] = useState({});
|
|
823
|
-
|
|
824
|
-
const host = state?.status === 'ready' ? shape(state.value) : null;
|
|
825
|
-
const live = draft !== null && draft.revision === revision ? draft.value : host;
|
|
826
|
-
const writable = state?.status === 'ready' && state?.writable === true && state?.mode !== 'memory';
|
|
827
|
-
const dirty = live !== null && host !== null && JSON.stringify(live) !== JSON.stringify(host);
|
|
828
|
-
const invalid = live === null ? null : validate(live, allowed);
|
|
829
150
|
|
|
830
151
|
if (state === undefined) {
|
|
831
152
|
// 官方页面在镜像读到该命名空间前不传 form:这是正常加载窗口,
|
|
@@ -838,135 +159,24 @@ window.__ModuleLoader__.load({
|
|
|
838
159
|
return h('div', { className: 'apollo-cfg' },
|
|
839
160
|
h('p', { className: 'apollo-note apollo-pulse', role: 'status' }, translate('loading')));
|
|
840
161
|
}
|
|
841
|
-
if (
|
|
162
|
+
if (state.status !== 'ready') {
|
|
842
163
|
return h('div', { className: 'apollo-cfg' },
|
|
843
164
|
h('p', { className: 'apollo-note', 'data-tone': 'warn' }, translate('unavailable')));
|
|
844
165
|
}
|
|
166
|
+
const host = shape(state.value);
|
|
167
|
+
const live = draft !== null && draft.revision === state.revision ? draft.value : host;
|
|
168
|
+
const writable = state.writable === true && state.mode !== 'memory';
|
|
169
|
+
const dirty = live.exitGuard !== host.exitGuard;
|
|
845
170
|
|
|
846
|
-
/** 记一次草稿修改(函数式叠加:连续编辑不丢改动;换 revision 即失效)。 */
|
|
847
|
-
const edit = (mutate) => {
|
|
848
|
-
setDraft((previous) => {
|
|
849
|
-
const base = previous !== null && previous.revision === revision ? previous.value : live;
|
|
850
|
-
return { revision, value: mutate(base) };
|
|
851
|
-
});
|
|
852
|
-
setNotice(null);
|
|
853
|
-
};
|
|
854
|
-
const patchTier = (tier, patch) => edit((current) => ({
|
|
855
|
-
...current,
|
|
856
|
-
tiers: { ...current.tiers, [tier]: { ...current.tiers[tier], ...patch } },
|
|
857
|
-
}));
|
|
858
|
-
const patchBackend = (spec, patch) => edit((current) => ({
|
|
859
|
-
...current,
|
|
860
|
-
subagents: {
|
|
861
|
-
...current.subagents,
|
|
862
|
-
[spec.cli]: { ...current.subagents[spec.cli], ...patch },
|
|
863
|
-
},
|
|
864
|
-
}));
|
|
865
|
-
/** 按 index 做函数式补丁:不依赖触发那一帧的数组快照。 */
|
|
866
|
-
const patchModel = (spec, index, patch) => edit((current) => {
|
|
867
|
-
const backend = current.subagents[spec.cli];
|
|
868
|
-
const models = backend.models.map((model, at) => (at === index ? { ...model, ...patch } : model));
|
|
869
|
-
return { ...current, subagents: { ...current.subagents, [spec.cli]: { ...backend, models } } };
|
|
870
|
-
});
|
|
871
|
-
const addModel = (spec) => edit((current) => {
|
|
872
|
-
const backend = current.subagents[spec.cli];
|
|
873
|
-
return {
|
|
874
|
-
...current,
|
|
875
|
-
subagents: {
|
|
876
|
-
...current.subagents,
|
|
877
|
-
[spec.cli]: { ...backend, models: [...backend.models, { id: '', efforts: [], defaultEffort: '' }] },
|
|
878
|
-
},
|
|
879
|
-
};
|
|
880
|
-
});
|
|
881
|
-
const removeModel = (spec, index) => edit((current) => {
|
|
882
|
-
const backend = current.subagents[spec.cli];
|
|
883
|
-
return {
|
|
884
|
-
...current,
|
|
885
|
-
subagents: {
|
|
886
|
-
...current.subagents,
|
|
887
|
-
[spec.cli]: { ...backend, models: backend.models.filter((model, at) => at !== index) },
|
|
888
|
-
},
|
|
889
|
-
};
|
|
890
|
-
});
|
|
891
|
-
/** 发现失败原因:只认宿主给的 error.code,不回显可能含凭据的细节。 */
|
|
892
|
-
const discoveryReason = (error) => {
|
|
893
|
-
const key = DISCOVERY_REASON_KEYS[String(error?.code ?? '')];
|
|
894
|
-
if (key === undefined) return translate('discoverUnknown');
|
|
895
|
-
return translate(key, { status: String(error?.status ?? '') });
|
|
896
|
-
};
|
|
897
|
-
/** 拉取发现结果并并入草稿:保留已有条目,只追加新 id。 */
|
|
898
|
-
const discoverModels = async (spec) => {
|
|
899
|
-
if (discovery[spec.cli]?.status === 'loading' || !writable) return;
|
|
900
|
-
setDiscovery((previous) => ({ ...previous, [spec.cli]: { status: 'loading' } }));
|
|
901
|
-
try {
|
|
902
|
-
const response = await fetch(MODELS_URL + '?cli=' + encodeURIComponent(spec.cli));
|
|
903
|
-
const body = await response.json().catch(() => undefined);
|
|
904
|
-
if (!response.ok || body?.ok !== true) throw new Error('HTTP ' + String(response.status));
|
|
905
|
-
const value = isRecord(body.value) ? body.value : {};
|
|
906
|
-
const found = modelList(value.models).filter((model) => model.id.trim() !== '');
|
|
907
|
-
if (found.length === 0) {
|
|
908
|
-
setDiscovery((previous) => ({
|
|
909
|
-
...previous,
|
|
910
|
-
[spec.cli]: {
|
|
911
|
-
status: 'error',
|
|
912
|
-
text: translate('discoverFailed', { reason: discoveryReason(value.error) }),
|
|
913
|
-
},
|
|
914
|
-
}));
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
const known = new Set(live.subagents[spec.cli].models
|
|
918
|
-
.map((model) => model.id.trim()).filter((id) => id !== ''));
|
|
919
|
-
const added = found.filter((model) => !known.has(model.id)).length;
|
|
920
|
-
edit((current) => {
|
|
921
|
-
const backend = current.subagents[spec.cli];
|
|
922
|
-
const existing = new Set(backend.models
|
|
923
|
-
.map((model) => model.id.trim()).filter((id) => id !== ''));
|
|
924
|
-
const models = [...backend.models];
|
|
925
|
-
for (const model of found) {
|
|
926
|
-
if (existing.has(model.id)) continue;
|
|
927
|
-
existing.add(model.id);
|
|
928
|
-
models.push(model);
|
|
929
|
-
}
|
|
930
|
-
const defaultModel = backend.defaultModel.trim() !== '' ? backend.defaultModel : (models[0]?.id ?? '');
|
|
931
|
-
return {
|
|
932
|
-
...current,
|
|
933
|
-
subagents: {
|
|
934
|
-
...current.subagents,
|
|
935
|
-
[spec.cli]: { ...backend, models, defaultModel },
|
|
936
|
-
},
|
|
937
|
-
};
|
|
938
|
-
});
|
|
939
|
-
setDiscovery((previous) => ({
|
|
940
|
-
...previous,
|
|
941
|
-
[spec.cli]: {
|
|
942
|
-
status: 'ok',
|
|
943
|
-
text: translate('discoverDone', { count: found.length, added }),
|
|
944
|
-
},
|
|
945
|
-
}));
|
|
946
|
-
} catch (error) {
|
|
947
|
-
setDiscovery((previous) => ({
|
|
948
|
-
...previous,
|
|
949
|
-
[spec.cli]: {
|
|
950
|
-
status: 'error',
|
|
951
|
-
text: translate('discoverFailed', { reason: String(error?.message ?? error) }),
|
|
952
|
-
},
|
|
953
|
-
}));
|
|
954
|
-
}
|
|
955
|
-
};
|
|
956
171
|
const save = async () => {
|
|
957
|
-
if (!dirty ||
|
|
958
|
-
const ops = diffOps(host, live);
|
|
959
|
-
if (ops.length === 0) return;
|
|
172
|
+
if (!dirty || !writable || busy) return;
|
|
960
173
|
setBusy(true);
|
|
961
174
|
setNotice(null);
|
|
962
175
|
try {
|
|
963
|
-
const accepted = await form.mutate(
|
|
964
|
-
if (accepted) {
|
|
965
|
-
|
|
966
|
-
setNotice({ tone: 'ok', text: translate('saved') });
|
|
967
|
-
} else {
|
|
176
|
+
const accepted = await form.mutate([{ op: 'set', path: ['exitGuard'], value: live.exitGuard }], state.revision);
|
|
177
|
+
if (accepted) setNotice({ tone: 'ok', text: translate('saved') });
|
|
178
|
+
else {
|
|
968
179
|
setDraft(null);
|
|
969
|
-
setGeneration((value) => value + 1);
|
|
970
180
|
setNotice({ tone: 'warn', text: translate('conflict') });
|
|
971
181
|
}
|
|
972
182
|
} catch (error) {
|
|
@@ -975,99 +185,40 @@ window.__ModuleLoader__.load({
|
|
|
975
185
|
setBusy(false);
|
|
976
186
|
}
|
|
977
187
|
};
|
|
978
|
-
const reset = () => {
|
|
979
|
-
setDraft(null);
|
|
980
|
-
setGeneration((value) => value + 1);
|
|
981
|
-
setNotice(null);
|
|
982
|
-
};
|
|
188
|
+
const reset = () => { setDraft(null); setNotice(null); };
|
|
983
189
|
|
|
984
190
|
const status = notice !== null
|
|
985
191
|
? h('span', { className: 'apollo-status', 'data-tone': notice.tone, role: notice.tone === 'error' ? 'alert' : 'status' }, notice.text)
|
|
986
|
-
:
|
|
987
|
-
? h('span', { className: 'apollo-status', 'data-tone': 'error', role: 'alert' },
|
|
988
|
-
translate(invalid.key, { backend: translate(invalid.titleKey) }))
|
|
989
|
-
: h('span', { className: 'apollo-status', role: 'status' }, writable
|
|
990
|
-
? (state.mode === 'host' ? '' : '')
|
|
991
|
-
: (state.status === 'unavailable' ? translate('unavailableNote') : translate('readonlyNote')));
|
|
192
|
+
: h('span', { className: 'apollo-status', role: 'status' }, writable ? '' : translate('readonlyNote'));
|
|
992
193
|
|
|
993
194
|
return h('div', { className: 'apollo-cfg' },
|
|
994
195
|
h('div', { className: 'apollo-sectionHead' },
|
|
995
196
|
h('h3', { className: 'apollo-title' }, translate('title')),
|
|
996
197
|
h('p', { className: 'apollo-intro' }, translate('intro'))),
|
|
997
|
-
writable ? null : h('p', { className: 'apollo-note', 'data-tone': 'warn' },
|
|
998
|
-
state.status === 'unavailable' ? translate('unavailable') : translate('readonly')),
|
|
198
|
+
writable ? null : h('p', { className: 'apollo-note', 'data-tone': 'warn' }, translate('readonly')),
|
|
999
199
|
h('div', { className: 'apollo-section' },
|
|
200
|
+
h('div', { className: 'apollo-sectionHead' },
|
|
201
|
+
h('h3', { className: 'apollo-title' }, translate('exitGuardLabel'))),
|
|
1000
202
|
h(SwitchRow, {
|
|
1001
203
|
id: 'apollo-exit-guard',
|
|
1002
204
|
label: translate('exitGuardLabel'),
|
|
1003
205
|
hint: translate('exitGuardHint'),
|
|
1004
206
|
checked: live.exitGuard,
|
|
1005
207
|
disabled: !writable || busy,
|
|
1006
|
-
onChange: (next) =>
|
|
208
|
+
onChange: (next) => { setDraft({ revision: state.revision, value: { exitGuard: next } }); setNotice(null); },
|
|
1007
209
|
})),
|
|
1008
210
|
h('div', { className: 'apollo-section' },
|
|
1009
211
|
h('div', { className: 'apollo-sectionHead' },
|
|
1010
|
-
h('h3', { className: 'apollo-title' }, translate('
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
: allowed === null
|
|
1015
|
-
? h('p', { className: 'apollo-note', 'data-tone': 'warn' }, translate('routesError'))
|
|
1016
|
-
: !allowed.enabled || allowed.routes.length === 0
|
|
1017
|
-
? h('p', { className: 'apollo-note', 'data-tone': 'warn' }, translate('tiersInactive'))
|
|
1018
|
-
: null,
|
|
1019
|
-
...TIERS.map((tier) => h(TierRow, {
|
|
1020
|
-
key: tier,
|
|
1021
|
-
tier,
|
|
1022
|
-
value: live.tiers[tier],
|
|
1023
|
-
allowed,
|
|
1024
|
-
disabled: !writable || busy,
|
|
1025
|
-
t: translate,
|
|
1026
|
-
onPatch: (patch) => patchTier(tier, patch),
|
|
1027
|
-
}))),
|
|
1028
|
-
h('div', { className: 'apollo-section' },
|
|
1029
|
-
h('div', { className: 'apollo-sectionHead' },
|
|
1030
|
-
h('h3', { className: 'apollo-title' }, translate('scopeTitle'))),
|
|
1031
|
-
h(SwitchRow, {
|
|
1032
|
-
id: 'apollo-scope-eng',
|
|
1033
|
-
label: translate('scopeEng'),
|
|
1034
|
-
hint: translate('scopeEngHint'),
|
|
1035
|
-
checked: live.subagents.eng,
|
|
1036
|
-
disabled: !writable || busy,
|
|
1037
|
-
onChange: (next) => edit((current) => ({ ...current, subagents: { ...current.subagents, eng: next } })),
|
|
1038
|
-
}),
|
|
1039
|
-
h(SwitchRow, {
|
|
1040
|
-
id: 'apollo-scope-global',
|
|
1041
|
-
label: translate('scopeGlobal'),
|
|
1042
|
-
hint: translate('scopeGlobalHint'),
|
|
1043
|
-
checked: live.subagents.global,
|
|
1044
|
-
disabled: !writable || busy,
|
|
1045
|
-
onChange: (next) => edit((current) => ({ ...current, subagents: { ...current.subagents, global: next } })),
|
|
1046
|
-
}),
|
|
1047
|
-
live.subagents.eng || live.subagents.global
|
|
1048
|
-
? null
|
|
1049
|
-
: h('p', { className: 'apollo-note', 'data-tone': 'warn' }, translate('scopeNone'))),
|
|
1050
|
-
...BACKENDS.map((spec) => h(BackendCard, {
|
|
1051
|
-
key: spec.cli,
|
|
1052
|
-
spec,
|
|
1053
|
-
backend: live.subagents[spec.cli],
|
|
1054
|
-
disabled: !writable || busy,
|
|
1055
|
-
capabilities,
|
|
1056
|
-
discovery,
|
|
1057
|
-
t: translate,
|
|
1058
|
-
generation,
|
|
1059
|
-
onPatch: (patch) => patchBackend(spec, patch),
|
|
1060
|
-
onModelPatch: (index, patch) => patchModel(spec, index, patch),
|
|
1061
|
-
onModelAdd: () => addModel(spec),
|
|
1062
|
-
onModelRemove: (index) => removeModel(spec, index),
|
|
1063
|
-
onDiscover: () => discoverModels(spec),
|
|
1064
|
-
})),
|
|
212
|
+
h('h3', { className: 'apollo-title' }, translate('migratedTitle'))),
|
|
213
|
+
h('p', { className: 'apollo-note' }, translate('migratedBody')),
|
|
214
|
+
h('p', { className: 'apollo-note' }, translate('deprecatedBody')),
|
|
215
|
+
h('p', { className: 'apollo-note', 'data-tone': 'warn' }, translate('reloadBody'))),
|
|
1065
216
|
h('div', { className: 'apollo-footer' },
|
|
1066
217
|
h('button', {
|
|
1067
218
|
type: 'button',
|
|
1068
219
|
className: 'apollo-button',
|
|
1069
220
|
'data-variant': 'primary',
|
|
1070
|
-
disabled: !writable || busy || !dirty
|
|
221
|
+
disabled: !writable || busy || !dirty,
|
|
1071
222
|
onClick: () => void save(),
|
|
1072
223
|
}, busy ? translate('saving') : translate('save')),
|
|
1073
224
|
h('button', {
|