deepspider 0.3.0 → 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.
- package/.env.example +3 -0
- package/README.md +13 -13
- package/package.json +6 -6
- package/src/agent/core/PanelBridge.js +29 -77
- package/src/agent/core/StreamHandler.js +139 -14
- package/src/agent/index.js +51 -12
- package/src/agent/logger.js +184 -9
- package/src/agent/middleware/report.js +42 -16
- package/src/agent/middleware/subagent.js +233 -0
- package/src/agent/middleware/toolGuard.js +77 -0
- package/src/agent/middleware/validationWorkflow.js +171 -0
- package/src/agent/prompts/system.js +181 -59
- package/src/agent/run.js +41 -6
- 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/evolve.js +0 -3
- package/src/agent/skills/sandbox/SKILL.md +35 -0
- package/src/agent/skills/static-analysis/SKILL.md +98 -2
- package/src/agent/subagents/anti-detect.js +10 -20
- package/src/agent/subagents/captcha.js +7 -19
- package/src/agent/subagents/crawler.js +25 -37
- package/src/agent/subagents/factory.js +109 -9
- package/src/agent/subagents/index.js +4 -13
- package/src/agent/subagents/js2python.js +7 -19
- package/src/agent/subagents/reverse.js +180 -0
- package/src/agent/tools/analysis.js +84 -1
- package/src/agent/tools/anti-detect.js +5 -2
- package/src/agent/tools/browser.js +160 -0
- package/src/agent/tools/captcha.js +1 -1
- package/src/agent/tools/capture.js +24 -3
- package/src/agent/tools/correlate.js +129 -15
- package/src/agent/tools/crawler.js +2 -1
- package/src/agent/tools/crawlerGenerator.js +90 -0
- package/src/agent/tools/debug.js +43 -6
- package/src/agent/tools/evolve.js +6 -3
- package/src/agent/tools/extractor.js +5 -1
- package/src/agent/tools/file.js +16 -7
- package/src/agent/tools/generateHook.js +66 -0
- package/src/agent/tools/hookManager.js +19 -9
- package/src/agent/tools/index.js +33 -20
- package/src/agent/tools/nodejs.js +41 -6
- package/src/agent/tools/python.js +4 -4
- package/src/agent/tools/report.js +2 -2
- package/src/agent/tools/runtime.js +1 -1
- package/src/agent/tools/sandbox.js +21 -1
- package/src/agent/tools/scratchpad.js +70 -0
- package/src/agent/tools/tracing.js +26 -0
- package/src/agent/tools/verifyAlgorithm.js +117 -0
- package/src/analyzer/EncryptionAnalyzer.js +2 -2
- package/src/browser/EnvBridge.js +27 -13
- package/src/browser/client.js +124 -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 +77 -13
- package/src/browser/interceptors/ScriptInterceptor.js +34 -9
- package/src/browser/interceptors/index.js +1 -0
- package/src/browser/ui/analysisPanel.js +469 -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 +26 -6
- 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 +130 -47
- 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
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
|
-
│
|
|
154
|
-
│
|
|
160
|
+
│reverse-agent│ │captcha-agent│ │anti-detect │
|
|
161
|
+
│ 逆向分析 │ │ 验证码处理 │ │ 反检测 │
|
|
155
162
|
└──────┬──────┘ └─────────────┘ └─────────────┘
|
|
156
163
|
▼
|
|
157
164
|
┌─────────────┐
|
|
158
|
-
│
|
|
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
|
-
|
|
|
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.
|
|
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.
|
|
58
|
-
"@langchain/core": "^1.1.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
54
|
-
const
|
|
55
|
-
await this.evaluateInPage(
|
|
56
|
-
|
|
41
|
+
const escapedType = JSON.stringify(type);
|
|
42
|
+
const escapedData = JSON.stringify(data);
|
|
43
|
+
await this.evaluateInPage(
|
|
44
|
+
`window.__deepspider__?.addStructuredMessage?.(${escapedType}, ${escapedData})`
|
|
45
|
+
);
|
|
46
|
+
} catch {
|
|
57
47
|
// ignore
|
|
58
48
|
}
|
|
59
49
|
}
|
|
60
50
|
|
|
61
51
|
/**
|
|
62
|
-
*
|
|
52
|
+
* 按 role 发送消息到前端面板
|
|
63
53
|
*/
|
|
64
|
-
async
|
|
65
|
-
if (!
|
|
66
|
-
|
|
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 (e) {
|
|
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
|
|
131
|
-
await this.evaluateInPage(`
|
|
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.
|
|
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
|
-
|
|
87
|
-
await this.
|
|
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
|
-
|
|
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
|
-
|
|
323
|
+
// 面板侧只累积,不推送
|
|
324
|
+
this.fullResponse = (this.fullResponse || '') + chunk;
|
|
202
325
|
}
|
|
203
326
|
break;
|
|
204
327
|
|
|
205
328
|
case 'on_tool_start':
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
257
|
-
return this.chatStream(resumeInput, retryCount + 1);
|
|
382
|
+
return this.chatStreamResume(retryCount + 1);
|
|
258
383
|
}
|
|
259
384
|
}
|
|
260
385
|
|
package/src/agent/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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 { 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
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
92
|
+
// 组装完整 middleware 栈(对照 createDeepAgent 源码 dist/index.js:3791-3826)
|
|
93
|
+
return createAgent({
|
|
82
94
|
name: 'deepspider',
|
|
83
95
|
model: llm,
|
|
84
96
|
tools: coreTools,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|