deepspider 0.3.1 → 0.3.2

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 (78) hide show
  1. package/.env.example +3 -0
  2. package/README.md +13 -13
  3. package/package.json +6 -6
  4. package/src/agent/core/PanelBridge.js +28 -76
  5. package/src/agent/core/StreamHandler.js +139 -14
  6. package/src/agent/index.js +51 -12
  7. package/src/agent/logger.js +183 -8
  8. package/src/agent/middleware/report.js +41 -15
  9. package/src/agent/middleware/subagent.js +233 -0
  10. package/src/agent/middleware/toolGuard.js +77 -0
  11. package/src/agent/middleware/validationWorkflow.js +171 -0
  12. package/src/agent/prompts/system.js +181 -59
  13. package/src/agent/run.js +41 -6
  14. package/src/agent/skills/crawler/SKILL.md +64 -3
  15. package/src/agent/skills/crawler/evolved.md +9 -1
  16. package/src/agent/skills/dynamic-analysis/SKILL.md +74 -7
  17. package/src/agent/skills/env/SKILL.md +75 -0
  18. package/src/agent/skills/sandbox/SKILL.md +35 -0
  19. package/src/agent/skills/static-analysis/SKILL.md +98 -2
  20. package/src/agent/subagents/anti-detect.js +10 -20
  21. package/src/agent/subagents/captcha.js +7 -19
  22. package/src/agent/subagents/crawler.js +25 -37
  23. package/src/agent/subagents/factory.js +109 -9
  24. package/src/agent/subagents/index.js +4 -13
  25. package/src/agent/subagents/js2python.js +7 -19
  26. package/src/agent/subagents/reverse.js +180 -0
  27. package/src/agent/tools/analysis.js +84 -1
  28. package/src/agent/tools/anti-detect.js +5 -2
  29. package/src/agent/tools/browser.js +160 -0
  30. package/src/agent/tools/capture.js +24 -3
  31. package/src/agent/tools/correlate.js +129 -15
  32. package/src/agent/tools/crawler.js +2 -1
  33. package/src/agent/tools/crawlerGenerator.js +90 -0
  34. package/src/agent/tools/debug.js +43 -6
  35. package/src/agent/tools/evolve.js +5 -2
  36. package/src/agent/tools/extractor.js +5 -1
  37. package/src/agent/tools/file.js +14 -5
  38. package/src/agent/tools/generateHook.js +66 -0
  39. package/src/agent/tools/hookManager.js +19 -9
  40. package/src/agent/tools/index.js +33 -20
  41. package/src/agent/tools/nodejs.js +41 -6
  42. package/src/agent/tools/sandbox.js +21 -1
  43. package/src/agent/tools/scratchpad.js +70 -0
  44. package/src/agent/tools/tracing.js +26 -0
  45. package/src/agent/tools/verifyAlgorithm.js +117 -0
  46. package/src/browser/EnvBridge.js +27 -13
  47. package/src/browser/client.js +124 -18
  48. package/src/browser/collector.js +101 -22
  49. package/src/browser/defaultHooks.js +3 -1
  50. package/src/browser/hooks/index.js +5 -0
  51. package/src/browser/interceptors/AntiDebugInterceptor.js +132 -0
  52. package/src/browser/interceptors/NetworkInterceptor.js +76 -12
  53. package/src/browser/interceptors/ScriptInterceptor.js +32 -7
  54. package/src/browser/interceptors/index.js +1 -0
  55. package/src/browser/ui/analysisPanel.js +469 -464
  56. package/src/cli/commands/config.js +11 -3
  57. package/src/config/paths.js +9 -1
  58. package/src/config/settings.js +7 -1
  59. package/src/core/PatchGenerator.js +24 -4
  60. package/src/core/Sandbox.js +140 -3
  61. package/src/env/EnvCodeGenerator.js +60 -88
  62. package/src/env/modules/bom/history.js +6 -0
  63. package/src/env/modules/bom/location.js +6 -0
  64. package/src/env/modules/bom/navigator.js +13 -0
  65. package/src/env/modules/bom/screen.js +6 -0
  66. package/src/env/modules/bom/storage.js +7 -0
  67. package/src/env/modules/dom/document.js +14 -0
  68. package/src/env/modules/dom/event.js +4 -0
  69. package/src/env/modules/index.js +27 -10
  70. package/src/env/modules/webapi/fetch.js +4 -0
  71. package/src/env/modules/webapi/url.js +4 -0
  72. package/src/env/modules/webapi/xhr.js +8 -0
  73. package/src/store/DataStore.js +125 -42
  74. package/src/store/Store.js +2 -1
  75. package/src/agent/subagents/dynamic.js +0 -64
  76. package/src/agent/subagents/env-agent.js +0 -82
  77. package/src/agent/subagents/sandbox.js +0 -55
  78. package/src/agent/subagents/static.js +0 -66
