@menghuan1918/dsh-apollo 0.3.0 → 0.4.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 +44 -23
- package/cordis.patch.yml +3 -3
- package/docs/DEVELOPMENT.md +88 -35
- package/package.json +4 -3
- package/presets/eng/NOTICE +3 -2
- package/presets/eng/plugins/exit-guard/index.js +6 -6
- package/presets/eng/plugins/mode-instructions/index.js +111 -35
- package/presets/eng/plugins/wait-subagent/index.js +9 -4
- package/presets/eng-ptc.patch.yml +186 -0
- package/presets/eng.patch.yml +14 -17
- package/skills/brainstorming/SKILL.md +54 -54
- package/skills/brainstorming/spec-document-reviewer-prompt.md +6 -6
- package/skills/code-review/SKILL.md +1 -1
- package/skills/dev/SKILL.md +16 -18
- package/skills/exec/SKILL.md +56 -69
- package/skills/handoff/SKILL.md +14 -14
- package/skills/main-agent-driven-development/SKILL.md +33 -33
- package/src/cli-adapter.js +1 -1
- package/src/client.js +329 -16
- package/src/config.js +30 -4
- package/src/panel.js +406 -14
- package/src/subagents.js +3 -4
- package/presets/eng-noask/NOTICE +0 -2
- package/presets/eng-noask/plugins/exit-guard/index.js +0 -111
- package/presets/eng-noask/plugins/exit-guard/package.json +0 -7
- package/presets/eng-noask/plugins/mode-instructions/index.js +0 -48
- package/presets/eng-noask/plugins/mode-instructions/package.json +0 -7
- package/presets/eng-noask/plugins/wait-subagent/index.js +0 -228
- package/presets/eng-noask/plugins/wait-subagent/package.json +0 -7
- package/presets/eng-noask.patch.yml +0 -341
package/src/client.js
CHANGED
|
@@ -20,6 +20,12 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
// 旧键并排注册,只为仍把依赖槽记成旧 unscoped 名的 profile 保留配置入口。
|
|
21
21
|
const ROW_KEYS = ['@menghuan1918/dsh-apollo#apollo-skills', 'dsh-apollo#apollo-skills'];
|
|
22
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' };
|
|
23
29
|
|
|
24
30
|
/** Codex CLI 权限档(与 src/config.js 的 CODEX_PERMISSION_MODES 同步)。 */
|
|
25
31
|
const CODEX = {
|
|
@@ -40,7 +46,7 @@ window.__ModuleLoader__.load({
|
|
|
40
46
|
const BACKENDS = [CODEX, CLAUDE];
|
|
41
47
|
|
|
42
48
|
const zh = {
|
|
43
|
-
summary: '
|
|
49
|
+
summary: '工程模式运行时设置:退出守卫、子代理模型分档、CLI 后端与接入范围',
|
|
44
50
|
title: '工程模式运行时设置',
|
|
45
51
|
intro: '保存后即时生效,无需重启。配置写入当前 profile 的行配置(cordis.patch.yml)。',
|
|
46
52
|
loading: '正在读取配置…',
|
|
@@ -50,9 +56,24 @@ window.__ModuleLoader__.load({
|
|
|
50
56
|
readonly: '当前连接只读(设置镜像为进程内 memory),无法写入宿主配置。',
|
|
51
57
|
exitGuardLabel: '退出守卫',
|
|
52
58
|
exitGuardHint: '回合结束时若仍有运行中的子代理或后台命令,注入提醒强制继续处理;用户中断(Esc)始终放行。',
|
|
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}所选路由不在子代理模型白名单内,请改选或改回沿用父会话路由。',
|
|
53
74
|
scopeTitle: '接入范围',
|
|
54
75
|
scopeEng: '工程模式预设',
|
|
55
|
-
scopeEngHint: 'eng / eng-
|
|
76
|
+
scopeEngHint: 'eng / eng-ptc 会话可以看到下面这些 CLI 子代理路由。',
|
|
56
77
|
scopeGlobal: '全部预设',
|
|
57
78
|
scopeGlobalHint: '所有预设的会话都能看到这些路由(影响面更大)。',
|
|
58
79
|
scopeNone: '两个范围都关闭时,这些 CLI 路由不会出现在任何会话里。',
|
|
@@ -71,6 +92,21 @@ window.__ModuleLoader__.load({
|
|
|
71
92
|
modelsLabel: '模型与 effort',
|
|
72
93
|
modelsHint: '每行一个模型:id + 允许的 effort(逗号分隔)+ 默认 effort。',
|
|
73
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: '返回内容过大',
|
|
74
110
|
modelIdPlaceholder: '模型 id',
|
|
75
111
|
modelEffortsPlaceholder: 'low, medium, high',
|
|
76
112
|
modelDefaultEffort: '默认 effort',
|
|
@@ -98,7 +134,7 @@ window.__ModuleLoader__.load({
|
|
|
98
134
|
};
|
|
99
135
|
|
|
100
136
|
const en = {
|
|
101
|
-
summary: 'Eng mode runtime settings: exit guard, CLI
|
|
137
|
+
summary: 'Eng mode runtime settings: exit guard, subagent model tiers, CLI backends, and scope',
|
|
102
138
|
title: 'Eng mode runtime settings',
|
|
103
139
|
intro: 'Saving applies immediately, no restart. Values persist in the active profile row config (cordis.patch.yml).',
|
|
104
140
|
loading: 'Loading configuration…',
|
|
@@ -108,9 +144,24 @@ window.__ModuleLoader__.load({
|
|
|
108
144
|
readonly: 'This connection is read-only (settings mirror is process-local memory); host configuration cannot be written.',
|
|
109
145
|
exitGuardLabel: 'Exit guard',
|
|
110
146
|
exitGuardHint: 'Blocks turn end while subagents or background jobs are still running; interrupt (Esc) always passes through.',
|
|
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.',
|
|
111
162
|
scopeTitle: 'Scope',
|
|
112
163
|
scopeEng: 'Eng preset',
|
|
113
|
-
scopeEngHint: 'eng / eng-
|
|
164
|
+
scopeEngHint: 'eng / eng-ptc sessions see the CLI subagent routes below.',
|
|
114
165
|
scopeGlobal: 'All presets',
|
|
115
166
|
scopeGlobalHint: 'Every preset’s sessions see these routes (broader impact).',
|
|
116
167
|
scopeNone: 'With both scopes off, these CLI routes appear in no session.',
|
|
@@ -129,6 +180,21 @@ window.__ModuleLoader__.load({
|
|
|
129
180
|
modelsLabel: 'Models and efforts',
|
|
130
181
|
modelsHint: 'One model per row: id + allowed efforts (comma separated) + default effort.',
|
|
131
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',
|
|
132
198
|
modelIdPlaceholder: 'model id',
|
|
133
199
|
modelEffortsPlaceholder: 'low, medium, high',
|
|
134
200
|
modelDefaultEffort: 'Default effort',
|
|
@@ -155,6 +221,19 @@ window.__ModuleLoader__.load({
|
|
|
155
221
|
unavailableNote: 'Not writable: this configuration is unavailable on the current connection.',
|
|
156
222
|
};
|
|
157
223
|
|
|
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
|
+
|
|
158
237
|
// ── 文案(locale 服务缺失时保持中文默认)───────────────────────────────
|
|
159
238
|
|
|
160
239
|
function interpolate(text, params) {
|
|
@@ -178,6 +257,9 @@ window.__ModuleLoader__.load({
|
|
|
178
257
|
'@media (prefers-reduced-motion:reduce){.apollo-pulse{animation:none}}',
|
|
179
258
|
'.apollo-title{margin:0;font-size:14px;font-weight:500;line-height:22px}',
|
|
180
259
|
'.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}',
|
|
181
263
|
'.apollo-intro{margin:0;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
|
|
182
264
|
'.apollo-section{display:flex;flex-direction:column;gap:2px;padding-top:12px;border-top:.5px solid var(--dsw-alias-border-l2)}',
|
|
183
265
|
'.apollo-sectionHead{display:flex;flex-direction:column;gap:2px;padding-bottom:4px}',
|
|
@@ -208,7 +290,8 @@ window.__ModuleLoader__.load({
|
|
|
208
290
|
'.apollo-models{display:flex;flex-direction:column;gap:6px}',
|
|
209
291
|
'.apollo-model{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) minmax(0,132px) auto;gap:6px;align-items:center}',
|
|
210
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)}',
|
|
211
|
-
'
|
|
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}}',
|
|
212
295
|
'.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
296
|
'.apollo-button:disabled{opacity:.4;cursor:not-allowed}',
|
|
214
297
|
'.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))}',
|
|
@@ -267,17 +350,29 @@ window.__ModuleLoader__.load({
|
|
|
267
350
|
return backend;
|
|
268
351
|
}
|
|
269
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
|
+
|
|
270
362
|
/** 把宿主快照值(或 undefined)规整成表单可安全读写的形状。 */
|
|
271
363
|
function shape(value) {
|
|
272
364
|
const source = isRecord(value) ? value : {};
|
|
273
365
|
const subagents = isRecord(source.subagents) ? source.subagents : {};
|
|
366
|
+
const tiers = isRecord(source.tiers) ? source.tiers : {};
|
|
274
367
|
const result = {
|
|
275
368
|
exitGuard: source.exitGuard === true,
|
|
369
|
+
tiers: {},
|
|
276
370
|
subagents: {
|
|
277
371
|
eng: subagents.eng !== false,
|
|
278
372
|
global: subagents.global === true,
|
|
279
373
|
},
|
|
280
374
|
};
|
|
375
|
+
for (const tier of TIERS) result.tiers[tier] = tierShape(tiers[tier]);
|
|
281
376
|
for (const spec of BACKENDS) result.subagents[spec.cli] = backendShape(subagents[spec.cli], spec);
|
|
282
377
|
return result;
|
|
283
378
|
}
|
|
@@ -299,6 +394,10 @@ window.__ModuleLoader__.load({
|
|
|
299
394
|
if (host.exitGuard !== current.exitGuard) {
|
|
300
395
|
ops.push({ op: 'set', path: ['exitGuard'], value: current.exitGuard });
|
|
301
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
|
+
}
|
|
302
401
|
for (const key of ['eng', 'global']) {
|
|
303
402
|
if (host.subagents[key] !== current.subagents[key]) {
|
|
304
403
|
ops.push({ op: 'set', path: ['subagents', key], value: current.subagents[key] });
|
|
@@ -319,9 +418,21 @@ window.__ModuleLoader__.load({
|
|
|
319
418
|
* 前端校验(宿主 schema 管不到跨字段关联,这里补齐):
|
|
320
419
|
* 模型 id 非空且唯一;**启用的**后端必须有默认模型,且该模型的 effort
|
|
321
420
|
* 列表与默认 effort 齐备——CLI 路由按这三元组选档,缺一项运行时就报错。
|
|
322
|
-
*
|
|
421
|
+
* 分档:白名单读取成功时,已选路由必须在白名单内(否则子代理派发会被策略拒绝)。
|
|
422
|
+
* @param current - 表单当前值。
|
|
423
|
+
* @param allowed - `routesOf()` 的结果;白名单未读到时为 null(不校验分档)。
|
|
424
|
+
* @returns `{ key, titleKey }`(key 用 `{backend}` 参数渲染后端/分档名),或 null。
|
|
323
425
|
*/
|
|
324
|
-
function validate(current) {
|
|
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
|
+
}
|
|
325
436
|
for (const spec of BACKENDS) {
|
|
326
437
|
const backend = current.subagents[spec.cli];
|
|
327
438
|
const titleKey = spec.cli === 'codex' ? 'codexTitle' : 'claudeTitle';
|
|
@@ -343,8 +454,8 @@ window.__ModuleLoader__.load({
|
|
|
343
454
|
|
|
344
455
|
// ── 能力探针(host 半边的只读接口)─────────────────────────────────────
|
|
345
456
|
|
|
346
|
-
/**
|
|
347
|
-
function
|
|
457
|
+
/** 拉取一个 `/apollo/api/*` 只读接口;失败只降级显示,不阻塞表单。 */
|
|
458
|
+
function useEndpoint(url) {
|
|
348
459
|
const [state, setState] = useState({ status: 'loading' });
|
|
349
460
|
useEffect(() => {
|
|
350
461
|
let active = true;
|
|
@@ -354,7 +465,7 @@ window.__ModuleLoader__.load({
|
|
|
354
465
|
};
|
|
355
466
|
void (async () => {
|
|
356
467
|
try {
|
|
357
|
-
const res = await fetch(
|
|
468
|
+
const res = await fetch(url, controller === undefined ? undefined : { signal: controller.signal });
|
|
358
469
|
const body = await res.json().catch(() => undefined);
|
|
359
470
|
if (!res.ok || body?.ok !== true) throw new Error('HTTP ' + String(res.status));
|
|
360
471
|
done({ status: 'ready', value: body.value });
|
|
@@ -366,10 +477,49 @@ window.__ModuleLoader__.load({
|
|
|
366
477
|
active = false;
|
|
367
478
|
controller?.abort();
|
|
368
479
|
};
|
|
369
|
-
}, []);
|
|
480
|
+
}, [url]);
|
|
370
481
|
return state;
|
|
371
482
|
}
|
|
372
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;
|
|
514
|
+
}
|
|
515
|
+
|
|
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
|
+
|
|
373
523
|
/** 一行探针结论:已检测到 <路径>(版本)/ 未找到。 */
|
|
374
524
|
function detectionOf(capabilities, spec, backend, translate) {
|
|
375
525
|
if (capabilities.status === 'loading') return translate('probing');
|
|
@@ -480,9 +630,64 @@ window.__ModuleLoader__.load({
|
|
|
480
630
|
}, '−'));
|
|
481
631
|
}
|
|
482
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
|
+
|
|
483
687
|
/** 一个 CLI 后端的配置卡片。 */
|
|
484
|
-
function BackendCard({ spec, backend, disabled, capabilities, t: translate, onPatch, onModelPatch, onModelAdd, onModelRemove, generation }) {
|
|
688
|
+
function BackendCard({ spec, backend, disabled, capabilities, discovery, t: translate, onPatch, onModelPatch, onModelAdd, onModelRemove, onDiscover, generation }) {
|
|
485
689
|
const title = translate(spec.cli === 'codex' ? 'codexTitle' : 'claudeTitle');
|
|
690
|
+
const status = discovery === undefined ? undefined : discovery[spec.cli];
|
|
486
691
|
const profileNames = capabilities.status === 'ready' && Array.isArray(capabilities.value?.codexProfiles)
|
|
487
692
|
? capabilities.value.codexProfiles.filter((name) => typeof name === 'string')
|
|
488
693
|
: [];
|
|
@@ -575,14 +780,27 @@ window.__ModuleLoader__.load({
|
|
|
575
780
|
onPatch: (patch) => onModelPatch(index, patch),
|
|
576
781
|
onRemove: () => onModelRemove(index),
|
|
577
782
|
}))),
|
|
578
|
-
h('div',
|
|
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')),
|
|
579
792
|
h('button', {
|
|
580
793
|
type: 'button',
|
|
581
794
|
className: 'apollo-button',
|
|
582
795
|
'data-variant': 'outline',
|
|
583
796
|
disabled,
|
|
584
797
|
onClick: onModelAdd,
|
|
585
|
-
}, translate('modelAdd')))
|
|
798
|
+
}, translate('modelAdd'))),
|
|
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'))));
|
|
586
804
|
return h('div', { className: backend.enabled ? 'apollo-card' : 'apollo-card apollo-cardOff' },
|
|
587
805
|
h('div', { className: 'apollo-cardHead' },
|
|
588
806
|
h('span', { className: 'apollo-cardTitle' }, title)),
|
|
@@ -591,19 +809,23 @@ window.__ModuleLoader__.load({
|
|
|
591
809
|
|
|
592
810
|
/** 行配置页面主体。 */
|
|
593
811
|
function ApolloConfigPage({ form, t: translate }) {
|
|
594
|
-
const capabilities =
|
|
812
|
+
const capabilities = useEndpoint(CAPABILITIES_URL);
|
|
813
|
+
const routesState = useEndpoint(ROUTES_URL);
|
|
814
|
+
const allowed = routesOf(routesState);
|
|
595
815
|
const state = form?.state;
|
|
596
816
|
const revision = state?.revision;
|
|
597
817
|
const [draft, setDraft] = useState(null);
|
|
598
818
|
const [generation, setGeneration] = useState(0);
|
|
599
819
|
const [busy, setBusy] = useState(false);
|
|
600
820
|
const [notice, setNotice] = useState(null);
|
|
821
|
+
/** 每个后端的发现状态:`{ status: 'loading' | 'ok' | 'error', text? }`。 */
|
|
822
|
+
const [discovery, setDiscovery] = useState({});
|
|
601
823
|
|
|
602
824
|
const host = state?.status === 'ready' ? shape(state.value) : null;
|
|
603
825
|
const live = draft !== null && draft.revision === revision ? draft.value : host;
|
|
604
826
|
const writable = state?.status === 'ready' && state?.writable === true && state?.mode !== 'memory';
|
|
605
827
|
const dirty = live !== null && host !== null && JSON.stringify(live) !== JSON.stringify(host);
|
|
606
|
-
const invalid = live === null ? null : validate(live);
|
|
828
|
+
const invalid = live === null ? null : validate(live, allowed);
|
|
607
829
|
|
|
608
830
|
if (state === undefined) {
|
|
609
831
|
// 官方页面在镜像读到该命名空间前不传 form:这是正常加载窗口,
|
|
@@ -629,6 +851,10 @@ window.__ModuleLoader__.load({
|
|
|
629
851
|
});
|
|
630
852
|
setNotice(null);
|
|
631
853
|
};
|
|
854
|
+
const patchTier = (tier, patch) => edit((current) => ({
|
|
855
|
+
...current,
|
|
856
|
+
tiers: { ...current.tiers, [tier]: { ...current.tiers[tier], ...patch } },
|
|
857
|
+
}));
|
|
632
858
|
const patchBackend = (spec, patch) => edit((current) => ({
|
|
633
859
|
...current,
|
|
634
860
|
subagents: {
|
|
@@ -662,6 +888,71 @@ window.__ModuleLoader__.load({
|
|
|
662
888
|
},
|
|
663
889
|
};
|
|
664
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
|
+
};
|
|
665
956
|
const save = async () => {
|
|
666
957
|
if (!dirty || invalid !== null || !writable) return;
|
|
667
958
|
const ops = diffOps(host, live);
|
|
@@ -714,6 +1005,26 @@ window.__ModuleLoader__.load({
|
|
|
714
1005
|
disabled: !writable || busy,
|
|
715
1006
|
onChange: (next) => edit((current) => ({ ...current, exitGuard: next })),
|
|
716
1007
|
})),
|
|
1008
|
+
h('div', { className: 'apollo-section' },
|
|
1009
|
+
h('div', { className: 'apollo-sectionHead' },
|
|
1010
|
+
h('h3', { className: 'apollo-title' }, translate('tiersTitle')),
|
|
1011
|
+
h('p', { className: 'apollo-hint' }, translate('tiersIntro'))),
|
|
1012
|
+
routesState.status === 'loading'
|
|
1013
|
+
? h('p', { className: 'apollo-hint' }, translate('routesLoading'))
|
|
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
|
+
}))),
|
|
717
1028
|
h('div', { className: 'apollo-section' },
|
|
718
1029
|
h('div', { className: 'apollo-sectionHead' },
|
|
719
1030
|
h('h3', { className: 'apollo-title' }, translate('scopeTitle'))),
|
|
@@ -742,12 +1053,14 @@ window.__ModuleLoader__.load({
|
|
|
742
1053
|
backend: live.subagents[spec.cli],
|
|
743
1054
|
disabled: !writable || busy,
|
|
744
1055
|
capabilities,
|
|
1056
|
+
discovery,
|
|
745
1057
|
t: translate,
|
|
746
1058
|
generation,
|
|
747
1059
|
onPatch: (patch) => patchBackend(spec, patch),
|
|
748
1060
|
onModelPatch: (index, patch) => patchModel(spec, index, patch),
|
|
749
1061
|
onModelAdd: () => addModel(spec),
|
|
750
1062
|
onModelRemove: (index) => removeModel(spec, index),
|
|
1063
|
+
onDiscover: () => discoverModels(spec),
|
|
751
1064
|
})),
|
|
752
1065
|
h('div', { className: 'apollo-footer' },
|
|
753
1066
|
h('button', {
|
package/src/config.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* 声明,改动会让 Loader 正常重挂本行(重挂才需要重新读)。
|
|
13
13
|
*
|
|
14
14
|
* 可编辑范围(volatile,即 dsh-settings 投影出的行配置表单字段):
|
|
15
|
-
* `exitGuard`、`subagents.eng`、`subagents.global
|
|
16
|
-
* `subagents.claude.*`。表单写入只能落在 volatile 路径上(dsh-settings 会拒绝
|
|
15
|
+
* `exitGuard`、`tiers.fast`、`tiers.quality`、`subagents.eng`、`subagents.global`、
|
|
16
|
+
* `subagents.codex.*`、`subagents.claude.*`。表单写入只能落在 volatile 路径上(dsh-settings 会拒绝
|
|
17
17
|
* 其他路径),所以部署标志不会从网页上被改写。
|
|
18
18
|
*
|
|
19
19
|
* 持久化:DSH 的 configEditor 把表单写入落到活动 profile 的 cordis.patch.yml
|
|
@@ -94,6 +94,16 @@ function backendSchema(spec) {
|
|
|
94
94
|
return z.object(dict).volatile()
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/** 子代理模型分档的一档:provider/model 为空表示沿用父会话路由。 */
|
|
98
|
+
const TierSchema = z.object({
|
|
99
|
+
provider: z.string().default(''),
|
|
100
|
+
model: z.string().default(''),
|
|
101
|
+
reasoningEffort: z.string().default(''),
|
|
102
|
+
}).volatile()
|
|
103
|
+
|
|
104
|
+
/** 分档名(提示词按此顺序渲染)。 */
|
|
105
|
+
export const TIER_NAMES = Object.freeze(['fast', 'quality'])
|
|
106
|
+
|
|
97
107
|
/**
|
|
98
108
|
* 本插件行的正式配置 schema。
|
|
99
109
|
*
|
|
@@ -104,9 +114,14 @@ function backendSchema(spec) {
|
|
|
104
114
|
export const Config = z.object({
|
|
105
115
|
/** 退出守卫总开关(默认关闭,显式开启才拦回合结束)。 */
|
|
106
116
|
exitGuard: z.boolean().default(false).volatile(),
|
|
117
|
+
/** 子代理模型分档(工程模式提示词按此渲染;路由须在 allowedModels 白名单内)。 */
|
|
118
|
+
tiers: z.object({
|
|
119
|
+
fast: TierSchema,
|
|
120
|
+
quality: TierSchema,
|
|
121
|
+
}),
|
|
107
122
|
/** 子代理 CLI 后端的接入范围与每后端配置。 */
|
|
108
123
|
subagents: z.object({
|
|
109
|
-
/** 工程模式预设(`eng`/`eng-
|
|
124
|
+
/** 工程模式预设(`eng`/`eng-ptc`)可见这些后端。 */
|
|
110
125
|
eng: z.boolean().default(true).volatile(),
|
|
111
126
|
/** 所有预设可见这些后端。 */
|
|
112
127
|
global: z.boolean().default(false).volatile(),
|
|
@@ -212,20 +227,31 @@ function backendOf(value, spec) {
|
|
|
212
227
|
return backend
|
|
213
228
|
}
|
|
214
229
|
|
|
230
|
+
/** 一档读取:provider 或 model 任一为空则整档清空(= 沿用父会话路由)。 */
|
|
231
|
+
function tierOf(value) {
|
|
232
|
+
const source = isRecord(value) ? value : {}
|
|
233
|
+
const provider = stringOf(source.provider, '')
|
|
234
|
+
const model = stringOf(source.model, '')
|
|
235
|
+
if (provider === '' || model === '') return { provider: '', model: '', reasoningEffort: '' }
|
|
236
|
+
return { provider, model, reasoningEffort: stringOf(source.reasoningEffort, '') }
|
|
237
|
+
}
|
|
238
|
+
|
|
215
239
|
/**
|
|
216
240
|
* 把任意配置来源规整成普通 JSON(形状 EXACT,非法值回退默认)。
|
|
217
241
|
*
|
|
218
242
|
* 输入可以是 `entry.fiber.config`(volatile 字段是引用对象)、旧 JSON 文档、
|
|
219
243
|
* 残缺对象或 `undefined`;返回值总是全新的、可安全改写的普通对象。
|
|
220
244
|
* @param config - 任意配置值。
|
|
221
|
-
* @returns `{ exitGuard, subagents: { eng, global, codex, claude }, trustedHosts, declareOwnsHost }`。
|
|
245
|
+
* @returns `{ exitGuard, tiers: { fast, quality }, subagents: { eng, global, codex, claude }, trustedHosts, declareOwnsHost }`。
|
|
222
246
|
*/
|
|
223
247
|
export function normalizeConfig(config) {
|
|
224
248
|
const projected = plain(config)
|
|
225
249
|
const source = isRecord(projected) ? projected : {}
|
|
226
250
|
const subagents = isRecord(source.subagents) ? source.subagents : {}
|
|
251
|
+
const tiers = isRecord(source.tiers) ? source.tiers : {}
|
|
227
252
|
return {
|
|
228
253
|
exitGuard: booleanOf(source.exitGuard, false),
|
|
254
|
+
tiers: { fast: tierOf(tiers.fast), quality: tierOf(tiers.quality) },
|
|
229
255
|
subagents: {
|
|
230
256
|
eng: booleanOf(subagents.eng, true),
|
|
231
257
|
global: booleanOf(subagents.global, false),
|