@zqagent/core 0.1.30 → 0.2.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.
Files changed (86) hide show
  1. package/CONTRIBUTING.md +65 -0
  2. package/LICENSE +190 -0
  3. package/README.md +42 -165
  4. package/README.oss.en.md +161 -0
  5. package/README.oss.md +165 -0
  6. package/README.oss.zh-TW.md +165 -0
  7. package/SECURITY.md +51 -0
  8. package/dist/{ToolRegistry-BmvPxgMr.d.cts → ToolRegistry-C3pTFiaJ.d.cts} +13 -4
  9. package/dist/{ToolRegistry-BmvPxgMr.d.ts → ToolRegistry-C3pTFiaJ.d.ts} +13 -4
  10. package/dist/cli/index.cjs +17799 -2062
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +16729 -985
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/{explore-OI6htHN1.d.cts → explore-D4lCKXbA.d.ts} +160 -19
  15. package/dist/{explore-Pmhyk4gv.d.ts → explore-Dj1j_GyN.d.cts} +160 -19
  16. package/dist/index.cjs +3626 -453
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +337 -27
  19. package/dist/index.d.ts +337 -27
  20. package/dist/index.js +3617 -452
  21. package/dist/index.js.map +1 -1
  22. package/dist/server/index.cjs +14029 -1482
  23. package/dist/server/index.cjs.map +1 -1
  24. package/dist/server/index.js +14036 -1482
  25. package/dist/server/index.js.map +1 -1
  26. package/dist/tools/ask.cjs +14 -0
  27. package/dist/tools/ask.cjs.map +1 -1
  28. package/dist/tools/ask.d.cts +8 -2
  29. package/dist/tools/ask.d.ts +8 -2
  30. package/dist/tools/ask.js +13 -0
  31. package/dist/tools/ask.js.map +1 -1
  32. package/dist/tools/diff.cjs +59 -30
  33. package/dist/tools/diff.cjs.map +1 -1
  34. package/dist/tools/diff.d.cts +1 -1
  35. package/dist/tools/diff.d.ts +1 -1
  36. package/dist/tools/diff.js +59 -30
  37. package/dist/tools/diff.js.map +1 -1
  38. package/dist/tools/directories.d.cts +1 -1
  39. package/dist/tools/directories.d.ts +1 -1
  40. package/dist/tools/explore.cjs +1050 -309
  41. package/dist/tools/explore.cjs.map +1 -1
  42. package/dist/tools/explore.d.cts +2 -2
  43. package/dist/tools/explore.d.ts +2 -2
  44. package/dist/tools/explore.js +1057 -309
  45. package/dist/tools/explore.js.map +1 -1
  46. package/dist/tools/files.cjs +58 -43
  47. package/dist/tools/files.cjs.map +1 -1
  48. package/dist/tools/files.d.cts +10 -4
  49. package/dist/tools/files.d.ts +10 -4
  50. package/dist/tools/files.js +56 -42
  51. package/dist/tools/files.js.map +1 -1
  52. package/dist/tools/git.cjs +816 -63
  53. package/dist/tools/git.cjs.map +1 -1
  54. package/dist/tools/git.d.cts +200 -2
  55. package/dist/tools/git.d.ts +200 -2
  56. package/dist/tools/git.js +788 -62
  57. package/dist/tools/git.js.map +1 -1
  58. package/dist/tools/media.cjs +1191 -0
  59. package/dist/tools/media.cjs.map +1 -0
  60. package/dist/tools/media.d.cts +31 -0
  61. package/dist/tools/media.d.ts +31 -0
  62. package/dist/tools/media.js +1159 -0
  63. package/dist/tools/media.js.map +1 -0
  64. package/dist/tools/plan.d.cts +1 -1
  65. package/dist/tools/plan.d.ts +1 -1
  66. package/dist/tools/search.cjs +513 -33
  67. package/dist/tools/search.cjs.map +1 -1
  68. package/dist/tools/search.d.cts +12 -1
  69. package/dist/tools/search.d.ts +12 -1
  70. package/dist/tools/search.js +508 -33
  71. package/dist/tools/search.js.map +1 -1
  72. package/dist/tools/shell.cjs +631 -41
  73. package/dist/tools/shell.cjs.map +1 -1
  74. package/dist/tools/shell.d.cts +37 -4
  75. package/dist/tools/shell.d.ts +37 -4
  76. package/dist/tools/shell.js +626 -40
  77. package/dist/tools/shell.js.map +1 -1
  78. package/dist/tools/todo.d.cts +1 -1
  79. package/dist/tools/todo.d.ts +1 -1
  80. package/dist/tools/web.cjs +51 -18
  81. package/dist/tools/web.cjs.map +1 -1
  82. package/dist/tools/web.d.cts +1 -1
  83. package/dist/tools/web.d.ts +1 -1
  84. package/dist/tools/web.js +51 -18
  85. package/dist/tools/web.js.map +1 -1
  86. package/package.json +43 -8
