dsh-subagent-profile 0.3.4 → 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 +1 -0
- package/README.zh.md +1 -0
- package/index.mjs +14 -5
- package/lib/client.js +948 -170
- package/lib/core/delegation.mjs +3 -2
- package/lib/core/evolution-advice.mjs +125 -26
- 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 +131 -26
- 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/http-helpers.mjs +35 -0
- package/lib/core/http-routes.mjs +21 -36
- package/lib/core/profile-provider.mjs +2 -1
- package/lib/core/profiles-store.mjs +23 -5
- package/lib/core/session-read.mjs +17 -0
- package/lib/core/shims.mjs +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// lib/core/evolution-renewal.mjs — 候选换代判定与换代审计(从 evolution-engine 拆出
|
|
2
|
+
// 守行门):到期判定、有效期初值、数据指纹、显著变化判定(阈值初值,待实测后校准)、
|
|
3
|
+
// suggestion-expire / suggestion-refresh 审计条目。纯函数、零 IO、零依赖,可裸 import 单测。
|
|
4
|
+
|
|
5
|
+
// 候选到期判定:expiry 为 null(不自动过期)恒不到期,否则 now >= expiry。
|
|
6
|
+
export function candidateExpired(entry, now) {
|
|
7
|
+
return typeof entry.expiry === 'number' && now >= entry.expiry;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// 候选有效期初值:手动维护恒不自动过期;自动换新按设置小时数计算,0 = 不自动过期。
|
|
11
|
+
export function candidateExpiryOf(mode, ttlH, now) {
|
|
12
|
+
if (mode === 'manual') return null;
|
|
13
|
+
return ttlH === 0 ? null : now + ttlH * 3600 * 1000;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 数据指纹:该身份键当前聚合的样本数/加权成功分/置信度(换代判定输入)。
|
|
17
|
+
export function dataFingerprintOf(item, summaries) {
|
|
18
|
+
const l1 = summaries !== null && typeof summaries === 'object' && summaries.l1 !== null && typeof summaries.l1 === 'object'
|
|
19
|
+
? summaries.l1
|
|
20
|
+
: {};
|
|
21
|
+
const group = typeof item.profileKey === 'string' ? l1[item.profileKey] : undefined;
|
|
22
|
+
const entry = group !== null && typeof group === 'object' ? group : {};
|
|
23
|
+
return {
|
|
24
|
+
n: Number.isFinite(entry.confidence?.n) ? entry.confidence.n : (Number.isFinite(entry.deployments_total) ? entry.deployments_total : 0),
|
|
25
|
+
score: Number.isFinite(entry.score?.weighted_success) ? entry.score.weighted_success : 0,
|
|
26
|
+
confidence: typeof entry.confidence?.level === 'string' ? entry.confidence.level : '',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 显著变化判定(初值,待实测后校准):样本数增加 ≥2,或加权成功分变化 ≥0.1,
|
|
31
|
+
// 或置信度升级。
|
|
32
|
+
const FINGERPRINT_CONFIDENCE_RANK = { low: 0, medium: 1, high: 2 };
|
|
33
|
+
export function dataSignificantlyChanged(prev, next) {
|
|
34
|
+
if (next.n - prev.n >= 2) return true;
|
|
35
|
+
if (Math.abs(next.score - prev.score) >= 0.1) return true;
|
|
36
|
+
return (FINGERPRINT_CONFIDENCE_RANK[next.confidence] ?? -1) > (FINGERPRINT_CONFIDENCE_RANK[prev.confidence] ?? -1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// suggestion-expire 事件:候选到期且数据显著变化、换代时转 expired 的留痕(仅记录)。
|
|
40
|
+
export function expireAuditEntry(asset, reason) {
|
|
41
|
+
return {
|
|
42
|
+
event: 'suggestion-expire',
|
|
43
|
+
profile_id: typeof asset.id === 'string' ? asset.id : '',
|
|
44
|
+
from: asset.from,
|
|
45
|
+
to: asset.to,
|
|
46
|
+
axis: asset.axis,
|
|
47
|
+
direction: asset.direction,
|
|
48
|
+
human_confirmed: false,
|
|
49
|
+
reason,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// suggestion-refresh 事件:人工触发「重新生成候选」的留痕(人工触发即确认)。
|
|
54
|
+
export function refreshAuditEntry({ profileKey, from, to, axis, direction }) {
|
|
55
|
+
return {
|
|
56
|
+
event: 'suggestion-refresh',
|
|
57
|
+
profile_id: profileKey,
|
|
58
|
+
from,
|
|
59
|
+
to,
|
|
60
|
+
axis,
|
|
61
|
+
direction,
|
|
62
|
+
human_confirmed: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// lib/core/evolution-routes.mjs — 进化资产与候选管理路由处理:/evolution/assets 只读
|
|
2
|
+
// 列表、/evolution/apply 应用写路径、/evolution/regenerate 重新生成候选、
|
|
3
|
+
// /advice/save-profile 保存为方案、/settings/candidate 候选设置。loopback 与 CSRF
|
|
4
|
+
// 守卫由 createHttpRoutes 入口统一完成,本模块只做 body 解析与响应组装。apply 拒绝
|
|
5
|
+
// (400)回传原因与可选逐项 checks;成功回传新方案 id、资产状态与 persisted 信号。
|
|
6
|
+
// apply 业务本体(三道闸/审计/锁)在 evolution-engine 内,本模块不触碰任何闸判定。
|
|
7
|
+
|
|
8
|
+
import { sanitizeProfile } from './pure.mjs';
|
|
9
|
+
import { computeAdvice } from './evolution-advice.mjs';
|
|
10
|
+
import { lowerHyphen, sourceConfigFromKey } from './evolution-draft.mjs';
|
|
11
|
+
import { json, readBody, persistOk } from './http-helpers.mjs';
|
|
12
|
+
|
|
13
|
+
async function handleEvolutionAssets(deps, res) {
|
|
14
|
+
const assets = deps.evolution !== undefined && typeof deps.evolution.list === 'function' ? deps.evolution.list() : [];
|
|
15
|
+
return json(res, 200, { ok: true, assets });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function handleEvolutionApply(deps, req, res) {
|
|
19
|
+
const body = await readBody(req);
|
|
20
|
+
const assetId = typeof body.assetId === 'string' ? body.assetId : '';
|
|
21
|
+
if (assetId === '') return json(res, 400, { ok: false, error: 'assetId 不能为空' });
|
|
22
|
+
if (deps.evolution === undefined || typeof deps.evolution.apply !== 'function') {
|
|
23
|
+
return json(res, 404, { ok: false, error: '进化 apply 引擎未装配' });
|
|
24
|
+
}
|
|
25
|
+
const result = await deps.evolution.apply({ assetId, humanConfirmed: body.human_confirmed === true });
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
const payload = { ok: false, error: result.reason };
|
|
28
|
+
if (result.checks !== undefined) payload.checks = result.checks;
|
|
29
|
+
return json(res, 400, payload);
|
|
30
|
+
}
|
|
31
|
+
return json(res, 200, {
|
|
32
|
+
ok: true,
|
|
33
|
+
id: result.id,
|
|
34
|
+
state: result.state,
|
|
35
|
+
persisted: result.persisted,
|
|
36
|
+
...(result.persisted ? {} : { persistWarning: '已应用但未持久化' }),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 候选设置写路径:更新模式(自动换新/手动维护)+ 有效期小时数(0 或 1~8760 整数)。
|
|
41
|
+
// 校验失败 400;成功持久化并回传 persisted 信号与归一后的值。
|
|
42
|
+
async function handleSettingsCandidate(deps, req, res) {
|
|
43
|
+
const body = await readBody(req);
|
|
44
|
+
const mode = body && typeof body.candidateMode === 'string' ? body.candidateMode : '';
|
|
45
|
+
const ttlH = body && typeof body.candidateTtlH === 'number' ? body.candidateTtlH : NaN;
|
|
46
|
+
if (mode !== 'auto' && mode !== 'manual') {
|
|
47
|
+
return json(res, 400, { ok: false, error: '候选更新模式无效(auto/manual)' });
|
|
48
|
+
}
|
|
49
|
+
if (!Number.isInteger(ttlH) || !(ttlH === 0 || (ttlH >= 1 && ttlH <= 8760))) {
|
|
50
|
+
return json(res, 400, { ok: false, error: '候选有效期无效(0 或 1~8760 小时整数)' });
|
|
51
|
+
}
|
|
52
|
+
const modePersist = deps.store.persistCandidateMode(mode);
|
|
53
|
+
const ttlPersist = deps.store.persistCandidateTtlH(ttlH);
|
|
54
|
+
const persisted = modePersist.persisted !== false && ttlPersist.persisted !== false;
|
|
55
|
+
return persistOk(res, {
|
|
56
|
+
candidateMode: deps.store.loadCandidateMode(),
|
|
57
|
+
candidateTtlH: deps.store.loadCandidateTtlH(),
|
|
58
|
+
}, { persisted });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 重新生成候选(人工触发):该身份键所有未处置候选先转 expired,再按当前建议
|
|
62
|
+
// 产新一代;审计由引擎写 suggestion-refresh。无当前建议时只清理不产新候选。
|
|
63
|
+
async function handleEvolutionRegenerate(deps, req, res) {
|
|
64
|
+
const body = await readBody(req);
|
|
65
|
+
const profileKey = body && typeof body.profileKey === 'string' ? body.profileKey : '';
|
|
66
|
+
if (profileKey === '') return json(res, 400, { ok: false, error: 'profileKey 不能为空' });
|
|
67
|
+
if (deps.evolution === undefined || typeof deps.evolution.regenerate !== 'function') {
|
|
68
|
+
return json(res, 404, { ok: false, error: '候选重新生成入口未装配' });
|
|
69
|
+
}
|
|
70
|
+
const computed = computeAdvice({ summariesFile: deps.summariesFile, dispatchFile: deps.dispatchFile, whitelist: deps.adviceWhitelist, logger: deps.logger });
|
|
71
|
+
const item = (Array.isArray(computed.advice) ? computed.advice : []).find((a) => a !== null && typeof a === 'object' && a.profileKey === profileKey);
|
|
72
|
+
const result = await deps.evolution.regenerate({ profileKey, advice: item !== undefined ? [item] : [], summaries: computed.summaries });
|
|
73
|
+
return json(res, 200, { ok: true, ...result });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 保存为方案:把建议来源的当前派发配置(身份键可还原的预设/提供方/模型段)存成
|
|
77
|
+
// 新方案,便于复用与对比。自定义人格与工具过滤原文未留存(台账只存存在性),
|
|
78
|
+
// 不在新方案内。身份键无可还原字段时 400。
|
|
79
|
+
async function handleAdviceSaveProfile(deps, req, res) {
|
|
80
|
+
const body = await readBody(req);
|
|
81
|
+
const profileKey = body && typeof body.profileKey === 'string' ? body.profileKey : '';
|
|
82
|
+
if (profileKey === '') return json(res, 400, { ok: false, error: 'profileKey 不能为空' });
|
|
83
|
+
const from = sourceConfigFromKey(profileKey, {});
|
|
84
|
+
const config = {};
|
|
85
|
+
for (const field of ['preset', 'provider', 'model']) {
|
|
86
|
+
if (typeof from[field] === 'string' && from[field] !== '') config[field] = from[field];
|
|
87
|
+
}
|
|
88
|
+
if (Object.keys(config).length === 0) {
|
|
89
|
+
return json(res, 400, { ok: false, error: '该配置无可保存字段(未指定预设/提供方/模型)' });
|
|
90
|
+
}
|
|
91
|
+
const slug = lowerHyphen(profileKey).slice(0, 24) || 'config';
|
|
92
|
+
const base = `advice-${slug}`.slice(0, 32);
|
|
93
|
+
const taken = new Set(deps.store.profiles.keys());
|
|
94
|
+
let id = base;
|
|
95
|
+
for (let attempt = 2; taken.has(id) && attempt <= 6; attempt += 1) {
|
|
96
|
+
id = `${base}-${attempt}`.slice(0, 32);
|
|
97
|
+
}
|
|
98
|
+
if (taken.has(id)) return json(res, 400, { ok: false, error: '方案编号冲突,无法保存' });
|
|
99
|
+
const name = slug.length > 0 ? `建议配置-${slug.slice(0, 16)}` : '建议配置';
|
|
100
|
+
const { clean, warnings } = sanitizeProfile({ ...config, id, name, description: '来自建议面板的配置快照(仅预设/模型/提供方;人格与工具过滤不落盘,按隐私设计)。便于复用与对比。' }, { strict: true });
|
|
101
|
+
if (warnings.length > 0) {
|
|
102
|
+
const detail = warnings.map((w) => `${w.field}:${w.reason}`).join(';');
|
|
103
|
+
return json(res, 400, { ok: false, error: `保存被拒绝:${detail}` });
|
|
104
|
+
}
|
|
105
|
+
deps.store.profiles.set(clean.id, { ...clean, persisted: true });
|
|
106
|
+
return persistOk(res, { id: clean.id, name: clean.name }, deps.store.persistProfiles());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { handleEvolutionAssets, handleEvolutionApply, handleEvolutionRegenerate, handleAdviceSaveProfile, handleSettingsCandidate };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// lib/core/http-helpers.mjs — 设置 HTTP 路由的共享小助手(从 http-routes 逐字移出):
|
|
2
|
+
// JSON 响应、POST body 解析(1MB 上限)、persistOk 包装(persisted:false 时回传
|
|
3
|
+
// 「已保存但未持久化」警示)。零依赖,多个路由模块共用。
|
|
4
|
+
|
|
5
|
+
function json(res, code, data) {
|
|
6
|
+
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7
|
+
res.end(JSON.stringify(data));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readBody(req) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
let data = '';
|
|
13
|
+
let size = 0;
|
|
14
|
+
req.on('data', (chunk) => {
|
|
15
|
+
size += chunk.length;
|
|
16
|
+
if (size > 1 << 20) { reject(new Error('请求体过大')); req.destroy(); return; }
|
|
17
|
+
data += chunk;
|
|
18
|
+
});
|
|
19
|
+
req.on('end', () => {
|
|
20
|
+
try { resolve(data === '' ? {} : JSON.parse(data)); } catch { reject(new Error('请求体不是合法 JSON')); }
|
|
21
|
+
});
|
|
22
|
+
req.on('error', reject);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function persistOk(res, payload, persist) {
|
|
27
|
+
return json(res, 200, {
|
|
28
|
+
ok: true,
|
|
29
|
+
...payload,
|
|
30
|
+
persisted: persist.persisted,
|
|
31
|
+
...(persist.persisted ? {} : { persistWarning: '已保存但未持久化' }),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { json, readBody, persistOk };
|
package/lib/core/http-routes.mjs
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
import { sanitizeProfile } from './pure.mjs';
|
|
6
6
|
import { BUILTIN_SEEDS } from './profiles-store.mjs';
|
|
7
7
|
import { detectVersions } from './shims.mjs';
|
|
8
|
-
import { readSummaries
|
|
8
|
+
import { readSummaries } from './evolution-advice.mjs';
|
|
9
9
|
import { profileStatsFromSummaries } from './profile-directory.mjs';
|
|
10
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';
|
|
11
13
|
|
|
12
14
|
// 只有 loopback 接口可以驱动设置 HTTP 路由。
|
|
13
15
|
const LOOPBACKS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
@@ -19,36 +21,6 @@ const CSRF_HEADER = 'X-DSH-Plugin';
|
|
|
19
21
|
const CSRF_HEADER_KEY = CSRF_HEADER.toLowerCase();
|
|
20
22
|
const CSRF_HEADER_VALUE = 'dsh-subagent-profile';
|
|
21
23
|
|
|
22
|
-
function json(res, code, data) {
|
|
23
|
-
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
24
|
-
res.end(JSON.stringify(data));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function readBody(req) {
|
|
28
|
-
return new Promise((resolve, reject) => {
|
|
29
|
-
let data = '';
|
|
30
|
-
let size = 0;
|
|
31
|
-
req.on('data', (chunk) => {
|
|
32
|
-
size += chunk.length;
|
|
33
|
-
if (size > 1 << 20) { reject(new Error('请求体过大')); req.destroy(); return; }
|
|
34
|
-
data += chunk;
|
|
35
|
-
});
|
|
36
|
-
req.on('end', () => {
|
|
37
|
-
try { resolve(data === '' ? {} : JSON.parse(data)); } catch { reject(new Error('请求体不是合法 JSON')); }
|
|
38
|
-
});
|
|
39
|
-
req.on('error', reject);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function persistOk(res, payload, persist) {
|
|
44
|
-
return json(res, 200, {
|
|
45
|
-
ok: true,
|
|
46
|
-
...payload,
|
|
47
|
-
persisted: persist.persisted,
|
|
48
|
-
...(persist.persisted ? {} : { persistWarning: '已保存但未持久化' }),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
24
|
function listClean(store, stats) {
|
|
53
25
|
const map = stats instanceof Map ? stats : new Map();
|
|
54
26
|
return [...store.profiles.values()].map((profile) => {
|
|
@@ -72,18 +44,24 @@ async function handleList(deps, res) {
|
|
|
72
44
|
let costSummary = { global: null, byModel: [] };
|
|
73
45
|
let advice = [];
|
|
74
46
|
let adviceGlobal = null;
|
|
47
|
+
let evolutionCandidates = [];
|
|
48
|
+
let adviceDetails = {};
|
|
75
49
|
try {
|
|
76
50
|
if (typeof deps.refreshAdvice === 'function') deps.refreshAdvice();
|
|
77
51
|
const summaries = typeof deps.summariesFile === 'string' ? readSummaries(deps.summariesFile, deps.logger) : null;
|
|
78
52
|
stats = profileStatsFromSummaries(deps.store, summaries);
|
|
79
53
|
costSummary = costSummaryFromSummaries(summaries);
|
|
80
|
-
if (deps.adviceWhitelist !== undefined && typeof deps.dispatchFile === 'string') {
|
|
81
|
-
|
|
54
|
+
if (deps.adviceWhitelist !== undefined && typeof deps.dispatchFile === 'string' && typeof deps.adviceSource === 'function') {
|
|
55
|
+
// 建议 + 候选生成统一走装配层 helper(computeAdvice 之后推导候选资产),
|
|
56
|
+
// 避免 /list 与其它消费点重复生成;候选随建议链开关同生共死。
|
|
57
|
+
const result = await deps.adviceSource();
|
|
82
58
|
advice = result.advice ?? [];
|
|
83
59
|
adviceGlobal = result.global ?? null;
|
|
60
|
+
evolutionCandidates = Array.isArray(result.candidates) ? result.candidates : [];
|
|
61
|
+
adviceDetails = result.adviceDetails !== null && typeof result.adviceDetails === 'object' ? result.adviceDetails : {};
|
|
84
62
|
}
|
|
85
63
|
} catch { /* 统计/建议是增量展示;失败不影响 /list 主响应 */ }
|
|
86
|
-
return json(res, 200, { ok: true, profiles: listClean(deps.store, stats), costSummary, advice, adviceGlobal });
|
|
64
|
+
return json(res, 200, { ok: true, profiles: listClean(deps.store, stats), costSummary, advice, adviceGlobal, evolutionCandidates, adviceDetails });
|
|
87
65
|
}
|
|
88
66
|
|
|
89
67
|
// 轻量摘要:enabled/advice 开关/模型目录/预设名册/审计分级。
|
|
@@ -98,6 +76,8 @@ async function handleSummary(deps, res) {
|
|
|
98
76
|
audit: deps.getAudit(),
|
|
99
77
|
escapeEnabled: deps.getEscapeEnabled(),
|
|
100
78
|
escapePresets: deps.escape.list(),
|
|
79
|
+
candidateMode: deps.store.loadCandidateMode(),
|
|
80
|
+
candidateTtlH: deps.store.loadCandidateTtlH(),
|
|
101
81
|
});
|
|
102
82
|
}
|
|
103
83
|
|
|
@@ -386,6 +366,11 @@ async function routeRequest(deps, req, res, url, sub) {
|
|
|
386
366
|
if (req.method === 'POST' && sub === '/draft/apply') return handleDraftApply(deps, req, res);
|
|
387
367
|
if (req.method === 'POST' && sub === '/draft/remove') return handleDraftRemove(deps, req, res);
|
|
388
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);
|
|
389
374
|
if (req.method === 'POST' && sub === '/options/refresh') return handleRefresh(deps, res);
|
|
390
375
|
if (req.method === 'POST' && sub === '/set-enabled') return handleSetEnabled(deps, req, res);
|
|
391
376
|
if (req.method === 'POST' && sub === '/set-evolution-advice') return handleSetEvolutionAdvice(deps, req, res);
|
|
@@ -400,8 +385,8 @@ async function routeRequest(deps, req, res, url, sub) {
|
|
|
400
385
|
json(res, 404, { ok: false, error: `未知路由 ${sub}` });
|
|
401
386
|
}
|
|
402
387
|
|
|
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 };
|
|
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 };
|
|
405
390
|
const handler = async (req, res) => {
|
|
406
391
|
const remote = req.socket?.remoteAddress;
|
|
407
392
|
if (!LOOPBACKS.has(remote)) return json(res, 403, { ok: false, error: '仅限本机访问' });
|
|
@@ -30,6 +30,7 @@ import { computeEffectiveAllow } from './intersection.mjs';
|
|
|
30
30
|
import { resolveWhitelist } from './whitelist.mjs';
|
|
31
31
|
import { assertCostGuard } from './cost-guard.mjs';
|
|
32
32
|
import { DELEGATION_CONTEXT, buildDispatchMeta } from './delegation.mjs';
|
|
33
|
+
import { readSessionEvents } from './session-read.mjs';
|
|
33
34
|
|
|
34
35
|
// --- start 预检段(从 start 拆出)-------------------------------------------------
|
|
35
36
|
|
|
@@ -195,7 +196,7 @@ function restrictChildTools(childCtx, parent, profile) {
|
|
|
195
196
|
// 已取消时 result 闭包跳过 followup。
|
|
196
197
|
function wireChildLifecycle(handle, request, childId, swapPreset, profile, logger) {
|
|
197
198
|
const child = handle.agent;
|
|
198
|
-
const boundary = child.session.
|
|
199
|
+
const boundary = readSessionEvents(child.session).length;
|
|
199
200
|
const flags = { cancelled: false };
|
|
200
201
|
const onAbort = () => {
|
|
201
202
|
flags.cancelled = true;
|
|
@@ -167,11 +167,19 @@ function persistProfiles(state, logger) {
|
|
|
167
167
|
|
|
168
168
|
// 插件开关态(state.json):enabled(默认开,禁用即关派发工具)+ evolutionAdvice
|
|
169
169
|
// (派发优化建议注入,默认关,需显式开)+ escapeEnabled(逃生舱放行,默认关——信任
|
|
170
|
-
// 底板,仅设置页显式 opt-in
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
// 底板,仅设置页显式 opt-in 可放开)+ applyEnabled(进化候选生成与应用写路径,默认
|
|
171
|
+
// 关——应用能力只由人显式开启)+ candidateMode/candidateTtlH(候选方案更新策略与
|
|
172
|
+
// 有效期,用户可配置)。开关共用同一 state.json,读写整份对象以互不覆盖;
|
|
173
|
+
// 缺失文件回退默认值(全新部署),损坏文件 fail-closed(全关,绝不静默回退
|
|
174
|
+
// enabled:true —— 安全禁用会被撤销),内存态仍驱动本进程。
|
|
175
|
+
const STATE_DEFAULTS = { enabled: true, evolutionAdvice: false, escapeEnabled: false, applyEnabled: false, candidateMode: 'auto', candidateTtlH: 24 };
|
|
176
|
+
const STATE_FAIL_CLOSED = { enabled: false, evolutionAdvice: false, escapeEnabled: false, applyEnabled: false, candidateMode: 'auto', candidateTtlH: 24 };
|
|
177
|
+
|
|
178
|
+
// 候选有效期小时数归一:0(不自动过期)或 1~8760(一年内)的整数原样保留,
|
|
179
|
+
// 其余值回退默认 24 小时(读取与写入共用同一口径,防坏值落盘)。
|
|
180
|
+
function validCandidateTtlH(value) {
|
|
181
|
+
return typeof value === 'number' && Number.isInteger(value) && (value === 0 || (value >= 1 && value <= 8760)) ? value : 24;
|
|
182
|
+
}
|
|
175
183
|
|
|
176
184
|
function loadState(state) {
|
|
177
185
|
if (!existsSync(state.stateFile)) return { ...STATE_DEFAULTS };
|
|
@@ -182,6 +190,9 @@ function loadState(state) {
|
|
|
182
190
|
enabled: parsed.enabled !== false,
|
|
183
191
|
evolutionAdvice: parsed.evolutionAdvice === true,
|
|
184
192
|
escapeEnabled: parsed.escapeEnabled === true,
|
|
193
|
+
applyEnabled: parsed.applyEnabled === true,
|
|
194
|
+
candidateMode: parsed.candidateMode === 'manual' ? 'manual' : 'auto',
|
|
195
|
+
candidateTtlH: validCandidateTtlH(parsed.candidateTtlH),
|
|
185
196
|
};
|
|
186
197
|
} catch (error) {
|
|
187
198
|
// fail-closed(S2):损坏的 state.json 若回退 enabled:true,会把「安全禁用」静默撤销。
|
|
@@ -246,6 +257,13 @@ export function createProfileStore({ dshHome, logger, onGovernanceFailure = () =
|
|
|
246
257
|
persistEvolutionAdvice: (value) => persistState(state, { evolutionAdvice: value === true }, onGovernanceFailure),
|
|
247
258
|
loadEscapeEnabled: () => loadState(state).escapeEnabled,
|
|
248
259
|
persistEscapeEnabled: (value) => persistState(state, { escapeEnabled: value === true }, onGovernanceFailure),
|
|
260
|
+
loadApplyEnabled: () => loadState(state).applyEnabled,
|
|
261
|
+
persistApplyEnabled: (value) => persistState(state, { applyEnabled: value === true }, onGovernanceFailure),
|
|
262
|
+
// 候选更新策略与有效期(state.json 同源读写,坏值归一默认)。
|
|
263
|
+
loadCandidateMode: () => loadState(state).candidateMode,
|
|
264
|
+
persistCandidateMode: (value) => persistState(state, { candidateMode: value === 'manual' ? 'manual' : 'auto' }, onGovernanceFailure),
|
|
265
|
+
loadCandidateTtlH: () => loadState(state).candidateTtlH,
|
|
266
|
+
persistCandidateTtlH: (value) => persistState(state, { candidateTtlH: validCandidateTtlH(value) }, onGovernanceFailure),
|
|
249
267
|
// getAllowFailOpen:cost guard 必须在派发时读取迁移标志;
|
|
250
268
|
// loadProfiles/persistProfiles 持有该写入。
|
|
251
269
|
getAllowFailOpen: () => state.allowFailOpen,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// lib/core/session-read.mjs — 会话事件统一读取口(双轨兼容)。
|
|
2
|
+
//
|
|
3
|
+
// 读取会话事件的两个不兼容环境:
|
|
4
|
+
// * 稳定线:session.events 是数组(本插件当前发布环境的形状);
|
|
5
|
+
// * 新线:session.events 属性被移除,改为按需读取 session.snapshotEvents()。
|
|
6
|
+
// 直接改成新 API 会让稳定线崩溃;直接保留旧读法会在新线崩溃。因此本 helper
|
|
7
|
+
// 逐次 feature-detect:events 数组优先,snapshotEvents() 其次,两轨都不可用
|
|
8
|
+
// 时返回空数组(fall-soft,绝不抛出)。
|
|
9
|
+
//
|
|
10
|
+
// 返回语义:稳定线分支返回 session.events 原数组引用(不拷贝),保持既有
|
|
11
|
+
// identity 语义与零开销;新线分支返回 snapshotEvents() 的返回值(默认全量快照)。
|
|
12
|
+
export function readSessionEvents(session) {
|
|
13
|
+
if (session === null || session === undefined) return [];
|
|
14
|
+
if (Array.isArray(session.events)) return session.events;
|
|
15
|
+
if (typeof session.snapshotEvents === 'function') return session.snapshotEvents();
|
|
16
|
+
return [];
|
|
17
|
+
}
|
package/lib/core/shims.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { createRequire } from 'node:module';
|
|
|
23
23
|
// 导出缺失会在 apply 能运行之前就以明确错误中止模块加载——这正是本类存在的隔离。
|
|
24
24
|
import { assertSubagentMaxDepth, resolveChildDepth } from '@deepseek-ai/dsh-subagent';
|
|
25
25
|
import { toStopReason } from './pure.mjs';
|
|
26
|
+
import { readSessionEvents } from './session-read.mjs';
|
|
26
27
|
|
|
27
28
|
// warn:模块顶层没有 ctx / logger,降级用 console.warn。
|
|
28
29
|
// 前缀让来源在共享宿主日志中可辨认。
|
|
@@ -162,7 +163,7 @@ const [foldConsumedWork, finalAssistantOutput, createUserMessage, appendDelegate
|
|
|
162
163
|
// finalAssistantOutput(最后一个非空 assistant/message,否则拼接的 text-delta
|
|
163
164
|
// 分片,再否则 undefined -> [])。
|
|
164
165
|
function readResult(child, boundary, cancelled) {
|
|
165
|
-
const own = child.session.
|
|
166
|
+
const own = readSessionEvents(child.session).slice(boundary);
|
|
166
167
|
const end = foldConsumedWork(own).end;
|
|
167
168
|
const recorded = toStopReason(end?.data.reason);
|
|
168
169
|
const stopReason = cancelled && recorded !== 'completed' ? 'aborted' : recorded;
|