dsh-rule-engine 0.5.11 → 0.5.13
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 +30 -99
- package/lib/core/authorization.js +517 -513
- package/lib/core/config.js +73 -66
- package/lib/core/contract.js +41 -0
- package/lib/core/guard-core.js +754 -724
- package/lib/core/intent.js +11 -3
- package/lib/core/label-fingerprint.js +145 -0
- package/lib/core/lexicon.js +72 -72
- package/lib/core/parser.js +111 -103
- package/lib/core/patterns.js +42 -3
- package/lib/core/state.js +324 -301
- package/lib/core/understander.js +221 -150
- package/lib/index.js +1788 -1663
- package/lib/service.js +17 -2
- package/package.json +1 -1
- package/scripts/publish-aptitude-check.mjs +144 -0
- package/scripts/verify-all.mjs +4 -0
package/lib/core/state.js
CHANGED
|
@@ -1,301 +1,324 @@
|
|
|
1
|
-
// state.js - 插件运行时状态(内存态)。
|
|
2
|
-
// 所有会话级状态以 sessionId 为 key;turn 级状态在 turn/start 重置。
|
|
3
|
-
import { readFileSync, statSync } from "node:fs";
|
|
4
|
-
import { loadRules } from "./parser.js";
|
|
5
|
-
import { agentsFilePath, disabledRulesFilePath } from "./paths.js";
|
|
6
|
-
import { understandAll } from "./understander.js";
|
|
7
|
-
import { writeUnderstanding } from "./understanding-store.js";
|
|
8
|
-
import { AUTH_TTL_MS } from "./authorization.js";
|
|
9
|
-
import { initSessionContract } from "./contract.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
export function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
if (state.
|
|
115
|
-
const keys = [...state.
|
|
116
|
-
for (let i = 0; i < keys.length -
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
s =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*/
|
|
192
|
-
export function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return s.backups
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
export function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
1
|
+
// state.js - 插件运行时状态(内存态)。
|
|
2
|
+
// 所有会话级状态以 sessionId 为 key;turn 级状态在 turn/start 重置。
|
|
3
|
+
import { readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { loadRules } from "./parser.js";
|
|
5
|
+
import { agentsFilePath, disabledRulesFilePath } from "./paths.js";
|
|
6
|
+
import { understandAll } from "./understander.js";
|
|
7
|
+
import { writeUnderstanding } from "./understanding-store.js";
|
|
8
|
+
import { AUTH_TTL_MS } from "./authorization.js";
|
|
9
|
+
import { initSessionContract } from "./contract.js";
|
|
10
|
+
import { loadLabelsFromDisk, saveLabelsToDisk } from "./label-fingerprint.js";
|
|
11
|
+
import { dshHome } from "./paths.js";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
const MAX_SESSIONS = 200;
|
|
15
|
+
const MAX_RETRY_KEYS = 500;
|
|
16
|
+
const MAX_INJECT_KEYS = 500;
|
|
17
|
+
const MTIME_CHECK_INTERVAL_MS = 2000;
|
|
18
|
+
|
|
19
|
+
export function createState() {
|
|
20
|
+
return {
|
|
21
|
+
loadedAt: 0,
|
|
22
|
+
enabled: true,
|
|
23
|
+
rules: [],
|
|
24
|
+
configs: [],
|
|
25
|
+
configOk: true,
|
|
26
|
+
configError: null,
|
|
27
|
+
bypassUntil: 0,
|
|
28
|
+
unlockUntil: 0,
|
|
29
|
+
mtimeMs: 0,
|
|
30
|
+
lastMtimeCheck: 0,
|
|
31
|
+
sessions: new Map(),
|
|
32
|
+
retryCounts: new Map(),
|
|
33
|
+
injectCounts: new Map(),
|
|
34
|
+
injectAt: new Map(), // v0.5.7:key `${sessionId}:${ruleId}` → 首次投递时间(提醒一次并记住)
|
|
35
|
+
injectBudget: new Map(), // v0.5.7:sessionId → 投递时间戳数组(会话每小时预算)
|
|
36
|
+
judgeCache: new Map(), // v0.5.7:sha256(ruleId+text) → 裁决结果(同文本不重复调用)
|
|
37
|
+
judgeBudget: new Map(), // v0.5.7:`${sessionId}:${UTC日期}` → {count}(每日裁决预算)
|
|
38
|
+
judgeFn: null, // v0.5.7:可注入裁决函数(生产=judge.js LLM 实现;测试=stub)
|
|
39
|
+
globalAuthorizations: [],
|
|
40
|
+
skillNames: null,
|
|
41
|
+
llmEnrichedKeys: new Set(),
|
|
42
|
+
lastActive: [],
|
|
43
|
+
lastEvent: null,
|
|
44
|
+
reloadCount: 0,
|
|
45
|
+
mountRevision: 0,
|
|
46
|
+
mountSignature: "",
|
|
47
|
+
taskContract: {
|
|
48
|
+
taskContractEnabled: false,
|
|
49
|
+
askEnabled: false,
|
|
50
|
+
taskContractMode: "observe"
|
|
51
|
+
},
|
|
52
|
+
labels: new Map(),
|
|
53
|
+
// 0.5.12(F2):打标指纹台账持久化行(磁盘 ~/.dsh/rule-engine-labels.json;内存态副本)
|
|
54
|
+
labelRows: loadLabelsFromDisk(join(dshHome(), "rule-engine-labels.json")),
|
|
55
|
+
askRejections: [], // { sessionId, at }(ask 被拒记录,弹窗消减/节流)
|
|
56
|
+
llmIntentCache: new Map(), // sha256(text) → verdict(LRU 上限 cfg.cacheSize)
|
|
57
|
+
llmIntentBudget: new Map(), // sessionId → 当日调用数(上限 cfg.dailyLimitPerSession)
|
|
58
|
+
llmIntentHits: 0, // LLM 缓存命中计数(/guard status 观测)
|
|
59
|
+
llmIntentLast: null, // { text, verdict, at }(最近一次 LLM 采用/失败,/guard status 观测)
|
|
60
|
+
verifyPass: [], // { at, sessionId, what }(测试通过/冷加载探针输出采集,交付闸门 M3 用)
|
|
61
|
+
deniedKeys: new Set(), // 引擎 deny 的调用 key(规则 1 计数解耦)
|
|
62
|
+
unknownToolApproved: new Set(), // 机制 B:已 ask 过的未归类工具(本会话内批准后不再重复询问)
|
|
63
|
+
unknownToolApprovedMeta: new Map(), // v0.5.9:name → {name,time,session}(持久化元数据,/guard tools 可视化)
|
|
64
|
+
unknownToolSessionAdded: new Set(), // v0.5.9:本会话新增的批准(与永久加载区分,/guard tools 展示)
|
|
65
|
+
// T3 通用化(2026-08-31):声明式 handler 绑定覆盖表 { ruleId: handlerName }
|
|
66
|
+
// 由 apply() 从插件配置(rule-engine.json 可选键 handlerOverrides)注入;重载时经理解层生效
|
|
67
|
+
handlerOverrides: {},
|
|
68
|
+
// 残余1 剥离(2026-08-31):本机默认偏好表(rule-engine.json 可选键 handlerDefaultMap)
|
|
69
|
+
// 由 apply() 注入;重载时经理解层生效(通用部署=空表,零本机编号)
|
|
70
|
+
handlerDefaultMap: {}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 把插件配置中的任务契约设置同步到运行时状态 */
|
|
75
|
+
export function applyTaskContractConfig(state, conf) {
|
|
76
|
+
state.taskContract = {
|
|
77
|
+
taskContractEnabled: conf?.taskContractEnabled === true,
|
|
78
|
+
askEnabled: conf?.askEnabled === true,
|
|
79
|
+
taskContractMode: conf?.taskContractMode === "armed" ? "armed" : "observe",
|
|
80
|
+
defaults: conf?.taskContractDefaults || {}
|
|
81
|
+
};
|
|
82
|
+
return state.taskContract;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** AGENTS.md mtime 变化时自动重解析(规则 21:规则是流动数据) */
|
|
86
|
+
export function maybeReloadIfChanged(state, now = Date.now()) {
|
|
87
|
+
if (now - state.lastMtimeCheck < MTIME_CHECK_INTERVAL_MS) return false;
|
|
88
|
+
state.lastMtimeCheck = now;
|
|
89
|
+
pruneState(state);
|
|
90
|
+
try {
|
|
91
|
+
const mtime = statSync(agentsFilePath()).mtimeMs;
|
|
92
|
+
if (mtime !== state.mtimeMs) {
|
|
93
|
+
reloadRules(state);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// AGENTS.md 暂时不可读时保持旧规则,不崩
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 清理过期/超量内存状态:会话、重试计数、注入计数 */
|
|
103
|
+
export function pruneState(state) {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
if (state.sessions.size > MAX_SESSIONS) {
|
|
106
|
+
const entries = [...state.sessions.entries()].sort((a, b) => (a[1].lastSeen || 0) - (b[1].lastSeen || 0));
|
|
107
|
+
const excess = entries.slice(0, state.sessions.size - MAX_SESSIONS);
|
|
108
|
+
for (const [key] of excess) state.sessions.delete(key);
|
|
109
|
+
}
|
|
110
|
+
if (state.retryCounts.size > MAX_RETRY_KEYS) {
|
|
111
|
+
const keys = [...state.retryCounts.keys()];
|
|
112
|
+
for (let i = 0; i < keys.length - MAX_RETRY_KEYS; i++) state.retryCounts.delete(keys[i]);
|
|
113
|
+
}
|
|
114
|
+
if (state.injectCounts.size > MAX_INJECT_KEYS) {
|
|
115
|
+
const keys = [...state.injectCounts.keys()];
|
|
116
|
+
for (let i = 0; i < keys.length - MAX_INJECT_KEYS; i++) state.injectCounts.delete(keys[i]);
|
|
117
|
+
}
|
|
118
|
+
// v0.5.7:投递资格/裁决状态超限清理(优先保留新 key 语义——按插入序删最旧)
|
|
119
|
+
for (const m of [state.injectAt, state.judgeCache]) {
|
|
120
|
+
if (m.size > MAX_INJECT_KEYS) {
|
|
121
|
+
const keys = [...m.keys()];
|
|
122
|
+
for (let i = 0; i < keys.length - MAX_INJECT_KEYS; i++) m.delete(keys[i]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (state.injectBudget.size > MAX_SESSIONS) {
|
|
126
|
+
const keys = [...state.injectBudget.keys()];
|
|
127
|
+
for (let i = 0; i < keys.length - MAX_SESSIONS; i++) state.injectBudget.delete(keys[i]);
|
|
128
|
+
}
|
|
129
|
+
if (state.judgeBudget.size > MAX_SESSIONS * 2) {
|
|
130
|
+
const keys = [...state.judgeBudget.keys()];
|
|
131
|
+
for (let i = 0; i < keys.length - MAX_SESSIONS * 2; i++) state.judgeBudget.delete(keys[i]);
|
|
132
|
+
}
|
|
133
|
+
// 清理过期授权(顺便释放内存)
|
|
134
|
+
for (const s of state.sessions.values()) {
|
|
135
|
+
s.authorizations = s.authorizations.filter((a) => !a.expiresAt || a.expiresAt > now);
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(state.globalAuthorizations)) {
|
|
138
|
+
state.globalAuthorizations = state.globalAuthorizations.filter((a) => !a.expiresAt || a.expiresAt > now);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getSessionState(state, sessionId) {
|
|
143
|
+
const key = sessionId || "global";
|
|
144
|
+
let s = state.sessions.get(key);
|
|
145
|
+
if (!s) {
|
|
146
|
+
s = {
|
|
147
|
+
id: key,
|
|
148
|
+
ruleStats: {}, // C2:每规则 detected/suppressed/injected 统计(会话级,跨回合累计)
|
|
149
|
+
manualReadSeen: false,
|
|
150
|
+
lastUserText: "",
|
|
151
|
+
selfCertCount: new Map(),
|
|
152
|
+
authorizations: [],
|
|
153
|
+
backups: [],
|
|
154
|
+
mountAuditRevision: 0,
|
|
155
|
+
mountAuditSignature: "",
|
|
156
|
+
lastCommandOutput: "",
|
|
157
|
+
lastSeen: Date.now(),
|
|
158
|
+
turn: freshTurn(),
|
|
159
|
+
// 批次 4(阶段 3 默认武装):global 会话自身不继承(防循环);其他会话继承 global 模板(/guard 命令写入)
|
|
160
|
+
contract: key === "global"
|
|
161
|
+
? initSessionContract(state.taskContract, null)
|
|
162
|
+
: initSessionContract(state.taskContract, getSessionState(state, "global").contract),
|
|
163
|
+
recentActions: []
|
|
164
|
+
};
|
|
165
|
+
state.sessions.set(key, s);
|
|
166
|
+
}
|
|
167
|
+
s.lastSeen = Date.now();
|
|
168
|
+
return s;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 记录一条授权证据(结构化:type/pathPrefix/at/source/expiresAt)。
|
|
173
|
+
* 2026-08-24(R1 统一授权存储):自动来源(执行分点/用户消息/ask)**绝不同步进 globalAuthorizations**——
|
|
174
|
+
* 全局池仅显式白名单可写(recordGlobalAuth),杜绝"一个会话的授权被其它会话复用"与 revoke 残留。
|
|
175
|
+
*/
|
|
176
|
+
export function recordAuthorization(state, sessionId, auth) {
|
|
177
|
+
const record = {
|
|
178
|
+
at: Date.now(),
|
|
179
|
+
source: "ask",
|
|
180
|
+
expiresAt: Date.now() + AUTH_TTL_MS,
|
|
181
|
+
...auth
|
|
182
|
+
};
|
|
183
|
+
const s = getSessionState(state, sessionId);
|
|
184
|
+
s.authorizations.push(record);
|
|
185
|
+
return s.authorizations;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 显式白名单授权(用户明确选择"记住此授权"时写入;当前无 UI 入口,结构预留)。
|
|
190
|
+
* 这是 globalAuthorizations 的唯一写入通道。
|
|
191
|
+
*/
|
|
192
|
+
export function recordGlobalAuth(state, sessionId, auth) {
|
|
193
|
+
const list = recordAuthorization(state, sessionId, auth);
|
|
194
|
+
const record = list[list.length - 1];
|
|
195
|
+
state.globalAuthorizations.push(record);
|
|
196
|
+
return list;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* revoke 全清(R1.3,2026-08-24):session.authorizations + turn.scopes + globalAuthorizations + askRejections。
|
|
201
|
+
* @returns {number} 清理的授权/范围条目数(供审计描述)
|
|
202
|
+
*/
|
|
203
|
+
export function clearAuthorizations(state) {
|
|
204
|
+
let count = 0;
|
|
205
|
+
for (const s of state.sessions.values()) {
|
|
206
|
+
count += (s.authorizations || []).length + ((s.turn && s.turn.scopes) ? s.turn.scopes.length : 0);
|
|
207
|
+
s.authorizations = [];
|
|
208
|
+
if (s.turn) s.turn.scopes = [];
|
|
209
|
+
}
|
|
210
|
+
count += (state.globalAuthorizations || []).length;
|
|
211
|
+
state.globalAuthorizations = [];
|
|
212
|
+
if (Array.isArray(state.askRejections)) state.askRejections = [];
|
|
213
|
+
return count;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 记录一条备份证据(目标路径 → 备份路径) */
|
|
217
|
+
export function recordBackup(state, sessionId, targetPath, backupPath) {
|
|
218
|
+
const s = getSessionState(state, sessionId);
|
|
219
|
+
s.backups.push({
|
|
220
|
+
targetPath: normalizeBackupPath(targetPath),
|
|
221
|
+
backupPath: normalizeBackupPath(backupPath),
|
|
222
|
+
at: Date.now()
|
|
223
|
+
});
|
|
224
|
+
return s.backups;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeBackupPath(p) {
|
|
228
|
+
return String(p || "").replace(/\\/g, "/").toLowerCase();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** 查找目标路径是否已有对应备份 */
|
|
232
|
+
export function findBackupForPath(state, sessionId, targetPath) {
|
|
233
|
+
const s = getSessionState(state, sessionId);
|
|
234
|
+
const t = normalizeBackupPath(targetPath);
|
|
235
|
+
return s.backups.find((b) => b.targetPath === t) || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function freshTurn() {
|
|
239
|
+
return {
|
|
240
|
+
number: 0,
|
|
241
|
+
userText: "",
|
|
242
|
+
realUserSeen: false, // v0.5.7:本回合是否有真实用户消息(注入轮不检测的依据)
|
|
243
|
+
askSeen: false,
|
|
244
|
+
askRejected: false, // 本回合 ask 已被拒(防连环 ask,弹窗消减)
|
|
245
|
+
questionOnly: false,
|
|
246
|
+
intents: null,
|
|
247
|
+
intentState: "lexicon", // lexicon | llm-pending | llm-ready(LLM 意图兜底状态)
|
|
248
|
+
llmIntent: null, // { raw, verdict, reason, confidence, at }(LLM 裁决,非高置信时词表兜底)
|
|
249
|
+
pendingAsk: null,
|
|
250
|
+
pendingToolCalls: new Map(),
|
|
251
|
+
getDateSeen: false,
|
|
252
|
+
backupSeen: false,
|
|
253
|
+
toolCount: 0,
|
|
254
|
+
firstToolName: "",
|
|
255
|
+
skillNames: [],
|
|
256
|
+
toolNames: [],
|
|
257
|
+
commands: [],
|
|
258
|
+
reasoningText: "",
|
|
259
|
+
// 规则 22 粒度升级(2026-08-24):本回合已获授权范围(execute 子句 + ask 授权)
|
|
260
|
+
scopes: [],
|
|
261
|
+
// M8 双通道机制(2026-08-24):dsh-manual-write 落盘后同轮 engram_store 校验
|
|
262
|
+
manualWriteSeen: false,
|
|
263
|
+
engramStoreSeen: false
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function resetTurn(state, sessionId, turnNumber) {
|
|
268
|
+
const s = getSessionState(state, sessionId);
|
|
269
|
+
s.turn = freshTurn();
|
|
270
|
+
s.turn.number = typeof turnNumber === "number" ? turnNumber : 0;
|
|
271
|
+
// 2026-08-24(事故复盘 P0-3 根修):不再继承上一轮 lastUserText——
|
|
272
|
+
// 上一轮若为询问(含“为什么”等)会把旧意图带进新回合导致误拦;
|
|
273
|
+
// turn.userText 只由真实的 user/message 事件写入;无消息回合不做规则 22 判定(12A/13A 仍把关)。
|
|
274
|
+
return s.turn;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 读取 dsh-rules-manager 的 disabled-rules.json 全部存档条目(禁用=正文移走+原样存档) */
|
|
278
|
+
export function loadDisabledRuleEntries() {
|
|
279
|
+
try {
|
|
280
|
+
const raw = readFileSync(disabledRulesFilePath(), "utf8");
|
|
281
|
+
const list = JSON.parse(raw);
|
|
282
|
+
if (!Array.isArray(list)) return [];
|
|
283
|
+
return list.filter((d) => d && d.index !== undefined && d.index !== null);
|
|
284
|
+
} catch {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** 已禁用规则 id 集合(P0-2;加载失败=空集,保守不误拦) */
|
|
290
|
+
export function loadDisabledRuleIds() {
|
|
291
|
+
return new Set(loadDisabledRuleEntries().map((d) => String(d.index)).filter(Boolean));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 重新加载并理解 AGENTS.md;统一在 reload 后刷新理解产物(P0-3)。
|
|
296
|
+
* 禁用语义(2026-08-31 修复):禁用规则以 disabled=true 占位存在——不只是标记"在场规则"。
|
|
297
|
+
* 正文已移走的禁用规则由 understandAll(opts.disabledEntries)用存档正文重建占位:
|
|
298
|
+
* ① 禁用不再"消失"(/guard rules 显示(已禁用),恢复=写回 AGENTS.md 后自动重新生效);
|
|
299
|
+
* ② 死映射消除(HANDLER_BY_RULE 中的 12B 等因规则在场而不再被判 dead,consistency-live 转绿)。
|
|
300
|
+
*/
|
|
301
|
+
export function reloadRules(state) {
|
|
302
|
+
const parsed = loadRules();
|
|
303
|
+
state.rules = parsed.ok ? parsed.rules : [];
|
|
304
|
+
const archived = loadDisabledRuleEntries();
|
|
305
|
+
// 禁用语义由理解层完整封装:在场标 disabled + 缺场存档占位重建(单真源,见 understandAll)
|
|
306
|
+
state.configs = understandAll(state.rules, {
|
|
307
|
+
disabledEntries: archived,
|
|
308
|
+
handlerOverrides: state.handlerOverrides || {},
|
|
309
|
+
defaultMap: state.handlerDefaultMap || {}
|
|
310
|
+
});
|
|
311
|
+
state.configOk = parsed.ok;
|
|
312
|
+
state.configError = parsed.error || null;
|
|
313
|
+
state.mtimeMs = parsed.mtimeMs || 0;
|
|
314
|
+
state.loadedAt = Date.now();
|
|
315
|
+
state.lastMtimeCheck = Date.now();
|
|
316
|
+
state.reloadCount++;
|
|
317
|
+
if (parsed.ok) writeUnderstanding(state.configs);
|
|
318
|
+
return state;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 获取某条理解配置 */
|
|
322
|
+
export function findConfig(state, ruleId) {
|
|
323
|
+
return state.configs.find((c) => String(c.ruleId) === String(ruleId));
|
|
324
|
+
}
|