dsh-rule-engine 0.6.3 → 0.6.5
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 +458 -425
- package/lib/core/authorization.js +12 -4
- package/lib/core/guard-core.js +55 -18
- package/lib/core/intent.js +1 -1
- package/lib/core/llm-intent.js +13 -5
- package/lib/core/matcher.js +19 -16
- package/lib/core/measure-kinds.js +127 -0
- package/lib/core/patterns.js +51 -8
- package/lib/core/state.js +12 -2
- package/lib/core/text-detect.js +20 -2
- package/lib/core/tool-catalog.js +16 -3
- package/lib/core/understander.js +23 -34
- package/lib/index.js +127 -19
- package/lib/messages.js +5 -0
- package/lib/service.js +43 -0
- package/package.json +4 -10
- package/scripts/audit-mount-consistency.mjs +0 -198
- package/scripts/build.sh +0 -13
- package/scripts/check-real.mjs +0 -24
- package/scripts/check-tool-coverage.mjs +0 -65
- package/scripts/dualtrack-check.mjs +0 -335
- package/scripts/dualtrack-whitelist.json +0 -7
- package/scripts/health-audit.mjs +0 -66
- package/scripts/lib/pnpm-exempt.mjs +0 -56
- package/scripts/local-residue-scan.mjs +0 -46
- package/scripts/plugins.json +0 -10
- package/scripts/probe-home.mjs +0 -3
- package/scripts/probe-intent.mjs +0 -14
- package/scripts/publish-aptitude-check.mjs +0 -102
- package/scripts/readme-version-check.mjs +0 -41
- package/scripts/release-plugin.mjs +0 -401
- package/scripts/rules-health.mjs +0 -55
- package/scripts/verify-all.mjs +0 -219
- package/scripts/verify-guard-live.mjs +0 -30
- package/scripts/verify-v474.mjs +0 -14
- package/upgrade-impact.json +0 -55
|
@@ -374,13 +374,17 @@ function pathUnderAuth(p, authPath) {
|
|
|
374
374
|
export function authMatches(auth, op) {
|
|
375
375
|
if (!auth || !op) return false;
|
|
376
376
|
if (auth.type !== "any" && op.type !== "any" && auth.type !== op.type) return false;
|
|
377
|
-
|
|
377
|
+
// A2(2026-09-10):授权侧支持多路径——ask 多路径问题文本经 index.js 的 ask 授权登记写入 pathPrefixes。
|
|
378
|
+
// 语义:任一授权路径覆盖任一操作候选路径即匹配(此前只认单一 auth.pathPrefix,ask 侧只记一条 → 其余路径的授权形同虚设)。
|
|
379
|
+
const authPaths = Array.isArray(auth.pathPrefixes) && auth.pathPrefixes.length > 0
|
|
380
|
+
? auth.pathPrefixes.map(normalizePath).filter(Boolean)
|
|
381
|
+
: (auth.pathPrefix ? [normalizePath(auth.pathPrefix)] : []);
|
|
382
|
+
if (authPaths.length > 0) {
|
|
378
383
|
const candidates = Array.isArray(op.pathPrefixes) && op.pathPrefixes.length > 0
|
|
379
384
|
? op.pathPrefixes
|
|
380
385
|
: (op.pathPrefix ? [op.pathPrefix] : []);
|
|
381
386
|
if (candidates.length === 0) return false;
|
|
382
|
-
|
|
383
|
-
if (!candidates.some((p) => pathUnderAuth(normalizePath(p), authPath))) return false;
|
|
387
|
+
if (!candidates.some((p) => authPaths.some((a) => pathUnderAuth(normalizePath(p), a)))) return false;
|
|
384
388
|
}
|
|
385
389
|
// 柱子 B(2026-08-31):对象锚定——授权声明了显式命名对象 → 操作命令文本必须命中该对象
|
|
386
390
|
//("推送 X 仓库"不覆盖"推送 Y 仓库";未锚定对象=不收紧)
|
|
@@ -402,7 +406,11 @@ export function findMatchingAuth(auths, op, now = Date.now()) {
|
|
|
402
406
|
export function describeAuth(auth) {
|
|
403
407
|
if (!auth) return "无";
|
|
404
408
|
const type = auth.type || "any";
|
|
405
|
-
|
|
409
|
+
// A2(2026-09-10):多路径授权逐条显示(ask 多路径问题文本);单路径/全局维持原措辞。
|
|
410
|
+
const paths = Array.isArray(auth.pathPrefixes) && auth.pathPrefixes.length > 0
|
|
411
|
+
? auth.pathPrefixes
|
|
412
|
+
: (auth.pathPrefix ? [auth.pathPrefix] : []);
|
|
413
|
+
const path = paths.length === 0 ? "全局" : paths.length === 1 ? `路径 ${paths[0]}` : `路径 ${paths.join(" / ")}`;
|
|
406
414
|
const expires = auth.expiresAt ? `|到期 ${new Date(auth.expiresAt).toISOString()}` : "";
|
|
407
415
|
return `${type}|${path}|${new Date(auth.at || 0).toISOString()}${expires}|仅本次运行有效`;
|
|
408
416
|
}
|
package/lib/core/guard-core.js
CHANGED
|
@@ -37,6 +37,7 @@ import { computeMountSignature, profileNameFromArgs } from "./mount-signature.js
|
|
|
37
37
|
import { findBackupForPath, getSessionState, maybeReloadIfChanged } from "./state.js";
|
|
38
38
|
import { isVersionedFile, validateEditedFile } from "./version-guard.js";
|
|
39
39
|
import { decideContractAction, defaultContract, isArmed } from "./contract.js";
|
|
40
|
+
import { kindOf } from "./measure-kinds.js";
|
|
40
41
|
import { classifyAction } from "./overengineering.js";
|
|
41
42
|
import { labelFingerprint, labelsAllowFingerprint } from "./label-fingerprint.js";
|
|
42
43
|
|
|
@@ -286,23 +287,26 @@ function resolveLocalDependencyPkgPath(bundleName, profilePkgPath, parsed) {
|
|
|
286
287
|
return existsSync(pkgPath) ? pkgPath : null;
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
/**
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
290
|
+
/** 官方 bundle 白名单(B1 修复,2026-09-10):规则 24 自己的豁免条款原文即「官方 bundle 由官方机制管理,
|
|
291
|
+
* 模型不主动修改」——引擎此前未在代码里兑现。这两个包**永远**位于 DSH 本体或 profiles 共享层,
|
|
292
|
+
* 独立 DSH_HOME 的新 profile 两者都可能尚未建立 → 校验必然失败并**卡死 profile 声明编辑**(试点主要阻塞点)。 */
|
|
293
|
+
const OFFICIAL_BUNDLES = new Set(["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"]);
|
|
294
|
+
|
|
295
|
+
/** 解析给定 profile package.json **内容**中非 bundle / 无法确认的项(内容级纯函数,供 B1 与 C1 复用) */
|
|
296
|
+
export function nonBundleInContent(content, profilePkgPath) {
|
|
295
297
|
let parsed;
|
|
296
298
|
try { parsed = JSON.parse(content); } catch { return null; }
|
|
297
299
|
const bundles = parsed?.dsh?.profile?.bundles;
|
|
298
300
|
if (!Array.isArray(bundles)) return null;
|
|
299
301
|
const bad = [];
|
|
300
302
|
for (const b of bundles) {
|
|
301
|
-
|
|
303
|
+
if (OFFICIAL_BUNDLES.has(b)) continue; // B1:官方 bundle 直接放行(规则 24 豁免条款)
|
|
304
|
+
const pkgPath = resolveBundlePkgPath(b, profilePkgPath) || resolveLocalDependencyPkgPath(b, profilePkgPath, parsed);
|
|
302
305
|
if (!pkgPath) {
|
|
303
306
|
const dep = parsed?.dependencies?.[b] ?? parsed?.devDependencies?.[b] ?? parsed?.optionalDependencies?.[b];
|
|
304
307
|
const depDesc = typeof dep === "string" ? `dependencies 为 ${dep}` : "dependencies 中无此包";
|
|
305
|
-
|
|
308
|
+
// B1 建议 3:给出可执行的排查方向(独立 DSH_HOME 场景最常见的成因)
|
|
309
|
+
bad.push(`${b}(找不到 package.json,无法确认类型;${depDesc}。请先用 dev_install_package 或先安装依赖再写 bundles;若为独立 DSH_HOME,请确认 profiles/node_modules 共享层存在——该层由 DSH 真实启动时的 healProfilesModuleFallback 自动建立)`);
|
|
306
310
|
continue;
|
|
307
311
|
}
|
|
308
312
|
try {
|
|
@@ -315,6 +319,31 @@ export function nonBundleInProfileBundles(name, args) {
|
|
|
315
319
|
return bad.length ? bad : null;
|
|
316
320
|
}
|
|
317
321
|
|
|
322
|
+
/** 若本次文件变更会写入 profile package.json 的 dsh.profile.bundles,返回其中非 bundle/无法确认的项 */
|
|
323
|
+
export function nonBundleInProfileBundles(name, args) {
|
|
324
|
+
const p = pathTarget(args);
|
|
325
|
+
if (!isProfilePackageJson(p)) return null;
|
|
326
|
+
const content = resultingFileContent(name, args);
|
|
327
|
+
if (!content) return null;
|
|
328
|
+
return nonBundleInContent(content, p);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** C1 修复(2026-09-10):判定本次变更是否为「使装配声明趋于自洽」的修复动作。
|
|
332
|
+
* 判据:变更**前** profile package.json 的 bundles 不自洽(存在解析不到的项),变更**后**自洽。
|
|
333
|
+
* 用途:mount-audit 守卫的**收敛豁免**——解除「不一致态只能靠被拦动作修复」的死循环。
|
|
334
|
+
* 保守边界:读不到原文件、或变更后仍不自洽 → 不豁免。 */
|
|
335
|
+
export function isConvergingBundleFix(name, args) {
|
|
336
|
+
const p = pathTarget(args);
|
|
337
|
+
if (!isProfilePackageJson(p)) return false;
|
|
338
|
+
const after = resultingFileContent(name, args);
|
|
339
|
+
if (!after) return false;
|
|
340
|
+
let before = null;
|
|
341
|
+
try { before = readFileSync(p, "utf8"); } catch { return false; }
|
|
342
|
+
const beforeBad = nonBundleInContent(before, p);
|
|
343
|
+
const afterBad = nonBundleInContent(after, p);
|
|
344
|
+
return Array.isArray(beforeBad) && beforeBad.length > 0 && (!afterBad || afterBad.length === 0);
|
|
345
|
+
}
|
|
346
|
+
|
|
318
347
|
/** 任务契约守卫:仅在总开关开启且会话 armed 时硬拦;ask 场景交给 tools/pre-execute */
|
|
319
348
|
function taskContractGuardDecision(state, exec, session) {
|
|
320
349
|
if (!state.taskContract?.taskContractEnabled) return null;
|
|
@@ -499,7 +528,7 @@ function matchRule(cfg, ctx) {
|
|
|
499
528
|
|
|
500
529
|
// 规则 22 机器化:无执行分点/存在歧义 → 本回合禁止变更类工具调用
|
|
501
530
|
//("你的执行方案难道没问题吗?"虽含"执行"仍是疑问句——同形词不构成指令,不豁免)
|
|
502
|
-
if (cfg.handler === "
|
|
531
|
+
if (kindOf(cfg.handler) === "intent-direct") {
|
|
503
532
|
// B2(2026-08-28 阶段二):委派/子代理回合不做"无执行分点"判定(规则 22④ 的识别层落地)——
|
|
504
533
|
// 子代理任务书以 source.kind="user" 注入,会被当用户消息判"无执行分点"而误拦(ERR-L8QAXS 实弹)。
|
|
505
534
|
if (isDelegatedSession(exec)) {
|
|
@@ -588,7 +617,7 @@ function matchRule(cfg, ctx) {
|
|
|
588
617
|
}
|
|
589
618
|
|
|
590
619
|
// 规则 1:同工具同参数连续失败 ≥2 次后拦第 3 次(失败计数由 tool/result 更新)
|
|
591
|
-
if (cfg.handler === "
|
|
620
|
+
if (kindOf(cfg.handler) === "retry") {
|
|
592
621
|
const key = `${name}:${JSON.stringify(args || {})}`;
|
|
593
622
|
const count = state.retryCounts.get(key) || 0;
|
|
594
623
|
const userText = session.turn.userText || session.lastUserText || "";
|
|
@@ -602,7 +631,7 @@ function matchRule(cfg, ctx) {
|
|
|
602
631
|
}
|
|
603
632
|
|
|
604
633
|
// 规则 9:内联命令 / BOM 写配置(PS7 语义:仅拦显式 utf8BOM)
|
|
605
|
-
if (cfg.handler === "
|
|
634
|
+
if (kindOf(cfg.handler) === "inline-command") {
|
|
606
635
|
if ((name === "pwsh" || name === "bash") && cmd) {
|
|
607
636
|
if (INLINE_CMD.test(cmd)) {
|
|
608
637
|
return makeHit(cfg, "【硬拦截】禁止内联命令(node -e / pwsh -c / node -p 等),请先写脚本文件再执行");
|
|
@@ -615,7 +644,7 @@ function matchRule(cfg, ctx) {
|
|
|
615
644
|
}
|
|
616
645
|
|
|
617
646
|
// 规则 18:DSH 任务首次工具调用前必须已读手册
|
|
618
|
-
if (cfg.handler === "
|
|
647
|
+
if (kindOf(cfg.handler) === "manual-first") {
|
|
619
648
|
const userText = session.turn.userText || session.lastUserText || "";
|
|
620
649
|
const firstTool = session.turn.toolCount === 0;
|
|
621
650
|
if (firstTool && !session.manualReadSeen && dshKeywordsRe().test(userText) && !isManualReadTool(name, args, state.localIntegrations?.manualExempt?.paths || [])) {
|
|
@@ -625,7 +654,7 @@ function matchRule(cfg, ctx) {
|
|
|
625
654
|
}
|
|
626
655
|
|
|
627
656
|
// 规则 13A:删除/覆盖/高风险写前需有“目标路径对应备份”证据
|
|
628
|
-
if (cfg.handler === "
|
|
657
|
+
if (kindOf(cfg.handler) === "backup") {
|
|
629
658
|
// 统一入口命令豁免:入口脚本每次写入前自身执行备份(backup() 保留 5 份),
|
|
630
659
|
// 引擎静态扫描看不到脚本内部动作(已知盲区);入口命令也已被 __self-protect 限定为唯一写通道。
|
|
631
660
|
if ((name === "pwsh" || name === "bash") && isEntryChannelCommand(cmd, state.localIntegrations?.entryScript)) return null;
|
|
@@ -668,7 +697,7 @@ function matchRule(cfg, ctx) {
|
|
|
668
697
|
// hints 含 "skill" 的其它规则 cfg(如 12A:hints=["ask","skill","sensitive",...])截胡——
|
|
669
698
|
// 非 skill 工具时 return null,导致 12A 敏感授权检查静默失效(C-2 实测 Move-Item 放行)。
|
|
670
699
|
// 现在:hints 兜底仅在「name === 'skill'」时进入;handler 明确为 rule12b-skill 时维持原语义。
|
|
671
|
-
if (cfg.handler === "
|
|
700
|
+
if (kindOf(cfg.handler) === "skill-auth") {
|
|
672
701
|
if (name === "skill") {
|
|
673
702
|
const skillName = typeof args?.name === "string" ? args.name : "";
|
|
674
703
|
if ((state.localIntegrations?.manualExempt?.skills || []).includes(skillName)) return null;
|
|
@@ -703,7 +732,7 @@ function matchRule(cfg, ctx) {
|
|
|
703
732
|
}
|
|
704
733
|
|
|
705
734
|
// 规则 12A:敏感操作需要匹配授权证据
|
|
706
|
-
if (cfg.handler === "
|
|
735
|
+
if (kindOf(cfg.handler) === "approval") {
|
|
707
736
|
if (isSensitiveToolCall(name, args, sessionIdOf(exec))) {
|
|
708
737
|
// 0.5.11(用户定稿):12A 与 22-7 判据同源——分析通道/低风险新建豁免在两条分支
|
|
709
738
|
// 使用同一组判据(isAnalysisOp/isLowRiskWorkspaceNew),不再各写一套(此前 22-7 放行、
|
|
@@ -742,7 +771,7 @@ function matchRule(cfg, ctx) {
|
|
|
742
771
|
}
|
|
743
772
|
|
|
744
773
|
// 规则 21:规则/配置文件变更需 unlock(元规则)
|
|
745
|
-
if (cfg.handler === "
|
|
774
|
+
if (kindOf(cfg.handler) === "meta") {
|
|
746
775
|
if ((name === "edit" || name === "write" || (name === "str_replace_editor" && args?.command !== "view")) && isProtectedConfigPath(p) && !unlock) {
|
|
747
776
|
return makeHit(cfg, "【硬拦截】规则/配置文件受保护:修改需用户先执行 /guard unlock");
|
|
748
777
|
}
|
|
@@ -750,7 +779,7 @@ function matchRule(cfg, ctx) {
|
|
|
750
779
|
}
|
|
751
780
|
|
|
752
781
|
// 规则 24:插件装配类型确认(A 硬拦)+ 变更类工具统一覆盖(原规则 25 语义,检查④)
|
|
753
|
-
if (cfg.handler === "
|
|
782
|
+
if (kindOf(cfg.handler) === "assembly") {
|
|
754
783
|
if (name === "dev_install_package") {
|
|
755
784
|
const dir = args?.dir;
|
|
756
785
|
if (dir) {
|
|
@@ -782,7 +811,7 @@ function matchRule(cfg, ctx) {
|
|
|
782
811
|
}
|
|
783
812
|
|
|
784
813
|
// 规则 27:装配变更后必须先通过全量审计,才能继续装配(C 时序;全局变更 + 本会话审计证据)
|
|
785
|
-
if (cfg.handler === "
|
|
814
|
+
if (kindOf(cfg.handler) === "mount-audit") {
|
|
786
815
|
if (isAssemblyMutationTool(name, args)) {
|
|
787
816
|
const currentSig = computeMountSignature(profileNameFromArgs(args));
|
|
788
817
|
state.mountSignature = currentSig;
|
|
@@ -791,6 +820,14 @@ function matchRule(cfg, ctx) {
|
|
|
791
820
|
? currentSig !== auditedSig
|
|
792
821
|
: (state.mountRevision > (session.mountAuditRevision || 0));
|
|
793
822
|
if (needsAudit) {
|
|
823
|
+
// C1 修复(2026-09-10):**收敛豁免**——允许"使装配趋于自洽"的变更,解除不一致态的死循环。
|
|
824
|
+
// 背景(独立 DSH_HOME 试点):改 dependencies → 通过;改 bundles 补齐 → 被拦;
|
|
825
|
+
// 试图回滚 dependencies → **同样被拦** → 审计失败项(bundles-missing)恰恰只能靠被拦的动作修复
|
|
826
|
+
// → 无法收敛(试点靠用户手工写文件才解开)。
|
|
827
|
+
// ① 回滚豁免:变更后签名回到"审计通过时的签名"
|
|
828
|
+
// ② 修复豁免:变更前声明不自洽(bundles 有解析不到的项)而变更后自洽
|
|
829
|
+
if (auditedSig && currentSig === auditedSig) return null; // ① 回到已审计状态
|
|
830
|
+
if (isConvergingBundleFix(name, args)) return null; // ② 修复审计点名的缺失/非法项
|
|
794
831
|
const why = auditedSig
|
|
795
832
|
? `装配内容已变化(装配状态哈希 ${currentSig.slice(0, 8)} ≠ 审计通过时 ${auditedSig.slice(0, 8)})`
|
|
796
833
|
: `插件装配已变更(mountRevision=${state.mountRevision})且本会话未通过全量审计`;
|
package/lib/core/intent.js
CHANGED
|
@@ -87,7 +87,7 @@ export function isEngineInjectedMessage(text) {
|
|
|
87
87
|
// source.kind 主判之外,对"标记为 user 但实为注入"的消息做模板兜底——
|
|
88
88
|
// 命中即整体跳过:不覆盖回合状态、不产生 scopes/授权。
|
|
89
89
|
// 本机实证来源:runtime context 快照(无 system-reminder 标签)、子代理完成通知(B3)、
|
|
90
|
-
// vision-router
|
|
90
|
+
// vision-router 自动挂载提醒(该包 client 侧注入)、引擎自身注入([规则引擎])。
|
|
91
91
|
// example-injector 的近距离引导模板待实测后收窄补充。
|
|
92
92
|
const SYSTEM_INJECTION_PREFIXES = [
|
|
93
93
|
/^Current runtime context\./,
|
package/lib/core/llm-intent.js
CHANGED
|
@@ -50,7 +50,9 @@ export function verdictForDeny(turn, cfg = {}, lexiconDenied = true) {
|
|
|
50
50
|
if (conf >= high) return { mutationDenied: false, source: "llm-high", reason: `LLM 高置信(${conf})` };
|
|
51
51
|
if (conf >= low) return { mutationDenied: false, source: "llm-low", reason: `LLM 低置信(${conf})` };
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
// 2026-09-13:把「兜底失败原因」透出——此前失败降级与「词表缺词」在返回值上完全同形,无法区分。
|
|
54
|
+
const llmError = (llm && llm.raw === turn?.userText && llm.error) ? String(llm.error) : "";
|
|
55
|
+
return { mutationDenied: true, source: "lexicon", reason: "", llmError };
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
@@ -120,18 +122,24 @@ export async function enrichIntentWithLlm(ctx, state, sid, text, cfg) {
|
|
|
120
122
|
state.llmIntentCache.delete(firstKey);
|
|
121
123
|
}
|
|
122
124
|
auditIntent(state, sid, text, verdict, ms, false);
|
|
123
|
-
} catch {
|
|
125
|
+
} catch (e) {
|
|
124
126
|
session.turn.intentState = "lexicon";
|
|
125
|
-
|
|
127
|
+
// 2026-09-13:失败也要留痕——此前 catch 路径不写 turn.llmIntent,
|
|
128
|
+
// 导致 96.7% 的失败率长期不可归因(审计只有「失败降级」,无原因)。
|
|
129
|
+
const errMsg = String((e && e.message) || e);
|
|
130
|
+
session.turn.llmIntent = { raw: text, verdict: null, error: errMsg, at: Date.now() };
|
|
131
|
+
auditIntent(state, sid, text, null, Date.now() - started, false, errMsg);
|
|
126
132
|
}
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
function auditIntent(state, sid, text, verdict, ms, limitHit) {
|
|
135
|
+
function auditIntent(state, sid, text, verdict, ms, limitHit, llmError) {
|
|
130
136
|
try {
|
|
131
137
|
state.llmIntentLast = {
|
|
132
138
|
text: String(text).slice(0, 60),
|
|
133
139
|
verdict: verdict ? { ...verdict, confidence: Number(verdict.confidence) } : null,
|
|
134
140
|
source: verdict ? "llm" : "fail",
|
|
141
|
+
// 2026-09-13:失败原因(供 /guard status 与排障读取;此前只记「fail」)
|
|
142
|
+
error: llmError || null,
|
|
135
143
|
at: Date.now()
|
|
136
144
|
};
|
|
137
145
|
audit({
|
|
@@ -141,7 +149,7 @@ function auditIntent(state, sid, text, verdict, ms, limitHit) {
|
|
|
141
149
|
event: "user/message",
|
|
142
150
|
reason: verdict
|
|
143
151
|
? `词表低置信 → LLM:${JSON.stringify(verdict)}(延迟 ${ms}ms${limitHit ? " 限额" : ""})`
|
|
144
|
-
: `词表低置信 → LLM 失败降级(延迟 ${ms}ms)`,
|
|
152
|
+
: `词表低置信 → LLM 失败降级(延迟 ${ms}ms${llmError ? `:${llmError}` : "(原因未捕获)"})`,
|
|
145
153
|
session: sid,
|
|
146
154
|
verdictSource: verdict ? "llm-intent" : "lexicon",
|
|
147
155
|
text: String(text).slice(0, 120)
|
package/lib/core/matcher.js
CHANGED
|
@@ -10,19 +10,21 @@ import {
|
|
|
10
10
|
activateTimeRe,
|
|
11
11
|
dshKeywordsRe
|
|
12
12
|
} from "./patterns.js";
|
|
13
|
+
import { kindOf } from "./measure-kinds.js";
|
|
13
14
|
|
|
14
15
|
export function activateForUserMessage(configs, userText) {
|
|
15
16
|
const text = userText || "";
|
|
16
17
|
return configs.filter((cfg) => {
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
if (
|
|
20
|
-
if (
|
|
21
|
-
if (
|
|
22
|
-
if (
|
|
23
|
-
if (
|
|
24
|
-
if (
|
|
25
|
-
if (
|
|
18
|
+
const kind = kindOf(cfg.handler);
|
|
19
|
+
if (kind === "manual-first" && dshKeywordsRe().test(text)) return true;
|
|
20
|
+
if (kind === "skill-auth" && activateSkillRe().test(text)) return true;
|
|
21
|
+
if (kind === "approval" && activateApprovalRe().test(text)) return true;
|
|
22
|
+
if (kind === "network" && activateNetworkRe().test(text)) return true;
|
|
23
|
+
if (kind === "backup" && activateBackupRe().test(text)) return true;
|
|
24
|
+
if (kind === "time" && activateTimeRe().test(text)) return true;
|
|
25
|
+
if (kind === "promise" && activatePromiseRe().test(text)) return true;
|
|
26
|
+
if (kind === "source" && activateSourceRe().test(text)) return true;
|
|
27
|
+
if (kind === "language" && CJK_PRESENT.test(text)) return true;
|
|
26
28
|
return false;
|
|
27
29
|
});
|
|
28
30
|
}
|
|
@@ -33,15 +35,16 @@ export function activateForToolCall(configs, toolName, args) {
|
|
|
33
35
|
const name = String(toolName || "");
|
|
34
36
|
const argText = JSON.stringify(args || {});
|
|
35
37
|
return configs.filter((cfg) => {
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
if (
|
|
39
|
-
if (
|
|
40
|
-
if (
|
|
41
|
-
if (
|
|
38
|
+
const kind = kindOf(cfg.handler);
|
|
39
|
+
if (kind === "inline-command" && (name === "pwsh" || name === "bash")) return true;
|
|
40
|
+
if (kind === "retry") return true;
|
|
41
|
+
if (kind === "manual-first") return true;
|
|
42
|
+
if (kind === "backup" && (name === "pwsh" || name === "bash" || name === "edit" || name === "write")) return true;
|
|
43
|
+
if (kind === "skill-auth" && name === "skill") return true;
|
|
44
|
+
if (kind === "approval" || kind === "sensitive") {
|
|
42
45
|
if (name === "pwsh" || name === "bash" || name === "edit" || name === "write" || name === "ask_user_question") return true;
|
|
43
46
|
}
|
|
44
|
-
if (
|
|
47
|
+
if (kind === "meta" && (name === "edit" || name === "write")) return true;
|
|
45
48
|
if (argText) {
|
|
46
49
|
const low = argText.toLowerCase();
|
|
47
50
|
if (cfg.triggerKeywords.some((k) => low.includes(k.toLowerCase()))) return true;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// measure-kinds.js - 措施类型注册表(架构 v3 §三 L1:通用层,零规则号)
|
|
2
|
+
//
|
|
3
|
+
// 依据:reports/2026-09-10-引擎分层架构-通用层零规则内容-v1.md
|
|
4
|
+
// §三 L1「措施类型注册表(measure kinds):每型一个通用解释器,命名去规则号」;
|
|
5
|
+
// §八 P1「措施类型注册表 + 解释器骨架;guard-core/matcher 改 kind → interpreter;内部名去规则号」。
|
|
6
|
+
//
|
|
7
|
+
// 三层分工(P1):
|
|
8
|
+
// ① MEASURE_KINDS —— 通用层的 kind 清单(**零规则号**,随包发布);
|
|
9
|
+
// ② LEGACY_HANDLER_ALIASES —— 历史内部名(带规则号)→ kind 的**兼容映射**
|
|
10
|
+
// (P4 清淤时迁个人层配置;保留在此是为了不破坏既有配置与规则正文声明);
|
|
11
|
+
// ③ kindOf() —— 唯一归一入口,接受 旧名 / kind 名 / 未知名 三种输入。
|
|
12
|
+
//
|
|
13
|
+
// 兼容面(P1 行为等价的前提):本机 rule-engine.json 的 handlerDefaultMap/handlerOverrides、
|
|
14
|
+
// 规则正文 `<!-- handler: … -->`、以及测试夹具,**仍可写旧内部名**——经 kindOf 归一后
|
|
15
|
+
// 与 kind 名完全等价。新用户/新规则写 kind 名即可,无需知道任何规则号。
|
|
16
|
+
export const MEASURE_KINDS = {
|
|
17
|
+
retry: "连续失败重试闸(第 3 次拒绝)",
|
|
18
|
+
time: "时间表述取证",
|
|
19
|
+
source: "引用出处标注",
|
|
20
|
+
promise: "承诺保守",
|
|
21
|
+
"inline-command": "禁止内联命令",
|
|
22
|
+
language: "语言与表达",
|
|
23
|
+
approval: "敏感操作授权确认",
|
|
24
|
+
"skill-auth": "技能调用授权",
|
|
25
|
+
network: "下载与网络",
|
|
26
|
+
backup: "写入前备份",
|
|
27
|
+
"manual-first": "先查手册再动手",
|
|
28
|
+
meta: "元规则管理",
|
|
29
|
+
"intent-direct": "无执行分点直拦",
|
|
30
|
+
"runtime-verify": "交付验证证据",
|
|
31
|
+
assembly: "插件装配类型守卫",
|
|
32
|
+
"release-asset": "发布资产规范",
|
|
33
|
+
"mount-audit": "挂载唯一性审计",
|
|
34
|
+
sensitive: "敏感操作(注入面)"
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** 历史内部名 → kind(兼容层;键含规则号,P4 迁个人层配置) */
|
|
38
|
+
export const LEGACY_HANDLER_ALIASES = {
|
|
39
|
+
"rule1-retry": "retry",
|
|
40
|
+
"rule2-time": "time",
|
|
41
|
+
"rule5-source": "source",
|
|
42
|
+
"rule7-promise": "promise",
|
|
43
|
+
"rule9-inline-bom": "inline-command",
|
|
44
|
+
"rule11-language": "language",
|
|
45
|
+
"rule12a-approval": "approval",
|
|
46
|
+
"rule12b-skill": "skill-auth",
|
|
47
|
+
"rule12c-network": "network",
|
|
48
|
+
"rule12d-sensitive": "sensitive",
|
|
49
|
+
"rule13a-backup": "backup",
|
|
50
|
+
"rule18-manual-first": "manual-first",
|
|
51
|
+
"rule21-meta": "meta",
|
|
52
|
+
"rule22-7-direct": "intent-direct",
|
|
53
|
+
"rule23-runtime-verify": "runtime-verify",
|
|
54
|
+
"rule24-assembly-type": "assembly",
|
|
55
|
+
"rule26-release-asset": "release-asset",
|
|
56
|
+
"rule27-mount-audit": "mount-audit"
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 归一为 kind 名。
|
|
61
|
+
* 旧内部名 → kind;kind 名 → 原样(幂等);未知名 → 原样(由覆盖自省提示未覆盖)。
|
|
62
|
+
* @param {string} name
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function kindOf(name) {
|
|
66
|
+
const s = String(name || "").trim();
|
|
67
|
+
if (!s) return "";
|
|
68
|
+
return LEGACY_HANDLER_ALIASES[s] || s;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 兼容旧调用名(understander 对外导出同义函数) */
|
|
72
|
+
export function normalizeHandlerName(name) {
|
|
73
|
+
return kindOf(name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── P2(2026-09-10):个人层「措施声明」解析 + 校验(架构正本 §三 L2/L3)──
|
|
77
|
+
|
|
78
|
+
/** 规则正文内联声明:`<!-- handler: <kind> -->` 或 `<!-- handler: <kind> {json} -->`
|
|
79
|
+
* 设计(方案 A,用户 2026-09-10 拍板):**扩展既有 handler 声明**而非新增独立块——
|
|
80
|
+
* 向后兼容(旧声明零影响)、单点解析入口。正则捕获 `-->` 之前的**全部内容**,之后再拆分,
|
|
81
|
+
* 避免用嵌套正则直接匹配 JSON(`}` 截断风险)。 */
|
|
82
|
+
const HANDLER_DECL_RE = /<!--\s*handler\s*:\s*([\s\S]*?)-->/i;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 解析个人层措施声明。
|
|
86
|
+
* @param {string} body 规则正文
|
|
87
|
+
* @returns {null | {kind: string, params: object} | {error: string}}
|
|
88
|
+
* null = 无声明;{kind, params} = 解析成功;{error} = 声明非法(调用方须 fail-closed)
|
|
89
|
+
*/
|
|
90
|
+
export function parseHandlerDecl(body) {
|
|
91
|
+
const m = String(body || "").match(HANDLER_DECL_RE);
|
|
92
|
+
if (!m) return null;
|
|
93
|
+
const raw = String(m[1] || "").trim();
|
|
94
|
+
if (!raw) return { error: "声明为空(`<!-- handler: -->`)" };
|
|
95
|
+
const km = /^([A-Za-z0-9][A-Za-z0-9-]*)\s*([\s\S]*)$/.exec(raw);
|
|
96
|
+
if (!km) return { error: `声明格式非法:${raw.slice(0, 60)}` };
|
|
97
|
+
const kind = kindOf(km[1]);
|
|
98
|
+
const rest = String(km[2] || "").trim();
|
|
99
|
+
let params = {};
|
|
100
|
+
if (rest) {
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(rest);
|
|
104
|
+
} catch {
|
|
105
|
+
return { error: `参数不是合法 JSON:${rest.slice(0, 60)}` };
|
|
106
|
+
}
|
|
107
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
108
|
+
return { error: `参数必须是 JSON 对象:${rest.slice(0, 60)}` };
|
|
109
|
+
}
|
|
110
|
+
params = parsed;
|
|
111
|
+
}
|
|
112
|
+
return { kind, params };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 校验措施声明(架构正本 §三 L3:**kind 未注册即拒绝并明确报错,fail-closed**)。
|
|
117
|
+
* @param {null | {kind: string} | {error: string}} decl parseHandlerDecl 的输出
|
|
118
|
+
* @returns {null | string} null = 通过(或无声明);字符串 = 明确错误原因
|
|
119
|
+
*/
|
|
120
|
+
export function validateMeasure(decl) {
|
|
121
|
+
if (!decl) return null;
|
|
122
|
+
if (decl.error) return decl.error;
|
|
123
|
+
if (!Object.prototype.hasOwnProperty.call(MEASURE_KINDS, decl.kind)) {
|
|
124
|
+
return `kind 未注册:${decl.kind}(已注册:${Object.keys(MEASURE_KINDS).join(" / ")})`;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
package/lib/core/patterns.js
CHANGED
|
@@ -303,6 +303,12 @@ const ALLOW_SEG_RE =
|
|
|
303
303
|
// 0.5.12(F4):纯字符串/纯输出段(整段是引号包裹字符串或 $() 子表达式)——不是命令,放行。
|
|
304
304
|
// 保守边界:只匹配"整段即字符串"(^...$ 锚定),不让 node -e "..." / curl -o "..." 借道。
|
|
305
305
|
const PURE_STRING_SEG_RE = /^(?:"[^"\r\n]*"|'[^'\r\n]*'|`[^`\r\n]*`|\$\(\s*[^()\r\n]*\s*\))+$/;
|
|
306
|
+
// F5(2026-09-10 修复,第三方实测):`Remove-Item Env:NAME` 的**操作对象是进程环境变量,不是文件**,
|
|
307
|
+
// 此前被 MUTATING_CMD_RE 的 Remove-Item 命中判写(ERR-JCSE3O 的类判定来源)。
|
|
308
|
+
// 边界(保守):仅当整段目标都是 Env:/$env: 形式时豁免;`Remove-Item <路径>`、混合目标、
|
|
309
|
+
// 选项在目标之前(如 `Remove-Item -Force Env:X`)一律不豁免——宁可误拦不可放行真删除。
|
|
310
|
+
const ENV_SCOPE_REMOVE_SEG_RE =
|
|
311
|
+
/^\s*Remove-Item\s+(?:\$env:|Env:)[\w()]+\s*(?:-[\w]+\s+(?:"[^"]*"|'[^']*'|[^\s;|]+)\s*)*$/i;
|
|
306
312
|
// 剥离 try/catch/finally 控制块:块内语句与主语句同样接受只读判定(Try-* 不是 cmdlet)。
|
|
307
313
|
// 注意:不做全量 [{}] 替换——if/else 子表达式($(if(...){...}else{...}))会被切碎误判。
|
|
308
314
|
function stripControlBlocks(command) {
|
|
@@ -319,12 +325,38 @@ export function isMutationCommand(command) {
|
|
|
319
325
|
return typeof command === "string" && MUTATING_CMD_RE.test(command);
|
|
320
326
|
}
|
|
321
327
|
|
|
322
|
-
/** 只读命令链按分隔符折段(机制 B,2026-08-24):`; 换行 && || |`(管道前后是命令链;`2>&1`/`2>$null` 的 stderr 重定向不拆分)
|
|
328
|
+
/** 只读命令链按分隔符折段(机制 B,2026-08-24):`; 换行 && || |`(管道前后是命令链;`2>&1`/`2>$null` 的 stderr 重定向不拆分)
|
|
329
|
+
* F5(2026-09-10 修复,第三方实测):**引号感知**——此前裸拆 `|` 会把**引号内的 `|`** 当分隔符
|
|
330
|
+
* (正则 alternation `-Pattern 'a|b'`、格式串 `"{0} | x"`、`-notmatch 'x|y'`)→ 切出无法识别的残段
|
|
331
|
+
* → 保守判非只读(本文件的引号感知分支)→ 整条只读命令被规则 22 按 write 拦(ERR-4WOAKG / 9T886X / Z8M7YC / IKXLU8)。
|
|
332
|
+
* 现:`'…'` / `"…"` / `` `…` `` 内的分隔符不拆。转义语义按 PS:`'` 内不转义(`''` 表一个单引号);
|
|
333
|
+
* `"` 与 `` ` `` 内 `\` / `` ` `` 转义下一字符(兼容跨语言写法)。 */
|
|
323
334
|
function splitCommandSegments(command) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
335
|
+
const s = String(command);
|
|
336
|
+
const out = [];
|
|
337
|
+
let buf = "";
|
|
338
|
+
let quote = null;
|
|
339
|
+
for (let i = 0; i < s.length; i++) {
|
|
340
|
+
const c = s[i];
|
|
341
|
+
if (quote) {
|
|
342
|
+
buf += c;
|
|
343
|
+
if (quote !== "'" && (c === "\\" || c === "`")) { buf += s[i + 1] ?? ""; i++; continue; }
|
|
344
|
+
if (c === quote) quote = null;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (c === "'" || c === '"' || c === "`") { quote = c; buf += c; continue; }
|
|
348
|
+
if (c === "\r") continue;
|
|
349
|
+
if (c === "\n" || c === ";") { out.push(buf); buf = ""; continue; }
|
|
350
|
+
if (c === "&" && s[i + 1] === "&") { out.push(buf); buf = ""; i++; continue; }
|
|
351
|
+
if (c === "|") {
|
|
352
|
+
if (s[i + 1] === "|") { out.push(buf); buf = ""; i++; continue; }
|
|
353
|
+
if (i > 0 && /\d/.test(s[i - 1])) { buf += c; continue; } // 保留原 (?<!\d) 语义
|
|
354
|
+
out.push(buf); buf = ""; continue;
|
|
355
|
+
}
|
|
356
|
+
buf += c;
|
|
357
|
+
}
|
|
358
|
+
out.push(buf);
|
|
359
|
+
return out.map((x) => x.trim()).filter(Boolean);
|
|
328
360
|
}
|
|
329
361
|
|
|
330
362
|
/** 判断命令文本是否只读(读文件/查询类,无写入/删除/提交副作用)。
|
|
@@ -345,6 +377,7 @@ export function isReadOnlyCommand(command) {
|
|
|
345
377
|
const s = String(seg).trim();
|
|
346
378
|
if (!s) return true; // 剥离残留的空段放行
|
|
347
379
|
if (s.startsWith("#")) return true;
|
|
380
|
+
if (ENV_SCOPE_REMOVE_SEG_RE.test(s)) return true; // F5:环境变量操作非文件删除(须在 MUTATING 之前)
|
|
348
381
|
if (MUTATING_CMD_RE.test(s)) return false;
|
|
349
382
|
if (MUTATING_METHOD_RE.test(s)) return false;
|
|
350
383
|
if (COM_CONTENT_ASSIGN_RE.test(s)) return false; // $doc.Text=... 是写文档
|
|
@@ -436,9 +469,11 @@ export function isAnalysisOp(toolName, args) {
|
|
|
436
469
|
// ═══════════ 0.5.10 已知环境坑特征表(建议5——错误码自动召回,K-01/K-05 依据)═══════════
|
|
437
470
|
// 工具/命令错误文本命中特征 → 审计 error-hint + 注入指向知识库的提示(低频:仅命中才提示;语义=指路不打扰)。
|
|
438
471
|
// hint 文案经 messages 层取(第 1 批 1b:去本机标识——原 hint 引用本机手册踩坑编号/代理端口/发布变量)
|
|
472
|
+
// 2026-09-11 去本机残留(分层 P4):第二条原写死本机代理端口号(key 亦含该端口号)——
|
|
473
|
+
// 通用层不应含任何本机标识;判据放宽为「连接被拒」本身(提示仅是"先查代理/服务是否启动",泛指无害)。
|
|
439
474
|
const KNOWN_PITFALLS = [
|
|
440
475
|
{ key: "SEC_E_NO_CREDENTIALS", re: /SEC_E_NO_CREDENTIALS/i, hintKey: "pitfall.tls" },
|
|
441
|
-
{ key: "
|
|
476
|
+
{ key: "conn-refused", re: /ECONNREFUSED/i, hintKey: "pitfall.proxy" },
|
|
442
477
|
{ key: "sandbox-pipe-EPERM", re: /EPERM/i, hintKey: "pitfall.sandbox-pipe" },
|
|
443
478
|
{ key: "ERR_MODULE_NOT_FOUND", re: /ERR_MODULE_NOT_FOUND/i, hintKey: "pitfall.module-missing" }
|
|
444
479
|
];
|
|
@@ -617,12 +652,15 @@ export function isAuditCommand(command) {
|
|
|
617
652
|
* 2026-08-31(验证通道放行面,用户定稿):清单扩展 + 由 isAnalysisOp 独立调用——
|
|
618
653
|
* ① 单测通配(任意 *.test.mjs)、② 验证/门禁脚本(check-tool-coverage / publish-aptitude-check)、
|
|
619
654
|
* ③ --dry-run(发布脚本 dry-run=只读推导,发布链无 dry-run 不匹配)、④ gh api 只读 GET、npm whoami。
|
|
655
|
+
* 2026-09-11(手册条款 16「重复劳动升级律」落地):新增 ⑤ 收尾脚本 session-wrapup.mjs——清理 .staging-* 入回收站
|
|
656
|
+
* + 列待提交清单 + 沉淀自检。放行依据=规则 13A 豁免原文「移动文件到 .backups/ 或 .backups/trash-<时间戳>/
|
|
657
|
+
* 的保护性备份操作,免询问」;**不含** commit / publish / push 等执行类动作(仍走严格授权,脚本只"列出"待提交文件)。
|
|
620
658
|
* 安全边界:执行类脚本(release-plugin 无 --dry-run / inject / uninject / install / publish / commit /
|
|
621
659
|
* push / Remove-Item 等)一律不匹配——执行类仍走授权语义严格版(0.5.11 防借道回归不放松)。
|
|
622
660
|
*/
|
|
623
661
|
export function isVerificationCommand(command) {
|
|
624
662
|
if (typeof command !== "string") return false;
|
|
625
|
-
if (/\b(?:node|npm|npx)\s+[^\n]*(?:run-all\.mjs|audit-mount-consistency\.mjs|loader-smoke\.e2e\.mjs|verify-all\.mjs|health-audit\.mjs|check-tool-coverage\.mjs|publish-aptitude-check\.mjs|\.test\.mjs\b|--check|--test|--dry-run)\b/i.test(command)) return true;
|
|
663
|
+
if (/\b(?:node|npm|npx)\s+[^\n]*(?:run-all\.mjs|audit-mount-consistency\.mjs|loader-smoke\.e2e\.mjs|verify-all\.mjs|health-audit\.mjs|check-tool-coverage\.mjs|publish-aptitude-check\.mjs|session-wrapup\.mjs|\.test\.mjs\b|--check|--test|--dry-run)\b/i.test(command)) return true;
|
|
626
664
|
if (/\b(?:npm|pnpm)\s+(?:test|run\s+test)\b/i.test(command)) return true;
|
|
627
665
|
if (/\bgh\s+api\b/i.test(command)) {
|
|
628
666
|
// 只读 GET 形态;写形态(--method/-X 显式方法、-F/--field 表单)不豁免(gg api 写=执行类)
|
|
@@ -853,7 +891,12 @@ export const PATTERN_MAP_KEYS = {
|
|
|
853
891
|
|
|
854
892
|
/** 数值型配置键(键 → 内置默认;与正则键共用同一配置层与校验/回退语义) */
|
|
855
893
|
export const PATTERN_NUM_KEYS = {
|
|
856
|
-
criticism_caps_ratio: 0.6 // 英文全大写比率阈值(≥ 该值且字母数 ≥6 视为"喊叫"强形态)
|
|
894
|
+
criticism_caps_ratio: 0.6, // 英文全大写比率阈值(≥ 该值且字母数 ≥6 视为"喊叫"强形态)
|
|
895
|
+
// 2026-09-15(踩坑 144 修法 (b) 收紧档落地):纯比率版把中文消息里的**编号标签**(A1→A3→B5→D)
|
|
896
|
+
// 与**缩写**(ESR/DSH)判成"喊叫"→ A″ 误冻结写类工具(本会话连续两回合实测)。
|
|
897
|
+
// 现要求"词形"证据:≥ min_tokens 个「连续字母 ≥ min_word_len」的 token,再看这些 token 的大写比率。
|
|
898
|
+
criticism_caps_min_tokens: 2,
|
|
899
|
+
criticism_caps_min_word_len: 4
|
|
857
900
|
};
|
|
858
901
|
|
|
859
902
|
// 内置默认:语言无关最小集(无中文)
|
package/lib/core/state.js
CHANGED
|
@@ -156,6 +156,12 @@ export function getSessionState(state, sessionId) {
|
|
|
156
156
|
selfCertCount: new Map(),
|
|
157
157
|
authorizations: [],
|
|
158
158
|
backups: [],
|
|
159
|
+
// D2 修复(2026-09-10,独立 DSH_HOME 试点反馈):待决 ask 提升为**会话级**。
|
|
160
|
+
// 此前放在 turn 级:用户先发新消息 → turn/start → resetTurn 整体替换 s.turn
|
|
161
|
+
// → pendingAsk 被刷成 null → 随后到达的 ask 答复走不进登记分支,
|
|
162
|
+
// 实测「用户消息与 ask 答复并发」时一次 ask 的授权**全部未登记**(ERR-M9Z47H)。
|
|
163
|
+
// 语义依据:pendingAsk 描述的是「一次工具调用的待决状态」,跨回合有效,不属回合文本状态。
|
|
164
|
+
pendingAsk: null,
|
|
159
165
|
mountAuditRevision: 0,
|
|
160
166
|
mountAuditSignature: "",
|
|
161
167
|
lastCommandOutput: "",
|
|
@@ -252,7 +258,8 @@ export function freshTurn() {
|
|
|
252
258
|
intents: null,
|
|
253
259
|
intentState: "lexicon", // lexicon | llm-pending | llm-ready(LLM 意图兜底状态)
|
|
254
260
|
llmIntent: null, // { raw, verdict, reason, confidence, at }(LLM 裁决,非高置信时词表兜底)
|
|
255
|
-
pendingAsk
|
|
261
|
+
// pendingAsk 已迁至**会话级**(见 getSessionState)——D2 修复,2026-09-10。
|
|
262
|
+
// 此处不再定义回合级 pendingAsk:避免两处同名状态互相遮蔽。
|
|
256
263
|
pendingToolCalls: new Map(),
|
|
257
264
|
getDateSeen: false,
|
|
258
265
|
backupSeen: false,
|
|
@@ -268,7 +275,10 @@ export function freshTurn() {
|
|
|
268
275
|
scopes: [],
|
|
269
276
|
// M8 双通道机制(2026-08-24):统一入口落盘后同轮 engram_store 校验(entryMarker 经配置)
|
|
270
277
|
manualWriteSeen: false,
|
|
271
|
-
engramStoreSeen: false
|
|
278
|
+
engramStoreSeen: false,
|
|
279
|
+
// D(2026-09-13):本回合是否已在「落盘那一刻」提示过——防 turn/end 兜底路径重复投递
|
|
280
|
+
// (maybeInject 的闸是同规则最多 3 次,非一次即止,故需回合级标记)
|
|
281
|
+
m8Hinted: false
|
|
272
282
|
};
|
|
273
283
|
}
|
|
274
284
|
|