dsh-grill-first 0.1.3 → 0.1.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## 0.1.4
6
+
7
+ "真的按你确认过的意思做"版本:修掉四个"配置写了但不生效"的地方,并让询问类消息不再被当成模糊任务。
8
+
9
+ ### 新增
10
+
11
+ - **`gateMode: "ask"` 档位**:需求没确认时,变更类操作弹一张 DSH 原生确认卡片问你"照现在的理解直接动手吗",你点头就放行——不想切 `strict` 硬门禁、又不想写配置文件的人用这个。卡片只弹一次,之后本次任务照常跑。
12
+ - **`memoryScope` 配置**:项目背景与历史默认存工作区;改成 `global` 则落到全局目录的 `project.json`,跨项目共享。
13
+ - **问句不再触发澄清**:问号、"怎么样 / 查一下 / 看看 / 解释一下 / 我想知道" 这类询问,以及 `look at / check my / tell me / explain` 这类英文问句,一律直接放行并接话作答——0.1.3 及以前会被当成"模糊任务"要求先澄清。
14
+ - **`intensity` 真正生效**:`gentle` 两维齐就放行、`standard` 三维、`grill` 还要求写清验收标准(以前三个档位阈值完全一样)。
15
+ - **英文任务也能判**:四张维度词表补了英文同义词(write/implement/refactor、only/scope、use/based on、test/verify…),英文按词边界匹配,不会把 `skipList` 误当跳过词。
16
+
17
+ ### 修复
18
+
19
+ - **`maxRounds` 现在真的会到上限**:以前 `grill_start` 每次都把轮数清零,问多少轮都不会停;现在问满上限后直接放行,并在返回值里给出 `rounds_used`。
20
+ - **用户追问不再把门禁关回去**:以前任何一条用户消息都会重置门禁,"继续""嗯""好的"也会;现在只把**新任务**当成新一轮,追问/答话不会打断进行中的工作。
21
+ - **历史记录不再无限增长**:项目历史只保留最近 100 条(摘要只用最近 3 条,多的读不到只是白占地方)。
22
+ - **删掉 `grill_memory` 这个不存在的只读白名单条目**(0.1.3 之前遗留的死引用)。
23
+
24
+ ### 内部
25
+
26
+ - 单测/端到端用例从 94 个补到 127 个,新增:问句识别、intensity 阈值、`ask` 档位、追问不重置门禁、`maxRounds` 上限、`memoryScope`、历史裁剪,以及一张"主人真实说过的 11 句话"回归样例表。
27
+ - CI 增加 `npm run typecheck`;新增 `.github/workflows/release.yml`:打 `v*` tag 自动发 npm(需要仓库 Secrets 配 `NPM_TOKEN`)。
28
+
5
29
  ## 0.1.3
6
30
 
7
31
  文档修正版。
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 InoriMutsumi
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 InoriMutsumi
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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-grill-first
2
2
 
