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.
Files changed (87) hide show
  1. package/.env.example +3 -0
  2. package/README.md +21 -15
  3. package/package.json +9 -7
  4. package/src/agent/core/PanelBridge.js +56 -78
  5. package/src/agent/core/StreamHandler.js +244 -20
  6. package/src/agent/index.js +120 -23
  7. package/src/agent/logger.js +183 -8
  8. package/src/agent/middleware/memoryFlush.js +48 -0
  9. package/src/agent/middleware/report.js +95 -37
  10. package/src/agent/middleware/subagent.js +236 -0
  11. package/src/agent/middleware/toolAvailability.js +37 -0
  12. package/src/agent/middleware/toolGuard.js +187 -0
  13. package/src/agent/middleware/validationWorkflow.js +171 -0
  14. package/src/agent/prompts/system.js +310 -59
  15. package/src/agent/run.js +168 -20
  16. package/src/agent/sessions.js +88 -0
  17. package/src/agent/skills/anti-detect/SKILL.md +89 -14
  18. package/src/agent/skills/captcha/SKILL.md +93 -19
  19. package/src/agent/skills/crawler/SKILL.md +64 -3
  20. package/src/agent/skills/crawler/evolved.md +9 -1
  21. package/src/agent/skills/dynamic-analysis/SKILL.md +74 -7
  22. package/src/agent/skills/env/SKILL.md +75 -0
  23. package/src/agent/skills/js2python/evolved.md +5 -1
  24. package/src/agent/skills/sandbox/SKILL.md +35 -0
  25. package/src/agent/skills/static-analysis/SKILL.md +98 -2
  26. package/src/agent/skills/static-analysis/evolved.md +5 -1
  27. package/src/agent/subagents/anti-detect.js +36 -24
  28. package/src/agent/subagents/captcha.js +35 -28
  29. package/src/agent/subagents/crawler.js +40 -105
  30. package/src/agent/subagents/factory.js +129 -9
  31. package/src/agent/subagents/index.js +4 -13
  32. package/src/agent/subagents/js2python.js +25 -35
  33. package/src/agent/subagents/reverse.js +180 -0
  34. package/src/agent/tools/analysis.js +101 -8
  35. package/src/agent/tools/anti-detect.js +5 -2
  36. package/src/agent/tools/browser.js +186 -13
  37. package/src/agent/tools/capture.js +24 -3
  38. package/src/agent/tools/correlate.js +129 -15
  39. package/src/agent/tools/crawler.js +3 -2
  40. package/src/agent/tools/crawlerGenerator.js +90 -0
  41. package/src/agent/tools/debug.js +43 -6
  42. package/src/agent/tools/evolve.js +5 -2
  43. package/src/agent/tools/extractor.js +5 -1
  44. package/src/agent/tools/file.js +14 -5
  45. package/src/agent/tools/generateHook.js +66 -0
  46. package/src/agent/tools/hookManager.js +19 -9
  47. package/src/agent/tools/index.js +36 -21
  48. package/src/agent/tools/nodejs.js +41 -6
  49. package/src/agent/tools/patch.js +1 -1
  50. package/src/agent/tools/sandbox.js +21 -1
  51. package/src/agent/tools/scratchpad.js +70 -0
  52. package/src/agent/tools/store.js +1 -1
  53. package/src/agent/tools/tracing.js +26 -0
  54. package/src/agent/tools/verifyAlgorithm.js +117 -0
  55. package/src/browser/EnvBridge.js +27 -13
  56. package/src/browser/client.js +128 -18
  57. package/src/browser/collector.js +101 -22
  58. package/src/browser/defaultHooks.js +3 -1
  59. package/src/browser/hooks/index.js +5 -0
  60. package/src/browser/interceptors/AntiDebugInterceptor.js +132 -0
  61. package/src/browser/interceptors/NetworkInterceptor.js +76 -12
  62. package/src/browser/interceptors/ScriptInterceptor.js +32 -7
  63. package/src/browser/interceptors/index.js +1 -0
  64. package/src/browser/ui/analysisPanel.js +541 -464
  65. package/src/cli/commands/config.js +11 -3
  66. package/src/config/paths.js +9 -1
  67. package/src/config/settings.js +7 -1
  68. package/src/core/PatchGenerator.js +24 -4
  69. package/src/core/Sandbox.js +140 -3
  70. package/src/env/EnvCodeGenerator.js +60 -88
  71. package/src/env/modules/bom/history.js +6 -0
  72. package/src/env/modules/bom/location.js +6 -0
  73. package/src/env/modules/bom/navigator.js +13 -0
  74. package/src/env/modules/bom/screen.js +6 -0
  75. package/src/env/modules/bom/storage.js +7 -0
  76. package/src/env/modules/dom/document.js +14 -0
  77. package/src/env/modules/dom/event.js +4 -0
  78. package/src/env/modules/index.js +27 -10
  79. package/src/env/modules/webapi/fetch.js +4 -0
  80. package/src/env/modules/webapi/url.js +4 -0
  81. package/src/env/modules/webapi/xhr.js +8 -0
  82. package/src/store/DataStore.js +125 -42
  83. package/src/store/Store.js +2 -1
  84. package/src/agent/subagents/dynamic.js +0 -64
  85. package/src/agent/subagents/env-agent.js +0 -82
  86. package/src/agent/subagents/sandbox.js +0 -55
  87. 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.panelBridge.reset();
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' && event.name === 'ChatOpenAI') {
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
- await this.panelBridge.flushPanelText();
87
- await this.panelBridge.finalizeMessage('assistant');
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' && event.name === 'ChatOpenAI') {
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
- await this.panelBridge.flushPanelText();
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
- await this.panelBridge.appendToPanel(chunk);
422
+ // 面板侧只累积,不推送
423
+ this.fullResponse = (this.fullResponse || '') + chunk;
202
424
  }