package/.env.example CHANGED
@@ -5,6 +5,9 @@ DEEPSPIDER_API_KEY=your_api_key_here
5
5
  DEEPSPIDER_BASE_URL=https://api.openai.com/v1
6
6
  DEEPSPIDER_MODEL=gpt-4o
7
7
 
8
+ # 浏览器持久化(可选,保持登录态)
9
+ # DEEPSPIDER_PERSIST_BROWSER=true
10
+
8
11
  # LangSmith 追踪配置(可选)
9
12
  LANGSMITH_TRACING=true
10
13
  LANGSMITH_API_KEY=your_langsmith_api_key_here
package/README.md CHANGED
@@ -53,6 +53,7 @@ DeepSpider 需要配置 LLM API 才能运行。支持任何兼容 OpenAI 格式
53
53
  | `apiKey` | `DEEPSPIDER_API_KEY` | API 密钥 |
54
54
  | `baseUrl` | `DEEPSPIDER_BASE_URL` | API 地址 |
55
55
  | `model` | `DEEPSPIDER_MODEL` | 模型名称 |
56
+ | `persistBrowserData` | `DEEPSPIDER_PERSIST_BROWSER` | 持久化浏览器数据(保持登录态) |
56
57
 
57
58
  优先级:环境变量 > 配置文件 (`~/.deepspider/config/settings.json`) > 默认值
58
59
 
@@ -92,6 +93,9 @@ deepspider config set model deepseek-chat
92
93
  # 启动 Agent - 指定目标网站
93
94
  deepspider https://example.com
94
95
 
96
+ # 启动 Agent - 持久化浏览器数据(一次性)
97
+ deepspider --persist https://example.com
98
+
95
99
  # 启动 Agent - 纯交互模式
96
100
  deepspider
97
101
 
@@ -103,6 +107,9 @@ deepspider config list # 查看所有配置
103
107
  deepspider config set apiKey sk-xxx
104
108
  deepspider config set model gpt-4o
105
109
 
110
+ # 持久化浏览器数据(需要登录的网站,下次启动自动恢复登录态)
111
+ deepspider config set persistBrowserData true
112
+
106
113
  # 检查更新
107
114
  deepspider update
108
115
  ```
@@ -150,19 +157,14 @@ pnpm test
150
157
  ┌───────────────┼───────────────┐
151
158
  ▼ ▼ ▼
152
159
  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
153
- static-agent │ │captcha-agent│ │anti-detect │
154
- 静态分析 │ │ 验证码处理 │ │ 反检测 │
160
+ reverse-agent│ │captcha-agent│ │anti-detect │
161
+ 逆向分析 │ │ 验证码处理 │ │ 反检测 │
155
162
  └──────┬──────┘ └─────────────┘ └─────────────┘
156
163
 
157
164
  ┌─────────────┐
158
- dynamic-agent
159
- 动态调试
160
- └──────┬──────┘
161
-
162
- ┌─────────────┐ ┌─────────────┐
163
- │sandbox-agent│ ──▶ │js2python │
164
- │ 沙箱验证 │ │ 代码转换 │
165
- └─────────────┘ └─────────────┘
165
+ js2python
166
+ 代码转换
167
+ └─────────────┘
166
168
  ```
167
169
 
168
170
  ### 子代理体系
@@ -170,9 +172,7 @@ pnpm test
170
172
  | 子代理 | 职责 | 核心工具 |
171
173
  |--------|------|----------|
172
174
  | crawler | 爬虫编排:整合各模块、生成完整脚本 | file, store, crawler |
