@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.
- package/CONTRIBUTING.md +65 -0
- package/LICENSE +190 -0
- package/README.md +42 -165
- package/README.oss.en.md +161 -0
- package/README.oss.md +165 -0
- package/README.oss.zh-TW.md +165 -0
- package/SECURITY.md +51 -0
- package/dist/{ToolRegistry-BmvPxgMr.d.cts → ToolRegistry-C3pTFiaJ.d.cts} +13 -4
- package/dist/{ToolRegistry-BmvPxgMr.d.ts → ToolRegistry-C3pTFiaJ.d.ts} +13 -4
- package/dist/cli/index.cjs +17799 -2062
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +16729 -985
- package/dist/cli/index.js.map +1 -1
- package/dist/{explore-OI6htHN1.d.cts → explore-D4lCKXbA.d.ts} +160 -19
- package/dist/{explore-Pmhyk4gv.d.ts → explore-Dj1j_GyN.d.cts} +160 -19
- package/dist/index.cjs +3626 -453
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +337 -27
- package/dist/index.d.ts +337 -27
- package/dist/index.js +3617 -452
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +14029 -1482
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.js +14036 -1482
- package/dist/server/index.js.map +1 -1
- package/dist/tools/ask.cjs +14 -0
- package/dist/tools/ask.cjs.map +1 -1
- package/dist/tools/ask.d.cts +8 -2
- package/dist/tools/ask.d.ts +8 -2
- package/dist/tools/ask.js +13 -0
- package/dist/tools/ask.js.map +1 -1
- package/dist/tools/diff.cjs +59 -30
- package/dist/tools/diff.cjs.map +1 -1
- package/dist/tools/diff.d.cts +1 -1
- package/dist/tools/diff.d.ts +1 -1
- package/dist/tools/diff.js +59 -30
- package/dist/tools/diff.js.map +1 -1
- package/dist/tools/directories.d.cts +1 -1
- package/dist/tools/directories.d.ts +1 -1
- package/dist/tools/explore.cjs +1050 -309
- package/dist/tools/explore.cjs.map +1 -1
- package/dist/tools/explore.d.cts +2 -2
- package/dist/tools/explore.d.ts +2 -2
- package/dist/tools/explore.js +1057 -309
- package/dist/tools/explore.js.map +1 -1
- package/dist/tools/files.cjs +58 -43
- package/dist/tools/files.cjs.map +1 -1
- package/dist/tools/files.d.cts +10 -4
- package/dist/tools/files.d.ts +10 -4
- package/dist/tools/files.js +56 -42
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/git.cjs +816 -63
- package/dist/tools/git.cjs.map +1 -1
- package/dist/tools/git.d.cts +200 -2
- package/dist/tools/git.d.ts +200 -2
- package/dist/tools/git.js +788 -62
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/media.cjs +1191 -0
- package/dist/tools/media.cjs.map +1 -0
- package/dist/tools/media.d.cts +31 -0
- package/dist/tools/media.d.ts +31 -0
- package/dist/tools/media.js +1159 -0
- package/dist/tools/media.js.map +1 -0
- package/dist/tools/plan.d.cts +1 -1
- package/dist/tools/plan.d.ts +1 -1
- package/dist/tools/search.cjs +513 -33
- package/dist/tools/search.cjs.map +1 -1
- package/dist/tools/search.d.cts +12 -1
- package/dist/tools/search.d.ts +12 -1
- package/dist/tools/search.js +508 -33
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/shell.cjs +631 -41
- package/dist/tools/shell.cjs.map +1 -1
- package/dist/tools/shell.d.cts +37 -4
- package/dist/tools/shell.d.ts +37 -4
- package/dist/tools/shell.js +626 -40
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/todo.d.cts +1 -1
- package/dist/tools/todo.d.ts +1 -1
- package/dist/tools/web.cjs +51 -18
- package/dist/tools/web.cjs.map +1 -1
- package/dist/tools/web.d.cts +1 -1
- package/dist/tools/web.d.ts +1 -1
- package/dist/tools/web.js +51 -18
- package/dist/tools/web.js.map +1 -1
- package/package.json +43 -8
package/dist/tools/explore.cjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
175
|
-
// 注: Gemini
|
|
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
|
-
// 来源:
|
|
191
|
-
// 注: DeepSeek-V4
|
|
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
|
-
|
|
197
|
-
//
|
|
198
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
"
|
|
209
|
-
|
|
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
|
|
333
|
+
function triggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capTokens) {
|
|
237
334
|
const window = contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
238
|
-
|
|
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 ??
|
|
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
|
-
|
|
926
|
-
|
|
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.
|
|
932
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
|
1606
|
-
return shouldCompact(
|
|
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(
|
|
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
|
|
1675
|
-
if (
|
|
1676
|
-
this._producedFiles.push(
|
|
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
|
|
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
|
-
|
|
1768
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2075
|
+
if (parent === probe) return null;
|
|
1791
2076
|
probe = parent;
|
|
1792
2077
|
}
|
|
1793
2078
|
}
|
|
1794
|
-
|
|
1795
|
-
return
|
|
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([
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 &&
|
|
1872
|
-
|
|
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
|
-
|
|
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
|
|
1921
|
-
if (!
|
|
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
|
|
1988
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
"
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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
|
-
|
|
2157
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
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(
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
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: (
|
|
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
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
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
|
-
|
|
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:
|
|
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";
|