@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.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
137
|
-
// 注: Gemini
|
|
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
|
-
// 来源:
|
|
153
|
-
// 注: DeepSeek-V4
|
|
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
|
-
|
|
159
|
-
//
|
|
160
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
"
|
|
171
|
-
|
|
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
|
|
302
|
+
function triggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capTokens) {
|
|
199
303
|
const window = contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
200
|
-
|
|
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 ??
|
|
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
|
-
|
|
888
|
-
|
|
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.
|
|
894
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
982
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
|
1568
|
-
return shouldCompact(
|
|
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(
|
|
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
|
|
1637
|
-
if (
|
|
1638
|
-
this._producedFiles.push(
|
|
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
|
|
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
|
-
|
|
1730
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2044
|
+
if (parent === probe) return null;
|
|
1753
2045
|
probe = parent;
|
|
1754
2046
|
}
|
|
1755
2047
|
}
|
|
1756
|
-
|
|
1757
|
-
return
|
|
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([
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 &&
|
|
1834
|
-
|
|
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
|
-
|
|
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
|
|
1883
|
-
if (!
|
|
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
|
|
1950
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
"
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
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
|
-
|
|
2119
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
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(
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
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: (
|
|
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
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
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
|
-
|
|
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:
|
|
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";
|