deepspider 0.2.11 → 0.2.12
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/.trellis/spec/backend/directory-structure.md +21 -2
- package/.trellis/spec/backend/quality-guidelines.md +75 -0
- package/README.md +25 -4
- package/docs/GUIDE.md +40 -36
- package/docs/PROMPT.md +0 -1
- package/docs/USAGE.md +5 -1
- package/package.json +1 -1
- package/src/agent/core/PanelBridge.js +133 -0
- package/src/agent/core/RetryManager.js +51 -0
- package/src/agent/core/StreamHandler.js +263 -0
- package/src/agent/core/index.js +7 -0
- package/src/agent/errors/ErrorClassifier.js +43 -0
- package/src/agent/errors/SpiderError.js +68 -0
- package/src/agent/errors/index.js +19 -0
- package/src/agent/run.js +29 -413
- package/src/agent/subagents/factory.js +60 -0
- package/src/agent/subagents/index.js +3 -0
- package/src/agent/tools/report.js +36 -4
- package/src/browser/client.js +47 -10
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSpider - 流式输出处理器
|
|
3
|
+
* 处理 Agent 的流式事件
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isApiServiceError, isToolSchemaError } from '../errors/ErrorClassifier.js';
|
|
7
|
+
import { RetryManager, sleep } from './RetryManager.js';
|
|
8
|
+
|
|
9
|
+
// DeepSeek 特殊标记清理
|
|
10
|
+
const DSML_PATTERN = /|DSML|/g;
|
|
11
|
+
function cleanDSML(text) {
|
|
12
|
+
return text ? text.replace(DSML_PATTERN, '') : text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 人工介入配置
|
|
16
|
+
const INTERVENTION_CONFIG = {
|
|
17
|
+
idleTimeoutMs: 120000, // 2分钟无响应触发提示
|
|
18
|
+
checkIntervalMs: 30000, // 30秒检测一次
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class StreamHandler {
|
|
22
|
+
constructor({ agent, config, panelBridge, riskTools = [], debug = () => {} }) {
|
|
23
|
+
this.agent = agent;
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.panelBridge = panelBridge;
|
|
26
|
+
this.riskTools = riskTools;
|
|
27
|
+
this.debug = debug;
|
|
28
|
+
this.retryManager = new RetryManager();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 流式对话 - 显示思考过程(带重试)
|
|
33
|
+
*/
|
|
34
|
+
async chatStream(input, retryCount = 0) {
|
|
35
|
+
let finalResponse = '';
|
|
36
|
+
let lastEventTime = Date.now();
|
|
37
|
+
let eventCount = 0;
|
|
38
|
+
let lastToolCall = null;
|
|
39
|
+
|
|
40
|
+
// 重置面板状态
|
|
41
|
+
this.panelBridge.reset();
|
|
42
|
+
await this.panelBridge.setBusy(true);
|
|
43
|
+
|
|
44
|
+
this.debug(`chatStream: 开始处理, 输入长度=${input.length}`);
|
|
45
|
+
|
|
46
|
+
// 心跳检测
|
|
47
|
+
let interventionNotified = false;
|
|
48
|
+
const heartbeat = this._createHeartbeat(
|
|
49
|
+
() => lastEventTime,
|
|
50
|
+
() => eventCount,
|
|
51
|
+
() => lastToolCall,
|
|
52
|
+
() => interventionNotified,
|
|
53
|
+
(v) => { interventionNotified = v; }
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
this.debug('chatStream: 创建事件流');
|
|
58
|
+
const eventStream = await this.agent.streamEvents(
|
|
59
|
+
{ messages: [{ role: 'user', content: input }] },
|
|
60
|
+
{ ...this.config, version: 'v2' }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
this.debug('chatStream: 开始遍历事件');
|
|
64
|
+
for await (const event of eventStream) {
|
|
65
|
+
lastEventTime = Date.now();
|
|
66
|
+
eventCount++;
|
|
67
|
+
|
|
68
|
+
if (event.event === 'on_tool_start') {
|
|
69
|
+
lastToolCall = event.name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await this._handleStreamEvent(event);
|
|
73
|
+
|
|
74
|
+
if (event.event === 'on_chat_model_end' && event.name === 'ChatOpenAI') {
|
|
75
|
+
const output = event.data?.output;
|
|
76
|
+
if (output?.content) {
|
|
77
|
+
finalResponse = output.content;
|
|
78
|
+
this.debug(`chatStream: 收到最终响应, 长度=${finalResponse.length}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
clearInterval(heartbeat);
|
|
84
|
+
console.log(`\n[完成] 共处理 ${eventCount} 个事件`);
|
|
85
|
+
|
|
86
|
+
await this.panelBridge.flushPanelText();
|
|
87
|
+
await this.panelBridge.finalizeMessage('assistant');
|
|
88
|
+
await this.panelBridge.setBusy(false);
|
|
89
|
+
|
|
90
|
+
this.debug(`chatStream: 完成, 响应长度=${finalResponse.length}`);
|
|
91
|
+
return finalResponse || '[无响应]';
|
|
92
|
+
} catch (error) {
|
|
93
|
+
clearInterval(heartbeat);
|
|
94
|
+
return this._handleError(error, input, eventCount, lastToolCall, retryCount);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 从检查点恢复流式对话
|
|
100
|
+
*/
|
|
101
|
+
async chatStreamResume(retryCount = 0) {
|
|
102
|
+
let finalResponse = '';
|
|
103
|
+
let lastEventTime = Date.now();
|
|
104
|
+
let eventCount = 0;
|
|
105
|
+
|
|
106
|
+
await this.panelBridge.setBusy(true);
|
|
107
|
+
this.debug(`chatStreamResume: 从检查点恢复, retryCount=${retryCount}`);
|
|
108
|
+
|
|
109
|
+
const heartbeat = setInterval(() => {
|
|
110
|
+
const elapsed = Math.round((Date.now() - lastEventTime) / 1000);
|
|
111
|
+
if (elapsed > 30) {
|
|
112
|
+
console.log(`\n[心跳] 恢复中,已等待 ${elapsed}s`);
|
|
113
|
+
}
|
|
114
|
+
}, 30000);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const eventStream = await this.agent.streamEvents(
|
|
118
|
+
{ messages: [] },
|
|
119
|
+
{ ...this.config, version: 'v2' }
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
for await (const event of eventStream) {
|
|
123
|
+
lastEventTime = Date.now();
|
|
124
|
+
eventCount++;
|
|
125
|
+
await this._handleStreamEvent(event);
|
|
126
|
+
|
|
127
|
+
if (event.event === 'on_chat_model_end' && event.name === 'ChatOpenAI') {
|
|
128
|
+
const output = event.data?.output;
|
|
129
|
+
if (output?.content) {
|
|
130
|
+
finalResponse = output.content;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
clearInterval(heartbeat);
|
|
136
|
+
await this.panelBridge.flushPanelText();
|
|
137
|
+
await this.panelBridge.setBusy(false);
|
|
138
|
+
console.log(`\n[恢复完成] 共处理 ${eventCount} 个事件`);
|
|
139
|
+
return finalResponse || '[无响应]';
|
|
140
|
+
} catch (error) {
|
|
141
|
+
clearInterval(heartbeat);
|
|
142
|
+
await this.panelBridge.setBusy(false);
|
|
143
|
+
const errMsg = error.message || String(error);
|
|
144
|
+
console.error(`\n[恢复失败] ${errMsg}`);
|
|
145
|
+
|
|
146
|
+
if (isApiServiceError(errMsg) && this.retryManager.canRetry(retryCount)) {
|
|
147
|
+
const delay = this.retryManager.getDelay(retryCount);
|
|
148
|
+
console.log(`\n[重试 ${retryCount + 1}/${this.retryManager.maxRetries}] ${delay}ms 后再次恢复...`);
|
|
149
|
+
await sleep(delay);
|
|
150
|
+
return this.chatStreamResume(retryCount + 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `恢复失败: ${errMsg}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 创建心跳检测定时器
|
|
159
|
+
*/
|
|
160
|
+
_createHeartbeat(getLastEventTime, getEventCount, getLastToolCall, getNotified, setNotified) {
|
|
161
|
+
return setInterval(() => {
|
|
162
|
+
const elapsed = Math.round((Date.now() - getLastEventTime()) / 1000);
|
|
163
|
+
if (elapsed > 30) {
|
|
164
|
+
console.log(`\n[心跳] 已等待 ${elapsed}s, 事件数=${getEventCount()}, 最后工具=${getLastToolCall() || '无'}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const isRiskTool = getLastToolCall() && this.riskTools.includes(getLastToolCall());
|
|
168
|
+
if (elapsed * 1000 > INTERVENTION_CONFIG.idleTimeoutMs && !getNotified() && isRiskTool) {
|
|
169
|
+
setNotified(true);
|
|
170
|
+
const msg = '⚠️ 页面操作后长时间无响应,可能遇到验证码或风控,请检查浏览器';
|
|
171
|
+
console.log('\n[提示] ' + msg);
|
|
172
|
+
this.panelBridge.sendToPanel('system', msg).catch(() => {});
|
|
173
|
+
}
|
|
174
|
+
}, INTERVENTION_CONFIG.checkIntervalMs);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 处理流式事件
|
|
179
|
+
*/
|
|
180
|
+
async _handleStreamEvent(event) {
|
|
181
|
+
const { event: eventType, name, data } = event;
|
|
182
|
+
|
|
183
|
+
// 过滤内部事件
|
|
184
|
+
if (name?.startsWith('ChannelWrite') ||
|
|
185
|
+
name?.startsWith('Branch') ||
|
|
186
|
+
name?.includes('Middleware') ||
|
|
187
|
+
name === 'RunnableSequence' ||
|
|
188
|
+
name === 'model_request' ||
|
|
189
|
+
name === 'tools') {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.debug(`handleStreamEvent: ${eventType}, name=${name}`);
|
|
194
|
+
|
|
195
|
+
switch (eventType) {
|
|
196
|
+
case 'on_chat_model_stream':
|
|
197
|
+
let chunk = data?.chunk?.content;
|
|
198
|
+
if (chunk && typeof chunk === 'string') {
|
|
199
|
+
chunk = cleanDSML(chunk);
|
|
200
|
+
process.stdout.write(chunk);
|
|
201
|
+
await this.panelBridge.appendToPanel(chunk);
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case 'on_tool_start':
|
|
206
|
+
this.debug('handleStreamEvent: 工具开始,先刷新缓冲区');
|
|
207
|
+
await this.panelBridge.flushPanelText();
|
|
208
|
+
this.panelBridge.hasStartedAssistantMsg = false;
|
|
209
|
+
const input = data?.input || {};
|
|
210
|
+
const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
|
|
211
|
+
const preview = inputStr.length > 100 ? inputStr.slice(0, 100) + '...' : inputStr;
|
|
212
|
+
console.log(`\n[调用] ${name}(${preview})`);
|
|
213
|
+
await this.panelBridge.sendToPanel('system', `[调用] ${name}`);
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case 'on_tool_end':
|
|
217
|
+
const output = data?.output;
|
|
218
|
+
let result = '';
|
|
219
|
+
this.debug(`on_tool_end: name=${name}, output type=${typeof output}`);
|
|
220
|
+
|
|
221
|
+
if (typeof output === 'string') {
|
|
222
|
+
result = output.slice(0, 80);
|
|
223
|
+
} else if (output?.content) {
|
|
224
|
+
result = String(output.content).slice(0, 80);
|
|
225
|
+
}
|
|
226
|
+
if (result) {
|
|
227
|
+
console.log(`[结果] ${result}${result.length >= 80 ? '...' : ''}`);
|
|
228
|
+
await this.panelBridge.sendToPanel('system', `[结果] ${result.slice(0, 50)}${result.length > 50 ? '...' : ''}`);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 处理错误和重试
|
|
236
|
+
*/
|
|
237
|
+
async _handleError(error, input, eventCount, lastToolCall, retryCount) {
|
|
238
|
+
const errMsg = error.message || String(error);
|
|
239
|
+
await this.panelBridge.setBusy(false);
|
|
240
|
+
console.error(`\n[异常] 事件数=${eventCount}, 最后工具=${lastToolCall || '无'}, 错误: ${errMsg}`);
|
|
241
|
+
|
|
242
|
+
if (this.retryManager.canRetry(retryCount)) {
|
|
243
|
+
if (isApiServiceError(errMsg)) {
|
|
244
|
+
const delay = this.retryManager.getDelay(retryCount);
|
|
245
|
+
console.log(`\n[重试 ${retryCount + 1}/${this.retryManager.maxRetries}] API错误,${delay}ms 后从检查点恢复...`);
|
|
246
|
+
await this.panelBridge.sendToPanel('system',
|
|
247
|
+
`服务暂时不可用,${Math.round(delay/1000)}s 后重试 (${retryCount + 1}/${this.retryManager.maxRetries})`);
|
|
248
|
+
await sleep(delay);
|
|
249
|
+
return this.chatStreamResume(retryCount + 1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (isToolSchemaError(errMsg)) {
|
|
253
|
+
console.log(`\n[重试 ${retryCount + 1}/${this.retryManager.maxRetries}] 工具参数错误,发送修正请求...`);
|
|
254
|
+
await this.panelBridge.sendToPanel('system',
|
|
255
|
+
`工具调用失败,正在修正 (${retryCount + 1}/${this.retryManager.maxRetries})`);
|
|
256
|
+
const resumeInput = `工具调用失败: ${errMsg}\n请检查参数格式并重试。`;
|
|
257
|
+
return this.chatStream(resumeInput, retryCount + 1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return `错误: ${errMsg}`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSpider - 错误分类器
|
|
3
|
+
* 判断错误类型,决定重试策略
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 判断是否为工具参数错误(需要 LLM 修正)
|
|
8
|
+
*/
|
|
9
|
+
export function isToolSchemaError(errMsg) {
|
|
10
|
+
return /did not match expected schema|Invalid input|tool input/i.test(errMsg);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 判断是否为 API 服务错误(可直接重试)
|
|
15
|
+
*/
|
|
16
|
+
export function isApiServiceError(errMsg) {
|
|
17
|
+
return /503|502|429|rate limit|无可用渠道|timeout|ECONNRESET|ETIMEDOUT/i.test(errMsg);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 判断是否为浏览器错误
|
|
22
|
+
*/
|
|
23
|
+
export function isBrowserError(errMsg) {
|
|
24
|
+
return /Target closed|page closed|context closed|browser disconnected/i.test(errMsg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 判断是否为网络错误
|
|
29
|
+
*/
|
|
30
|
+
export function isNetworkError(errMsg) {
|
|
31
|
+
return /ENOTFOUND|ECONNREFUSED|network|fetch failed/i.test(errMsg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 获取错误类型
|
|
36
|
+
*/
|
|
37
|
+
export function classifyError(errMsg) {
|
|
38
|
+
if (isToolSchemaError(errMsg)) return 'tool_schema';
|
|
39
|
+
if (isApiServiceError(errMsg)) return 'api_service';
|
|
40
|
+
if (isBrowserError(errMsg)) return 'browser';
|
|
41
|
+
if (isNetworkError(errMsg)) return 'network';
|
|
42
|
+
return 'unknown';
|
|
43
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSpider - 结构化错误类型
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 基础错误类
|
|
7
|
+
*/
|
|
8
|
+
export class SpiderError extends Error {
|
|
9
|
+
constructor(message, code, details = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'SpiderError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
toJSON() {
|
|
17
|
+
return {
|
|
18
|
+
name: this.name,
|
|
19
|
+
code: this.code,
|
|
20
|
+
message: this.message,
|
|
21
|
+
details: this.details,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* API 服务错误 - 503/502/429 等
|
|
28
|
+
*/
|
|
29
|
+
export class ApiServiceError extends SpiderError {
|
|
30
|
+
constructor(message, details = {}) {
|
|
31
|
+
super(message, 'API_SERVICE_ERROR', details);
|
|
32
|
+
this.name = 'ApiServiceError';
|
|
33
|
+
this.retryable = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 工具参数错误 - 工具调用参数不匹配
|
|
39
|
+
*/
|
|
40
|
+
export class ToolSchemaError extends SpiderError {
|
|
41
|
+
constructor(message, details = {}) {
|
|
42
|
+
super(message, 'TOOL_SCHEMA_ERROR', details);
|
|
43
|
+
this.name = 'ToolSchemaError';
|
|
44
|
+
this.retryable = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 浏览器错误 - 页面关闭、上下文丢失等
|
|
50
|
+
*/
|
|
51
|
+
export class BrowserError extends SpiderError {
|
|
52
|
+
constructor(message, details = {}) {
|
|
53
|
+
super(message, 'BROWSER_ERROR', details);
|
|
54
|
+
this.name = 'BrowserError';
|
|
55
|
+
this.retryable = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 网络错误 - 连接失败、DNS 解析等
|
|
61
|
+
*/
|
|
62
|
+
export class NetworkError extends SpiderError {
|
|
63
|
+
constructor(message, details = {}) {
|
|
64
|
+
super(message, 'NETWORK_ERROR', details);
|
|
65
|
+
this.name = 'NetworkError';
|
|
66
|
+
this.retryable = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSpider - Errors 模块索引
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
isToolSchemaError,
|
|
7
|
+
isApiServiceError,
|
|
8
|
+
isBrowserError,
|
|
9
|
+
isNetworkError,
|
|
10
|
+
classifyError,
|
|
11
|
+
} from './ErrorClassifier.js';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
SpiderError,
|
|
15
|
+
ApiServiceError,
|
|
16
|
+
ToolSchemaError,
|
|
17
|
+
BrowserError,
|
|
18
|
+
NetworkError,
|
|
19
|
+
} from './SpiderError.js';
|