3
- ![tests](https://img.shields.io/badge/tests-91%20passing-brightgreen)
3
+ ![tests](https://img.shields.io/badge/tests-127%20passing-brightgreen)
4
4
  ![license](https://img.shields.io/badge/license-MIT-blue)
5
5
  ![DSH plugin](https://img.shields.io/badge/DSH-plugin-blueviolet)
6
6
 
@@ -18,6 +18,8 @@ AI 助手最常见的失败不是"不会做",而是"做完了才发现做的
18
18
 
19
19
  严格档位(`gateMode: "strict"`)下,需求没确认前模型无法执行写文件、跑命令这类**变更类**操作——从机制上保证"没问清就不许动手"。
20
20
 
21
+ `ask` 档位(0.1.4 新增)介于两者之间:需求没确认时,变更类操作会弹一张 DSH 原生确认卡片问你"照现在的理解直接动手吗",你点「是」就放行,点「否」由你补一句需求——不写配置文件也能当场决定。
22
+
21
23
  ## 安装
22
24
 
23
25
  装插件前请先退出正在跑的任务。三种方式任选:
@@ -56,38 +58,41 @@ dsh plugin --profile <你的 profile 名> add github:InoriMutsumi/dsh-grill-firs
56
58
  ### 想跳过提问时
57
59
 
58
60
  - 消息里带上「**直接做**」「**跳过提问**」「**不用问**」「**skip**」中任意一个 → 立刻开工;
61
+ - 只是**问情况**("这个插件怎么样""查一下我的仓库里有什么""解释一下这段代码")→ 不算新任务,不追问;
59
62
  - 或者在设置里把 `enabled` 关掉 / 把 `gateMode` 调成 `off` → 整个插件静默。
60
63
 
61
64
  ## 配置项
62
65
 
63
- DSH 设置界面 → 插件 → dsh-grill-first,共 7 项:
66
+ DSH 设置界面 → 插件 → dsh-grill-first,共 8 项:
64
67
 
65
68
  | 配置 | 类型 | 默认 | 说明 |
66
69
  |---|---|---|---|
67
70
  | `enabled` | 布尔 | `true` | 总开关。关闭后既不追问也不拦截工具 |
68
- | `gateMode` | `off` / `warn` / `strict` | `warn` | `off` 不拦截;`warn` 只在日志里记录"需求未确认";`strict` 未确认前拦截变更类工具 |
71
+ | `gateMode` | `off` / `warn` / `ask` / `strict` | `warn` | `off` 不拦截;`warn` 只在日志里记录"需求未确认";`ask` 未确认时弹卡片问你;`strict` 未确认前直接拦截变更类工具 |
69
72
  | `maxQuestionsPerRound` | 1-6 | `3` | 每轮最多问几个问题 |
70
- | `maxRounds` | 1-10 | `3` | 最多追问几轮 |
71
- | `intensity` | `gentle` / `standard` / `grill` | `standard` | 追问力度:只问关键点 / 标准 / 刨根问底 |
73
+ | `maxRounds` | 1-10 | `3` | 最多追问几轮;问满后 `grill_start` 直接放行 |
74
+ | `intensity` | `gentle` / `standard` / `grill` | `standard` | 追问力度:`gentle` 两维齐就放行 / `standard` 三维 / `grill` 还要求写清验收标准 |
72
75
  | `bypassKeywords` | 字符串数组 | `["直接做","跳过提问","不用问","skip"]` | 命中即跳过提问 |
73
76
  | `memoryEnabled` | 布尔 | `true` | 是否启用两层记忆 |
77
+ | `memoryScope` | `project` / `global` | `project` | 项目背景/历史存哪:`project` 存工作区,`global` 存全局目录(跨项目共享) |
74
78
 
75
79
  > 默认是 `warn` 而不是 `strict`:默认档位保证升级插件不会突然卡住你已有的工作流。想要硬门禁请手动切 `strict`。
76
80
 
77
81
  ### 怎么改配置
78
82
 
79
- DSH 设置页里的插件条目目前只提供「启用 / 停用」开关(客户端的插件页不渲染第三方插件的配置表单),要改上面 7 项请编辑 **profile 的补丁层** `cordis.patch.yml`:
83
+ DSH 设置页里的插件条目目前只提供「启用 / 停用」开关(客户端的插件页不渲染第三方插件的配置表单),要改上面 8 项请编辑 **profile 的补丁层** `cordis.patch.yml`:
80
84
 
81
85
  ```yaml
82
86
  # <profile 目录>/cordis.patch.yml
83
87
  - id: dsh-grill-first
84
88
  name: dsh-grill-first
85
89
  config:
86
- gateMode: strict # off | warn | strict
90
+ gateMode: strict # off | warn | ask | strict
87
91
  maxQuestionsPerRound: 3 # 1-6
88
92
  maxRounds: 3 # 1-10
89
93
  intensity: standard # gentle | standard | grill
90
94
  memoryEnabled: true
95
+ memoryScope: project # project | global
91
96
  bypassKeywords: [直接做, 跳过提问, 不用问, skip]
92
97
  ```
93
98
 
@@ -102,6 +107,8 @@ DSH 设置页里的插件条目目前只提供「启用 / 停用」开关(客
102
107
  | 全局层 | `<profile 目录>/grill-first/global.json`(不可写时退化到 `~/.dsh-grill-first/global.json`) | 通用偏好,如"注释一律用中文" |
103
108
  | 项目层 | `<工作区>/.dsh/grill-first/memory.json` | 项目背景、任务历史(任务名 + 需求摘要 + 日期) |
104
109
 
110
+ 项目层默认落在工作区;把 `memoryScope` 改成 `global` 后改落 `<全局目录>/project.json`——适合"所有项目通用"的背景约定,不想往每个仓库里塞 `.dsh/` 时也用这个。历史记录只保留最近 100 条(0.1.4 起)。
111
+
105
112
  下次追问时,`grill_start` 会把这些内容作为 `memory_context` 返回,模型就不会重复问你同一件事。
106
113
 
107
114
  ## 开发
@@ -109,7 +116,7 @@ DSH 设置页里的插件条目目前只提供「启用 / 停用」开关(客
109
116
  ```bash
110
117
  npm install # 装开发依赖(schemastery 由宿主提供,仅开发期需要)
111
118
  npm run build # TypeScript → dist/
112
- npm test # 构建 + 94 个单测 / 端到端场景测试
119
+ npm test # 构建 + 127 个单测 / 端到端场景测试
113
120
  node scripts/selfcheck.mjs # 静态自检:中文 JSDoc、any 用量、需求→代码位置
114
121
  ```
115
122
 
@@ -121,7 +128,7 @@ node scripts/selfcheck.mjs # 静态自检:中文 JSDoc、any 用量、需求
121
128
  src/
122
129
  index.ts 插件装配入口(工具 + 协议段 + 门禁)
123
130
  config.ts 配置归一化(不依赖宿主,可单测)
124
- configSchema.ts Schemastery 配置声明(设置界面用,7 项)
131
+ configSchema.ts Schemastery 配置声明(设置界面用,8 项)
125
132
  version.ts 从 package.json 读版本号,供 grill_start 返回
126
133
  judge/heuristics.ts 任务模糊度判定(纯函数)
127
134
  prompt/protocol.ts 注入系统提示的中文协议文本
@@ -145,7 +152,7 @@ src/
145
152
  > DSH 自己的门禁用 `semver.satisfies(..., { includePrerelease: true })` 判定,`^0.2.0-rc.2` 能匹配 `0.2.1-rc.2`;
146
153
  > 但标准 npm 语义(不加 `includePrerelease`)会判不匹配,所以上游要求写显式分支。
147
154
 
148
- 已验证的宿主版本:`0.2.0-rc.2`(0.1.x 开发期)、`0.2.1-rc.2`(2026-10-17 复核:`typecheck`、94 个单测、`verify-installed` 一致,且实测加载运行)。升完 DSH 后的自检三步:
155
+ 已验证的宿主版本:`0.2.0-rc.2`(0.1.x 开发期)、`0.2.1-rc.2`(2026-10-17 复核:`typecheck`、94 个单测、`verify-installed` 一致,且实测加载运行;0.1.4 起 127 个单测)。升完 DSH 后的自检三步:
149
156
 
150
157
  ```bash
151
158
  node scripts/verify-installed.mjs # 安装拷贝与仓库产物是否一致
@@ -156,6 +163,8 @@ node scripts/verify-installed.mjs # 安装拷贝与仓库产物是否一致
156
163
  ## 已知限制
157
164
 
158
165
  - 门禁只对**根 agent**(能直接跟你对话的那个)生效;子 agent / 团队成员不拦,否则正常的委派工作流会被卡死。因此**委派类工具本身**(`subagent`、`workflow`、`spawn_teammate`…)在未确认前也会被拦,防止"派个子代理替自己动手"绕过门禁。
166
+ - `ask` 档位**只问一次**:卡片弹过之后门禁即视为放行(否则一次任务里写 20 个文件就要弹 20 张卡片)。想每次都拦,请用 `strict`。
167
+ - 判定器是**关键词启发式**,不是语义模型:问句靠问号/疑问词/"查一下/看看"这类信号识别,极少数既像提问又像下令的句子可能判错;判错时直接说「直接做」或让模型调一次 `grill_start` 即可纠正。英文关键词按词边界匹配,中文按子串匹配。
159
168
  - `warn` 档位只能"记录不拦截":DSH 的 `tools/pre-execute` 事件没有"放行但同时给模型提示"这种返回形态。
160
169
  - 记忆是本地文件,不含加密;删掉对应 json 即清空。
161
170
  - `grill_confirm` 优先走 DSH 的结构化提问通道;该通道不可用时(例如子 agent 内部)会返回 `needs_user_confirmation: true`,由模型改用内置提问工具兜底。
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
- # DSH bundle 补丁:把本插件插入当前 profile 的插件表(格式参照已装插件 dsh-find-plugins)。
2
- - insert:
3
- - id: dsh-grill-first
4
- name: 'dsh-grill-first'
1
+ # DSH bundle 补丁:把本插件插入当前 profile 的插件表(格式参照已装插件 dsh-find-plugins)。
2
+ - insert:
3
+ - id: dsh-grill-first
4
+ name: 'dsh-grill-first'
package/dist/config.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- /** 门禁档位:off=不拦截、warn=只记录、strict=拦截变更类工具。 */
2
- export type GateMode = 'off' | 'warn' | 'strict';
1
+ /** 门禁档位:off=不拦截、warn=只记录、strict=拦截变更类工具、ask=先弹卡片问用户。 */
2
+ export type GateMode = 'off' | 'warn' | 'strict' | 'ask';
3
+ /** 项目层记忆的落点:project=跟着当前工作区走、global=全局共享一份。 */
4
+ export type MemoryScope = 'project' | 'global';
3
5
  /** 追问力度。 */
4
6
  export type Intensity = 'gentle' | 'standard' | 'grill';
5
7
  /** 归一化后的插件配置。 */
@@ -18,6 +20,8 @@ export interface GrillConfig {
18
20
  bypassKeywords: string[];
19
21
  /** 是否启用两层记忆(R5)。 */
20
22
  memoryEnabled: boolean;
23
+ /** 项目层记忆落在哪里(0.1.4 新增:有些项目不想被写进 .dsh/)。 */
24
+ memoryScope: MemoryScope;
21
25
  }
22
26
  /** 默认配置(与工作表 3.7 的默认值一致)。 */
23
27
  export declare const DEFAULT_CONFIG: GrillConfig;
package/dist/config.js CHANGED
@@ -14,6 +14,7 @@ export const DEFAULT_CONFIG = {
14
14
  intensity: 'standard',
15
15
  bypassKeywords: [...DEFAULT_BYPASS_KEYWORDS],
16
16
  memoryEnabled: true,
17
+ memoryScope: 'project',
17
18
  };
18
19
  /** 取整并夹到 [min, max];非法值返回 fallback。 */
19
20
  function clampInt(value, min, max, fallback) {
@@ -53,11 +54,12 @@ export function normalizeConfig(raw) {
53
54
  const input = (raw ?? {});
54
55
  return {
55
56
  enabled: pickBoolean(input.enabled, DEFAULT_CONFIG.enabled),
56
- gateMode: pickEnum(input.gateMode, ['off', 'warn', 'strict'], DEFAULT_CONFIG.gateMode),
57
+ gateMode: pickEnum(input.gateMode, ['off', 'warn', 'strict', 'ask'], DEFAULT_CONFIG.gateMode),
57
58
  maxQuestionsPerRound: clampInt(input.maxQuestionsPerRound, 1, 6, DEFAULT_CONFIG.maxQuestionsPerRound),
58
59
  maxRounds: clampInt(input.maxRounds, 1, 10, DEFAULT_CONFIG.maxRounds),
59
60
  intensity: pickEnum(input.intensity, ['gentle', 'standard', 'grill'], DEFAULT_CONFIG.intensity),
60
61
  bypassKeywords: pickStringList(input.bypassKeywords, DEFAULT_CONFIG.bypassKeywords),
61
62
  memoryEnabled: pickBoolean(input.memoryEnabled, DEFAULT_CONFIG.memoryEnabled),
63
+ memoryScope: pickEnum(input.memoryScope, ['project', 'global'], DEFAULT_CONFIG.memoryScope),
62
64
  };
63
65
  }
@@ -10,18 +10,20 @@ import z from '@deepseek-ai/schemastery';
10
10
  /** 插件配置 schema:改了这里,设置界面与 cordis.yml 校验同时生效。 */
11
11
  export declare const Config: z<Schemastery.ObjectS<NoInfer<{
12
12
  enabled: z<boolean, boolean, "defined">;
13
- gateMode: z<"off" | "warn" | "strict", "off" | "warn" | "strict", "defined">;
13
+ gateMode: z<"off" | "warn" | "strict" | "ask", "off" | "warn" | "strict" | "ask", "defined">;
14
14
  maxQuestionsPerRound: z<number, number, "defined">;
15
15
  maxRounds: z<number, number, "defined">;
16
16
  intensity: z<"gentle" | "standard" | "grill", "gentle" | "standard" | "grill", "defined">;
17
17
  bypassKeywords: z<string[], string[], "defined">;
18
18
  memoryEnabled: z<boolean, boolean, "defined">;
19
+ memoryScope: z<"project" | "global", "project" | "global", "defined">;
19
20
  }>>, Schemastery.ObjectT<NoInfer<{
20
21
  enabled: z<boolean, boolean, "defined">;
21
- gateMode: z<"off" | "warn" | "strict", "off" | "warn" | "strict", "defined">;
22
+ gateMode: z<"off" | "warn" | "strict" | "ask", "off" | "warn" | "strict" | "ask", "defined">;
22
23
  maxQuestionsPerRound: z<number, number, "defined">;
23
24
  maxRounds: z<number, number, "defined">;
24
25
  intensity: z<"gentle" | "standard" | "grill", "gentle" | "standard" | "grill", "defined">;
25
26
  bypassKeywords: z<string[], string[], "defined">;
26
27
  memoryEnabled: z<boolean, boolean, "defined">;
28
+ memoryScope: z<"project" | "global", "project" | "global", "defined">;
27
29
  }>>, "plain">;
@@ -9,13 +9,15 @@
9
9
  import z from '@deepseek-ai/schemastery';
10
10
  import { DEFAULT_BYPASS_KEYWORDS } from './judge/heuristics.js';
11
11
  /** 门禁档位 schema。 */
12
- const gateModeSchema = z.union([z.const('off'), z.const('warn'), z.const('strict')]).default('warn');
12
+ const gateModeSchema = z
13
+ .union([z.const('off'), z.const('warn'), z.const('strict'), z.const('ask')])
14
+ .default('warn');
13
15
  /** 追问力度 schema。 */
14
16
  const intensitySchema = z.union([z.const('gentle'), z.const('standard'), z.const('grill')]).default('standard');
15
17
  /** 插件配置 schema:改了这里,设置界面与 cordis.yml 校验同时生效。 */
16
18
  export const Config = z.object({
17
19
  enabled: z.boolean().default(true).description('总开关:关闭后任何任务都不再追问,也不拦截工具。'),
18
- gateMode: gateModeSchema.description('门禁档位:off 不拦截 / warn 只记录 / strict 未确认需求前拦截写类工具。'),
20
+ gateMode: gateModeSchema.description('门禁档位:off 不拦截 / warn 只记录 / strict 未确认需求前拦截写类工具 / ask 拦截前先弹卡片问一句。'),
19
21
  maxQuestionsPerRound: z
20
22
  .natural()
21
23
  .max(6)
@@ -28,4 +30,8 @@ export const Config = z.object({
28
30
  .default([...DEFAULT_BYPASS_KEYWORDS])
29
31
  .description('跳过关键词:用户消息里出现这些词就不再追问,直接开工。'),
30
32
  memoryEnabled: z.boolean().default(true).description('是否启用两层记忆(全局偏好 + 项目背景/历史)。'),
33
+ memoryScope: z
34
+ .union([z.const('project'), z.const('global')])
35
+ .default('project')
36
+ .description('项目层记忆落点:project 写进当前工作区的 .dsh/grill-first/,global 写进插件家目录。'),
31
37
  });
@@ -9,8 +9,8 @@
9
9
  * 3. warn 模式只记录不拦截(pre-execute 没有"放行 + 附带提示"这种返回形态)。
10
10
  */
11
11
  import type { GrillContext } from '../types.js';
12
- /** 门禁档位。 */
13
- export type GateMode = 'off' | 'warn' | 'strict';
12
+ /** 门禁档位:ask 档在拦截前先弹一张"要不要继续"的卡片,交给人决定(0.1.4 新增)。 */
13
+ export type GateMode = 'off' | 'warn' | 'strict' | 'ask';
14
14
  /** 门禁状态:open = 已确认或无需确认;closed = 尚未确认。 */
15
15
  export type GateState = 'open' | 'closed';
16
16
  /** 变更类工具名单(宁松勿紧)。 */
@@ -19,14 +19,16 @@ export declare const MUTATING_TOOLS: readonly string[];
19
19
  export declare const READ_ONLY_TOOLS: readonly string[];
20
20
  /** 拦截时给模型看的拒绝文案(工作表 3.6 原文)。 */
21
21
  export declare const GATE_DENY_REASON = "\u9700\u6C42\u5C1A\u672A\u786E\u8BA4\uFF1A\u8BF7\u5148\u8C03\u7528 grill_confirm \u5B8C\u6210\u9700\u6C42\u786E\u8BA4\uFF0C\u6216\u8BA9\u7528\u6237\u8BF4'\u76F4\u63A5\u505A'\u8DF3\u8FC7\u3002";
22
+ /** ask 档弹给用户看的说明(宿主用 displayReason 渲染卡片正文)。 */
23
+ export declare const GATE_ASK_REASON = "\u8FD9\u6B21\u4EFB\u52A1\u7684\u9700\u6C42\u8FD8\u6CA1\u786E\u8BA4\uFF0C\u8981\u7167\u73B0\u5728\u7684\u7406\u89E3\u76F4\u63A5\u52A8\u624B\u5417\uFF1F";
22
24
  /**
23
25
  * 判断某个工具名是否属于"会真正改动工作区/交付物"的变更类工具。
24
26
  * @param name 工具名。
25
27
  */
26
28
  export declare function isMutatingTool(name: string): boolean;
27
- /** 门禁判定结果。 */
29
+ /** 门禁判定结果:ask = 弹卡片问用户后由其决定(宿主 PreToolDecision 支持)。 */
28
30
  export interface GateDecision {
29
- action: 'allow' | 'deny';
31
+ action: 'allow' | 'deny' | 'ask';
30
32
  reason?: string;
31
33
  }
32
34
  /**
@@ -60,6 +62,14 @@ export interface GateOptions {
60
62
  mode: GateMode;
61
63
  /** warn 模式下的提示回调(默认写日志)。 */
62
64
  onWarning?: (toolName: string, agentId: string) => void;
65
+ /**
66
+ * 新用户消息到达时,是否把门禁重置为 closed。
67
+ *
68
+ * 0.1.4 修:以前每条用户消息都关门禁,导致"刚确认完、用户回一句继续"又被拦。
69
+ * 现在由判定器看一眼消息内容——追问/询问/说了跳过词之类不关门禁。
70
+ * 返回 true 才关门;未提供该回调时保持旧行为(一律关)。
71
+ */
72
+ shouldResetOnMessage?: (text: string) => boolean;
63
73
  }
64
74
  /**
65
75
  * 把门禁挂到宿主事件上:`tools/pre-execute` 拦截 + `agent/inbox/inserted` 重置。
@@ -50,10 +50,11 @@ export const READ_ONLY_TOOLS = [
50
50
  'update_goal',
51
51
  'grill_start',
52
52
  'grill_confirm',
53
- 'grill_memory',
54
53
  ];
55
54
  /** 拦截时给模型看的拒绝文案(工作表 3.6 原文)。 */
56
55
  export const GATE_DENY_REASON = "需求尚未确认:请先调用 grill_confirm 完成需求确认,或让用户说'直接做'跳过。";
56
+ /** ask 档弹给用户看的说明(宿主用 displayReason 渲染卡片正文)。 */
57
+ export const GATE_ASK_REASON = '这次任务的需求还没确认,要照现在的理解直接动手吗?';
57
58
  const READ_ONLY_SET = new Set(READ_ONLY_TOOLS.map((n) => n.toLowerCase()));
58
59
  const MUTATING_SET = new Set(MUTATING_TOOLS.map((n) => n.toLowerCase()));
59
60
  /** univer_* 里只有这几个是只读的,其余按写类处理。 */
@@ -81,14 +82,46 @@ export function isMutatingTool(name) {
81
82
  * @param input 工具名 + 当前门禁状态 + 档位。
82
83
  */
83
84
  export function decideGate(input) {
84
- if (input.mode !== 'strict')
85
+ // off / warn 只记录不拦截(warn 的提示在 registerGate 里单独打日志)。
86
+ if (input.mode === 'off' || input.mode === 'warn')
85
87
  return { action: 'allow' };
86
88
  if (input.state !== 'closed')
87
89
  return { action: 'allow' };
88
90
  if (!isMutatingTool(input.toolName))
89
91
  return { action: 'allow' };
92
+ if (input.mode === 'ask')
93
+ return { action: 'ask', reason: GATE_DENY_REASON };
90
94
  return { action: 'deny', reason: GATE_DENY_REASON };
91
95
  }
96
+ /**
97
+ * 从宿主的 inbox 负载里尽量捞出用户消息文本。
98
+ *
99
+ * 不同宿主版本的字段名不统一(text / content / parts),取不到时返回空串;
100
+ * 空串表示"不知道用户说了什么",此时保持旧行为(关门禁)最安全。
101
+ * @param payload `agent/inbox/inserted` 的事件负载。
102
+ */
103
+ function extractMessageText(payload) {
104
+ const message = payload?.message;
105
+ if (typeof message === 'string')
106
+ return message.trim();
107
+ if (!message || typeof message !== 'object')
108
+ return '';
109
+ const record = message;
110
+ for (const key of ['text', 'content', 'body', 'prompt', 'parts']) {
111
+ const value = record[key];
112
+ if (typeof value === 'string' && value.trim() !== '')
113
+ return value.trim();
114
+ if (!Array.isArray(value))
115
+ continue;
116
+ const joined = value
117
+ .map((part) => (typeof part === 'string' ? part : part?.text))
118
+ .filter((part) => typeof part === 'string' && part.trim() !== '')
119
+ .join('\n');
120
+ if (joined.trim() !== '')
121
+ return joined.trim();
122
+ }
123
+ return '';
124
+ }
92
125
  /** 每个 agent 的门禁状态表(agent 级隔离)。 */
93
126
  export class GateRegistry {
94
127
  states = new Map();
@@ -144,6 +177,17 @@ export function registerGate(ctx, gates, options) {
144
177
  ctx.logger?.info?.(`[grill-first] 拦截 ${exec.name}(需求未确认,agent=${agentId})`);
145
178
  return { kind: 'deny', reason: decision.reason ?? GATE_DENY_REASON };
146
179
  }
180
+ if (decision.action === 'ask') {
181
+ // 只问一次:卡片弹过之后无论用户点头还是否决,都由用户决定,插件不再逐次弹
182
+ // (否则一次任务里写 20 个文件就会弹 20 张卡片)。
183
+ ctx.logger?.info?.(`[grill-first] 询问 ${exec.name}(需求未确认,agent=${agentId})`);
184
+ gates.open(agentId);
185
+ return {
186
+ kind: 'ask',
187
+ reason: decision.reason ?? GATE_DENY_REASON,
188
+ displayReason: GATE_ASK_REASON,
189
+ };
190
+ }
147
191
  if (options.mode === 'warn' && state === 'closed' && isMutatingTool(exec?.name ?? '')) {
148
192
  options.onWarning?.(exec.name, agentId);
149
193
  }
@@ -159,6 +203,9 @@ export function registerGate(ctx, gates, options) {
159
203
  const agentId = payload?.agent?.id ?? '';
160
204
  if (agentId === '' || !isRootAgent(ctx, agentId))
161
205
  return;
206
+ const text = extractMessageText(payload);
207
+ if (text !== '' && options.shouldResetOnMessage && !options.shouldResetOnMessage(text))
208
+ return;
162
209
  gates.close(agentId);
163
210
  });
164
211
  }
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * 在插件卸载/配置变更重挂载时自动清理。
6
6
  */
7
7
  import { normalizeConfig } from './config.js';
8
+ import { judgeTask } from './judge/heuristics.js';
8
9
  import { Config } from './configSchema.js';
9
10
  import { GateRegistry, registerGate } from './gate/preExecute.js';
10
11
  import { PROTOCOL_SECTION_NAME, PROTOCOL_SECTION_ORDER, buildProtocolText } from './prompt/protocol.js';
@@ -68,6 +69,7 @@ export function apply(ctx, rawConfig) {
68
69
  gates,
69
70
  sessions,
70
71
  workspaceRoot,
72
+ memoryScope: () => config.memoryScope,
71
73
  ...(ctx.logger ? { logger: ctx.logger } : {}),
72
74
  }));
73
75
  register(createGrillConfirmTool({
@@ -75,6 +77,7 @@ export function apply(ctx, rawConfig) {
75
77
  gates,
76
78
  sessions,
77
79
  workspaceRoot,
80
+ memoryScope: () => config.memoryScope,
78
81
  questions: () => ctx.get('userQuestions'),
79
82
  ...(ctx.logger ? { logger: ctx.logger } : {}),
80
83
  }));
@@ -99,6 +102,16 @@ export function apply(ctx, rawConfig) {
99
102
  installProtocolSection(ctx, config);
100
103
  registerGate(ctx, gates, {
101
104
  mode: config.gateMode,
105
+ // 新消息到达时先看一眼内容:追问/询问类不关门禁,否则"刚确认完又被拦"。
106
+ shouldResetOnMessage: (text) => {
107
+ const judged = judgeTask({
108
+ text,
109
+ mode: 'new-task',
110
+ bypassKeywords: config.bypassKeywords,
111
+ intensity: config.intensity,
112
+ });
113
+ return judged.verdict !== 'clear';
114
+ },
102
115
  onWarning: (toolName, agentId) => {
103
116
  ctx.logger?.warn(`[grill-first] 需求尚未确认(warn 模式,仅记录未拦截):${toolName}(agent=${agentId})`);
104
117
  },
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * 纯函数启发式判定(R1):不调 LLM,只看任务文本判断"需求是否已经够清楚"。
3
3
  *
4
- * 判定顺序(先命中先返回):bypass 关键词 → 追问/继续类消息 → 文本过短且无动作 →
4
+ * 判定顺序(先命中先返回):bypass 关键词 → 追问/继续类消息 → 纯询问(问句/只读咨询)→ 文本过短且无动作 →
5
5
  * 四个需求维度(目标/范围/约束/验收标准)的覆盖度。
6
6
  */
7
7
  /** 判定模式:新任务 / 任务中途的需求变更检查。 */
8
8
  export type JudgeMode = 'new-task' | 'drift-check';
9
+ /** 追问力度:只影响判定阈值,不改变判定顺序。 */
10
+ export type JudgeIntensity = 'gentle' | 'standard' | 'grill';
9
11
  /** 判定结论。 */
10
12
  export type JudgeVerdict = 'clear' | 'needs-clarification';
11
13
  /** 四个需求维度是否在文本里被提到。 */
@@ -20,6 +22,8 @@ export interface JudgeInput {
20
22
  text: string;
21
23
  mode: JudgeMode;
22
24
  bypassKeywords?: readonly string[];
25
+ /** 追问力度(默认 standard,保持旧行为)。 */
26
+ intensity?: JudgeIntensity;
23
27
  }
24
28
  /** 判定输出。 */
25
29
  export interface JudgeResult {
@@ -45,6 +49,13 @@ export declare function hasBypassKeyword(text: string, keywords: readonly string
45
49
  export declare function isFollowUpMessage(text: string): boolean;
46
50
  /** 由缺失维度生成追问提示。 */
47
51
  export declare function buildFocusHints(hit: DimensionHit, extra?: string[]): string[];
52
+ /**
53
+ * 是否是"纯询问"消息:问句或让我查/看/评估,且没有任何动手信号。
54
+ *
55
+ * 这类消息不需要澄清——用户就是要一个回答,继续追问反而是打扰。
56
+ * @param text 用户原文。
57
+ */
58
+ export declare function isInquiry(text: string): boolean;
48
59
  /**
49
60
  * 判定任务文本是否需要澄清(纯函数,可单测)。
50
61
  * @param input 任务文本 + 模式 + 跳过关键词。
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 纯函数启发式判定(R1):不调 LLM,只看任务文本判断"需求是否已经够清楚"。
3
3
  *
4
- * 判定顺序(先命中先返回):bypass 关键词 → 追问/继续类消息 → 文本过短且无动作 →
4
+ * 判定顺序(先命中先返回):bypass 关键词 → 追问/继续类消息 → 纯询问(问句/只读咨询)→ 文本过短且无动作 →
5
5
  * 四个需求维度(目标/范围/约束/验收标准)的覆盖度。
6
6
  */
7
7
  /** 默认跳过关键词(R4)。 */
@@ -14,17 +14,23 @@ export const DIMENSION_HINTS = {
14
14
  acceptance: '验收标准缺失:怎样算完成?用什么方式验证?',
15
15
  };
16
16
  /** 维度关键词:目标(有动作、有产出)。 */
17
- const GOAL_WORDS = /(做|写|实现|搭建|开发|创建|新增|加个|修复|改|优化|重构|迁移|升级|翻译|整理|分析|生成|设计|部署|发布|测试|排查|调研|对比|总结|转换|导出|导入|接入|对接|编写|画|制作|搞|弄)/;
17
+ const GOAL_WORDS = /(做|写|实现|搭建|开发|创建|新增|加个|修复|改|优化|重构|迁移|升级|翻译|整理|分析|生成|设计|部署|发布|测试|排查|调研|对比|总结|转换|导出|导入|接入|对接|编写|画|制作|搞|弄|\b(?:fix|add|implement|build|create|refactor|migrate|write|update|remove|delete|optimi[sz]e|translate|generate|analy[sz]e|design|deploy|publish|rewrite|upgrade|install|configure|support|enable|disable|clean|split|merge|rename)\b)/i;
18
18
  /** 维度关键词:范围(边界、对象)。 */
19
- const SCOPE_WORDS = /(只|仅|只要|只需要|范围|限定|针对|面向|所有|全部|部分|哪些|哪个|模块|页面|文件|目录|接口|字段|表|库|版本|第一步|先做|不含|除外)/;
19
+ const SCOPE_WORDS = /(只|仅|只要|只需要|范围|限定|针对|面向|所有|全部|部分|哪些|哪个|模块|页面|文件|目录|接口|字段|表|库|版本|第一步|先做|不含|除外|\b(?:only|all|each|every|field|fields|page|pages|file|files|directory|module|api|endpoint|component|table|schema|version|repo|repository|branch|config|first)\b)/i;
20
20
  /** 维度关键词:约束(技术栈、限制、风格)。忽略大小写,便于匹配 Python / UTF-8 这类写法。 */
21
- const CONSTRAINT_WORDS = /(用|基于|必须|不能|不要|需要|依赖|兼容|保持|风格|注释|技术栈|框架|语言|不许|禁止|限制|预算|时间|截止|格式|规范|utf-?8|中文|英文|node|python|js|ts|react|vue|java|go|rust)/i;
21
+ const CONSTRAINT_WORDS = /(用|基于|必须|不能|不要|需要|依赖|兼容|保持|风格|注释|技术栈|框架|语言|不许|禁止|限制|预算|时间|截止|格式|规范|utf-?8|中文|英文|node|python|js|ts|react|vue|java|go|rust|\b(?:must|should|shall|need|needs|require|requires|required|use|using|based\s+on|compatible|compatibility|keep|preserve|avoid|without|existing)\b)/i;
22
22
  /** 维度关键词:验收标准。 */
23
- const ACCEPTANCE_WORDS = /(验收|标准|测试|通过|算完成|完成标准|结果|输出|期望|应该|返回|效果|达到|满足|可运行|跑通|全绿|正确|不报错|可用|验证|检查|示例|样例|截图|对比)/;
23
+ const ACCEPTANCE_WORDS = /(验收|标准|测试|通过|算完成|完成标准|结果|输出|期望|应该|返回|效果|达到|满足|可运行|跑通|全绿|正确|不报错|可用|验证|检查|示例|样例|截图|对比|\b(?:acceptance|criteria|test|tests|pass|passes|passing|expect|expected|output|verify|verified|green|works|working|screenshot|sample|example)\b)/i;
24
24
  /** 动作动词(用于"短文本是否已构成一个可执行指令")。 */
25
25
  const ACTION_VERB = /(做|写|改|加|删|实现|搭建|开发|修复|优化|翻译|整理|分析|生成|设计|部署|测试|排查|调研|总结|转换|导出|接入|创建|重构|迁移|升级|安装|配置|画|制作)/;
26
26
  /** 追问/继续类消息的开头。 */
27
27
  const FOLLOW_UP_LEAD = /^(继续|接着|然后呢|然后|下一步|再来|嗯|好|可以|行|ok|OK|对|是的|收到|谢谢|辛苦)/;
28
+ /** 问句收尾:出现即说明用户在提问,而不是在派活。 */
29
+ const QUESTION_TAIL = /(吗|呢|么|嘛|是不是|能不能|可不可以|有没有|怎么样|如何|为什么|为啥|啥意思|多少|哪个好)\s*[??]?$|[??]\s*$/;
30
+ /** 询问式开头:让我查/看/评估/解释,而不是让我动手。 */
31
+ const INQUIRY_LEAD = /^(?:please\s+|pls\s+|could you\s+|can you\s+)?(请问|问一下|想问|问下|看看|看一下|检查|查一下|查查|列出|列一下|统计|解释|说明|对比|评估|推荐|你觉得|你认为|这个|那个|现在|目前|为啥|为什么|look|check|see|show|list|tell|explain|describe|review|evaluate|summari[sz]e|search|find|verify|what|why|how|which)/i;
32
+ /** 明确的动手信号:一旦出现就不算纯询问("改一下这个吗"仍然要先澄清)。 */
33
+ const WRITE_SIGNAL = /(改|写|删|新建|实现|修复|重构|迁移|生成|替换|安装|部署|发布|合并|提交|制作|画|创建|搭建|开发|添加|增加|移除)|\b(?:fix|add|implement|build|create|refactor|migrate|write|update|remove|delete|change|edit|rename|move|split|merge|install|deploy|publish|upgrade|generate|replace)\b/i;
28
34
  /** 具体化信号:出现即说明用户给了可落地的细节。 */
29
35
  const SPECIFIC_PATTERNS = [
30
36
  /[A-Za-z0-9_\-./\\]+\.(ts|js|mjs|cjs|tsx|jsx|json|md|py|go|rs|java|c|cc|cpp|h|hpp|yml|yaml|toml|txt|csv|xlsx|png|jpg|jpeg|svg|html|css|vue|sh|ps1)/i,
@@ -92,6 +98,26 @@ export function buildFocusHints(hit, extra = []) {
92
98
  }
93
99
  return [...extra, ...hints].slice(0, 4);
94
100
  }
101
+ /**
102
+ * 是否是"纯询问"消息:问句或让我查/看/评估,且没有任何动手信号。
103
+ *
104
+ * 这类消息不需要澄清——用户就是要一个回答,继续追问反而是打扰。
105
+ * @param text 用户原文。
106
+ */
107
+ export function isInquiry(text) {
108
+ const flat = text.trim();
109
+ if (flat === '')
110
+ return false;
111
+ if (WRITE_SIGNAL.test(flat))
112
+ return false;
113
+ return QUESTION_TAIL.test(flat) || INQUIRY_LEAD.test(flat);
114
+ }
115
+ /** 各力度档位的判定阈值:维度下限 + 是否强制覆盖验收标准。 */
116
+ const INTENSITY_THRESHOLDS = {
117
+ gentle: { dims: 2, needAcceptance: false },
118
+ standard: { dims: 3, needAcceptance: false },
119
+ grill: { dims: 3, needAcceptance: true },
120
+ };
95
121
  /**
96
122
  * 判定任务文本是否需要澄清(纯函数,可单测)。
97
123
  * @param input 任务文本 + 模式 + 跳过关键词。
@@ -102,6 +128,8 @@ export function judgeTask(input) {
102
128
  const dimensions = detectDimensions(text);
103
129
  const satisfied = satisfiedCount(dimensions);
104
130
  const score = satisfied * 25;
131
+ const intensity = input.intensity ?? 'standard';
132
+ const threshold = INTENSITY_THRESHOLDS[intensity] ?? INTENSITY_THRESHOLDS.standard;
105
133
  if (text === '') {
106
134
  return {
107
135
  verdict: 'needs-clarification',
@@ -117,9 +145,12 @@ export function judgeTask(input) {
117
145
  if (isFollowUpMessage(text)) {
118
146
  return { verdict: 'clear', score: 95, focus_hints: [], dimensions, reason: 'follow-up' };
119
147
  }
148
+ if (isInquiry(text)) {
149
+ return { verdict: 'clear', score: 90, focus_hints: [], dimensions, reason: 'inquiry' };
150
+ }
120
151
  if (input.mode === 'drift-check') {
121
152
  const drift = DRIFT_MARKERS.test(text);
122
- if (satisfied >= 3 && text.length >= 20) {
153
+ if (satisfied >= threshold.dims && text.length >= 20) {
123
154
  return { verdict: 'clear', score, focus_hints: [], dimensions, reason: 'drift-well-specified' };
124
155
  }
125
156
  return {
@@ -141,7 +172,7 @@ export function judgeTask(input) {
141
172
  reason: 'too-short',
142
173
  };
143
174
  }
144
- if (satisfied >= 3) {
175
+ if (satisfied >= threshold.dims && (!threshold.needAcceptance || dimensions.acceptance)) {
145
176
  return { verdict: 'clear', score, focus_hints: [], dimensions, reason: 'dimensions-covered' };
146
177
  }
147
178
  if (satisfied === 2 && (hasSpecifics(text) || TRANSFORM_PATTERN.test(text))) {
@@ -26,6 +26,10 @@ export interface MemorySnapshot {
26
26
  }
27
27
  /** forget() 的作用范围。 */
28
28
  export type MemoryScope = 'global' | 'project' | 'all';
29
+ /** 项目层记忆的落点(0.1.4):跟着工作区走,或全局共享一份。 */
30
+ export type ProjectScope = 'project' | 'global';
31
+ /** 项目层历史条目的保留上限(0.1.4 修:以前只增不减,文件会越长越大)。 */
32
+ export declare const MAX_HISTORY = 100;
29
33
  /** 摘要长度上限(字符数),供注入提示用。 */
30
34
  export declare const SUMMARY_LIMIT = 500;
31
35
  /** 空全局层。 */
@@ -44,13 +48,18 @@ export declare function resolveWorkspace(workspaceRoot?: string): string;
44
48
  * 固定到 `~/.dsh/grill-first/`(跨 profile 共享用户偏好,语义上也更合理)。
45
49
  */
46
50
  export declare function globalMemoryPath(): string;
47
- /** 项目记忆文件路径:`<workspace>/.dsh/grill-first/memory.json`。 */
48
- export declare function projectMemoryPath(workspaceRoot?: string): string;
51
+ /**
52
+ * 项目记忆文件路径。
53
+ * @param workspaceRoot 工作区根目录。
54
+ * @param scope project(默认)= `<workspace>/.dsh/grill-first/memory.json`;
55
+ * global = 与 global.json 同目录的 `project.json`(不想往项目里写文件时用)。
56
+ */
57
+ export declare function projectMemoryPath(workspaceRoot?: string, scope?: ProjectScope): string;
49
58
  /**
50
59
  * 读取两层记忆。
51
60
  * @param workspaceRoot 工作区根目录;省略时用 `process.cwd()`。
52
61
  */
53
- export declare function loadMemory(workspaceRoot?: string): Promise<MemorySnapshot>;
62
+ export declare function loadMemory(workspaceRoot?: string, scope?: ProjectScope): Promise<MemorySnapshot>;
54
63
  /**
55
64
  * 追加一条全局偏好(按文本去重,重复则刷新 updated 时间)。
56
65
  * @param text 偏好原文,例如"注释一律用中文"。
@@ -63,14 +72,14 @@ export declare function savePreference(text: string, tags: string[], workspaceRo
63
72
  * @param text 项目背景原文。
64
73
  * @param workspaceRoot 工作区根目录。
65
74
  */
66
- export declare function saveProjectContext(text: string, workspaceRoot?: string): Promise<void>;
75
+ export declare function saveProjectContext(text: string, workspaceRoot?: string, scope?: ProjectScope): Promise<void>;
67
76
  /**
68
77
  * 追加一条历史需求记录(R3:需求清单只进这里,绝不落盘成需求文件)。
69
78
  * @param task 任务原文或摘要。
70
79
  * @param requirements 用户确认过的四段式需求清单。
71
80
  * @param workspaceRoot 工作区根目录。
72
81
  */
73
- export declare function saveHistory(task: string, requirements: string, workspaceRoot?: string): Promise<void>;
82
+ export declare function saveHistory(task: string, requirements: string, workspaceRoot?: string, scope?: ProjectScope): Promise<void>;
74
83
  /**
75
84
  * 清除记忆。
76
85
  * @param scope `global` 只清全局层、`project` 只清项目层、`all` 两层都清。
@@ -8,6 +8,8 @@
8
8
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
9
9
  import { homedir } from 'node:os';
10
10
  import path from 'node:path';
11
+ /** 项目层历史条目的保留上限(0.1.4 修:以前只增不减,文件会越长越大)。 */
12
+ export const MAX_HISTORY = 100;
11
13
  /** 摘要长度上限(字符数),供注入提示用。 */
12
14
  export const SUMMARY_LIMIT = 500;
13
15
  /** 摘要里各段的条数上限。 */
@@ -43,8 +45,15 @@ export function globalMemoryPath() {
43
45
  const base = override && override.trim() !== '' ? path.resolve(override) : path.join(homedir(), '.dsh', 'grill-first');
44
46
  return path.join(base, 'global.json');
45
47
  }
46
- /** 项目记忆文件路径:`<workspace>/.dsh/grill-first/memory.json`。 */
47
- export function projectMemoryPath(workspaceRoot) {
48
+ /**
49
+ * 项目记忆文件路径。
50
+ * @param workspaceRoot 工作区根目录。
51
+ * @param scope project(默认)= `<workspace>/.dsh/grill-first/memory.json`;
52
+ * global = 与 global.json 同目录的 `project.json`(不想往项目里写文件时用)。
53
+ */
54
+ export function projectMemoryPath(workspaceRoot, scope = 'project') {
55
+ if (scope === 'global')
56
+ return path.join(path.dirname(globalMemoryPath()), 'project.json');
48
57
  return path.join(resolveWorkspace(workspaceRoot), '.dsh', 'grill-first', 'memory.json');
49
58
  }
50
59
  /** 归一化用于去重比较的文本(去空白、统一小写、去尾部标点)。 */
@@ -109,9 +118,9 @@ function coerceProject(value) {
109
118
  * 读取两层记忆。
110
119
  * @param workspaceRoot 工作区根目录;省略时用 `process.cwd()`。
111
120
  */
112
- export async function loadMemory(workspaceRoot) {
121
+ export async function loadMemory(workspaceRoot, scope = 'project') {
113
122
  const global = coerceGlobal(await readJson(globalMemoryPath(), emptyGlobal()));
114
- const project = coerceProject(await readJson(projectMemoryPath(workspaceRoot), emptyProject()));
123
+ const project = coerceProject(await readJson(projectMemoryPath(workspaceRoot, scope), emptyProject()));
115
124
  return { global, project };
116
125
  }
117
126
  /**
@@ -152,11 +161,11 @@ export async function savePreference(text, tags, workspaceRoot) {
152
161
  * @param text 项目背景原文。
153
162
  * @param workspaceRoot 工作区根目录。
154
163
  */
155
- export async function saveProjectContext(text, workspaceRoot) {
164
+ export async function saveProjectContext(text, workspaceRoot, scope = 'project') {
156
165
  const trimmed = text.trim();
157
166
  if (trimmed === '')
158
167
  return;
159
- const file = projectMemoryPath(workspaceRoot);
168
+ const file = projectMemoryPath(workspaceRoot, scope);
160
169
  const memory = coerceProject(await readJson(file, emptyProject()));
161
170
  const key = normalizeKey(trimmed);
162
171
  if (!memory.project_context.some((t) => normalizeKey(t) === key)) {
@@ -170,14 +179,17 @@ export async function saveProjectContext(text, workspaceRoot) {
170
179
  * @param requirements 用户确认过的四段式需求清单。
171
180
  * @param workspaceRoot 工作区根目录。
172
181
  */
173
- export async function saveHistory(task, requirements, workspaceRoot) {
174
- const file = projectMemoryPath(workspaceRoot);
182
+ export async function saveHistory(task, requirements, workspaceRoot, scope = 'project') {
183
+ const file = projectMemoryPath(workspaceRoot, scope);
175
184
  const memory = coerceProject(await readJson(file, emptyProject()));
176
185
  memory.history.push({
177
186
  task: task.trim().slice(0, 200),
178
187
  requirements: requirements.trim().slice(0, 2000),
179
188
  date: new Date().toISOString().slice(0, 10),
180
189
  });
190
+ // 只保留最近 MAX_HISTORY 条:摘要只用最后 3 条,其余留着也读不到。
191
+ if (memory.history.length > MAX_HISTORY)
192
+ memory.history = memory.history.slice(-MAX_HISTORY);
181
193
  await writeJsonAtomic(file, memory);
182
194
  }
183
195
  /**
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * R3 红线:需求清单只进记忆的 history 字段,绝不写任何需求文件。
11
11
  */
12
- import type { GrillConfig } from '../config.js';
12
+ import type { GrillConfig, MemoryScope } from '../config.js';
13
13
  import type { GateRegistry } from '../gate/preExecute.js';
14
14
  import type { GrillAgent, ToolDefinition, UserQuestionsService } from '../types.js';
15
15
  import type { SessionRegistry } from './sessions.js';
@@ -20,6 +20,8 @@ export interface GrillConfirmDeps {
20
20
  sessions: SessionRegistry;
21
21
  /** 解析当前会话的工作区根目录(项目层记忆落点)。 */
22
22
  workspaceRoot: (agent?: GrillAgent) => string;
23
+ /** 项目层记忆落在工作区还是插件家目录(memoryScope 配置)。 */
24
+ memoryScope?: () => MemoryScope;
23
25
  /** 结构化提问服务(可能不存在)。 */
24
26
  questions: () => UserQuestionsService | undefined;
25
27
  logger?: {
@@ -38,6 +40,9 @@ export interface GrillConfirmResult {
38
40
  skipped?: boolean;
39
41
  reason?: string;
40
42
  error?: string;
43
+ /** 已追问轮数 / 剩余轮数(选"要改"时给出,供模型判断还要不要继续问)。 */
44
+ rounds_used?: number;
45
+ rounds_left?: number;
41
46
  saved?: {
42
47
  history: boolean;
43
48
  preferences: number;
@@ -104,14 +104,15 @@ export function createGrillConfirmTool(deps) {
104
104
  if (config.memoryEnabled) {
105
105
  const root = deps.workspaceRoot(exec?.agent);
106
106
  try {
107
- await saveHistory(task, requirements, root);
107
+ const scope = deps.memoryScope?.() ?? 'project';
108
+ await saveHistory(task, requirements, root, scope);
108
109
  saved.history = true;
109
110
  for (const preference of preferences) {
110
111
  await savePreference(preference, ['用户确认'], root);
111
112
  saved.preferences += 1;
112
113
  }
113
114
  const context = requirements.replace(/\s+/g, ' ').slice(0, PROJECT_CONTEXT_LIMIT);
114
- await saveProjectContext(context, root);
115
+ await saveProjectContext(context, root, scope);
115
116
  saved.project_context = true;
116
117
  }
117
118
  catch (error) {
@@ -176,12 +177,22 @@ export function createGrillConfirmTool(deps) {
176
177
  const result = await accept('structured-question');
177
178
  return custom === '' ? result : { ...result, user_note: custom };
178
179
  }
180
+ const rounds = deps.sessions.bumpRound(agentId);
181
+ const maxRounds = config.maxRounds;
182
+ const roundsLeft = Math.max(0, maxRounds - rounds);
179
183
  return {
180
184
  confirmed: false,
181
185
  requirements,
182
186
  feedback: custom !== ''
183
187
  ? custom
184
188
  : '用户选择了"要改",但没有写明改哪里:请追问具体修改点,修订需求清单后再次调用 grill_confirm。',
189
+ rounds_used: rounds,
190
+ rounds_left: roundsLeft,
191
+ ...(roundsLeft === 0
192
+ ? {
193
+ instructions: `已达追问轮数上限(${maxRounds} 轮):不要再问,直接按当前理解开工——下次调用 grill_start 会返回 clear。`,
194
+ }
195
+ : {}),
185
196
  };
186
197
  },
187
198
  };
@@ -4,7 +4,7 @@
4
4
  * - clear:放行(门禁置 open),模型直接开工;
5
5
  * - needs-clarification:门禁置 closed,并返回记忆摘要、追问方向与轮次上限。
6
6
  */
7
- import type { GrillConfig } from '../config.js';
7
+ import type { GrillConfig, MemoryScope } from '../config.js';
8
8
  import type { GateRegistry } from '../gate/preExecute.js';
9
9
  import type { GrillAgent, ToolDefinition } from '../types.js';
10
10
  import type { SessionRegistry } from './sessions.js';
@@ -15,6 +15,8 @@ export interface GrillStartDeps {
15
15
  sessions: SessionRegistry;
16
16
  /** 解析当前会话的工作区根目录(项目层记忆落点)。 */
17
17
  workspaceRoot: (agent?: GrillAgent) => string;
18
+ /** 项目层记忆落在工作区还是插件家目录(memoryScope 配置)。 */
19
+ memoryScope?: () => MemoryScope;
18
20
  logger?: {
19
21
  warn(message: string): void;
20
22
  };
@@ -35,6 +37,8 @@ export interface GrillStartResult {
35
37
  task_id?: string;
36
38
  previous_task?: string;
37
39
  hint?: string;
40
+ /** 已经追问过的轮数(达到 maxRounds 后不再追问)。 */
41
+ rounds_used?: number;
38
42
  /** 当前运行的插件版本(取自 package.json):用于确认改动是否已随重启生效。 */
39
43
  version?: string;
40
44
  }
@@ -43,6 +43,7 @@ const OUTPUT_SCHEMA = {
43
43
  task_id: { type: 'string' },
44
44
  previous_task: { type: 'string' },
45
45
  hint: { type: 'string' },
46
+ rounds_used: { type: 'number', description: '已经追问过的轮数' },
46
47
  version: { type: 'string', description: '运行中的插件版本;若缺失说明宿主加载的是旧版代码,需重启 DSH' },
47
48
  },
48
49
  additionalProperties: true,
@@ -77,7 +78,12 @@ export function createGrillStartTool(deps) {
77
78
  version: PLUGIN_VERSION,
78
79
  };
79
80
  }
80
- const judged = judgeTask({ text: task, mode, bypassKeywords: config.bypassKeywords });
81
+ const judged = judgeTask({
82
+ text: task,
83
+ mode,
84
+ bypassKeywords: config.bypassKeywords,
85
+ intensity: config.intensity,
86
+ });
81
87
  const previous = deps.sessions.get(agentId);
82
88
  if (judged.verdict === 'clear') {
83
89
  deps.sessions.end(agentId);
@@ -93,12 +99,29 @@ export function createGrillStartTool(deps) {
93
99
  };
94
100
  }
95
101
  const startedAt = Date.now();
96
- deps.sessions.start({ agentId, task, mode, startedAt });
102
+ // maxRounds 兜底:问够轮数就不再问,直接按当前理解开工(0.1.4 修:以前这个配置项根本没生效)。
103
+ const roundsUsed = previous?.rounds ?? 0;
104
+ if (roundsUsed >= config.maxRounds) {
105
+ deps.sessions.end(agentId);
106
+ deps.gates.open(agentId);
107
+ return {
108
+ verdict: 'clear',
109
+ skip: true,
110
+ reason: 'max-rounds-reached',
111
+ score: judged.score,
112
+ gate_state: 'open',
113
+ mode,
114
+ rounds_used: roundsUsed,
115
+ hint: `已经追问 ${roundsUsed} 轮(上限 ${config.maxRounds}),按当前理解直接开工;有歧义就先做最小可验证版本。`,
116
+ version: PLUGIN_VERSION,
117
+ };
118
+ }
119
+ deps.sessions.start({ agentId, task, mode, startedAt, rounds: roundsUsed });
97
120
  deps.gates.close(agentId);
98
121
  let memoryContext = '';
99
122
  if (config.memoryEnabled) {
100
123
  try {
101
- memoryContext = summarize(await loadMemory(deps.workspaceRoot(exec?.agent)));
124
+ memoryContext = summarize(await loadMemory(deps.workspaceRoot(exec?.agent), (deps.memoryScope?.() ?? 'project')));
102
125
  }
103
126
  catch (error) {
104
127
  deps.logger?.warn(`[grill-first] 读取记忆失败:${error instanceof Error ? error.message : String(error)}`);
@@ -118,6 +141,7 @@ export function createGrillStartTool(deps) {
118
141
  intensity: config.intensity,
119
142
  mode,
120
143
  task_id: `${agentId}:${startedAt}`,
144
+ rounds_used: roundsUsed,
121
145
  version: PLUGIN_VERSION,
122
146
  ...(previous?.task ? { previous_task: previous.task } : {}),
123
147
  };
@@ -9,12 +9,16 @@ export interface GrillSession {
9
9
  task: string;
10
10
  mode: JudgeMode;
11
11
  startedAt: number;
12
+ /** 已经追问过几轮(用户选"要改"算一轮),用于落实 maxRounds 上限。 */
13
+ rounds?: number;
12
14
  }
13
15
  /** agent → 会话 的内存态映射(进程内,不持久化)。 */
14
16
  export declare class SessionRegistry {
15
17
  private readonly sessions;
16
18
  /** 登记/覆盖一个 agent 的当前任务。 */
17
19
  start(session: GrillSession): void;
20
+ /** 记一轮追问(用户选"要改"时调用),返回累计轮数。 */
21
+ bumpRound(agentId: string): number;
18
22
  /** 读取某个 agent 的当前任务。 */
19
23
  get(agentId: string): GrillSession | undefined;
20
24
  /** 结束某个 agent 的澄清会话(确认完成或跳过)。 */
@@ -5,7 +5,15 @@ export class SessionRegistry {
5
5
  start(session) {
6
6
  if (!session.agentId)
7
7
  return;
8
- this.sessions.set(session.agentId, session);
8
+ this.sessions.set(session.agentId, { ...session, rounds: session.rounds ?? 0 });
9
+ }
10
+ /** 记一轮追问(用户选"要改"时调用),返回累计轮数。 */
11
+ bumpRound(agentId) {
12
+ const session = this.sessions.get(agentId);
13
+ if (!session)
14
+ return 0;
15
+ session.rounds = (session.rounds ?? 0) + 1;
16
+ return session.rounds;
9
17
  }
10
18
  /** 读取某个 agent 的当前任务。 */
11
19
  get(agentId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-grill-first",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "DSH 插件:开工前刨根问底——把模糊需求问成明确需求,用户确认后才放行主任务",
5
5
  "icon": "./icon.png",
6
6
  "author": "InoriMutsumi",
package/README.repo.md DELETED
@@ -1,161 +0,0 @@
1
- # dsh-grill-first
2
-
3
- ![tests](https://img.shields.io/badge/tests-91%20passing-brightgreen)
4
- ![license](https://img.shields.io/badge/license-MIT-blue)
5
- ![DSH plugin](https://img.shields.io/badge/DSH-plugin-blueviolet)
6
-
7
- > DSH(DeepSeek Harness)插件:**开工前先刨根问底**——把模糊需求问成明确需求,你在对话里点头之后,主任务才开工。
8
-
9
- ## 它解决什么问题
10
-
11
- AI 助手最常见的失败不是"不会做",而是"做完了才发现做的不是你要的"。这个插件在**任何新任务开工前**插入一道需求澄清环节:
12
-
13
- 1. 模型先把你的任务原文交给 `grill_start` 判定;
14
- 2. 任务模糊(缺目标 / 范围 / 约束 / 验收标准)→ 它按轮追问你,每轮 3 个问题,最多 3 轮,每轮都基于你上一轮的回答继续深挖;
15
- 3. 问清后整理成中文四段清单(目标 / 范围 / 约束 / 验收标准),用 `grill_confirm` 请你确认;
16
- 4. 你点"确认,按此开工"→ 本次任务放行;你点"要改"→ 它按你的反馈修订后再来一次;
17
- 5. 一句话就能说清的小任务、以及带「直接做 / 跳过提问 / 不用问 / skip」字样的消息,**不会被打扰**。
18
-
19
- 严格档位(`gateMode: "strict"`)下,需求没确认前模型无法执行写文件、跑命令这类**变更类**操作——从机制上保证"没问清就不许动手"。
20
-
21
- ## 安装
22
-
23
- 装插件前请先退出正在跑的任务。两种方式任选:
24
-
25
- **方式一:DSH 插件市场**(推荐)
26
-
27
- 在 DSH 设置 → 插件里搜索 `dsh-grill-first` 一键安装。
28
-
29
- **方式二:本地包 / GitHub 仓库**
30
-
31
- ```bash
32
- dsh plugin --profile <你的 profile 名> add /绝对路径/dsh-grill-first-0.1.1.tgz
33
- dsh plugin --profile <你的 profile 名> add github:InoriMutsumi/dsh-grill-first
34
- ```
35
-
36
- 装完**即生效**:本机实测 DSH 的 live profile 会热加载新插件,不需要重启(若你的 profile 是启动式、或列表里没出现,再完全退出 DSH 重开一次)。
37
-
38
- ## 使用
39
-
40
- 装好之后不需要你做什么:它靠两件事自动工作。
41
-
42
- - **协议提示段**:插件往系统提示里注入一小段中文协议(约 600 字),告诉模型"接到新任务先调 `grill_start`"。你在系统提示里能看到 `【开工前提问协议(grill-first)】` 就说明装好了。
43
- - **工具**:模型会用这两个工具推进澄清流程。
44
-
45
- | 工具 | 作用 |
46
- |---|---|
47
- | `grill_start` | 判定任务是否需要澄清;返回 `verdict`、`focus_hints`(该问什么)、`memory_context`(已知偏好,避免重复问) |
48
- | `grill_confirm` | 把四段式需求清单交给你确认;确认后放行任务,并把确认过的内容写进记忆 |
49
-
50
- ### 想跳过提问时
51
-
52
- - 消息里带上「**直接做**」「**跳过提问**」「**不用问**」「**skip**」中任意一个 → 立刻开工;
53
- - 或者在设置里把 `enabled` 关掉 / 把 `gateMode` 调成 `off` → 整个插件静默。
54
-
55
- ## 配置项
56
-
57
- DSH 设置界面 → 插件 → dsh-grill-first,共 7 项:
58
-
59
- | 配置 | 类型 | 默认 | 说明 |
60
- |---|---|---|---|
61
- | `enabled` | 布尔 | `true` | 总开关。关闭后既不追问也不拦截工具 |
62
- | `gateMode` | `off` / `warn` / `strict` | `warn` | `off` 不拦截;`warn` 只在日志里记录"需求未确认";`strict` 未确认前拦截变更类工具 |
63
- | `maxQuestionsPerRound` | 1-6 | `3` | 每轮最多问几个问题 |
64
- | `maxRounds` | 1-10 | `3` | 最多追问几轮 |
65
- | `intensity` | `gentle` / `standard` / `grill` | `standard` | 追问力度:只问关键点 / 标准 / 刨根问底 |
66
- | `bypassKeywords` | 字符串数组 | `["直接做","跳过提问","不用问","skip"]` | 命中即跳过提问 |
67
- | `memoryEnabled` | 布尔 | `true` | 是否启用两层记忆 |
68
-
69
- > 默认是 `warn` 而不是 `strict`:默认档位保证升级插件不会突然卡住你已有的工作流。想要硬门禁请手动切 `strict`。
70
-
71
- ### 怎么改配置
72
-
73
- DSH 设置页里的插件条目目前只提供「启用 / 停用」开关(客户端的插件页不渲染第三方插件的配置表单),要改上面 7 项请编辑 **profile 的补丁层** `cordis.patch.yml`:
74
-
75
- ```yaml
76
- # <profile 目录>/cordis.patch.yml
77
- - id: dsh-grill-first
78
- name: dsh-grill-first
79
- config:
80
- gateMode: strict # off | warn | strict
81
- maxQuestionsPerRound: 3 # 1-6
82
- maxRounds: 3 # 1-10
83
- intensity: standard # gentle | standard | grill
84
- memoryEnabled: true
85
- bypassKeywords: [直接做, 跳过提问, 不用问, skip]
86
- ```
87
-
88
- 改完重启 DSH 生效。只写想改的项也可以,其余项用默认值。
89
-
90
- ## 两层记忆
91
-
92
- 只记录**你确认过**的内容,不记录对话原文,也不会把需求清单写成文件。
93
-
94
- | 层 | 位置 | 内容 |
95
- |---|---|---|
96
- | 全局层 | `<profile 目录>/grill-first/global.json`(不可写时退化到 `~/.dsh-grill-first/global.json`) | 通用偏好,如"注释一律用中文" |
97
- | 项目层 | `<工作区>/.dsh/grill-first/memory.json` | 项目背景、任务历史(任务名 + 需求摘要 + 日期) |
98
-
99
- 下次追问时,`grill_start` 会把这些内容作为 `memory_context` 返回,模型就不会重复问你同一件事。
100
-
101
- ## 开发
102
-
103
- ```bash
104
- npm install # 装开发依赖(schemastery 由宿主提供,仅开发期需要)
105
- npm run build # TypeScript → dist/
106
- npm test # 构建 + 94 个单测 / 端到端场景测试
107
- node scripts/selfcheck.mjs # 静态自检:中文 JSDoc、any 用量、需求→代码位置
108
- ```
109
-
110
- 推送到 GitHub 后 `.github/workflows/ci.yml` 会自动跑上面那两条(`npm ci` + `npm test`)。`dist/` 是**故意提交**的:这样 `dsh plugin add github:…` 装完即可用,不需要在你的机器上编译。
111
-
112
- 代码结构:
113
-
114
- ```
115
- src/
116
- index.ts 插件装配入口(工具 + 协议段 + 门禁)
117
- config.ts 配置归一化(不依赖宿主,可单测)
118
- configSchema.ts Schemastery 配置声明(设置界面用,7 项)
119
- version.ts 从 package.json 读版本号,供 grill_start 返回
120
- judge/heuristics.ts 任务模糊度判定(纯函数)
121
- prompt/protocol.ts 注入系统提示的中文协议文本
122
- tools/grillStart.ts grill_start 工具
123
- tools/grillConfirm.ts grill_confirm 工具
124
- gate/preExecute.ts 变更类工具门禁(tools/pre-execute + 新消息重置)
125
- memory/store.ts 两层记忆的读写
126
- ```
127
-
128
- ## 兼容性与适配
129
-
130
- 本插件对外只有两处依赖,都不归 DSH 的"插件版本门禁"管:
131
-
132
- - **peer 依赖**:只有 `@deepseek-ai/schemastery`(`^3.18.1`,宿主自带;DSH 0.2.1-rc.2 里解析到的实际版本是 3.18.5)。
133
- - **宿主扩展点**:cordis 上下文 API —— `ctx.tools`(注册工具)、`ctx.get('userQuestions')`(结构化提问通道)、`ctx.inject(['systemPrompt'])`、事件 `tools/pre-execute` 与 `agent/inbox/inserted`。
134
-
135
- 它**不 import 任何 `@deepseek-ai/dsh*` 运行时包**,所以既没有 `engines.dsh`,也没有 `@deepseek-ai/dsh` peer。DSH 的 `plugin-compatibility` 检查(`dsh-app-boot` 里的 `evaluatePluginCompatibility`)只在插件声明了 `engines.dsh` 或 `@deepseek-ai/dsh*` peer 时才介入,判不过才会被拦;本插件两条都不沾,因此升 DSH 一般不需要改代码。
136
-
137
- > 以后若要 import harness 内部包,请显式声明 peer 范围并带上预发布分支,例如
138
- > `">=0.2.0-rc.2 <0.3.0-0 || >=0.2.1-rc.1 <0.3.0-0"`。
139
- > DSH 自己的门禁用 `semver.satisfies(..., { includePrerelease: true })` 判定,`^0.2.0-rc.2` 能匹配 `0.2.1-rc.2`;
140
- > 但标准 npm 语义(不加 `includePrerelease`)会判不匹配,所以上游要求写显式分支。
141
-
142
- 已验证的宿主版本:`0.2.0-rc.2`(0.1.x 开发期)、`0.2.1-rc.2`(2026-10-17 复核:`typecheck`、94 个单测、`verify-installed` 一致,且实测加载运行)。升完 DSH 后的自检三步:
143
-
144
- ```bash
145
- node scripts/verify-installed.mjs # 安装拷贝与仓库产物是否一致
146
- # 调一次 grill_start:返回里的 version 就是当前正在跑的插件版本
147
- # 再执行一个需要确认的任务:确认门禁仍然会拦(而不是悄悄放行)
148
- ```
149
-
150
- ## 已知限制
151
-
152
- - 门禁只对**根 agent**(能直接跟你对话的那个)生效;子 agent / 团队成员不拦,否则正常的委派工作流会被卡死。因此**委派类工具本身**(`subagent`、`workflow`、`spawn_teammate`…)在未确认前也会被拦,防止"派个子代理替自己动手"绕过门禁。
153
- - `warn` 档位只能"记录不拦截":DSH 的 `tools/pre-execute` 事件没有"放行但同时给模型提示"这种返回形态。
154
- - 记忆是本地文件,不含加密;删掉对应 json 即清空。
155
- - `grill_confirm` 优先走 DSH 的结构化提问通道;该通道不可用时(例如子 agent 内部)会返回 `needs_user_confirmation: true`,由模型改用内置提问工具兜底。
156
- - **改代码后必须重启 DSH**:DSH 加载的是 profile 里那份安装拷贝,而且插件**代码**改动不会热生效(实测:`cordis.patch.yml` 里的**配置**改动是热生效的,包代码不是)。判断运行中的是哪一版:调一次 `grill_start`,返回里的 `version` 字段就是当前在跑的版本;看不到 `version` 说明跑的是 0.1.0 之前的旧代码。
157
- - 跳过关键词里**英文按词边界匹配**(`skip` 能命中 `skip`、`Skip this`、`(skip)`,但不会命中 `skipList`、`skip_list`、`bypass-keyword` 这类标识符);中文关键词按子串匹配(`直接做` 命中 `那你直接做好了`)。
158
-
159
- ## 许可
160
-
161
- MIT,见 [LICENSE](./LICENSE)。