@@ -110,12 +110,13 @@ var ToolRegistry = class {
110
110
  /**
111
111
  * 执行工具
112
112
  *
113
+ * @param ctx 执行上下文(引擎注入的中断信号等)
113
114
  * @throws 工具未注册时抛出错误
114
115
  */
115
- execute(name, args) {
116
+ execute(name, args, ctx) {
116
117
  const spec = this.tools.get(name);
117
118
  if (!spec) throw new Error(`Tool not registered: ${name}`);
118
- return spec.func(args);
119
+ return spec.func(args, ctx);
119
120
  }
120
121
  /**
121
122
  * 构建默认 Schema
@@ -141,15 +142,77 @@ function contextTokens(u) {
141
142
  return u.input + u.cacheRead + u.cacheWrite;
142
143
  }
143
144
 
145
+ // src/providers/diagnostics.ts
146
+ function isNetworkErrorText(text) {
147
+ const t = text.toLowerCase();
148
+ return [
149
+ "connection error",
150
+ "fetch failed",
151
+ "econnreset",
152
+ "econnrefused",
153
+ "etimedout",
154
+ "ehostunreach",
155
+ "enetunreach",
156
+ "enotfound",
157
+ "eai_again",
158
+ "socket hang up",
159
+ "und_err",
160
+ "network",
161
+ "timed out",
162
+ "tls",
163
+ "certificate",
164
+ "unable_to_verify",
165
+ "self signed"
166
+ ].some((m) => t.includes(m));
167
+ }
168
+ function asRecord(v) {
169
+ return v && typeof v === "object" ? v : null;
170
+ }
171
+ function causeChain(err, maxNodes = 6) {
172
+ const out = [];
173
+ const seen = /* @__PURE__ */ new Set();
174
+ const queue = [err];
175
+ while (queue.length > 0 && out.length < maxNodes) {
176
+ const cur = queue.shift();
177
+ if (!cur || typeof cur !== "object" || seen.has(cur)) continue;
178
+ seen.add(cur);
179
+ const rec = asRecord(cur);
180
+ if (!rec) continue;
181
+ out.push(rec);
182
+ if (rec["cause"]) queue.push(rec["cause"]);
183
+ const errs = rec["errors"];
184
+ if (Array.isArray(errs)) queue.push(...errs);
185
+ }
186
+ return out;
187
+ }
188
+ function isErrorCodeLike(v) {
189
+ return /^[A-Z][A-Z0-9_]*$/.test(v);
190
+ }
191
+ function errorCode(err) {
192
+ for (const src of causeChain(err)) {
193
+ for (const k of ["code", "errno"]) {
194
+ const v = src[k];
195
+ if (typeof v === "string" && v) return v;
196
+ }
197
+ const name = src["name"];
198
+ if (typeof name === "string" && name !== "Error" && isErrorCodeLike(name)) {
199
+ return name;
200
+ }
201
+ }
202
+ return void 0;
203
+ }
204
+
144
205
  // src/engine/compaction.ts
145
206
  var DEFAULT_THRESHOLD_PCT = 0.8;
146
- var DEFAULT_CAP_TOKENS = 25e4;
147
207
  var DEFAULT_CONTEXT_WINDOW = 128e3;
148
208
  var MODEL_CONTEXT_WINDOWS = {
149
209
  // ==================== OpenAI ====================
150
- // 来源: OpenAI官方API文档
210
+ // 来源: OpenAI 官方 API 文档 (platform.openai.com/docs/models)
151
211
  "gpt-5.5": 105e4,
152
212
  "gpt-5.4": 105e4,
213
+ "gpt-4.1": 1047576,
214
+ "gpt-4.1-mini": 1047576,
215
+ "gpt-4.1-nano": 1047576,
153
216
  "gpt-4o": 128e3,
154
217
  "gpt-4o-mini": 128e3,
155
218
  "gpt-4-turbo": 128e3,
@@ -161,52 +224,86 @@ var MODEL_CONTEXT_WINDOWS = {
161
224
  "o1-mini": 128e3,
162
225
  o3: 2e5,
163
226
  "o3-mini": 2e5,
227
+ o4: 2e5,
228
+ "o4-mini": 2e5,
164
229
  // ==================== Anthropic ====================
165
- // 注: Claude系列模型上下文窗口多为200K,但Claude 4系列在特定平台(如Amazon Bedrock)可能支持更高
230
+ // 来源: Anthropic 官方文档 (docs.anthropic.com)
231
+ // 注: Claude 系列模型上下文窗口均为 200K
166
232
  "claude-3-opus": 2e5,
167
233
  "claude-3-sonnet": 2e5,
168
234
  "claude-3-haiku": 2e5,
169
235
  "claude-3.5-sonnet": 2e5,
236
+ "claude-3.5-haiku": 2e5,
170
237
  "claude-4-sonnet": 2e5,
171
- // 基础窗口,实际可能因部署平台而异
172
238
  "claude-4-opus": 2e5,
239
+ "claude-opus-4": 2e5,
240
+ "claude-sonnet-4": 2e5,
173
241
  // ==================== Google Gemini ====================
174
- // 来源: Google Colab及OpenRouter
175
- // 注: Gemini模型以超大上下文窗口著称,部分版本支持2M
242
+ // 来源: Google AI 官方文档 (ai.google.dev)
243
+ // 注: Gemini 模型以超大上下文窗口著称
244
+ "gemini-2.5-pro": 2e6,
245
+ "gemini-2.5-flash": 1e6,
246
+ "gemini-2.0-flash": 1048576,
176
247
  "gemini-3-pro": 2e6,
177
- // 最高支持2M
178
248
  "gemini-3-flash": 1e6,
179
- // 支持1M
180
249
  // ==================== Meta Llama ====================
181
- // 来源: NVIDIA NIM文档及Oracle Cloud文档
250
+ // 来源: NVIDIA NIM 文档及 Oracle Cloud 文档
182
251
  // 注意: 不同部署平台的上下文长度可能有差异
183
252
  "llama-4-scout": 128e3,
184
- // 实际部署中常见128K
185
253
  "llama-4-maverick": 512e3,
186
- // 支持512K
187
254
  "llama-3.3": 128e3,
188
255
  "llama-3.1": 128e3,
256
+ "llama-3": 128e3,
189
257
  // ==================== DeepSeek ====================
190
- // 来源: 基于DeepSeek-V4系列公开信息
191
- // 注: DeepSeek-V4系列两款模型均支持100万(1M)上下文窗口
258
+ // 来源: DeepSeek 官方文档 (api-docs.deepseek.com)
259
+ // 注: deepseek-flash 当前由 DeepSeek-V4.1-Flash 提供服务,上下文 1M
260
+ "deepseek-flash": 1e6,
261
+ "deepseek-v4.1-flash": 1e6,
192
262
  "deepseek-v4-pro": 1e6,
193
263
  "deepseek-v4-flash": 1e6,
194
264
  "deepseek-v3": 128e3,
195
265
  "deepseek-r1": 128e3,
196
- // ==================== Alibaba Qwen ====================
197
- // 来源: Replicate平台模型卡片
198
- // 注: Qwen 3.5/3.6系列原生支持262K,可扩展至1M
266
+ "deepseek-reasoner": 128e3,
267
+ // ==================== Alibaba Qwen (通义千问) ====================
268
+ // 来源: 阿里云百炼文档 (help.aliyun.com/zh/model-studio)
269
+ // 注: Qwen3.8 系列支持 1M,Qwen3.5/3.6 原生 262K
270
+ "qwen3.8-max": 1e6,
271
+ "qwen3.8-flash": 1e6,
272
+ "qwen3.7-max": 1e6,
273
+ "qwen3.7-plus": 1e6,
274
+ "qwen3.6": 262144,
275
+ "qwen3.5": 262144,
199
276
  "qwen-3.5": 262144,
200
- // 原生262K,可扩展至1M
201
277
  "qwen-3.6": 262144,
202
- // 原生262K,可扩展至1M
278
+ "qwen-max": 128e3,
279
+ "qwen-plus": 128e3,
280
+ "qwen-turbo": 128e3,
281
+ // ==================== 智谱 GLM ====================
282
+ // 来源: 智谱 AI 开放文档 (docs.bigmodel.cn)
283
+ "glm-5.3": 128e3,
284
+ "glm-5.2": 128e3,
285
+ "glm-4-plus": 128e3,
286
+ "glm-4-flash": 128e3,
287
+ "glm-4": 128e3,
288
+ // ==================== Kimi / Moonshot ====================
289
+ // 来源: Moonshot API 文档 (platform.moonshot.cn/docs)
290
+ "kimi-k3": 256e3,
291
+ "kimi-k2.7": 256e3,
292
+ "kimi-k2.6": 256e3,
293
+ "kimi-k2": 256e3,
294
+ "moonshot-v1-128k": 128e3,
295
+ "moonshot-v1-32k": 32e3,
296
+ "moonshot-v1-8k": 8e3,
297
+ // ==================== 小米 MiMo ====================
298
+ "mimo-v2.5-pro": 128e3,
203
299
  // ==================== Mistral ====================
204
- // 来源: AWS Bedrock文档
300
+ // 来源: Mistral AI 官方文档及 AWS Bedrock 文档
205
301
  "mistral-large-3": 256e3,
206
- // ==================== Others ====================
207
- // 来源: Kimi官方博客及智谱AI开放文档
208
- "kimi-k2": 256e3,
209
- "glm-4-plus": 128e3
302
+ "mistral-large": 128e3,
303
+ "mistral-small": 128e3,
304
+ "mixtral-8x22b": 65536,
305
+ // ==================== MiniMax ====================
306
+ "minimax-m3": 1e6
210
307
  };
211
308
  function lookupContextWindow(modelId) {
212
309
  const lower = modelId.toLowerCase();
@@ -233,9 +330,13 @@ function estimateTokens(messages) {
233
330
  }
234
331
  return Math.floor(total / 4);
235
332
  }
236
- function triggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capTokens = DEFAULT_CAP_TOKENS) {
333
+ function triggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capTokens) {
237
334
  const window = contextWindow ?? DEFAULT_CONTEXT_WINDOW;
238
- return Math.min(Math.floor(thresholdPct * window), capTokens);
335
+ const base = Math.floor(thresholdPct * window);
336
+ if (capTokens != null && Number.isFinite(capTokens) && capTokens > 0) {
337
+ return Math.min(base, Math.floor(capTokens));
338
+ }
339
+ return base;
239
340
  }
240
341
  function shouldCompact(signal, contextWindow, thresholdPct, capTokens) {
241
342
  return signal >= triggerTokens(contextWindow, thresholdPct, capTokens);
@@ -591,12 +692,21 @@ var EXPLORER_INSTRUCTIONS = `\u4F60\u662F\u4E00\u4E2A\u53EA\u8BFB\u7684\u4EE3\u7
591
692
  3. **\u5305\u542B\u5173\u952E\u4EE3\u7801\u7247\u6BB5** \u2014 \u6458\u5F55\u6700\u76F8\u5173\u7684\u4EE3\u7801\uFF08\u4E0D\u662F\u6574\u6BB5\u590D\u5236\uFF0C\u53EA\u4FDD\u7559\u6838\u5FC3\u903B\u8F91\uFF09
592
693
  4. **\u8BB0\u5F55\u610F\u5916\u53D1\u73B0** \u2014 \u6CBF\u9014\u53D1\u73B0\u7684\u4EFB\u4F55\u503C\u5F97\u6CE8\u610F\u7684\u7EC6\u8282\u3001\u4E0D\u4E00\u81F4\u3001\u6F5C\u5728\u95EE\u9898
593
694
  5. **\u58F0\u660E\u641C\u7D22\u8303\u56F4** \u2014 \u5982\u679C\u627E\u4E0D\u5230\u67D0\u4E9B\u5185\u5BB9\uFF0C\u660E\u786E\u8BF4\u660E\u4F60\u641C\u7D22\u4E86\u4EC0\u4E48\u3001\u5C1D\u8BD5\u4E86\u4EC0\u4E48\uFF0C\u907F\u514D\u8C03\u7528\u65B9\u91CD\u590D\u641C\u7D22`;
594
- var GEN_SESSION_NAME_INSTRUCTIONS = `\u4F60\u662F\u4E00\u4E2A\u4F1A\u8BDD\u547D\u540D\u52A9\u624B\u3002\u6839\u636E\u7528\u6237\u7684\u8F93\u5165\uFF0C\u751F\u6210\u4E00\u4E2A\u7B80\u77ED\u3001\u6E05\u6670\u3001\u63CF\u8FF0\u6027\u7684\u4F1A\u8BDD\u540D\u79F0\u3002\u540D\u79F0\u5E94\u6982\u62EC\u4F1A\u8BDD\u7684\u4E3B\u8981\u4E3B\u9898\u6216\u76EE\u6807\uFF0C\u907F\u514D\u4F7F\u7528\u6A21\u7CCA\u6216\u901A\u7528\u7684\u8BCD\u6C47\u3002`;
695
+ var GEN_SESSION_NAME_INSTRUCTIONS = `\u4F60\u662F\u4E00\u4E2A\u4F1A\u8BDD\u547D\u540D\u52A9\u624B\u3002\u6839\u636E\u7528\u6237\u7684\u8F93\u5165\uFF0C\u751F\u6210\u4E00\u4E2A\u7B80\u77ED\u3001\u6E05\u6670\u3001\u63CF\u8FF0\u6027\u7684\u4F1A\u8BDD\u540D\u79F0\u3002\u540D\u79F0\u5E94\u6982\u62EC\u4F1A\u8BDD\u7684\u4E3B\u8981\u4E3B\u9898\u6216\u76EE\u6807\uFF0C\u907F\u514D\u4F7F\u7528\u6A21\u7CCA\u6216\u901A\u7528\u7684\u8BCD\u6C47\u3002
696
+
697
+ \u8981\u6C42\uFF1A
698
+ 1. \u53EA\u8F93\u51FA\u540D\u79F0\u672C\u8EAB\uFF0C\u4E0D\u8981\u5F15\u53F7\u3001\u6807\u70B9\u3001\u6362\u884C\u6216\u4EFB\u4F55\u89E3\u91CA\uFF1B
699
+ 2. \u540D\u79F0\u4E0D\u8D85\u8FC7 20 \u4E2A\u5B57\u7B26\u3002`;
595
700
 
596
701
  // src/engine/TurnEngine.ts
597
702
  function genReqId() {
598
703
  return crypto.randomBytes(6).toString("base64url");
599
704
  }
705
+ var INTERACTIVE_TOOLS = /* @__PURE__ */ new Set([
706
+ "ask_user",
707
+ "propose_plan",
708
+ "request_directory"
709
+ ]);
600
710
  function toolResultMessage(tc, result, taskId) {
601
711
  const content = typeof result === "string" ? result : JSON.stringify(result, null, 2);
602
712
  return {
@@ -643,6 +753,23 @@ function preview(value, maxLen = 200) {
643
753
  const flat = text.replace(/\n/g, "\\n");
644
754
  return flat.length <= maxLen ? flat : flat.slice(0, maxLen - 3) + "...";
645
755
  }
756
+ function isTransientNetworkError(exc) {
757
+ const status = exc?.status;
758
+ if (typeof status === "number") {
759
+ return false;
760
+ }
761
+ const msg = `${exc instanceof Error ? exc.message : String(exc)} ${errorCode(exc) ?? ""}`;
762
+ return isNetworkErrorText(msg);
763
+ }
764
+ function networkRetryMax() {
765
+ const n = Number(process.env.NETWORK_RETRY_MAX);
766
+ return Number.isFinite(n) && n >= 0 ? n : 2;
767
+ }
768
+ function networkRetryBaseMs() {
769
+ const n = Number(process.env.NETWORK_RETRY_BASE_MS);
770
+ return Number.isFinite(n) && n >= 0 ? n : 800;
771
+ }
772
+ var sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
646
773
  function isRetriableError(exc) {
647
774
  const status = exc?.status;
648
775
  if (typeof status === "number") {
@@ -653,6 +780,7 @@ function isRetriableError(exc) {
653
780
  (exc instanceof Error ? exc.message : exc) ?? ""
654
781
  ).toLowerCase();
655
782
  const markers = [
783
+ "connection error",
656
784
  "fetch failed",
657
785
  "econnreset",
658
786
  "etimedout",
@@ -685,6 +813,12 @@ var TurnEngine = class _TurnEngine {
685
813
  _hook;
686
814
  // 中断标记:requestInterrupt() 置位后,引擎在下一个安全点停止
687
815
  _cancel = false;
816
+ // 在飞的 LLM 流式中止句柄:requestInterrupt() 直接 abort,
817
+ // 否则流静默挂死时引擎永远等不到检查标志位的安全点
818
+ _llmAbort = null;
819
+ // 在一轮工具执行期间生效的中止句柄:requestInterrupt() 一并 abort,
820
+ // 使正在执行的可中断工具(run_shell)立即终止子进程,而不是等它自然结束
821
+ _toolAbort = null;
688
822
  // Steering 消息队列:外部排队注入的用户级指令,迭代间隙注入
689
823
  _steering = [];
690
824
  // 上下文压缩
@@ -692,6 +826,10 @@ var TurnEngine = class _TurnEngine {
692
826
  _lastContextTokens = null;
693
827
  /** 显式配置的上下文窗口大小(token 数),未设置时为 undefined(走自动检测) */
694
828
  _contextWindow;
829
+ /** 压缩触发阈值比例(默认 0.8) */
830
+ _thresholdPct;
831
+ /** 压缩触发阈值绝对上限(null = 不限制) */
832
+ _capTokens;
695
833
  // 产出文件追踪(本轮 Turn 中成功写入的文件)
696
834
  _producedFiles = [];
697
835
  _turnStartTime = 0;
@@ -738,18 +876,25 @@ var TurnEngine = class _TurnEngine {
738
876
  this.model = opts.model;
739
877
  this.instructions = opts.instructions;
740
878
  this.genSessionNamePrompt = opts.genSessionNamePrompt || GEN_SESSION_NAME_INSTRUCTIONS;
741
- this.maxIterations = opts.maxIterations ?? 150;
879
+ this.maxIterations = opts.maxIterations ?? 300;
742
880
  this.messages = opts.messages ? [...opts.messages] : [];
743
881
  this._hook = opts.hook;
744
882
  this._contextWindow = opts.contextWindow;
883
+ this._thresholdPct = opts.thresholdPct ?? DEFAULT_THRESHOLD_PCT;
884
+ this._capTokens = opts.capTokens ?? null;
745
885
  this._reasoningEffort = opts.reasoningEffort;
746
886
  if (this.messages.length === 0 || this.messages[0].role !== "system") {
747
887
  this.messages.unshift({ role: "system", content: this.instructions });
748
888
  }
749
889
  }
750
- /** 外部中断:置位取消标记,引擎会在安全点停止 */
890
+ /**
891
+ * 外部中断:置位取消标记 + 掐断在飞的 LLM 请求 + 中止正在执行的工具,
892
+ * 引擎随后按中断收尾
893
+ */
751
894
  requestInterrupt() {
752
895
  this._cancel = true;
896
+ this._llmAbort?.abort();
897
+ this._toolAbort?.abort();
753
898
  }
754
899
  /** 设置审批回调 */
755
900
  setApprover(approver) {
@@ -851,7 +996,10 @@ var TurnEngine = class _TurnEngine {
851
996
  {
852
997
  reqId: `llm_chat_${Date.now()}_${genReqId()}`,
853
998
  logTime: (/* @__PURE__ */ new Date()).toISOString()
854
- }
999
+ },
1000
+ // 不设 maxTokens:推理模型(deepseek 等)的思考会占用输出配额,
1001
+ // 过小会导致 content 为空从而拿不到名称;命名 prompt 已约束只输出短标题。
1002
+ {}
855
1003
  );
856
1004
  return name;
857
1005
  }
@@ -882,6 +1030,47 @@ var TurnEngine = class _TurnEngine {
882
1030
  return [];
883
1031
  }
884
1032
  // ===================== 核心循环 =====================
1033
+ /**
1034
+ * 中断收尾:保留已流式输出的内容、记一条 interrupted 提示,
1035
+ * 产出 INTERRUPTED + TURN_END 事件并结束本轮。
1036
+ *
1037
+ * 两处调用:流式结束后发现取消标记、以及流被 abort 中断抛错时。
1038
+ */
1039
+ *_finishInterrupted(args) {
1040
+ const { iterations, reqId, streamed, streamedThinking } = args;
1041
+ if (streamed) {
1042
+ this.messages.push(
1043
+ assistantMessage(
1044
+ {
1045
+ text: streamed,
1046
+ toolCalls: [],
1047
+ finishReason: null,
1048
+ reasoning: streamedThinking || null,
1049
+ usage: null
1050
+ },
1051
+ this.model,
1052
+ this.taskId
1053
+ )
1054
+ );
1055
+ }
1056
+ this._appendNotice("interrupted");
1057
+ this._emitHook((h) => h.onInterrupted?.(iterations));
1058
+ yield {
1059
+ type: "interrupted" /* INTERRUPTED */,
1060
+ data: { iterations, reqId, taskId: this.taskId }
1061
+ };
1062
+ const files = this._buildProducedFiles();
1063
+ yield {
1064
+ type: "turn_end" /* TURN_END */,
1065
+ data: {
1066
+ status: "interrupted",
1067
+ iterations,
1068
+ produced_files: files,
1069
+ taskId: this.taskId
1070
+ }
1071
+ };
1072
+ this._emitHook((h) => h.onTurnEnd?.("interrupted", iterations, files));
1073
+ }
885
1074
  /**
886
1075
  * 核心迭代循环
887
1076
  *
@@ -891,6 +1080,8 @@ var TurnEngine = class _TurnEngine {
891
1080
  */
892
1081
  async *_loop() {
893
1082
  let iterations = 0;
1083
+ let netRetries = 0;
1084
+ const netMax = networkRetryMax();
894
1085
  while (true) {
895
1086
  if (iterations >= this.maxIterations) {
896
1087
  const files = this._buildProducedFiles();
@@ -922,23 +1113,26 @@ var TurnEngine = class _TurnEngine {
922
1113
  };
923
1114
  this._emitHook((h) => h.onEvent?.("llm_request" /* LLM_REQUEST */, llmReqData));
924
1115
  yield { type: "llm_request" /* LLM_REQUEST */, data: llmReqData };
925
- this._emitHook((h) => h.onCompacting?.());
926
- yield { type: "compacting" /* COMPACTING */, data: { taskId: this.taskId } };
1116
+ const startInfo = this._compactionStartInfo("threshold");
1117
+ this._emitHook((h) => h.onCompacting?.(startInfo));
1118
+ yield {
1119
+ type: "compacting" /* COMPACTING */,
1120
+ data: { taskId: this.taskId, ...startInfo }
1121
+ };
927
1122
  const result = await this._compactNow(compactReqId);
928
1123
  if (result.notice) {
929
1124
  compactionNotice = result.notice;
930
1125
  this._appendNotice("compacted", result.notice);
931
- this._emitHook(
932
- (h) => h.onCompacted?.(
933
- this.compactionState ? "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" : "\u4E0A\u4E0B\u6587\u5DF2\u88C1\u526A"
934
- )
935
- );
1126
+ const doneInfo = this._compactionDoneInfo(result);
1127
+ this._emitHook((h) => h.onCompacted?.(result.notice, doneInfo));
936
1128
  yield {
937
1129
  type: "compacted" /* COMPACTED */,
938
1130
  data: {
939
1131
  reqId: compactReqId,
940
1132
  taskId: this.taskId,
941
1133
  method: result.method,
1134
+ messagesBefore: doneInfo.messagesBefore,
1135
+ messagesAfter: doneInfo.messagesAfter,
942
1136
  ...result.boundaryIndex !== void 0 ? { boundaryIndex: result.boundaryIndex } : {},
943
1137
  ...result.modelUsed ? { model: result.modelUsed } : {},
944
1138
  ...result.summaryPreview ? { summaryPreview: result.summaryPreview } : {},
@@ -966,11 +1160,18 @@ var TurnEngine = class _TurnEngine {
966
1160
  };
967
1161
  this._emitHook((h) => h.onEvent?.("llm_request" /* LLM_REQUEST */, llmReqData));
968
1162
  yield { type: "llm_request" /* LLM_REQUEST */, data: llmReqData };
1163
+ this._llmAbort = new AbortController();
1164
+ if (this._cancel) this._llmAbort.abort();
969
1165
  const chunks = await this.provider.stream(
970
1166
  this.model,
971
1167
  outbound,
972
1168
  schemas,
973
- { reqId, logTime, reasoningEffort: this._reasoningEffort }
1169
+ {
1170
+ reqId,
1171
+ logTime,
1172
+ reasoningEffort: this._reasoningEffort,
1173
+ signal: this._llmAbort.signal
1174
+ }
974
1175
  );
975
1176
  for await (const chunk of chunks) {
976
1177
  if (this._cancel) break;
@@ -1003,6 +1204,15 @@ var TurnEngine = class _TurnEngine {
1003
1204
  }
1004
1205
  }
1005
1206
  } catch (exc) {
1207
+ if (this._cancel && turn === null) {
1208
+ yield* this._finishInterrupted({
1209
+ iterations,
1210
+ reqId,
1211
+ streamed,
1212
+ streamedThinking
1213
+ });
1214
+ return;
1215
+ }
1006
1216
  const overflow = isContextOverflow(exc);
1007
1217
  if (overflow && !this._cancel) {
1008
1218
  const overflowReqId = `compact_${Date.now()}_${genReqId()}`;
@@ -1016,22 +1226,31 @@ var TurnEngine = class _TurnEngine {
1016
1226
  };
1017
1227
  this._emitHook((h) => h.onEvent?.("llm_request" /* LLM_REQUEST */, llmReqData));
1018
1228
  yield { type: "llm_request" /* LLM_REQUEST */, data: llmReqData };
1019
- this._emitHook((h) => h.onCompacting?.());
1020
- yield { type: "compacting" /* COMPACTING */, data: { taskId: this.taskId } };
1229
+ const startInfo = this._compactionStartInfo("overflow");
1230
+ this._emitHook((h) => h.onCompacting?.(startInfo));
1231
+ yield {
1232
+ type: "compacting" /* COMPACTING */,
1233
+ data: { taskId: this.taskId, ...startInfo }
1234
+ };
1021
1235
  const result = await this._compactNow(overflowReqId, true);
1022
1236
  if (result.notice) {
1023
1237
  const notice = result.notice;
1024
1238
  this._appendNotice("compacted", notice);
1025
- this._emitHook((h) => h.onCompacted?.(notice));
1239
+ const doneInfo = this._compactionDoneInfo(result);
1240
+ this._emitHook((h) => h.onCompacted?.(notice, doneInfo));
1026
1241
  yield {
1027
1242
  type: "compacted" /* COMPACTED */,
1028
1243
  data: {
1029
1244
  reqId: overflowReqId,
1030
1245
  taskId: this.taskId,
1031
1246
  method: result.method,
1247
+ messagesBefore: doneInfo.messagesBefore,
1248
+ messagesAfter: doneInfo.messagesAfter,
1032
1249
  ...result.boundaryIndex !== void 0 ? { boundaryIndex: result.boundaryIndex } : {},
1033
1250
  ...result.modelUsed ? { model: result.modelUsed } : {},
1034
- ...result.summaryPreview ? { summaryPreview: result.summaryPreview } : {}
1251
+ ...result.summaryPreview ? { summaryPreview: result.summaryPreview } : {},
1252
+ ...result.keptUserMessages !== void 0 ? { keptUserMessages: result.keptUserMessages } : {},
1253
+ ...result.droppedUserMessages !== void 0 ? { droppedUserMessages: result.droppedUserMessages } : {}
1035
1254
  }
1036
1255
  };
1037
1256
  if (streamed) {
@@ -1052,6 +1271,20 @@ var TurnEngine = class _TurnEngine {
1052
1271
  continue;
1053
1272
  }
1054
1273
  }
1274
+ if (!this._cancel && !streamed && isTransientNetworkError(exc) && netRetries < netMax) {
1275
+ netRetries++;
1276
+ const reason = `${exc instanceof Error ? exc.message : String(exc)}`.slice(0, 200);
1277
+ const waitMs = networkRetryBaseMs() * 2 ** (netRetries - 1);
1278
+ this._appendNotice("retry", `\u7F51\u7EDC\u4E2D\u65AD\uFF0C\u6B63\u5728\u91CD\u8BD5\uFF08${netRetries}/${netMax}\uFF09\uFF1A${reason}`);
1279
+ this._emitHook((h) => h.onRetry?.(netRetries, netMax, reason));
1280
+ yield {
1281
+ type: "llm_retry" /* LLM_RETRY */,
1282
+ data: { attempt: netRetries, max: netMax, reason, waitMs, iteration: iterations, reqId }
1283
+ };
1284
+ iterations--;
1285
+ await sleepMs(waitMs);
1286
+ continue;
1287
+ }
1055
1288
  const errMsg = exc instanceof Error ? exc.message : String(exc);
1056
1289
  const errorType = exc instanceof Error ? exc.constructor.name : "unknown";
1057
1290
  const errorStack = exc instanceof Error ? exc.stack : void 0;
@@ -1106,39 +1339,14 @@ var TurnEngine = class _TurnEngine {
1106
1339
  this._emitHook((h) => h.onTurnEnd?.("error", iterations, files));
1107
1340
  return;
1108
1341
  }
1342
+ netRetries = 0;
1109
1343
  if (this._cancel && turn === null) {
1110
- if (streamed) {
1111
- this.messages.push(
1112
- assistantMessage(
1113
- {
1114
- text: streamed,
1115
- toolCalls: [],
1116
- finishReason: null,
1117
- reasoning: streamedThinking || null,
1118
- usage: null
1119
- },
1120
- this.model,
1121
- this.taskId
1122
- )
1123
- );
1124
- }
1125
- this._appendNotice("interrupted");
1126
- this._emitHook((h) => h.onInterrupted?.(iterations));
1127
- yield {
1128
- type: "interrupted" /* INTERRUPTED */,
1129
- data: { iterations, reqId, taskId: this.taskId }
1130
- };
1131
- const files = this._buildProducedFiles();
1132
- yield {
1133
- type: "turn_end" /* TURN_END */,
1134
- data: {
1135
- status: "interrupted",
1136
- iterations,
1137
- produced_files: files,
1138
- taskId: this.taskId
1139
- }
1140
- };
1141
- this._emitHook((h) => h.onTurnEnd?.("interrupted", iterations, files));
1344
+ yield* this._finishInterrupted({
1345
+ iterations,
1346
+ reqId,
1347
+ streamed,
1348
+ streamedThinking
1349
+ });
1142
1350
  return;
1143
1351
  }
1144
1352
  if (turn === null) {
@@ -1257,9 +1465,14 @@ var TurnEngine = class _TurnEngine {
1257
1465
  * PERMISSION_REQUIRED 事件并调用 approver;被拒/未知工具
1258
1466
  * 直接产出错误结果,不进入执行。
1259
1467
  * B. 分类执行:低风险工具并发执行(Promise.all),其余串行;
1468
+ * 交互类工具(ask_user / propose_plan / request_directory)虽然
1469
+ * 风险级别为 low,但需要用户逐条作答,一律串行;
1260
1470
  * 每个结果都转成 tool 消息追加到历史并产出 TOOL_FINISHED。
1261
1471
  */
1262
1472
  async *_handleToolCalls(toolCalls) {
1473
+ const abort = new AbortController();
1474
+ this._toolAbort = abort;
1475
+ if (this._cancel) abort.abort();
1263
1476
  const cleared = [];
1264
1477
  for (const tc of toolCalls) {
1265
1478
  tc.id = tc.id || `tool_${Date.now()}_${genReqId()}`;
@@ -1405,7 +1618,7 @@ var TurnEngine = class _TurnEngine {
1405
1618
  }
1406
1619
  const concurrent = cleared.filter((tc) => {
1407
1620
  const spec = this.registry.get(tc.name);
1408
- return spec?.metadata.riskLevel === "low" && !spec.metadata.requiresApproval;
1621
+ return spec?.metadata.riskLevel === "low" && !spec.metadata.requiresApproval && !INTERACTIVE_TOOLS.has(tc.name);
1409
1622
  });
1410
1623
  const serial = cleared.filter((tc) => !concurrent.includes(tc));
1411
1624
  if (concurrent.length > 0) {
@@ -1421,7 +1634,8 @@ var TurnEngine = class _TurnEngine {
1421
1634
  try {
1422
1635
  let result = this.registry.execute(
1423
1636
  tc.name,
1424
- tc.arguments
1637
+ tc.arguments,
1638
+ { signal: abort.signal }
1425
1639
  );
1426
1640
  if (result instanceof Promise) result = await result;
1427
1641
  return { result, status: "ok" };
@@ -1490,7 +1704,8 @@ var TurnEngine = class _TurnEngine {
1490
1704
  try {
1491
1705
  result = this.registry.execute(
1492
1706
  tc.name,
1493
- tc.arguments
1707
+ tc.arguments,
1708
+ { signal: abort.signal }
1494
1709
  );
1495
1710
  if (result instanceof Promise) result = await result;
1496
1711
  status = "ok";
@@ -1527,6 +1742,7 @@ var TurnEngine = class _TurnEngine {
1527
1742
  }
1528
1743
  };
1529
1744
  }
1745
+ if (this._toolAbort === abort) this._toolAbort = null;
1530
1746
  }
1531
1747
  // ===================== 辅助方法 =====================
1532
1748
  /**
@@ -1600,10 +1816,54 @@ var TurnEngine = class _TurnEngine {
1600
1816
  return false;
1601
1817
  }
1602
1818
  // ===================== 自动压缩 =====================
1819
+ /** 当前上下文 token 用量:优先采信 API 返回的精确数量,否则用 chars/4 估算 */
1820
+ _compactionTokens() {
1821
+ if (this._lastContextTokens != null) {
1822
+ return { tokens: this._lastContextTokens, estimated: false };
1823
+ }
1824
+ return {
1825
+ tokens: estimateTokens(this._outboundMessages()),
1826
+ estimated: true
1827
+ };
1828
+ }
1603
1829
  /** 检查是否需要压缩(每轮 LLM 调用前) */
1604
1830
  _compactionDue() {
1605
- const signal = this._lastContextTokens ?? estimateTokens(this._outboundMessages());
1606
- return shouldCompact(signal, this._getContextWindow());
1831
+ const { tokens } = this._compactionTokens();
1832
+ return shouldCompact(
1833
+ tokens,
1834
+ this._getContextWindow(),
1835
+ this._thresholdPct,
1836
+ this._capTokens
1837
+ );
1838
+ }
1839
+ /** 组装压缩开始时的用量信息(供 hook 与 COMPACTING 事件携带) */
1840
+ _compactionStartInfo(trigger) {
1841
+ const { tokens, estimated } = this._compactionTokens();
1842
+ const contextWindow = this._getContextWindow();
1843
+ return {
1844
+ trigger,
1845
+ tokens,
1846
+ estimated,
1847
+ contextWindow,
1848
+ thresholdPct: this._thresholdPct,
1849
+ thresholdTokens: triggerTokens(
1850
+ contextWindow,
1851
+ this._thresholdPct,
1852
+ this._capTokens
1853
+ ),
1854
+ outboundMessages: this._outboundMessages().length
1855
+ };
1856
+ }
1857
+ /** 组装压缩结果信息(供 hook 与 COMPACTED 事件携带) */
1858
+ _compactionDoneInfo(result) {
1859
+ return {
1860
+ method: result.method === "trimmed" ? "trimmed" : "summarized",
1861
+ messagesBefore: result.messagesBefore ?? 0,
1862
+ messagesAfter: result.messagesAfter ?? 0,
1863
+ ...result.modelUsed ? { model: result.modelUsed } : {},
1864
+ ...result.keptUserMessages !== void 0 ? { keptUserMessages: result.keptUserMessages } : {},
1865
+ ...result.droppedUserMessages !== void 0 ? { droppedUserMessages: result.droppedUserMessages } : {}
1866
+ };
1607
1867
  }
1608
1868
  /** 获取当前模型的上下文窗口大小(token 数)。优先级:显式配置 > 自动检测 > 默认值 128K。 */
1609
1869
  _getContextWindow() {
@@ -1613,8 +1873,13 @@ var TurnEngine = class _TurnEngine {
1613
1873
  /** 执行压缩策略(对齐 Python 版 _compact_now) */
1614
1874
  async _compactNow(reqId, force = false) {
1615
1875
  const contextWindow = this._getContextWindow();
1876
+ const messagesBefore = this._outboundMessages().length;
1616
1877
  const keep = Math.floor(
1617
- KEEP_RECENT_FRACTION * triggerTokens(contextWindow)
1878
+ KEEP_RECENT_FRACTION * triggerTokens(
1879
+ contextWindow,
1880
+ this._thresholdPct,
1881
+ this._capTokens
1882
+ )
1618
1883
  );
1619
1884
  const model = this.model;
1620
1885
  let state = null;
@@ -1646,7 +1911,9 @@ var TurnEngine = class _TurnEngine {
1646
1911
  boundaryIndex: state.boundaryIndex,
1647
1912
  summaryPreview: state.summaryText.slice(0, 200),
1648
1913
  keptUserMessages: state.userMessages.length,
1649
- droppedUserMessages: state.userMessagesDropped
1914
+ droppedUserMessages: state.userMessagesDropped,
1915
+ messagesBefore,
1916
+ messagesAfter: this._outboundMessages().length
1650
1917
  };
1651
1918
  }
1652
1919
  if (failed || force) {
@@ -1657,7 +1924,9 @@ var TurnEngine = class _TurnEngine {
1657
1924
  return {
1658
1925
  notice: "\u4E0A\u4E0B\u6587\u5DF2\u88C1\u526A \u2014 \u6700\u65E7\u7684\u8F6E\u6B21\u5DF2\u4E22\u5F03\uFF08\u6458\u8981\u4E0D\u53EF\u7528\uFF09",
1659
1926
  method: "trimmed",
1660
- boundaryIndex: trimmed.boundaryIndex
1927
+ boundaryIndex: trimmed.boundaryIndex,
1928
+ messagesBefore,
1929
+ messagesAfter: this._outboundMessages().length
1661
1930
  };
1662
1931
  }
1663
1932
  }
@@ -1671,9 +1940,9 @@ var TurnEngine = class _TurnEngine {
1671
1940
  return;
1672
1941
  }
1673
1942
  if (result && typeof result === "object") {
1674
- const path4 = result.path;
1675
- if (path4) {
1676
- this._producedFiles.push(path4);
1943
+ const path8 = result.path;
1944
+ if (path8) {
1945
+ this._producedFiles.push(path8);
1677
1946
  return;
1678
1947
  }
1679
1948
  const changed = result.changed_files;
@@ -1755,20 +2024,36 @@ var TurnEngine = class _TurnEngine {
1755
2024
  }
1756
2025
  };
1757
2026
 
2027
+ // src/engine/limits.ts
2028
+ function parseMaxIterations(value, fallback) {
2029
+ if (value === void 0 || value === null || value.trim() === "") {
2030
+ return fallback;
2031
+ }
2032
+ const n = Number(value);
2033
+ if (!Number.isFinite(n) || n <= 0) return fallback;
2034
+ return Math.floor(n);
2035
+ }
2036
+
1758
2037
  // src/tools/pathSafety.ts
1759
2038
  var fs2 = __toESM(require("fs"), 1);
1760
2039
  var path = __toESM(require("path"), 1);
1761
- function isWithinRoot(workspace, target) {
2040
+ function isInsideRelative(rel) {
2041
+ return !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`);
2042
+ }
2043
+ function isFilesystemRoot(p) {
2044
+ return path.parse(p).root === p;
2045
+ }
2046
+ function resolveWithinRoot(workspace, target) {
2047
+ const abs = path.resolve(workspace, target);
1762
2048
  let rootReal;
1763
2049
  try {
1764
2050
  rootReal = fs2.realpathSync(workspace);
1765
2051
  } catch {
1766
2052
  const rootAbs = path.resolve(workspace);
1767
- const abs2 = path.resolve(workspace, target);
1768
- const rel2 = path.relative(rootAbs, abs2);
1769
- return !rel2.startsWith("..") && !path.isAbsolute(rel2);
2053
+ if (!isInsideRelative(path.relative(rootAbs, abs))) return null;
2054
+ return { root: rootAbs, real: abs };
1770
2055
  }
1771
- const abs = path.resolve(workspace, target);
2056
+ if (isFilesystemRoot(rootReal)) return null;
1772
2057
  let real;
1773
2058
  try {
1774
2059
  real = fs2.realpathSync(abs);
@@ -1782,26 +2067,36 @@ function isWithinRoot(workspace, target) {
1782
2067
  real = path.join(fs2.realpathSync(probe), ...tail);
1783
2068
  break;
1784
2069
  } catch {
1785
- return false;
2070
+ return null;
1786
2071
  }
1787
2072
  }
1788
2073
  tail.unshift(path.basename(probe));
1789
2074
  const parent = path.dirname(probe);
1790
- if (parent === probe) return false;
2075
+ if (parent === probe) return null;
1791
2076
  probe = parent;
1792
2077
  }
1793
2078
  }
1794
- const rel = path.relative(rootReal, real);
1795
- return !rel.startsWith("..") && !path.isAbsolute(rel);
2079
+ if (!isInsideRelative(path.relative(rootReal, real))) return null;
2080
+ return { root: rootReal, real };
2081
+ }
2082
+ function isWithinRoot(workspace, target) {
2083
+ return resolveWithinRoot(workspace, target) !== null;
1796
2084
  }
1797
2085
 
1798
2086
  // src/permissions/PermissionEngine.ts
1799
2087
  var path2 = __toESM(require("path"), 1);
1800
2088
  var READ_ONLY_MODES = /* @__PURE__ */ new Set(["discuss" /* DISCUSS */, "plan" /* PLAN */]);
1801
- var WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "replace_in_file", "apply_patch", "apply_unified_diff"]);
2089
+ var WRITE_TOOLS = /* @__PURE__ */ new Set([
2090
+ "write_file",
2091
+ "replace_in_file",
2092
+ "apply_patch",
2093
+ "apply_unified_diff"
2094
+ ]);
1802
2095
  var SHELL_TOOL = "run_shell";
1803
2096
  var BASE_RISK = {
1804
- ...Object.fromEntries([...WRITE_TOOLS].map((n) => [n, "write_local" /* WRITE_LOCAL */])),
2097
+ ...Object.fromEntries(
2098
+ [...WRITE_TOOLS].map((n) => [n, "write_local" /* WRITE_LOCAL */])
2099
+ ),
1805
2100
  [SHELL_TOOL]: "exec" /* EXEC */
1806
2101
  };
1807
2102
  function classify(toolName, metadata) {
@@ -1819,6 +2114,14 @@ var SHELL_OPERATORS = [";", "&", "|", ">", "<", "`", "$(", "(", "\n", "\r"];
1819
2114
  function hasShellOperators(command) {
1820
2115
  return SHELL_OPERATORS.some((op) => command.includes(op));
1821
2116
  }
2117
+ function commandBin(command) {
2118
+ const first = String(command ?? "").trim().split(/\s+/)[0] ?? "";
2119
+ return first.split("/").pop() ?? first;
2120
+ }
2121
+ function isBareCommand(command) {
2122
+ const first = String(command ?? "").trim().split(/\s+/)[0] ?? "";
2123
+ return first.length > 0 && !first.includes("/");
2124
+ }
1822
2125
  var PermissionEngine = class {
1823
2126
  /** 工作区根目录(写入路径必须位于其下) */
1824
2127
  workspaceRoot;
@@ -1849,12 +2152,20 @@ var PermissionEngine = class {
1849
2152
  const isShell = risk === "exec" /* EXEC */;
1850
2153
  const consequential = isConsequential(risk);
1851
2154
  if (READ_ONLY_MODES.has(this.mode) && consequential) {
1852
- return { allowed: false, reason: `${this.mode} \u6A21\u5F0F\u4E3A\u53EA\u8BFB`, needsUser: false };
2155
+ return {
2156
+ allowed: false,
2157
+ reason: `${this.mode} \u6A21\u5F0F\u4E3A\u53EA\u8BFB`,
2158
+ needsUser: false
2159
+ };
1853
2160
  }
1854
2161
  if (isWrite) {
1855
2162
  const filePath = args["path"];
1856
2163
  if (filePath && !this._underWritableRoot(filePath)) {
1857
- return { allowed: false, reason: `\u8DEF\u5F84\u4E0D\u5728\u53EF\u5199\u76EE\u5F55\u4E0B: ${filePath}`, needsUser: false };
2164
+ return {
2165
+ allowed: false,
2166
+ reason: `\u8DEF\u5F84\u4E0D\u5728\u53EF\u5199\u76EE\u5F55\u4E0B: ${filePath}`,
2167
+ needsUser: false
2168
+ };
1858
2169
  }
1859
2170
  }
1860
2171
  if (!consequential) {
@@ -1868,8 +2179,15 @@ var PermissionEngine = class {
1868
2179
  if (this._commandAllowed(command)) {
1869
2180
  return { allowed: true, reason: "\u547D\u4EE4\u5728\u767D\u540D\u5355\u4E2D", needsUser: false };
1870
2181
  }
1871
- if (command && this.sessionAllowCommands.has(command)) {
1872
- return { allowed: true, reason: "\u4F1A\u8BDD\u5DF2\u5141\u8BB8\u6B64\u547D\u4EE4", needsUser: false };
2182
+ if (!hasShellOperators(command) && isBareCommand(command)) {
2183
+ const bin = commandBin(command);
2184
+ if (bin && this.sessionAllowCommands.has(bin)) {
2185
+ return {
2186
+ allowed: true,
2187
+ reason: "\u4F1A\u8BDD\u5DF2\u5141\u8BB8\u6B64\u547D\u4EE4",
2188
+ needsUser: false
2189
+ };
2190
+ }
1873
2191
  }
1874
2192
  }
1875
2193
  if (this.sessionAllowTools.has(toolName)) {
@@ -1881,9 +2199,10 @@ var PermissionEngine = class {
1881
2199
  allowToolForSession(toolName) {
1882
2200
  this.sessionAllowTools.add(toolName);
1883
2201
  }
1884
- /** 将命令加入会话白名单 */
2202
+ /** 将命令加入会话白名单(按可执行程序名记录,同类命令后续直接放行) */
1885
2203
  allowCommandForSession(command) {
1886
- if (command) this.sessionAllowCommands.add(command);
2204
+ const bin = commandBin(command);
2205
+ if (bin) this.sessionAllowCommands.add(bin);
1887
2206
  }
1888
2207
  /**
1889
2208
  * 检查路径是否在可写根目录下
@@ -1917,10 +2236,11 @@ function makeReadFile(workspace) {
1917
2236
  const filePath = String(args.path ?? "");
1918
2237
  const startLine = Math.max(1, Number(args.start_line ?? 1));
1919
2238
  const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);
1920
- const target = path3.resolve(root, filePath);
1921
- if (!isWithinRoot(root, target)) {
2239
+ const resolved = resolveWithinRoot(root, filePath);
2240
+ if (!resolved) {
1922
2241
  return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
1923
2242
  }
2243
+ const target = resolved.real;
1924
2244
  if (!fs3.existsSync(target) || !fs3.statSync(target).isFile()) {
1925
2245
  return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
1926
2246
  }
@@ -1942,7 +2262,7 @@ function makeReadFile(workspace) {
1942
2262
  }
1943
2263
  const end = startLine + selected.length - 1;
1944
2264
  const result = {
1945
- path: path3.relative(root, target),
2265
+ path: path3.relative(resolved.root, target),
1946
2266
  start_line: startLine,
1947
2267
  end_line: end,
1948
2268
  total_lines: total,
@@ -1954,38 +2274,14 @@ function makeReadFile(workspace) {
1954
2274
  return result;
1955
2275
  };
1956
2276
  }
1957
- function makeWriteFile(workspace) {
1958
- const root = path3.resolve(workspace);
1959
- return function write_file(args) {
1960
- const filePath = String(args.path ?? "");
1961
- const content = String(args.content ?? "");
1962
- const overwrite = args.overwrite !== false;
1963
- const target = path3.resolve(root, filePath);
1964
- if (!isWithinRoot(root, target)) {
1965
- return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
1966
- }
1967
- if (fs3.existsSync(target) && fs3.statSync(target).isDirectory()) {
1968
- return { error: "\u8DEF\u5F84\u662F\u76EE\u5F55\uFF0C\u4E0D\u662F\u6587\u4EF6" };
1969
- }
1970
- if (fs3.existsSync(target) && !overwrite) {
1971
- return { error: "\u6587\u4EF6\u5DF2\u5B58\u5728 (overwrite=false)" };
1972
- }
1973
- try {
1974
- fs3.mkdirSync(path3.dirname(target), { recursive: true });
1975
- fs3.writeFileSync(target, content, "utf-8");
1976
- } catch (exc) {
1977
- return { error: `\u5199\u5165\u5931\u8D25: ${String(exc)}` };
1978
- }
1979
- return path3.relative(root, target);
1980
- };
1981
- }
1982
2277
  function makeListFiles(workspace) {
1983
2278
  const root = path3.resolve(workspace);
1984
2279
  return function list_files(args) {
1985
2280
  const dirPath = String(args.path ?? ".");
1986
2281
  const maxResults = Math.min(Number(args.max_results ?? 100), 100);
1987
- const target = path3.resolve(root, dirPath);
1988
- if (!isWithinRoot(root, target)) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
2282
+ const resolved = resolveWithinRoot(root, dirPath);
2283
+ if (!resolved) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
2284
+ const target = resolved.real;
1989
2285
  if (!fs3.existsSync(target) || !fs3.statSync(target).isDirectory()) {
1990
2286
  return { error: `\u4E0D\u662F\u76EE\u5F55: ${dirPath}` };
1991
2287
  }
@@ -1999,45 +2295,13 @@ function makeListFiles(workspace) {
1999
2295
  results.push(e.isDirectory() ? `${e.name}/` : e.name);
2000
2296
  }
2001
2297
  results.sort();
2002
- return { path: path3.relative(root, target), entries: results.slice(0, maxResults) };
2298
+ return { path: path3.relative(resolved.root, target), entries: results.slice(0, maxResults) };
2003
2299
  } catch (exc) {
2004
2300
  return { error: `\u5217\u51FA\u6587\u4EF6\u5931\u8D25: ${String(exc)}` };
2005
2301
  }
2006
2302
  };
2007
2303
  }
2008
- function makeReplaceInFile(workspace) {
2009
- const root = path3.resolve(workspace);
2010
- return function replace_in_file(args) {
2011
- const filePath = String(args.path ?? "");
2012
- const oldStr = String(args.old ?? "");
2013
- const newStr = String(args.new ?? "");
2014
- const expectedReplacements = Math.max(1, Number(args.expected_replacements ?? 1));
2015
- if (!oldStr) return { error: "old \u4E0D\u80FD\u4E3A\u7A7A" };
2016
- const target = path3.resolve(root, filePath);
2017
- if (!isWithinRoot(root, target)) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
2018
- if (!fs3.existsSync(target) || !fs3.statSync(target).isFile()) {
2019
- return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
2020
- }
2021
- try {
2022
- const content = fs3.readFileSync(target, "utf-8");
2023
- const count = content.split(oldStr).length - 1;
2024
- if (count !== expectedReplacements) {
2025
- return { error: `\u671F\u671B ${expectedReplacements} \u5904\u66FF\u6362\uFF0C\u5B9E\u9645\u627E\u5230 ${count} \u5904: ${filePath}` };
2026
- }
2027
- const updated = content.replaceAll(oldStr, newStr);
2028
- fs3.writeFileSync(target, updated, "utf-8");
2029
- return {
2030
- path: path3.relative(root, target),
2031
- replacements: count,
2032
- chars_before: content.length,
2033
- chars_after: updated.length
2034
- };
2035
- } catch (exc) {
2036
- return { error: `\u66FF\u6362\u5931\u8D25: ${String(exc)}` };
2037
- }
2038
- };
2039
- }
2040
- function registerFileTools(registry, workspace) {
2304
+ function registerReadOnlyFileTools(registry, workspace) {
2041
2305
  const readFile = makeReadFile(workspace);
2042
2306
  registry.register(
2043
2307
  (args) => readFile(args),
@@ -2059,49 +2323,6 @@ function registerFileTools(registry, workspace) {
2059
2323
  }
2060
2324
  }
2061
2325
  );
2062
- const writeFile = makeWriteFile(workspace);
2063
- registry.register(
2064
- (args) => writeFile(args),
2065
- { category: "filesystem", riskLevel: "medium", capabilities: ["write_file"], requiresApproval: true },
2066
- {
2067
- type: "function",
2068
- function: {
2069
- name: "write_file",
2070
- description: "\u5199\u5165\u4E00\u4E2A UTF-8 \u6587\u672C\u6587\u4EF6\u3002\u7528\u4E8E\u521B\u5EFA\u65B0\u6587\u4EF6\u6216\u5B8C\u5168\u91CD\u5199\u73B0\u6709\u6587\u4EF6\u3002",
2071
- parameters: {
2072
- type: "object",
2073
- properties: {
2074
- path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84 (\u76F8\u5BF9\u4E8E\u5DE5\u4F5C\u533A)" },
2075
- content: { type: "string", description: "\u6587\u4EF6\u5185\u5BB9 (UTF-8)" },
2076
- overwrite: { type: "boolean", description: "\u662F\u5426\u8986\u76D6\u5DF2\u5B58\u5728\u7684\u6587\u4EF6 (\u9ED8\u8BA4 true)" }
2077
- },
2078
- required: ["path", "content"]
2079
- }
2080
- }
2081
- }
2082
- );
2083
- const replaceFile = makeReplaceInFile(workspace);
2084
- registry.register(
2085
- (args) => replaceFile(args),
2086
- { category: "filesystem", riskLevel: "medium", capabilities: ["edit_file"], requiresApproval: true },
2087
- {
2088
- type: "function",
2089
- function: {
2090
- name: "replace_in_file",
2091
- description: "\u5728\u6587\u4EF6\u4E2D\u7CBE\u786E\u66FF\u6362\u4E00\u6BB5\u6587\u672C\u3002\u5148\u6309\u9884\u671F\u6B21\u6570\u7CBE\u786E\u5339\u914D old \u5B57\u7B26\u4E32\uFF0C\u7136\u540E\u66FF\u6362\u4E3A new\u3002\u7528\u4E8E\u5C0F\u8303\u56F4\u7CBE\u786E\u7F16\u8F91\u3002",
2092
- parameters: {
2093
- type: "object",
2094
- properties: {
2095
- path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84 (\u76F8\u5BF9\u4E8E\u5DE5\u4F5C\u533A)" },
2096
- old: { type: "string", description: "\u8981\u88AB\u66FF\u6362\u7684\u7CBE\u786E\u6587\u672C" },
2097
- new: { type: "string", description: "\u66FF\u6362\u540E\u7684\u65B0\u6587\u672C" },
2098
- expected_replacements: { type: "integer", description: "\u671F\u671B\u7684\u66FF\u6362\u6B21\u6570 (\u9ED8\u8BA4 1)" }
2099
- },
2100
- required: ["path", "old", "new"]
2101
- }
2102
- }
2103
- }
2104
- );
2105
2326
  const listFiles = makeListFiles(workspace);
2106
2327
  registry.register(
2107
2328
  (args) => listFiles(args),
@@ -2123,47 +2344,515 @@ function registerFileTools(registry, workspace) {
2123
2344
  );
2124
2345
  }
2125
2346
 
2347
+ // src/tools/execAsync.ts
2348
+ var import_node_child_process2 = require("child_process");
2349
+
2350
+ // src/tools/killTree.ts
2351
+ var import_node_child_process = require("child_process");
2352
+ function killTree(pid) {
2353
+ if (!pid) return;
2354
+ if (process.platform === "win32") {
2355
+ try {
2356
+ (0, import_node_child_process.spawnSync)("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
2357
+ } catch {
2358
+ }
2359
+ return;
2360
+ }
2361
+ try {
2362
+ process.kill(-pid, "SIGKILL");
2363
+ } catch {
2364
+ try {
2365
+ process.kill(pid, "SIGKILL");
2366
+ } catch {
2367
+ }
2368
+ }
2369
+ }
2370
+
2371
+ // src/tools/execAsync.ts
2372
+ var liveChildren = /* @__PURE__ */ new Set();
2373
+ var exitCleanupInstalled = false;
2374
+ function trackChild(pid) {
2375
+ if (!pid) return;
2376
+ liveChildren.add(pid);
2377
+ if (exitCleanupInstalled) return;
2378
+ exitCleanupInstalled = true;
2379
+ process.once("exit", () => {
2380
+ for (const p of liveChildren) killTree(p);
2381
+ });
2382
+ }
2383
+ var DEFAULT_MAX_BUFFER = 1024 * 1024 * 5;
2384
+ var KILL_GRACE_MS = 1e3;
2385
+ function makeSink(maxBuffer) {
2386
+ const chunks = [];
2387
+ let len = 0;
2388
+ let over = false;
2389
+ return {
2390
+ push(chunk) {
2391
+ if (len >= maxBuffer) {
2392
+ over = true;
2393
+ return;
2394
+ }
2395
+ const room = maxBuffer - len;
2396
+ if (chunk.length > room) {
2397
+ chunks.push(chunk.subarray(0, room));
2398
+ len = maxBuffer;
2399
+ over = true;
2400
+ return;
2401
+ }
2402
+ chunks.push(chunk);
2403
+ len += chunk.length;
2404
+ },
2405
+ done: () => Buffer.concat(chunks),
2406
+ over: () => over
2407
+ };
2408
+ }
2409
+ function execAsync(file, argv, opts = {}) {
2410
+ return new Promise((resolve7) => {
2411
+ const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
2412
+ const out = makeSink(maxBuffer);
2413
+ const err = makeSink(maxBuffer);
2414
+ if (opts.signal?.aborted) {
2415
+ resolve7({
2416
+ status: null,
2417
+ stdout: Buffer.alloc(0),
2418
+ stderr: Buffer.alloc(0),
2419
+ timedOut: false,
2420
+ truncated: false,
2421
+ aborted: true
2422
+ });
2423
+ return;
2424
+ }
2425
+ let child;
2426
+ try {
2427
+ child = (0, import_node_child_process2.spawn)(file, argv, {
2428
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
2429
+ ...opts.env !== void 0 ? { env: opts.env } : {},
2430
+ // 自成进程组(POSIX):超时/中断时用 killTree 一次收掉整棵进程树
2431
+ detached: process.platform !== "win32",
2432
+ stdio: [opts.input === void 0 ? "ignore" : "pipe", "pipe", "pipe"],
2433
+ windowsHide: true
2434
+ });
2435
+ } catch (e) {
2436
+ resolve7({
2437
+ status: null,
2438
+ stdout: Buffer.alloc(0),
2439
+ stderr: Buffer.alloc(0),
2440
+ error: e,
2441
+ timedOut: false,
2442
+ truncated: false
2443
+ });
2444
+ return;
2445
+ }
2446
+ trackChild(child.pid);
2447
+ let settled = false;
2448
+ let timedOut = false;
2449
+ let aborted = false;
2450
+ let spawnError;
2451
+ let timer = null;
2452
+ let graceTimer = null;
2453
+ const onAbort = () => {
2454
+ aborted = true;
2455
+ killAndSettle();
2456
+ };
2457
+ const finish = (status) => {
2458
+ if (settled) return;
2459
+ settled = true;
2460
+ if (timer) clearTimeout(timer);
2461
+ if (graceTimer) clearTimeout(graceTimer);
2462
+ if (child.pid !== void 0) liveChildren.delete(child.pid);
2463
+ opts.signal?.removeEventListener("abort", onAbort);
2464
+ const result = {
2465
+ status,
2466
+ stdout: out.done(),
2467
+ stderr: err.done(),
2468
+ timedOut,
2469
+ truncated: out.over() || err.over()
2470
+ };
2471
+ if (aborted) result.aborted = true;
2472
+ if (spawnError) result.error = spawnError;
2473
+ resolve7(result);
2474
+ };
2475
+ const killAndSettle = () => {
2476
+ killTree(child.pid);
2477
+ if (settled || graceTimer) return;
2478
+ graceTimer = setTimeout(() => {
2479
+ child.stdout?.destroy();
2480
+ child.stderr?.destroy();
2481
+ finish(null);
2482
+ }, KILL_GRACE_MS);
2483
+ };
2484
+ if (opts.timeoutMs !== void 0 && opts.timeoutMs > 0) {
2485
+ timer = setTimeout(() => {
2486
+ timedOut = true;
2487
+ killAndSettle();
2488
+ }, opts.timeoutMs);
2489
+ }
2490
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
2491
+ child.stdout?.on("data", (c) => out.push(c));
2492
+ child.stderr?.on("data", (c) => err.push(c));
2493
+ child.on("error", (e) => {
2494
+ spawnError = e;
2495
+ if (child.pid === void 0) finish(null);
2496
+ });
2497
+ if (opts.input !== void 0 && child.stdin) {
2498
+ child.stdin.on("error", () => {
2499
+ });
2500
+ child.stdin.end(opts.input);
2501
+ }
2502
+ child.on("close", (code) => finish(code));
2503
+ });
2504
+ }
2505
+
2506
+ // src/tools/shellRuntime.ts
2507
+ var path4 = __toESM(require("path"), 1);
2508
+ var WINDOWS_SUFFIXES = [".exe", ".cmd", ".bat", ".com", ""];
2509
+ function pathApi(platform) {
2510
+ return platform === "win32" ? path4.win32 : path4.posix;
2511
+ }
2512
+ function joinPath(platform, ...parts) {
2513
+ return pathApi(platform).join(...parts);
2514
+ }
2515
+ function findExecutable(name, ctx) {
2516
+ if (name.includes("/") || name.includes("\\")) {
2517
+ return ctx.fileExists(name) ? name : null;
2518
+ }
2519
+ const suffixes = ctx.platform === "win32" ? WINDOWS_SUFFIXES : [""];
2520
+ const delimiter = ctx.platform === "win32" ? ";" : ":";
2521
+ const dirs = (ctx.env.PATH ?? "").split(delimiter).filter((d) => d.length > 0);
2522
+ for (const dir of dirs) {
2523
+ for (const suffix of suffixes) {
2524
+ const candidate = joinPath(ctx.platform, dir, name + suffix);
2525
+ if (ctx.fileExists(candidate)) return candidate;
2526
+ }
2527
+ }
2528
+ return null;
2529
+ }
2530
+
2531
+ // src/tools/shellBackground.ts
2532
+ var import_node_child_process3 = require("child_process");
2533
+ var DEFAULT_BUFFER_LIMIT = 64 * 1024;
2534
+ var RingBuffer = class {
2535
+ constructor(limit = DEFAULT_BUFFER_LIMIT) {
2536
+ this.limit = limit;
2537
+ }
2538
+ limit;
2539
+ chunks = [];
2540
+ size = 0;
2541
+ add(chunk) {
2542
+ if (!chunk) return;
2543
+ this.chunks.push(chunk);
2544
+ this.size += chunk.length;
2545
+ while (this.size > this.limit && this.chunks.length > 1) {
2546
+ this.size -= this.chunks.shift().length;
2547
+ }
2548
+ if (this.size > this.limit) {
2549
+ const joined = this.chunks.join("");
2550
+ this.chunks = [joined.slice(-this.limit)];
2551
+ this.size = this.chunks[0].length;
2552
+ }
2553
+ }
2554
+ get() {
2555
+ const joined = this.chunks.join("");
2556
+ return joined.length > this.limit ? joined.slice(-this.limit) : joined;
2557
+ }
2558
+ };
2559
+ var ShellTaskManager = class {
2560
+ tasks = /* @__PURE__ */ new Map();
2561
+ seq = 0;
2562
+ cleanupInstalled = false;
2563
+ /** 启动一个后台任务,立即返回快照(不等退出) */
2564
+ start(opts) {
2565
+ this.installCleanup();
2566
+ const id = `shell-${++this.seq}-${Date.now().toString(36)}`;
2567
+ const child = (0, import_node_child_process3.spawn)(opts.file, opts.argv, {
2568
+ cwd: opts.cwd,
2569
+ env: opts.env,
2570
+ detached: process.platform !== "win32",
2571
+ windowsHide: true,
2572
+ stdio: ["ignore", "pipe", "pipe"]
2573
+ });
2574
+ const task = {
2575
+ id,
2576
+ command: opts.command,
2577
+ scope: opts.scope ?? null,
2578
+ child,
2579
+ stdout: new RingBuffer(),
2580
+ stderr: new RingBuffer(),
2581
+ status: "running",
2582
+ exitCode: null,
2583
+ signal: null,
2584
+ startedAt: Date.now(),
2585
+ endedAt: null
2586
+ };
2587
+ this.tasks.set(id, task);
2588
+ child.stdout?.on("data", (d) => task.stdout.add(d.toString("utf-8")));
2589
+ child.stderr?.on("data", (d) => task.stderr.add(d.toString("utf-8")));
2590
+ child.on("error", (err) => task.stderr.add(`[spawn error] ${err.message}
2591
+ `));
2592
+ child.on("exit", (code, signal) => {
2593
+ task.exitCode = code;
2594
+ task.signal = signal;
2595
+ task.endedAt = Date.now();
2596
+ if (task.status !== "killed") task.status = "exited";
2597
+ });
2598
+ child.unref();
2599
+ return this.snapshot(task);
2600
+ }
2601
+ /** 读取任务输出与状态 */
2602
+ output(taskId) {
2603
+ const task = this.tasks.get(taskId);
2604
+ if (!task) return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
2605
+ return this.snapshot(task);
2606
+ }
2607
+ /** 终止任务(进程树)并返回快照;传 scope 时只允许终止该归属下的任务 */
2608
+ kill(taskId, scope) {
2609
+ const task = this.tasks.get(taskId);
2610
+ if (!task) return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
2611
+ if (scope !== void 0 && task.scope !== scope) {
2612
+ return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
2613
+ }
2614
+ if (task.status === "running") {
2615
+ task.status = "killed";
2616
+ task.endedAt = Date.now();
2617
+ killTree(task.child.pid);
2618
+ }
2619
+ return this.snapshot(task);
2620
+ }
2621
+ /** 列出任务快照(调试/展示用);传 scope 时只返回该归属下的任务 */
2622
+ list(scope) {
2623
+ const all = [...this.tasks.values()];
2624
+ return (scope === void 0 ? all : all.filter((t) => t.scope === scope)).map(
2625
+ (t) => this.snapshot(t)
2626
+ );
2627
+ }
2628
+ /** 终止全部任务(进程退出时调用) */
2629
+ disposeAll() {
2630
+ for (const task of this.tasks.values()) {
2631
+ if (task.status === "running") {
2632
+ task.status = "killed";
2633
+ killTree(task.child.pid);
2634
+ }
2635
+ }
2636
+ }
2637
+ snapshot(task) {
2638
+ return {
2639
+ task_id: task.id,
2640
+ command: task.command,
2641
+ status: task.status,
2642
+ running: task.status === "running",
2643
+ exit_code: task.exitCode,
2644
+ signal: task.signal,
2645
+ stdout: task.stdout.get(),
2646
+ stderr: task.stderr.get(),
2647
+ duration_ms: (task.endedAt ?? Date.now()) - task.startedAt
2648
+ };
2649
+ }
2650
+ installCleanup() {
2651
+ if (this.cleanupInstalled) return;
2652
+ this.cleanupInstalled = true;
2653
+ process.once("exit", () => this.disposeAll());
2654
+ }
2655
+ };
2656
+
2657
+ // src/tools/shell.ts
2658
+ var MAX_BUFFER = 1024 * 1024 * 5;
2659
+ function decodeOutput(buf) {
2660
+ if (!buf || buf.length === 0) return "";
2661
+ try {
2662
+ return new TextDecoder("utf-8", { fatal: true }).decode(buf);
2663
+ } catch {
2664
+ for (const enc of ["gbk", "big5", "shift_jis", "latin1"]) {
2665
+ try {
2666
+ return new TextDecoder(enc).decode(buf);
2667
+ } catch {
2668
+ }
2669
+ }
2670
+ return buf.toString("latin1");
2671
+ }
2672
+ }
2673
+ var shellTasks = new ShellTaskManager();
2674
+
2675
+ // src/tools/nodeGrep.ts
2676
+ var fs4 = __toESM(require("fs"), 1);
2677
+ var path5 = __toESM(require("path"), 1);
2678
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
2679
+ ".git",
2680
+ "node_modules",
2681
+ ".svn",
2682
+ ".hg",
2683
+ "dist",
2684
+ "build",
2685
+ ".next",
2686
+ ".cache"
2687
+ ]);
2688
+ function looksBinary(buf) {
2689
+ const n = Math.min(buf.length, 8192);
2690
+ for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
2691
+ return false;
2692
+ }
2693
+ function nodeGrep(opts) {
2694
+ const maxMatches = opts.maxMatches ?? 50;
2695
+ const maxFileBytes = opts.maxFileBytes ?? 1024 * 1024;
2696
+ let regex;
2697
+ try {
2698
+ regex = new RegExp(opts.pattern);
2699
+ } catch (err) {
2700
+ throw new Error(`\u65E0\u6548\u7684\u6B63\u5219\u8868\u8FBE\u5F0F: ${opts.pattern}\uFF08${String(err)}\uFF09`);
2701
+ }
2702
+ const target = path5.resolve(opts.workspace, opts.target);
2703
+ if (!fs4.existsSync(target)) {
2704
+ throw new Error(`\u641C\u7D22\u8DEF\u5F84\u4E0D\u5B58\u5728: ${opts.target}`);
2705
+ }
2706
+ const matches = [];
2707
+ const scanFile = (file) => {
2708
+ let stat2;
2709
+ try {
2710
+ stat2 = fs4.statSync(file);
2711
+ } catch {
2712
+ return false;
2713
+ }
2714
+ if (!stat2.isFile() || stat2.size > maxFileBytes) return false;
2715
+ let buf;
2716
+ try {
2717
+ buf = fs4.readFileSync(file);
2718
+ } catch {
2719
+ return false;
2720
+ }
2721
+ if (looksBinary(buf)) return false;
2722
+ const rel = path5.relative(opts.workspace, file) || path5.basename(file);
2723
+ const lines = buf.toString("utf-8").split("\n");
2724
+ for (let i = 0; i < lines.length; i++) {
2725
+ const raw = lines[i].replace(/\r$/, "");
2726
+ if (regex.test(raw)) {
2727
+ matches.push({ file: rel, line: i + 1, text: raw.trim().slice(0, 200) });
2728
+ if (matches.length >= maxMatches) return true;
2729
+ }
2730
+ }
2731
+ return false;
2732
+ };
2733
+ const stat = fs4.statSync(target);
2734
+ if (stat.isFile()) {
2735
+ scanFile(target);
2736
+ return matches;
2737
+ }
2738
+ const stack = [target];
2739
+ while (stack.length > 0) {
2740
+ const dir = stack.pop();
2741
+ let entries;
2742
+ try {
2743
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
2744
+ } catch {
2745
+ continue;
2746
+ }
2747
+ for (const entry of entries) {
2748
+ const full = path5.join(dir, entry.name);
2749
+ if (entry.isDirectory()) {
2750
+ if (!SKIP_DIRS.has(entry.name)) stack.push(full);
2751
+ } else if (entry.isFile()) {
2752
+ if (scanFile(full)) return matches;
2753
+ }
2754
+ }
2755
+ }
2756
+ return matches;
2757
+ }
2758
+
2759
+ // src/tools/ripgrep.ts
2760
+ var fs5 = __toESM(require("fs"), 1);
2761
+ var os = __toESM(require("os"), 1);
2762
+ var path6 = __toESM(require("path"), 1);
2763
+ var import_node_module = require("module");
2764
+ var cached;
2765
+ function extractBundledRipgrep() {
2766
+ try {
2767
+ const req = typeof require === "function" ? require : (0, import_node_module.createRequire)(path6.join(process.cwd(), "__zqagent__.js"));
2768
+ const sea = req("node:sea");
2769
+ if (!sea?.isSea?.() || typeof sea.getAsset !== "function") return null;
2770
+ const asset = sea.getAsset("rg");
2771
+ if (!asset) return null;
2772
+ const dir = fs5.mkdtempSync(path6.join(os.tmpdir(), "zqagent-rg-"));
2773
+ const file = path6.join(dir, process.platform === "win32" ? "rg.exe" : "rg");
2774
+ fs5.writeFileSync(file, Buffer.from(asset));
2775
+ fs5.chmodSync(file, 493);
2776
+ return file;
2777
+ } catch {
2778
+ return null;
2779
+ }
2780
+ }
2781
+ function resolveRipgrep() {
2782
+ if (cached !== void 0) return cached;
2783
+ const explicit = (process.env.ZQAGENT_RG ?? "").trim();
2784
+ if (explicit && fs5.existsSync(explicit)) {
2785
+ cached = explicit;
2786
+ return cached;
2787
+ }
2788
+ const bundled = extractBundledRipgrep();
2789
+ if (bundled) {
2790
+ cached = bundled;
2791
+ return cached;
2792
+ }
2793
+ cached = findExecutable("rg", {
2794
+ platform: process.platform,
2795
+ env: process.env,
2796
+ fileExists: (p) => {
2797
+ try {
2798
+ return fs5.statSync(p).isFile();
2799
+ } catch {
2800
+ return false;
2801
+ }
2802
+ }
2803
+ });
2804
+ return cached;
2805
+ }
2806
+
2126
2807
  // src/tools/search.ts
2127
- var import_child_process = require("child_process");
2808
+ function parseRgOutput(result) {
2809
+ return result.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0).slice(0, 50).map((line) => {
2810
+ const m = /^(.+?):(\d+):(.*)$/.exec(line);
2811
+ if (!m) return { file: line.trim(), line: 0, text: "" };
2812
+ return { file: m[1], line: Number(m[2]), text: m[3].trim().slice(0, 200) };
2813
+ });
2814
+ }
2128
2815
  function makeGrep(workspace) {
2129
- return function grep(args) {
2816
+ return async function grep(args) {
2130
2817
  const pattern = String(args.pattern ?? "");
2131
2818
  const filePath = String(args.path ?? ".");
2132
2819
  if (!pattern.trim()) {
2133
2820
  return { error: "\u641C\u7D22\u6A21\u5F0F\u4E0D\u80FD\u4E3A\u7A7A" };
2134
2821
  }
2135
- const execOpts = {
2136
- cwd: workspace,
2137
- encoding: "utf-8",
2138
- timeout: 1e4,
2139
- maxBuffer: 1024 * 1024 * 2
2140
- };
2141
- try {
2142
- let result;
2143
- try {
2144
- result = (0, import_child_process.execFileSync)(
2145
- "rg",
2146
- ["--line-number", "--no-heading", "--max-count", "50", pattern, filePath],
2147
- execOpts
2148
- ).toString();
2149
- } catch {
2150
- result = (0, import_child_process.execFileSync)(
2151
- "grep",
2152
- ["-rn", "--max-count", "50", pattern, filePath],
2153
- execOpts
2154
- ).toString();
2822
+ const rg = resolveRipgrep();
2823
+ if (rg) {
2824
+ const res = await execAsync(
2825
+ rg,
2826
+ [
2827
+ // --with-filename:单文件模式下 rg 默认不输出文件名前缀,会导致
2828
+ // "行号:内容" 被误当成 "文件:行号:内容" 解析而错位;显式要求带文件名。
2829
+ "--with-filename",
2830
+ "--line-number",
2831
+ "--no-heading",
2832
+ "--max-count",
2833
+ "50",
2834
+ pattern,
2835
+ filePath
2836
+ ],
2837
+ {
2838
+ cwd: workspace,
2839
+ timeoutMs: 1e4,
2840
+ maxBuffer: 1024 * 1024 * 2
2841
+ }
2842
+ );
2843
+ if (res.status === 0) {
2844
+ const matches = parseRgOutput(decodeOutput(res.stdout));
2845
+ return { query: pattern, engine: "ripgrep", matches, count: matches.length };
2155
2846
  }
2156
- const lines = result.trim().split("\n").slice(0, 50);
2157
- const matches = lines.map((line) => {
2158
- const [file, lineno, ...rest] = line.split(":");
2159
- return { file, line: Number(lineno), text: rest.join(":").trim().slice(0, 200) };
2160
- });
2161
- return { query: pattern, matches, count: matches.length };
2162
- } catch (exc) {
2163
- if (exc instanceof Error && "status" in exc && exc.status === 1) {
2164
- return { query: pattern, matches: [], count: 0 };
2847
+ if (res.status === 1) {
2848
+ return { query: pattern, engine: "ripgrep", matches: [], count: 0 };
2165
2849
  }
2166
- return { error: `\u641C\u7D22\u5931\u8D25: ${String(exc)}` };
2850
+ }
2851
+ try {
2852
+ const matches = nodeGrep({ pattern, target: filePath, workspace });
2853
+ return { query: pattern, engine: "node", matches, count: matches.length };
2854
+ } catch (exc) {
2855
+ return { error: `\u641C\u7D22\u5931\u8D25: ${String(exc instanceof Error ? exc.message : exc)}` };
2167
2856
  }
2168
2857
  };
2169
2858
  }
@@ -2176,7 +2865,7 @@ function registerSearchTool(registry, workspace) {
2176
2865
  type: "function",
2177
2866
  function: {
2178
2867
  name: "grep",
2179
- description: "\u5728\u6587\u4EF6\u4E2D\u641C\u7D22\u6587\u672C\u6A21\u5F0F\u3002\u8FD4\u56DE\u5339\u914D\u7684\u6587\u4EF6\u540D\u3001\u884C\u53F7\u548C\u5185\u5BB9\u3002\u4F18\u5148\u4F7F\u7528 ripgrep (rg) \u4EE5\u83B7\u5F97\u66F4\u597D\u7684\u6027\u80FD\u3002",
2868
+ description: "\u5728\u6587\u4EF6\u4E2D\u641C\u7D22\u6587\u672C\u6A21\u5F0F\uFF08\u652F\u6301\u5B8C\u6574 JS \u6B63\u5219\u8BED\u6CD5\uFF09\u3002\u8FD4\u56DE\u5339\u914D\u7684\u6587\u4EF6\u540D\u3001\u884C\u53F7\u548C\u5185\u5BB9\uFF0C\u6700\u591A 50 \u6761\u3002\u4F18\u5148\u4F7F\u7528 ripgrep (rg) \u4EE5\u83B7\u5F97\u66F4\u597D\u7684\u6027\u80FD\uFF1Brg \u4E0D\u53EF\u7528\u65F6\u81EA\u52A8\u56DE\u9000\u5230\u5185\u7F6E\u626B\u63CF\u5668\u3002",
2180
2869
  parameters: {
2181
2870
  type: "object",
2182
2871
  properties: {
@@ -2191,90 +2880,138 @@ function registerSearchTool(registry, workspace) {
2191
2880
  }
2192
2881
 
2193
2882
  // src/tools/git.ts
2194
- var import_child_process2 = require("child_process");
2883
+ var fs6 = __toESM(require("fs"), 1);
2884
+ var path7 = __toESM(require("path"), 1);
2885
+ async function runGit(repoDir, args, timeout = 15e3) {
2886
+ const res = await execAsync("git", ["-c", "core.quotepath=false", ...args], {
2887
+ cwd: repoDir,
2888
+ timeoutMs: timeout,
2889
+ maxBuffer: 1024 * 1024 * 5,
2890
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
2891
+ });
2892
+ return {
2893
+ status: res.status ?? 1,
2894
+ stdout: decodeOutput(res.stdout),
2895
+ stderr: decodeOutput(res.stderr) || (res.error?.message ?? "")
2896
+ };
2897
+ }
2898
+ async function resolveRepoRoot(workspace, pathArg) {
2899
+ let dir = workspace;
2900
+ if (pathArg) {
2901
+ const abs = path7.resolve(workspace, pathArg);
2902
+ try {
2903
+ dir = fs6.statSync(abs).isDirectory() ? abs : path7.dirname(abs);
2904
+ } catch {
2905
+ dir = abs;
2906
+ }
2907
+ }
2908
+ const res = await runGit(dir, ["rev-parse", "--show-toplevel"], 1e4);
2909
+ if (res.status === 0 && res.stdout.trim()) return res.stdout.trim();
2910
+ return workspace;
2911
+ }
2912
+ function repoDirArg(workspace, raw) {
2913
+ if (typeof raw !== "string") return void 0;
2914
+ const v = raw.trim();
2915
+ if (!v) return void 0;
2916
+ const base = path7.resolve(workspace);
2917
+ const abs = path7.resolve(base, v);
2918
+ const rel = path7.relative(base, abs);
2919
+ if (rel.startsWith("..") || path7.isAbsolute(rel)) return void 0;
2920
+ return abs;
2921
+ }
2922
+ function toPathspec(repoRoot, workspace, pathArg) {
2923
+ if (!pathArg) return null;
2924
+ const abs = path7.resolve(workspace, pathArg);
2925
+ let rel = path7.relative(repoRoot, abs);
2926
+ if (rel === "") return ".";
2927
+ if (rel.startsWith("..") || path7.isAbsolute(rel)) {
2928
+ try {
2929
+ const realRepo = fs6.realpathSync(repoRoot);
2930
+ const realAbs = fs6.realpathSync(abs);
2931
+ const rel2 = path7.relative(realRepo, realAbs);
2932
+ if (rel2 === "") return ".";
2933
+ if (!rel2.startsWith("..") && !path7.isAbsolute(rel2)) return rel2;
2934
+ } catch {
2935
+ }
2936
+ return null;
2937
+ }
2938
+ return rel;
2939
+ }
2195
2940
  function makeGitLog(workspace) {
2196
- return function git_log(args) {
2941
+ return async function git_log(args) {
2197
2942
  const filePath = args.path ? String(args.path) : void 0;
2198
2943
  const maxCount = Math.min(Number(args.max_count ?? 20), 200);
2199
2944
  if (maxCount < 1) return { error: "max_count \u5FC5\u987B >= 1" };
2945
+ const repo = await resolveRepoRoot(workspace, repoDirArg(workspace, args.repo) ?? filePath);
2200
2946
  const cmd = [
2201
- "-C",
2202
- workspace,
2203
2947
  "log",
2204
2948
  `-n${maxCount}`,
2205
2949
  "--pretty=format:%h%x1f%an%x1f%ad%x1f%s",
2206
2950
  "--date=short"
2207
2951
  ];
2208
- if (filePath) cmd.push("--", filePath);
2209
- try {
2210
- const out = (0, import_child_process2.execFileSync)("git", cmd, { encoding: "utf-8", timeout: 15e3 });
2211
- const commits = out.trim().split("\n").filter(Boolean).map((line) => {
2212
- const parts = line.split("");
2213
- if (parts.length === 4) {
2214
- return { hash: parts[0], author: parts[1], date: parts[2], subject: parts[3] };
2215
- }
2216
- return { raw: line };
2217
- });
2218
- return { count: commits.length, commits };
2219
- } catch (exc) {
2220
- const err = exc;
2221
- return { error: (err.stderr ?? err.message ?? "git log failed").slice(0, 300) };
2222
- }
2952
+ const pathspec = toPathspec(repo, workspace, filePath);
2953
+ if (pathspec) cmd.push("--", pathspec);
2954
+ const res = await runGit(repo, cmd);
2955
+ if (res.status !== 0) {
2956
+ return { error: (res.stderr || "git log failed").trim().slice(0, 300) };
2957
+ }
2958
+ const commits = res.stdout.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0).map((line) => {
2959
+ const parts = line.split("");
2960
+ if (parts.length === 4) {
2961
+ return { hash: parts[0], author: parts[1], date: parts[2], subject: parts[3] };
2962
+ }
2963
+ return { raw: line };
2964
+ });
2965
+ return { repo, count: commits.length, commits };
2223
2966
  };
2224
2967
  }
2225
2968
  function makeGitStatus(workspace) {
2226
- return function git_status(_args) {
2227
- try {
2228
- const out = (0, import_child_process2.execFileSync)(
2229
- "git",
2230
- ["-C", workspace, "status", "--porcelain"],
2231
- { encoding: "utf-8", timeout: 1e4 }
2232
- );
2233
- const lines = out.trim().split("\n").filter(Boolean);
2234
- const entries = lines.map((line) => {
2235
- const status = line.slice(0, 2).trim();
2236
- const file = line.slice(3);
2237
- return { status, file };
2238
- });
2239
- return {
2240
- clean: entries.length === 0,
2241
- entries,
2242
- count: entries.length
2243
- };
2244
- } catch (exc) {
2245
- const err = exc;
2246
- if ((err.stderr ?? "").toLowerCase().includes("not a git repository")) {
2969
+ return async function git_status(args) {
2970
+ const filePath = args.path ? String(args.path) : void 0;
2971
+ const repo = await resolveRepoRoot(workspace, repoDirArg(workspace, args.repo) ?? filePath);
2972
+ const res = await runGit(repo, ["status", "--porcelain"], 1e4);
2973
+ if (res.status !== 0) {
2974
+ const msg = res.stderr;
2975
+ if (msg.toLowerCase().includes("not a git repository")) {
2247
2976
  return { error: "\u4E0D\u662F git \u4ED3\u5E93" };
2248
2977
  }
2249
- return { error: (err.stderr ?? err.message ?? "git status failed").slice(0, 300) };
2250
- }
2978
+ return { error: (msg || "git status failed").trim().slice(0, 300) };
2979
+ }
2980
+ const lines = res.stdout.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0);
2981
+ const entries = lines.map((line) => {
2982
+ const status = line.slice(0, 2).trim();
2983
+ const index = line.slice(0, 1).trim();
2984
+ const worktree = line.slice(1, 2).trim();
2985
+ let file = line.slice(3);
2986
+ const renameArrow = file.indexOf(" -> ");
2987
+ if (renameArrow >= 0) file = file.slice(renameArrow + 4);
2988
+ return { status, file, index, worktree };
2989
+ });
2990
+ return { repo, clean: entries.length === 0, entries, count: entries.length };
2251
2991
  };
2252
2992
  }
2253
2993
  function makeGitDiff(workspace) {
2254
- return function git_diff(args) {
2994
+ return async function git_diff(args) {
2255
2995
  const staged = args.staged === true;
2256
2996
  const filePath = args.path ? String(args.path) : void 0;
2257
2997
  const contextLines = Math.min(Number(args.context ?? 3), 10);
2998
+ const repo = await resolveRepoRoot(workspace, repoDirArg(workspace, args.repo) ?? filePath);
2258
2999
  const cmd = ["diff"];
2259
3000
  if (staged) cmd.push("--staged");
2260
3001
  cmd.push(`-U${contextLines}`);
2261
- if (filePath) cmd.push("--", filePath);
2262
- try {
2263
- const out = (0, import_child_process2.execFileSync)("git", ["-C", workspace, ...cmd], {
2264
- encoding: "utf-8",
2265
- timeout: 15e3,
2266
- maxBuffer: 1024 * 1024 * 2
2267
- });
2268
- return {
2269
- staged,
2270
- path: filePath ?? null,
2271
- diff: out.slice(0, 5e4),
2272
- truncated: out.length > 5e4
2273
- };
2274
- } catch (exc) {
2275
- const err = exc;
2276
- return { error: (err.stderr ?? err.message ?? "git diff failed").slice(0, 300) };
3002
+ const pathspec = toPathspec(repo, workspace, filePath);
3003
+ if (pathspec) cmd.push("--", pathspec);
3004
+ const res = await runGit(repo, cmd);
3005
+ if (res.status !== 0) {
3006
+ return { error: (res.stderr || "git diff failed").trim().slice(0, 300) };
2277
3007
  }
3008
+ return {
3009
+ repo,
3010
+ staged,
3011
+ path: filePath ?? null,
3012
+ diff: res.stdout.slice(0, 5e4),
3013
+ truncated: res.stdout.length > 5e4
3014
+ };
2278
3015
  };
2279
3016
  }
2280
3017
  function registerGitTools(registry, workspace) {
@@ -2286,11 +3023,11 @@ function registerGitTools(registry, workspace) {
2286
3023
  type: "function",
2287
3024
  function: {
2288
3025
  name: "git_log",
2289
- description: "\u67E5\u770B\u6700\u8FD1\u7684 git \u63D0\u4EA4\u5386\u53F2 (hash, \u4F5C\u8005, \u65E5\u671F, \u4E3B\u9898)\u3002\u53EF\u9009\u5730\u9650\u5B9A\u5230\u67D0\u4E2A\u8DEF\u5F84\u3002\u7528\u4E8E\u5728\u7F16\u8F91\u524D\u4E86\u89E3\u4EE3\u7801\u7684\u6F14\u8FDB\u8FC7\u7A0B\u3002\u53EA\u8BFB\u3002",
3026
+ description: "\u67E5\u770B\u6700\u8FD1\u7684 git \u63D0\u4EA4\u5386\u53F2 (hash, \u4F5C\u8005, \u65E5\u671F, \u4E3B\u9898)\u3002\u53EF\u9009\u5730\u9650\u5B9A\u5230\u67D0\u4E2A\u8DEF\u5F84\uFF08\u8BE5\u8DEF\u5F84\u6240\u5728\u4ED3\u5E93\u4F1A\u88AB\u81EA\u52A8\u5B9A\u4F4D\uFF09\u3002\u7528\u4E8E\u5728\u7F16\u8F91\u524D\u4E86\u89E3\u4EE3\u7801\u7684\u6F14\u8FDB\u8FC7\u7A0B\u3002\u53EA\u8BFB\u3002",
2290
3027
  parameters: {
2291
3028
  type: "object",
2292
3029
  properties: {
2293
- path: { type: "string", description: "\u53EF\u9009\uFF1A\u9650\u5B9A\u5230\u67D0\u4E2A\u6587\u4EF6\u6216\u76EE\u5F55" },
3030
+ path: { type: "string", description: "\u53EF\u9009\uFF1A\u9650\u5B9A\u5230\u67D0\u4E2A\u6587\u4EF6\u6216\u76EE\u5F55\uFF08\u5E76\u6309\u5176\u6240\u5728\u4ED3\u5E93\u67E5\u8BE2\uFF09" },
2294
3031
  max_count: { type: "integer", description: "\u8FD4\u56DE\u591A\u5C11\u6761\u63D0\u4EA4 (\u9ED8\u8BA4 20\uFF0C\u6700\u5927 200)" }
2295
3032
  }
2296
3033
  }
@@ -2308,7 +3045,9 @@ function registerGitTools(registry, workspace) {
2308
3045
  description: "\u67E5\u770B\u5DE5\u4F5C\u533A git \u72B6\u6001 (\u5DF2\u4FEE\u6539\u3001\u5DF2\u6682\u5B58\u3001\u672A\u8DDF\u8E2A\u7684\u6587\u4EF6)\u3002\u53EA\u8BFB\u3002",
2309
3046
  parameters: {
2310
3047
  type: "object",
2311
- properties: {}
3048
+ properties: {
3049
+ path: { type: "string", description: "\u53EF\u9009\uFF1A\u4F18\u5148\u67E5\u8BE2\u8BE5\u8DEF\u5F84\u6240\u5728\u7684\u4ED3\u5E93\uFF08\u9ED8\u8BA4\u5DE5\u4F5C\u533A\u6839\uFF09" }
3050
+ }
2312
3051
  }
2313
3052
  }
2314
3053
  }
@@ -2326,7 +3065,7 @@ function registerGitTools(registry, workspace) {
2326
3065
  type: "object",
2327
3066
  properties: {
2328
3067
  staged: { type: "boolean", description: "\u662F\u5426\u67E5\u770B\u5DF2\u6682\u5B58\u7684\u66F4\u6539 (\u9ED8\u8BA4 false)" },
2329
- path: { type: "string", description: "\u53EF\u9009\uFF1A\u9650\u5B9A\u5230\u67D0\u4E2A\u6587\u4EF6" },
3068
+ path: { type: "string", description: "\u53EF\u9009\uFF1A\u9650\u5B9A\u5230\u67D0\u4E2A\u6587\u4EF6\uFF08\u5E76\u6309\u5176\u5728\u4ED3\u5E93\u4E2D\u7684\u8DEF\u5F84\uFF09" },
2330
3069
  context: { type: "integer", description: "\u4E0A\u4E0B\u6587\u884C\u6570 (\u9ED8\u8BA4 3)" }
2331
3070
  }
2332
3071
  }
@@ -2341,7 +3080,7 @@ function buildExplorerEngine(opts) {
2341
3080
  if (opts.readonlyRegistrars) {
2342
3081
  opts.readonlyRegistrars(registry);
2343
3082
  } else {
2344
- registerFileTools(registry, opts.workspace);
3083
+ registerReadOnlyFileTools(registry, opts.workspace);
2345
3084
  registerSearchTool(registry, opts.workspace);
2346
3085
  registerGitTools(registry, opts.workspace);
2347
3086
  }
@@ -2355,7 +3094,8 @@ function buildExplorerEngine(opts) {
2355
3094
  permissions,
2356
3095
  model: opts.model,
2357
3096
  instructions: EXPLORER_INSTRUCTIONS,
2358
- maxIterations: 10
3097
+ maxIterations: parseMaxIterations(process.env.EXPLORE_MAX_ITERATIONS, 30),
3098
+ hook: opts.hook
2359
3099
  });
2360
3100
  }
2361
3101
  function makeExploreTool(opts) {
@@ -2366,7 +3106,8 @@ function makeExploreTool(opts) {
2366
3106
  workspace: opts.workspace,
2367
3107
  provider: opts.provider,
2368
3108
  model: opts.model,
2369
- readonlyRegistrars: opts.readonlyRegistrars
3109
+ readonlyRegistrars: opts.readonlyRegistrars,
3110
+ hook: opts.hookFactory?.()
2370
3111
  });
2371
3112
  let report = "";
2372
3113
  let status = "unknown";