dsh-subagent-profile 0.3.3 → 0.3.4

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.
@@ -1,34 +1,19 @@
1
- // lib/core/http-routes.mjs — Client UI 的设置 HTTP loopback 路由。
2
- // 读路由(list / options 摘要 / 按模型 efforts / tools)+ 写路由
3
- // (set-enabled / add / remove / reset / reset-all / set-profile-enabled)。
4
- // /options 目录(models / presets / tools / efforts)由共享 catalog 缓存
5
- // (lib/core/catalog-cache.mjs)提供,这些路由不再自行遍历 llm 目录。
6
- //
7
- // 注入:所有 apply 闭包 / ctx 依赖都是显式参数——
8
- // store profile store(profiles Map / persistProfiles /
9
- // persistEnabled / deletedBuiltins),
10
- // getEnabled 读取 apply 闭包 `enabled` 标志(被 /set-enabled 改写),
11
- // setEnabled 写它,
12
- // syncTool 在 /set-enabled 时注销/注册 dispatch 工具,
13
- // catalog 共享 catalog 缓存(getSnapshot / invalidate),
14
- // logger ctx.logger(路由错误告警)。
15
- // 工厂返回 scope.effect 设置函数,调用方保持原有注册形状:
16
- // effect(() => register + disposer)。
17
- //
18
- // 写路由各抽为模块级处理函数(if 链分发保持);/options 拆三读路由 + 手动刷新,
19
- // 目录数据统一来自 catalog 快照。
1
+ // lib/core/http-routes.mjs — Client UI 设置 HTTP loopback 路由。/options 目录由共享
2
+ // catalog 缓存(catalog-cache.mjs)提供;注入全是显式参数——store、getEnabled/setEnabled、
3
+ // syncTool、catalog、logger;写路由各抽为模块级处理函数,工厂返回 scope.effect 设置函数。
20
4
 
21
5
  import { sanitizeProfile } from './pure.mjs';
22
6
  import { BUILTIN_SEEDS } from './profiles-store.mjs';
23
7
  import { detectVersions } from './shims.mjs';
8
+ import { readSummaries, computeAdvice } from './evolution-advice.mjs';
9
+ import { profileStatsFromSummaries } from './profile-directory.mjs';
10
+ import { costSummaryFromSummaries } from './cost-evidence.mjs';
24
11
 
25
12
  // 只有 loopback 接口可以驱动设置 HTTP 路由。
26
13
  const LOOPBACKS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
27
14
 
28
- // 写路由 CSRF 传输授权三件套。loopback 只挡「来源不是本机」;这三件挡「浏览器里的
29
- // 跨域表单/脚本冒充本机提交」——schema 校验的是数据形状,管不到谁有权写。
30
- // URL.hostname 对 IPv6 字面量返回带方括号形态(http://[::1]:port → '[::1]'),
31
- // 两种形态都列入,避免 IPv6 loopback 源被误拒。
15
+ // CSRF 三件套:loopback 只挡「非本机」;这三件挡「跨域表单/脚本冒充本机提交」。
16
+ // IPv6 字面量带方括号形态,两种形态都列入避免误拒。
32
17
  const LOOPBACK_ORIGIN_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
33
18
  const CSRF_HEADER = 'X-DSH-Plugin';
34
19
  const CSRF_HEADER_KEY = CSRF_HEADER.toLowerCase();
@@ -55,9 +40,6 @@ function readBody(req) {
55
40
  });
56
41
  }
57
42
 
