dsh-rule-engine 0.5.9 → 0.5.11
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 +86 -7
- package/lib/core/authorization.js +64 -19
- package/lib/core/guard-core.js +100 -13
- package/lib/core/hotwords.js +86 -0
- package/lib/core/intent.js +51 -14
- package/lib/core/judge.js +72 -0
- package/lib/core/lexicon.js +72 -0
- package/lib/core/llm-intent.js +10 -2
- package/lib/core/llm-understander.js +15 -0
- package/lib/core/patterns.js +162 -12
- package/lib/core/text-detect.js +67 -11
- package/lib/core/tool-catalog.js +3 -0
- package/lib/core/understander.js +1 -1
- package/lib/core/version-guard.js +6 -3
- package/lib/index.js +235 -50
- package/package.json +1 -1
- package/scripts/health-audit.mjs +7 -1
- package/scripts/probe-home.mjs +3 -0
- package/scripts/probe-intent.mjs +14 -0
- package/scripts/verify-all.mjs +44 -2
- package/scripts/verify-guard-live.mjs +30 -0
- package/scripts/verify-v474.mjs +14 -0
package/lib/core/intent.js
CHANGED
|
@@ -18,25 +18,49 @@
|
|
|
18
18
|
// 消灭“两套疑问词判定不一致”(如 authorization 的“影响吗”与 intent 的“怎么”)。
|
|
19
19
|
// - 新增 STATUS_SIGNAL_RE / isStatusSignal():状态信号分类(宽松版,危险词排除,
|
|
20
20
|
// 12A/13A 兜底),与“无用户消息回合”同等待遇。
|
|
21
|
+
// 0.5.11(用户拍板):词表唯一源 = lib/core/lexicon.js——下面 5 张表全部从那里 import,
|
|
22
|
+
// 本文件不再自持定义(原定义删除);对外 export 名保持不变(QUESTION_RE/STATUS_SIGNAL_RE
|
|
23
|
+
// 仍对外可见,/guard 与测试引用不受影响)。
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
QUESTION_WORDS_RE,
|
|
27
|
+
ACTION_WORDS_RE,
|
|
28
|
+
STRONG_EXEC_RE as STRONG_EXEC_LEX,
|
|
29
|
+
STATUS_SIGNAL_WORDS_RE,
|
|
30
|
+
DANGEROUS_ACTION_WORDS_RE
|
|
31
|
+
} from "./lexicon.js";
|
|
32
|
+
import { hasHotwordAction } from "./hotwords.js";
|
|
21
33
|
|
|
22
34
|
export const TAG_RE = /【(执行|方案|问询|信息)】/;
|
|
23
35
|
|
|
24
|
-
//
|
|
25
|
-
export const QUESTION_RE =
|
|
36
|
+
// 统一疑问词表(唯一源:lexicon.js;此处再导出保持外部引用兼容)
|
|
37
|
+
export const QUESTION_RE = QUESTION_WORDS_RE;
|
|
26
38
|
|
|
27
39
|
const DEFER_RE = /待确认|确认后再|等我确认|先方案|先别|不要执行|之后再说|确认后|再确认|先确认|不要直接/;
|
|
28
|
-
const ACTION_RE =
|
|
40
|
+
const ACTION_RE = ACTION_WORDS_RE;
|
|
29
41
|
const PLAN_RE = /方案|建议|评估|分析|设计|规划|计划|给出|提供/;
|
|
30
|
-
const STRONG_EXEC_RE =
|
|
42
|
+
const STRONG_EXEC_RE = STRONG_EXEC_LEX;
|
|
31
43
|
const NUMBERED_ITEM_RE = /^\s*(?:\(?(\d+)[\)、..)]\s*)/;
|
|
44
|
+
// A1(2026-08-28 阶段一):具体动作词(区别于"执行/开始/进行"等宽泛词)。
|
|
45
|
+
// 用途:hasAction+hasPlan 时区分"给出执行方案"(方案请求)与"将建议书转化到业务通告"(执行指令)。
|
|
46
|
+
const SPECIFIC_ACTION_RE =
|
|
47
|
+
/写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|安装|发布|下载|提交|运行|修复|替换|重建|启动|重启|停止|卸载|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:完|出|一下|一遍|出来)?|展示|打开|提取|还原|导入|导出|采纳|选择|选定|落盘|落地|清理|移除|清空/i;
|
|
48
|
+
|
|
49
|
+
// A3(2026-08-28,阶段一):@文件引用信号——用户消息中 @ 引用路径/文件(DSH 官方
|
|
50
|
+
// file-reference 语法,@path 或 @"path with spaces"/@file.docx),且含处理动词 → 视为执行分点。
|
|
51
|
+
// 边界:@ 前必须是非词字符(开头/空白/标点),避免邮箱 a@b.com 误伤。
|
|
52
|
+
const AT_REF_RE = /(?:^|[\s((【《"',。;:!?])\s*@(?:["']?(?:[A-Za-z]:[\\/]|[^"'\\\s,。;:!?()【】《》、]+\.(?:docx?|md|txt|pdf|json|ya?ml|xlsx?|csv|pptx?|html?|zip|mjs|js|ts|py|exe|ps1)))/i;
|
|
53
|
+
const AT_ACTION_RE = /看|读|改|写|转|查|处理|提取|打开|展示|审阅|检查|分析|整理|校对|核对|还原|更新|删除|移动|执行|处理/i;
|
|
32
54
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
/**
|
|
56
|
+
* 状态信号:对先前指令的就绪/完成确认(“我已重启 / 已输入 / 重启完成 / 好了 / done”)。
|
|
57
|
+
* 语义 = 与“无用户消息回合”同等待遇:不触发规则 22 的“无执行分点”判定(规则 22⑩)。
|
|
58
|
+
* 唯一源:lexicon.js STATUS_SIGNAL_WORDS_RE;此处再导出保持外部引用兼容。
|
|
59
|
+
*/
|
|
60
|
+
export const STATUS_SIGNAL_RE = STATUS_SIGNAL_WORDS_RE;
|
|
37
61
|
|
|
38
62
|
// 危险动作词:命中则不当状态信号(防“我已删除了某文件”被当成就绪确认)
|
|
39
|
-
const DANGEROUS_ACTION_RE =
|
|
63
|
+
const DANGEROUS_ACTION_RE = DANGEROUS_ACTION_WORDS_RE;
|
|
40
64
|
|
|
41
65
|
/** DSH 在会话上下文里注入的系统文本(工作区指令/技能目录等),不是用户的真实消息 */
|
|
42
66
|
export function isInjectedContextMessage(text) {
|
|
@@ -114,10 +138,12 @@ function splitClauses(text) {
|
|
|
114
138
|
const raw = String(text || "").trim();
|
|
115
139
|
if (!raw) return [];
|
|
116
140
|
|
|
117
|
-
// 同行内多个编号("1、A 2、B"
|
|
141
|
+
// 同行内多个编号("1、A 2、B"):在编号前插入换行,统一按行切分。
|
|
118
142
|
// 修复 2026-08-24 实测缺陷:同行编号列表被并入单子句,"方案"等词导致整条误判 plan。
|
|
119
|
-
//
|
|
120
|
-
|
|
143
|
+
// 修复 2026-08-28(用户"1、问 2、问 3、跑"被整条吞):编号前为**空白或标点**(?。;、等)也切——
|
|
144
|
+
// 用户在中文标点后直接发编号(无空格),"?2、"应切为列表项"2、";保守保留"执行第2、3条"不切
|
|
145
|
+
//("第"是汉字非标点,编号前非空白非标点 → 整体词不切)。
|
|
146
|
+
const normalized = raw.replace(/(?:[\s。;,、??!!])(\d+[\)、..)])/g, "\n$1");
|
|
121
147
|
const lines = normalized.split(/\r?\n/);
|
|
122
148
|
const hasNumbered = lines.some((line) => NUMBERED_ITEM_RE.test(line));
|
|
123
149
|
const hasTagLines = lines.filter((line) => TAG_RE.test(line)).length >= 2;
|
|
@@ -178,9 +204,12 @@ function classifyClause(clause) {
|
|
|
178
204
|
const tagType = normalizeTag(tag);
|
|
179
205
|
const hasQuestion = QUESTION_RE.test(text);
|
|
180
206
|
const hasDefer = DEFER_RE.test(text);
|
|
181
|
-
|
|
207
|
+
// B1(2026-08-29):词表命中 ∪ 热词命中(热词=LLM 兜底已确认过的动作词,见 hotwords.js)
|
|
208
|
+
const hasAction = ACTION_RE.test(text) || hasHotwordAction(text);
|
|
182
209
|
const hasPlan = PLAN_RE.test(text);
|
|
183
210
|
const hasStrongExec = STRONG_EXEC_RE.test(text);
|
|
211
|
+
// A3(阶段一):@文件引用信号 + 处理动词 → 执行分点(用户用 @ 引用文件即"处理它")
|
|
212
|
+
const hasAtRef = AT_REF_RE.test(text) && AT_ACTION_RE.test(text);
|
|
184
213
|
|
|
185
214
|
let type = "info";
|
|
186
215
|
let ambiguous = false;
|
|
@@ -198,9 +227,17 @@ function classifyClause(clause) {
|
|
|
198
227
|
type = "status";
|
|
199
228
|
} else if (hasDefer && hasAction) {
|
|
200
229
|
type = "plan";
|
|
201
|
-
} else if (hasPlan && !
|
|
230
|
+
} else if (hasAction && hasPlan && !SPECIFIC_ACTION_RE.test(text)) {
|
|
231
|
+
// 只有宽泛动作词(执行/开始/进行…)+ 方案词 → 方案请求("给出执行方案"≠执行指令)
|
|
202
232
|
type = "plan";
|
|
203
233
|
} else if (hasAction) {
|
|
234
|
+
// A1(2026-08-28 阶段一):动作词优先于方案词——"将建议书内容转化到业务通告中"
|
|
235
|
+
// 含"建议"(PLAN_RE)却被判 plan 导致无执行分点(真实事故根因);有明确动作词即执行分点。
|
|
236
|
+
type = "execute";
|
|
237
|
+
} else if (hasPlan && !hasStrongExec) {
|
|
238
|
+
type = "plan";
|
|
239
|
+
} else if (hasAtRef) {
|
|
240
|
+
// 词表未命中但含 @文件引用 + 处理动词:判执行分点(词表盲区治本方向之一)
|
|
204
241
|
type = "execute";
|
|
205
242
|
} else if (hasPlan) {
|
|
206
243
|
type = "plan";
|
package/lib/core/judge.js
CHANGED
|
@@ -83,3 +83,75 @@ export async function judgeViolation(ctx, state, sessionId, violation, text) {
|
|
|
83
83
|
return { action: "unavailable", note: `裁决调用失败:${error instanceof Error ? error.message : String(error)}` };
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 批量裁决(0.5.11,用户定稿"频率不高"假设打穿修复):一次回复的全部嫌疑 → **一次** LLM 调用、
|
|
89
|
+
* **一次**预算消耗、对每条嫌疑给独立判定。
|
|
90
|
+
* 背景:此前 deliverSuspects 对每条嫌疑单独 judgeViolation(每条一次 LLM + 一条预算)——长会话
|
|
91
|
+
* 大量嫌疑一天 200+ 次调用,50/日预算早上就满(judge-unavailable 89 条实证);单次调用聚合后,
|
|
92
|
+
* "一次回复一次裁决"才是设计时"裁决频率不高"的真实粒度。
|
|
93
|
+
* @returns {Promise<Array<{violation, verdict}>>} 每条嫌疑对应一个 verdict(deliver/suppress/unavailable)
|
|
94
|
+
*/
|
|
95
|
+
export async function judgeViolationsBatch(ctx, state, sessionId, violations, text) {
|
|
96
|
+
if (!Array.isArray(violations) || violations.length === 0) return [];
|
|
97
|
+
const bKey = judgeBudgetKey(sessionId);
|
|
98
|
+
const budget = state.judgeBudget?.get(bKey) || { count: 0 };
|
|
99
|
+
if (budget.count >= JUDGE_DAILY_LIMIT) {
|
|
100
|
+
return violations.map((v) => ({ violation: v, verdict: { action: "unavailable", note: `裁决日预算已满(${JUDGE_DAILY_LIMIT}/日/会话)` } }));
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const route = await resolveRoute(ctx);
|
|
104
|
+
if (!route) return violations.map((v) => ({ violation: v, verdict: { action: "unavailable", note: "无法解析模型路由(追随会话模型解析失败)" } }));
|
|
105
|
+
// 一次性列出全部嫌疑(编号+规则+提示),一次判定
|
|
106
|
+
const list = violations.map((v, i) => {
|
|
107
|
+
const rawCfg = Array.isArray(state.configs) ? state.configs.find((c) => String(c.ruleId) === String(v.ruleId)) : undefined;
|
|
108
|
+
return [
|
|
109
|
+
`[${i + 1}] 规则 ${v.ruleId}(${v.title})`,
|
|
110
|
+
`检测提示:${String(v.reason || "")}`,
|
|
111
|
+
`规则正文:${rawCfg?.body || "(未提供)"}`
|
|
112
|
+
].join("\n");
|
|
113
|
+
}).join("\n\n");
|
|
114
|
+
const prompt = [
|
|
115
|
+
"你是 DSH 规则裁决器。判断下面这段【执行者回复】是否符合下列【每条规则】。只输出 JSON,不要其他内容。",
|
|
116
|
+
'JSON 格式:{"results": [{"index": 1, "violated": true|false, "note": "一句话理由"}, ...]}',
|
|
117
|
+
"判定要点:",
|
|
118
|
+
"1. 回复若是在表达合规声明(如“我不再/将避免/规则 X 已自证/无重复推销”)或引述规则文本——violated=false;",
|
|
119
|
+
"2. 只有确实发生规则禁止的行为才 violated=true;",
|
|
120
|
+
"3. 拿不准时给出倾向性判断(false 偏不打扰);",
|
|
121
|
+
"4. 每条嫌疑都给独立判定,index 对应下面列号。",
|
|
122
|
+
"嫌疑列表:",
|
|
123
|
+
list,
|
|
124
|
+
"执行者回复:",
|
|
125
|
+
text,
|
|
126
|
+
"只输出 JSON。"
|
|
127
|
+
].join("\n");
|
|
128
|
+
const messages = [{ role: "user", content: [{ type: "text", text: prompt }] }];
|
|
129
|
+
let out = "";
|
|
130
|
+
for await (const chunk of ctx.llm.stream({
|
|
131
|
+
provider: route.provider,
|
|
132
|
+
model: route.model,
|
|
133
|
+
messages,
|
|
134
|
+
maxTokens: 800,
|
|
135
|
+
temperature: 0
|
|
136
|
+
})) {
|
|
137
|
+
if (chunk && chunk.type === "text-delta" && typeof chunk.text === "string") out += chunk.text;
|
|
138
|
+
}
|
|
139
|
+
budget.count += 1;
|
|
140
|
+
state.judgeBudget.set(bKey, budget); // 一次调用 = 一条预算(聚合粒度,而非逐条)
|
|
141
|
+
const parsed = parseJsonOutput(out);
|
|
142
|
+
const results = Array.isArray(parsed?.results) ? parsed.results : [];
|
|
143
|
+
return violations.map((v, i) => {
|
|
144
|
+
const r = results.find((x) => Number(x?.index) === i + 1);
|
|
145
|
+
return {
|
|
146
|
+
violation: v,
|
|
147
|
+
verdict: {
|
|
148
|
+
action: r?.violated === true ? "deliver" : "suppress",
|
|
149
|
+
note: typeof r?.note === "string" ? r.note.slice(0, 120) : "",
|
|
150
|
+
model: route.model
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return violations.map((v) => ({ violation: v, verdict: { action: "unavailable", note: `批量裁决调用失败:${error instanceof Error ? error.message : String(error)}` } }));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// lexicon.js - 规则 22 语义判定的唯一词表源(0.5.11,用户拍板"唯一词表源由引擎维护")。
|
|
2
|
+
// 背景(2026-08-28 全量审查):正文曾手抄词表(L144/148),引擎内部 intent.js 与 authorization.js
|
|
3
|
+
// 又各写一份(许可词在 authorization 出现 3 次:AUTH_WORDS_RE/APPROVAL_RE/APPROVAL_EXEC_RE)——
|
|
4
|
+
// 同语义多处定义 = 漂移根源。本文件 = 五表唯一源:
|
|
5
|
+
// ① 执行许可词表(许可词 + 执行语,两者缺一才不构成许可)
|
|
6
|
+
// ② 执行动作词表(意图/授权判定共用)
|
|
7
|
+
// ③ 疑问词表(问询分点判定)
|
|
8
|
+
// ④ 状态信号词表("我已重启/好了/done"等,状态信号不触发无执行分点)
|
|
9
|
+
// ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"的明细)
|
|
10
|
+
// 调用方(intent.js/authorization.js)一律从这里 import,禁止再定义第二份。
|
|
11
|
+
// 设计原则:词表可以在,但只能有一份;语义变更改这里,正文措辞改 AGENTS.md,二者成对。
|
|
12
|
+
|
|
13
|
+
// ── ① 执行许可词表 ──
|
|
14
|
+
// 许可词:用户确认/同意/批准/授权/可以/允许/好/行/ok/yes 等 + 执行语(即可/现在/马上/…)
|
|
15
|
+
// 组合规则:许可词 [0,16 字符内] 执行语 → 构成执行许可;纯"确认方案/理解/方向"不构成。
|
|
16
|
+
export const APPROVAL_WORDS_RE = /(?:确认|同意|批准|授权|可以|允许|好|行|ok|yes)/i;
|
|
17
|
+
export const EXEC_FOLLOW_RE = /(?:即可|现在|马上|继续|开始|执行|落盘|去做|实施|进行|删除|修改|改|补|修|做|写入|创建|安装|发布|下载|提交|运行)/i;
|
|
18
|
+
export const APPROVAL_EXEC_RE = new RegExp(
|
|
19
|
+
APPROVAL_WORDS_RE.source.replace(/\\\//g, "/") +
|
|
20
|
+
"[^\\n]{0,16}" +
|
|
21
|
+
EXEC_FOLLOW_RE.source.replace(/\\\//g, "/"),
|
|
22
|
+
"i"
|
|
23
|
+
);
|
|
24
|
+
// 纯方案/理解/方向确认(不构成执行授权)
|
|
25
|
+
export const PLAN_ONLY_RE = /确认方案|确认理解|确认方向|确认无误|理解了|明白方向|了解|没问题|收到/i;
|
|
26
|
+
|
|
27
|
+
// ── ② 执行动作词表(intent.ACTION_RE / authorization.EXEC_ACTION_RE 的合并唯一源)──
|
|
28
|
+
// 注意:单一表同时服务"意图判定"(这句话是不是执行分点)与"ask 结果批准判定"(选择=批准),
|
|
29
|
+
// 语义一致,不另分裂;新增动作词只改这里。
|
|
30
|
+
export const ACTION_WORDS_RE =
|
|
31
|
+
/执行|跑|落|落盘|认可|开始|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|安装|发布|下载|提交|运行|修复|替换|重建|启动|重启|停止|卸载|开始改|开始写|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|继续|推进|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|开工|动手|进行|完成|选择|采纳|选定|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:出|一下|一遍|完|出来)?|展示|打开|提取|还原|导入|导出|清理|移除|清空|残留/i;
|
|
32
|
+
|
|
33
|
+
// 强执行语(直接执行指令,不等授权确认——用于意图置信提升)
|
|
34
|
+
export const STRONG_EXEC_RE =
|
|
35
|
+
/执行吧|开始执行|直接执行|立即执行|现在执行|马上执行|请落盘|开始写|开始改|执行以下|执行第|执行这个|执行它|落盘|开始蒸馏|开始迁移|开始修复|开始改进|开始优化|采纳|选定/i;
|
|
36
|
+
|
|
37
|
+
// 指令式动作词(用户命令式指令 = 视为授权执行;与批准词不同源、用途不同)
|
|
38
|
+
export const DIRECTIVE_WORDS_RE =
|
|
39
|
+
/(?:请\s*)?(?:删除|移除|修改|编辑|替换|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|创建|复制|移动|执行|运行|下载|提交|推送|备份|安装|卸载|清理|启动|停止|重建|修复|改(?:为|成|动|一下)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|实施|推进|继续|处理|解决)/i;
|
|
40
|
+
|
|
41
|
+
// 拒绝词(授权/批准判定用)
|
|
42
|
+
export const REJECTION_WORDS_RE = /(?:不允许|拒绝|不要|不同意|取消(?:这次|并停)?|(?:^|[^是])否(?:[^则]|$)|no(?!tes?|thing|pe)\b|\bno\b|No)/i;
|
|
43
|
+
|
|
44
|
+
// ── ③ 疑问词表(问询分点判定;authorization.isQuestionMessage 同源)──
|
|
45
|
+
export const QUESTION_WORDS_RE =
|
|
46
|
+
/[??]|吗|为什么|是否|能不能|行不行|难道|是不是|能否|怎么|为何|是否要|影响吗|可以吗|要不要|需不需要/;
|
|
47
|
+
|
|
48
|
+
// ── ④ 状态信号词表("我已重启/已输入/好了/done";触发时不判"无执行分点")──
|
|
49
|
+
export const STATUS_SIGNAL_WORDS_RE =
|
|
50
|
+
/(?:我已|我已经|我(?:们)?(?:刚)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好))(?:了|完毕|完成|好)?|(?:^|\s)(?:已|已经)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好|unlock)(?:了|完毕|完成|好)?(?:\s|$)|(?:^|\s)已经(?:刚)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好|unlock)[^\s]*(?:\s|$)|(?:^|\s)(?:重启|输入|命令|操作)(?:完成|完毕|好了|已输入)(?:\s|$)|(?:^|\s)好了(?:\s|$)|^(?:done|restarted|finished|entered)$/i;
|
|
51
|
+
// 危险动作词:命中则不当作状态信号(防"我已删除了某文件"被当成就绪确认)
|
|
52
|
+
export const DANGEROUS_ACTION_WORDS_RE = /删除|覆盖|移除|清空|卸载|remove-item|rm\s+-r|del\s+/i;
|
|
53
|
+
|
|
54
|
+
// ── ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"明细)──
|
|
55
|
+
// 只读/展示/询问/产物类工具在"无执行分点"回合也可调用(引擎 tool-catalog 的 analysis/artifact 分类
|
|
56
|
+
// 为总开关;本清单 = 规则正文"等豁免"所指的可枚举集,与 tool-catalog 一致,此处集中注释成对)。
|
|
57
|
+
export const EXEMPT_TOOL_NAMES = new Set([
|
|
58
|
+
// 只读/查询
|
|
59
|
+
"read", "grep", "glob", "read_image",
|
|
60
|
+
// 视觉分析
|
|
61
|
+
"vision_describe", "vision_detect", "vision_ground", "vision_colors", "vision_ocr", "vision_bootstrap",
|
|
62
|
+
// 状态查询
|
|
63
|
+
"job_list", "job_output", "list_agents", "get_goal", "schedule_list",
|
|
64
|
+
"engram_recall", "engram_detail", "engram_model",
|
|
65
|
+
"esr_status", "esr_ready", "esr_model",
|
|
66
|
+
// 网络只读
|
|
67
|
+
"web_fetch", "web_search",
|
|
68
|
+
// 询问/展示/产物
|
|
69
|
+
"ask_user_question", "todo_write", "visualize", "report",
|
|
70
|
+
// inspect 只读
|
|
71
|
+
"cordis_inspect_list", "cordis_inspect_query", "cordis_inspect_self"
|
|
72
|
+
]);
|
package/lib/core/llm-intent.js
CHANGED
|
@@ -9,10 +9,12 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
import { parseUserIntents, shouldDenyMutation } from "./intent.js";
|
|
10
10
|
import { resolveRoute, callLlm } from "./llm-understander.js";
|
|
11
11
|
import { audit } from "./audit.js";
|
|
12
|
+
import { learnHotword } from "./hotwords.js";
|
|
13
|
+
import { ACTION_WORDS_RE } from "./lexicon.js";
|
|
12
14
|
|
|
13
15
|
const LLM_PROMPT = [
|
|
14
16
|
"你是 DSH 规则引擎的意图判定器。输出严格 JSON,不要输出其他内容:",
|
|
15
|
-
'{"hasExecute":bool,"hasPlan":bool,"hasQuestion":bool,"ambiguous":bool,"reason":"一句话","confidence":0-1}',
|
|
17
|
+
'{"hasExecute":bool,"hasPlan":bool,"hasQuestion":bool,"ambiguous":bool,"reason":"一句话","confidence":0-1,"actionWord":"句中明确动作词(2-6字,如:通读、校对、梳理;无明确动作词则空串)"}',
|
|
16
18
|
"规则:hasExecute=消息含明确执行指令(动作词+非疑问句);hasQuestion=含疑问/征询;hasPlan=要方案/评估/设计;ambiguous=同一消息内标签或语气冲突(如【执行】标签+疑问正文);confidence=你的把握。",
|
|
17
19
|
"待判定用户消息:{{TEXT}}"
|
|
18
20
|
].join("\n");
|
|
@@ -99,8 +101,14 @@ export async function enrichIntentWithLlm(ctx, state, sid, text, cfg) {
|
|
|
99
101
|
hasQuestion: Boolean(out.hasQuestion),
|
|
100
102
|
ambiguous: Boolean(out.ambiguous),
|
|
101
103
|
reason: String(out.reason || "").slice(0, 120),
|
|
102
|
-
confidence: Number(out.confidence) || 0
|
|
104
|
+
confidence: Number(out.confidence) || 0,
|
|
105
|
+
actionWord: String(out.actionWord || "")
|
|
103
106
|
};
|
|
107
|
+
// B1(2026-08-29 方案甲):LLM 明确判 execute 且词表未命中 → 学动作词入热词表
|
|
108
|
+
//(下次同词词表直接命中,不再依赖 LLM;学习失败静默,不影响主流程)
|
|
109
|
+
if (verdict.hasExecute && verdict.actionWord && !ACTION_WORDS_RE.test(text)) {
|
|
110
|
+
learnHotword(verdict.actionWord);
|
|
111
|
+
}
|
|
104
112
|
const ms = Date.now() - started;
|
|
105
113
|
session.turn.llmIntent = { raw: text, verdict, at: Date.now(), ms };
|
|
106
114
|
session.turn.intentState = "llm-ready";
|
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
export async function resolveRoute(ctx) {
|
|
6
6
|
if (!ctx?.llm) return null;
|
|
7
7
|
try {
|
|
8
|
+
// 0.5.11(用户定稿,2026-08-28):后台判定(judge/llm-intent/规则理解)必须用
|
|
9
|
+
// **当前会话模型**,而不是"模型列表第一个"(此前列表第一=dsf 基础模型,与主会话
|
|
10
|
+
// dsfve 能力不一致——裁决结果与主会话判断脱节)。
|
|
11
|
+
// 优先级:① 官方当前模型选择(agentDefaultModel.currentSelection:provider+model)
|
|
12
|
+
// ② 进程环境变量(DSH_LLM_MODEL/DSH_LLM_PROVIDER,显式覆盖)
|
|
13
|
+
// ③ 模型列表第一个(原兜底)
|
|
14
|
+
try {
|
|
15
|
+
const sel = ctx.get?.("agentDefaultModel")?.currentSelection?.();
|
|
16
|
+
if (sel && typeof sel.model === "string" && sel.model.length > 0) {
|
|
17
|
+
const provider = sel.provider || process.env.DSH_LLM_PROVIDER || "";
|
|
18
|
+
if (provider) return { provider, model: sel.model };
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
// currentSelection 失败 → 继续走兜底
|
|
22
|
+
}
|
|
8
23
|
const providers = ctx.llm.listProviders?.() || [];
|
|
9
24
|
if (providers.length === 0) return null;
|
|
10
25
|
// v0.5.7 修复(实弹抓到,2026-08-26):必须用 provider 的 **id**(如 "deepseek-official")调
|
package/lib/core/patterns.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// patterns.js - 规则引擎共享的正则与常量。
|
|
2
2
|
// 注意:本文件只放可测试的纯函数/常量,不依赖 Cordis。
|
|
3
3
|
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { isAbsolute } from "node:path";
|
|
6
|
+
|
|
4
7
|
export const INLINE_CMD =
|
|
5
8
|
/\b(?:node|pwsh|powershell)\s+(?:-[ep]|--eval|--print|-Command|-c)\b/i;
|
|
6
9
|
|
|
@@ -28,6 +31,10 @@ export const DATA_DIR_RE =
|
|
|
28
31
|
export const TIME_WORDS =
|
|
29
32
|
/今天|昨天|前天|上周|本周|刚才|\d+\s*分钟前|\d{1,2}\s*月\s*\d{1,2}\s*日|\d{4}\s*年\s*\d{1,2}\s*月\s*\d{1,2}\s*日/;
|
|
30
33
|
|
|
34
|
+
/** 事件时间证据标注(规则 2②:过去事件时间必须绑定事件自身证据;"之前/当时"等模糊词不触发检测、不采信) */
|
|
35
|
+
export const EVIDENCE_MARK_RE =
|
|
36
|
+
/日志\s*(?:ts|时间戳)|mtime|启动时间|进程\s*StartTime|文件\s*修改时间|来源[::]|ts\s*[:=]|Get-Date\s*输出|事件时间已核实|【时间未核实】/i;
|
|
37
|
+
|
|
31
38
|
export const PROMISE_WORDS =
|
|
32
39
|
/包在我身上|肯定能|绝对没问题|保证(?!不|无法)|一定可以|放心(?:,|,)?肯定|万无一失/;
|
|
33
40
|
|
|
@@ -237,9 +244,33 @@ export function isGetDateCommand(command) {
|
|
|
237
244
|
|
|
238
245
|
const READ_ONLY_TOOLS = new Set(["read", "grep", "glob", "read_image"]);
|
|
239
246
|
const READONLY_CMD_RE =
|
|
240
|
-
/(?:\b(?:Get-Content|Get-ChildItem|Get-Item|Get-Command|Get-Date|Select-String|Find-String|Test-Path|Get-Process|Get-Service|cat|type|dir|ls|grep|findstr|more|netstat|where|echo|Write-Output|Write-Host|Get-Location|pwd|cwd|Get-Help|help|Select-Object|Out-String|Format-Table|Format-List|Format-Wide|Measure-Object|Sort-Object|Where-Object|Group-Object|Out-Null|ConvertTo-Json|ConvertFrom-Json)\b)|(?:\bgit\s+(?:-[^\s]+\s+[^\s]+\s+|--[^\s]+\s+)*\b(?:status|log|diff|show|branch|tag|remote|rev-parse|diff-tree|ls-files)(?:\s|$))|(?:\bdsh\s+(?:--version|--help|--dump-config|plugin\s+(?:list|show|status))(?:\s|$))|(?:\bdsh\s+--profile\s+[^\s]+\s+(?:--dump-config|plugin\s+(?:list|show|status))(?:\s|$))|(?:\bnode\s+(?:--check|--test|--version)(?:\s|$))|(?:\bnpm\s+(?:test|run\s+test)(?:\s|$))|(?:\bpnpm\s+(?:test|run\s+test)(?:\s|$))|(?:\bgh\s+(?:auth\s+status|repo\s+view)(?:\s|$))|(?:\bnpm\s+(?:ls|view)(?:\s|$))/i;
|
|
247
|
+
/(?:\b(?:Get-Content|Get-ChildItem|Get-Item|Get-Command|Get-Date|Select-String|Find-String|Test-Path|Get-Process|Get-Service|Get-ItemProperty|Get-Variable|Get-FileHash|cat|type|dir|ls|grep|findstr|more|netstat|where|echo|Write-Output|Write-Host|Get-Location|pwd|cwd|Get-Help|help|Select-Object|Out-String|Format-Table|Format-List|Format-Wide|Measure-Object|Sort-Object|Where-Object|Group-Object|ForEach-Object|Out-Null|ConvertTo-Json|ConvertFrom-Json|Join-Path|Split-Path|Resolve-Path|New-Object|Add-Type|Get-Member|Get-ChildItemProperty|Get-CimInstance|Get-WmiObject)\b)|(?:\bgit\s+(?:-[^\s]+\s+[^\s]+\s+|--[^\s]+\s+)*\b(?:status|log|diff|show|branch|tag|remote|rev-parse|diff-tree|ls-files)(?:\s|$))|(?:\bdsh\s+(?:--version|--help|--dump-config|plugin\s+(?:list|show|status))(?:\s|$))|(?:\bdsh\s+--profile\s+[^\s]+\s+(?:--dump-config|plugin\s+(?:list|show|status))(?:\s|$))|(?:\bnode\s+(?:--check|--test|--version)(?:\s|$))|(?:\bnpm\s+(?:test|run\s+test)(?:\s|$))|(?:\bpnpm\s+(?:test|run\s+test)(?:\s|$))|(?:\bgh\s+(?:auth\s+status|repo\s+view)(?:\s|$))|(?:\bnpm\s+(?:ls|view)(?:\s|$))/i;
|
|
241
248
|
const MUTATING_CMD_RE =
|
|
242
|
-
/(?:Set-Content|Add-Content|Out-File|Remove-Item|Move-Item|Copy-Item|Rename-Item|New-Item|Clear-Content|git\s+(?:push|commit)|rm\s+-r|rmdir\s+\/s|del\s+\/s|(?:^|[^0-9])>>|(?:^|[^0-9])
|
|
249
|
+
/(?:Set-Content|Add-Content|Out-File|Tee-Object|Remove-Item|Move-Item|Copy-Item|Rename-Item|New-Item|Clear-Content|Start-Process|Invoke-Expression|Invoke-Item|Export-Csv|Export-Clixml|Export-ModuleMember|Compress-Archive|Expand-Archive|Add-Type\s+-Output(?:Assembly|Type)|git\s+(?:push|commit)|rm\s+-r|rmdir\s+\/s|del\s+\/s|(?:^|[^0-9])>>|(?:^|[^0-9])>|(?:curl|wget|iwr|Invoke-WebRequest|Invoke-RestMethod)\s+(?=[^\n;|]*\s(?:-o|--output|-OutFile|OutFile)\s))/i;
|
|
250
|
+
// .NET 静态/实例写方法(MUTATING 补充:Set-Content 等 cmdlet 之外的写路径)
|
|
251
|
+
const MUTATING_METHOD_RE =
|
|
252
|
+
/(?:\[[\w.]+\]::\s*(?:Write|Set|Remove|Delete|Move|Copy|Create|Save|Append)\w*\s*\(|\.(?:Write(?:AllText|Line|Bytes|Text)?|SetContent|MoveTo?|CopyTo|Delete|Remove|SaveAs?2?\s*\(|CreateDirectory|CreateFile|WriteLine)\s*\()/i;
|
|
253
|
+
// B1(阶段一,2026-08-28):只读豁免——白名单之外的"分明无害段"(纯变量赋值 / COM 只读打开 /
|
|
254
|
+
// 内容属性读取 / 变量引出),让读取 .doc 的 COM 命令(Open(...,$true)+Content.Text)不再误判写。
|
|
255
|
+
// 保守边界:内容属性赋值($doc.Text=... / $doc.Content.Text=...)是写文档,不豁免。
|
|
256
|
+
const COM_CONTENT_ASSIGN_RE = /\.(?:Text|Content|Value|Range|Paragraphs|Sections|Documents|Document)\s*=/i;
|
|
257
|
+
const ALLOW_SEG_RE =
|
|
258
|
+
/(?:\$[A-Za-z_][\w$]*(?:(?:\.[\w$]+)|(?:\[[^\]]+\]))*\s*=)|(?:\$[\w$.]+\.(?:Content|Range|Text|Value|Paragraphs|Sections|Length|Count|Name|Selected)\b)|(?:\$[\w$.]+$)|(?:\b(?:Documents|Document)\.(?:Open|Item)\s*\([^)]*,\s*\d+\s*,\s*\$?true\s*\))/;
|
|
259
|
+
// 剥离 try/catch/finally 控制块:块内语句与主语句同样接受只读判定(Try-* 不是 cmdlet)。
|
|
260
|
+
// 注意:不做全量 [{}] 替换——if/else 子表达式($(if(...){...}else{...}))会被切碎误判。
|
|
261
|
+
function stripControlBlocks(command) {
|
|
262
|
+
return String(command)
|
|
263
|
+
.replace(/try\s*\{([\s\S]*?)\}\s*catch\s*\([^)]*\)\s*\{([\s\S]*?)\}/gi, "$1; $2")
|
|
264
|
+
.replace(/try\s*\{([\s\S]*?)\}\s*catch\s*\{([\s\S]*?)\}/gi, "$1; $2")
|
|
265
|
+
.replace(/try\s*\{([\s\S]*?)\}\s*finally\s*\{([\s\S]*?)\}/gi, "$1; $2")
|
|
266
|
+
.replace(/^[\s]*(?:try|catch(?:\([^)]*\))?|finally)\s*\{[^\S\n]*(?:#.*)?\s*$/gm, ";")
|
|
267
|
+
.replace(/^\s*\}\s*(?:catch(?:\([^)]*\))?|finally)?\s*\{?\s*$/gm, ";");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 写/删除/提交类命令判定(0.5.10 单真源:guard-core self-protect 等全部复用——修复 Write-Output 被 `write` 子串误杀) */
|
|
271
|
+
export function isMutationCommand(command) {
|
|
272
|
+
return typeof command === "string" && MUTATING_CMD_RE.test(command);
|
|
273
|
+
}
|
|
243
274
|
|
|
244
275
|
/** 只读命令链按分隔符折段(机制 B,2026-08-24):`; 换行 && || |`(管道前后是命令链;`2>&1`/`2>$null` 的 stderr 重定向不拆分) */
|
|
245
276
|
function splitCommandSegments(command) {
|
|
@@ -251,14 +282,25 @@ function splitCommandSegments(command) {
|
|
|
251
282
|
|
|
252
283
|
/** 判断命令文本是否只读(读文件/查询类,无写入/删除/提交副作用)。
|
|
253
284
|
* 2026-08-24 分段化:整条命令按 ; | & 换行拆段,每段都必须只读——
|
|
254
|
-
* 修复 "git -C <dir> status" 变体不匹配、"git status; echo ok" 链式被误判为写 的实测缺陷。
|
|
285
|
+
* 修复 "git -C <dir> status" 变体不匹配、"git status; echo ok" 链式被误判为写 的实测缺陷。
|
|
286
|
+
* B1(2026-08-28 阶段一):段判定三档化,修复只读豁免失效(读取 .doc 的 COM 命令被判写):
|
|
287
|
+
* ① 写特征(MUTATING_CMD_RE / MUTATING_METHOD_RE / 内容属性赋值)→ 非只读;
|
|
288
|
+
* ② 白名单命令 / 分明无害段(纯变量赋值、COM 只读打开、内容属性读取、变量引出)→ 只读;
|
|
289
|
+
* ③ 其它不认识的段 → 保守判非只读(不放大白名单,宁紧勿松)。 */
|
|
255
290
|
export function isReadOnlyCommand(command) {
|
|
256
291
|
if (typeof command !== "string") return false;
|
|
257
|
-
const segments = splitCommandSegments(command);
|
|
292
|
+
const segments = splitCommandSegments(stripControlBlocks(command));
|
|
258
293
|
if (segments.length === 0) return false;
|
|
259
294
|
return segments.every((seg) => {
|
|
260
|
-
|
|
261
|
-
|
|
295
|
+
const s = String(seg).trim();
|
|
296
|
+
if (!s) return true; // 剥离残留的空段放行
|
|
297
|
+
if (s.startsWith("#")) return true;
|
|
298
|
+
if (MUTATING_CMD_RE.test(s)) return false;
|
|
299
|
+
if (MUTATING_METHOD_RE.test(s)) return false;
|
|
300
|
+
if (COM_CONTENT_ASSIGN_RE.test(s)) return false; // $doc.Text=... 是写文档
|
|
301
|
+
if (READONLY_CMD_RE.test(s)) return true;
|
|
302
|
+
if (ALLOW_SEG_RE.test(s)) return true;
|
|
303
|
+
return false; // 看不清的段:保守按非只读处理
|
|
262
304
|
});
|
|
263
305
|
}
|
|
264
306
|
|
|
@@ -273,6 +315,88 @@ export function isReadOnlyTool(toolName, args) {
|
|
|
273
315
|
return false;
|
|
274
316
|
}
|
|
275
317
|
|
|
318
|
+
// ═══════════ 0.5.10 分析通道(用户多次提出"只读分析需要写临时脚本/输出")═══════════
|
|
319
|
+
// 0.5.11(用户定稿):移除"repo scripts/*.mjs 一律=分析"的语义豁免——脚本名判不了脚本干什么,
|
|
320
|
+
// 执行类脚本(release/inject/uninstall 等)会借道放行(执行口径漏洞)。语义判给模型;
|
|
321
|
+
// 本通道只保留物理判据:严格只读(isReadOnlyTool)+ 分析临时区写(logs/.analysis-tmp/.backups
|
|
322
|
+
// 按路径段机械判定)+ 下载到临时区。红线由 guard-core 追加(工作区外/受保护名/覆盖既有)。
|
|
323
|
+
const ANALYSIS_SCRATCH_SEG_RE = /(?:^|[\\/])(?:logs|\.analysis-tmp|\.backups)[\\/]/i;
|
|
324
|
+
// 0.5.10 建议:远程下载到临时区(调研场景 curl/iwr/wget -o 临时区 → analysis;12C 联网审查照旧)
|
|
325
|
+
// 目标支持引号包裹(含空格路径)+ 裸 token
|
|
326
|
+
const DOWNLOAD_OUT_RE = /(?:curl|wget|iwr|Invoke-WebRequest)(?:(?!\n)[^&;|])*?(?:-o\s+|OutFile\s+|>\s*)\s*(?:'([^']+)'|"([^"]+)"|([^\s"'&;|]+))/i;
|
|
327
|
+
|
|
328
|
+
/** 是否分析临时区路径(logs/.analysis-tmp/.backups 段;工作区外红线由 guard-core 用 isOutsideWorkspace 把关) */
|
|
329
|
+
export function isAnalysisScratchPath(p) {
|
|
330
|
+
if (typeof p !== "string" || !p) return false;
|
|
331
|
+
return ANALYSIS_SCRATCH_SEG_RE.test(p.replace(/\\/g, "/"));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** 提取命令文本中命中分析临时区的路径 token(保守:仅取带临时区特征的片段) */
|
|
335
|
+
export function extractAnalysisScratchPaths(command) {
|
|
336
|
+
const n = String(command || "");
|
|
337
|
+
const out = [];
|
|
338
|
+
const re = /(?:logs|\.analysis-tmp|\.backups)[\\/][^\s"'&|;]+/gi;
|
|
339
|
+
let m;
|
|
340
|
+
while ((m = re.exec(n))) out.push(m[0].replace(/\\/g, "/"));
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 分析类操作判定(0.5.10 单真源;0.5.11 修订):true = 可按 analysis 放行(无副作用,或副作用仅限分析临时区)。
|
|
346
|
+
* 场景:① 严格只读(isReadOnlyTool);②(已移出,0.5.11)脚本区调用不再按"目录=分析"放行——
|
|
347
|
+
* 脚本名判不了脚本行为,执行类脚本(release/inject/uninject 等)曾借道放行;语义判给模型;
|
|
348
|
+
* ③ 临时区写(命令含写特征且提取到的目标路径全部命中分析临时区,且无受保护文件名)。
|
|
349
|
+
* 红线(guard-core 追加,本函数不含):工作区外路径(isOutsideWorkspace)、受保护名、覆盖既有正式文件。
|
|
350
|
+
*/
|
|
351
|
+
export function isAnalysisOp(toolName, args) {
|
|
352
|
+
const name = String(toolName || "");
|
|
353
|
+
if (isReadOnlyTool(name, args)) return true;
|
|
354
|
+
const cmd = commandText(args) || "";
|
|
355
|
+
// A 方案(2026-08-28):write/edit 工具直接写分析临时区(logs/.analysis-tmp/.backups 路径段)
|
|
356
|
+
// → 分析通道放行(方案回合"写临时脚本/产物"是调研工具,不是落盘;0.5.10 只覆盖 pwsh 命令,
|
|
357
|
+
// write 工具写临时区此前误判正式路径 → A 拦截误伤调研工具)。红线同 pwsh:受保护名不豁免。
|
|
358
|
+
if (name === "write" || (name === "str_replace_editor" && args?.command !== "view")) {
|
|
359
|
+
const p = pathTarget(args);
|
|
360
|
+
if (p && isAnalysisScratchPath(p) && !PROTECTED_FILENAME_RE.test(p)) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (name === "node" || name === "pwsh" || name === "bash") {
|
|
365
|
+
// ③ 临时区写:写命令(不含删除/移动——删除/移动属 13A 红线,不走分析豁免)+ 提取路径全部命中临时区 + 无受保护名
|
|
366
|
+
if (isMutationCommand(cmd) && !/(?:Remove-Item|Move-Item|Rename-Item|del\s|rm\s|git\s+push|git\s+commit)/i.test(cmd)) {
|
|
367
|
+
const paths = extractAnalysisScratchPaths(cmd);
|
|
368
|
+
if (paths.length > 0 && paths.every((p) => isAnalysisScratchPath(p)) && !PROTECTED_FILENAME_RE.test(cmd)) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// ④ 远程下载到分析临时区(调研场景:下载 tgz/网页到临时区再解压查看;12C 联网审批与内网审查照旧)
|
|
373
|
+
const dl = cmd.match(DOWNLOAD_OUT_RE);
|
|
374
|
+
const dlTarget = dl ? (dl[1] || dl[2] || dl[3] || "") : "";
|
|
375
|
+
if (dlTarget && isAnalysisScratchPath(dlTarget) && !PROTECTED_FILENAME_RE.test(cmd)) {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ═══════════ 0.5.10 已知环境坑特征表(建议5——错误码自动召回,K-01/K-05 依据)═══════════
|
|
383
|
+
// 工具/命令错误文本命中特征 → 审计 error-hint + 注入指向知识库的提示(低频:仅命中才提示;语义=指路不打扰)。
|
|
384
|
+
const KNOWN_PITFALLS = [
|
|
385
|
+
{ key: "SEC_E_NO_CREDENTIALS", re: /SEC_E_NO_CREDENTIALS/i, hint: "TLS 凭据失败(常见于沙箱/受限会话)——先查手册踩坑 60/89;Node fetch 配 NODE_USE_ENV_PROXY 可绕" },
|
|
386
|
+
{ key: "proxy-7890-refused", re: /ECONNREFUSED[^\n]*7890/i, hint: "代理端口 7890 无监听——确认代理软件已启动;git/gh 可直连,npm registry 直连本机不可用(发布脚本默认直连,DSH_RELEASE_PROXY 可指定)" },
|
|
387
|
+
{ key: "sandbox-pipe-EPERM", re: /EPERM/i, hint: "受限沙箱下子进程管道受限(踩坑 22⑤)——需 danger-full-access 运行" },
|
|
388
|
+
{ key: "ERR_MODULE_NOT_FOUND", re: /ERR_MODULE_NOT_FOUND/i, hint: "模块缺失——检查依赖闭包/空壳目录(踩坑 54)" }
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
/** 已知坑特征匹配(纯函数):错误文本命中特征表 → 返回条目;未命中 → null */
|
|
392
|
+
export function matchKnownPitfall(text) {
|
|
393
|
+
const t = String(text || "");
|
|
394
|
+
for (const p of KNOWN_PITFALLS) if (p.re.test(t)) return p;
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export { KNOWN_PITFALLS };
|
|
399
|
+
|
|
276
400
|
/**
|
|
277
401
|
* node -e 内联代码的危险特征判定(机制批 M4:diag-run 受控诊断工具使用)。
|
|
278
402
|
* 黑名单命中任一 = 判定有写/网络/进程副作用(保守:宁可拒)。规则 9 正文不动(A2=B 方案)。
|
|
@@ -461,6 +585,13 @@ export function isLowRiskWorkspaceNew(toolName, args) {
|
|
|
461
585
|
if (isOutsideWorkspace(p)) return false;
|
|
462
586
|
if (isAssemblyMutationTool(name, args)) return false;
|
|
463
587
|
if (isHighRiskEntryFile(p)) return false;
|
|
588
|
+
// 0.5.11(用户定稿):豁免原文是"工作区内低风险**新建**"——"新建"=目标尚不存在。
|
|
589
|
+
// 已存在的文件无论编辑还是覆盖写入都算变更(改旧文件),不是新建 → 不豁免,走正常授权判定。
|
|
590
|
+
// 相对路径无法判定存在性 → 保守不豁免(宁要授权,不放行未知目标的覆盖)。
|
|
591
|
+
if (p) {
|
|
592
|
+
if (!isAbsolute(p)) return false;
|
|
593
|
+
if (existsSync(p)) return false;
|
|
594
|
+
}
|
|
464
595
|
return true;
|
|
465
596
|
}
|
|
466
597
|
|
|
@@ -516,12 +647,24 @@ export function isNegatingSuggestion(text) {
|
|
|
516
647
|
}
|
|
517
648
|
|
|
518
649
|
/**
|
|
519
|
-
*
|
|
520
|
-
*
|
|
650
|
+
* 通用引述/转述语境判定(2026-08-29 B3,取代 isPromiseQuoteContext 单一用途):
|
|
651
|
+
* 给定词表 wordRe 每次命中,处于 ① 引号包裹(词两侧 8 字符内配对引号)或
|
|
652
|
+
* ② 转述/改写语境 → 视为引述而非断言(规则 2 时间词 / 规则 7 承诺词通用)。
|
|
653
|
+
* 转述语境 = 第三人称对象 + 说/表示/提到…(before 14 字符内),或引述/转述/原文/改成类标记。
|
|
654
|
+
* 不豁免第一人称("我说保证"仍按承诺——承诺转述不了自己)。
|
|
655
|
+
* 修复背景:B3 规则 7 仅认引号+白名单标记词,无引号转述("用户之前说万无一失")误报;
|
|
656
|
+
* 规则 2 原本无引述豁免(引述用户原话"昨天"也报未核对)。
|
|
521
657
|
*/
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
658
|
+
const PARAPHRASE_BEFORE_RE =
|
|
659
|
+
/(?:用户|你|他|她|它|对方|作者|维护者|客户|大家|网友|某人|群友|别人).{0,6}?(?:说|说过|表示|提到|提到过|称|强调|认为|写道|原话)/;
|
|
660
|
+
const PARAPHRASE_MARK_BEFORE_RE = /引述|引用|原文|转述|转发|据\S{0,5}说|写过|改成|改为|换成/;
|
|
661
|
+
const PARAPHRASE_MARK_AFTER_RE = /改成|改为|换成|转述|引用/;
|
|
662
|
+
|
|
663
|
+
export function isQuoteOrParaphraseContext(text, wordRe) {
|
|
664
|
+
if (typeof text !== "string" || !(wordRe instanceof RegExp)) return false;
|
|
665
|
+
// matchAll 要求全局正则;用副本(不污染共享词表的 lastIndex)
|
|
666
|
+
const re = new RegExp(wordRe.source, wordRe.flags.includes("g") ? wordRe.flags : wordRe.flags + "g");
|
|
667
|
+
for (const m of text.matchAll(re)) {
|
|
525
668
|
const before = text.slice(0, m.index);
|
|
526
669
|
const after = text.slice(m.index + m[0].length);
|
|
527
670
|
// 引述包装 = 承诺词两侧 8 字符内都有引号(ASCII/中文弯引号均算;不区分左右——
|
|
@@ -529,7 +672,14 @@ export function isPromiseQuoteContext(text) {
|
|
|
529
672
|
const openNear = /[“"「『‘'””」』’]/.test(before.slice(-8));
|
|
530
673
|
const closeNear = /[“"「『‘'””」』’]/.test(after.slice(0, 8));
|
|
531
674
|
if (openNear && closeNear) return true;
|
|
532
|
-
if (
|
|
675
|
+
if (PARAPHRASE_BEFORE_RE.test(before.slice(-14))) return true;
|
|
676
|
+
if (PARAPHRASE_MARK_BEFORE_RE.test(before.slice(-12))) return true;
|
|
677
|
+
if (PARAPHRASE_MARK_AFTER_RE.test(after.slice(0, 12))) return true;
|
|
533
678
|
}
|
|
534
679
|
return false;
|
|
535
680
|
}
|
|
681
|
+
|
|
682
|
+
/** v0.5.7 P0.5-5(用户拍板)+ B3 扩展:承诺词处于引述/改写/转述语境 → 是引述不是承诺 */
|
|
683
|
+
export function isPromiseQuoteContext(text) {
|
|
684
|
+
return isQuoteOrParaphraseContext(text, PROMISE_WORDS);
|
|
685
|
+
}
|