173
- | static | 静态分析:解包、反混淆、加密定位 | webcrack, deobfuscate, analyze |
174
- | dynamic | 动态分析:浏览器控制、Hook、数据采集 | browser, debug, capture |
175
- | sandbox | 沙箱执行:环境补全、代码执行 | sandbox, env, patch |
175
+ | reverse | 逆向分析全流程:反混淆、断点调试、Hook、沙箱验证、补环境 | tracing, deobfuscate, debug, capture, sandbox, env |
176
176
  | js2python | JS转Python:加密代码转换、验证 | python, analyzer |
177
177
  | captcha | 验证码处理:OCR、滑块、点选 | captcha_ocr, captcha_slide |
178
178
  | anti-detect | 反检测:指纹管理、代理池 | proxy, fingerprint |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepspider",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "智能爬虫工程平台 - 基于 DeepAgents + Patchright 的 AI 爬虫 Agent",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -19,7 +19,7 @@
19
19
  "cli": "node bin/cli.js",
20
20
  "mcp": "node src/mcp/server.js",
21
21
  "agent": "node bin/cli.js",
22
- "test": "node --test test/",
22
+ "test": "node --test 'test/*.test.js'",
23
23
  "lint": "eslint src/",
24
24
  "lint:fix": "eslint src/ --fix",
25
25
  "setup:crypto": "uv venv .venv --python 3.11 2>/dev/null || true && uv pip install -r requirements-crypto.txt",
@@ -54,20 +54,20 @@
54
54
  "@babel/parser": "^7.28.6",
55
55
  "@babel/traverse": "^7.28.6",
56
56
  "@babel/types": "^7.28.6",
57
- "@langchain/anthropic": "^1.3.12",
58
- "@langchain/core": "^1.1.17",
57
+ "@langchain/anthropic": "^1.3.17",
58
+ "@langchain/core": "^1.1.24",
59
59
  "@langchain/langgraph": "^1.1.2",
60
60
  "@langchain/openai": "^1.2.3",
61
61
  "@modelcontextprotocol/sdk": "^1.26.0",
62
62
  "crypto-js": "^4.2.0",
63
- "deepagents": "^1.6.0",
63
+ "deepagents": "^1.7.6",
64
64
  "dotenv": "^17.2.3",
65
65
  "hono": "4.11.7",
66
66
  "isolated-vm": "^6.0.2",
67
67
  "js-md5": "^0.8.3",
68
68
  "js-sha256": "^0.11.1",
69
69
  "jsencrypt": "^3.5.4",
70
- "langchain": "^1.2.15",
70
+ "langchain": "^1.2.24",
71
71
  "marked": "^17.0.1",
72
72
  "patchright": "^1.57.0",
73
73
  "sm-crypto": "^0.4.0",
@@ -1,22 +1,12 @@
1
1
  /**
2
2
  * DeepSpider - 面板通信桥接
3
- * 处理与浏览器面板的消息通信
3
+ * 处理与浏览器面板的结构化消息通信
4
4
  */
5
5
 
