@zqagent/core 0.1.30 → 0.2.5

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 +102 -146
  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
@@ -1,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/tools/ToolRegistry.ts
2
9
  var ToolRegistry = class {
3
10
  /** 工具名 → 工具规格 的映射表 */
@@ -72,12 +79,13 @@ var ToolRegistry = class {
72
79
  /**
73
80
  * 执行工具
74
81
  *
82
+ * @param ctx 执行上下文(引擎注入的中断信号等)
75
83
  * @throws 工具未注册时抛出错误
76
84
  */
77
- execute(name, args) {
85
+ execute(name, args, ctx) {
78
86
  const spec = this.tools.get(name);
79
87
  if (!spec) throw new Error(`Tool not registered: ${name}`);
80
- return spec.func(args);
88
+ return spec.func(args, ctx);
81
89
  }
82
90
  /**
83
91
  * 构建默认 Schema
@@ -103,15 +111,77 @@ function contextTokens(u) {
103
111
  return u.input + u.cacheRead + u.cacheWrite;
104
112
  }
105
113
 
114
+ // src/providers/diagnostics.ts
115
+ function isNetworkErrorText(text) {
116
+ const t = text.toLowerCase();
117
+ return [
118
+ "connection error",
119
+ "fetch failed",
120
+ "econnreset",
121
+ "econnrefused",
122
+ "etimedout",
123
+ "ehostunreach",
124
+ "enetunreach",
125
+ "enotfound",
126
+ "eai_again",
127
+ "socket hang up",
128
+ "und_err",
129
+ "network",
130
+ "timed out",
131
+ "tls",
132
+ "certificate",
133
+ "unable_to_verify",
134
+ "self signed"
135
+ ].some((m) => t.includes(m));
136
+ }
137
+ function asRecord(v) {
138
+ return v && typeof v === "object" ? v : null;
139
+ }
140
+ function causeChain(err, maxNodes = 6) {
141
+ const out = [];
142
+ const seen = /* @__PURE__ */ new Set();
143
+ const queue = [err];
144
+ while (queue.length > 0 && out.length < maxNodes) {
145
+ const cur = queue.shift();
146
+ if (!cur || typeof cur !== "object" || seen.has(cur)) continue;
147
+ seen.add(cur);
148
+ const rec = asRecord(cur);
149
+ if (!rec) continue;
150
+ out.push(rec);
151
+ if (rec["cause"]) queue.push(rec["cause"]);
152
+ const errs = rec["errors"];
153
+ if (Array.isArray(errs)) queue.push(...errs);
154
+ }
155
+ return out;
156
+ }
157
+ function isErrorCodeLike(v) {
158
+ return /^[A-Z][A-Z0-9_]*$/.test(v);
159
+ }
160
+ function errorCode(err) {
161
+ for (const src of causeChain(err)) {
162
+ for (const k of ["code", "errno"]) {
163
+ const v = src[k];
164
+ if (typeof v === "string" && v) return v;
165
+ }
166
+ const name = src["name"];
167
+ if (typeof name === "string" && name !== "Error" && isErrorCodeLike(name)) {
168
+ return name;
169
+ }
170
+ }
171
+ return void 0;
172
+ }
173
+
106
174
  // src/engine/compaction.ts
107
175
  var DEFAULT_THRESHOLD_PCT = 0.8;
108
- var DEFAULT_CAP_TOKENS = 25e4;
109
176
  var DEFAULT_CONTEXT_WINDOW = 128e3;
110
177
  var MODEL_CONTEXT_WINDOWS = {
111
178
  // ==================== OpenAI ====================
112
- // 来源: OpenAI官方API文档
179
+ // 来源: OpenAI 官方 API 文档 (platform.openai.com/docs/models)
113
180
  "gpt-5.5": 105e4,
114
181
  "gpt-5.4": 105e4,
182
+ "gpt-4.1": 1047576,
183
+ "gpt-4.1-mini": 1047576,
184
+ "gpt-4.1-nano": 1047576,
115
185
  "gpt-4o": 128e3,
116
186
  "gpt-4o-mini": 128e3,
117
187
  "gpt-4-turbo": 128e3,
@@ -123,52 +193,86 @@ var MODEL_CONTEXT_WINDOWS = {
123
193
  "o1-mini": 128e3,
124
194
  o3: 2e5,
125
195
  "o3-mini": 2e5,
196
+ o4: 2e5,
197
+ "o4-mini": 2e5,
126
198
  // ==================== Anthropic ====================
127
- // 注: Claude系列模型上下文窗口多为200K,但Claude 4系列在特定平台(如Amazon Bedrock)可能支持更高
199
+ // 来源: Anthropic 官方文档 (docs.anthropic.com)
200
+ // 注: Claude 系列模型上下文窗口均为 200K
128
201
  "claude-3-opus": 2e5,
129
202
  "claude-3-sonnet": 2e5,
130
203
  "claude-3-haiku": 2e5,
131
204
  "claude-3.5-sonnet": 2e5,
205
+ "claude-3.5-haiku": 2e5,
132
206
  "claude-4-sonnet": 2e5,
133
- // 基础窗口,实际可能因部署平台而异
134
207
  "claude-4-opus": 2e5,
208
+ "claude-opus-4": 2e5,
209
+ "claude-sonnet-4": 2e5,
135
210
  // ==================== Google Gemini ====================
136
- // 来源: Google Colab及OpenRouter
137
- // 注: Gemini模型以超大上下文窗口著称,部分版本支持2M
211
+ // 来源: Google AI 官方文档 (ai.google.dev)
212
+ // 注: Gemini 模型以超大上下文窗口著称
213
+ "gemini-2.5-pro": 2e6,
214
+ "gemini-2.5-flash": 1e6,
215
+ "gemini-2.0-flash": 1048576,
138
216
  "gemini-3-pro": 2e6,
139
- // 最高支持2M
140
217
  "gemini-3-flash": 1e6,
141
- // 支持1M
142
218
  // ==================== Meta Llama ====================
143
- // 来源: NVIDIA NIM文档及Oracle Cloud文档
219
+ // 来源: NVIDIA NIM 文档及 Oracle Cloud 文档
144
220
  // 注意: 不同部署平台的上下文长度可能有差异
145
221
  "llama-4-scout": 128e3,
146
- // 实际部署中常见128K
147
222
  "llama-4-maverick": 512e3,
148
- // 支持512K
149
223
  "llama-3.3": 128e3,
150
224
  "llama-3.1": 128e3,
225
+ "llama-3": 128e3,
151
226
  // ==================== DeepSeek ====================
152
- // 来源: 基于DeepSeek-V4系列公开信息
153
- // 注: DeepSeek-V4系列两款模型均支持100万(1M)上下文窗口
227
+ // 来源: DeepSeek 官方文档 (api-docs.deepseek.com)
228
+ // 注: deepseek-flash 当前由 DeepSeek-V4.1-Flash 提供服务,上下文 1M
229
+ "deepseek-flash": 1e6,
230
+ "deepseek-v4.1-flash": 1e6,
154
231
  "deepseek-v4-pro": 1e6,
155
232
  "deepseek-v4-flash": 1e6,
156
233
  "deepseek-v3": 128e3,
157
234
  "deepseek-r1": 128e3,
158
- // ==================== Alibaba Qwen ====================
159
- // 来源: Replicate平台模型卡片
160
- // 注: Qwen 3.5/3.6系列原生支持262K,可扩展至1M
235
+ "deepseek-reasoner": 128e3,
236
+ // ==================== Alibaba Qwen (通义千问) ====================
237
+ // 来源: 阿里云百炼文档 (help.aliyun.com/zh/model-studio)
238
+ // 注: Qwen3.8 系列支持 1M,Qwen3.5/3.6 原生 262K
239
+ "qwen3.8-max": 1e6,
240
+ "qwen3.8-flash": 1e6,
241
+ "qwen3.7-max": 1e6,
242
+ "qwen3.7-plus": 1e6,
243
+ "qwen3.6": 262144,
244
+ "qwen3.5": 262144,
161
245
  "qwen-3.5": 262144,
162
- // 原生262K,可扩展至1M
163
246
  "qwen-3.6": 262144,
164
- // 原生262K,可扩展至1M
247
+ "qwen-max": 128e3,
248
+ "qwen-plus": 128e3,
249
+ "qwen-turbo": 128e3,
250
+ // ==================== 智谱 GLM ====================
251
+ // 来源: 智谱 AI 开放文档 (docs.bigmodel.cn)
252
+ "glm-5.3": 128e3,
253
+ "glm-5.2": 128e3,
254
+ "glm-4-plus": 128e3,
255
+ "glm-4-flash": 128e3,
256
+ "glm-4": 128e3,
257
+ // ==================== Kimi / Moonshot ====================
258
+ // 来源: Moonshot API 文档 (platform.moonshot.cn/docs)
259
+ "kimi-k3": 256e3,
260
+ "kimi-k2.7": 256e3,
261
+ "kimi-k2.6": 256e3,
262
+ "kimi-k2": 256e3,
263
+ "moonshot-v1-128k": 128e3,
264
+ "moonshot-v1-32k": 32e3,
265
+ "moonshot-v1-8k": 8e3,
266
+ // ==================== 小米 MiMo ====================
267
+ "mimo-v2.5-pro": 128e3,
165
268
  // ==================== Mistral ====================
166
- // 来源: AWS Bedrock文档
269
+ // 来源: Mistral AI 官方文档及 AWS Bedrock 文档
167
270
  "mistral-large-3": 256e3,
168
- // ==================== Others ====================
169
- // 来源: Kimi官方博客及智谱AI开放文档
170
- "kimi-k2": 256e3,
171
- "glm-4-plus": 128e3
271
+ "mistral-large": 128e3,
272
+ "mistral-small": 128e3,
273
+ "mixtral-8x22b": 65536,
274
+ // ==================== MiniMax ====================
275
+ "minimax-m3": 1e6
172
276
  };
173
277
  function lookupContextWindow(modelId) {
174
278
  const lower = modelId.toLowerCase();
@@ -195,9 +299,13 @@ function estimateTokens(messages) {
195
299
  }
196
300
  return Math.floor(total / 4);
197
301
  }
198
- function triggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capTokens = DEFAULT_CAP_TOKENS) {
302
+ function triggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capTokens) {
199
303
  const window = contextWindow ?? DEFAULT_CONTEXT_WINDOW;
200
- return Math.min(Math.floor(thresholdPct * window), capTokens);
304
+ const base = Math.floor(thresholdPct * window);
305
+ if (capTokens != null && Number.isFinite(capTokens) && capTokens > 0) {
306
+ return Math.min(base, Math.floor(capTokens));
307
+ }
308
+ return base;
201
309
  }
202
310
  function shouldCompact(signal, contextWindow, thresholdPct, capTokens) {
203
311
  return signal >= triggerTokens(contextWindow, thresholdPct, capTokens);
@@ -553,12 +661,21 @@ var EXPLORER_INSTRUCTIONS = `\u4F60\u662F\u4E00\u4E2A\u53EA\u8BFB\u7684\u4EE3\u7
553
661
  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
554
662
  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
555
663
  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`;
556
- 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`;
664
+ 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
665
+
666
+ \u8981\u6C42\uFF1A
667
+ 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
668
+ 2. \u540D\u79F0\u4E0D\u8D85\u8FC7 20 \u4E2A\u5B57\u7B26\u3002`;
557
669
 
558
670
  // src/engine/TurnEngine.ts
559
671
  function genReqId() {
560
672
  return crypto.randomBytes(6).toString("base64url");
561
673
  }
674
+ var INTERACTIVE_TOOLS = /* @__PURE__ */ new Set([
675
+ "ask_user",
676
+ "propose_plan",
677
+ "request_directory"
678
+ ]);
562
679
  function toolResultMessage(tc, result, taskId) {
563
680
  const content = typeof result === "string" ? result : JSON.stringify(result, null, 2);
564
681
  return {
@@ -605,6 +722,23 @@ function preview(value, maxLen = 200) {
605
722
  const flat = text.replace(/\n/g, "\\n");
606
723
  return flat.length <= maxLen ? flat : flat.slice(0, maxLen - 3) + "...";
607
724
  }
725
+ function isTransientNetworkError(exc) {
726
+ const status = exc?.status;
727
+ if (typeof status === "number") {
728
+ return false;
729
+ }
730
+ const msg = `${exc instanceof Error ? exc.message : String(exc)} ${errorCode(exc) ?? ""}`;
731
+ return isNetworkErrorText(msg);
732
+ }
733
+ function networkRetryMax() {
734
+ const n = Number(process.env.NETWORK_RETRY_MAX);
735
+ return Number.isFinite(n) && n >= 0 ? n : 2;
736
+ }
737
+ function networkRetryBaseMs() {
738
+ const n = Number(process.env.NETWORK_RETRY_BASE_MS);
739
+ return Number.isFinite(n) && n >= 0 ? n : 800;
740
+ }
741
+ var sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
608
742
  function isRetriableError(exc) {
609
743
  const status = exc?.status;
610
744
  if (typeof status === "number") {
@@ -615,6 +749,7 @@ function isRetriableError(exc) {
615
749
  (exc instanceof Error ? exc.message : exc) ?? ""
616
750
  ).toLowerCase();
617
751
  const markers = [
752
+ "connection error",
618
753
  "fetch failed",
619
754
  "econnreset",
620
755
  "etimedout",
@@ -647,6 +782,12 @@ var TurnEngine = class _TurnEngine {
647
782
  _hook;
648
783
  // 中断标记:requestInterrupt() 置位后,引擎在下一个安全点停止
649
784
  _cancel = false;
785
+ // 在飞的 LLM 流式中止句柄:requestInterrupt() 直接 abort,
786
+ // 否则流静默挂死时引擎永远等不到检查标志位的安全点
787
+ _llmAbort = null;
788
+ // 在一轮工具执行期间生效的中止句柄:requestInterrupt() 一并 abort,
789
+ // 使正在执行的可中断工具(run_shell)立即终止子进程,而不是等它自然结束
790
+ _toolAbort = null;
650
791
  // Steering 消息队列:外部排队注入的用户级指令,迭代间隙注入
651
792
  _steering = [];
652
793
  // 上下文压缩
@@ -654,6 +795,10 @@ var TurnEngine = class _TurnEngine {
654
795
  _lastContextTokens = null;
655
796
  /** 显式配置的上下文窗口大小(token 数),未设置时为 undefined(走自动检测) */
656
797
  _contextWindow;
798
+ /** 压缩触发阈值比例(默认 0.8) */
799
+ _thresholdPct;
800
+ /** 压缩触发阈值绝对上限(null = 不限制) */
801
+ _capTokens;
657
802
  // 产出文件追踪(本轮 Turn 中成功写入的文件)
658
803
  _producedFiles = [];
659
804
  _turnStartTime = 0;
@@ -700,18 +845,25 @@ var TurnEngine = class _TurnEngine {
700
845
  this.model = opts.model;
701
846
  this.instructions = opts.instructions;
702
847
  this.genSessionNamePrompt = opts.genSessionNamePrompt || GEN_SESSION_NAME_INSTRUCTIONS;
703
- this.maxIterations = opts.maxIterations ?? 150;
848
+ this.maxIterations = opts.maxIterations ?? 300;
704
849
  this.messages = opts.messages ? [...opts.messages] : [];
705
850
  this._hook = opts.hook;
706
851
  this._contextWindow = opts.contextWindow;
852
+ this._thresholdPct = opts.thresholdPct ?? DEFAULT_THRESHOLD_PCT;
853
+ this._capTokens = opts.capTokens ?? null;
707
854
  this._reasoningEffort = opts.reasoningEffort;
708
855
  if (this.messages.length === 0 || this.messages[0].role !== "system") {
709
856
  this.messages.unshift({ role: "system", content: this.instructions });
710
857
  }
711
858
  }
712
- /** 外部中断:置位取消标记,引擎会在安全点停止 */
859
+ /**
860
+ * 外部中断:置位取消标记 + 掐断在飞的 LLM 请求 + 中止正在执行的工具,
861
+ * 引擎随后按中断收尾
862
+ */
713
863
  requestInterrupt() {
714
864
  this._cancel = true;
865
+ this._llmAbort?.abort();
866
+ this._toolAbort?.abort();
715
867
  }
716
868
  /** 设置审批回调 */
717
869
  setApprover(approver) {
@@ -813,7 +965,10 @@ var TurnEngine = class _TurnEngine {
813
965
  {
814
966
  reqId: `llm_chat_${Date.now()}_${genReqId()}`,
815
967
  logTime: (/* @__PURE__ */ new Date()).toISOString()
816
- }
968
+ },
969
+ // 不设 maxTokens:推理模型(deepseek 等)的思考会占用输出配额,
970
+ // 过小会导致 content 为空从而拿不到名称;命名 prompt 已约束只输出短标题。
971
+ {}
817
972
  );
818
973
  return name;
819
974
  }
@@ -844,6 +999,47 @@ var TurnEngine = class _TurnEngine {
844
999
  return [];
845
1000
  }
846
1001
  // ===================== 核心循环 =====================
1002
+ /**
1003
+ * 中断收尾:保留已流式输出的内容、记一条 interrupted 提示,
1004
+ * 产出 INTERRUPTED + TURN_END 事件并结束本轮。
1005
+ *
1006
+ * 两处调用:流式结束后发现取消标记、以及流被 abort 中断抛错时。
1007
+ */
1008
+ *_finishInterrupted(args) {
1009
+ const { iterations, reqId, streamed, streamedThinking } = args;
1010
+ if (streamed) {
1011
+ this.messages.push(
1012
+ assistantMessage(
1013
+ {
1014
+ text: streamed,
1015
+ toolCalls: [],
1016
+ finishReason: null,
1017
+ reasoning: streamedThinking || null,
1018
+ usage: null
1019
+ },
1020
+ this.model,
1021
+ this.taskId
1022
+ )
1023
+ );
1024
+ }
1025
+ this._appendNotice("interrupted");
1026
+ this._emitHook((h) => h.onInterrupted?.(iterations));
1027
+ yield {
1028
+ type: "interrupted" /* INTERRUPTED */,
1029
+ data: { iterations, reqId, taskId: this.taskId }
1030
+ };
1031
+ const files = this._buildProducedFiles();
1032
+ yield {
1033
+ type: "turn_end" /* TURN_END */,
1034
+ data: {
1035
+ status: "interrupted",
1036
+ iterations,
1037
+ produced_files: files,
1038
+ taskId: this.taskId
1039
+ }
1040
+ };
1041
+ this._emitHook((h) => h.onTurnEnd?.("interrupted", iterations, files));
1042
+ }
847
1043
  /**
848
1044
  * 核心迭代循环
849
1045
  *
@@ -853,6 +1049,8 @@ var TurnEngine = class _TurnEngine {
853
1049
  */
854
1050
  async *_loop() {
855
1051
  let iterations = 0;
1052
+ let netRetries = 0;
1053
+ const netMax = networkRetryMax();
856
1054
  while (true) {
857
1055
  if (iterations >= this.maxIterations) {
858
1056
  const files = this._buildProducedFiles();
@@ -884,23 +1082,26 @@ var TurnEngine = class _TurnEngine {
884
1082
  };
885
1083
  this._emitHook((h) => h.onEvent?.("llm_request" /* LLM_REQUEST */, llmReqData));
886
1084
  yield { type: "llm_request" /* LLM_REQUEST */, data: llmReqData };
887
- this._emitHook((h) => h.onCompacting?.());
888
- yield { type: "compacting" /* COMPACTING */, data: { taskId: this.taskId } };
1085
+ const startInfo = this._compactionStartInfo("threshold");
1086
+ this._emitHook((h) => h.onCompacting?.(startInfo));
1087
+ yield {
1088
+ type: "compacting" /* COMPACTING */,
1089
+ data: { taskId: this.taskId, ...startInfo }
1090
+ };
889
1091
  const result = await this._compactNow(compactReqId);
890
1092
  if (result.notice) {
891
1093
  compactionNotice = result.notice;
892
1094
  this._appendNotice("compacted", result.notice);
893
- this._emitHook(
894
- (h) => h.onCompacted?.(
895
- this.compactionState ? "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" : "\u4E0A\u4E0B\u6587\u5DF2\u88C1\u526A"
896
- )
897
- );
1095
+ const doneInfo = this._compactionDoneInfo(result);
1096
+ this._emitHook((h) => h.onCompacted?.(result.notice, doneInfo));
898
1097
  yield {
899
1098
  type: "compacted" /* COMPACTED */,
900
1099
  data: {
901
1100
  reqId: compactReqId,
902
1101
  taskId: this.taskId,
903
1102
  method: result.method,
1103
+ messagesBefore: doneInfo.messagesBefore,
1104
+ messagesAfter: doneInfo.messagesAfter,
904
1105
  ...result.boundaryIndex !== void 0 ? { boundaryIndex: result.boundaryIndex } : {},
905
1106
  ...result.modelUsed ? { model: result.modelUsed } : {},
906
1107
  ...result.summaryPreview ? { summaryPreview: result.summaryPreview } : {},
@@ -928,11 +1129,18 @@ var TurnEngine = class _TurnEngine {
928
1129
  };
929
1130
  this._emitHook((h) => h.onEvent?.("llm_request" /* LLM_REQUEST */, llmReqData));
930
1131
  yield { type: "llm_request" /* LLM_REQUEST */, data: llmReqData };
1132
+ this._llmAbort = new AbortController();
1133
+ if (this._cancel) this._llmAbort.abort();
931
1134
  const chunks = await this.provider.stream(
932
1135
  this.model,
933
1136
  outbound,
934
1137
  schemas,
935
- { reqId, logTime, reasoningEffort: this._reasoningEffort }
1138
+ {
1139
+ reqId,
1140
+ logTime,
1141
+ reasoningEffort: this._reasoningEffort,
1142
+ signal: this._llmAbort.signal
1143
+ }
936
1144
  );
937
1145
  for await (const chunk of chunks) {
938
1146
  if (this._cancel) break;
@@ -965,6 +1173,15 @@ var TurnEngine = class _TurnEngine {
965
1173
  }
966
1174
  }
967
1175
  } catch (exc) {
1176
+ if (this._cancel && turn === null) {
1177
+ yield* this._finishInterrupted({
1178
+ iterations,
1179
+ reqId,
1180
+ streamed,
1181
+ streamedThinking
1182
+ });
1183
+ return;
1184
+ }
968
1185
  const overflow = isContextOverflow(exc);
969
1186
  if (overflow && !this._cancel) {
970
1187
  const overflowReqId = `compact_${Date.now()}_${genReqId()}`;
@@ -978,22 +1195,31 @@ var TurnEngine = class _TurnEngine {
978
1195
  };
979
1196
  this._emitHook((h) => h.onEvent?.("llm_request" /* LLM_REQUEST */, llmReqData));
980
1197
  yield { type: "llm_request" /* LLM_REQUEST */, data: llmReqData };
981
- this._emitHook((h) => h.onCompacting?.());
982
- yield { type: "compacting" /* COMPACTING */, data: { taskId: this.taskId } };
1198
+ const startInfo = this._compactionStartInfo("overflow");
1199
+ this._emitHook((h) => h.onCompacting?.(startInfo));
1200
+ yield {
1201
+ type: "compacting" /* COMPACTING */,
1202
+ data: { taskId: this.taskId, ...startInfo }
1203
+ };
983
1204
  const result = await this._compactNow(overflowReqId, true);
984
1205
  if (result.notice) {
985
1206
  const notice = result.notice;
986
1207
  this._appendNotice("compacted", notice);
987
- this._emitHook((h) => h.onCompacted?.(notice));
1208
+ const doneInfo = this._compactionDoneInfo(result);
1209
+ this._emitHook((h) => h.onCompacted?.(notice, doneInfo));
988
1210
  yield {
989
1211
  type: "compacted" /* COMPACTED */,
990
1212
  data: {
991
1213
  reqId: overflowReqId,
992
1214
  taskId: this.taskId,
993
1215
  method: result.method,
1216
+ messagesBefore: doneInfo.messagesBefore,
1217
+ messagesAfter: doneInfo.messagesAfter,
994
1218
  ...result.boundaryIndex !== void 0 ? { boundaryIndex: result.boundaryIndex } : {},
995
1219
  ...result.modelUsed ? { model: result.modelUsed } : {},
996
- ...result.summaryPreview ? { summaryPreview: result.summaryPreview } : {}
1220
+ ...result.summaryPreview ? { summaryPreview: result.summaryPreview } : {},
1221
+ ...result.keptUserMessages !== void 0 ? { keptUserMessages: result.keptUserMessages } : {},
1222
+ ...result.droppedUserMessages !== void 0 ? { droppedUserMessages: result.droppedUserMessages } : {}
997
1223
  }
998
1224
  };
999
1225
  if (streamed) {
@@ -1014,6 +1240,20 @@ var TurnEngine = class _TurnEngine {
1014
1240
  continue;
1015
1241
  }
1016
1242
  }
1243
+ if (!this._cancel && !streamed && isTransientNetworkError(exc) && netRetries < netMax) {
1244
+ netRetries++;
1245
+ const reason = `${exc instanceof Error ? exc.message : String(exc)}`.slice(0, 200);
1246
+ const waitMs = networkRetryBaseMs() * 2 ** (netRetries - 1);
1247
+ this._appendNotice("retry", `\u7F51\u7EDC\u4E2D\u65AD\uFF0C\u6B63\u5728\u91CD\u8BD5\uFF08${netRetries}/${netMax}\uFF09\uFF1A${reason}`);
1248
+ this._emitHook((h) => h.onRetry?.(netRetries, netMax, reason));
1249
+ yield {
1250
+ type: "llm_retry" /* LLM_RETRY */,
1251
+ data: { attempt: netRetries, max: netMax, reason, waitMs, iteration: iterations, reqId }
1252
+ };
1253
+ iterations--;
1254
+ await sleepMs(waitMs);
1255
+ continue;
1256
+ }
1017
1257
  const errMsg = exc instanceof Error ? exc.message : String(exc);
1018
1258
  const errorType = exc instanceof Error ? exc.constructor.name : "unknown";
1019
1259
  const errorStack = exc instanceof Error ? exc.stack : void 0;
@@ -1068,39 +1308,14 @@ var TurnEngine = class _TurnEngine {
1068
1308
  this._emitHook((h) => h.onTurnEnd?.("error", iterations, files));
1069
1309
  return;
1070
1310
  }
1311
+ netRetries = 0;
1071
1312
  if (this._cancel && turn === null) {
1072
- if (streamed) {
1073
- this.messages.push(
1074
- assistantMessage(
1075
- {
1076
- text: streamed,
1077
- toolCalls: [],
1078
- finishReason: null,
1079
- reasoning: streamedThinking || null,
1080
- usage: null
1081
- },
1082
- this.model,
1083
- this.taskId
1084
- )
1085
- );
1086
- }
1087
- this._appendNotice("interrupted");
1088
- this._emitHook((h) => h.onInterrupted?.(iterations));
1089
- yield {
1090
- type: "interrupted" /* INTERRUPTED */,
1091
- data: { iterations, reqId, taskId: this.taskId }
1092
- };
1093
- const files = this._buildProducedFiles();
1094
- yield {
1095
- type: "turn_end" /* TURN_END */,
1096
- data: {
1097
- status: "interrupted",
1098
- iterations,
1099
- produced_files: files,
1100
- taskId: this.taskId
1101
- }
1102
- };
1103
- this._emitHook((h) => h.onTurnEnd?.("interrupted", iterations, files));
1313
+ yield* this._finishInterrupted({
1314
+ iterations,
1315
+ reqId,
1316
+ streamed,
1317
+ streamedThinking
1318
+ });
1104
1319
  return;
1105
1320
  }
1106
1321
  if (turn === null) {
@@ -1219,9 +1434,14 @@ var TurnEngine = class _TurnEngine {
1219
1434
  * PERMISSION_REQUIRED 事件并调用 approver;被拒/未知工具
1220
1435
  * 直接产出错误结果,不进入执行。
1221
1436
  * B. 分类执行:低风险工具并发执行(Promise.all),其余串行;
1437
+ * 交互类工具(ask_user / propose_plan / request_directory)虽然
1438
+ * 风险级别为 low,但需要用户逐条作答,一律串行;
1222
1439
  * 每个结果都转成 tool 消息追加到历史并产出 TOOL_FINISHED。
1223
1440
  */
1224
1441
  async *_handleToolCalls(toolCalls) {
1442
+ const abort = new AbortController();
1443
+ this._toolAbort = abort;
1444
+ if (this._cancel) abort.abort();
1225
1445
  const cleared = [];
1226
1446
  for (const tc of toolCalls) {
1227
1447
  tc.id = tc.id || `tool_${Date.now()}_${genReqId()}`;
@@ -1367,7 +1587,7 @@ var TurnEngine = class _TurnEngine {
1367
1587
  }
1368
1588
  const concurrent = cleared.filter((tc) => {
1369
1589
  const spec = this.registry.get(tc.name);
1370
- return spec?.metadata.riskLevel === "low" && !spec.metadata.requiresApproval;
1590
+ return spec?.metadata.riskLevel === "low" && !spec.metadata.requiresApproval && !INTERACTIVE_TOOLS.has(tc.name);
1371
1591
  });
1372
1592
  const serial = cleared.filter((tc) => !concurrent.includes(tc));
1373
1593
  if (concurrent.length > 0) {
@@ -1383,7 +1603,8 @@ var TurnEngine = class _TurnEngine {
1383
1603
  try {
1384
1604
  let result = this.registry.execute(
1385
1605
  tc.name,
1386
- tc.arguments
1606
+ tc.arguments,
1607
+ { signal: abort.signal }
1387
1608
  );
1388
1609
  if (result instanceof Promise) result = await result;
1389
1610
  return { result, status: "ok" };
@@ -1452,7 +1673,8 @@ var TurnEngine = class _TurnEngine {
1452
1673
  try {
1453
1674
  result = this.registry.execute(
1454
1675
  tc.name,
1455
- tc.arguments
1676
+ tc.arguments,
1677
+ { signal: abort.signal }
1456
1678
  );
1457
1679
  if (result instanceof Promise) result = await result;
1458
1680
  status = "ok";
@@ -1489,6 +1711,7 @@ var TurnEngine = class _TurnEngine {
1489
1711
  }
1490
1712
  };
1491
1713
  }
1714
+ if (this._toolAbort === abort) this._toolAbort = null;
1492
1715
  }
1493
1716
  // ===================== 辅助方法 =====================
1494
1717
  /**
@@ -1562,10 +1785,54 @@ var TurnEngine = class _TurnEngine {
1562
1785
  return false;
1563
1786
  }
1564
1787
  // ===================== 自动压缩 =====================
1788
+ /** 当前上下文 token 用量:优先采信 API 返回的精确数量,否则用 chars/4 估算 */
1789
+ _compactionTokens() {
1790
+ if (this._lastContextTokens != null) {
1791
+ return { tokens: this._lastContextTokens, estimated: false };
1792
+ }
1793
+ return {
1794
+ tokens: estimateTokens(this._outboundMessages()),
1795
+ estimated: true
1796
+ };
1797
+ }
1565
1798
  /** 检查是否需要压缩(每轮 LLM 调用前) */
1566
1799
  _compactionDue() {
1567
- const signal = this._lastContextTokens ?? estimateTokens(this._outboundMessages());
1568
- return shouldCompact(signal, this._getContextWindow());
1800
+ const { tokens } = this._compactionTokens();
1801
+ return shouldCompact(
1802
+ tokens,
1803
+ this._getContextWindow(),
1804
+ this._thresholdPct,
1805
+ this._capTokens
1806
+ );
1807
+ }
1808
+ /** 组装压缩开始时的用量信息(供 hook 与 COMPACTING 事件携带) */
1809
+ _compactionStartInfo(trigger) {
1810
+ const { tokens, estimated } = this._compactionTokens();
1811
+ const contextWindow = this._getContextWindow();
1812
+ return {
1813
+ trigger,
1814
+ tokens,
1815
+ estimated,
1816
+ contextWindow,
1817
+ thresholdPct: this._thresholdPct,
1818
+ thresholdTokens: triggerTokens(
1819
+ contextWindow,
1820
+ this._thresholdPct,
1821
+ this._capTokens
1822
+ ),
1823
+ outboundMessages: this._outboundMessages().length
1824
+ };
1825
+ }
1826
+ /** 组装压缩结果信息(供 hook 与 COMPACTED 事件携带) */
1827
+ _compactionDoneInfo(result) {
1828
+ return {
1829
+ method: result.method === "trimmed" ? "trimmed" : "summarized",
1830
+ messagesBefore: result.messagesBefore ?? 0,
1831
+ messagesAfter: result.messagesAfter ?? 0,
1832
+ ...result.modelUsed ? { model: result.modelUsed } : {},
1833
+ ...result.keptUserMessages !== void 0 ? { keptUserMessages: result.keptUserMessages } : {},
1834
+ ...result.droppedUserMessages !== void 0 ? { droppedUserMessages: result.droppedUserMessages } : {}
1835
+ };
1569
1836
  }
1570
1837
  /** 获取当前模型的上下文窗口大小(token 数)。优先级:显式配置 > 自动检测 > 默认值 128K。 */
1571
1838
  _getContextWindow() {
@@ -1575,8 +1842,13 @@ var TurnEngine = class _TurnEngine {
1575
1842
  /** 执行压缩策略(对齐 Python 版 _compact_now) */
1576
1843
  async _compactNow(reqId, force = false) {
1577
1844
  const contextWindow = this._getContextWindow();
1845
+ const messagesBefore = this._outboundMessages().length;
1578
1846
  const keep = Math.floor(
1579
- KEEP_RECENT_FRACTION * triggerTokens(contextWindow)
1847
+ KEEP_RECENT_FRACTION * triggerTokens(
1848
+ contextWindow,
1849
+ this._thresholdPct,
1850
+ this._capTokens
1851
+ )
1580
1852
  );
1581
1853
  const model = this.model;
1582
1854
  let state = null;
@@ -1608,7 +1880,9 @@ var TurnEngine = class _TurnEngine {
1608
1880
  boundaryIndex: state.boundaryIndex,
1609
1881
  summaryPreview: state.summaryText.slice(0, 200),
1610
1882
  keptUserMessages: state.userMessages.length,
1611
- droppedUserMessages: state.userMessagesDropped
1883
+ droppedUserMessages: state.userMessagesDropped,
1884
+ messagesBefore,
1885
+ messagesAfter: this._outboundMessages().length
1612
1886
  };
1613
1887
  }
1614
1888
  if (failed || force) {
@@ -1619,7 +1893,9 @@ var TurnEngine = class _TurnEngine {
1619
1893
  return {
1620
1894
  notice: "\u4E0A\u4E0B\u6587\u5DF2\u88C1\u526A \u2014 \u6700\u65E7\u7684\u8F6E\u6B21\u5DF2\u4E22\u5F03\uFF08\u6458\u8981\u4E0D\u53EF\u7528\uFF09",
1621
1895
  method: "trimmed",
1622
- boundaryIndex: trimmed.boundaryIndex
1896
+ boundaryIndex: trimmed.boundaryIndex,
1897
+ messagesBefore,
1898
+ messagesAfter: this._outboundMessages().length
1623
1899
  };
1624
1900
  }
1625
1901
  }
@@ -1633,9 +1909,9 @@ var TurnEngine = class _TurnEngine {
1633
1909
  return;
1634
1910
  }
1635
1911
  if (result && typeof result === "object") {
1636
- const path4 = result.path;
1637
- if (path4) {
1638
- this._producedFiles.push(path4);
1912
+ const path8 = result.path;
1913
+ if (path8) {
1914
+ this._producedFiles.push(path8);
1639
1915
  return;
1640
1916
  }
1641
1917
  const changed = result.changed_files;
@@ -1717,20 +1993,36 @@ var TurnEngine = class _TurnEngine {
1717
1993
  }
1718
1994
  };
1719
1995
 
1996
+ // src/engine/limits.ts
1997
+ function parseMaxIterations(value, fallback) {
1998
+ if (value === void 0 || value === null || value.trim() === "") {
1999
+ return fallback;
2000
+ }
2001
+ const n = Number(value);
2002
+ if (!Number.isFinite(n) || n <= 0) return fallback;
2003
+ return Math.floor(n);
2004
+ }
2005
+
1720
2006
  // src/tools/pathSafety.ts
1721
2007
  import * as fs2 from "fs";
1722
2008
  import * as path from "path";
1723
- function isWithinRoot(workspace, target) {
2009
+ function isInsideRelative(rel) {
2010
+ return !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`);
2011
+ }
2012
+ function isFilesystemRoot(p) {
2013
+ return path.parse(p).root === p;
2014
+ }
2015
+ function resolveWithinRoot(workspace, target) {
2016
+ const abs = path.resolve(workspace, target);
1724
2017
  let rootReal;
1725
2018
  try {
1726
2019
  rootReal = fs2.realpathSync(workspace);
1727
2020
  } catch {
1728
2021
  const rootAbs = path.resolve(workspace);
1729
- const abs2 = path.resolve(workspace, target);
1730
- const rel2 = path.relative(rootAbs, abs2);
1731
- return !rel2.startsWith("..") && !path.isAbsolute(rel2);
2022
+ if (!isInsideRelative(path.relative(rootAbs, abs))) return null;
2023
+ return { root: rootAbs, real: abs };
1732
2024
  }
1733
- const abs = path.resolve(workspace, target);
2025
+ if (isFilesystemRoot(rootReal)) return null;
1734
2026
  let real;
1735
2027
  try {
1736
2028
  real = fs2.realpathSync(abs);
@@ -1744,26 +2036,36 @@ function isWithinRoot(workspace, target) {
1744
2036
  real = path.join(fs2.realpathSync(probe), ...tail);
1745
2037
  break;
1746
2038
  } catch {
1747
- return false;
2039
+ return null;
1748
2040
  }
1749
2041
  }
1750
2042
  tail.unshift(path.basename(probe));
1751
2043
  const parent = path.dirname(probe);
1752
- if (parent === probe) return false;
2044
+ if (parent === probe) return null;
1753
2045
  probe = parent;
1754
2046
  }
1755
2047
  }
1756
- const rel = path.relative(rootReal, real);
1757
- return !rel.startsWith("..") && !path.isAbsolute(rel);
2048
+ if (!isInsideRelative(path.relative(rootReal, real))) return null;
2049
+ return { root: rootReal, real };
2050
+ }
2051
+ function isWithinRoot(workspace, target) {
2052
+ return resolveWithinRoot(workspace, target) !== null;
1758
2053
  }
1759
2054
 
1760
2055
  // src/permissions/PermissionEngine.ts
1761
2056
  import * as path2 from "path";
1762
2057
  var READ_ONLY_MODES = /* @__PURE__ */ new Set(["discuss" /* DISCUSS */, "plan" /* PLAN */]);
1763
- var WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "replace_in_file", "apply_patch", "apply_unified_diff"]);
2058
+ var WRITE_TOOLS = /* @__PURE__ */ new Set([
2059
+ "write_file",
2060
+ "replace_in_file",
2061
+ "apply_patch",
2062
+ "apply_unified_diff"
2063
+ ]);
1764
2064
  var SHELL_TOOL = "run_shell";
1765
2065
  var BASE_RISK = {
1766
- ...Object.fromEntries([...WRITE_TOOLS].map((n) => [n, "write_local" /* WRITE_LOCAL */])),
2066
+ ...Object.fromEntries(
2067
+ [...WRITE_TOOLS].map((n) => [n, "write_local" /* WRITE_LOCAL */])
2068
+ ),
1767
2069
  [SHELL_TOOL]: "exec" /* EXEC */
1768
2070
  };
1769
2071
  function classify(toolName, metadata) {
@@ -1781,6 +2083,14 @@ var SHELL_OPERATORS = [";", "&", "|", ">", "<", "`", "$(", "(", "\n", "\r"];
1781
2083
  function hasShellOperators(command) {
1782
2084
  return SHELL_OPERATORS.some((op) => command.includes(op));
1783
2085
  }
2086
+ function commandBin(command) {
2087
+ const first = String(command ?? "").trim().split(/\s+/)[0] ?? "";
2088
+ return first.split("/").pop() ?? first;
2089
+ }
2090
+ function isBareCommand(command) {
2091
+ const first = String(command ?? "").trim().split(/\s+/)[0] ?? "";
2092
+ return first.length > 0 && !first.includes("/");
2093
+ }
1784
2094
  var PermissionEngine = class {
1785
2095
  /** 工作区根目录(写入路径必须位于其下) */
1786
2096
  workspaceRoot;
@@ -1811,12 +2121,20 @@ var PermissionEngine = class {
1811
2121
  const isShell = risk === "exec" /* EXEC */;
1812
2122
  const consequential = isConsequential(risk);
1813
2123
  if (READ_ONLY_MODES.has(this.mode) && consequential) {
1814
- return { allowed: false, reason: `${this.mode} \u6A21\u5F0F\u4E3A\u53EA\u8BFB`, needsUser: false };
2124
+ return {
2125
+ allowed: false,
2126
+ reason: `${this.mode} \u6A21\u5F0F\u4E3A\u53EA\u8BFB`,
2127
+ needsUser: false
2128
+ };
1815
2129
  }
1816
2130
  if (isWrite) {
1817
2131
  const filePath = args["path"];
1818
2132
  if (filePath && !this._underWritableRoot(filePath)) {
1819
- return { allowed: false, reason: `\u8DEF\u5F84\u4E0D\u5728\u53EF\u5199\u76EE\u5F55\u4E0B: ${filePath}`, needsUser: false };
2133
+ return {
2134
+ allowed: false,
2135
+ reason: `\u8DEF\u5F84\u4E0D\u5728\u53EF\u5199\u76EE\u5F55\u4E0B: ${filePath}`,
2136
+ needsUser: false
2137
+ };
1820
2138
  }
1821
2139
  }
1822
2140
  if (!consequential) {
@@ -1830,8 +2148,15 @@ var PermissionEngine = class {
1830
2148
  if (this._commandAllowed(command)) {
1831
2149
  return { allowed: true, reason: "\u547D\u4EE4\u5728\u767D\u540D\u5355\u4E2D", needsUser: false };
1832
2150
  }
1833
- if (command && this.sessionAllowCommands.has(command)) {
1834
- return { allowed: true, reason: "\u4F1A\u8BDD\u5DF2\u5141\u8BB8\u6B64\u547D\u4EE4", needsUser: false };
2151
+ if (!hasShellOperators(command) && isBareCommand(command)) {
2152
+ const bin = commandBin(command);
2153
+ if (bin && this.sessionAllowCommands.has(bin)) {
2154
+ return {
2155
+ allowed: true,
2156
+ reason: "\u4F1A\u8BDD\u5DF2\u5141\u8BB8\u6B64\u547D\u4EE4",
2157
+ needsUser: false
2158
+ };
2159
+ }
1835
2160
  }
1836
2161
  }
1837
2162
  if (this.sessionAllowTools.has(toolName)) {
@@ -1843,9 +2168,10 @@ var PermissionEngine = class {
1843
2168
  allowToolForSession(toolName) {
1844
2169
  this.sessionAllowTools.add(toolName);
1845
2170
  }
1846
- /** 将命令加入会话白名单 */
2171
+ /** 将命令加入会话白名单(按可执行程序名记录,同类命令后续直接放行) */
1847
2172
  allowCommandForSession(command) {
1848
- if (command) this.sessionAllowCommands.add(command);
2173
+ const bin = commandBin(command);
2174
+ if (bin) this.sessionAllowCommands.add(bin);
1849
2175
  }
1850
2176
  /**
1851
2177
  * 检查路径是否在可写根目录下
@@ -1879,10 +2205,11 @@ function makeReadFile(workspace) {
1879
2205
  const filePath = String(args.path ?? "");
1880
2206
  const startLine = Math.max(1, Number(args.start_line ?? 1));
1881
2207
  const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);
1882
- const target = path3.resolve(root, filePath);
1883
- if (!isWithinRoot(root, target)) {
2208
+ const resolved = resolveWithinRoot(root, filePath);
2209
+ if (!resolved) {
1884
2210
  return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
1885
2211
  }
2212
+ const target = resolved.real;
1886
2213
  if (!fs3.existsSync(target) || !fs3.statSync(target).isFile()) {
1887
2214
  return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
1888
2215
  }
@@ -1904,7 +2231,7 @@ function makeReadFile(workspace) {
1904
2231
  }
1905
2232
  const end = startLine + selected.length - 1;
1906
2233
  const result = {
1907
- path: path3.relative(root, target),
2234
+ path: path3.relative(resolved.root, target),
1908
2235
  start_line: startLine,
1909
2236
  end_line: end,
1910
2237
  total_lines: total,
@@ -1916,38 +2243,14 @@ function makeReadFile(workspace) {
1916
2243
  return result;
1917
2244
  };
1918
2245
  }
1919
- function makeWriteFile(workspace) {
1920
- const root = path3.resolve(workspace);
1921
- return function write_file(args) {
1922
- const filePath = String(args.path ?? "");
1923
- const content = String(args.content ?? "");
1924
- const overwrite = args.overwrite !== false;
1925
- const target = path3.resolve(root, filePath);
1926
- if (!isWithinRoot(root, target)) {
1927
- return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
1928
- }
1929
- if (fs3.existsSync(target) && fs3.statSync(target).isDirectory()) {
1930
- return { error: "\u8DEF\u5F84\u662F\u76EE\u5F55\uFF0C\u4E0D\u662F\u6587\u4EF6" };
1931
- }
1932
- if (fs3.existsSync(target) && !overwrite) {
1933
- return { error: "\u6587\u4EF6\u5DF2\u5B58\u5728 (overwrite=false)" };
1934
- }
1935
- try {
1936
- fs3.mkdirSync(path3.dirname(target), { recursive: true });
1937
- fs3.writeFileSync(target, content, "utf-8");
1938
- } catch (exc) {
1939
- return { error: `\u5199\u5165\u5931\u8D25: ${String(exc)}` };
1940
- }
1941
- return path3.relative(root, target);
1942
- };
1943
- }
1944
2246
  function makeListFiles(workspace) {
1945
2247
  const root = path3.resolve(workspace);
1946
2248
  return function list_files(args) {
1947
2249
  const dirPath = String(args.path ?? ".");
1948
2250
  const maxResults = Math.min(Number(args.max_results ?? 100), 100);
1949
- const target = path3.resolve(root, dirPath);
1950
- if (!isWithinRoot(root, target)) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
2251
+ const resolved = resolveWithinRoot(root, dirPath);
2252
+ if (!resolved) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
2253
+ const target = resolved.real;
1951
2254
  if (!fs3.existsSync(target) || !fs3.statSync(target).isDirectory()) {
1952
2255
  return { error: `\u4E0D\u662F\u76EE\u5F55: ${dirPath}` };
1953
2256
  }
@@ -1961,45 +2264,13 @@ function makeListFiles(workspace) {
1961
2264
  results.push(e.isDirectory() ? `${e.name}/` : e.name);
1962
2265
  }
1963
2266
  results.sort();
1964
- return { path: path3.relative(root, target), entries: results.slice(0, maxResults) };
2267
+ return { path: path3.relative(resolved.root, target), entries: results.slice(0, maxResults) };
1965
2268
  } catch (exc) {
1966
2269
  return { error: `\u5217\u51FA\u6587\u4EF6\u5931\u8D25: ${String(exc)}` };
1967
2270
  }
1968
2271
  };
1969
2272
  }
1970
- function makeReplaceInFile(workspace) {
1971
- const root = path3.resolve(workspace);
1972
- return function replace_in_file(args) {
1973
- const filePath = String(args.path ?? "");
1974
- const oldStr = String(args.old ?? "");
1975
- const newStr = String(args.new ?? "");
1976
- const expectedReplacements = Math.max(1, Number(args.expected_replacements ?? 1));
1977
- if (!oldStr) return { error: "old \u4E0D\u80FD\u4E3A\u7A7A" };
1978
- const target = path3.resolve(root, filePath);
1979
- if (!isWithinRoot(root, target)) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
1980
- if (!fs3.existsSync(target) || !fs3.statSync(target).isFile()) {
1981
- return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
1982
- }
1983
- try {
1984
- const content = fs3.readFileSync(target, "utf-8");
1985
- const count = content.split(oldStr).length - 1;
1986
- if (count !== expectedReplacements) {
1987
- return { error: `\u671F\u671B ${expectedReplacements} \u5904\u66FF\u6362\uFF0C\u5B9E\u9645\u627E\u5230 ${count} \u5904: ${filePath}` };
1988
- }
1989
- const updated = content.replaceAll(oldStr, newStr);
1990
- fs3.writeFileSync(target, updated, "utf-8");
1991
- return {
1992
- path: path3.relative(root, target),
1993
- replacements: count,
1994
- chars_before: content.length,
1995
- chars_after: updated.length
1996
- };
1997
- } catch (exc) {
1998
- return { error: `\u66FF\u6362\u5931\u8D25: ${String(exc)}` };
1999
- }
2000
- };
2001
- }
2002
- function registerFileTools(registry, workspace) {
2273
+ function registerReadOnlyFileTools(registry, workspace) {
2003
2274
  const readFile = makeReadFile(workspace);
2004
2275
  registry.register(
2005
2276
  (args) => readFile(args),
@@ -2021,49 +2292,6 @@ function registerFileTools(registry, workspace) {
2021
2292
  }
2022
2293
  }
2023
2294
  );
2024
- const writeFile = makeWriteFile(workspace);
2025
- registry.register(
2026
- (args) => writeFile(args),
2027
- { category: "filesystem", riskLevel: "medium", capabilities: ["write_file"], requiresApproval: true },
2028
- {
2029
- type: "function",
2030
- function: {
2031
- name: "write_file",
2032
- 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",
2033
- parameters: {
2034
- type: "object",
2035
- properties: {
2036
- path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84 (\u76F8\u5BF9\u4E8E\u5DE5\u4F5C\u533A)" },
2037
- content: { type: "string", description: "\u6587\u4EF6\u5185\u5BB9 (UTF-8)" },
2038
- overwrite: { type: "boolean", description: "\u662F\u5426\u8986\u76D6\u5DF2\u5B58\u5728\u7684\u6587\u4EF6 (\u9ED8\u8BA4 true)" }
2039
- },
2040
- required: ["path", "content"]
2041
- }
2042
- }
2043
- }
2044
- );
2045
- const replaceFile = makeReplaceInFile(workspace);
2046
- registry.register(
2047
- (args) => replaceFile(args),
2048
- { category: "filesystem", riskLevel: "medium", capabilities: ["edit_file"], requiresApproval: true },
2049
- {
2050
- type: "function",
2051
- function: {
2052
- name: "replace_in_file",
2053
- 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",
2054
- parameters: {
2055
- type: "object",
2056
- properties: {
2057
- path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84 (\u76F8\u5BF9\u4E8E\u5DE5\u4F5C\u533A)" },
2058
- old: { type: "string", description: "\u8981\u88AB\u66FF\u6362\u7684\u7CBE\u786E\u6587\u672C" },
2059
- new: { type: "string", description: "\u66FF\u6362\u540E\u7684\u65B0\u6587\u672C" },
2060
- expected_replacements: { type: "integer", description: "\u671F\u671B\u7684\u66FF\u6362\u6B21\u6570 (\u9ED8\u8BA4 1)" }
2061
- },
2062
- required: ["path", "old", "new"]
2063
- }
2064
- }
2065
- }
2066
- );
2067
2295
  const listFiles = makeListFiles(workspace);
2068
2296
  registry.register(
2069
2297
  (args) => listFiles(args),
@@ -2085,47 +2313,515 @@ function registerFileTools(registry, workspace) {
2085
2313
  );
2086
2314
  }
2087
2315
 
2316
+ // src/tools/execAsync.ts
2317
+ import { spawn } from "child_process";
2318
+
2319
+ // src/tools/killTree.ts
2320
+ import { spawnSync } from "child_process";
2321
+ function killTree(pid) {
2322
+ if (!pid) return;
2323
+ if (process.platform === "win32") {
2324
+ try {
2325
+ spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
2326
+ } catch {
2327
+ }
2328
+ return;
2329
+ }
2330
+ try {
2331
+ process.kill(-pid, "SIGKILL");
2332
+ } catch {
2333
+ try {
2334
+ process.kill(pid, "SIGKILL");
2335
+ } catch {
2336
+ }
2337
+ }
2338
+ }
2339
+
2340
+ // src/tools/execAsync.ts
2341
+ var liveChildren = /* @__PURE__ */ new Set();
2342
+ var exitCleanupInstalled = false;
2343
+ function trackChild(pid) {
2344
+ if (!pid) return;
2345
+ liveChildren.add(pid);
2346
+ if (exitCleanupInstalled) return;
2347
+ exitCleanupInstalled = true;
2348
+ process.once("exit", () => {
2349
+ for (const p of liveChildren) killTree(p);
2350
+ });
2351
+ }
2352
+ var DEFAULT_MAX_BUFFER = 1024 * 1024 * 5;
2353
+ var KILL_GRACE_MS = 1e3;
2354
+ function makeSink(maxBuffer) {
2355
+ const chunks = [];
2356
+ let len = 0;
2357
+ let over = false;
2358
+ return {
2359
+ push(chunk) {
2360
+ if (len >= maxBuffer) {
2361
+ over = true;
2362
+ return;
2363
+ }
2364
+ const room = maxBuffer - len;
2365
+ if (chunk.length > room) {
2366
+ chunks.push(chunk.subarray(0, room));
2367
+ len = maxBuffer;
2368
+ over = true;
2369
+ return;
2370
+ }
2371
+ chunks.push(chunk);
2372
+ len += chunk.length;
2373
+ },
2374
+ done: () => Buffer.concat(chunks),
2375
+ over: () => over
2376
+ };
2377
+ }
2378
+ function execAsync(file, argv, opts = {}) {
2379
+ return new Promise((resolve7) => {
2380
+ const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
2381
+ const out = makeSink(maxBuffer);
2382
+ const err = makeSink(maxBuffer);
2383
+ if (opts.signal?.aborted) {
2384
+ resolve7({
2385
+ status: null,
2386
+ stdout: Buffer.alloc(0),
2387
+ stderr: Buffer.alloc(0),
2388
+ timedOut: false,
2389
+ truncated: false,
2390
+ aborted: true
2391
+ });
2392
+ return;
2393
+ }
2394
+ let child;
2395
+ try {
2396
+ child = spawn(file, argv, {
2397
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
2398
+ ...opts.env !== void 0 ? { env: opts.env } : {},
2399
+ // 自成进程组(POSIX):超时/中断时用 killTree 一次收掉整棵进程树
2400
+ detached: process.platform !== "win32",
2401
+ stdio: [opts.input === void 0 ? "ignore" : "pipe", "pipe", "pipe"],
2402
+ windowsHide: true
2403
+ });
2404
+ } catch (e) {
2405
+ resolve7({
2406
+ status: null,
2407
+ stdout: Buffer.alloc(0),
2408
+ stderr: Buffer.alloc(0),
2409
+ error: e,
2410
+ timedOut: false,
2411
+ truncated: false
2412
+ });
2413
+ return;
2414
+ }
2415
+ trackChild(child.pid);
2416
+ let settled = false;
2417
+ let timedOut = false;
2418
+ let aborted = false;
2419
+ let spawnError;
2420
+ let timer = null;
2421
+ let graceTimer = null;
2422
+ const onAbort = () => {
2423
+ aborted = true;
2424
+ killAndSettle();
2425
+ };
2426
+ const finish = (status) => {
2427
+ if (settled) return;
2428
+ settled = true;
2429
+ if (timer) clearTimeout(timer);
2430
+ if (graceTimer) clearTimeout(graceTimer);
2431
+ if (child.pid !== void 0) liveChildren.delete(child.pid);
2432
+ opts.signal?.removeEventListener("abort", onAbort);
2433
+ const result = {
2434
+ status,
2435
+ stdout: out.done(),
2436
+ stderr: err.done(),
2437
+ timedOut,
2438
+ truncated: out.over() || err.over()
2439
+ };
2440
+ if (aborted) result.aborted = true;
2441
+ if (spawnError) result.error = spawnError;
2442
+ resolve7(result);
2443
+ };
2444
+ const killAndSettle = () => {
2445
+ killTree(child.pid);
2446
+ if (settled || graceTimer) return;
2447
+ graceTimer = setTimeout(() => {
2448
+ child.stdout?.destroy();
2449
+ child.stderr?.destroy();
2450
+ finish(null);
2451
+ }, KILL_GRACE_MS);
2452
+ };
2453
+ if (opts.timeoutMs !== void 0 && opts.timeoutMs > 0) {
2454
+ timer = setTimeout(() => {
2455
+ timedOut = true;
2456
+ killAndSettle();
2457
+ }, opts.timeoutMs);
2458
+ }
2459
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
2460
+ child.stdout?.on("data", (c) => out.push(c));
2461
+ child.stderr?.on("data", (c) => err.push(c));
2462
+ child.on("error", (e) => {
2463
+ spawnError = e;
2464
+ if (child.pid === void 0) finish(null);
2465
+ });
2466
+ if (opts.input !== void 0 && child.stdin) {
2467
+ child.stdin.on("error", () => {
2468
+ });
2469
+ child.stdin.end(opts.input);
2470
+ }
2471
+ child.on("close", (code) => finish(code));
2472
+ });
2473
+ }
2474
+
2475
+ // src/tools/shellRuntime.ts
2476
+ import * as path4 from "path";
2477
+ var WINDOWS_SUFFIXES = [".exe", ".cmd", ".bat", ".com", ""];
2478
+ function pathApi(platform) {
2479
+ return platform === "win32" ? path4.win32 : path4.posix;
2480
+ }
2481
+ function joinPath(platform, ...parts) {
2482
+ return pathApi(platform).join(...parts);
2483
+ }
2484
+ function findExecutable(name, ctx) {
2485
+ if (name.includes("/") || name.includes("\\")) {
2486
+ return ctx.fileExists(name) ? name : null;
2487
+ }
2488
+ const suffixes = ctx.platform === "win32" ? WINDOWS_SUFFIXES : [""];
2489
+ const delimiter = ctx.platform === "win32" ? ";" : ":";
2490
+ const dirs = (ctx.env.PATH ?? "").split(delimiter).filter((d) => d.length > 0);
2491
+ for (const dir of dirs) {
2492
+ for (const suffix of suffixes) {
2493
+ const candidate = joinPath(ctx.platform, dir, name + suffix);
2494
+ if (ctx.fileExists(candidate)) return candidate;
2495
+ }
2496
+ }
2497
+ return null;
2498
+ }
2499
+
2500
+ // src/tools/shellBackground.ts
2501
+ import { spawn as spawn2 } from "child_process";
2502
+ var DEFAULT_BUFFER_LIMIT = 64 * 1024;
2503
+ var RingBuffer = class {
2504
+ constructor(limit = DEFAULT_BUFFER_LIMIT) {
2505
+ this.limit = limit;
2506
+ }
2507
+ limit;
2508
+ chunks = [];
2509
+ size = 0;
2510
+ add(chunk) {
2511
+ if (!chunk) return;
2512
+ this.chunks.push(chunk);
2513
+ this.size += chunk.length;
2514
+ while (this.size > this.limit && this.chunks.length > 1) {
2515
+ this.size -= this.chunks.shift().length;
2516
+ }
2517
+ if (this.size > this.limit) {
2518
+ const joined = this.chunks.join("");
2519
+ this.chunks = [joined.slice(-this.limit)];
2520
+ this.size = this.chunks[0].length;
2521
+ }
2522
+ }
2523
+ get() {
2524
+ const joined = this.chunks.join("");
2525
+ return joined.length > this.limit ? joined.slice(-this.limit) : joined;
2526
+ }
2527
+ };
2528
+ var ShellTaskManager = class {
2529
+ tasks = /* @__PURE__ */ new Map();
2530
+ seq = 0;
2531
+ cleanupInstalled = false;
2532
+ /** 启动一个后台任务,立即返回快照(不等退出) */
2533
+ start(opts) {
2534
+ this.installCleanup();
2535
+ const id = `shell-${++this.seq}-${Date.now().toString(36)}`;
2536
+ const child = spawn2(opts.file, opts.argv, {
2537
+ cwd: opts.cwd,
2538
+ env: opts.env,
2539
+ detached: process.platform !== "win32",
2540
+ windowsHide: true,
2541
+ stdio: ["ignore", "pipe", "pipe"]
2542
+ });
2543
+ const task = {
2544
+ id,
2545
+ command: opts.command,
2546
+ scope: opts.scope ?? null,
2547
+ child,
2548
+ stdout: new RingBuffer(),
2549
+ stderr: new RingBuffer(),
2550
+ status: "running",
2551
+ exitCode: null,
2552
+ signal: null,
2553
+ startedAt: Date.now(),
2554
+ endedAt: null
2555
+ };
2556
+ this.tasks.set(id, task);
2557
+ child.stdout?.on("data", (d) => task.stdout.add(d.toString("utf-8")));
2558
+ child.stderr?.on("data", (d) => task.stderr.add(d.toString("utf-8")));
2559
+ child.on("error", (err) => task.stderr.add(`[spawn error] ${err.message}
2560
+ `));
2561
+ child.on("exit", (code, signal) => {
2562
+ task.exitCode = code;
2563
+ task.signal = signal;
2564
+ task.endedAt = Date.now();
2565
+ if (task.status !== "killed") task.status = "exited";
2566
+ });
2567
+ child.unref();
2568
+ return this.snapshot(task);
2569
+ }
2570
+ /** 读取任务输出与状态 */
2571
+ output(taskId) {
2572
+ const task = this.tasks.get(taskId);
2573
+ if (!task) return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
2574
+ return this.snapshot(task);
2575
+ }
2576
+ /** 终止任务(进程树)并返回快照;传 scope 时只允许终止该归属下的任务 */
2577
+ kill(taskId, scope) {
2578
+ const task = this.tasks.get(taskId);
2579
+ if (!task) return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
2580
+ if (scope !== void 0 && task.scope !== scope) {
2581
+ return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
2582
+ }
2583
+ if (task.status === "running") {
2584
+ task.status = "killed";
2585
+ task.endedAt = Date.now();
2586
+ killTree(task.child.pid);
2587
+ }
2588
+ return this.snapshot(task);
2589
+ }
2590
+ /** 列出任务快照(调试/展示用);传 scope 时只返回该归属下的任务 */
2591
+ list(scope) {
2592
+ const all = [...this.tasks.values()];
2593
+ return (scope === void 0 ? all : all.filter((t) => t.scope === scope)).map(
2594
+ (t) => this.snapshot(t)
2595
+ );
2596
+ }
2597
+ /** 终止全部任务(进程退出时调用) */
2598
+ disposeAll() {
2599
+ for (const task of this.tasks.values()) {
2600
+ if (task.status === "running") {
2601
+ task.status = "killed";
2602
+ killTree(task.child.pid);
2603
+ }
2604
+ }
2605
+ }
2606
+ snapshot(task) {
2607
+ return {
2608
+ task_id: task.id,
2609
+ command: task.command,
2610
+ status: task.status,
2611
+ running: task.status === "running",
2612
+ exit_code: task.exitCode,
2613
+ signal: task.signal,
2614
+ stdout: task.stdout.get(),
2615
+ stderr: task.stderr.get(),
2616
+ duration_ms: (task.endedAt ?? Date.now()) - task.startedAt
2617
+ };
2618
+ }
2619
+ installCleanup() {
2620
+ if (this.cleanupInstalled) return;
2621
+ this.cleanupInstalled = true;
2622
+ process.once("exit", () => this.disposeAll());
2623
+ }
2624
+ };
2625
+
2626
+ // src/tools/shell.ts
2627
+ var MAX_BUFFER = 1024 * 1024 * 5;
2628
+ function decodeOutput(buf) {
2629
+ if (!buf || buf.length === 0) return "";
2630
+ try {
2631
+ return new TextDecoder("utf-8", { fatal: true }).decode(buf);
2632
+ } catch {
2633
+ for (const enc of ["gbk", "big5", "shift_jis", "latin1"]) {
2634
+ try {
2635
+ return new TextDecoder(enc).decode(buf);
2636
+ } catch {
2637
+ }
2638
+ }
2639
+ return buf.toString("latin1");
2640
+ }
2641
+ }
2642
+ var shellTasks = new ShellTaskManager();
2643
+
2644
+ // src/tools/nodeGrep.ts
2645
+ import * as fs4 from "fs";
2646
+ import * as path5 from "path";
2647
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
2648
+ ".git",
2649
+ "node_modules",
2650
+ ".svn",
2651
+ ".hg",
2652
+ "dist",
2653
+ "build",
2654
+ ".next",
2655
+ ".cache"
2656
+ ]);
2657
+ function looksBinary(buf) {
2658
+ const n = Math.min(buf.length, 8192);
2659
+ for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
2660
+ return false;
2661
+ }
2662
+ function nodeGrep(opts) {
2663
+ const maxMatches = opts.maxMatches ?? 50;
2664
+ const maxFileBytes = opts.maxFileBytes ?? 1024 * 1024;
2665
+ let regex;
2666
+ try {
2667
+ regex = new RegExp(opts.pattern);
2668
+ } catch (err) {
2669
+ throw new Error(`\u65E0\u6548\u7684\u6B63\u5219\u8868\u8FBE\u5F0F: ${opts.pattern}\uFF08${String(err)}\uFF09`);
2670
+ }
2671
+ const target = path5.resolve(opts.workspace, opts.target);
2672
+ if (!fs4.existsSync(target)) {
2673
+ throw new Error(`\u641C\u7D22\u8DEF\u5F84\u4E0D\u5B58\u5728: ${opts.target}`);
2674
+ }
2675
+ const matches = [];
2676
+ const scanFile = (file) => {
2677
+ let stat2;
2678
+ try {
2679
+ stat2 = fs4.statSync(file);
2680
+ } catch {
2681
+ return false;
2682
+ }
2683
+ if (!stat2.isFile() || stat2.size > maxFileBytes) return false;
2684
+ let buf;
2685
+ try {
2686
+ buf = fs4.readFileSync(file);
2687
+ } catch {
2688
+ return false;
2689
+ }
2690
+ if (looksBinary(buf)) return false;
2691
+ const rel = path5.relative(opts.workspace, file) || path5.basename(file);
2692
+ const lines = buf.toString("utf-8").split("\n");
2693
+ for (let i = 0; i < lines.length; i++) {
2694
+ const raw = lines[i].replace(/\r$/, "");
2695
+ if (regex.test(raw)) {
2696
+ matches.push({ file: rel, line: i + 1, text: raw.trim().slice(0, 200) });
2697
+ if (matches.length >= maxMatches) return true;
2698
+ }
2699
+ }
2700
+ return false;
2701
+ };
2702
+ const stat = fs4.statSync(target);
2703
+ if (stat.isFile()) {
2704
+ scanFile(target);
2705
+ return matches;
2706
+ }
2707
+ const stack = [target];
2708
+ while (stack.length > 0) {
2709
+ const dir = stack.pop();
2710
+ let entries;
2711
+ try {
2712
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
2713
+ } catch {
2714
+ continue;
2715
+ }
2716
+ for (const entry of entries) {
2717
+ const full = path5.join(dir, entry.name);
2718
+ if (entry.isDirectory()) {
2719
+ if (!SKIP_DIRS.has(entry.name)) stack.push(full);
2720
+ } else if (entry.isFile()) {
2721
+ if (scanFile(full)) return matches;
2722
+ }
2723
+ }
2724
+ }
2725
+ return matches;
2726
+ }
2727
+
2728
+ // src/tools/ripgrep.ts
2729
+ import * as fs5 from "fs";
2730
+ import * as os from "os";
2731
+ import * as path6 from "path";
2732
+ import { createRequire } from "module";
2733
+ var cached;
2734
+ function extractBundledRipgrep() {
2735
+ try {
2736
+ const req = typeof __require === "function" ? __require : createRequire(path6.join(process.cwd(), "__zqagent__.js"));
2737
+ const sea = req("node:sea");
2738
+ if (!sea?.isSea?.() || typeof sea.getAsset !== "function") return null;
2739
+ const asset = sea.getAsset("rg");
2740
+ if (!asset) return null;
2741
+ const dir = fs5.mkdtempSync(path6.join(os.tmpdir(), "zqagent-rg-"));
2742
+ const file = path6.join(dir, process.platform === "win32" ? "rg.exe" : "rg");
2743
+ fs5.writeFileSync(file, Buffer.from(asset));
2744
+ fs5.chmodSync(file, 493);
2745
+ return file;
2746
+ } catch {
2747
+ return null;
2748
+ }
2749
+ }
2750
+ function resolveRipgrep() {
2751
+ if (cached !== void 0) return cached;
2752
+ const explicit = (process.env.ZQAGENT_RG ?? "").trim();
2753
+ if (explicit && fs5.existsSync(explicit)) {
2754
+ cached = explicit;
2755
+ return cached;
2756
+ }
2757
+ const bundled = extractBundledRipgrep();
2758
+ if (bundled) {
2759
+ cached = bundled;
2760
+ return cached;
2761
+ }
2762
+ cached = findExecutable("rg", {
2763
+ platform: process.platform,
2764
+ env: process.env,
2765
+ fileExists: (p) => {
2766
+ try {
2767
+ return fs5.statSync(p).isFile();
2768
+ } catch {
2769
+ return false;
2770
+ }
2771
+ }
2772
+ });
2773
+ return cached;
2774
+ }
2775
+
2088
2776
  // src/tools/search.ts
2089
- import { execFileSync } from "child_process";
2777
+ function parseRgOutput(result) {
2778
+ return result.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0).slice(0, 50).map((line) => {
2779
+ const m = /^(.+?):(\d+):(.*)$/.exec(line);
2780
+ if (!m) return { file: line.trim(), line: 0, text: "" };
2781
+ return { file: m[1], line: Number(m[2]), text: m[3].trim().slice(0, 200) };
2782
+ });
2783
+ }
2090
2784
  function makeGrep(workspace) {
2091
- return function grep(args) {
2785
+ return async function grep(args) {
2092
2786
  const pattern = String(args.pattern ?? "");
2093
2787
  const filePath = String(args.path ?? ".");
2094
2788
  if (!pattern.trim()) {
2095
2789
  return { error: "\u641C\u7D22\u6A21\u5F0F\u4E0D\u80FD\u4E3A\u7A7A" };
2096
2790
  }
2097
- const execOpts = {
2098
- cwd: workspace,
2099
- encoding: "utf-8",
2100
- timeout: 1e4,
2101
- maxBuffer: 1024 * 1024 * 2
2102
- };
2103
- try {
2104
- let result;
2105
- try {
2106
- result = execFileSync(
2107
- "rg",
2108
- ["--line-number", "--no-heading", "--max-count", "50", pattern, filePath],
2109
- execOpts
2110
- ).toString();
2111
- } catch {
2112
- result = execFileSync(
2113
- "grep",
2114
- ["-rn", "--max-count", "50", pattern, filePath],
2115
- execOpts
2116
- ).toString();
2791
+ const rg = resolveRipgrep();
2792
+ if (rg) {
2793
+ const res = await execAsync(
2794
+ rg,
2795
+ [
2796
+ // --with-filename:单文件模式下 rg 默认不输出文件名前缀,会导致
2797
+ // "行号:内容" 被误当成 "文件:行号:内容" 解析而错位;显式要求带文件名。
2798
+ "--with-filename",
2799
+ "--line-number",
2800
+ "--no-heading",
2801
+ "--max-count",
2802
+ "50",
2803
+ pattern,
2804
+ filePath
2805
+ ],
2806
+ {
2807
+ cwd: workspace,
2808
+ timeoutMs: 1e4,
2809
+ maxBuffer: 1024 * 1024 * 2
2810
+ }
2811
+ );
2812
+ if (res.status === 0) {
2813
+ const matches = parseRgOutput(decodeOutput(res.stdout));
2814
+ return { query: pattern, engine: "ripgrep", matches, count: matches.length };
2117
2815
  }
2118
- const lines = result.trim().split("\n").slice(0, 50);
2119
- const matches = lines.map((line) => {
2120
- const [file, lineno, ...rest] = line.split(":");
2121
- return { file, line: Number(lineno), text: rest.join(":").trim().slice(0, 200) };
2122
- });
2123
- return { query: pattern, matches, count: matches.length };
2124
- } catch (exc) {
2125
- if (exc instanceof Error && "status" in exc && exc.status === 1) {
2126
- return { query: pattern, matches: [], count: 0 };
2816
+ if (res.status === 1) {
2817
+ return { query: pattern, engine: "ripgrep", matches: [], count: 0 };
2127
2818
  }
2128
- return { error: `\u641C\u7D22\u5931\u8D25: ${String(exc)}` };
2819
+ }
2820
+ try {
2821
+ const matches = nodeGrep({ pattern, target: filePath, workspace });
2822
+ return { query: pattern, engine: "node", matches, count: matches.length };
2823
+ } catch (exc) {
2824
+ return { error: `\u641C\u7D22\u5931\u8D25: ${String(exc instanceof Error ? exc.message : exc)}` };
2129
2825
  }
2130
2826
  };
2131
2827
  }
@@ -2138,7 +2834,7 @@ function registerSearchTool(registry, workspace) {
2138
2834
  type: "function",
2139
2835
  function: {
2140
2836
  name: "grep",
2141
- 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",
2837
+ 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",
2142
2838
  parameters: {
2143
2839
  type: "object",
2144
2840
  properties: {
@@ -2153,90 +2849,138 @@ function registerSearchTool(registry, workspace) {
2153
2849
  }
2154
2850
 
2155
2851
  // src/tools/git.ts
2156
- import { execFileSync as execFileSync2 } from "child_process";
2852
+ import * as fs6 from "fs";
2853
+ import * as path7 from "path";
2854
+ async function runGit(repoDir, args, timeout = 15e3) {
2855
+ const res = await execAsync("git", ["-c", "core.quotepath=false", ...args], {
2856
+ cwd: repoDir,
2857
+ timeoutMs: timeout,
2858
+ maxBuffer: 1024 * 1024 * 5,
2859
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
2860
+ });
2861
+ return {
2862
+ status: res.status ?? 1,
2863
+ stdout: decodeOutput(res.stdout),
2864
+ stderr: decodeOutput(res.stderr) || (res.error?.message ?? "")
2865
+ };
2866
+ }
2867
+ async function resolveRepoRoot(workspace, pathArg) {
2868
+ let dir = workspace;
2869
+ if (pathArg) {
2870
+ const abs = path7.resolve(workspace, pathArg);
2871
+ try {
2872
+ dir = fs6.statSync(abs).isDirectory() ? abs : path7.dirname(abs);
2873
+ } catch {
2874
+ dir = abs;
2875
+ }
2876
+ }
2877
+ const res = await runGit(dir, ["rev-parse", "--show-toplevel"], 1e4);
2878
+ if (res.status === 0 && res.stdout.trim()) return res.stdout.trim();
2879
+ return workspace;
2880
+ }
2881
+ function repoDirArg(workspace, raw) {
2882
+ if (typeof raw !== "string") return void 0;
2883
+ const v = raw.trim();
2884
+ if (!v) return void 0;
2885
+ const base = path7.resolve(workspace);
2886
+ const abs = path7.resolve(base, v);
2887
+ const rel = path7.relative(base, abs);
2888
+ if (rel.startsWith("..") || path7.isAbsolute(rel)) return void 0;
2889
+ return abs;
2890
+ }
2891
+ function toPathspec(repoRoot, workspace, pathArg) {
2892
+ if (!pathArg) return null;
2893
+ const abs = path7.resolve(workspace, pathArg);
2894
+ let rel = path7.relative(repoRoot, abs);
2895
+ if (rel === "") return ".";
2896
+ if (rel.startsWith("..") || path7.isAbsolute(rel)) {
2897
+ try {
2898
+ const realRepo = fs6.realpathSync(repoRoot);
2899
+ const realAbs = fs6.realpathSync(abs);
2900
+ const rel2 = path7.relative(realRepo, realAbs);
2901
+ if (rel2 === "") return ".";
2902
+ if (!rel2.startsWith("..") && !path7.isAbsolute(rel2)) return rel2;
2903
+ } catch {
2904
+ }
2905
+ return null;
2906
+ }
2907
+ return rel;
2908
+ }
2157
2909
  function makeGitLog(workspace) {
2158
- return function git_log(args) {
2910
+ return async function git_log(args) {
2159
2911
  const filePath = args.path ? String(args.path) : void 0;
2160
2912
  const maxCount = Math.min(Number(args.max_count ?? 20), 200);
2161
2913
  if (maxCount < 1) return { error: "max_count \u5FC5\u987B >= 1" };
2914
+ const repo = await resolveRepoRoot(workspace, repoDirArg(workspace, args.repo) ?? filePath);
2162
2915
  const cmd = [
2163
- "-C",
2164
- workspace,
2165
2916
  "log",
2166
2917
  `-n${maxCount}`,
2167
2918
  "--pretty=format:%h%x1f%an%x1f%ad%x1f%s",
2168
2919
  "--date=short"
2169
2920
  ];
2170
- if (filePath) cmd.push("--", filePath);
2171
- try {
2172
- const out = execFileSync2("git", cmd, { encoding: "utf-8", timeout: 15e3 });
2173
- const commits = out.trim().split("\n").filter(Boolean).map((line) => {
2174
- const parts = line.split("");
2175
- if (parts.length === 4) {
2176
- return { hash: parts[0], author: parts[1], date: parts[2], subject: parts[3] };
2177
- }
2178
- return { raw: line };
2179
- });
2180
- return { count: commits.length, commits };
2181
- } catch (exc) {
2182
- const err = exc;
2183
- return { error: (err.stderr ?? err.message ?? "git log failed").slice(0, 300) };
2184
- }
2921
+ const pathspec = toPathspec(repo, workspace, filePath);
2922
+ if (pathspec) cmd.push("--", pathspec);
2923
+ const res = await runGit(repo, cmd);
2924
+ if (res.status !== 0) {
2925
+ return { error: (res.stderr || "git log failed").trim().slice(0, 300) };
2926
+ }
2927
+ const commits = res.stdout.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0).map((line) => {
2928
+ const parts = line.split("");
2929
+ if (parts.length === 4) {
2930
+ return { hash: parts[0], author: parts[1], date: parts[2], subject: parts[3] };
2931
+ }
2932
+ return { raw: line };
2933
+ });
2934
+ return { repo, count: commits.length, commits };
2185
2935
  };
2186
2936
  }
2187
2937
  function makeGitStatus(workspace) {
2188
- return function git_status(_args) {
2189
- try {
2190
- const out = execFileSync2(
2191
- "git",
2192
- ["-C", workspace, "status", "--porcelain"],
2193
- { encoding: "utf-8", timeout: 1e4 }
2194
- );
2195
- const lines = out.trim().split("\n").filter(Boolean);
2196
- const entries = lines.map((line) => {
2197
- const status = line.slice(0, 2).trim();
2198
- const file = line.slice(3);
2199
- return { status, file };
2200
- });
2201
- return {
2202
- clean: entries.length === 0,
2203
- entries,
2204
- count: entries.length
2205
- };
2206
- } catch (exc) {
2207
- const err = exc;
2208
- if ((err.stderr ?? "").toLowerCase().includes("not a git repository")) {
2938
+ return async function git_status(args) {
2939
+ const filePath = args.path ? String(args.path) : void 0;
2940
+ const repo = await resolveRepoRoot(workspace, repoDirArg(workspace, args.repo) ?? filePath);
2941
+ const res = await runGit(repo, ["status", "--porcelain"], 1e4);
2942
+ if (res.status !== 0) {
2943
+ const msg = res.stderr;
2944
+ if (msg.toLowerCase().includes("not a git repository")) {
2209
2945
  return { error: "\u4E0D\u662F git \u4ED3\u5E93" };
2210
2946
  }
2211
- return { error: (err.stderr ?? err.message ?? "git status failed").slice(0, 300) };
2212
- }
2947
+ return { error: (msg || "git status failed").trim().slice(0, 300) };
2948
+ }
2949
+ const lines = res.stdout.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0);
2950
+ const entries = lines.map((line) => {
2951
+ const status = line.slice(0, 2).trim();
2952
+ const index = line.slice(0, 1).trim();
2953
+ const worktree = line.slice(1, 2).trim();
2954
+ let file = line.slice(3);
2955
+ const renameArrow = file.indexOf(" -> ");
2956
+ if (renameArrow >= 0) file = file.slice(renameArrow + 4);
2957
+ return { status, file, index, worktree };
2958
+ });
2959
+ return { repo, clean: entries.length === 0, entries, count: entries.length };
2213
2960
  };
2214
2961
  }
2215
2962
  function makeGitDiff(workspace) {
2216
- return function git_diff(args) {
2963
+ return async function git_diff(args) {
2217
2964
  const staged = args.staged === true;
2218
2965
  const filePath = args.path ? String(args.path) : void 0;
2219
2966
  const contextLines = Math.min(Number(args.context ?? 3), 10);
2967
+ const repo = await resolveRepoRoot(workspace, repoDirArg(workspace, args.repo) ?? filePath);
2220
2968
  const cmd = ["diff"];
2221
2969
  if (staged) cmd.push("--staged");
2222
2970
  cmd.push(`-U${contextLines}`);
2223
- if (filePath) cmd.push("--", filePath);
2224
- try {
2225
- const out = execFileSync2("git", ["-C", workspace, ...cmd], {
2226
- encoding: "utf-8",
2227
- timeout: 15e3,
2228
- maxBuffer: 1024 * 1024 * 2
2229
- });
2230
- return {
2231
- staged,
2232
- path: filePath ?? null,
2233
- diff: out.slice(0, 5e4),
2234
- truncated: out.length > 5e4
2235
- };
2236
- } catch (exc) {
2237
- const err = exc;
2238
- return { error: (err.stderr ?? err.message ?? "git diff failed").slice(0, 300) };
2971
+ const pathspec = toPathspec(repo, workspace, filePath);
2972
+ if (pathspec) cmd.push("--", pathspec);
2973
+ const res = await runGit(repo, cmd);
2974
+ if (res.status !== 0) {
2975
+ return { error: (res.stderr || "git diff failed").trim().slice(0, 300) };
2239
2976
  }
2977
+ return {
2978
+ repo,
2979
+ staged,
2980
+ path: filePath ?? null,
2981
+ diff: res.stdout.slice(0, 5e4),
2982
+ truncated: res.stdout.length > 5e4
2983
+ };
2240
2984
  };
2241
2985
  }
2242
2986
  function registerGitTools(registry, workspace) {
@@ -2248,11 +2992,11 @@ function registerGitTools(registry, workspace) {
2248
2992
  type: "function",
2249
2993
  function: {
2250
2994
  name: "git_log",
2251
- 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",
2995
+ 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",
2252
2996
  parameters: {
2253
2997
  type: "object",
2254
2998
  properties: {
2255
- path: { type: "string", description: "\u53EF\u9009\uFF1A\u9650\u5B9A\u5230\u67D0\u4E2A\u6587\u4EF6\u6216\u76EE\u5F55" },
2999
+ 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" },
2256
3000
  max_count: { type: "integer", description: "\u8FD4\u56DE\u591A\u5C11\u6761\u63D0\u4EA4 (\u9ED8\u8BA4 20\uFF0C\u6700\u5927 200)" }
2257
3001
  }
2258
3002
  }
@@ -2270,7 +3014,9 @@ function registerGitTools(registry, workspace) {
2270
3014
  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",
2271
3015
  parameters: {
2272
3016
  type: "object",
2273
- properties: {}
3017
+ properties: {
3018
+ 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" }
3019
+ }
2274
3020
  }
2275
3021
  }
2276
3022
  }
@@ -2288,7 +3034,7 @@ function registerGitTools(registry, workspace) {
2288
3034
  type: "object",
2289
3035
  properties: {
2290
3036
  staged: { type: "boolean", description: "\u662F\u5426\u67E5\u770B\u5DF2\u6682\u5B58\u7684\u66F4\u6539 (\u9ED8\u8BA4 false)" },
2291
- path: { type: "string", description: "\u53EF\u9009\uFF1A\u9650\u5B9A\u5230\u67D0\u4E2A\u6587\u4EF6" },
3037
+ 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" },
2292
3038
  context: { type: "integer", description: "\u4E0A\u4E0B\u6587\u884C\u6570 (\u9ED8\u8BA4 3)" }
2293
3039
  }
2294
3040
  }
@@ -2303,7 +3049,7 @@ function buildExplorerEngine(opts) {
2303
3049
  if (opts.readonlyRegistrars) {
2304
3050
  opts.readonlyRegistrars(registry);
2305
3051
  } else {
2306
- registerFileTools(registry, opts.workspace);
3052
+ registerReadOnlyFileTools(registry, opts.workspace);
2307
3053
  registerSearchTool(registry, opts.workspace);
2308
3054
  registerGitTools(registry, opts.workspace);
2309
3055
  }
@@ -2317,7 +3063,8 @@ function buildExplorerEngine(opts) {
2317
3063
  permissions,
2318
3064
  model: opts.model,
2319
3065
  instructions: EXPLORER_INSTRUCTIONS,
2320
- maxIterations: 10
3066
+ maxIterations: parseMaxIterations(process.env.EXPLORE_MAX_ITERATIONS, 30),
3067
+ hook: opts.hook
2321
3068
  });
2322
3069
  }
2323
3070
  function makeExploreTool(opts) {
@@ -2328,7 +3075,8 @@ function makeExploreTool(opts) {
2328
3075
  workspace: opts.workspace,
2329
3076
  provider: opts.provider,
2330
3077
  model: opts.model,
2331
- readonlyRegistrars: opts.readonlyRegistrars
3078
+ readonlyRegistrars: opts.readonlyRegistrars,
3079
+ hook: opts.hookFactory?.()
2332
3080
  });
2333
3081
  let report = "";
2334
3082
  let status = "unknown";