dsh-rule-engine 0.5.10 → 0.5.12

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.
@@ -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,50 @@ 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|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)\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])>)/i;
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
+ // 0.5.12(F4):PowerShell 别名展开表——写/删除/移动/执行别名 → 标准 cmdlet 名
251
+ // 供 normalizeAliases 做命令规范化(MUTATING_CMD_RE 覆盖完整 cmdlet;别名先展开再判)
252
+ const PS_ALIAS_MAP = [
253
+ [/^rm\b(?![a-z])/i, "Remove-Item"], [/^rd\b(?![a-z])/i, "Remove-Item"], [/^ri\b(?![a-z])/i, "Remove-Item"],
254
+ [/^del\b(?![a-z])/i, "Remove-Item"], [/^erase\b(?![a-z])/i, "Remove-Item"],
255
+ [/^cp\b(?![a-z])/i, "Copy-Item"], [/^copy\b(?![a-z])/i, "Copy-Item"],
256
+ [/^mv\b(?![a-z])/i, "Move-Item"], [/^move\b(?![a-z])/i, "Move-Item"],
257
+ [/^ren\b(?![a-z])/i, "Rename-Item"], [/^iex\b(?![a-z])/i, "Invoke-Expression"],
258
+ [/^gc\b(?![a-z])/i, "Get-Content"], [/^gci\b(?![a-z])/i, "Get-ChildItem"], [/^gi\b(?![a-z])/i, "Get-Item"],
259
+ [/^gp\b(?![a-z])/i, "Get-ItemProperty"], [/^gv\b(?![a-z])/i, "Get-Variable"], [/^gl\b(?![a-z])/i, "Get-Location"]
260
+ ];
261
+ /** 规范化命令文本中的 PS 别名 → 标准 cmdlet 大写(只变换命令头,不动参数/字符串) */
262
+ export function normalizeAliases(command) {
263
+ const s = String(command || "");
264
+ return s.replace(/(?:^|[\s;|&{(])\s*(rm|rd|ri|del|erase|cp|copy|mv|move|ren|iex|gc|gci|gi|gp|gv|gl)(?=\s+)/gi, (m, name) => {
265
+ const hit = PS_ALIAS_MAP.find(([re]) => re.test(name));
266
+ return hit ? `${m.slice(0, m.length - name.length)}${hit[1]}` : m;
267
+ });
268
+ }
269
+ // .NET 静态/实例写方法(MUTATING 补充:Set-Content 等 cmdlet 之外的写路径)
270
+ const MUTATING_METHOD_RE =
271
+ /(?:\[[\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;
272
+ // B1(阶段一,2026-08-28):只读豁免——白名单之外的"分明无害段"(纯变量赋值 / COM 只读打开 /
273
+ // 内容属性读取 / 变量引出),让读取 .doc 的 COM 命令(Open(...,$true)+Content.Text)不再误判写。
274
+ // 保守边界:内容属性赋值($doc.Text=... / $doc.Content.Text=...)是写文档,不豁免。
275
+ const COM_CONTENT_ASSIGN_RE = /\.(?:Text|Content|Value|Range|Paragraphs|Sections|Documents|Document)\s*=/i;
276
+ const ALLOW_SEG_RE =
277
+ /(?:\$[A-Za-z_][\w$]*(?:(?:\.[\w$]+)|(?:\[[^\]]+\]))*\s*=)|(?:\$[\w$.]+\.(?:Content|Range|Text|Value|Paragraphs|Sections|Length|Count|Name|Selected|LinkType|Target|FullName|Extension|Directory|PSPath|Property|Attributes|Mode|LastWriteTime|Exists)\b)|(?:\$[\w$.]+$)|(?:\b(?:Documents|Document)\.(?:Open|Item)\s*\([^)]*,\s*\d+\s*,\s*\$?true\s*\))|(?:\(!?\$[\w$.]+(?:\.\w+)?\)|\([^()\r\n]*\$[\w$.]+\.[\w.]+\s*\))/;
278
+ // 0.5.12(F4):纯字符串/纯输出段(整段是引号包裹字符串或 $() 子表达式)——不是命令,放行。
279
+ // 保守边界:只匹配"整段即字符串"(^...$ 锚定),不让 node -e "..." / curl -o "..." 借道。
280
+ const PURE_STRING_SEG_RE = /^(?:"[^"\r\n]*"|'[^'\r\n]*'|`[^`\r\n]*`|\$\(\s*[^()\r\n]*\s*\))+$/;
281
+ // 剥离 try/catch/finally 控制块:块内语句与主语句同样接受只读判定(Try-* 不是 cmdlet)。
282
+ // 注意:不做全量 [{}] 替换——if/else 子表达式($(if(...){...}else{...}))会被切碎误判。
283
+ function stripControlBlocks(command) {
284
+ return String(command)
285
+ .replace(/try\s*\{([\s\S]*?)\}\s*catch\s*\([^)]*\)\s*\{([\s\S]*?)\}/gi, "$1; $2")
286
+ .replace(/try\s*\{([\s\S]*?)\}\s*catch\s*\{([\s\S]*?)\}/gi, "$1; $2")
287
+ .replace(/try\s*\{([\s\S]*?)\}\s*finally\s*\{([\s\S]*?)\}/gi, "$1; $2")
288
+ .replace(/^[\s]*(?:try|catch(?:\([^)]*\))?|finally)\s*\{[^\S\n]*(?:#.*)?\s*$/gm, ";")
289
+ .replace(/^\s*\}\s*(?:catch(?:\([^)]*\))?|finally)?\s*\{?\s*$/gm, ";");
290
+ }
243
291
 
244
292
  /** 写/删除/提交类命令判定(0.5.10 单真源:guard-core self-protect 等全部复用——修复 Write-Output 被 `write` 子串误杀) */
245
293
  export function isMutationCommand(command) {
@@ -256,14 +304,29 @@ function splitCommandSegments(command) {
256
304
 
257
305
  /** 判断命令文本是否只读(读文件/查询类,无写入/删除/提交副作用)。
258
306
  * 2026-08-24 分段化:整条命令按 ; | & 换行拆段,每段都必须只读——
259
- * 修复 "git -C <dir> status" 变体不匹配、"git status; echo ok" 链式被误判为写 的实测缺陷。 */
307
+ * 修复 "git -C <dir> status" 变体不匹配、"git status; echo ok" 链式被误判为写 的实测缺陷。
308
+ * B1(2026-08-28 阶段一):段判定三档化,修复只读豁免失效(读取 .doc 的 COM 命令被判写):
309
+ * ① 写特征(MUTATING_CMD_RE / MUTATING_METHOD_RE / 内容属性赋值)→ 非只读;
310
+ * ② 白名单命令 / 分明无害段(纯变量赋值、COM 只读打开、内容属性读取、变量引出)→ 只读;
311
+ * ③ 其它不认识的段 → 保守判非只读(不放大白名单,宁紧勿松)。 */
260
312
  export function isReadOnlyCommand(command) {
261
313
  if (typeof command !== "string") return false;
262
- const segments = splitCommandSegments(command);
314
+ // 0.5.12(F4):先做别名规范化(rm→Remove-Item、gc→Get-Content…),
315
+ // 保证 MUTATING/READONLY 词表只看标准 cmdlet(别名双向缺口:写漏 rm/del/iex、读漏 gc/gci)
316
+ const normalized = normalizeAliases(command);
317
+ const segments = splitCommandSegments(stripControlBlocks(normalized));
263
318
  if (segments.length === 0) return false;
264
319
  return segments.every((seg) => {
265
- if (MUTATING_CMD_RE.test(seg)) return false;
266
- return READONLY_CMD_RE.test(seg);
320
+ const s = String(seg).trim();
321
+ if (!s) return true; // 剥离残留的空段放行
322
+ if (s.startsWith("#")) return true;
323
+ if (MUTATING_CMD_RE.test(s)) return false;
324
+ if (MUTATING_METHOD_RE.test(s)) return false;
325
+ if (COM_CONTENT_ASSIGN_RE.test(s)) return false; // $doc.Text=... 是写文档
326
+ if (READONLY_CMD_RE.test(s)) return true;
327
+ if (ALLOW_SEG_RE.test(s)) return true;
328
+ if (PURE_STRING_SEG_RE.test(s)) return true; // 0.5.12(F4):纯字符串/输出段(非命令)
329
+ return false; // 看不清的段:保守按非只读处理
267
330
  });
268
331
  }
269
332
 
@@ -279,11 +342,14 @@ export function isReadOnlyTool(toolName, args) {
279
342
  }
280
343
 
281
344
  // ═══════════ 0.5.10 分析通道(用户多次提出"只读分析需要写临时脚本/输出")═══════════
282
- // 两类区:① 分析脚本区(scripts/analysis/*.mjs,仓库内可审查)② 分析临时区(logs/ .analysis-tmp/ .backups/,
283
- // 输出/解压副本/分析脚本产物——不纳入 git 跟踪、不属正式变更)。红线由 guard-core 追加(工作区外/受保护名/覆盖既有)。
345
+ // 0.5.11(用户定稿):移除"repo scripts/*.mjs 一律=分析"的语义豁免——脚本名判不了脚本干什么,
346
+ // 执行类脚本(release/inject/uninstall 等)会借道放行(执行口径漏洞)。语义判给模型;
347
+ // 本通道只保留物理判据:严格只读(isReadOnlyTool)+ 分析临时区写(logs/.analysis-tmp/.backups
348
+ // 按路径段机械判定)+ 下载到临时区。红线由 guard-core 追加(工作区外/受保护名/覆盖既有)。
284
349
  const ANALYSIS_SCRATCH_SEG_RE = /(?:^|[\\/])(?:logs|\.analysis-tmp|\.backups)[\\/]/i;
285
- // 分析脚本区:仓库内 scripts/ 任意 .mjs(可审查工具库——unzstd/聚合日志/诊断脚本)+ scripts/analysis/ 专用
286
- const ANALYSIS_SCRIPT_RE = /node\s+[^&;|]*[\\/]scripts[\\/][^\s&;|"]+\.mjs/i;
350
+ // 0.5.10 建议:远程下载到临时区(调研场景 curl/iwr/wget -o 临时区 → analysis;12C 联网审查照旧)
351
+ // 目标支持引号包裹(含空格路径)+ 裸 token
352
+ const DOWNLOAD_OUT_RE = /(?:curl|wget|iwr|Invoke-WebRequest)(?:(?!\n)[^&;|])*?(?:-o\s+|OutFile\s+|>\s*)\s*(?:'([^']+)'|"([^"]+)"|([^\s"'&;|]+))/i;
287
353
 
288
354
  /** 是否分析临时区路径(logs/.analysis-tmp/.backups 段;工作区外红线由 guard-core 用 isOutsideWorkspace 把关) */
289
355
  export function isAnalysisScratchPath(p) {
@@ -302,8 +368,9 @@ export function extractAnalysisScratchPaths(command) {
302
368
  }
303
369
 
304
370
  /**
305
- * 分析类操作判定(0.5.10 单真源):true = 可按 analysis 放行(无副作用,或副作用仅限分析临时区)。
306
- * 场景:① 严格只读(isReadOnlyTool);② 分析脚本区调用(node scripts/analysis/*.mjs);
371
+ * 分析类操作判定(0.5.10 单真源;0.5.11 修订):true = 可按 analysis 放行(无副作用,或副作用仅限分析临时区)。
372
+ * 场景:① 严格只读(isReadOnlyTool);②(已移出,0.5.11)脚本区调用不再按"目录=分析"放行——
373
+ * 脚本名判不了脚本行为,执行类脚本(release/inject/uninject 等)曾借道放行;语义判给模型;
307
374
  * ③ 临时区写(命令含写特征且提取到的目标路径全部命中分析临时区,且无受保护文件名)。
308
375
  * 红线(guard-core 追加,本函数不含):工作区外路径(isOutsideWorkspace)、受保护名、覆盖既有正式文件。
309
376
  */
@@ -311,12 +378,16 @@ export function isAnalysisOp(toolName, args) {
311
378
  const name = String(toolName || "");
312
379
  if (isReadOnlyTool(name, args)) return true;
313
380
  const cmd = commandText(args) || "";
314
- if (name === "node" || name === "pwsh" || name === "bash") {
315
- // ② 分析脚本区调用(脚本自身可写临时区/读全局;命令无其他副作用特征即可)
316
- if (ANALYSIS_SCRIPT_RE.test(cmd)) {
317
- const paths = extractAnalysisScratchPaths(cmd);
318
- if (paths.every((p) => isAnalysisScratchPath(p)) && !PROTECTED_FILENAME_RE.test(cmd)) return true;
381
+ // A 方案(2026-08-28):write/edit 工具直接写分析临时区(logs/.analysis-tmp/.backups 路径段)
382
+ // → 分析通道放行(方案回合"写临时脚本/产物"是调研工具,不是落盘;0.5.10 只覆盖 pwsh 命令,
383
+ // write 工具写临时区此前误判正式路径 → A 拦截误伤调研工具)。红线同 pwsh:受保护名不豁免。
384
+ if (name === "write" || (name === "str_replace_editor" && args?.command !== "view")) {
385
+ const p = pathTarget(args);
386
+ if (p && isAnalysisScratchPath(p) && !PROTECTED_FILENAME_RE.test(p)) {
387
+ return true;
319
388
  }
389
+ }
390
+ if (name === "node" || name === "pwsh" || name === "bash") {
320
391
  // ③ 临时区写:写命令(不含删除/移动——删除/移动属 13A 红线,不走分析豁免)+ 提取路径全部命中临时区 + 无受保护名
321
392
  if (isMutationCommand(cmd) && !/(?:Remove-Item|Move-Item|Rename-Item|del\s|rm\s|git\s+push|git\s+commit)/i.test(cmd)) {
322
393
  const paths = extractAnalysisScratchPaths(cmd);
@@ -324,6 +395,12 @@ export function isAnalysisOp(toolName, args) {
324
395
  return true;
325
396
  }
326
397
  }
398
+ // ④ 远程下载到分析临时区(调研场景:下载 tgz/网页到临时区再解压查看;12C 联网审批与内网审查照旧)
399
+ const dl = cmd.match(DOWNLOAD_OUT_RE);
400
+ const dlTarget = dl ? (dl[1] || dl[2] || dl[3] || "") : "";
401
+ if (dlTarget && isAnalysisScratchPath(dlTarget) && !PROTECTED_FILENAME_RE.test(cmd)) {
402
+ return true;
403
+ }
327
404
  }
328
405
  return false;
329
406
  }
@@ -534,6 +611,13 @@ export function isLowRiskWorkspaceNew(toolName, args) {
534
611
  if (isOutsideWorkspace(p)) return false;
535
612
  if (isAssemblyMutationTool(name, args)) return false;
536
613
  if (isHighRiskEntryFile(p)) return false;
614
+ // 0.5.11(用户定稿):豁免原文是"工作区内低风险**新建**"——"新建"=目标尚不存在。
615
+ // 已存在的文件无论编辑还是覆盖写入都算变更(改旧文件),不是新建 → 不豁免,走正常授权判定。
616
+ // 相对路径无法判定存在性 → 保守不豁免(宁要授权,不放行未知目标的覆盖)。
617
+ if (p) {
618
+ if (!isAbsolute(p)) return false;
619
+ if (existsSync(p)) return false;
620
+ }
537
621
  return true;
538
622
  }
539
623
 
@@ -589,12 +673,24 @@ export function isNegatingSuggestion(text) {
589
673
  }
590
674
 
591
675
  /**
592
- * v0.5.7 P0.5-5(用户拍板):承诺词处于引述/改写语境("把'保证'改成…")→ 是引述不是承诺。
593
- * 判定:词前 8 字符内配对开引号 + 词后 8 字符内配对闭引号;或前后 12 字符内引述标记词。
676
+ * 通用引述/转述语境判定(2026-08-29 B3,取代 isPromiseQuoteContext 单一用途):
677
+ * 给定词表 wordRe 每次命中,处于 ① 引号包裹(词两侧 8 字符内配对引号)或
678
+ * ② 转述/改写语境 → 视为引述而非断言(规则 2 时间词 / 规则 7 承诺词通用)。
679
+ * 转述语境 = 第三人称对象 + 说/表示/提到…(before 14 字符内),或引述/转述/原文/改成类标记。
680
+ * 不豁免第一人称("我说保证"仍按承诺——承诺转述不了自己)。
681
+ * 修复背景:B3 规则 7 仅认引号+白名单标记词,无引号转述("用户之前说万无一失")误报;
682
+ * 规则 2 原本无引述豁免(引述用户原话"昨天"也报未核对)。
594
683
  */
595
- export function isPromiseQuoteContext(text) {
596
- if (typeof text !== "string") return false;
597
- for (const m of text.matchAll(/(?:包在我身上|肯定能|绝对没问题|保证|一定可以|万无一失)/g)) {
684
+ const PARAPHRASE_BEFORE_RE =
685
+ /(?:用户|你|他|她|它|对方|作者|维护者|客户|大家|网友|某人|群友|别人).{0,6}?(?:说|说过|表示|提到|提到过|称|强调|认为|写道|原话)/;
686
+ const PARAPHRASE_MARK_BEFORE_RE = /引述|引用|原文|转述|转发|据\S{0,5}说|写过|改成|改为|换成/;
687
+ const PARAPHRASE_MARK_AFTER_RE = /改成|改为|换成|转述|引用/;
688
+
689
+ export function isQuoteOrParaphraseContext(text, wordRe) {
690
+ if (typeof text !== "string" || !(wordRe instanceof RegExp)) return false;
691
+ // matchAll 要求全局正则;用副本(不污染共享词表的 lastIndex)
692
+ const re = new RegExp(wordRe.source, wordRe.flags.includes("g") ? wordRe.flags : wordRe.flags + "g");
693
+ for (const m of text.matchAll(re)) {
598
694
  const before = text.slice(0, m.index);
599
695
  const after = text.slice(m.index + m[0].length);
600
696
  // 引述包装 = 承诺词两侧 8 字符内都有引号(ASCII/中文弯引号均算;不区分左右——
@@ -602,7 +698,14 @@ export function isPromiseQuoteContext(text) {
602
698
  const openNear = /[“"「『‘'””」』’]/.test(before.slice(-8));
603
699
  const closeNear = /[“"「『‘'””」』’]/.test(after.slice(0, 8));
604
700
  if (openNear && closeNear) return true;
605
- if (/引述|引用|原文|写过|改成|改为|换成/.test(before.slice(-12)) || /改成|改为|换成/.test(after.slice(0, 12))) return true;
701
+ if (PARAPHRASE_BEFORE_RE.test(before.slice(-14))) return true;
702
+ if (PARAPHRASE_MARK_BEFORE_RE.test(before.slice(-12))) return true;
703
+ if (PARAPHRASE_MARK_AFTER_RE.test(after.slice(0, 12))) return true;
606
704
  }
607
705
  return false;
608
706
  }
707
+
708
+ /** v0.5.7 P0.5-5(用户拍板)+ B3 扩展:承诺词处于引述/改写/转述语境 → 是引述不是承诺 */
709
+ export function isPromiseQuoteContext(text) {
710
+ return isQuoteOrParaphraseContext(text, PROMISE_WORDS);
711
+ }