dsh-rule-engine 0.3.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/LICENSE +21 -0
- package/README.md +132 -0
- package/cordis.patch.yml +7 -0
- package/lib/core/audit.js +44 -0
- package/lib/core/authorization.js +227 -0
- package/lib/core/config.js +30 -0
- package/lib/core/guard-core.js +453 -0
- package/lib/core/llm-understander.js +104 -0
- package/lib/core/matcher.js +50 -0
- package/lib/core/mount-signature.js +87 -0
- package/lib/core/parser.js +101 -0
- package/lib/core/paths.js +24 -0
- package/lib/core/patterns.js +351 -0
- package/lib/core/runtime.js +5 -0
- package/lib/core/silent-error.js +44 -0
- package/lib/core/state.js +185 -0
- package/lib/core/text-detect.js +185 -0
- package/lib/core/understander.js +127 -0
- package/lib/core/understanding-store.js +27 -0
- package/lib/core/version-guard.js +117 -0
- package/lib/index.js +648 -0
- package/lib/service.js +174 -0
- package/package.json +48 -0
- package/scripts/audit-mount-consistency.mjs +198 -0
- package/scripts/build.sh +13 -0
- package/scripts/check-real.mjs +24 -0
- package/upgrade-impact.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-rule-engine contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# dsh-rule-engine
|
|
2
|
+
|
|
3
|
+
DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯一真相源,自动解析规则四要素与执行等级,再通过「工具守卫 + 文本检测 + 时序检查 + 审计台账」执行用户规则,而不是内置一套与用户无关的安全清单。
|
|
4
|
+
|
|
5
|
+
## 项目背景
|
|
6
|
+
|
|
7
|
+
这个项目来自一个非常具体的个人需求:
|
|
8
|
+
|
|
9
|
+
- 作者是**零编程基础**用户,但极其重视规则的制定、执行、遵守与复盘。
|
|
10
|
+
- 作者发现:规则如果只写在文本里、靠模型“自觉”执行,会反复失效(例如时间词写错、内联命令违规、交付前漏验证等)。
|
|
11
|
+
- 因此核心思路是:**规则的执行不能只靠自觉,要尽量靠插件在机制层强制**。
|
|
12
|
+
- 本插件所有规则均从 `AGENTS.md` 动态解析,规则增删改后无需重写插件。
|
|
13
|
+
|
|
14
|
+
当前实现基于已有的 `AGENTS.md` 规则体系拓展,社区暂无类似插件供参考(大概率为该等约束可能限制开发自由性,不适用于专业编程人员),**可能存在大量不完备、误判或边界问题**。欢迎任何使用者提出调整建议、提交 issue 或 PR。项目仍处于“可运行但需要持续打磨”的阶段。
|
|
15
|
+
|
|
16
|
+
## 功能分层
|
|
17
|
+
|
|
18
|
+
- 阶段 1 容器:解析 AGENTS.md 全部规则 → 理解产物(`rule-understanding.json` 可生成)
|
|
19
|
+
- 阶段 2 匹配机 + 工具守卫 + 文本检测
|
|
20
|
+
- 阶段 3 时序检查 + 授权询问集成
|
|
21
|
+
- 阶段 4 D 级自证调度 + `/guard` 命令完善
|
|
22
|
+
|
|
23
|
+
当前实现以「模式库兜底」为主,LLM 理解器预留扩展点;所有规则均从 AGENTS.md 实时解析。
|
|
24
|
+
|
|
25
|
+
## 命令
|
|
26
|
+
|
|
27
|
+
| 命令 | 作用 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `/guard status` | 引擎状态(规则数/置信度/放行/解锁) |
|
|
30
|
+
| `/guard rules` | 规则清单 + 理解产物 |
|
|
31
|
+
| `/guard active` | 最近激活了哪些规则、为什么 |
|
|
32
|
+
| `/guard log [N]` | 最近 N 条审计 |
|
|
33
|
+
| `/guard unlock [N]` | 解锁配置写保护 N 分钟(仅用户) |
|
|
34
|
+
| `/guard bypass [N]` | 临时整体放行 N 分钟(仅用户) |
|
|
35
|
+
| `/guard lock` | 立即恢复全部守卫(取消解锁/放行) |
|
|
36
|
+
| `/guard revoke` | 撤销全部授权记录 |
|
|
37
|
+
| `/guard reload` | 强制重解析 AGENTS.md |
|
|
38
|
+
|
|
39
|
+
## 装配方式
|
|
40
|
+
|
|
41
|
+
本插件已按官方 **bundle** 规范打包,包内自带 `cordis.patch.yml`。
|
|
42
|
+
|
|
43
|
+
推荐安装方式:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
dsh plugin --profile web add dsh-rule-engine
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
或手动将 `dsh-rule-engine` 加入 profile 的 `dsh.profile.bundles` 数组。包内的 `cordis.patch.yml` 会自动挂载插件行:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
- insert:
|
|
53
|
+
- id: dsh-rule-engine
|
|
54
|
+
name: 'dsh-rule-engine'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
如果你是从源码手动调试,也可以沿用 insert 方式挂载,但正式安装建议走 bundle。
|
|
58
|
+
|
|
59
|
+
## 安全设计
|
|
60
|
+
|
|
61
|
+
- 只读操作(read/grep/glob/read_image/str_replace_editor view)无条件放行,拦截只针对变更类操作
|
|
62
|
+
- 插件自身配置/理解产物对模型只读:直接 `edit/write` 会被守卫拒绝,需 `/guard unlock`
|
|
63
|
+
- AGENTS.md mtime 变化后自动重解析(`fs.watch` + stat 兜底),规则增删改无需重启
|
|
64
|
+
- 低置信规则不参与硬拦,避免误伤;在 `/guard rules` 中标记人工复核
|
|
65
|
+
- 授权证据按“操作类型 + 目标路径前缀”结构化匹配,区分“询问”与“授权”
|
|
66
|
+
- 备份证据按“目标路径 → 备份路径”记录,删除/覆盖前必须存在对应路径且备份文件真实存在
|
|
67
|
+
- 版本/手册类文件写后自检:版本号连续、append 不覆盖上一行,失败自动回滚并审计
|
|
68
|
+
- 跨工具一致性:同一敏感操作经 `edit` / `write` / `str_replace_editor` / `pwsh` 必须得到相同拦截/放行结论
|
|
69
|
+
- 命令输出静默错误检测:全 false/0/null 或与上一条完全一致时审计 + 注入提醒,不阻断
|
|
70
|
+
- 技能目录实时联动:`ctx.skills` 目录变化后自动刷新,已禁用/不存在的技能不触发 12B
|
|
71
|
+
- LLM 增量理解:对非 high 置信规则调用 `ctx.llm` 补全结构化理解,失败自动回退模式库
|
|
72
|
+
- D 级自证泛化:按规则特征触发自证提示,每规则每会话限 3 次
|
|
73
|
+
- 授权记录默认 10 分钟 TTL,无路径的全局授权 TTL 缩短为 2 分钟;可用 `/guard revoke` 撤销
|
|
74
|
+
- 用户直接命令式指令(如“删除这个文件”)也视为授权
|
|
75
|
+
- 规则 1 支持“用户明确要求重试”豁免
|
|
76
|
+
- 会话状态有容量上限并自动清理,防止长跑内存膨胀
|
|
77
|
+
- LLM 理解按“规则 + AGENTS.md mtime”去重,避免重复烧 token
|
|
78
|
+
- 审计日志:`~/.dsh/rule-engine.log.jsonl`
|
|
79
|
+
- 守卫使用 `ctx.tools.guard()` 单调拒绝,模型无法自行绕过
|
|
80
|
+
|
|
81
|
+
## 当前局限与后续优化路线
|
|
82
|
+
|
|
83
|
+
当前版本已经具备完整四层骨架,但距离“成熟”仍有距离。以下是一些**难度较高、尚未完全实现**的优化方向,欢迎社区共同推进:
|
|
84
|
+
|
|
85
|
+
1. **LLM 理解器深化**
|
|
86
|
+
当前只对非 high 置信规则做一次 LLM 增量理解;未来应支持“规则变更窗口期”、增量重理解、低置信人工复核队列。
|
|
87
|
+
|
|
88
|
+
2. **授权语义精确化**
|
|
89
|
+
当前 ask 授权记录为宽泛 `any` + 路径前缀;未来可要求 ask 面板显式声明操作类型,或支持“一次授权仅针对单个 callId”。
|
|
90
|
+
|
|
91
|
+
3. **备份证据完整化**
|
|
92
|
+
当前校验备份文件存在;未来可增加哈希/大小一致性校验、备份链管理与自动清理。
|
|
93
|
+
|
|
94
|
+
4. **规则 12C / 13B / 10 / 15 / 19 等流程类规则深度执行**
|
|
95
|
+
这些规则需要更多业务语义(下载校验、会话三层验证、版本判断、知识沉淀),目前偏“自证提示”,尚未做到机器可判定。
|
|
96
|
+
|
|
97
|
+
5. **跨会话持久化**
|
|
98
|
+
授权/备份目前为内存态,重启失效。持久化涉及写入保护、并发与恢复,风险较高,暂未实现。
|
|
99
|
+
|
|
100
|
+
6. **输出文本实时拦截**
|
|
101
|
+
受 DSH 官方架构限制,`assistant/message` 无法“拦下不发”,只能事后审计 + 纠正注入;这是平台边界,不是插件能单独突破的。
|
|
102
|
+
|
|
103
|
+
## 致谢
|
|
104
|
+
|
|
105
|
+
感谢以下项目与作者的无私开源付出,本项目在开发过程中直接受益:
|
|
106
|
+
|
|
107
|
+
- **DeepSeek Harness 官方团队(@deepseek-ai)**:提供了 DSH 平台、插件机制与官方文档。
|
|
108
|
+
- **本机已安装插件的作者们**:
|
|
109
|
+
- dsh-guardian(lonelymoon87)
|
|
110
|
+
- dsh-visualize(Nagi-ovo)
|
|
111
|
+
- dsh-usage(kestiny18)
|
|
112
|
+
- dsh-rules-manager(jilian-dsh)
|
|
113
|
+
- dsh-vision-router、dsh-super-injector 等未列出的作者
|
|
114
|
+
- **学习参考的社区文档/库作者**:
|
|
115
|
+
- dsh-handbook(Electricitysheep)
|
|
116
|
+
- SandBase deepseek-harness-handbook(sandbaseai)
|
|
117
|
+
- 以及 DSH 官方文档镜像与源码维护者
|
|
118
|
+
|
|
119
|
+
## 免责声明
|
|
120
|
+
|
|
121
|
+
本项目是**个人/社区项目**,**不属于 DeepSeek Harness 官方项目**,与官方无隶属关系。使用风险自负,请在生产环境前充分测试。
|
|
122
|
+
|
|
123
|
+
## 开发与测试
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm test
|
|
127
|
+
bash scripts/build.sh
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// audit.js - 审计台账(JSONL)。
|
|
2
|
+
// 每次拦截/纠察/自证都追加一行;超限自动裁剪保留尾部。
|
|
3
|
+
import { appendFileSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { auditFilePath } from "./paths.js";
|
|
5
|
+
|
|
6
|
+
const LOG_MAX_BYTES = 512 * 1024;
|
|
7
|
+
const LOG_KEEP_LINES = 400;
|
|
8
|
+
|
|
9
|
+
export function audit(entry) {
|
|
10
|
+
try {
|
|
11
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n";
|
|
12
|
+
appendFileSync(auditFilePath(), line, "utf8");
|
|
13
|
+
trimLog();
|
|
14
|
+
} catch {
|
|
15
|
+
// 审计失败不阻断主流程
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function trimLog() {
|
|
20
|
+
try {
|
|
21
|
+
const p = auditFilePath();
|
|
22
|
+
if (statSync(p).size <= LOG_MAX_BYTES) return;
|
|
23
|
+
const lines = readFileSync(p, "utf8").split("\n");
|
|
24
|
+
const kept = lines.slice(-LOG_KEEP_LINES).join("\n");
|
|
25
|
+
writeFileSync(p, kept, "utf8");
|
|
26
|
+
} catch {
|
|
27
|
+
// 裁剪失败忽略
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readAuditLog(n) {
|
|
32
|
+
try {
|
|
33
|
+
const lines = readFileSync(auditFilePath(), "utf8").split("\n").filter(Boolean);
|
|
34
|
+
return lines.slice(-n).map((line) => {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(line);
|
|
37
|
+
} catch {
|
|
38
|
+
return { raw: line };
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// authorization.js - 授权证据结构化匹配(P0:授权关联具体操作,区分询问与授权)
|
|
2
|
+
// 纯函数,可独立测试。
|
|
3
|
+
import { GENERIC_EXEC_TOOLS, pathTarget, commandText, writeTargetPathsFromCommand } from "./patterns.js";
|
|
4
|
+
|
|
5
|
+
export const AUTH_TTL_MS = 10 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
const AUTH_WORDS_RE = /(?:可以|同意|授权|去做|开始|执行吧|批准|允许|确认|好的|行|去吧|ok|yes)/i;
|
|
8
|
+
const QUESTION_WORDS_RE = /(?:影响吗|可以吗|要不要|是否|能不能|行不行|需不需要|吗\s*$|?|\?)/i;
|
|
9
|
+
const DIRECTIVE_RE = /(?:请\s*)?(?:删除|移除|修改|编辑|替换|写入|创建|复制|移动|执行|运行|下载|提交|推送|备份|安装|卸载|清理|启动|停止|重建|修复|保存)/i;
|
|
10
|
+
const APPROVAL_RE = /(?:允许|同意|可以|是|好|确认|授权|执行|批准|去吧|开始|ok|yes)/i;
|
|
11
|
+
const REJECTION_RE = /(?:不允许|拒绝|不要|否|取消|不同意|no|No)/i;
|
|
12
|
+
|
|
13
|
+
const TYPE_HINTS = [
|
|
14
|
+
{ re: /删除|移除|remove|delete/i, type: "delete" },
|
|
15
|
+
{ re: /修改|编辑|替换|写入|写文件|edit|write|replace/i, type: "write" },
|
|
16
|
+
{ re: /备份|backup/i, type: "backup" },
|
|
17
|
+
{ re: /git\s+(push|commit)|提交|推送/i, type: "git" },
|
|
18
|
+
{ re: /执行|命令|运行|run|execute|pwsh|bash/i, type: "command" },
|
|
19
|
+
{ re: /技能|skill/i, type: "skill" },
|
|
20
|
+
{ re: /下载|网络|curl|fetch/i, type: "network" }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const PATH_TOKEN_RE = /["'][A-Za-z]:[\\/](?!\/)[^"']+["']|[A-Za-z]:[\\/](?!\/)[^\s'"`,。;:!?()【】《》、]+|(?:^|[\s"'`])\/(?:[^\s"'`,。;:!?()【】《》、]+)/g;
|
|
24
|
+
|
|
25
|
+
export function normalizePath(p) {
|
|
26
|
+
if (typeof p !== "string") return "";
|
|
27
|
+
return p.replace(/\\/g, "/").toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 从文本推断操作类型 */
|
|
31
|
+
export function inferTypeFromText(text) {
|
|
32
|
+
const s = String(text || "");
|
|
33
|
+
for (const hint of TYPE_HINTS) {
|
|
34
|
+
if (hint.re.test(s)) return hint.type;
|
|
35
|
+
}
|
|
36
|
+
return "any";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 从文本提取路径前缀(取最长的路径 token;支持引号包裹的含空格路径) */
|
|
40
|
+
export function inferPathPrefixFromText(text) {
|
|
41
|
+
const s = String(text || "");
|
|
42
|
+
const matches = s.match(PATH_TOKEN_RE) || [];
|
|
43
|
+
if (matches.length === 0) return "";
|
|
44
|
+
const normalized = matches
|
|
45
|
+
.map((m) => normalizePath(m.replace(/^["']|["']$/g, "")))
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
if (normalized.length === 0) return "";
|
|
48
|
+
return normalized.sort((a, b) => b.length - a.length)[0];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 从一次工具调用推导操作范围 */
|
|
52
|
+
export function operationOf(toolName, args) {
|
|
53
|
+
const name = String(toolName || "");
|
|
54
|
+
const p = pathTarget(args);
|
|
55
|
+
const cmd = commandText(args);
|
|
56
|
+
let type = "any";
|
|
57
|
+
let pathPrefix = "";
|
|
58
|
+
|
|
59
|
+
if (name === "edit" || name === "write" || (name === "str_replace_editor" && args?.command !== "view")) {
|
|
60
|
+
type = "write";
|
|
61
|
+
pathPrefix = normalizePath(p || "");
|
|
62
|
+
} else if (name === "pwsh" || name === "bash") {
|
|
63
|
+
const text = cmd || "";
|
|
64
|
+
type = inferTypeFromText(text);
|
|
65
|
+
if (/git\s+(push|commit)/i.test(text)) type = "git";
|
|
66
|
+
else if (/remove-item|rm\s+-r|rmdir|del\s+/i.test(text)) type = "delete";
|
|
67
|
+
else if (/backup|copy-item[^\n]*\.(?:bak|backups)[\\/]?|copy-item[^\n]*trash-/i.test(text)) type = "backup";
|
|
68
|
+
else if (/set-content|add-content|out-file|writealltext|copy-item|move-item|rename-item/i.test(text)) type = "write";
|
|
69
|
+
const writeTargets = writeTargetPathsFromCommand(text).map(normalizePath).filter(Boolean);
|
|
70
|
+
pathPrefix = writeTargets.sort((a, b) => b.length - a.length)[0] || inferPathPrefixFromText(text);
|
|
71
|
+
} else if (name === "skill") {
|
|
72
|
+
type = "skill";
|
|
73
|
+
pathPrefix = "";
|
|
74
|
+
} else if (GENERIC_EXEC_TOOLS.has(name)) {
|
|
75
|
+
type = "command";
|
|
76
|
+
pathPrefix = "";
|
|
77
|
+
} else if (name === "ask_user_question") {
|
|
78
|
+
type = "ask";
|
|
79
|
+
pathPrefix = "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { type, pathPrefix };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 判断授权记录是否匹配本次操作 */
|
|
86
|
+
export function authMatches(auth, op) {
|
|
87
|
+
if (!auth || !op) return false;
|
|
88
|
+
if (auth.type !== "any" && op.type !== "any" && auth.type !== op.type) return false;
|
|
89
|
+
if (auth.pathPrefix) {
|
|
90
|
+
const p = normalizePath(op.pathPrefix || "");
|
|
91
|
+
if (!p.startsWith(auth.pathPrefix)) return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 在授权列表中查找匹配项,返回最近一条(过期记录忽略) */
|
|
97
|
+
export function findMatchingAuth(auths, op, now = Date.now()) {
|
|
98
|
+
if (!Array.isArray(auths)) return null;
|
|
99
|
+
const matches = auths.filter((a) => authMatches(a, op) && (!a.expiresAt || a.expiresAt > now));
|
|
100
|
+
if (matches.length === 0) return null;
|
|
101
|
+
return matches.sort((a, b) => (b.at || 0) - (a.at || 0))[0];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function describeAuth(auth) {
|
|
105
|
+
if (!auth) return "无";
|
|
106
|
+
const type = auth.type || "any";
|
|
107
|
+
const path = auth.pathPrefix ? `路径 ${auth.pathPrefix}` : "全局";
|
|
108
|
+
const expires = auth.expiresAt ? `|到期 ${new Date(auth.expiresAt).toISOString()}` : "";
|
|
109
|
+
return `${type}|${path}|${new Date(auth.at || 0).toISOString()}${expires}|仅本次运行有效`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function describeOp(op) {
|
|
113
|
+
if (!op) return "未知";
|
|
114
|
+
return `${op.type || "any"}|路径 ${op.pathPrefix || "未指定"}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 用户消息是否为“询问影响”而非授权 */
|
|
118
|
+
export function isQuestionMessage(text) {
|
|
119
|
+
return QUESTION_WORDS_RE.test(String(text || ""));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 用户消息是否包含明确授权词(且不是询问句) */
|
|
123
|
+
export function isAuthMessage(text) {
|
|
124
|
+
const s = String(text || "");
|
|
125
|
+
return AUTH_WORDS_RE.test(s) && !isQuestionMessage(s);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 用户消息是否为直接命令式指令(非询问,视为授权执行) */
|
|
129
|
+
export function isDirectiveMessage(text) {
|
|
130
|
+
const s = String(text || "");
|
|
131
|
+
return DIRECTIVE_RE.test(s) && !isQuestionMessage(s);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 选项/自由文本是否表示批准 */
|
|
135
|
+
export function isApprovalText(text) {
|
|
136
|
+
return APPROVAL_RE.test(String(text || ""));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 选项/自由文本是否表示拒绝 */
|
|
140
|
+
export function isRejectionText(text) {
|
|
141
|
+
return REJECTION_RE.test(String(text || ""));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 从 ask_user_question 的 arguments 中提取问题文本(用于推断授权范围) */
|
|
145
|
+
export function askQuestionText(questions) {
|
|
146
|
+
if (!Array.isArray(questions)) return "";
|
|
147
|
+
const parts = [];
|
|
148
|
+
for (const q of questions) {
|
|
149
|
+
if (q && typeof q === "object") {
|
|
150
|
+
if (q.question) parts.push(q.question);
|
|
151
|
+
if (q.header) parts.push(q.header);
|
|
152
|
+
if (q.detail) parts.push(q.detail);
|
|
153
|
+
if (Array.isArray(q.options)) {
|
|
154
|
+
for (const o of q.options) {
|
|
155
|
+
if (o && typeof o === "object") {
|
|
156
|
+
if (o.label) parts.push(o.label);
|
|
157
|
+
if (o.description) parts.push(o.description);
|
|
158
|
+
} else if (typeof o === "string") {
|
|
159
|
+
parts.push(o);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} else if (typeof q === "string") {
|
|
164
|
+
parts.push(q);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return parts.join(" ");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 从 ask_user_question 的结果中提取是否批准 */
|
|
171
|
+
export function askResultApproved(result) {
|
|
172
|
+
const answers = extractAnswers(result);
|
|
173
|
+
if (!answers || answers.length === 0) return false;
|
|
174
|
+
let approved = false;
|
|
175
|
+
let rejected = false;
|
|
176
|
+
for (const item of answers) {
|
|
177
|
+
const texts = [...(item.selected || []), item.custom || ""].filter(Boolean);
|
|
178
|
+
for (const t of texts) {
|
|
179
|
+
if (isRejectionText(t)) rejected = true;
|
|
180
|
+
else if (isApprovalText(t)) approved = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return approved && !rejected;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function extractToolResultText(result) {
|
|
187
|
+
const msg = result?.message ?? result;
|
|
188
|
+
const content = Array.isArray(msg?.content)
|
|
189
|
+
? msg.content
|
|
190
|
+
: Array.isArray(result?.content)
|
|
191
|
+
? result.content
|
|
192
|
+
: null;
|
|
193
|
+
if (!content) return null;
|
|
194
|
+
const block = content.find((b) => b && b.type === "tool-result");
|
|
195
|
+
if (!block) return null;
|
|
196
|
+
const inner = Array.isArray(block.content) ? block.content : [];
|
|
197
|
+
const text = inner
|
|
198
|
+
.filter((b) => b && b.type === "text" && typeof b.text === "string")
|
|
199
|
+
.map((b) => b.text)
|
|
200
|
+
.join("\n");
|
|
201
|
+
return text || null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function extractAnswers(result) {
|
|
205
|
+
if (!result) return [];
|
|
206
|
+
if (Array.isArray(result)) return result;
|
|
207
|
+
if (typeof result === "string") {
|
|
208
|
+
try {
|
|
209
|
+
return extractAnswers(JSON.parse(result));
|
|
210
|
+
} catch {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// tool/result 事件实际形状:message.content[0] = { type:'tool-result', content:[{type:'text', text:'{...}'}] }
|
|
215
|
+
const toolText = extractToolResultText(result);
|
|
216
|
+
if (toolText) {
|
|
217
|
+
try {
|
|
218
|
+
return extractAnswers(JSON.parse(toolText));
|
|
219
|
+
} catch {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(result.answers)) return result.answers;
|
|
224
|
+
if (Array.isArray(result.value?.answers)) return result.value.answers;
|
|
225
|
+
if (Array.isArray(result.result?.answers)) return result.result.answers;
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// config.js - 插件配置加载。
|
|
2
|
+
// 配置缺失时使用默认值;损坏时也回退默认并记录错误,避免裸奔。
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { configFilePath } from "./paths.js";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_CONFIG = {
|
|
7
|
+
enabled: true,
|
|
8
|
+
correctInject: true,
|
|
9
|
+
selfProtect: true,
|
|
10
|
+
injectLimitPerRulePerSession: 3
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function loadPluginConfig() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = readFileSync(configFilePath(), "utf8");
|
|
16
|
+
const data = JSON.parse(raw);
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
error: null,
|
|
20
|
+
...DEFAULT_CONFIG,
|
|
21
|
+
...(data && typeof data === "object" ? data : {})
|
|
22
|
+
};
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
error: `配置读取失败,已用默认配置:${error instanceof Error ? error.message : String(error)}`,
|
|
27
|
+
...DEFAULT_CONFIG
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|