dsh-rule-engine 0.5.13 → 0.5.14
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 +8 -8
- package/lib/core/authorization.js +28 -4
- package/lib/core/intent.js +11 -0
- package/lib/core/lexicon.js +1 -1
- package/lib/core/state.js +1 -0
- package/lib/core/text-detect.js +56 -11
- package/lib/index.js +2 -0
- package/package.json +1 -1
- package/scripts/release-plugin.mjs +277 -0
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# dsh-rule-engine
|
|
2
2
|
|
|
3
3
|

|
|
4
|
-

|
|
5
5
|
|
|
6
6
|
DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯一真相源,自动解析规则四要素与执行等级,再通过「工具守卫 + 文本检测 + 时序检查 + 审计台账」执行用户规则,而不是内置一套与用户无关的安全清单。
|
|
7
7
|
|
|
8
|
-
> 当前版本 **0.5.
|
|
8
|
+
> 当前版本 **0.5.13**(2026-08-31 已发布三通道:npm=0.5.13 / git 4c52982 / Release v0.5.13 带 tgz;上一版 0.5.12 = 2026-08-30,git 269ea91)。**工作区当前迭代 0.5.14**(2026-09-01:分点三柱——条件句零授权/显式命名对象锚定/clauseId 隔离 + skill 词收紧 + 分点回归测试,发布待授权)。本插件面向"规则机器化执行":规则写在 AGENTS.md 里,引擎负责让它们真的被遵守;所有规则动态解析,规则增删改后无需重写插件。
|
|
9
9
|
|
|
10
10
|
## 项目背景
|
|
11
11
|
|
|
@@ -58,7 +58,7 @@ DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯
|
|
|
58
58
|
- `node scripts/health-audit.mjs` —— 找茬清单:近 24h 失败/降级类统计(intent-llm 失败、judge-unavailable、verify-gap、inject-skip…)+ 关键导出接线交叉(疑似未接线 = 告警)——"失败可见化",不再有静默躺 20 小时的降级;
|
|
59
59
|
- 执行协议(本仓库自身交付纪律):方案冻结单(范围/影响面/测试计划/失败预测)→ todo 化 → 小步闭环(每改动立即 `node --check`)→ 对账交付(计划×实际逐项 ✅/❌/跳过原因)。**验收五查(0.5.11 用户定稿:验收 = 改动关联产物全做一遍,不是只跑测试)**:① 本次改动的**全部**测试/用例在 run-all 或对应门禁中收录;② README/手册/版本记录与引擎语义成对(新增行为必写文档);③ 调用点 grep:改公共函数签名/导出 → 全部调用点逐一核对;④ 相关测试断言同步(本例:consistency 死映射 17→16);⑤ 误删/错改产物清理(错误版测试/脚本删除后无残留引用)。**⑥ 实弹验证(0.5.11 用户定稿)**:改动在运行态真实生效 = 重启 DSH 加载(profile 为 link: 时重启即生效)→ 运行态验证(行为/守卫/日志实拍)→ 通过后**才**允许进入发布——**发布必须以"重启生效+验证通过"为前提,未生效验证不得发布**(顺序:改 → 测试 → 重启生效 → 实弹验证 → 发布)。任一未做 = 交付不算完成。**⑦ 发布前置(2026-08-28 用户定稿)**:发布前必须先本地跑测试——本项目测试(test-service/npm test)+ 验证脚本 + 运行态实测(截图/输出证据),未全过不得发布;发布后核对三处成对——README 徽章=package.json 版本(release 脚本自动 bump,需验证)、三通道(npm/git/Release)逐个确认成功(含 Release 带正式 tgz asset);发布后遗留(徽章/README 成对缺口)必须当场修。**发布流程三阶段(2026-08-29 用户定稿)**:阶段 A **内容验证(发布前,只读/本地)**——A1 测试全过(run-all+verify-all+loader+运行态实测)、A2 dry-run 推导正确(`release-plugin.mjs <pkg> --dry-run`:oldVer→nextVer+徽章变化)、A3 版本三处一致(徽章=package.json=发布目标号)、A4 Asset/tag 预检(tgz 已 pack;目标 tag 预检不存在——防 ㉙ 同 tag 占位);阶段 A0 **授权**——展示改动清单+版本号 → ask_user_question 明确授权(规则 26⑤:先授权→运行脚本);阶段 B **一键三通道**——`release-plugin.mjs`(publish→push→Release 带 tgz);阶段 C **事后核对**——三通道确认(npm view/git tag/gh api asset)+ 徽章/README 成对复核 + 本机 link 不重装 + 沉淀(版本记录/engram)。**顺序铁律:A→A0→B→C,验证在发布前(A),不是发布后补(C)——发布内容经确认无误才允许三通道。**
|
|
60
60
|
|
|
61
|
-
> **版本状态说明(2026-
|
|
61
|
+
> **版本状态说明(2026-09-01 更新)**:0.5.11 与 0.5.12、0.5.13 均已独立发布(见上;0.5.13 = 2026-08-31 三通道)。0.5.14 = 当前工作区迭代(发布前统一以对外号为准)。历史说明:0.5.11 发布时含 0.5.10 git 提交欠账 7 文件补齐(baseline/intent/judge/llm-intent/semantic/tool-catalog/whitelist.js——npm 包本已包含,git 通道缺失)。
|
|
62
62
|
|
|
63
63
|
## 版本历史(摘要)
|
|
64
64
|
|
|
@@ -66,10 +66,10 @@ DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯
|
|
|
66
66
|
|
|
67
67
|
| 版本 | 日期 | 要点 |
|
|
68
68
|
|---|---|---|
|
|
69
|
-
| **0.5.14** | 2026-
|
|
70
|
-
| **0.5.13** | 2026-08-
|
|
71
|
-
| **0.5.12** | 2026-08-
|
|
72
|
-
| **0.5.11** | 2026-08-
|
|
69
|
+
| **0.5.14** | 2026-09-01 | 工作区迭代(发布待授权):分点三柱(条件句零授权/显式命名对象锚定/clauseId 隔离)+ skill 词收紧(Skill 名词不再触发技能类)+ 分点回归测试 |
|
|
70
|
+
| **0.5.13** | 2026-08-31 | 通用化(声明式绑定/禁用语义/会话寻址/验证通道/发布适用性门禁)+ 阶段二·三(委派豁免/中文顿号路径/ask 节流区分/D1-D3 注入/F1 规则 2/F2 verify-gap) |
|
|
71
|
+
| **0.5.12** | 2026-08-30 | 意图优先级修正(动作词先于方案词)、LLM 意图兜底同步等待、@文件引用信号、只读判定三档(写特征/白名单/保守拒)、规则 22 粒度并入会话授权、ERR 码打标链路、F2 打标指纹、F5 契约类别白名单 |
|
|
72
|
+
| **0.5.11** | 2026-08-29 | 判定内核第一轮:新建豁免补"新建"语义、分析通道移除脚本区语义豁免、12A 与 22-7 判据同源、后台判定模型 = 当前会话模型;词表唯一源(lexicon.js)、rule14 空转映射清理、规则 24 机器执行修正(守卫链覆盖测试) |
|
|
73
73
|
| **0.5.10** | 2026-08-27 | 分析通道(单真源 isAnalysisOp:严格只读 ∪ 分析临时区写)、写类判定单真源(isMutationCommand)、只读词表补全、统一入口加固(转义检测 + 写后校验)、误判打标闭环、已知坑错误码召回 |
|
|
74
74
|
| **0.5.9** | 2026-08-27 | 工具分类单真源(tool-catalog.js)、官方 59 工具全集覆盖、前缀规则(mcp__/esr_/dev_ 等命名空间自动归类)、unknownPolicy(默认 deny)、白名单 v2 带元数据、/guard tools、工具箱覆盖门禁 |
|
|
75
75
|
| **0.5.8** | 2026-08-26 | 白名单持久化(rule-engine-tools.json)、只读命令词表补全(Select-Object 等)、`npm run verify` 注册 |
|
|
@@ -208,7 +208,7 @@ bash scripts/build.sh
|
|
|
208
208
|
|
|
209
209
|
交付前体检(0.5.7 起):`node scripts/verify-all.mjs`(七层:语法/单元/组合冒烟/工具箱覆盖/守卫链覆盖/关联一致性/真实判例)与 `node scripts/health-audit.mjs`(找茬)——详见「质量与验证」。
|
|
210
210
|
|
|
211
|
-
发布:`node
|
|
211
|
+
发布:`node scripts/release-plugin.mjs <插件名> <版本号>`(一键 npm + git + GitHub Release;发布脚本随插件仓库管理——`scripts/release-plugin.mjs`;改发布脚本后须 `--dry-run` + 代码审查,注意 dry-run 不覆盖 git 段)。
|
|
212
212
|
|
|
213
213
|
## License
|
|
214
214
|
|
|
@@ -41,7 +41,10 @@ const TYPE_HINTS = [
|
|
|
41
41
|
// 0.5.12(F1 重证):command 规则补 ①引号包裹路径 ②无扩展名可执行文件(tsc/tsdown 等)
|
|
42
42
|
// ③pnpm/yarn/bun 显式收录(此前靠 npm\s+ 子串巧合命中 pnpm,非设计保证)
|
|
43
43
|
{ re: /命令|运行|执行\s*(?:命令|脚本|以下|程序)|run|execute|pwsh|bash|node\s+(?!-e\b|-p\b|--eval\b|--print\b)(?:['"])?[\w./\\-]+(?:['"])?(?:\s|$)|(?:npm|pnpm|yarn|bun)\s+(?:run|exec|install|add)|(?:^|[^a-z])(?:tsc|tsdown|vite|webpack|rollup)\b/i, type: "command" },
|
|
44
|
-
|
|
44
|
+
// 2026-08-31(skill 词残留收紧):12B 已禁用(技能调用流程),原 /技能|skill/i 把任意文本中的
|
|
45
|
+
// "Skill"名词(如 repo 描述)判成技能类→授权范围不匹配误拦(VCI2RX 实证)。
|
|
46
|
+
// 收紧为**真实技能操作**(skill 工具/技能目录管理上下文),名词性"Skill"不再命中。
|
|
47
|
+
{ re: /(?:技能|skill)\s*(?:调用|目录|管理|列表|启用|禁用|安装|删除|加载|查询)|(?:启用|禁用|调用|安装|删除|加载|查询|打开|查看|管理)\s*技能|(?:启用|禁用|调用|安装|删除|加载|查询|打开|查看|管理)\s*skill\b|skill\s+(?:tools?|dir|enable|disable|list|install)\b|\/guard\s+skills?\b/i, type: "skill" },
|
|
45
48
|
// 0.5.12(F1 重证):network 词表由裸子串改词边界+上下文——`fetch` 出现在目录名/标识符中
|
|
46
49
|
//(dsh-web-fetch-playwright)不再误判;真网络形态(URL/调用/下载动词)仍命中
|
|
47
50
|
{ re: /下载|网络|https?:\/\/\S*|(?:^|[^a-z])(?:curl|fetch|wget|iwr|Invoke-WebRequest|Invoke-RestMethod)\s*(?:\(|\.|[^\w]|$)/i, type: "network" },
|
|
@@ -231,7 +234,7 @@ export function pairActionScopes(raw, clauseId, strict = false) {
|
|
|
231
234
|
const type = verbType(v.word, text);
|
|
232
235
|
for (const pathPrefix of boundPaths) {
|
|
233
236
|
if (!scopes.some((s) => s.type === type && s.pathPrefix === pathPrefix)) {
|
|
234
|
-
scopes.push({ type, pathPrefix, source: "clause", clauseId });
|
|
237
|
+
scopes.push({ type, pathPrefix, source: "clause", clauseId, object: extractActionObjects(text) });
|
|
235
238
|
}
|
|
236
239
|
}
|
|
237
240
|
}
|
|
@@ -242,7 +245,7 @@ export function pairActionScopes(raw, clauseId, strict = false) {
|
|
|
242
245
|
export function describeScopes(scopes) {
|
|
243
246
|
if (!Array.isArray(scopes) || scopes.length === 0) return "无";
|
|
244
247
|
return scopes
|
|
245
|
-
.map((s) => `${s.type || "any"}|路径 ${s.pathPrefix || "全局"}`)
|
|
248
|
+
.map((s) => `${s.type || "any"}|路径 ${s.pathPrefix || "全局"}${Array.isArray(s.object) && s.object.length ? `|对象 ${s.object.join(",")}` : ""}`)
|
|
246
249
|
.join(";");
|
|
247
250
|
}
|
|
248
251
|
|
|
@@ -297,7 +300,22 @@ export function operationOf(toolName, args) {
|
|
|
297
300
|
pathPrefix = "";
|
|
298
301
|
}
|
|
299
302
|
|
|
300
|
-
return { type, pathPrefix, pathPrefixes };
|
|
303
|
+
return { type, pathPrefix, pathPrefixes, commandText: cmd || "" };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 柱子 B(2026-08-31):显式命名对象锚定——分点文本中"X 仓库/repo/目录/项目/包/文件"的
|
|
308
|
+
* **实体名 token**(含连字符/点/下划线/数字的命名形态;"skill 仓库"这类类别概念词不锚定——
|
|
309
|
+
* 防过度误拦)。锚定后授权仅覆盖该对象;未锚定(空)→ 不收紧(维持既有语义)。
|
|
310
|
+
*/
|
|
311
|
+
const NAMED_OBJECT_RE = /([A-Za-z0-9]+(?:[-_.][A-Za-z0-9]+)+|[\u4e00-\u9fff]{2,12}[A-Za-z0-9]*)(?:\s*)(?:远程|远端)?(?:仓库|repo(?:sitory)?|目录|项目|包|package|文件)/gi;
|
|
312
|
+
export function extractActionObjects(text) {
|
|
313
|
+
const out = new Set();
|
|
314
|
+
for (const m of String(text || "").matchAll(NAMED_OBJECT_RE)) {
|
|
315
|
+
const token = m[1].trim();
|
|
316
|
+
if (token.length >= 3) out.add(token.toLowerCase());
|
|
317
|
+
}
|
|
318
|
+
return [...out];
|
|
301
319
|
}
|
|
302
320
|
|
|
303
321
|
/** 路径是否在授权边界内:相等,或为授权路径的子路径(子路径以 / 为边界,防止文件前缀误伤) */
|
|
@@ -320,6 +338,12 @@ export function authMatches(auth, op) {
|
|
|
320
338
|
const authPath = normalizePath(auth.pathPrefix);
|
|
321
339
|
if (!candidates.some((p) => pathUnderAuth(normalizePath(p), authPath))) return false;
|
|
322
340
|
}
|
|
341
|
+
// 柱子 B(2026-08-31):对象锚定——授权声明了显式命名对象 → 操作命令文本必须命中该对象
|
|
342
|
+
//("推送 X 仓库"不覆盖"推送 Y 仓库";未锚定对象=不收紧)
|
|
343
|
+
if (Array.isArray(auth.object) && auth.object.length > 0) {
|
|
344
|
+
const opText = String(op.commandText || "").toLowerCase();
|
|
345
|
+
if (!opText || !auth.object.some((o) => opText.includes(String(o).toLowerCase()))) return false;
|
|
346
|
+
}
|
|
323
347
|
return true;
|
|
324
348
|
}
|
|
325
349
|
|
package/lib/core/intent.js
CHANGED
|
@@ -37,6 +37,11 @@ export const TAG_RE = /【(执行|方案|问询|信息)】/;
|
|
|
37
37
|
export const QUESTION_RE = QUESTION_WORDS_RE;
|
|
38
38
|
|
|
39
39
|
const DEFER_RE = /待确认|确认后再|等我确认|先方案|先别|不要执行|之后再说|确认后|再确认|先确认|不要直接/;
|
|
40
|
+
// 2026-08-31(柱子 C:条件句识别——"推送完成后…/等 X 了…/待…后…"型分点):
|
|
41
|
+
// 条件句分点 = 依赖前序分点完成后的后续动作,**本条不产生任何授权/scope**(安全方向:宁拦不放)。
|
|
42
|
+
// 词面拒绝"推送/上传"等动作词被当作本条独立动作(3 号分点"推送完成后发地址"实证:引擎曾把
|
|
43
|
+
// "推送"当本条动作致 git 全局授权弥漫——分点失效根因之一)。
|
|
44
|
+
const CONDITIONAL_RE = /(?:完成|结束|做完|弄完|搞定|好了|恢复|重启?)\s*(?:了|之后|后|以后|再)|(?:等|待|等\s*[\u4e00-\u9fff]{0,6}?)\s*[\u4e00-\u9fff]{0,8}?(?:后|之后|了)\s*|(?:推送|上传|提交|发布|执行)\s*(?:完成|结束|完|好)\s*(?:了|之?后)?/;
|
|
40
45
|
const ACTION_RE = ACTION_WORDS_RE;
|
|
41
46
|
const PLAN_RE = /方案|建议|评估|分析|设计|规划|计划|给出|提供/;
|
|
42
47
|
const STRONG_EXEC_RE = STRONG_EXEC_LEX;
|
|
@@ -221,6 +226,8 @@ function classifyClause(clause) {
|
|
|
221
226
|
let type = "info";
|
|
222
227
|
let ambiguous = false;
|
|
223
228
|
|
|
229
|
+
// 2026-08-31(柱子 C):条件句优先判定——"推送完成后/等 X 后"=依赖型分点,不产生独立授权
|
|
230
|
+
//(必须在动作词判定之前:否则"完成后"被当执行分点,其 scope 污染授权池——分点失效根因)
|
|
224
231
|
if (tagType) {
|
|
225
232
|
if (tagType === "execute" && hasQuestion) {
|
|
226
233
|
type = "ambiguous";
|
|
@@ -232,6 +239,10 @@ function classifyClause(clause) {
|
|
|
232
239
|
type = "question";
|
|
233
240
|
} else if (isStatusSignal(text)) {
|
|
234
241
|
type = "status";
|
|
242
|
+
} else if (CONDITIONAL_RE.test(text)) {
|
|
243
|
+
// 2026-08-31(柱子 C):条件句——"推送完成后/等 X 后"=依赖型分点,不产生独立授权/scope
|
|
244
|
+
//(若按动作词判 execute,其"推送"字眼将污染 git 授权池——分点失效根因;宁拦不放)
|
|
245
|
+
type = "conditional";
|
|
235
246
|
} else if (hasDefer && hasAction) {
|
|
236
247
|
type = "plan";
|
|
237
248
|
} else if (hasAction && hasPlan && !SPECIFIC_ACTION_RE.test(text) && !(PLAN_EXECUTING_RE.test(text) && !PLAN_REQUEST_RE.test(text))) {
|
package/lib/core/lexicon.js
CHANGED
|
@@ -28,7 +28,7 @@ export const PLAN_ONLY_RE = /确认方案|确认理解|确认方向|确认无误
|
|
|
28
28
|
// 注意:单一表同时服务"意图判定"(这句话是不是执行分点)与"ask 结果批准判定"(选择=批准),
|
|
29
29
|
// 语义一致,不另分裂;新增动作词只改这里。
|
|
30
30
|
export const ACTION_WORDS_RE =
|
|
31
|
-
/执行|跑|落|落盘|认可|开始|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)
|
|
31
|
+
/执行|跑|落|落盘|认可|开始|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|建(?:设|立|好|一下|个|一个)?|安装|发布|下载|提交|推送|push|运行|修复|替换|重建|启动|重启|停止|卸载|开始改|开始写|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|继续|推进|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|开工|动手|进行|完成|选择|采纳|选定|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:出|一下|一遍|完|出来)?|展示|打开|提取|还原|导入|导出|清理|移除|清空|残留|合入|合并|并入|融合|整合|起草|开展|搭建|重构|兼容|投产|校验|核验|过一遍|跑一遍|复验/i;
|
|
32
32
|
|
|
33
33
|
// 强执行语(直接执行指令,不等授权确认——用于意图置信提升)
|
|
34
34
|
export const STRONG_EXEC_RE =
|
package/lib/core/state.js
CHANGED
|
@@ -147,6 +147,7 @@ export function getSessionState(state, sessionId) {
|
|
|
147
147
|
id: key,
|
|
148
148
|
ruleStats: {}, // C2:每规则 detected/suppressed/injected 统计(会话级,跨回合累计)
|
|
149
149
|
manualReadSeen: false,
|
|
150
|
+
lastQueryTurn: -1, // 规则 5/31(2026-09-01):最近一次查询类工具调用所在回合号(判定"近 3 回合有据")
|
|
150
151
|
lastUserText: "",
|
|
151
152
|
selfCertCount: new Map(),
|
|
152
153
|
authorizations: [],
|
package/lib/core/text-detect.js
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
TERM_EXPLANATION_RE,
|
|
12
12
|
SUGGEST_RE,
|
|
13
13
|
isNegatingSuggestion,
|
|
14
|
-
isQuoteOrParaphraseContext
|
|
14
|
+
isQuoteOrParaphraseContext,
|
|
15
|
+
isReadOnlyTool
|
|
15
16
|
} from "./patterns.js";
|
|
16
17
|
import { detectOverengineeringText } from "./overengineering.js";
|
|
17
18
|
|
|
@@ -50,9 +51,16 @@ const SELF_CERT_HINTS = {
|
|
|
50
51
|
"19": { re: /学到新.*DSH|踩到新坑|踩坑.*沉淀|手册有误|知识必沉淀/, reason: "请按规则 19 自证:按①-⑧沉淀并当次汇报正文同步位置" },
|
|
51
52
|
"28": { re: /新建.*文件|放入.*目录|工作区.*归类/, reason: "请按规则 28 自证:新文件按工作区目录索引归类,不确定则查 README 或放 _inbox" },
|
|
52
53
|
"29": { re: /依赖闭包|node_modules.*验证|动过.*node_modules/, reason: "请按规则 29 自证:重启前验证依赖闭包(关键包非空/顶层 junction 完整/无 dangling)" },
|
|
53
|
-
"30": { re: /变更验证|验证证据|不破坏.*依赖/, reason: "请按规则 30 自证:执行前证明不破坏依赖+已授权,完成后附验证证据" }
|
|
54
|
+
"30": { re: /变更验证|验证证据|不破坏.*依赖/, reason: "请按规则 30 自证:执行前证明不破坏依赖+已授权,完成后附验证证据" },
|
|
55
|
+
// 规则 31(2026-09-01 用户拍板):提到"撞墙/盲试"类表述 → D 级自证:写明验证目标→撞了什么/为什么撞→结论来源
|
|
56
|
+
"31": { re: /撞(?:了)?南墙|撞墙|盲试|乱撞|连续盲试/, reason: "规则 31 自证:请写明「验证目标→撞了什么/为什么撞→结论来源(手册位置/源码行号)」;不连续盲试" }
|
|
54
57
|
};
|
|
55
58
|
|
|
59
|
+
// 规则 31(2026-09-01 用户拍板):内部文档引用锚点词("文档"仅在后随"写了/记载…"语境命中,防泛化误报)
|
|
60
|
+
const INTERNAL_REF_RE = /(?:手册|踩坑\s*\d+|条款\s*\d+|第\s*\d+\s*章|SKILL\.md|AGENTS\.md|源码|README|文档[^\n]{0,12}(?:写了|记载|记录|写明|说明))/i;
|
|
61
|
+
// 验证目标启发式:思维链含这些词 → 大概率是有序查证(防规则 31③误报)
|
|
62
|
+
const VERIFY_INTENT_RE = /(?:验证|查证|核对|目标|交叉|对照|确认)/;
|
|
63
|
+
|
|
56
64
|
/** 简单判断一段文本是否以英文为主 */
|
|
57
65
|
function isMostlyEnglish(text) {
|
|
58
66
|
if (!text) return false;
|
|
@@ -157,19 +165,56 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
|
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
const sourceCfg = byId.get("5");
|
|
160
|
-
if (sourceCfg &&
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
if (sourceCfg && !SOURCE_MARK.test(text)) {
|
|
169
|
+
if (URL_RE.test(text)) {
|
|
170
|
+
hits.push({
|
|
171
|
+
ruleId: "5",
|
|
172
|
+
title: sourceCfg.title,
|
|
173
|
+
kind: "correct",
|
|
174
|
+
reason: "回答包含 URL 但未标注出处/来源"
|
|
175
|
+
});
|
|
176
|
+
} else if (INTERNAL_REF_RE.test(text) && !isQuoteOrParaphraseContext(text, INTERNAL_REF_RE)) {
|
|
177
|
+
// 规则 5 扩展(2026-09-01 用户拍板):内部文档引用(手册/踩坑/条款/源码…)须有依据——
|
|
178
|
+
// 近 3 回合无对应 read/grep 时提示(B 级:留痕+注入,不拦截)。
|
|
179
|
+
const curTurn = session.turn.number || 0;
|
|
180
|
+
const lastQuery = session.lastQueryTurn ?? -1;
|
|
181
|
+
if (lastQuery < 0 || curTurn - lastQuery > 3) {
|
|
182
|
+
hits.push({
|
|
183
|
+
ruleId: "5",
|
|
184
|
+
title: sourceCfg.title,
|
|
185
|
+
kind: "correct",
|
|
186
|
+
reason: "回答引用内部文档(手册/踩坑/条款/源码等)但近 3 回合无对应 read/grep——请标注手册位置或删去断言"
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 规则 31(2026-09-01 用户拍板):同回合同一只读工具 ≥3 次且思维链无验证目标语 → 提示(B 级:留痕+注入)
|
|
193
|
+
const rule31Cfg = byId.get("31");
|
|
194
|
+
if (rule31Cfg) {
|
|
195
|
+
const counts = new Map();
|
|
196
|
+
const pending = session.turn?.pendingToolCalls;
|
|
197
|
+
if (pending && typeof pending.values === "function") {
|
|
198
|
+
for (const pc of pending.values()) {
|
|
199
|
+
if (pc && pc.name && isReadOnlyTool(pc.name, pc.args)) {
|
|
200
|
+
counts.set(pc.name, (counts.get(pc.name) || 0) + 1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const repeated = [...counts.entries()].find(([, c]) => c >= 3);
|
|
205
|
+
if (repeated && !VERIFY_INTENT_RE.test(reasoningText)) {
|
|
206
|
+
hits.push({
|
|
207
|
+
ruleId: "31",
|
|
208
|
+
title: rule31Cfg.title,
|
|
209
|
+
kind: "correct",
|
|
210
|
+
reason: `规则 31:回合同一只读工具(${repeated[0]})调用 ${repeated[1]} 次且思维链无验证目标语——有序交叉验证请忽略,连续盲试请先写明「要验证什么」`
|
|
211
|
+
});
|
|
212
|
+
}
|
|
167
213
|
}
|
|
168
214
|
|
|
169
215
|
const langCfg = byId.get("11");
|
|
170
216
|
const visibleEnglish = isMostlyEnglish(text);
|
|
171
|
-
const reasoningEnglish = isMostlyEnglish(reasoningText);
|
|
172
|
-
if (langCfg && session.lastUserText && CJK_RE.test(session.lastUserText) && (visibleEnglish || reasoningEnglish)) {
|
|
217
|
+
const reasoningEnglish = isMostlyEnglish(reasoningText); if (langCfg && session.lastUserText && CJK_RE.test(session.lastUserText) && (visibleEnglish || reasoningEnglish)) {
|
|
173
218
|
hits.push({
|
|
174
219
|
ruleId: "11",
|
|
175
220
|
title: langCfg.title,
|
package/lib/index.js
CHANGED
|
@@ -639,6 +639,8 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
639
639
|
}
|
|
640
640
|
}
|
|
641
641
|
if (isManualReadTool(toolName, args)) s.manualReadSeen = true;
|
|
642
|
+
// 规则 5/31 扩展(2026-09-01):查询类工具调用记下回合号("近 3 回合有据"判定)
|
|
643
|
+
if (isReadOnlyTool(toolName, args)) s.lastQueryTurn = s.turn.number;
|
|
642
644
|
if (toolName === "skill" && args?.name) s.turn.skillNames.push(args.name);
|
|
643
645
|
s.turn.toolNames.push(toolName);
|
|
644
646
|
if (state.taskContract?.taskContractEnabled) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// release-plugin.mjs —— DSH 插件全渠道一键发布(npm + git + GitHub Release)
|
|
2
|
+
// 用法:node release-plugin.mjs <插件名|插件目录> [版本号] [--sync-profile]
|
|
3
|
+
// - 插件名从 scripts/plugins.json 清单解析(规则 26 ⑤);或直接给目录路径
|
|
4
|
+
// - 版本号省略时自动 patch+1(如 1.4.7 -> 1.4.8)
|
|
5
|
+
// - 自动同步 README 徽章 version-X.Y.Z
|
|
6
|
+
// - --sync-profile:发布成功后自动更新 profiles/<profile>/pnpm-workspace.yaml 豁免名单
|
|
7
|
+
// (备份原文件)+ 运行全量装配审计(写 .dsh 需以 danger-full-access 运行本脚本)
|
|
8
|
+
// 前置:npm 已认证、gh 已认证、git 已配置。
|
|
9
|
+
// 网络:默认直连(2026-08-27 实测 npm registry / github.com / api.github.com 直连可用,无需代理;
|
|
10
|
+
// 代理挂掉时不再 ECONNREFUSED)。需要代理的环境设 DSH_RELEASE_PROXY=http://127.0.0.1:7890。
|
|
11
|
+
// 步骤:版本 bump -> 测试 -> npm pack -> npm publish -> git commit+push -> gh release(带 tgz asset)
|
|
12
|
+
// 安全:token 经环境变量注入,不在命令文本/日志中打印
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
|
|
15
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const PROXY = process.env.DSH_RELEASE_PROXY || ""; // 默认直连;需要代理时显式设置
|
|
19
|
+
/** 代理前缀(命令文本):空 = 直连 */
|
|
20
|
+
const proxyPrefix = () => (PROXY ? `set HTTPS_PROXY=${PROXY}&& set HTTP_PROXY=${PROXY}&& ` : "");
|
|
21
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const WORKSPACE = join(SCRIPT_DIR, "..");
|
|
23
|
+
|
|
24
|
+
function run(cmd, opts = {}) {
|
|
25
|
+
console.log(`> ${cmd.slice(0, 120)}${cmd.length > 120 ? "..." : ""}`);
|
|
26
|
+
return execSync(cmd, { stdio: "inherit", encoding: "utf8", shell: true, ...opts });
|
|
27
|
+
}
|
|
28
|
+
function quiet(cmd) {
|
|
29
|
+
try {
|
|
30
|
+
return execSync(cmd, { encoding: "utf8", shell: true, stdio: "pipe" }).toString().trim();
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function fail(msg) {
|
|
36
|
+
console.error(`\n[发布中止] ${msg}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── 0. 解析目标(清单名 或 目录路径)────────────────────────────
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
const syncProfile = args.includes("--sync-profile");
|
|
43
|
+
const dryRun = args.includes("--dry-run"); // 2026-08-29:发布前预览(只推导+打印,不改文件)
|
|
44
|
+
const target = args.find((a) => !a.startsWith("--")) || "";
|
|
45
|
+
let manifest = null;
|
|
46
|
+
let entry = null;
|
|
47
|
+
let dir = null;
|
|
48
|
+
try {
|
|
49
|
+
manifest = JSON.parse(readFileSync(join(SCRIPT_DIR, "plugins.json"), "utf8"));
|
|
50
|
+
entry = manifest.plugins.find((p) => p.name === target) || null;
|
|
51
|
+
} catch {
|
|
52
|
+
// 清单缺失时仅支持目录路径
|
|
53
|
+
}
|
|
54
|
+
if (entry) {
|
|
55
|
+
dir = resolve(WORKSPACE, entry.dir);
|
|
56
|
+
} else if (target) {
|
|
57
|
+
dir = resolve(target);
|
|
58
|
+
} else {
|
|
59
|
+
fail("用法:node release-plugin.mjs <插件名|插件目录> [版本号] [--sync-profile]");
|
|
60
|
+
}
|
|
61
|
+
if (!existsSync(join(dir, "package.json"))) fail(`目录不存在或不是插件包:${dir}`);
|
|
62
|
+
|
|
63
|
+
// ── 0. 读取包信息 ────────────────────────────────────────────────
|
|
64
|
+
const pkgPath = join(dir, "package.json");
|
|
65
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
66
|
+
const name = pkg.name;
|
|
67
|
+
const oldVer = pkg.version;
|
|
68
|
+
// 版本号 = 非 -- 开头的参数(排除目标名本身),位置任意
|
|
69
|
+
// 2026-08-29 修复(0.5.11-pre 体系):oldVer 为 -pre/-rc 等预发布后缀时——
|
|
70
|
+
// ① 发布语义 = 剥后缀发正式版(0.5.11-pre → 发布 0.5.11,不是 +1);
|
|
71
|
+
// ② 自动推导不再 split(".") 直接 Number("11-pre" 会得 NaN——旧 bug)。
|
|
72
|
+
// 显式传参优先;且显式传参如果是 -pre 形态(意外),本发布不剥(警告留痕)。
|
|
73
|
+
const nextVer = args.find((a) => !a.startsWith("--") && a !== target) || (() => {
|
|
74
|
+
// 预发布后缀(-pre/-rc)→ 剥后缀发正式版(0.5.11-pre → 0.5.11)
|
|
75
|
+
if (/-(?:pre|rc|beta|alpha)(?:[.\d]*)$/i.test(oldVer)) return oldVer.replace(/-.*$/, "");
|
|
76
|
+
// 纯数字 → +1(历史行为)
|
|
77
|
+
const [maj, min, pat] = oldVer.split(".").map(Number);
|
|
78
|
+
if (Number.isNaN(pat)) fail(`无法解析版本号:${oldVer}`);
|
|
79
|
+
return `${maj}.${min}.${pat + 1}`;
|
|
80
|
+
})();
|
|
81
|
+
const repoOverride = entry?.repo || null;
|
|
82
|
+
console.log(`\n=== 发布 ${name}: ${oldVer} -> ${nextVer} ===\n`);
|
|
83
|
+
|
|
84
|
+
// ── 0.5 本地配套包版本一致性(防止 host/client 版本错位)──────────
|
|
85
|
+
function checkDependencyPair() {
|
|
86
|
+
if (!entry?.dependsOn || !manifest) return;
|
|
87
|
+
for (const depName of entry.dependsOn) {
|
|
88
|
+
const depEntry = manifest.plugins.find((p) => p.name === depName);
|
|
89
|
+
const depDir = depEntry ? resolve(WORKSPACE, depEntry.dir) : null;
|
|
90
|
+
if (!depDir || !existsSync(join(depDir, "package.json"))) {
|
|
91
|
+
console.log(`(未找到依赖 ${depName} 的本地包,跳过版本一致性检查)`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const depPkg = JSON.parse(readFileSync(join(depDir, "package.json"), "utf8"));
|
|
95
|
+
const range = pkg.dependencies?.[depName];
|
|
96
|
+
if (!range) continue;
|
|
97
|
+
const m = range.match(/^\^(\d+)\.(\d+)\.(\d+)/);
|
|
98
|
+
if (!m) {
|
|
99
|
+
console.log(`(依赖 ${depName} 范围 ${range} 不是简单 ^x.y.z,跳过自动校验)`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const v = depPkg.version.split(".").map(Number);
|
|
103
|
+
const ok = v[0] === +m[1] && (v[1] > +m[2] || (v[1] === +m[2] && v[2] >= +m[3]));
|
|
104
|
+
if (!ok) fail(`${name} 声明依赖 ${depName}@${range},但本地 ${depName} 是 ${depPkg.version}`);
|
|
105
|
+
console.log(`[依赖一致性] ${name} -> ${depName} ${depPkg.version} ✓`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
checkDependencyPair();
|
|
109
|
+
|
|
110
|
+
// ── 1. 前置检查 ──────────────────────────────────────────────────
|
|
111
|
+
if (!/^\d+\.\d+\.\d+$/.test(nextVer)) fail(`版本号格式错误:${nextVer}`);
|
|
112
|
+
if (!quiet("gh auth status").includes("Logged in")) fail("gh 未认证");
|
|
113
|
+
const whoami = quiet("npm whoami");
|
|
114
|
+
if (!whoami) fail("npm 未认证(npm whoami 失败)");
|
|
115
|
+
|
|
116
|
+
// ── 1.5 DSH 兼容预检(发布前防“升级后插件不兼容”)──────────────
|
|
117
|
+
const compatScript = join(SCRIPT_DIR, "check-dsh-compat.mjs");
|
|
118
|
+
if (existsSync(compatScript)) {
|
|
119
|
+
console.log("\n=== DSH 兼容预检 ===");
|
|
120
|
+
run(`node "${compatScript}" "${dir}"`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── 1.6 profile dump-config 冒烟(确认 DSH 可导出当前 profile 配置)──
|
|
124
|
+
function findDshBin() {
|
|
125
|
+
const candidates = [process.env.DSH_BIN, "D:/npm-global/dsh.cmd", "D:/npm-global/dsh", "dsh"].filter(Boolean);
|
|
126
|
+
for (const c of candidates) {
|
|
127
|
+
if (quiet(`where ${c}`)) return c;
|
|
128
|
+
}
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
const dshCmd = findDshBin();
|
|
132
|
+
if (dshCmd) {
|
|
133
|
+
const dump = quiet(`"${dshCmd}" --profile ${manifest?.profile || "web"} --dump-config`);
|
|
134
|
+
if (dump && !/error|Error|ENOENT/i.test(dump)) {
|
|
135
|
+
console.log("(profile dump-config 冒烟通过:DSH 配置可正常导出)");
|
|
136
|
+
} else {
|
|
137
|
+
console.log("(profile dump-config 冒烟未通过:请人工确认 DSH 可用后发布)");
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
console.log("(未找到 dsh 命令,跳过 profile dump-config 冒烟)");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 2. 版本 bump(package.json + README 徽章)────────────────────
|
|
144
|
+
if (dryRun) {
|
|
145
|
+
console.log(`[DRY-RUN] 不修改任何文件。`);
|
|
146
|
+
console.log(`[DRY-RUN] oldVer=${oldVer} → nextVer=${nextVer}`);
|
|
147
|
+
const rd = join(dir, "README.md");
|
|
148
|
+
if (existsSync(rd)) {
|
|
149
|
+
const t = readFileSync(rd, "utf8");
|
|
150
|
+
const oldBadgeVer = `version-${oldVer}`;
|
|
151
|
+
const oldBadgeUrl = `version-${oldVer.replace(/-/g, "--")}`;
|
|
152
|
+
const nextUrl = `version-${nextVer}`;
|
|
153
|
+
const replaced = t.replaceAll(oldBadgeVer, nextUrl).replaceAll(oldBadgeUrl, nextUrl);
|
|
154
|
+
console.log(`[DRY-RUN] README 徽章将变为: ${replaced.match(/version-[^\s]+/)?.[0] || "(未命中,检查徽章形态)"}`);
|
|
155
|
+
}
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
writeFileSync(pkgPath, readFileSync(pkgPath, "utf8").replace(`"version": "${oldVer}"`, `"version": "${nextVer}"`));
|
|
159
|
+
for (const readme of ["README.md", "README.en.md"]) {
|
|
160
|
+
const rp = join(dir, readme);
|
|
161
|
+
if (existsSync(rp)) {
|
|
162
|
+
const text = readFileSync(rp, "utf8");
|
|
163
|
+
// 2026-08-29 修复:徽章 URL 编码把 `0.5.11-pre` 转成 `0.5.11--pre`(`-` 被 shields 双横线),
|
|
164
|
+
// 旧正则 `version-0.5.11-pre` 匹配不到 `version-0.5.11--pre` → 徽章漏改(㉙ 机器根因之一)。
|
|
165
|
+
// replaceAll 用字面串(非正则),无需转义。
|
|
166
|
+
const oldBadgeVer = `version-${oldVer}`; // JSON 形态:version-0.5.11-pre
|
|
167
|
+
const oldBadgeUrl = `version-${oldVer.replace(/-/g, "--")}`; // URL 形态:version-0.5.11--pre
|
|
168
|
+
const nextUrl = `version-${nextVer}`;
|
|
169
|
+
const replaced = text
|
|
170
|
+
.replaceAll(oldBadgeVer, nextUrl)
|
|
171
|
+
.replaceAll(oldBadgeUrl, nextUrl);
|
|
172
|
+
if (replaced !== text) writeFileSync(rp, replaced);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
console.log("版本已 bump(package.json + README 徽章)");
|
|
176
|
+
|
|
177
|
+
// ── 3. 测试(规则 23:发布前运行时验证)─────────────────────────
|
|
178
|
+
if (pkg.scripts && pkg.scripts.test) {
|
|
179
|
+
console.log("\n=== 运行测试 ===");
|
|
180
|
+
run(`cd /d "${dir}" && npm test --prefix "${dir}"`);
|
|
181
|
+
} else {
|
|
182
|
+
console.log("(无 test 脚本,跳过)");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── 4. pack + publish ────────────────────────────────────────────
|
|
186
|
+
console.log("\n=== npm pack ===");
|
|
187
|
+
run(`cd /d "${dir}" && npm pack --pack-destination .`);
|
|
188
|
+
const tgz = `${name}-${nextVer}.tgz`;
|
|
189
|
+
if (!existsSync(join(dir, tgz))) fail(`打包产物缺失:${tgz}`);
|
|
190
|
+
|
|
191
|
+
console.log("\n=== npm publish ===");
|
|
192
|
+
run(`cd /d "${dir}" && ${proxyPrefix()}npm publish ${tgz}`);
|
|
193
|
+
|
|
194
|
+
// ── 5. git commit + push(token 经环境变量注入 URL,命令文本不含密钥明文)─────
|
|
195
|
+
console.log("\n=== git commit + push ===");
|
|
196
|
+
// 2026-08-25 修复 pushUrl 双重拼接:origin 可能是 ssh(git@github.com:)、https(https://github.com/)
|
|
197
|
+
// 或裸路径(user/repo)三种形态——统一归一化为 "owner/repo" 路径再拼 token URL(此前 https origin 未剥前缀
|
|
198
|
+
// 导致 "https://github.com/https://github.com/..." 双重 URL,push 404)
|
|
199
|
+
const repo = (repoOverride || quiet(`cd /d "${dir}" && git remote get-url origin`))
|
|
200
|
+
.replace(/^git@github\.com:/, "")
|
|
201
|
+
.replace(/^https?:\/\/github\.com\//, "")
|
|
202
|
+
.replace(/\.git$/, "");
|
|
203
|
+
const token = quiet("gh auth token");
|
|
204
|
+
if (!token) fail("无法获取 gh token");
|
|
205
|
+
// 与 v3.72 同款通道:token 拼进 HTTPS URL 直推(避开沙箱下 msys 凭据管道的 EPERM)
|
|
206
|
+
const pushUrl = `https://jilian-dsh:${token}@github.com/${repo}.git`;
|
|
207
|
+
// 2026-08-31 修复:repoRoot 实测 git 仓库根(插件目录可能只是子目录,如 rules-manager 在 oss 仓库内);
|
|
208
|
+
// stageSpec = 插件目录相对仓库根路径(防 git add -A 误带仓库内无关改动/未跟踪物)
|
|
209
|
+
const repoRoot = quiet(`cd /d "${dir}" && git rev-parse --show-toplevel`).trim();
|
|
210
|
+
const relDir = relative(repoRoot, dir).split(/[\\/]/).join("/");
|
|
211
|
+
const stageSpec = relDir && relDir !== "." ? relDir : "";
|
|
212
|
+
// 2026-08-29 防漏机检(0.5.10 git 欠账事故:git add -u 只更新已跟踪文件,未跟踪新文件
|
|
213
|
+
// (npm pack 按目录打包、天然包含)会漏进 git 提交 → git 与 npm 内容不一致)。
|
|
214
|
+
// 现在:提交前检测未跟踪文件并打印名单,改用 add -A 一并提交(.gitignore 已排除杂质)。
|
|
215
|
+
const untrackedList = quiet(`cd /d "${repoRoot}" && git status --porcelain -- ${stageSpec}`)
|
|
216
|
+
.split("\n").filter((l) => l.startsWith("??")).map((l) => l.slice(3));
|
|
217
|
+
if (untrackedList.length > 0) {
|
|
218
|
+
console.log(`(未跟踪文件 ${untrackedList.length} 个将一并提交:${untrackedList.slice(0, 6).join(", ")}${untrackedList.length > 6 ? " 等" : ""})`);
|
|
219
|
+
}
|
|
220
|
+
run(`cd /d "${repoRoot}" && ${proxyPrefix()}git add -A -- ${stageSpec} && git -c core.autocrlf=false commit -m "release: ${name} v${nextVer}" || exit 0`);
|
|
221
|
+
run(`cd /d "${repoRoot}" && ${proxyPrefix()}git push "${pushUrl}" HEAD`);
|
|
222
|
+
|
|
223
|
+
// ── 6. GitHub Release(带正式 tgz asset,规则 26;清单标记 skipRelease 的包不建)─────
|
|
224
|
+
if (entry?.skipRelease) {
|
|
225
|
+
console.log("(清单标记 skipRelease,跳过 GitHub Release)");
|
|
226
|
+
} else {
|
|
227
|
+
console.log("\n=== GitHub Release ===");
|
|
228
|
+
const notesFile = join(process.env.TEMP || ".", `notes-${name}-${nextVer}.md`);
|
|
229
|
+
writeFileSync(notesFile, `## v${nextVer}\n\nRelease generated by scripts/release-plugin.mjs\n`);
|
|
230
|
+
// 必须在插件目录(git 仓库)内运行:gh release 内部会做 git 检查
|
|
231
|
+
run(`cd /d "${dir}" && ${proxyPrefix()}gh release create v${nextVer} "${join(dir, tgz)}" --title "${name} v${nextVer}" --notes-file "${notesFile}"`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── 7. 本机 profile 同步(--sync-profile:豁免名单 + 装配审计;需 danger-full-access 运行)──
|
|
235
|
+
if (syncProfile) {
|
|
236
|
+
console.log("\n=== 本机 profile 同步 ===");
|
|
237
|
+
let manifest = null;
|
|
238
|
+
try {
|
|
239
|
+
manifest = JSON.parse(readFileSync(join(SCRIPT_DIR, "plugins.json"), "utf8"));
|
|
240
|
+
} catch {
|
|
241
|
+
manifest = { profile: "web" };
|
|
242
|
+
}
|
|
243
|
+
const profileDir = resolve(WORKSPACE, "..", ".dsh", "profiles", manifest.profile || "web");
|
|
244
|
+
const wsYaml = join(profileDir, "pnpm-workspace.yaml");
|
|
245
|
+
if (existsSync(wsYaml)) {
|
|
246
|
+
const raw = readFileSync(wsYaml, "utf8");
|
|
247
|
+
const lineRe = new RegExp(`^( - ${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}@[^\\n]*)$`, "m");
|
|
248
|
+
const m = raw.match(lineRe);
|
|
249
|
+
if (m && !m[1].includes(nextVer)) {
|
|
250
|
+
const bakDir = join(profileDir, "..", ".backups");
|
|
251
|
+
mkdirSync(bakDir, { recursive: true });
|
|
252
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
253
|
+
const bak = join(bakDir, `pnpm-workspace-${stamp}.yaml`);
|
|
254
|
+
copyFileSync(wsYaml, bak);
|
|
255
|
+
const updated = raw.replace(lineRe, `${m[1]} || ${nextVer}`);
|
|
256
|
+
writeFileSync(wsYaml, updated);
|
|
257
|
+
console.log(`豁免名单已追加 ${name}@${nextVer}(备份 ${bak})`);
|
|
258
|
+
} else if (!m) {
|
|
259
|
+
console.log(`(豁免名单无 ${name} 条目,跳过)`);
|
|
260
|
+
} else {
|
|
261
|
+
console.log(`(豁免名单已含 ${nextVer},跳过)`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// 全量装配审计(规则 27)
|
|
265
|
+
const auditScript = join(SCRIPT_DIR, "..", "projects", "oss", "dsh-rule-engine", "scripts", "audit-mount-consistency.mjs");
|
|
266
|
+
if (existsSync(auditScript)) {
|
|
267
|
+
const out = quiet(`node "${auditScript}" --profile ${manifest.profile || "web"}`);
|
|
268
|
+
const pass = /MOUNT CONSISTENT/.test(out);
|
|
269
|
+
console.log(out.split("\n").filter((l) => /RESULT|DUPLICATES|MOUNT|summary/.test(l)).join("\n"));
|
|
270
|
+
if (!pass) fail("装配审计未通过(MOUNT CONSISTENT 未出现),请先处理再重启 DSH");
|
|
271
|
+
console.log("装配审计 MOUNT CONSISTENT ✓");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(`\n=== 发布完成:${name} v${nextVer} ===`);
|
|
276
|
+
console.log(`npm: ${name}@${nextVer}`);
|
|
277
|
+
console.log(`release: https://github.com/${repo}/releases/tag/v${nextVer}`);
|