58
- // 写失败契约:写路由返回 HTTP 200 且 `persisted` 恒在场;磁盘写失败时
59
- // persistWarning 说明「已保存但未持久化」(内存态驱动本进程,磁盘未更新)。
60
- // 客户端把它渲染为琥珀色警告。
61
43
  function persistOk(res, payload, persist) {
62
44
  return json(res, 200, {
63
45
  ok: true,
@@ -67,25 +49,44 @@ function persistOk(res, payload, persist) {
67
49
  });
68
50
  }
69
51
 
70
- function listClean(store) {
52
+ function listClean(store, stats) {
53
+ const map = stats instanceof Map ? stats : new Map();
71
54
  return [...store.profiles.values()].map((profile) => {
72
55
  const clean = {};
73
56
  for (const [key, value] of Object.entries(profile)) if (value !== undefined && key !== 'persisted') clean[key] = value;
74
- // 内部 `persisted` 标志已在上方剥离;对外暴露 UI 面向的 "modified"
75
- // 信号,供重置面板标注被改过的内置方案。
57
+ // 对外暴露 UI 面向的 "modified" 信号(内置方案被改过)。
76
58
  if (profile.builtin === true && profile.persisted === true) clean.modified = true;
59
+ // 方案卡成本预览数据(有数据才带;N 必显作置信度信号)。
60
+ const stat = map.get(profile.id);
61
+ if (stat !== undefined) {
62
+ if (stat.avgCost !== null && stat.avgCost !== undefined) clean.avgCost = stat.avgCost;
63
+ if (stat.successRate !== null && stat.successRate !== undefined) clean.successRate = stat.successRate;
64
+ if (stat.n !== undefined) clean.n = stat.n;
65
+ }
77
66
  return clean;
78
67
  });
79
68
  }
80
69
 
81
- // --- 路由处理函数(从 createHttpRoutes 拆出;if 链分发在内保持)----------------
82
-
83
70
  async function handleList(deps, res) {
84
- return json(res, 200, { ok: true, profiles: listClean(deps.store) });
71
+ let stats = new Map();
72
+ let costSummary = { global: null, byModel: [] };
73
+ let advice = [];
74
+ let adviceGlobal = null;
75
+ try {
76
+ if (typeof deps.refreshAdvice === 'function') deps.refreshAdvice();
77
+ const summaries = typeof deps.summariesFile === 'string' ? readSummaries(deps.summariesFile, deps.logger) : null;
78
+ stats = profileStatsFromSummaries(deps.store, summaries);
79
+ costSummary = costSummaryFromSummaries(summaries);
80
+ if (deps.adviceWhitelist !== undefined && typeof deps.dispatchFile === 'string') {
81
+ const result = computeAdvice({ summariesFile: deps.summariesFile, dispatchFile: deps.dispatchFile, whitelist: deps.adviceWhitelist, logger: deps.logger });
82
+ advice = result.advice ?? [];
83
+ adviceGlobal = result.global ?? null;
84
+ }
85
+ } catch { /* 统计/建议是增量展示;失败不影响 /list 主响应 */ }
86
+ return json(res, 200, { ok: true, profiles: listClean(deps.store, stats), costSummary, advice, adviceGlobal });
85
87
  }
86
88
 
87
- // 轻量摘要:enabled + evolutionAdvice + 模型目录(含 provider 信息,客户端据此派生提供方列表)+ 预设名册
88
- // + 审计分级(lostTelemetry/lostGovernance/health,供设置页红字与丢失计数展示)。
89
+ // 轻量摘要:enabled/advice 开关/模型目录/预设名册/审计分级。
89
90
  async function handleSummary(deps, res) {
90
91
  const snapshot = await deps.catalog.getSnapshot();
91
92
  return json(res, 200, {
@@ -116,25 +117,91 @@ async function handleTools(deps, res) {
116
117
 
117
118
  // 版本探测:三包 version + 越界/未知的中文 warnings(纯探测,同步)。
118
119
  function handleVersions(res) {
119
- const { versions, warnings } = detectVersions();
120
- return json(res, 200, { ok: true, versions, warnings });
120
+ const { versions, warnings, pluginVersion } = detectVersions();
121
+ return json(res, 200, { ok: true, versions, warnings, pluginVersion });
121
122
  }
122
123
 
123
- // 手动刷新:清缓存兜底(TTL 过期前的目录变更经此立即生效)+ 重算 evolution:advice
124
- // 聚合(T1:/options/refresh 此前不触发聚合,summaries.json 永无生成路径)。
125
124
  async function handleRefresh(deps, res) {
126
125
  deps.catalog.invalidate();
127
126
  if (typeof deps.refreshAdvice === 'function') deps.refreshAdvice();
128
127
  return json(res, 200, { ok: true, refreshed: true });
129
128
  }
130
129
 
131
- // 失败台账读取:GET /ledger/failures?session=<id> → { ok:true, failures:[...] }。
132
- // 未知 session 返回空数组;台账是会话级内存结构,进程重启即失。
130
+ // 提醒系统:提醒与审计同源(host reminder store),全动作留痕。
131
+ async function handleRemindersList(deps, res) {
132
+ const store = deps.reminderStore;
133
+ return json(res, 200, { ok: true, reminders: store !== undefined ? store.list() : [], unread: store !== undefined ? store.unreadCount() : 0 });
134
+ }
135
+
136
+ async function handleRemindersAck(deps, req, res) {
137
+ const body = await readBody(req);
138
+ const ids = Array.isArray(body.ids) ? body.ids.filter((id) => typeof id === 'string') : [];
139
+ const acked = deps.reminderStore !== undefined ? deps.reminderStore.ack(ids) : [];
140
+ return json(res, 200, { ok: true, acked });
141
+ }
142
+
143
+ async function handleReminderAction(deps, req, res) {
144
+ const body = await readBody(req);
145
+ const id = typeof body.id === 'string' ? body.id : '';
146
+ const action = typeof body.action === 'string' ? body.action : '';
147
+ const done = deps.reminderStore !== undefined ? deps.reminderStore.actOn(id, action) : false;
148
+ if (!done) return json(res, 400, { ok: false, error: 'subagent-profiles: 提醒 id 或动作无效' });
149
+ return json(res, 200, { ok: true, id, action });
150
+ }
151
+
133
152
  async function handleLedgerFailures(deps, url, res) {
134
153
  const session = url.searchParams.get('session') ?? '';
135
154
  return json(res, 200, { ok: true, failures: deps.ledger.get(session) });
136
155
  }
137
156
 
157
+ async function handleLedgerJobs(deps, url, res) {
158
+ const session = url.searchParams.get('session') ?? '';
159
+ const jobs = deps.backgroundLedger !== undefined ? deps.backgroundLedger.get(session) : [];
160
+ return json(res, 200, { ok: true, jobs });
161
+ }
162
+
163
+ async function handleDraftsList(deps, res) {
164
+ return json(res, 200, { ok: true, drafts: deps.draftsStore.list() });
165
+ }
166
+
167
+ async function handleDraftAdd(deps, req, res) {
168
+ const body = await readBody(req);
169
+ const config = body && typeof body.config === 'object' && body.config !== null ? body.config : {};
170
+ if (typeof config.id !== 'string' || config.id === '') return json(res, 400, { ok: false, error: 'draft config.id 必须为非空字符串' });
171
+ const draft = {
172
+ id: 'draft-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
173
+ source: typeof body.source === 'string' && body.source !== '' ? body.source : 'human',
174
+ config,
175
+ name: typeof body.name === 'string' && body.name !== '' ? body.name : config.id,
176
+ description: typeof body.description === 'string' ? body.description : '',
177
+ createdAt: Date.now(),
178
+ };
179
+ const persist = deps.draftsStore.add(draft);
180
+ return persistOk(res, { draft }, persist);
181
+ }
182
+
183
+ async function handleDraftApply(deps, req, res) {
184
+ const body = await readBody(req);
185
+ const id = body && typeof body.id === 'string' ? body.id : '';
186
+ if (id === '') return json(res, 400, { ok: false, error: 'draft id 不能为空' });
187
+ const draft = deps.draftsStore.get(id);
188
+ if (draft === undefined) return json(res, 404, { ok: false, error: 'draft 不存在(可能已被应用或删除)' });
189
+ const next = { ...draft };
190
+ if (typeof body.name === 'string' && body.name !== '') next.name = body.name;
191
+ if (typeof body.description === 'string') next.description = body.description;
192
+ const applied = await deps.applyDraft(next);
193
+ deps.draftsStore.remove(id);
194
+ return persistOk(res, applied, { persisted: applied.persisted !== false });
195
+ }
196
+
197
+ async function handleDraftRemove(deps, req, res) {
198
+ const body = await readBody(req);
199
+ const id = body && typeof body.id === 'string' ? body.id : '';
200
+ if (id === '') return json(res, 400, { ok: false, error: 'draft id 不能为空' });
201
+ const result = deps.draftsStore.remove(id);
202
+ return persistOk(res, { id }, result);
203
+ }
204
+
138
205
  async function handleSetEnabled(deps, req, res) {
139
206
  const body = await readBody(req);
140
207
  const next = !!(body && body.enabled === true);
@@ -144,8 +211,7 @@ async function handleSetEnabled(deps, req, res) {
144
211
  return persistOk(res, { enabled: deps.getEnabled() }, persist);
145
212
  }
146
213
 
147
- // 只读建议注入开关(默认关):改写 apply 闭包 evolutionAdvice 并持久化到 state.json。
148
- // 不改派发行为(建议只读提示);写路由 CSRF 三件套自动生效。
214
+ // 派发优化建议注入开关(默认关,只提示不改派发行为)。
149
215
  async function handleSetEvolutionAdvice(deps, req, res) {
150
216
  const body = await readBody(req);
151
217
  const next = !!(body && body.advice === true);
@@ -154,8 +220,7 @@ async function handleSetEvolutionAdvice(deps, req, res) {
154
220
  return persistOk(res, { evolutionAdvice: deps.getEvolutionAdvice() }, persist);
155
221
  }
156
222
 
157
- // 逃生舱开关(默认关):改写 apply 闭包 escapeEnabled 并持久化到 state.json。
158
- // 只放行「放行集内且其余三道闸全过」的非 system 预设;非成员即便开关开也恒拒。
223
+ // 逃生舱开关(默认关):只放行「放行集内且三道闸全过」的非 system 预设。
159
224
  async function handleSetEscape(deps, req, res) {
160
225
  const body = await readBody(req);
161
226
  const next = !!(body && body.enabled === true);
@@ -164,8 +229,7 @@ async function handleSetEscape(deps, req, res) {
164
229
  return persistOk(res, { escapeEnabled: deps.getEscapeEnabled() }, persist);
165
230
  }
166
231
 
167
- // 放行集写路径校验:解析请求体的 preset id(非字符串/缺失归一为空串,由调用方
168
- // 400 拒绝)。纯同步、无副作用。
232
+ // 放行集写路径校验:preset id 非字符串/缺失归一空串(调用方 400 拒绝)。
169
233
  function escapePresetId(body) {
170
234
  const raw = body && typeof body === 'object' && typeof body.preset === 'string' ? body.preset : '';
171
235
  return raw.trim();
@@ -197,24 +261,20 @@ async function handleAdd(deps, req, res) {
197
261
  if (typeof profile.id !== 'string' || profile.id.length === 0) {
198
262
  return json(res, 400, { ok: false, error: 'subagent-profiles: profile id 必须为非空字符串' });
199
263
  }
200
- // 写路径上限:strict=true —— 超限/非法字段直接 400 拒绝,
201
- // 与 loadProfiles(strict=false 迁移宽松读取)的行为区分。列被拒字段与中文原因。
264
+ // 写路径 strict=true:超限/非法字段 400 拒绝(与 loadProfiles 宽松读取区分)。
202
265
  const { clean, warnings } = sanitizeProfile(profile, { strict: true });
203
266
  if (warnings.length > 0) {
204
267
  const detail = warnings.map((w) => `${w.field}:${w.reason}`).join(';');
205
268
  return json(res, 400, { ok: false, error: `写入被拒绝:${detail}` });
206
269
  }
207
270
  const hadToolFilter = profile.toolFilter !== undefined;
208
- // tokenTier 不参与上方通用 merge 循环:sanitizeProfile 对「未提供」恒回填
209
- // balanced,若进循环会破坏「未传→保留 existing」语义(编辑内置 researcher
210
- // 时 cheap 会被重置为 balanced)。故单独用 raw-body 守卫:传了才写(strict
211
- // 模式下非法值已在上面 400 拒绝,clean.tokenTier 必为合法 enum)。
271
+ // tokenTier 单独用 raw-body 守卫(sanitize 恒回填 balanced,进 merge 会破坏
272
+ // 「未传→保留 existing」语义);strict 下非法值已在上面 400 拒绝。
212
273
  const hadTokenTier = profile.tokenTier !== undefined;
213
274
  const existing = deps.store.profiles.get(clean.id);
214
275
  const seed = BUILTIN_SEEDS.find((s) => s.id === clean.id);
215
276
  const isBuiltin = (existing !== undefined && existing.builtin === true) || seed !== undefined;
216
- // 合并而非替换:从现有 profile(或它被删时的 seed)出发,表单未带的字段
217
- // (如内置方案的 persona/preset)在编辑或重新添加后仍然保留。
277
+ // 合并而非替换:表单未带字段(内置 persona/preset 等)编辑后仍保留。
218
278
  const merged = { ...(existing ?? seed ?? {}) };
219
279
  merged.id = clean.id;
220
280
  for (const key of ['name', 'description', 'preset', 'provider', 'model', 'reasoningEffort', 'persona', 'enabled']) {
@@ -222,8 +282,7 @@ async function handleAdd(deps, req, res) {
222
282
  if (clean[key] === '' || clean[key] === null) { delete merged[key]; continue; } // 空:清除字段
223
283
  merged[key] = clean[key];
224
284
  }
225
- // toolFilter 特殊处理:前端改成多选下拉后总是传数组,空数组 = 清除。请求未传
226
- // toolFilter 时保留 existing 原值(merge 语义);传了但被 sanitize 归一为空则清除。
285
+ // toolFilter:空数组 = 清除;未传保留 existing;传了但归一为空则清除。
227
286
  if (hadToolFilter) {
228
287
  const tf = clean.toolFilter;
229
288
  if (tf !== undefined && ((Array.isArray(tf.allow) && tf.allow.length > 0) || (Array.isArray(tf.deny) && tf.deny.length > 0))) {
@@ -279,22 +338,18 @@ async function handleSetProfileEnabled(deps, req, res) {
279
338
  return json(res, 404, { ok: false, error: `subagent-profiles: profile "${id}" 不存在(可在设置页确认可用的 profile id)` });
280
339
  }
281
340
  existing.enabled = body && body.enabled === false ? false : true;
282
- // 无条件持久化(不只是内置方案):运行时注册的 profile 的启用/禁用也必须
283
- // 在重启后保留。
341
+ // 无条件持久化:运行时注册的 profile 启用/禁用也须重启后保留。
284
342
  existing.persisted = true;
285
343
  return persistOk(res, { id, enabled: existing.enabled }, deps.store.persistProfiles());
286
344
  }
287
345
 
288
- // 写路由 CSRF 传输授权:Origin 白名单 + Content-Type + 自定义头三道检查。
289
- // 返回 null 表示放行;否则返回 { reason, origin, contentType }——reason 进 403
290
- // 响应与 warn 日志,origin/contentType 只进日志(二者本就是请求头,非秘密)。
291
- // GET 只读路由不经过此处(只读无状态变更,不设防)。
346
+ // CSRF 传输授权:Origin 白名单 + Content-Type + 自定义头;reason 进 403 与 warn
347
+ // 日志,GET 只读不设防。
292
348
  function csrfViolation(req) {
293
349
  const headers = req.headers ?? {};
294
350
  const origin = typeof headers.origin === 'string' ? headers.origin : '';
295
351
  const contentType = typeof headers['content-type'] === 'string' ? headers['content-type'] : '';
296
- // 1. Origin 白名单:带 Origin 时只放行本机源(hostname 宽松判定,端口不限);
297
- // 无 Origin 的本地脚本/curl 跳过此道,靠第 3 道自定义头兜底。
352
+ // 1. Origin 白名单:只放行本机源;无 Origin 的本地脚本靠第 3 道兜底。
298
353
  if (origin !== '') {
299
354
  let host;
300
355
  try { host = new URL(origin).hostname; } catch { host = null; }
@@ -313,9 +368,40 @@ function csrfViolation(req) {
313
368
  return null;
314
369
  }
315
370
 
316
- export function createHttpRoutes({ webServer, store, getEnabled, setEnabled, syncTool, catalog, ledger, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice, logger }) {
317
- const deps = { store, getEnabled, setEnabled, syncTool, catalog, ledger, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice, logger };
318
- // 路由分发(if 链保持,判断顺序与 404/500 兜底不变)。
371
+ // 路由分发链(从 createHttpRoutes 抽出守行门):纯 if 链分发,每条路由一行;
372
+ // 未知路由 404 兜底。CSRF loopback 守卫在 createHttpRoutes 入口完成。
373
+ async function routeRequest(deps, req, res, url, sub) {
374
+ if (req.method === 'GET' && (sub === '/' || sub === '/list')) return handleList(deps, res);
375
+ if (req.method === 'GET' && sub === '/options/summary') return handleSummary(deps, res);
376
+ if (req.method === 'GET' && sub === '/options/versions') return handleVersions(res);
377
+ if (req.method === 'GET' && sub === '/options/efforts') return handleEfforts(deps, url, res);
378
+ if (req.method === 'GET' && sub === '/options/tools') return handleTools(deps, res);
379
+ if (req.method === 'GET' && sub === '/ledger/failures') return handleLedgerFailures(deps, url, res);
380
+ if (req.method === 'GET' && sub === '/ledger/jobs') return handleLedgerJobs(deps, url, res);
381
+ if (req.method === 'GET' && sub === '/reminders') return handleRemindersList(deps, res);
382
+ if (req.method === 'POST' && sub === '/reminders/ack') return handleRemindersAck(deps, req, res);
383
+ if (req.method === 'POST' && sub === '/reminders/action') return handleReminderAction(deps, req, res);
384
+ if (req.method === 'GET' && sub === '/drafts') return handleDraftsList(deps, res);
385
+ if (req.method === 'POST' && sub === '/draft') return handleDraftAdd(deps, req, res);
386
+ if (req.method === 'POST' && sub === '/draft/apply') return handleDraftApply(deps, req, res);
387
+ if (req.method === 'POST' && sub === '/draft/remove') return handleDraftRemove(deps, req, res);
388
+ if (req.method === 'POST' && sub === '/draft/preview') { const body = await readBody(req); return json(res, 200, { ok: true, ...(await deps.previewDraft(body)) }); }
389
+ if (req.method === 'POST' && sub === '/options/refresh') return handleRefresh(deps, res);
390
+ if (req.method === 'POST' && sub === '/set-enabled') return handleSetEnabled(deps, req, res);
391
+ if (req.method === 'POST' && sub === '/set-evolution-advice') return handleSetEvolutionAdvice(deps, req, res);
392
+ if (req.method === 'POST' && sub === '/set-escape') return handleSetEscape(deps, req, res);
393
+ if (req.method === 'POST' && sub === '/escape-add') return handleEscapeAdd(deps, req, res);
394
+ if (req.method === 'POST' && sub === '/escape-remove') return handleEscapeRemove(deps, req, res);
395
+ if (req.method === 'POST' && sub === '/add') return handleAdd(deps, req, res);
396
+ if (req.method === 'POST' && sub === '/remove') return handleRemove(deps, req, res);
397
+ if (req.method === 'POST' && sub === '/reset') return handleReset(deps, req, res);
398
+ if (req.method === 'POST' && sub === '/reset-all') return handleResetAll(deps, res);
399
+ if (req.method === 'POST' && sub === '/set-profile-enabled') return handleSetProfileEnabled(deps, req, res);
400
+ json(res, 404, { ok: false, error: `未知路由 ${sub}` });
401
+ }
402
+
403
+ export function createHttpRoutes({ webServer, store, getEnabled, setEnabled, syncTool, catalog, ledger, backgroundLedger, draftsStore, applyDraft, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice, summariesFile, dispatchFile, adviceWhitelist, previewDraft, reminderStore, logger }) {
404
+ const deps = { store, getEnabled, setEnabled, syncTool, catalog, ledger, backgroundLedger, draftsStore, applyDraft, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice, summariesFile, dispatchFile, adviceWhitelist, previewDraft, reminderStore, logger };
319
405
  const handler = async (req, res) => {
320
406
  const remote = req.socket?.remoteAddress;
321
407
  if (!LOOPBACKS.has(remote)) return json(res, 403, { ok: false, error: '仅限本机访问' });
@@ -333,24 +419,7 @@ export function createHttpRoutes({ webServer, store, getEnabled, setEnabled, syn
333
419
  return json(res, 403, { ok: false, error: `CSRF 防护拒绝:${violation.reason}` });
334
420
  }
335
421
  }
336
- if (req.method === 'GET' && (sub === '/' || sub === '/list')) return handleList(deps, res);
337
- if (req.method === 'GET' && sub === '/options/summary') return handleSummary(deps, res);
338
- if (req.method === 'GET' && sub === '/options/versions') return handleVersions(res);
339
- if (req.method === 'GET' && sub === '/options/efforts') return handleEfforts(deps, url, res);
340
- if (req.method === 'GET' && sub === '/options/tools') return handleTools(deps, res);
341
- if (req.method === 'GET' && sub === '/ledger/failures') return handleLedgerFailures(deps, url, res);
342
- if (req.method === 'POST' && sub === '/options/refresh') return handleRefresh(deps, res);
343
- if (req.method === 'POST' && sub === '/set-enabled') return handleSetEnabled(deps, req, res);
344
- if (req.method === 'POST' && sub === '/set-evolution-advice') return handleSetEvolutionAdvice(deps, req, res);
345
- if (req.method === 'POST' && sub === '/set-escape') return handleSetEscape(deps, req, res);
346
- if (req.method === 'POST' && sub === '/escape-add') return handleEscapeAdd(deps, req, res);
347
- if (req.method === 'POST' && sub === '/escape-remove') return handleEscapeRemove(deps, req, res);
348
- if (req.method === 'POST' && sub === '/add') return handleAdd(deps, req, res);
349
- if (req.method === 'POST' && sub === '/remove') return handleRemove(deps, req, res);
350
- if (req.method === 'POST' && sub === '/reset') return handleReset(deps, req, res);
351
- if (req.method === 'POST' && sub === '/reset-all') return handleResetAll(deps, res);
352
- if (req.method === 'POST' && sub === '/set-profile-enabled') return handleSetProfileEnabled(deps, req, res);
353
- json(res, 404, { ok: false, error: `未知路由 ${sub}` });
422
+ return routeRequest(deps, req, res, url, sub);
354
423
  } catch (error) {
355
424
  // 通用 500 不回显内部错误信息(防泄漏),详情只进宿主日志。
356
425
  deps.logger.error('[dsh-subagent-profile] settings route error:', error instanceof Error ? (error.stack ?? error.message) : String(error));