deepspider 0.3.1 → 0.4.0
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/.env.example +3 -0
- package/README.md +21 -15
- package/package.json +9 -7
- package/src/agent/core/PanelBridge.js +56 -78
- package/src/agent/core/StreamHandler.js +244 -20
- package/src/agent/index.js +120 -23
- package/src/agent/logger.js +183 -8
- package/src/agent/middleware/memoryFlush.js +48 -0
- package/src/agent/middleware/report.js +95 -37
- package/src/agent/middleware/subagent.js +236 -0
- package/src/agent/middleware/toolAvailability.js +37 -0
- package/src/agent/middleware/toolGuard.js +187 -0
- package/src/agent/middleware/validationWorkflow.js +171 -0
- package/src/agent/prompts/system.js +310 -59
- package/src/agent/run.js +168 -20
- package/src/agent/sessions.js +88 -0
- package/src/agent/skills/anti-detect/SKILL.md +89 -14
- package/src/agent/skills/captcha/SKILL.md +93 -19
- package/src/agent/skills/crawler/SKILL.md +64 -3
- package/src/agent/skills/crawler/evolved.md +9 -1
- package/src/agent/skills/dynamic-analysis/SKILL.md +74 -7
- package/src/agent/skills/env/SKILL.md +75 -0
- package/src/agent/skills/js2python/evolved.md +5 -1
- package/src/agent/skills/sandbox/SKILL.md +35 -0
- package/src/agent/skills/static-analysis/SKILL.md +98 -2
- package/src/agent/skills/static-analysis/evolved.md +5 -1
- package/src/agent/subagents/anti-detect.js +36 -24
- package/src/agent/subagents/captcha.js +35 -28
- package/src/agent/subagents/crawler.js +40 -105
- package/src/agent/subagents/factory.js +129 -9
- package/src/agent/subagents/index.js +4 -13
- package/src/agent/subagents/js2python.js +25 -35
- package/src/agent/subagents/reverse.js +180 -0
- package/src/agent/tools/analysis.js +101 -8
- package/src/agent/tools/anti-detect.js +5 -2
- package/src/agent/tools/browser.js +186 -13
- package/src/agent/tools/capture.js +24 -3
- package/src/agent/tools/correlate.js +129 -15
- package/src/agent/tools/crawler.js +3 -2
- package/src/agent/tools/crawlerGenerator.js +90 -0
- package/src/agent/tools/debug.js +43 -6
- package/src/agent/tools/evolve.js +5 -2
- package/src/agent/tools/extractor.js +5 -1
- package/src/agent/tools/file.js +14 -5
- package/src/agent/tools/generateHook.js +66 -0
- package/src/agent/tools/hookManager.js +19 -9
- package/src/agent/tools/index.js +36 -21
- package/src/agent/tools/nodejs.js +41 -6
- package/src/agent/tools/patch.js +1 -1
- package/src/agent/tools/sandbox.js +21 -1
- package/src/agent/tools/scratchpad.js +70 -0
- package/src/agent/tools/store.js +1 -1
- package/src/agent/tools/tracing.js +26 -0
- package/src/agent/tools/verifyAlgorithm.js +117 -0
- package/src/browser/EnvBridge.js +27 -13
- package/src/browser/client.js +128 -18
- package/src/browser/collector.js +101 -22
- package/src/browser/defaultHooks.js +3 -1
- package/src/browser/hooks/index.js +5 -0
- package/src/browser/interceptors/AntiDebugInterceptor.js +132 -0
- package/src/browser/interceptors/NetworkInterceptor.js +76 -12
- package/src/browser/interceptors/ScriptInterceptor.js +32 -7
- package/src/browser/interceptors/index.js +1 -0
- package/src/browser/ui/analysisPanel.js +541 -464
- package/src/cli/commands/config.js +11 -3
- package/src/config/paths.js +9 -1
- package/src/config/settings.js +7 -1
- package/src/core/PatchGenerator.js +24 -4
- package/src/core/Sandbox.js +140 -3
- package/src/env/EnvCodeGenerator.js +60 -88
- package/src/env/modules/bom/history.js +6 -0
- package/src/env/modules/bom/location.js +6 -0
- package/src/env/modules/bom/navigator.js +13 -0
- package/src/env/modules/bom/screen.js +6 -0
- package/src/env/modules/bom/storage.js +7 -0
- package/src/env/modules/dom/document.js +14 -0
- package/src/env/modules/dom/event.js +4 -0
- package/src/env/modules/index.js +27 -10
- package/src/env/modules/webapi/fetch.js +4 -0
- package/src/env/modules/webapi/url.js +4 -0
- package/src/env/modules/webapi/xhr.js +8 -0
- package/src/store/DataStore.js +125 -42
- package/src/store/Store.js +2 -1
- package/src/agent/subagents/dynamic.js +0 -64
- package/src/agent/subagents/env-agent.js +0 -82
- package/src/agent/subagents/sandbox.js +0 -55
- package/src/agent/subagents/static.js +0 -66
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepSpider - 流式输出处理器
|
|
3
|
-
* 处理 Agent
|
|
3
|
+
* 处理 Agent 的流式事件,支持 interrupt HITL 交互
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { Command } from '@langchain/langgraph';
|
|
6
7
|
import { isApiServiceError, isToolSchemaError } from '../errors/ErrorClassifier.js';
|
|
7
8
|
import { RetryManager, sleep } from './RetryManager.js';
|
|
8
9
|
|
|
@@ -12,6 +13,31 @@ function cleanDSML(text) {
|
|
|
12
13
|
return text ? text.replace(DSML_PATTERN, '') : text;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
// 流式事件停滞超时(单个事件间隔上限)
|
|
17
|
+
const STALL_TIMEOUT_MS = 150000; // 150s — 超过此时间无新事件则中断流
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 包装异步迭代器,每个 next() 加独立超时
|
|
21
|
+
* 防止 LLM API 或 middleware 无响应时 for-await 永久挂起
|
|
22
|
+
*/
|
|
23
|
+
async function* withStallTimeout(asyncIterator, timeoutMs = STALL_TIMEOUT_MS) {
|
|
24
|
+
while (true) {
|
|
25
|
+
let timer;
|
|
26
|
+
const result = await Promise.race([
|
|
27
|
+
asyncIterator.next(),
|
|
28
|
+
new Promise((_, reject) => {
|
|
29
|
+
timer = setTimeout(
|
|
30
|
+
() => reject(new Error(`Stream timeout: no events for ${Math.round(timeoutMs / 1000)}s`)),
|
|
31
|
+
timeoutMs,
|
|
32
|
+
);
|
|
33
|
+
}),
|
|
34
|
+
]);
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
if (result.done) break;
|
|
37
|
+
yield result.value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
// 人工介入配置
|
|
16
42
|
const INTERVENTION_CONFIG = {
|
|
17
43
|
idleTimeoutMs: 120000, // 2分钟无响应触发提示
|
|
@@ -26,6 +52,7 @@ export class StreamHandler {
|
|
|
26
52
|
this.riskTools = riskTools;
|
|
27
53
|
this.debug = debug;
|
|
28
54
|
this.retryManager = new RetryManager();
|
|
55
|
+
this.fullResponse = '';
|
|
29
56
|
}
|
|
30
57
|
|
|
31
58
|
/**
|
|
@@ -37,8 +64,8 @@ export class StreamHandler {
|
|
|
37
64
|
let eventCount = 0;
|
|
38
65
|
let lastToolCall = null;
|
|
39
66
|
|
|
40
|
-
//
|
|
41
|
-
this.
|
|
67
|
+
// 重置状态
|
|
68
|
+
this.fullResponse = '';
|
|
42
69
|
await this.panelBridge.setBusy(true);
|
|
43
70
|
|
|
44
71
|
this.debug(`chatStream: 开始处理, 输入长度=${input.length}`);
|
|
@@ -61,7 +88,7 @@ export class StreamHandler {
|
|
|
61
88
|
);
|
|
62
89
|
|
|
63
90
|
this.debug('chatStream: 开始遍历事件');
|
|
64
|
-
for await (const event of eventStream) {
|
|
91
|
+
for await (const event of withStallTimeout(eventStream)) {
|
|
65
92
|
lastEventTime = Date.now();
|
|
66
93
|
eventCount++;
|
|
67
94
|
|
|
@@ -71,10 +98,12 @@ export class StreamHandler {
|
|
|
71
98
|
|
|
72
99
|
await this._handleStreamEvent(event);
|
|
73
100
|
|
|
74
|
-
if (event.event === 'on_chat_model_end'
|
|
101
|
+
if (event.event === 'on_chat_model_end') {
|
|
75
102
|
const output = event.data?.output;
|
|
76
103
|
if (output?.content) {
|
|
77
|
-
finalResponse = output.content
|
|
104
|
+
finalResponse = typeof output.content === 'string'
|
|
105
|
+
? output.content
|
|
106
|
+
: output.content.filter(c => c.type === 'text').map(c => c.text).join('');
|
|
78
107
|
this.debug(`chatStream: 收到最终响应, 长度=${finalResponse.length}`);
|
|
79
108
|
}
|
|
80
109
|
}
|
|
@@ -83,8 +112,17 @@ export class StreamHandler {
|
|
|
83
112
|
clearInterval(heartbeat);
|
|
84
113
|
console.log(`\n[完成] 共处理 ${eventCount} 个事件`);
|
|
85
114
|
|
|
86
|
-
|
|
87
|
-
await this.
|
|
115
|
+
// 发送剩余累积文本
|
|
116
|
+
const flushed = await this._flushFullResponse();
|
|
117
|
+
|
|
118
|
+
// 检测 interrupt 并渲染到面板
|
|
119
|
+
const hasInterrupt = await this._checkAndRenderInterrupt();
|
|
120
|
+
|
|
121
|
+
// 兜底:如果没有文本输出也没有 interrupt,发送完成通知
|
|
122
|
+
if (!flushed && !hasInterrupt && eventCount > 0 && lastToolCall) {
|
|
123
|
+
await this.panelBridge.sendToPanel('system', '✅ 任务完成');
|
|
124
|
+
}
|
|
125
|
+
|
|
88
126
|
await this.panelBridge.setBusy(false);
|
|
89
127
|
|
|
90
128
|
this.debug(`chatStream: 完成, 响应长度=${finalResponse.length}`);
|
|
@@ -96,16 +134,97 @@ export class StreamHandler {
|
|
|
96
134
|
}
|
|
97
135
|
|
|
98
136
|
/**
|
|
99
|
-
*
|
|
137
|
+
* 用 Command({ resume }) 恢复被 interrupt 暂停的 graph
|
|
138
|
+
*/
|
|
139
|
+
async resumeInterrupt(value) {
|
|
140
|
+
let finalResponse = '';
|
|
141
|
+
let lastEventTime = Date.now();
|
|
142
|
+
let eventCount = 0;
|
|
143
|
+
|
|
144
|
+
this.fullResponse = '';
|
|
145
|
+
await this.panelBridge.setBusy(true);
|
|
146
|
+
this.debug(`resumeInterrupt: 恢复 interrupt, value=${value}`);
|
|
147
|
+
|
|
148
|
+
const heartbeat = setInterval(() => {
|
|
149
|
+
const elapsed = Math.round((Date.now() - lastEventTime) / 1000);
|
|
150
|
+
if (elapsed > 30) {
|
|
151
|
+
console.log(`\n[心跳] 恢复中,已等待 ${elapsed}s`);
|
|
152
|
+
}
|
|
153
|
+
}, 30000);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const eventStream = await this.agent.streamEvents(
|
|
157
|
+
new Command({ resume: value }),
|
|
158
|
+
{ ...this.config, version: 'v2' }
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
for await (const event of withStallTimeout(eventStream)) {
|
|
162
|
+
lastEventTime = Date.now();
|
|
163
|
+
eventCount++;
|
|
164
|
+
await this._handleStreamEvent(event);
|
|
165
|
+
|
|
166
|
+
if (event.event === 'on_chat_model_end') {
|
|
167
|
+
const output = event.data?.output;
|
|
168
|
+
if (output?.content) {
|
|
169
|
+
finalResponse = typeof output.content === 'string'
|
|
170
|
+
? output.content
|
|
171
|
+
: output.content.filter(c => c.type === 'text').map(c => c.text).join('');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
clearInterval(heartbeat);
|
|
177
|
+
|
|
178
|
+
const flushed = await this._flushFullResponse();
|
|
179
|
+
const hasInterrupt = await this._checkAndRenderInterrupt();
|
|
180
|
+
|
|
181
|
+
// 兜底:如果没有文本输出也没有 interrupt,发送完成通知
|
|
182
|
+
if (!flushed && !hasInterrupt && eventCount > 0) {
|
|
183
|
+
await this.panelBridge.sendToPanel('system', '✅ 任务完成');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await this.panelBridge.setBusy(false);
|
|
187
|
+
|
|
188
|
+
console.log(`\n[恢复完成] 共处理 ${eventCount} 个事件`);
|
|
189
|
+
return finalResponse || '[无响应]';
|
|
190
|
+
} catch (error) {
|
|
191
|
+
clearInterval(heartbeat);
|
|
192
|
+
await this.panelBridge.setBusy(false);
|
|
193
|
+
const errMsg = error.message || String(error);
|
|
194
|
+
console.error(`\n[恢复失败] ${errMsg}`);
|
|
195
|
+
return `恢复失败: ${errMsg}`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 从检查点恢复流式对话(错误重试用)
|
|
100
201
|
*/
|
|
101
202
|
async chatStreamResume(retryCount = 0) {
|
|
102
203
|
let finalResponse = '';
|
|
103
204
|
let lastEventTime = Date.now();
|
|
104
205
|
let eventCount = 0;
|
|
105
206
|
|
|
207
|
+
this.fullResponse = '';
|
|
106
208
|
await this.panelBridge.setBusy(true);
|
|
107
209
|
this.debug(`chatStreamResume: 从检查点恢复, retryCount=${retryCount}`);
|
|
108
210
|
|
|
211
|
+
// 恢复前:检查 checkpoint 是否有实际消息
|
|
212
|
+
if (retryCount === 0) {
|
|
213
|
+
try {
|
|
214
|
+
const state = await this.agent.getState(this.config);
|
|
215
|
+
const messages = state?.values?.messages;
|
|
216
|
+
if (!messages?.length) {
|
|
217
|
+
console.log('[恢复] checkpoint 无历史消息,跳过恢复');
|
|
218
|
+
await this.panelBridge.sendToPanel('system', '该会话无历史记录,请重新开始分析');
|
|
219
|
+
await this.panelBridge.setBusy(false);
|
|
220
|
+
return '[无历史消息]';
|
|
221
|
+
}
|
|
222
|
+
await this._restoreHistoryToPanel(messages);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
this.debug('chatStreamResume: getState 失败:', e.message);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
109
228
|
const heartbeat = setInterval(() => {
|
|
110
229
|
const elapsed = Math.round((Date.now() - lastEventTime) / 1000);
|
|
111
230
|
if (elapsed > 30) {
|
|
@@ -119,22 +238,32 @@ export class StreamHandler {
|
|
|
119
238
|
{ ...this.config, version: 'v2' }
|
|
120
239
|
);
|
|
121
240
|
|
|
122
|
-
for await (const event of eventStream) {
|
|
241
|
+
for await (const event of withStallTimeout(eventStream)) {
|
|
123
242
|
lastEventTime = Date.now();
|
|
124
243
|
eventCount++;
|
|
125
244
|
await this._handleStreamEvent(event);
|
|
126
245
|
|
|
127
|
-
if (event.event === 'on_chat_model_end'
|
|
246
|
+
if (event.event === 'on_chat_model_end') {
|
|
128
247
|
const output = event.data?.output;
|
|
129
248
|
if (output?.content) {
|
|
130
|
-
finalResponse = output.content
|
|
249
|
+
finalResponse = typeof output.content === 'string'
|
|
250
|
+
? output.content
|
|
251
|
+
: output.content.filter(c => c.type === 'text').map(c => c.text).join('');
|
|
131
252
|
}
|
|
132
253
|
}
|
|
133
254
|
}
|
|
134
255
|
|
|
135
256
|
clearInterval(heartbeat);
|
|
136
|
-
|
|
257
|
+
|
|
258
|
+
const flushed2 = await this._flushFullResponse();
|
|
259
|
+
const hasInterrupt2 = await this._checkAndRenderInterrupt();
|
|
260
|
+
|
|
261
|
+
if (!flushed2 && !hasInterrupt2 && eventCount > 0) {
|
|
262
|
+
await this.panelBridge.sendToPanel('system', '✅ 任务完成');
|
|
263
|
+
}
|
|
264
|
+
|
|
137
265
|
await this.panelBridge.setBusy(false);
|
|
266
|
+
|
|
138
267
|
console.log(`\n[恢复完成] 共处理 ${eventCount} 个事件`);
|
|
139
268
|
return finalResponse || '[无响应]';
|
|
140
269
|
} catch (error) {
|
|
@@ -154,6 +283,97 @@ export class StreamHandler {
|
|
|
154
283
|
}
|
|
155
284
|
}
|
|
156
285
|
|
|
286
|
+
/**
|
|
287
|
+
* 从 checkpoint 恢复历史消息到前端面板
|
|
288
|
+
*/
|
|
289
|
+
async _restoreHistoryToPanel(messages) {
|
|
290
|
+
try {
|
|
291
|
+
if (!messages?.length) return;
|
|
292
|
+
this.debug(`_restoreHistoryToPanel: ${messages.length} 条历史消息`);
|
|
293
|
+
|
|
294
|
+
const batch = [];
|
|
295
|
+
for (const msg of messages) {
|
|
296
|
+
const type = msg._getType?.() || msg.constructor?.name;
|
|
297
|
+
const content = Array.isArray(msg.content)
|
|
298
|
+
? msg.content.filter(c => c.type === 'text').map(c => c.text).join('')
|
|
299
|
+
: (typeof msg.content === 'string' ? msg.content : '');
|
|
300
|
+
if (!content.trim()) continue;
|
|
301
|
+
|
|
302
|
+
if (type === 'human') {
|
|
303
|
+
batch.push({ type: 'user', data: { content } });
|
|
304
|
+
} else if (type === 'ai') {
|
|
305
|
+
batch.push({ type: 'text', data: { content } });
|
|
306
|
+
} else if (type === 'tool') {
|
|
307
|
+
const summary = content.length > 200 ? content.slice(0, 200) + '...' : content;
|
|
308
|
+
batch.push({ type: 'system', data: { content: `[工具结果] ${summary}` } });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
await this.panelBridge.sendBatch(batch);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
this.debug('_restoreHistoryToPanel 失败:', e.message);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 发送剩余累积文本到面板
|
|
319
|
+
* 返回 true 如果有文本被发送
|
|
320
|
+
*/
|
|
321
|
+
async _flushFullResponse() {
|
|
322
|
+
if (this.fullResponse?.trim()) {
|
|
323
|
+
await this.panelBridge.sendToPanel('assistant', this.fullResponse);
|
|
324
|
+
this.fullResponse = '';
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
this.fullResponse = '';
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 检测 graph interrupt 状态,将 interrupt payload 渲染为面板结构化消息
|
|
333
|
+
*
|
|
334
|
+
* interrupt payload 协议:
|
|
335
|
+
* { type: 'choices', question, options: [{id, label, description?}] }
|
|
336
|
+
* { type: 'confirm', question, confirmText?, cancelText? }
|
|
337
|
+
*/
|
|
338
|
+
async _checkAndRenderInterrupt() {
|
|
339
|
+
try {
|
|
340
|
+
const state = await this.agent.getState(this.config);
|
|
341
|
+
this.debug(`_checkAndRenderInterrupt: state.next=${JSON.stringify(state?.next)}, tasks=${state?.tasks?.length ?? 'undefined'}`);
|
|
342
|
+
|
|
343
|
+
if (!state?.tasks) {
|
|
344
|
+
this.debug('_checkAndRenderInterrupt: state.tasks 为空,尝试检查 next');
|
|
345
|
+
// 某些 LangGraph 版本用 next 为空数组表示 interrupt
|
|
346
|
+
// 如果 next 不为空,说明 graph 正常结束,无 interrupt
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let found = false;
|
|
351
|
+
for (const task of state.tasks) {
|
|
352
|
+
this.debug(`_checkAndRenderInterrupt: task id=${task.id}, interrupts=${task.interrupts?.length ?? 0}`);
|
|
353
|
+
if (!task.interrupts?.length) continue;
|
|
354
|
+
for (const intr of task.interrupts) {
|
|
355
|
+
const payload = intr.value;
|
|
356
|
+
this.debug(`_checkAndRenderInterrupt: interrupt payload=${JSON.stringify(payload)?.slice(0, 200)}`);
|
|
357
|
+
if (!payload?.type) continue;
|
|
358
|
+
|
|
359
|
+
console.log(`\n[交互] 等待用户 ${payload.type === 'choices' ? '选择' : '确认'}...`);
|
|
360
|
+
|
|
361
|
+
if (payload.type === 'choices' || payload.type === 'confirm') {
|
|
362
|
+
// 删除面板中 interrupt 工具调用前 LLM 输出的冗余描述文字
|
|
363
|
+
await this.panelBridge.removeLastAssistantMessage();
|
|
364
|
+
await this.panelBridge.sendMessage(payload.type, payload);
|
|
365
|
+
found = true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return found;
|
|
370
|
+
} catch (e) {
|
|
371
|
+
this.debug('_checkAndRenderInterrupt 失败:', e.message);
|
|
372
|
+
console.log(`[DEBUG] _checkAndRenderInterrupt error: ${e.message}`);
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
157
377
|
/**
|
|
158
378
|
* 创建心跳检测定时器
|
|
159
379
|
*/
|
|
@@ -197,15 +417,20 @@ export class StreamHandler {
|
|
|
197
417
|
let chunk = data?.chunk?.content;
|
|
198
418
|
if (chunk && typeof chunk === 'string') {
|
|
199
419
|
chunk = cleanDSML(chunk);
|
|
420
|
+
// CLI 侧仍流式输出
|
|
200
421
|
process.stdout.write(chunk);
|
|
201
|
-
|
|
422
|
+
// 面板侧只累积,不推送
|
|
423
|
+
this.fullResponse = (this.fullResponse || '') + chunk;
|
|
202
424
|
}
|
|
203
425
|
break;
|
|
204
426
|
|
|
205
427
|
case 'on_tool_start':
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
428
|
+
// 工具调用前,先把已累积的 LLM 文字发送到面板
|
|
429
|
+
if (this.fullResponse?.trim()) {
|
|
430
|
+
await this.panelBridge.sendToPanel('assistant', this.fullResponse);
|
|
431
|
+
this.fullResponse = '';
|
|
432
|
+
}
|
|
433
|
+
this.debug('handleStreamEvent: 工具开始');
|
|
209
434
|
const input = data?.input || {};
|
|
210
435
|
const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
|
|
211
436
|
const preview = inputStr.length > 100 ? inputStr.slice(0, 100) + '...' : inputStr;
|
|
@@ -250,11 +475,10 @@ export class StreamHandler {
|
|
|
250
475
|
}
|
|
251
476
|
|
|
252
477
|
if (isToolSchemaError(errMsg)) {
|
|
253
|
-
console.log(`\n[重试 ${retryCount + 1}/${this.retryManager.maxRetries}]
|
|
478
|
+
console.log(`\n[重试 ${retryCount + 1}/${this.retryManager.maxRetries}] 工具参数错误,从检查点恢复...`);
|
|
254
479
|
await this.panelBridge.sendToPanel('system',
|
|
255
480
|
`工具调用失败,正在修正 (${retryCount + 1}/${this.retryManager.maxRetries})`);
|
|
256
|
-
|
|
257
|
-
return this.chatStream(resumeInput, retryCount + 1);
|
|
481
|
+
return this.chatStreamResume(retryCount + 1);
|
|
258
482
|
}
|
|
259
483
|
}
|
|
260
484
|
|
package/src/agent/index.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepSpider - DeepAgent 主入口
|
|
3
|
-
*
|
|
3
|
+
* 使用 createAgent 手动组装 middleware 栈,替换 createDeepAgent
|
|
4
|
+
* 目的:用自定义 subagent middleware 支持 context 结构化传递
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import 'dotenv/config';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { StateBackend, FilesystemBackend, createFilesystemMiddleware, createPatchToolCallsMiddleware } from 'deepagents';
|
|
9
|
+
import { createAgent, toolRetryMiddleware, summarizationMiddleware, anthropicPromptCachingMiddleware, todoListMiddleware, humanInTheLoopMiddleware } from 'langchain';
|
|
10
|
+
import { ChatAnthropic } from '@langchain/anthropic';
|
|
11
|
+
import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite';
|
|
10
12
|
|
|
11
13
|
import { coreTools } from './tools/index.js';
|
|
12
14
|
import { allSubagents } from './subagents/index.js';
|
|
13
15
|
import { systemPrompt } from './prompts/system.js';
|
|
14
16
|
import { createReportMiddleware } from './middleware/report.js';
|
|
15
17
|
import { createFilterToolsMiddleware } from './middleware/filterTools.js';
|
|
18
|
+
import { createCustomSubAgentMiddleware } from './middleware/subagent.js';
|
|
19
|
+
import { createToolGuardMiddleware } from './middleware/toolGuard.js';
|
|
20
|
+
import { createToolCallLimitMiddleware } from './subagents/factory.js';
|
|
21
|
+
import { createValidationWorkflowMiddleware } from './middleware/validationWorkflow.js';
|
|
22
|
+
import { createMemoryFlushMiddleware } from './middleware/memoryFlush.js';
|
|
23
|
+
import { createToolAvailabilityMiddleware } from './middleware/toolAvailability.js';
|
|
24
|
+
|
|
25
|
+
// createDeepAgent 内部拼接的 BASE_PROMPT
|
|
26
|
+
const BASE_PROMPT = 'In order to complete the objective that the user asks of you, you have access to a number of standard tools.';
|
|
16
27
|
|
|
17
28
|
// 从环境变量读取配置
|
|
18
29
|
const config = {
|
|
@@ -21,9 +32,48 @@ const config = {
|
|
|
21
32
|
model: process.env.DEEPSPIDER_MODEL || 'gpt-4o',
|
|
22
33
|
};
|
|
23
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 递归移除 JSON Schema 中 Anthropic API 不支持的关键字
|
|
37
|
+
* Zod v4 的 toJSONSchema 会生成 $schema 和 propertyNames,Anthropic 拒绝
|
|
38
|
+
* additionalProperties: {} 空对象也不被接受,改成 true
|
|
39
|
+
*/
|
|
40
|
+
function stripUnsupportedSchemaKeys(obj) {
|
|
41
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
42
|
+
if (Array.isArray(obj)) return obj.map(stripUnsupportedSchemaKeys);
|
|
43
|
+
const res = {};
|
|
44
|
+
for (const k in obj) {
|
|
45
|
+
if (k === '$schema' || k === 'propertyNames') continue;
|
|
46
|
+
// additionalProperties: {} → true (空对象等于"任意类型",但Anthropic不接受空对象)
|
|
47
|
+
if (k === 'additionalProperties' && obj[k] !== null && typeof obj[k] === 'object' && Object.keys(obj[k]).length === 0) {
|
|
48
|
+
res[k] = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
res[k] = stripUnsupportedSchemaKeys(obj[k]);
|
|
52
|
+
}
|
|
53
|
+
return res;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 自定义 fetch:拦截 LLM API 请求,strip 工具 schema 中 Zod v4 生成的不兼容字段
|
|
58
|
+
* 保留作为安全网,防止 $schema / propertyNames / additionalProperties:{} 泄漏到 API
|
|
59
|
+
*/
|
|
60
|
+
const _origFetch = globalThis.fetch;
|
|
61
|
+
globalThis.fetch = async function(url, opts) {
|
|
62
|
+
if (opts?.body && typeof opts.body === 'string' && opts.body.includes('"tools"')) {
|
|
63
|
+
try {
|
|
64
|
+
const body = JSON.parse(opts.body);
|
|
65
|
+
if (body.tools) {
|
|
66
|
+
body.tools = stripUnsupportedSchemaKeys(body.tools);
|
|
67
|
+
opts = { ...opts, body: JSON.stringify(body) };
|
|
68
|
+
}
|
|
69
|
+
} catch { /* ignore parse errors on non-LLM requests */ }
|
|
70
|
+
}
|
|
71
|
+
return _origFetch(url, opts);
|
|
72
|
+
};
|
|
73
|
+
|
|
24
74
|
/**
|
|
25
75
|
* 创建 LLM 模型实例
|
|
26
|
-
* 使用
|
|
76
|
+
* 使用 ChatAnthropic 发送原生 Anthropic 格式,避免代理的 OpenAI→Anthropic 转换引入 schema 错误
|
|
27
77
|
*/
|
|
28
78
|
function createModel(options = {}) {
|
|
29
79
|
const {
|
|
@@ -32,10 +82,13 @@ function createModel(options = {}) {
|
|
|
32
82
|
baseUrl = config.baseUrl,
|
|
33
83
|
} = options;
|
|
34
84
|
|
|
35
|
-
|
|
85
|
+
// ChatAnthropic 的 baseURL 不含 /v1(SDK 自动拼接)
|
|
86
|
+
const anthropicBaseUrl = baseUrl?.replace(/\/v1\/?$/, '') || undefined;
|
|
87
|
+
|
|
88
|
+
return new ChatAnthropic({
|
|
36
89
|
model,
|
|
37
|
-
apiKey,
|
|
38
|
-
|
|
90
|
+
anthropicApiKey: apiKey,
|
|
91
|
+
anthropicApiUrl: anthropicBaseUrl,
|
|
39
92
|
temperature: 0,
|
|
40
93
|
});
|
|
41
94
|
}
|
|
@@ -51,18 +104,27 @@ export function createDeepSpiderAgent(options = {}) {
|
|
|
51
104
|
enableMemory = true,
|
|
52
105
|
enableInterrupt = false,
|
|
53
106
|
onReportReady = null, // 报告就绪回调
|
|
107
|
+
onFileSaved = null, // 文件保存通知回调
|
|
108
|
+
checkpointer,
|
|
54
109
|
} = options;
|
|
55
110
|
|
|
56
|
-
// 创建 LLM
|
|
111
|
+
// 创建 LLM 模型实例(加 timeout 防止 API 无响应时 streamEvents 永久挂起)
|
|
57
112
|
const llm = createModel({ model, apiKey, baseUrl });
|
|
113
|
+
llm.timeout = 120000; // 120s — 主 LLM 超时
|
|
114
|
+
|
|
115
|
+
// 摘要专用 LLM:故意不设 timeout
|
|
116
|
+
// 原因:summarizationMiddleware 的 createSummary 有 try-catch,超时会返回错误字符串,
|
|
117
|
+
// 但 beforeModel 仍会用这个错误字符串替换所有原始消息(REMOVE_ALL_MESSAGES),导致数据丢失。
|
|
118
|
+
// 安全网由 StreamHandler.withStallTimeout (150s) 提供 — 它在 BeforeModelNode 完成前触发,
|
|
119
|
+
// 不会写入 checkpoint,原始数据得以保留。
|
|
120
|
+
const summaryLlm = createModel({ model, apiKey, baseUrl });
|
|
58
121
|
|
|
59
122
|
// 后端配置:使用文件系统持久化
|
|
60
123
|
const backend = enableMemory
|
|
61
124
|
? new FilesystemBackend({ rootDir: './.deepspider-agent' })
|
|
62
125
|
: new StateBackend();
|
|
63
126
|
|
|
64
|
-
|
|
65
|
-
const checkpointer = new MemorySaver();
|
|
127
|
+
const resolvedCheckpointer = checkpointer ?? SqliteSaver.fromConnString(':memory:');
|
|
66
128
|
|
|
67
129
|
// 人机交互配置
|
|
68
130
|
const interruptOn = enableInterrupt
|
|
@@ -72,26 +134,61 @@ export function createDeepSpiderAgent(options = {}) {
|
|
|
72
134
|
}
|
|
73
135
|
: undefined;
|
|
74
136
|
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
137
|
+
// 框架级子代理默认中间件(对照 createDeepAgent 内部的 subagentMiddleware)
|
|
138
|
+
const subagentDefaultMiddleware = [
|
|
139
|
+
todoListMiddleware(),
|
|
140
|
+
createFilesystemMiddleware({ backend }),
|
|
141
|
+
summarizationMiddleware({ model: summaryLlm, trigger: { tokens: 100000 }, keep: { messages: 6 } }),
|
|
142
|
+
anthropicPromptCachingMiddleware({ unsupportedModelBehavior: 'ignore' }),
|
|
143
|
+
createPatchToolCallsMiddleware(),
|
|
79
144
|
];
|
|
80
145
|
|
|
81
|
-
|
|
146
|
+
// 组装完整 middleware 栈(对照 createDeepAgent 源码 dist/index.js:3791-3826)
|
|
147
|
+
return createAgent({
|
|
82
148
|
name: 'deepspider',
|
|
83
149
|
model: llm,
|
|
84
150
|
tools: coreTools,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
151
|
+
systemPrompt: `${systemPrompt}\n\n${BASE_PROMPT}`,
|
|
152
|
+
middleware: [
|
|
153
|
+
// === 框架内置 middleware ===
|
|
154
|
+
todoListMiddleware(),
|
|
155
|
+
createFilesystemMiddleware({ backend }),
|
|
156
|
+
createCustomSubAgentMiddleware({
|
|
157
|
+
defaultModel: llm,
|
|
158
|
+
defaultTools: coreTools,
|
|
159
|
+
subagents: allSubagents,
|
|
160
|
+
defaultMiddleware: subagentDefaultMiddleware,
|
|
161
|
+
generalPurposeAgent: false,
|
|
162
|
+
defaultInterruptOn: interruptOn,
|
|
163
|
+
}),
|
|
164
|
+
// === 预警 + 拦截(在 summarization 之前)===
|
|
165
|
+
createMemoryFlushMiddleware(),
|
|
166
|
+
createToolAvailabilityMiddleware(),
|
|
167
|
+
summarizationMiddleware({ model: summaryLlm, trigger: { tokens: 100000 }, keep: { messages: 6 } }),
|
|
168
|
+
anthropicPromptCachingMiddleware({ unsupportedModelBehavior: 'ignore' }),
|
|
169
|
+
createPatchToolCallsMiddleware(),
|
|
170
|
+
// === HITL(如果启用)===
|
|
171
|
+
...(interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []),
|
|
172
|
+
// === 自定义 middleware ===
|
|
173
|
+
toolRetryMiddleware({
|
|
174
|
+
maxRetries: 0,
|
|
175
|
+
onFailure: (err) => {
|
|
176
|
+
// GraphInterrupt / ParentCommand 等 LangGraph 内部控制流异常必须透传,不能吞掉
|
|
177
|
+
if (err?.is_bubble_up === true) throw err;
|
|
178
|
+
return `Tool call failed: ${err.message}\nPlease fix the arguments and retry.`;
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
createToolGuardMiddleware(),
|
|
182
|
+
createToolCallLimitMiddleware(200),
|
|
183
|
+
createFilterToolsMiddleware(),
|
|
184
|
+
createValidationWorkflowMiddleware(),
|
|
185
|
+
createReportMiddleware({ onReportReady, onFileSaved }),
|
|
186
|
+
],
|
|
187
|
+
checkpointer: resolvedCheckpointer,
|
|
91
188
|
});
|
|
92
189
|
}
|
|
93
190
|
|
|
94
|
-
//
|
|
191
|
+
// 默认导出(内存模式,兼容 MCP 等非 CLI 场景)
|
|
95
192
|
export const agent = createDeepSpiderAgent();
|
|
96
193
|
|
|
97
194
|
export default agent;
|