dsh-subagent-profile 0.3.3 → 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 +49 -20
- package/README.zh.md +102 -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 +153 -75
- package/lib/client.js +3081 -444
- 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 +11 -7
- 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 +323 -0
- package/lib/core/evolution-assets.mjs +167 -0
- package/lib/core/evolution-draft.mjs +312 -0
- package/lib/core/evolution-engine.mjs +269 -0
- package/lib/core/evolution-generate.mjs +196 -0
- package/lib/core/evolution-ledger.mjs +149 -33
- package/lib/core/evolution-persistence.mjs +213 -0
- package/lib/core/evolution-renewal.mjs +64 -0
- package/lib/core/evolution-routes.mjs +109 -0
- package/lib/core/evolution-summary.mjs +46 -223
- package/lib/core/http-helpers.mjs +35 -0
- package/lib/core/http-routes.mjs +169 -115
- 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 +5 -3
- package/lib/core/profiles-store.mjs +24 -6
- package/lib/core/reminder-store.mjs +172 -0
- package/lib/core/session-read.mjs +17 -0
- package/lib/core/shims.mjs +13 -2
- package/package.json +82 -82
- package/presets/orchestrator/agent.cordis.yml +243 -243
package/lib/core/http-routes.mjs
CHANGED
|
@@ -1,91 +1,70 @@
|
|
|
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 } from './evolution-advice.mjs';
|
|
9
|
+
import { profileStatsFromSummaries } from './profile-directory.mjs';
|
|
10
|
+
import { costSummaryFromSummaries } from './cost-evidence.mjs';
|
|
11
|
+
import { json, readBody, persistOk } from './http-helpers.mjs';
|
|
12
|
+
import { handleEvolutionAssets, handleEvolutionApply, handleEvolutionRegenerate, handleAdviceSaveProfile, handleSettingsCandidate } from './evolution-routes.mjs';
|
|
24
13
|
|
|
25
14
|
// 只有 loopback 接口可以驱动设置 HTTP 路由。
|
|
26
15
|
const LOOPBACKS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
27
16
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// URL.hostname 对 IPv6 字面量返回带方括号形态(http://[::1]:port → '[::1]'),
|
|
31
|
-
// 两种形态都列入,避免 IPv6 loopback 源被误拒。
|
|
17
|
+
// CSRF 三件套:loopback 只挡「非本机」;这三件挡「跨域表单/脚本冒充本机提交」。
|
|
18
|
+
// IPv6 字面量带方括号形态,两种形态都列入避免误拒。
|
|
32
19
|
const LOOPBACK_ORIGIN_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
|
|
33
20
|
const CSRF_HEADER = 'X-DSH-Plugin';
|
|
34
21
|
const CSRF_HEADER_KEY = CSRF_HEADER.toLowerCase();
|
|
35
22
|
const CSRF_HEADER_VALUE = 'dsh-subagent-profile';
|
|
36
23
|
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
res.end(JSON.stringify(data));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function readBody(req) {
|
|
43
|
-
return new Promise((resolve, reject) => {
|
|
44
|
-
let data = '';
|
|
45
|
-
let size = 0;
|
|
46
|
-
req.on('data', (chunk) => {
|
|
47
|
-
size += chunk.length;
|
|
48
|
-
if (size > 1 << 20) { reject(new Error('请求体过大')); req.destroy(); return; }
|
|
49
|
-
data += chunk;
|
|
50
|
-
});
|
|
51
|
-
req.on('end', () => {
|
|
52
|
-
try { resolve(data === '' ? {} : JSON.parse(data)); } catch { reject(new Error('请求体不是合法 JSON')); }
|
|
53
|
-
});
|
|
54
|
-
req.on('error', reject);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 写失败契约:写路由返回 HTTP 200 且 `persisted` 恒在场;磁盘写失败时
|
|
59
|
-
// persistWarning 说明「已保存但未持久化」(内存态驱动本进程,磁盘未更新)。
|
|
60
|
-
// 客户端把它渲染为琥珀色警告。
|
|
61
|
-
function persistOk(res, payload, persist) {
|
|
62
|
-
return json(res, 200, {
|
|
63
|
-
ok: true,
|
|
64
|
-
...payload,
|
|
65
|
-
persisted: persist.persisted,
|
|
66
|
-
...(persist.persisted ? {} : { persistWarning: '已保存但未持久化' }),
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function listClean(store) {
|
|
24
|
+
function listClean(store, stats) {
|
|
25
|
+
const map = stats instanceof Map ? stats : new Map();
|
|
71
26
|
return [...store.profiles.values()].map((profile) => {
|
|
72
27
|
const clean = {};
|
|
73
28
|
for (const [key, value] of Object.entries(profile)) if (value !== undefined && key !== 'persisted') clean[key] = value;
|
|
74
|
-
//
|
|
75
|
-
// 信号,供重置面板标注被改过的内置方案。
|
|
29
|
+
// 对外暴露 UI 面向的 "modified" 信号(内置方案被改过)。
|
|
76
30
|
if (profile.builtin === true && profile.persisted === true) clean.modified = true;
|
|
31
|
+
// 方案卡成本预览数据(有数据才带;N 必显作置信度信号)。
|
|
32
|
+
const stat = map.get(profile.id);
|
|
33
|
+
if (stat !== undefined) {
|
|
34
|
+
if (stat.avgCost !== null && stat.avgCost !== undefined) clean.avgCost = stat.avgCost;
|
|
35
|
+
if (stat.successRate !== null && stat.successRate !== undefined) clean.successRate = stat.successRate;
|
|
36
|
+
if (stat.n !== undefined) clean.n = stat.n;
|
|
37
|
+
}
|
|
77
38
|
return clean;
|
|
78
39
|
});
|
|
79
40
|
}
|
|
80
41
|
|
|
81
|
-
// --- 路由处理函数(从 createHttpRoutes 拆出;if 链分发在内保持)----------------
|
|
82
|
-
|
|
83
42
|
async function handleList(deps, res) {
|
|
84
|
-
|
|
43
|
+
let stats = new Map();
|
|
44
|
+
let costSummary = { global: null, byModel: [] };
|
|
45
|
+
let advice = [];
|
|
46
|
+
let adviceGlobal = null;
|
|
47
|
+
let evolutionCandidates = [];
|
|
48
|
+
let adviceDetails = {};
|
|
49
|
+
try {
|
|
50
|
+
if (typeof deps.refreshAdvice === 'function') deps.refreshAdvice();
|
|
51
|
+
const summaries = typeof deps.summariesFile === 'string' ? readSummaries(deps.summariesFile, deps.logger) : null;
|
|
52
|
+
stats = profileStatsFromSummaries(deps.store, summaries);
|
|
53
|
+
costSummary = costSummaryFromSummaries(summaries);
|
|
54
|
+
if (deps.adviceWhitelist !== undefined && typeof deps.dispatchFile === 'string' && typeof deps.adviceSource === 'function') {
|
|
55
|
+
// 建议 + 候选生成统一走装配层 helper(computeAdvice 之后推导候选资产),
|
|
56
|
+
// 避免 /list 与其它消费点重复生成;候选随建议链开关同生共死。
|
|
57
|
+
const result = await deps.adviceSource();
|
|
58
|
+
advice = result.advice ?? [];
|
|
59
|
+
adviceGlobal = result.global ?? null;
|
|
60
|
+
evolutionCandidates = Array.isArray(result.candidates) ? result.candidates : [];
|
|
61
|
+
adviceDetails = result.adviceDetails !== null && typeof result.adviceDetails === 'object' ? result.adviceDetails : {};
|
|
62
|
+
}
|
|
63
|
+
} catch { /* 统计/建议是增量展示;失败不影响 /list 主响应 */ }
|
|
64
|
+
return json(res, 200, { ok: true, profiles: listClean(deps.store, stats), costSummary, advice, adviceGlobal, evolutionCandidates, adviceDetails });
|
|
85
65
|
}
|
|
86
66
|
|
|
87
|
-
// 轻量摘要:enabled
|
|
88
|
-
// + 审计分级(lostTelemetry/lostGovernance/health,供设置页红字与丢失计数展示)。
|
|
67
|
+
// 轻量摘要:enabled/advice 开关/模型目录/预设名册/审计分级。
|
|
89
68
|
async function handleSummary(deps, res) {
|
|
90
69
|
const snapshot = await deps.catalog.getSnapshot();
|
|
91
70
|
return json(res, 200, {
|
|
@@ -97,6 +76,8 @@ async function handleSummary(deps, res) {
|
|
|
97
76
|
audit: deps.getAudit(),
|
|
98
77
|
escapeEnabled: deps.getEscapeEnabled(),
|
|
99
78
|
escapePresets: deps.escape.list(),
|
|
79
|
+
candidateMode: deps.store.loadCandidateMode(),
|
|
80
|
+
candidateTtlH: deps.store.loadCandidateTtlH(),
|
|
100
81
|
});
|
|
101
82
|
}
|
|
102
83
|
|
|
@@ -116,25 +97,91 @@ async function handleTools(deps, res) {
|
|
|
116
97
|
|
|
117
98
|
// 版本探测:三包 version + 越界/未知的中文 warnings(纯探测,同步)。
|
|
118
99
|
function handleVersions(res) {
|
|
119
|
-
const { versions, warnings } = detectVersions();
|
|
120
|
-
return json(res, 200, { ok: true, versions, warnings });
|
|
100
|
+
const { versions, warnings, pluginVersion } = detectVersions();
|
|
101
|
+
return json(res, 200, { ok: true, versions, warnings, pluginVersion });
|
|
121
102
|
}
|
|
122
103
|
|
|
123
|
-
// 手动刷新:清缓存兜底(TTL 过期前的目录变更经此立即生效)+ 重算 evolution:advice
|
|
124
|
-
// 聚合(T1:/options/refresh 此前不触发聚合,summaries.json 永无生成路径)。
|
|
125
104
|
async function handleRefresh(deps, res) {
|
|
126
105
|
deps.catalog.invalidate();
|
|
127
106
|
if (typeof deps.refreshAdvice === 'function') deps.refreshAdvice();
|
|
128
107
|
return json(res, 200, { ok: true, refreshed: true });
|
|
129
108
|
}
|
|
130
109
|
|
|
131
|
-
//
|
|
132
|
-
|
|
110
|
+
// 提醒系统:提醒与审计同源(host reminder store),全动作留痕。
|
|
111
|
+
async function handleRemindersList(deps, res) {
|
|
112
|
+
const store = deps.reminderStore;
|
|
113
|
+
return json(res, 200, { ok: true, reminders: store !== undefined ? store.list() : [], unread: store !== undefined ? store.unreadCount() : 0 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function handleRemindersAck(deps, req, res) {
|
|
117
|
+
const body = await readBody(req);
|
|
118
|
+
const ids = Array.isArray(body.ids) ? body.ids.filter((id) => typeof id === 'string') : [];
|
|
119
|
+
const acked = deps.reminderStore !== undefined ? deps.reminderStore.ack(ids) : [];
|
|
120
|
+
return json(res, 200, { ok: true, acked });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function handleReminderAction(deps, req, res) {
|
|
124
|
+
const body = await readBody(req);
|
|
125
|
+
const id = typeof body.id === 'string' ? body.id : '';
|
|
126
|
+
const action = typeof body.action === 'string' ? body.action : '';
|
|
127
|
+
const done = deps.reminderStore !== undefined ? deps.reminderStore.actOn(id, action) : false;
|
|
128
|
+
if (!done) return json(res, 400, { ok: false, error: 'subagent-profiles: 提醒 id 或动作无效' });
|
|
129
|
+
return json(res, 200, { ok: true, id, action });
|
|
130
|
+
}
|
|
131
|
+
|
|
133
132
|
async function handleLedgerFailures(deps, url, res) {
|
|
134
133
|
const session = url.searchParams.get('session') ?? '';
|
|
135
134
|
return json(res, 200, { ok: true, failures: deps.ledger.get(session) });
|
|
136
135
|
}
|
|
137
136
|
|
|
137
|
+
async function handleLedgerJobs(deps, url, res) {
|
|
138
|
+
const session = url.searchParams.get('session') ?? '';
|
|
139
|
+
const jobs = deps.backgroundLedger !== undefined ? deps.backgroundLedger.get(session) : [];
|
|
140
|
+
return json(res, 200, { ok: true, jobs });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function handleDraftsList(deps, res) {
|
|
144
|
+
return json(res, 200, { ok: true, drafts: deps.draftsStore.list() });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function handleDraftAdd(deps, req, res) {
|
|
148
|
+
const body = await readBody(req);
|
|
149
|
+
const config = body && typeof body.config === 'object' && body.config !== null ? body.config : {};
|
|
150
|
+
if (typeof config.id !== 'string' || config.id === '') return json(res, 400, { ok: false, error: 'draft config.id 必须为非空字符串' });
|
|
151
|
+
const draft = {
|
|
152
|
+
id: 'draft-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
|
|
153
|
+
source: typeof body.source === 'string' && body.source !== '' ? body.source : 'human',
|
|
154
|
+
config,
|
|
155
|
+
name: typeof body.name === 'string' && body.name !== '' ? body.name : config.id,
|
|
156
|
+
description: typeof body.description === 'string' ? body.description : '',
|
|
157
|
+
createdAt: Date.now(),
|
|
158
|
+
};
|
|
159
|
+
const persist = deps.draftsStore.add(draft);
|
|
160
|
+
return persistOk(res, { draft }, persist);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function handleDraftApply(deps, req, res) {
|
|
164
|
+
const body = await readBody(req);
|
|
165
|
+
const id = body && typeof body.id === 'string' ? body.id : '';
|
|
166
|
+
if (id === '') return json(res, 400, { ok: false, error: 'draft id 不能为空' });
|
|
167
|
+
const draft = deps.draftsStore.get(id);
|
|
168
|
+
if (draft === undefined) return json(res, 404, { ok: false, error: 'draft 不存在(可能已被应用或删除)' });
|
|
169
|
+
const next = { ...draft };
|
|
170
|
+
if (typeof body.name === 'string' && body.name !== '') next.name = body.name;
|
|
171
|
+
if (typeof body.description === 'string') next.description = body.description;
|
|
172
|
+
const applied = await deps.applyDraft(next);
|
|
173
|
+
deps.draftsStore.remove(id);
|
|
174
|
+
return persistOk(res, applied, { persisted: applied.persisted !== false });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function handleDraftRemove(deps, req, res) {
|
|
178
|
+
const body = await readBody(req);
|
|
179
|
+
const id = body && typeof body.id === 'string' ? body.id : '';
|
|
180
|
+
if (id === '') return json(res, 400, { ok: false, error: 'draft id 不能为空' });
|
|
181
|
+
const result = deps.draftsStore.remove(id);
|
|
182
|
+
return persistOk(res, { id }, result);
|
|
183
|
+
}
|
|
184
|
+
|
|
138
185
|
async function handleSetEnabled(deps, req, res) {
|
|
139
186
|
const body = await readBody(req);
|
|
140
187
|
const next = !!(body && body.enabled === true);
|
|
@@ -144,8 +191,7 @@ async function handleSetEnabled(deps, req, res) {
|
|
|
144
191
|
return persistOk(res, { enabled: deps.getEnabled() }, persist);
|
|
145
192
|
}
|
|
146
193
|
|
|
147
|
-
//
|
|
148
|
-
// 不改派发行为(建议只读提示);写路由 CSRF 三件套自动生效。
|
|
194
|
+
// 派发优化建议注入开关(默认关,只提示不改派发行为)。
|
|
149
195
|
async function handleSetEvolutionAdvice(deps, req, res) {
|
|
150
196
|
const body = await readBody(req);
|
|
151
197
|
const next = !!(body && body.advice === true);
|
|
@@ -154,8 +200,7 @@ async function handleSetEvolutionAdvice(deps, req, res) {
|
|
|
154
200
|
return persistOk(res, { evolutionAdvice: deps.getEvolutionAdvice() }, persist);
|
|
155
201
|
}
|
|
156
202
|
|
|
157
|
-
//
|
|
158
|
-
// 只放行「放行集内且其余三道闸全过」的非 system 预设;非成员即便开关开也恒拒。
|
|
203
|
+
// 逃生舱开关(默认关):只放行「放行集内且三道闸全过」的非 system 预设。
|
|
159
204
|
async function handleSetEscape(deps, req, res) {
|
|
160
205
|
const body = await readBody(req);
|
|
161
206
|
const next = !!(body && body.enabled === true);
|
|
@@ -164,8 +209,7 @@ async function handleSetEscape(deps, req, res) {
|
|
|
164
209
|
return persistOk(res, { escapeEnabled: deps.getEscapeEnabled() }, persist);
|
|
165
210
|
}
|
|
166
211
|
|
|
167
|
-
//
|
|
168
|
-
// 400 拒绝)。纯同步、无副作用。
|
|
212
|
+
// 放行集写路径校验:preset id 非字符串/缺失归一空串(调用方 400 拒绝)。
|
|
169
213
|
function escapePresetId(body) {
|
|
170
214
|
const raw = body && typeof body === 'object' && typeof body.preset === 'string' ? body.preset : '';
|
|
171
215
|
return raw.trim();
|
|
@@ -197,24 +241,20 @@ async function handleAdd(deps, req, res) {
|
|
|
197
241
|
if (typeof profile.id !== 'string' || profile.id.length === 0) {
|
|
198
242
|
return json(res, 400, { ok: false, error: 'subagent-profiles: profile id 必须为非空字符串' });
|
|
199
243
|
}
|
|
200
|
-
//
|
|
201
|
-
// 与 loadProfiles(strict=false 迁移宽松读取)的行为区分。列被拒字段与中文原因。
|
|
244
|
+
// 写路径 strict=true:超限/非法字段 400 拒绝(与 loadProfiles 宽松读取区分)。
|
|
202
245
|
const { clean, warnings } = sanitizeProfile(profile, { strict: true });
|
|
203
246
|
if (warnings.length > 0) {
|
|
204
247
|
const detail = warnings.map((w) => `${w.field}:${w.reason}`).join(';');
|
|
205
248
|
return json(res, 400, { ok: false, error: `写入被拒绝:${detail}` });
|
|
206
249
|
}
|
|
207
250
|
const hadToolFilter = profile.toolFilter !== undefined;
|
|
208
|
-
// tokenTier
|
|
209
|
-
//
|
|
210
|
-
// 时 cheap 会被重置为 balanced)。故单独用 raw-body 守卫:传了才写(strict
|
|
211
|
-
// 模式下非法值已在上面 400 拒绝,clean.tokenTier 必为合法 enum)。
|
|
251
|
+
// tokenTier 单独用 raw-body 守卫(sanitize 恒回填 balanced,进 merge 会破坏
|
|
252
|
+
// 「未传→保留 existing」语义);strict 下非法值已在上面 400 拒绝。
|
|
212
253
|
const hadTokenTier = profile.tokenTier !== undefined;
|
|
213
254
|
const existing = deps.store.profiles.get(clean.id);
|
|
214
255
|
const seed = BUILTIN_SEEDS.find((s) => s.id === clean.id);
|
|
215
256
|
const isBuiltin = (existing !== undefined && existing.builtin === true) || seed !== undefined;
|
|
216
|
-
//
|
|
217
|
-
// (如内置方案的 persona/preset)在编辑或重新添加后仍然保留。
|
|
257
|
+
// 合并而非替换:表单未带字段(内置 persona/preset 等)编辑后仍保留。
|
|
218
258
|
const merged = { ...(existing ?? seed ?? {}) };
|
|
219
259
|
merged.id = clean.id;
|
|
220
260
|
for (const key of ['name', 'description', 'preset', 'provider', 'model', 'reasoningEffort', 'persona', 'enabled']) {
|
|
@@ -222,8 +262,7 @@ async function handleAdd(deps, req, res) {
|
|
|
222
262
|
if (clean[key] === '' || clean[key] === null) { delete merged[key]; continue; } // 空:清除字段
|
|
223
263
|
merged[key] = clean[key];
|
|
224
264
|
}
|
|
225
|
-
// toolFilter
|
|
226
|
-
// toolFilter 时保留 existing 原值(merge 语义);传了但被 sanitize 归一为空则清除。
|
|
265
|
+
// toolFilter:空数组 = 清除;未传保留 existing;传了但归一为空则清除。
|
|
227
266
|
if (hadToolFilter) {
|
|
228
267
|
const tf = clean.toolFilter;
|
|
229
268
|
if (tf !== undefined && ((Array.isArray(tf.allow) && tf.allow.length > 0) || (Array.isArray(tf.deny) && tf.deny.length > 0))) {
|
|
@@ -279,22 +318,18 @@ async function handleSetProfileEnabled(deps, req, res) {
|
|
|
279
318
|
return json(res, 404, { ok: false, error: `subagent-profiles: profile "${id}" 不存在(可在设置页确认可用的 profile id)` });
|
|
280
319
|
}
|
|
281
320
|
existing.enabled = body && body.enabled === false ? false : true;
|
|
282
|
-
//
|
|
283
|
-
// 在重启后保留。
|
|
321
|
+
// 无条件持久化:运行时注册的 profile 启用/禁用也须重启后保留。
|
|
284
322
|
existing.persisted = true;
|
|
285
323
|
return persistOk(res, { id, enabled: existing.enabled }, deps.store.persistProfiles());
|
|
286
324
|
}
|
|
287
325
|
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// 响应与 warn 日志,origin/contentType 只进日志(二者本就是请求头,非秘密)。
|
|
291
|
-
// GET 只读路由不经过此处(只读无状态变更,不设防)。
|
|
326
|
+
// CSRF 传输授权:Origin 白名单 + Content-Type + 自定义头;reason 进 403 与 warn
|
|
327
|
+
// 日志,GET 只读不设防。
|
|
292
328
|
function csrfViolation(req) {
|
|
293
329
|
const headers = req.headers ?? {};
|
|
294
330
|
const origin = typeof headers.origin === 'string' ? headers.origin : '';
|
|
295
331
|
const contentType = typeof headers['content-type'] === 'string' ? headers['content-type'] : '';
|
|
296
|
-
// 1. Origin
|
|
297
|
-
// 无 Origin 的本地脚本/curl 跳过此道,靠第 3 道自定义头兜底。
|
|
332
|
+
// 1. Origin 白名单:只放行本机源;无 Origin 的本地脚本靠第 3 道兜底。
|
|
298
333
|
if (origin !== '') {
|
|
299
334
|
let host;
|
|
300
335
|
try { host = new URL(origin).hostname; } catch { host = null; }
|
|
@@ -313,9 +348,45 @@ function csrfViolation(req) {
|
|
|
313
348
|
return null;
|
|
314
349
|
}
|
|
315
350
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
351
|
+
// 路由分发链(从 createHttpRoutes 抽出守行门):纯 if 链分发,每条路由一行;
|
|
352
|
+
// 未知路由 404 兜底。CSRF 与 loopback 守卫在 createHttpRoutes 入口完成。
|
|
353
|
+
async function routeRequest(deps, req, res, url, sub) {
|
|
354
|
+
if (req.method === 'GET' && (sub === '/' || sub === '/list')) return handleList(deps, res);
|
|
355
|
+
if (req.method === 'GET' && sub === '/options/summary') return handleSummary(deps, res);
|
|
356
|
+
if (req.method === 'GET' && sub === '/options/versions') return handleVersions(res);
|
|
357
|
+
if (req.method === 'GET' && sub === '/options/efforts') return handleEfforts(deps, url, res);
|
|
358
|
+
if (req.method === 'GET' && sub === '/options/tools') return handleTools(deps, res);
|
|
359
|
+
if (req.method === 'GET' && sub === '/ledger/failures') return handleLedgerFailures(deps, url, res);
|
|
360
|
+
if (req.method === 'GET' && sub === '/ledger/jobs') return handleLedgerJobs(deps, url, res);
|
|
361
|
+
if (req.method === 'GET' && sub === '/reminders') return handleRemindersList(deps, res);
|
|
362
|
+
if (req.method === 'POST' && sub === '/reminders/ack') return handleRemindersAck(deps, req, res);
|
|
363
|
+
if (req.method === 'POST' && sub === '/reminders/action') return handleReminderAction(deps, req, res);
|
|
364
|
+
if (req.method === 'GET' && sub === '/drafts') return handleDraftsList(deps, res);
|
|
365
|
+
if (req.method === 'POST' && sub === '/draft') return handleDraftAdd(deps, req, res);
|
|
366
|
+
if (req.method === 'POST' && sub === '/draft/apply') return handleDraftApply(deps, req, res);
|
|
367
|
+
if (req.method === 'POST' && sub === '/draft/remove') return handleDraftRemove(deps, req, res);
|
|
368
|
+
if (req.method === 'POST' && sub === '/draft/preview') { const body = await readBody(req); return json(res, 200, { ok: true, ...(await deps.previewDraft(body)) }); }
|
|
369
|
+
if (req.method === 'GET' && sub === '/evolution/assets') return handleEvolutionAssets(deps, res);
|
|
370
|
+
if (req.method === 'POST' && sub === '/evolution/apply') return handleEvolutionApply(deps, req, res);
|
|
371
|
+
if (req.method === 'POST' && sub === '/evolution/regenerate') return handleEvolutionRegenerate(deps, req, res);
|
|
372
|
+
if (req.method === 'POST' && sub === '/advice/save-profile') return handleAdviceSaveProfile(deps, req, res);
|
|
373
|
+
if (req.method === 'POST' && sub === '/settings/candidate') return handleSettingsCandidate(deps, req, res);
|
|
374
|
+
if (req.method === 'POST' && sub === '/options/refresh') return handleRefresh(deps, res);
|
|
375
|
+
if (req.method === 'POST' && sub === '/set-enabled') return handleSetEnabled(deps, req, res);
|
|
376
|
+
if (req.method === 'POST' && sub === '/set-evolution-advice') return handleSetEvolutionAdvice(deps, req, res);
|
|
377
|
+
if (req.method === 'POST' && sub === '/set-escape') return handleSetEscape(deps, req, res);
|
|
378
|
+
if (req.method === 'POST' && sub === '/escape-add') return handleEscapeAdd(deps, req, res);
|
|
379
|
+
if (req.method === 'POST' && sub === '/escape-remove') return handleEscapeRemove(deps, req, res);
|
|
380
|
+
if (req.method === 'POST' && sub === '/add') return handleAdd(deps, req, res);
|
|
381
|
+
if (req.method === 'POST' && sub === '/remove') return handleRemove(deps, req, res);
|
|
382
|
+
if (req.method === 'POST' && sub === '/reset') return handleReset(deps, req, res);
|
|
383
|
+
if (req.method === 'POST' && sub === '/reset-all') return handleResetAll(deps, res);
|
|
384
|
+
if (req.method === 'POST' && sub === '/set-profile-enabled') return handleSetProfileEnabled(deps, req, res);
|
|
385
|
+
json(res, 404, { ok: false, error: `未知路由 ${sub}` });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
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, adviceSource, evolution, logger }) {
|
|
389
|
+
const deps = { store, getEnabled, setEnabled, syncTool, catalog, ledger, backgroundLedger, draftsStore, applyDraft, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice, summariesFile, dispatchFile, adviceWhitelist, previewDraft, reminderStore, adviceSource, evolution, logger };
|
|
319
390
|
const handler = async (req, res) => {
|
|
320
391
|
const remote = req.socket?.remoteAddress;
|
|
321
392
|
if (!LOOPBACKS.has(remote)) return json(res, 403, { ok: false, error: '仅限本机访问' });
|
|
@@ -333,24 +404,7 @@ export function createHttpRoutes({ webServer, store, getEnabled, setEnabled, syn
|
|
|
333
404
|
return json(res, 403, { ok: false, error: `CSRF 防护拒绝:${violation.reason}` });
|
|
334
405
|
}
|
|
335
406
|
}
|
|
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}` });
|
|
407
|
+
return routeRequest(deps, req, res, url, sub);
|
|
354
408
|
} catch (error) {
|
|
355
409
|
// 通用 500 不回显内部错误信息(防泄漏),详情只进宿主日志。
|
|
356
410
|
deps.logger.error('[dsh-subagent-profile] settings route error:', error instanceof Error ? (error.stack ?? error.message) : String(error));
|