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.
- package/README.md +48 -20
- package/README.zh.md +101 -73
- package/docs/screenshots/dispatch-card.png +0 -0
- package/docs/screenshots/settings-page1.png +0 -0
- package/docs/screenshots/settings-page2.png +0 -0
- package/index.mjs +142 -73
- package/lib/client.js +2239 -380
- package/lib/core/adoption-reminder.mjs +48 -0
- package/lib/core/adoption-tracker.mjs +430 -0
- package/lib/core/background-ledger.mjs +71 -0
- package/lib/core/cost-evidence.mjs +145 -0
- package/lib/core/cost-guard.mjs +1 -1
- package/lib/core/decision-trace.mjs +11 -31
- package/lib/core/delegation.mjs +8 -5
- package/lib/core/dispatch-gates.mjs +12 -5
- package/lib/core/dispatch-guard.mjs +6 -0
- package/lib/core/dispatch-schema.mjs +10 -5
- package/lib/core/dispatch-tool.mjs +52 -45
- package/lib/core/draft-gates.mjs +45 -0
- package/lib/core/drafts-store.mjs +45 -0
- package/lib/core/evolution-advice.mjs +224 -0
- package/lib/core/evolution-ledger.mjs +20 -9
- package/lib/core/evolution-summary.mjs +46 -223
- package/lib/core/http-routes.mjs +154 -85
- package/lib/core/presets-sync.mjs +254 -256
- package/lib/core/prices.mjs +46 -0
- package/lib/core/profile-directory.mjs +139 -0
- package/lib/core/profile-provider.mjs +3 -2
- package/lib/core/profiles-store.mjs +1 -1
- package/lib/core/reminder-store.mjs +172 -0
- package/lib/core/shims.mjs +11 -1
- package/package.json +82 -82
- package/presets/orchestrator/agent.cordis.yml +243 -243
package/lib/core/http-routes.mjs
CHANGED
|
@@ -1,34 +1,19 @@
|
|
|
1
|
-
// lib/core/http-routes.mjs — Client UI
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
//
|
|
29
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
132
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
209
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
289
|
-
//
|
|
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
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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));
|