6
6
  export class PanelBridge {
7
7
  constructor(browserGetter, debugFn = () => {}) {
8
8
  this.getBrowser = browserGetter;
9
9
  this.debug = debugFn;
10
- this.textBuffer = '';
11
- this.hasStartedAssistantMsg = false;
12
- }
13
-
14
- /**
15
- * 重置状态
16
- */
17
- reset() {
18
- this.textBuffer = '';
19
- this.hasStartedAssistantMsg = false;
20
10
  }
21
11
 
22
12
  /**
@@ -40,81 +30,31 @@ export class PanelBridge {
40
30
  }
41
31
 
42
32
  /**
43
- * 发送消息到前端面板
33
+ * 发送结构化消息到前端面板
44
34
  */
45
- async sendToPanel(role, content) {
46
- if (!content?.trim()) return;
47
-
35
+ async sendMessage(type, data) {
48
36
  const browser = this.getBrowser();
49
37
  const page = browser?.getPage?.();
50
38
  if (!page) return;
51
39
 
52
40
  try {
53
- const escaped = JSON.stringify(content.trim());
54
- const code = `window.__deepspider__?.addMessage?.('${role}', ${escaped})`;
55
- await this.evaluateInPage(code);
41
+ const escapedType = JSON.stringify(type);
42
+ const escapedData = JSON.stringify(data);
43
+ await this.evaluateInPage(
44
+ `window.__deepspider__?.addStructuredMessage?.(${escapedType}, ${escapedData})`
45
+ );
56
46
  } catch {
57
47
  // ignore
58
48
  }
59
49
  }
60
50
 
61
51
  /**
62
- * 累积文本到缓冲区(用于 LLM 流式输出)
52
+ * role 发送消息到前端面板
63
53
  */
64
- async appendToPanel(text) {
65
- if (!text) return;
66
- this.textBuffer += text;
67
-
68
- // 每累积一定量或遇到换行时刷新
69
- if (this.textBuffer.length > 200 || text.includes('\n')) {
70
- await this.flushPanelText();
71
- }
72
- }
73
-
74
- /**
75
- * 刷新累积的文本到面板
76
- */
77
- async flushPanelText() {
78
- if (!this.textBuffer.trim()) return;
79
-
80
- const browser = this.getBrowser();
81
- const page = browser?.getPage?.();
82
- if (!page) {
83
- this.textBuffer = '';
84
- return;
85
- }
86
-
87
- try {
88
- const content = this.textBuffer.trim();
89
- const escaped = JSON.stringify(content);
90
-
91
- if (!this.hasStartedAssistantMsg) {
92
- const code = `(function() {
93
- const fn = window.__deepspider__?.addMessage;
94
- if (typeof fn === 'function') {
95
- fn('assistant', ${escaped});
96
- return { ok: true };
97
- }
98
- return { ok: false };
99
- })()`;
100
- await this.evaluateInPage(code);
101
- this.hasStartedAssistantMsg = true;
102
- } else {
103
- const code = `(function() {
104
- const fn = window.__deepspider__?.appendToLastMessage;
105
- if (typeof fn === 'function') {
106
- fn('assistant', ${escaped});
107
- return { ok: true };
108
- }
109
- return { ok: false };
110
- })()`;
111
- await this.evaluateInPage(code);
112
- }
113
- } catch {
114
- // ignore
115
- }
116
-
117
- this.textBuffer = '';
54
+ async sendToPanel(role, content) {
55
+ if (!content?.trim()) return;
56
+ const type = role === 'system' ? 'system' : role === 'user' ? 'user' : 'text';
57
+ await this.sendMessage(type, { content: content.trim() });
118
58
  }
119
59
 
120
60
  /**
@@ -125,9 +65,21 @@ export class PanelBridge {
125
65
  }
126
66
 
127
67
  /**
128
- * 完成消息,触发渲染
68
+ * 删除面板中最后一条 assistant 消息
69
+ * 用于 interrupt 场景:LLM 在调用 interrupt 工具前输出的冗余描述文字需要清除
129
70
  */
130
- async finalizeMessage(role) {
131
- await this.evaluateInPage(`window.__deepspider__?.finalizeMessage?.("${role}")`);
71
+ async removeLastAssistantMessage() {
72
+ await this.evaluateInPage(`
73
+ (function() {
74
+ const ds = window.__deepspider__;
75
+ if (!ds?.chatMessages) return;
76
+ for (let i = ds.chatMessages.length - 1; i >= 0; i--) {
77
+ if (ds.chatMessages[i].role === 'assistant') {
78
+ ds.chatMessages.splice(i, 1);
79
+ break;
80
+ }
81
+ }
82
+ })()
83
+ `);
132
84
  }
133
85
  }
@@ -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
 
@@ -26,6 +27,7 @@ export class StreamHandler {
26
27
  this.riskTools = riskTools;
27
28
  this.debug = debug;
28
29
  this.retryManager = new RetryManager();
30
+ this.fullResponse = '';
29
31
  }
30
32
 
31
33
  /**
@@ -37,8 +39,8 @@ export class StreamHandler {
37
39
  let eventCount = 0;
38
40
  let lastToolCall = null;
39
41
 
40
- // 重置面板状态
41
- this.panelBridge.reset();
42
+ // 重置状态
43
+ this.fullResponse = '';
42
44
  await this.panelBridge.setBusy(true);
43
45
 
44
46
  this.debug(`chatStream: 开始处理, 输入长度=${input.length}`);
@@ -83,8 +85,12 @@ export class StreamHandler {
83
85
  clearInterval(heartbeat);
84
86
  console.log(`\n[完成] 共处理 ${eventCount} 个事件`);
85
87
 
86
- await this.panelBridge.flushPanelText();
87
- await this.panelBridge.finalizeMessage('assistant');
88
+ // 发送剩余累积文本
89
+ await this._flushFullResponse();
90
+
91
+ // 检测 interrupt 并渲染到面板
92
+ await this._checkAndRenderInterrupt();
93
+
88
94
  await this.panelBridge.setBusy(false);
89
95
 
90
96
  this.debug(`chatStream: 完成, 响应长度=${finalResponse.length}`);
@@ -96,13 +102,69 @@ export class StreamHandler {
96
102
  }
97
103
 
98
104
  /**
99
- * 从检查点恢复流式对话
105
+ * 用 Command({ resume }) 恢复被 interrupt 暂停的 graph
106
+ */
107
+ async resumeInterrupt(value) {
108
+ let finalResponse = '';
109
+ let lastEventTime = Date.now();
110
+ let eventCount = 0;
111
+
112
+ this.fullResponse = '';
113
+ await this.panelBridge.setBusy(true);
114
+ this.debug(`resumeInterrupt: 恢复 interrupt, value=${value}`);
115
+
116
+ const heartbeat = setInterval(() => {
117
+ const elapsed = Math.round((Date.now() - lastEventTime) / 1000);
118
+ if (elapsed > 30) {
119
+ console.log(`\n[心跳] 恢复中,已等待 ${elapsed}s`);
120
+ }
121
+ }, 30000);
122
+
123
+ try {
124
+ const eventStream = await this.agent.streamEvents(
125
+ new Command({ resume: value }),
126
+ { ...this.config, version: 'v2' }
127
+ );
128
+
129
+ for await (const event of eventStream) {
130
+ lastEventTime = Date.now();
131
+ eventCount++;
132
+ await this._handleStreamEvent(event);
133
+
134
+ if (event.event === 'on_chat_model_end' && event.name === 'ChatOpenAI') {
135
+ const output = event.data?.output;
136
+ if (output?.content) {
137
+ finalResponse = output.content;
138
+ }
139
+ }
140
+ }
141
+
142
+ clearInterval(heartbeat);
143
+
144
+ await this._flushFullResponse();
145
+ await this._checkAndRenderInterrupt();
146
+ await this.panelBridge.setBusy(false);
147
+
148
+ console.log(`\n[恢复完成] 共处理 ${eventCount} 个事件`);
149
+ return finalResponse || '[无响应]';
150
+ } catch (error) {
151
+ clearInterval(heartbeat);
152
+ await this.panelBridge.setBusy(false);
153
+ const errMsg = error.message || String(error);
154
+ console.error(`\n[恢复失败] ${errMsg}`);
155
+ return `恢复失败: ${errMsg}`;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * 从检查点恢复流式对话(错误重试用)
100
161
  */
101
162
  async chatStreamResume(retryCount = 0) {
102
163
  let finalResponse = '';
103
164
  let lastEventTime = Date.now();
104
165
  let eventCount = 0;
105
166
 
167
+ this.fullResponse = '';
106
168
  await this.panelBridge.setBusy(true);
107
169
  this.debug(`chatStreamResume: 从检查点恢复, retryCount=${retryCount}`);
108
170
 
@@ -133,8 +195,11 @@ export class StreamHandler {
133
195
  }
134
196
 
135
197
  clearInterval(heartbeat);
136
- await this.panelBridge.flushPanelText();
198
+
199
+ await this._flushFullResponse();
200
+ await this._checkAndRenderInterrupt();
137
201
  await this.panelBridge.setBusy(false);
202
+
138
203
  console.log(`\n[恢复完成] 共处理 ${eventCount} 个事件`);
139
204
  return finalResponse || '[无响应]';
140
205
  } catch (error) {
@@ -154,6 +219,62 @@ export class StreamHandler {
154
219
  }
155
220
  }
156
221
 
222
+ /**
223
+ * 发送剩余累积文本到面板
224
+ */
225
+ async _flushFullResponse() {
226
+ if (this.fullResponse?.trim()) {
227
+ await this.panelBridge.sendToPanel('assistant', this.fullResponse);
228
+ }
229
+ this.fullResponse = '';
230
+ }
231
+
232
+ /**
233
+ * 检测 graph interrupt 状态,将 interrupt payload 渲染为面板结构化消息
234
+ *
235
+ * interrupt payload 协议:
236
+ * { type: 'choices', question, options: [{id, label, description?}] }
237
+ * { type: 'confirm', question, confirmText?, cancelText? }
238
+ */
239
+ async _checkAndRenderInterrupt() {
240
+ try {
241
+ const state = await this.agent.getState(this.config);
242
+ this.debug(`_checkAndRenderInterrupt: state.next=${JSON.stringify(state?.next)}, tasks=${state?.tasks?.length ?? 'undefined'}`);
243
+
244
+ if (!state?.tasks) {
245
+ this.debug('_checkAndRenderInterrupt: state.tasks 为空,尝试检查 next');
246
+ // 某些 LangGraph 版本用 next 为空数组表示 interrupt
247
+ // 如果 next 不为空,说明 graph 正常结束,无 interrupt
248
+ return false;
249
+ }
250
+
251
+ let found = false;
252
+ for (const task of state.tasks) {
253
+ this.debug(`_checkAndRenderInterrupt: task id=${task.id}, interrupts=${task.interrupts?.length ?? 0}`);
254
+ if (!task.interrupts?.length) continue;
255
+ for (const intr of task.interrupts) {
256
+ const payload = intr.value;
257
+ this.debug(`_checkAndRenderInterrupt: interrupt payload=${JSON.stringify(payload)?.slice(0, 200)}`);
258
+ if (!payload?.type) continue;
259
+
260
+ console.log(`\n[交互] 等待用户 ${payload.type === 'choices' ? '选择' : '确认'}...`);
261
+
262
+ if (payload.type === 'choices' || payload.type === 'confirm') {
263
+ // 删除面板中 interrupt 工具调用前 LLM 输出的冗余描述文字
264
+ await this.panelBridge.removeLastAssistantMessage();
265
+ await this.panelBridge.sendMessage(payload.type, payload);
266
+ found = true;
267
+ }
268
+ }
269
+ }
270
+ return found;
271
+ } catch (e) {
272
+ this.debug('_checkAndRenderInterrupt 失败:', e.message);
273
+ console.log(`[DEBUG] _checkAndRenderInterrupt error: ${e.message}`);
274
+ return false;
275
+ }
276
+ }
277
+
157
278
  /**
158
279
  * 创建心跳检测定时器
159
280
  */
@@ -197,15 +318,20 @@ export class StreamHandler {
197
318
  let chunk = data?.chunk?.content;
198
319
  if (chunk && typeof chunk === 'string') {
199
320
  chunk = cleanDSML(chunk);
321
+ // CLI 侧仍流式输出
200
322
  process.stdout.write(chunk);
201
- await this.panelBridge.appendToPanel(chunk);
323
+ // 面板侧只累积,不推送
324
+ this.fullResponse = (this.fullResponse || '') + chunk;
202
325
  }
203
326
  break;
204
327
 
205
328
  case 'on_tool_start':
206
- this.debug('handleStreamEvent: 工具开始,先刷新缓冲区');
207
- await this.panelBridge.flushPanelText();
208
- this.panelBridge.hasStartedAssistantMsg = false;
329
+ // 工具调用前,先把已累积的 LLM 文字发送到面板
330
+ if (this.fullResponse?.trim()) {
331
+ await this.panelBridge.sendToPanel('assistant', this.fullResponse);
332
+ this.fullResponse = '';
333
+ }
334
+ this.debug('handleStreamEvent: 工具开始');
209
335
  const input = data?.input || {};
210
336
  const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
211
337
  const preview = inputStr.length > 100 ? inputStr.slice(0, 100) + '...' : inputStr;
@@ -250,11 +376,10 @@ export class StreamHandler {
250
376
  }
251
377
 
252
378
  if (isToolSchemaError(errMsg)) {
253
- console.log(`\n[重试 ${retryCount + 1}/${this.retryManager.maxRetries}] 工具参数错误,发送修正请求...`);
379
+ console.log(`\n[重试 ${retryCount + 1}/${this.retryManager.maxRetries}] 工具参数错误,从检查点恢复...`);
254
380
  await this.panelBridge.sendToPanel('system',
255
381
  `工具调用失败,正在修正 (${retryCount + 1}/${this.retryManager.maxRetries})`);
256
- const resumeInput = `工具调用失败: ${errMsg}\n请检查参数格式并重试。`;
257
- return this.chatStream(resumeInput, retryCount + 1);
382
+ return this.chatStreamResume(retryCount + 1);
258
383
  }
259
384
  }
260
385
 
@@ -1,10 +1,12 @@
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 { StateBackend, FilesystemBackend, createFilesystemMiddleware, createPatchToolCallsMiddleware } from 'deepagents';
9
+ import { createAgent, toolRetryMiddleware, summarizationMiddleware, anthropicPromptCachingMiddleware, todoListMiddleware, humanInTheLoopMiddleware } from 'langchain';
8
10
  import { ChatOpenAI } from '@langchain/openai';
9
11
  import { MemorySaver } from '@langchain/langgraph';
10
12
 
@@ -13,6 +15,12 @@ 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 { createValidationWorkflowMiddleware } from './middleware/validationWorkflow.js';
21
+
22
+ // createDeepAgent 内部拼接的 BASE_PROMPT
23
+ 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
24
 
17
25
  // 从环境变量读取配置
18
26
  const config = {
@@ -72,22 +80,53 @@ export function createDeepSpiderAgent(options = {}) {
72
80
  }
73
81
  : undefined;
74
82
 
75
- // 中间件配置
76
- const middleware = [
77
- createFilterToolsMiddleware(), // 过滤内置的 write_file/read_file
78
- createReportMiddleware({ onReportReady }),
83
+ // 框架级子代理默认中间件(对照 createDeepAgent 内部的 subagentMiddleware)
84
+ const subagentDefaultMiddleware = [
85
+ todoListMiddleware(),
86
+ createFilesystemMiddleware({ backend }),
87
+ summarizationMiddleware({ model: llm, trigger: { tokens: 170000 }, keep: { messages: 6 } }),
88
+ anthropicPromptCachingMiddleware({ unsupportedModelBehavior: 'ignore' }),
89
+ createPatchToolCallsMiddleware(),
79
90
  ];
80
91
 
81
- return createDeepAgent({
92
+ // 组装完整 middleware 栈(对照 createDeepAgent 源码 dist/index.js:3791-3826)
93
+ return createAgent({
82
94
  name: 'deepspider',
83
95
  model: llm,
84
96
  tools: coreTools,
85
- subagents: allSubagents,
86
- systemPrompt,
87
- backend,
97
+ systemPrompt: `${systemPrompt}\n\n${BASE_PROMPT}`,
98
+ middleware: [
99
+ // === 框架内置 middleware ===
100
+ todoListMiddleware(),
101
+ createFilesystemMiddleware({ backend }),
102
+ createCustomSubAgentMiddleware({
103
+ defaultModel: llm,
104
+ defaultTools: coreTools,
105
+ subagents: allSubagents,
106
+ defaultMiddleware: subagentDefaultMiddleware,
107
+ generalPurposeAgent: false,
108
+ defaultInterruptOn: interruptOn,
109
+ }),
110
+ summarizationMiddleware({ model: llm, trigger: { tokens: 170000 }, keep: { messages: 6 } }),
111
+ anthropicPromptCachingMiddleware({ unsupportedModelBehavior: 'ignore' }),
112
+ createPatchToolCallsMiddleware(),
113
+ // === HITL(如果启用)===
114
+ ...(interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []),
115
+ // === 自定义 middleware ===
116
+ toolRetryMiddleware({
117
+ maxRetries: 0,
118
+ onFailure: (err) => {
119
+ // GraphInterrupt / ParentCommand 等 LangGraph 内部控制流异常必须透传,不能吞掉
120
+ if (err?.is_bubble_up === true) throw err;
121
+ return `Tool call failed: ${err.message}\nPlease fix the arguments and retry.`;
122
+ },
123
+ }),
124
+ createToolGuardMiddleware(),
125
+ createFilterToolsMiddleware(),
126
+ createValidationWorkflowMiddleware(),
127
+ createReportMiddleware({ onReportReady }),
128
+ ],
88
129
  checkpointer,
89
- interruptOn,
90
- middleware,
91
130
  });
92
131
  }
93
132