203
425
  break;
204
426
 
205
427
  case 'on_tool_start':
206
- this.debug('handleStreamEvent: 工具开始,先刷新缓冲区');
207
- await this.panelBridge.flushPanelText();
208
- this.panelBridge.hasStartedAssistantMsg = false;
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
- const resumeInput = `工具调用失败: ${errMsg}\n请检查参数格式并重试。`;
257
- return this.chatStream(resumeInput, retryCount + 1);
481
+ return this.chatStreamResume(retryCount + 1);
258
482
  }
259
483
  }
260
484
 
@@ -1,18 +1,29 @@
1
1
  /**
2
2
  * DeepSpider - DeepAgent 主入口
3
- * 基于 DeepAgents 最佳实践重构
3
+ * 使用 createAgent 手动组装 middleware 栈,替换 createDeepAgent
4
+ * 目的:用自定义 subagent middleware 支持 context 结构化传递
4
5
  */
5
6
 
6
7
  import 'dotenv/config';
7
- import { createDeepAgent, StateBackend, FilesystemBackend } from 'deepagents';
8
- import { ChatOpenAI } from '@langchain/openai';
9
- import { MemorySaver } from '@langchain/langgraph';
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
- * 使用 ChatOpenAI 兼容 OpenAI 格式的任意供应商
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
- return new ChatOpenAI({
85
+ // ChatAnthropic 的 baseURL 不含 /v1(SDK 自动拼接)
86
+ const anthropicBaseUrl = baseUrl?.replace(/\/v1\/?$/, '') || undefined;
87
+
88
+ return new ChatAnthropic({
36
89
  model,
37
- apiKey,
38
- configuration: baseUrl ? { baseURL: baseUrl } : undefined,
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
- // Checkpointer:保存对话状态,支持断点恢复
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 middleware = [
77
- createFilterToolsMiddleware(), // 过滤内置的 write_file/read_file
78
- createReportMiddleware({ onReportReady }),
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
- return createDeepAgent({
146
+ // 组装完整 middleware 栈(对照 createDeepAgent 源码 dist/index.js:3791-3826)
147
+ return createAgent({
82
148
  name: 'deepspider',
83
149
  model: llm,
84
150
  tools: coreTools,
85
- subagents: allSubagents,
86
- systemPrompt,
87
- backend,
88
- checkpointer,
89
- interruptOn,
90
- middleware,
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;