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.
Files changed (84) 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 +29 -77
  5. package/src/agent/core/StreamHandler.js +139 -14
  6. package/src/agent/index.js +51 -12
  7. package/src/agent/logger.js +184 -9
  8. package/src/agent/middleware/report.js +42 -16
  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/evolve.js +0 -3
  19. package/src/agent/skills/sandbox/SKILL.md +35 -0
  20. package/src/agent/skills/static-analysis/SKILL.md +98 -2
  21. package/src/agent/subagents/anti-detect.js +10 -20
  22. package/src/agent/subagents/captcha.js +7 -19
  23. package/src/agent/subagents/crawler.js +25 -37
  24. package/src/agent/subagents/factory.js +109 -9
  25. package/src/agent/subagents/index.js +4 -13
  26. package/src/agent/subagents/js2python.js +7 -19
  27. package/src/agent/subagents/reverse.js +180 -0
  28. package/src/agent/tools/analysis.js +84 -1
  29. package/src/agent/tools/anti-detect.js +5 -2
  30. package/src/agent/tools/browser.js +160 -0
  31. package/src/agent/tools/captcha.js +1 -1
  32. package/src/agent/tools/capture.js +24 -3
  33. package/src/agent/tools/correlate.js +129 -15
  34. package/src/agent/tools/crawler.js +2 -1
  35. package/src/agent/tools/crawlerGenerator.js +90 -0
  36. package/src/agent/tools/debug.js +43 -6
  37. package/src/agent/tools/evolve.js +6 -3
  38. package/src/agent/tools/extractor.js +5 -1
  39. package/src/agent/tools/file.js +16 -7
  40. package/src/agent/tools/generateHook.js +66 -0
  41. package/src/agent/tools/hookManager.js +19 -9
  42. package/src/agent/tools/index.js +33 -20
  43. package/src/agent/tools/nodejs.js +41 -6
  44. package/src/agent/tools/python.js +4 -4
  45. package/src/agent/tools/report.js +2 -2
  46. package/src/agent/tools/runtime.js +1 -1
  47. package/src/agent/tools/sandbox.js +21 -1
  48. package/src/agent/tools/scratchpad.js +70 -0
  49. package/src/agent/tools/tracing.js +26 -0
  50. package/src/agent/tools/verifyAlgorithm.js +117 -0
  51. package/src/analyzer/EncryptionAnalyzer.js +2 -2
  52. package/src/browser/EnvBridge.js +27 -13
  53. package/src/browser/client.js +124 -18
  54. package/src/browser/collector.js +101 -22
  55. package/src/browser/defaultHooks.js +3 -1
  56. package/src/browser/hooks/index.js +5 -0
  57. package/src/browser/interceptors/AntiDebugInterceptor.js +132 -0
  58. package/src/browser/interceptors/NetworkInterceptor.js +77 -13
  59. package/src/browser/interceptors/ScriptInterceptor.js +34 -9
  60. package/src/browser/interceptors/index.js +1 -0
  61. package/src/browser/ui/analysisPanel.js +469 -464
  62. package/src/cli/commands/config.js +11 -3
  63. package/src/config/paths.js +9 -1
  64. package/src/config/settings.js +7 -1
  65. package/src/core/PatchGenerator.js +26 -6
  66. package/src/core/Sandbox.js +140 -3
  67. package/src/env/EnvCodeGenerator.js +60 -88
  68. package/src/env/modules/bom/history.js +6 -0
  69. package/src/env/modules/bom/location.js +6 -0
  70. package/src/env/modules/bom/navigator.js +13 -0
  71. package/src/env/modules/bom/screen.js +6 -0
  72. package/src/env/modules/bom/storage.js +7 -0
  73. package/src/env/modules/dom/document.js +14 -0
  74. package/src/env/modules/dom/event.js +4 -0
  75. package/src/env/modules/index.js +27 -10
  76. package/src/env/modules/webapi/fetch.js +4 -0
  77. package/src/env/modules/webapi/url.js +4 -0
  78. package/src/env/modules/webapi/xhr.js +8 -0
  79. package/src/store/DataStore.js +130 -47
  80. package/src/store/Store.js +2 -1
  81. package/src/agent/subagents/dynamic.js +0 -64
  82. package/src/agent/subagents/env-agent.js +0 -82
  83. package/src/agent/subagents/sandbox.js +0 -55
  84. package/src/agent/subagents/static.js +0 -66
@@ -4,12 +4,14 @@
4
4
  */
5
5
 
6
6
  import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
7
- import { appendFileSync, mkdirSync, existsSync } from 'fs';
8
- import { join, dirname } from 'path';
7
+ import { appendFileSync, mkdirSync, existsSync, statSync, renameSync, unlinkSync } from 'fs';
8
+ import { join } from 'path';
9
9
  import { DEEPSPIDER_HOME } from '../config/paths.js';
10
10
 
11
11
  const LOG_DIR = join(DEEPSPIDER_HOME, 'logs');
12
12
  const LOG_FILE = join(LOG_DIR, 'agent.log');
13
+ const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
14
+ const MAX_LOG_FILES = 3; // agent.log, agent.log.1, agent.log.2
13
15
 
14
16
  function ensureLogDir() {
15
17
  if (!existsSync(LOG_DIR)) {
@@ -17,6 +19,32 @@ function ensureLogDir() {
17
19
  }
18
20
  }
19
21
 
22
+ /**
23
+ * 日志滚动:agent.log 超过 MAX_LOG_SIZE 时轮转
24
+ * agent.log → agent.log.1 → agent.log.2 → 删除
25
+ */
26
+ function rotateIfNeeded(logFile) {
27
+ try {
28
+ if (!existsSync(logFile)) return;
29
+ const { size } = statSync(logFile);
30
+ if (size < MAX_LOG_SIZE) return;
31
+
32
+ // 删除最老的
33
+ const oldest = `${logFile}.${MAX_LOG_FILES - 1}`;
34
+ if (existsSync(oldest)) unlinkSync(oldest);
35
+
36
+ // 依次轮转
37
+ for (let i = MAX_LOG_FILES - 2; i >= 1; i--) {
38
+ const from = `${logFile}.${i}`;
39
+ const to = `${logFile}.${i + 1}`;
40
+ if (existsSync(from)) renameSync(from, to);
41
+ }
42
+
43
+ // 当前文件变为 .1
44
+ renameSync(logFile, `${logFile}.1`);
45
+ } catch { /* 滚动失败不影响主流程 */ }
46
+ }
47
+
20
48
  function formatTime() {
21
49
  return new Date().toISOString();
22
50
  }
@@ -27,6 +55,147 @@ function truncate(str, maxLen = 500) {
27
55
  return s.length > maxLen ? s.slice(0, maxLen) + '...' : s;
28
56
  }
29
57
 
58
+ /**
59
+ * 内存日志环形缓冲区
60
+ * 始终可用,不依赖 DEBUG 开关
61
+ */
62
+ export class InMemoryLogStore {
63
+ constructor(maxSize = 500) {
64
+ this.logs = [];
65
+ this.maxSize = maxSize;
66
+ this.startTime = Date.now();
67
+ }
68
+
69
+ add(entry) {
70
+ this.logs.push(entry);
71
+ if (this.logs.length > this.maxSize) this.logs.shift();
72
+ }
73
+
74
+ query({ category, level, limit = 50, toolName } = {}) {
75
+ let result = this.logs;
76
+ if (category) result = result.filter(l => l.category === category);
77
+ if (level) result = result.filter(l => l.level === level);
78
+ if (toolName) result = result.filter(l => l.data?.toolName === toolName);
79
+ return result.slice(-limit);
80
+ }
81
+
82
+ getStats() {
83
+ const cats = {};
84
+ for (const l of this.logs) {
85
+ cats[l.category] = (cats[l.category] || 0) + 1;
86
+ }
87
+ return {
88
+ total: this.logs.length,
89
+ categories: cats,
90
+ errors: this.logs.filter(l => l.level === 'ERROR').length,
91
+ uptimeMs: Date.now() - this.startTime,
92
+ };
93
+ }
94
+
95
+ clear() { this.logs = []; }
96
+ }
97
+
98
+ export const logStore = new InMemoryLogStore();
99
+
100
+ /**
101
+ * 内存日志回调处理器
102
+ * 始终启用,将日志写入 logStore
103
+ */
104
+ export class InMemoryLoggerCallback extends BaseCallbackHandler {
105
+ name = 'InMemoryLoggerCallback';
106
+
107
+ log(level, category, message, data = null) {
108
+ logStore.add({ time: formatTime(), level, category, message, data });
109
+ }
110
+
111
+ handleLLMStart(llm, prompts, runId) {
112
+ this.log('INFO', 'LLM', 'LLM 调用开始', {
113
+ runId,
114
+ model: llm?.id?.[2] || llm?.name,
115
+ promptCount: prompts?.length,
116
+ promptPreview: truncate(prompts?.[0], 200),
117
+ });
118
+ }
119
+
120
+ handleLLMEnd(output, runId) {
121
+ const content = output?.generations?.[0]?.[0]?.text
122
+ || output?.generations?.[0]?.[0]?.message?.content;
123
+ this.log('INFO', 'LLM', 'LLM 调用结束', {
124
+ runId,
125
+ outputPreview: truncate(content, 300),
126
+ tokenUsage: output?.llmOutput?.tokenUsage,
127
+ });
128
+ }
129
+
130
+ handleLLMError(error, runId) {
131
+ this.log('ERROR', 'LLM', 'LLM 调用错误', {
132
+ runId,
133
+ error: error?.message || String(error),
134
+ });
135
+ }
136
+
137
+ handleToolStart(tool, input, runId) {
138
+ this.log('INFO', 'TOOL', `工具调用: ${tool?.name || 'unknown'}`, {
139
+ runId,
140
+ toolName: tool?.name,
141
+ input: truncate(input, 500),
142
+ });
143
+ }
144
+
145
+ handleToolEnd(output, runId) {
146
+ this.log('INFO', 'TOOL', '工具返回', {
147
+ runId,
148
+ output: truncate(output, 500),
149
+ });
150
+ }
151
+
152
+ handleToolError(error, runId) {
153
+ this.log('ERROR', 'TOOL', '工具错误', {
154
+ runId,
155
+ error: error?.message || String(error),
156
+ stack: error?.stack?.split('\n').slice(0, 5),
157
+ });
158
+ }
159
+
160
+ handleChainStart(chain, inputs, runId) {
161
+ this.log('DEBUG', 'CHAIN', `Chain 开始: ${chain?.name || 'unknown'}`, {
162
+ runId,
163
+ chainName: chain?.name,
164
+ inputKeys: Object.keys(inputs || {}),
165
+ });
166
+ }
167
+
168
+ handleChainEnd(outputs, runId) {
169
+ this.log('DEBUG', 'CHAIN', 'Chain 结束', {
170
+ runId,
171
+ outputKeys: Object.keys(outputs || {}),
172
+ });
173
+ }
174
+
175
+ handleChainError(error, runId) {
176
+ this.log('ERROR', 'CHAIN', 'Chain 错误', {
177
+ runId,
178
+ error: error?.message || String(error),
179
+ });
180
+ }
181
+
182
+ handleAgentAction(action, runId) {
183
+ this.log('INFO', 'AGENT', `Agent 动作: ${action?.tool}`, {
184
+ runId,
185
+ tool: action?.tool,
186
+ toolInput: truncate(action?.toolInput, 300),
187
+ log: truncate(action?.log, 200),
188
+ });
189
+ }
190
+
191
+ handleAgentEnd(action, runId) {
192
+ this.log('INFO', 'AGENT', 'Agent 结束', {
193
+ runId,
194
+ returnValues: truncate(action?.returnValues, 300),
195
+ });
196
+ }
197
+ }
198
+
30
199
  /**
31
200
  * 文件日志回调处理器
32
201
  */
@@ -41,6 +210,8 @@ export class FileLoggerCallback extends BaseCallbackHandler {
41
210
  }
42
211
 
43
212
  log(level, category, message, data = null) {
213
+ rotateIfNeeded(this.logFile);
214
+
44
215
  const line = JSON.stringify({
45
216
  time: formatTime(),
46
217
  level,
@@ -149,16 +320,20 @@ export class FileLoggerCallback extends BaseCallbackHandler {
149
320
  }
150
321
 
151
322
  /**
152
- * 创建日志回调实例
323
+ * 创建日志回调实例数组
324
+ * 始终包含 InMemoryLoggerCallback + FileLoggerCallback
325
+ * DEBUG=true 时额外开启 verbose(控制台输出)
153
326
  */
154
327
  export function createLogger(options = {}) {
155
- const enabled = process.env.DEBUG === 'true' || options.enabled;
156
- if (!enabled) return null;
328
+ const verbose = process.env.DEBUG === 'true' || options.verbose || false;
157
329
 
158
- return new FileLoggerCallback({
159
- verbose: options.verbose || false,
160
- logFile: options.logFile || LOG_FILE,
161
- });
330
+ return [
331
+ new InMemoryLoggerCallback(),
332
+ new FileLoggerCallback({
333
+ verbose,
334
+ logFile: options.logFile || LOG_FILE,
335
+ }),
336
+ ];
162
337
  }
163
338
 
164
339
  export default FileLoggerCallback;
@@ -13,10 +13,33 @@ const reportStateSchema = z.object({
13
13
  reportReady: z.boolean().default(false),
14
14
  });
15
15
 
16
+ /**
17
+ * 检测并触发报告显示
18
+ */
19
+ async function detectAndTriggerReport(result, onReportReady) {
20
+ try {
21
+ const content = typeof result?.content === 'string'
22
+ ? JSON.parse(result.content)
23
+ : result?.content;
24
+
25
+ if (content?.success && content?.path?.endsWith('.md')) {
26
+ console.log('[reportMiddleware] 检测到 .md 文件:', content.path);
27
+
28
+ if (onReportReady) {
29
+ await onReportReady(content.path);
30
+ }
31
+ return true;
32
+ }
33
+ } catch {
34
+ // 解析失败,忽略
35
+ }
36
+ return false;
37
+ }
38
+
16
39
  /**
17
40
  * 创建报告中间件
18
- * 在 afterModel 中检测 artifact_save 工具调用结果
19
- * afterAgent 中触发报告显示回调
41
+ * 在 wrapToolCall 中检测 artifact_save 工具调用结果,立即触发报告
42
+ * 同时在 afterModel 和 afterAgent 中保留检测逻辑作为备选
20
43
  */
21
44
  export function createReportMiddleware(options = {}) {
22
45
  const { onReportReady } = options;
@@ -25,7 +48,20 @@ export function createReportMiddleware(options = {}) {
25
48
  name: 'reportMiddleware',
26
49
  stateSchema: reportStateSchema,
27
50
 
28
- // 模型调用后,检测工具调用结果
51
+ // 工具调用包装器:在 artifact_save 完成时立即检测
52
+ wrapToolCall: async (request, handler) => {
53
+ const toolName = request.tool?.name ?? request.toolCall?.name;
54
+ const result = await handler(request);
55
+
56
+ // 检测 artifact_save 工具返回的 .md 文件
57
+ if (toolName === 'artifact_save') {
58
+ await detectAndTriggerReport(result, onReportReady);
59
+ }
60
+
61
+ return result;
62
+ },
63
+
64
+ // 模型调用后,检测工具调用结果(备选方案)
29
65
  afterModel: (state) => {
30
66
  const messages = state.messages;
31
67
  if (!messages || messages.length === 0) return undefined;
@@ -41,10 +77,10 @@ export function createReportMiddleware(options = {}) {
41
77
 
42
78
  // 检测是否是 artifact_save 写入的 .md 文件
43
79
  if (content.success && content.path?.endsWith('.md')) {
44
- console.log('[reportMiddleware] 检测到 .md 文件:', content.path);
80
+ console.log('[reportMiddleware] afterModel 检测到 .md 文件:', content.path);
45
81
  return { lastWrittenMdFile: content.path };
46
82
  }
47
- } catch (e) {
83
+ } catch {
48
84
  // 解析失败,忽略
49
85
  }
50
86
  }
@@ -52,22 +88,12 @@ export function createReportMiddleware(options = {}) {
52
88
  return undefined;
53
89
  },
54
90
 
55
- // Agent 执行完成后
91
+ // Agent 执行完成后(streamEvents 模式下可能不被调用)
56
92
  afterAgent: async (state) => {
57
93
  const mdFile = state.lastWrittenMdFile;
58
94
 
59
95
  if (mdFile) {
60
96
  console.log('[reportMiddleware] afterAgent: 准备显示报告:', mdFile);
61
-
62
- // 调用回调通知外部
63
- if (onReportReady) {
64
- try {
65
- await onReportReady(mdFile);
66
- } catch (e) {
67
- console.error('[reportMiddleware] onReportReady 失败:', e.message);
68
- }
69
- }
70
-
71
97
  return { reportReady: true };
72
98
  }
73
99
 
@@ -0,0 +1,233 @@
1
+ /**
2
+ * DeepSpider - 自定义子代理中间件
3
+ * 复刻 deepagents 内置的 createSubAgentMiddleware,增加 context 结构化传递
4
+ *
5
+ * 与内置版本的唯一区别:task tool schema 新增 context 字段(z.record(z.string(), z.string()).optional()),
6
+ * LLM 按需填写 key-value 对,子代理收到的 HumanMessage 中 context 以 <context> 块拼接在 description 之后。
7
+ */
8
+
9
+ import { createMiddleware, createAgent, tool, humanInTheLoopMiddleware } from 'langchain';
10
+ import { HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
11
+ import { getCurrentTaskInput, Command } from '@langchain/langgraph';
12
+ import { TASK_SYSTEM_PROMPT } from 'deepagents';
13
+ import { z } from 'zod';
14
+
15
+ // 子代理 state 中需要排除的 key(与 deepagents 内部一致)
16
+ const EXCLUDED_STATE_KEYS = [
17
+ 'messages',
18
+ 'todos',
19
+ 'structuredResponse',
20
+ 'skillsMetadata',
21
+ 'memoryContents',
22
+ ];
23
+
24
+ /**
25
+ * 过滤 state,排除不应传递给子代理的 key
26
+ */
27
+ function filterStateForSubagent(state) {
28
+ const filtered = {};
29
+ for (const [key, value] of Object.entries(state)) {
30
+ if (!EXCLUDED_STATE_KEYS.includes(key)) filtered[key] = value;
31
+ }
32
+ return filtered;
33
+ }
34
+
35
+ /**
36
+ * 构造 Command 返回,将子代理结果的 state 更新 + 最后一条消息作为 ToolMessage 返回
37
+ */
38
+ const TRUST_SIGNAL = `\n\n---\n⚠️ 子代理已完成任务。请直接使用子代理输出的文件和结论,不要重复执行 artifact_load / artifact_glob / ls 等文件读取操作来检查子代理已保存的文件。如果需要对生成的代码做端到端验证,那是你的职责,请正常执行。`;
39
+
40
+ function returnCommandWithStateUpdate(result, toolCallId) {
41
+ const stateUpdate = filterStateForSubagent(result);
42
+ const messages = result.messages;
43
+ const lastMessage = messages?.[messages.length - 1];
44
+ const content = (lastMessage?.content || 'Task completed') + TRUST_SIGNAL;
45
+ return new Command({
46
+ update: {
47
+ ...stateUpdate,
48
+ messages: [new ToolMessage({
49
+ content,
50
+ tool_call_id: toolCallId,
51
+ name: 'task',
52
+ })],
53
+ },
54
+ });
55
+ }
56
+
57
+ /**
58
+ * 生成 task tool 的 description(复刻 deepagents 内部的 getTaskToolDescription)
59
+ */
60
+ function getTaskToolDescription(subagentDescriptions) {
61
+ return `
62
+ Launch an ephemeral subagent to handle complex, multi-step independent tasks with isolated context windows.
63
+
64
+ Available agent types and the tools they have access to:
65
+ ${subagentDescriptions.join('\n')}
66
+
67
+ When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
68
+
69
+ ## Usage notes:
70
+ 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
71
+ 2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
72
+ 3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
73
+ 4. The agent's outputs should generally be trusted
74
+ 5. Clearly tell the agent whether you expect it to create content, perform analysis, or just do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
75
+ 6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
76
+
77
+ ## context 参数
78
+ 委托子代理时,使用 context 参数传递结构化上下文(key-value 对),如站点标识、请求 ID、目标参数名等。context 会注入到子代理的初始消息中,确保关键信息不丢失。
79
+ `.trim();
80
+ }
81
+
82
+ /**
83
+ * 编译子代理:遍历 subagents 数组,用 createAgent 编译为可运行实例
84
+ */
85
+ function getSubagents(options) {
86
+ const {
87
+ defaultModel,
88
+ defaultTools,
89
+ defaultMiddleware,
90
+ generalPurposeMiddleware: gpMiddleware,
91
+ defaultInterruptOn,
92
+ subagents,
93
+ generalPurposeAgent,
94
+ } = options;
95
+
96
+ const defaultSubagentMiddleware = defaultMiddleware || [];
97
+ const generalPurposeMiddlewareBase = gpMiddleware || defaultSubagentMiddleware;
98
+ const agents = {};
99
+ const descriptions = [];
100
+
101
+ // 通用子代理(DeepSpider 默认不启用,但保留能力)
102
+ if (generalPurposeAgent) {
103
+ const generalPurposeMiddleware = [...generalPurposeMiddlewareBase];
104
+ if (defaultInterruptOn) generalPurposeMiddleware.push(humanInTheLoopMiddleware({ interruptOn: defaultInterruptOn }));
105
+ agents['general-purpose'] = createAgent({
106
+ model: defaultModel,
107
+ systemPrompt: 'In order to complete the objective that the user asks of you, you have access to a number of standard tools.',
108
+ tools: defaultTools,
109
+ middleware: generalPurposeMiddleware,
110
+ name: 'general-purpose',
111
+ });
112
+ descriptions.push('- general-purpose: General-purpose agent for researching complex questions, searching for files and content, and executing multi-step tasks.');
113
+ }
114
+
115
+ // 自定义子代理
116
+ for (const agentParams of subagents) {
117
+ descriptions.push(`- ${agentParams.name}: ${agentParams.description}`);
118
+
119
+ if ('runnable' in agentParams) {
120
+ // CompiledSubAgent — 已编译,直接使用
121
+ agents[agentParams.name] = agentParams.runnable;
122
+ } else {
123
+ const middleware = agentParams.middleware
124
+ ? [...defaultSubagentMiddleware, ...agentParams.middleware]
125
+ : [...defaultSubagentMiddleware];
126
+ const interruptOn = agentParams.interruptOn || defaultInterruptOn;
127
+ if (interruptOn) middleware.push(humanInTheLoopMiddleware({ interruptOn }));
128
+
129
+ agents[agentParams.name] = createAgent({
130
+ model: agentParams.model ?? defaultModel,
131
+ systemPrompt: agentParams.systemPrompt,
132
+ tools: agentParams.tools ?? defaultTools,
133
+ middleware,
134
+ name: agentParams.name,
135
+ });
136
+ }
137
+ }
138
+
139
+ return { agents, descriptions };
140
+ }
141
+
142
+ /**
143
+ * 创建增强版 task tool:schema 增加 context 字段
144
+ */
145
+ function createEnhancedTaskTool(options) {
146
+ const { agents: subagentGraphs, descriptions: subagentDescriptions } = getSubagents(options);
147
+ const availableTypes = Object.keys(subagentGraphs).join(', ');
148
+
149
+ return tool(
150
+ async (input, config) => {
151
+ const { description, subagent_type, context } = input;
152
+
153
+ if (!(subagent_type in subagentGraphs)) {
154
+ const allowedTypes = Object.keys(subagentGraphs).map((k) => `\`${k}\``).join(', ');
155
+ throw new Error(`Error: invoked agent of type ${subagent_type}, the only allowed types are ${allowedTypes}`);
156
+ }
157
+
158
+ // 构造子代理的初始消息:description + context 块
159
+ let content = description;
160
+ if (context && Object.keys(context).length > 0) {
161
+ content += `\n\n<context>\n${JSON.stringify(context)}\n</context>`;
162
+ }
163
+
164
+ const subagent = subagentGraphs[subagent_type];
165
+ const subagentState = filterStateForSubagent(getCurrentTaskInput());
166
+ subagentState.messages = [new HumanMessage({ content })];
167
+
168
+ const result = await subagent.invoke(subagentState, config);
169
+ if (!config.toolCall?.id) throw new Error('Tool call ID is required for subagent invocation');
170
+ return returnCommandWithStateUpdate(result, config.toolCall.id);
171
+ },
172
+ {
173
+ name: 'task',
174
+ description: getTaskToolDescription(subagentDescriptions),
175
+ schema: z.object({
176
+ description: z.string().describe('The task to execute with the selected agent'),
177
+ subagent_type: z.string().describe(`Name of the agent to use. Available: ${availableTypes}`),
178
+ context: z.record(z.string(), z.string()).optional().describe('Structured key-value context to pass to the subagent (e.g. site, requestId, targetParam)'),
179
+ }),
180
+ },
181
+ );
182
+ }
183
+
184
+ /**
185
+ * 创建自定义子代理中间件
186
+ * 替换 deepagents 内置的 createSubAgentMiddleware,增加 context 结构化传递
187
+ *
188
+ * @param {Object} options
189
+ * @param {LanguageModelLike} options.defaultModel - LLM 实例
190
+ * @param {StructuredTool[]} options.defaultTools - 默认工具集
191
+ * @param {SubAgent[]} options.subagents - 子代理配置数组
192
+ * @param {AgentMiddleware[]} options.defaultMiddleware - 子代理默认中间件
193
+ * @param {boolean} [options.generalPurposeAgent=false] - 是否创建通用子代理
194
+ * @param {Object} [options.defaultInterruptOn] - HITL 配置
195
+ */
196
+ export function createCustomSubAgentMiddleware(options) {
197
+ const {
198
+ defaultModel,
199
+ defaultTools = [],
200
+ subagents = [],
201
+ defaultMiddleware = null,
202
+ generalPurposeMiddleware = null,
203
+ generalPurposeAgent = false,
204
+ defaultInterruptOn = null,
205
+ } = options;
206
+
207
+ const taskToolOptions = {
208
+ defaultModel,
209
+ defaultTools,
210
+ subagents,
211
+ defaultMiddleware,
212
+ generalPurposeMiddleware,
213
+ generalPurposeAgent,
214
+ defaultInterruptOn,
215
+ };
216
+
217
+ const enhancedTaskTool = createEnhancedTaskTool(taskToolOptions);
218
+
219
+ // context 使用说明,拼接到 TASK_SYSTEM_PROMPT 末尾
220
+ const contextGuide = `\n\n委托子代理时,使用 context 参数传递结构化上下文(key-value 对),如站点标识、请求 ID、目标参数名等。context 会注入到子代理的初始消息中,确保关键信息不丢失。`;
221
+ const fullSystemPrompt = TASK_SYSTEM_PROMPT + contextGuide;
222
+
223
+ return createMiddleware({
224
+ name: 'subAgentMiddleware',
225
+ tools: [enhancedTaskTool],
226
+ wrapModelCall: async (request, handler) => {
227
+ return handler({
228
+ ...request,
229
+ systemMessage: request.systemMessage.concat(new SystemMessage({ content: fullSystemPrompt })),
230
+ });
231
+ },
232
+ });
233
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * DeepSpider - 工具连续失败检测中间件
3
+ * 检测同一工具连续失败(超时、错误),在 ToolMessage 中追加警告引导 LLM 换策略
4
+ */
5
+
6
+ import { createMiddleware } from 'langchain';
7
+
8
+ // 默认配置
9
+ const DEFAULTS = {
10
+ maxConsecutiveFailures: 3, // 连续失败 N 次后触发强警告
11
+ warnAfter: 2, // 连续失败 N 次后开始追加提示
12
+ resetOnSuccess: true, // 成功时重置计数
13
+ };
14
+
15
+ /**
16
+ * 判断 ToolMessage 是否表示失败
17
+ */
18
+ function isToolFailure(result) {
19
+ // ToolMessage.status === 'error' (toolRetryMiddleware 设置)
20
+ if (result?.status === 'error') return true;
21
+
22
+ // 工具返回的 JSON 中 success === false
23
+ const content = typeof result?.content === 'string' ? result.content : '';
24
+ if (!content.startsWith('{')) return false;
25
+ try {
26
+ const parsed = JSON.parse(content);
27
+ return parsed.success === false;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * 创建工具连续失败检测中间件
35
+ */
36
+ export function createToolGuardMiddleware(options = {}) {
37
+ const config = { ...DEFAULTS, ...options };
38
+
39
+ // toolName → { count, lastArgs }
40
+ const failureTracker = new Map();
41
+
42
+ return createMiddleware({
43
+ name: 'toolGuardMiddleware',
44
+
45
+ wrapToolCall: async (request, handler) => {
46
+ const toolName = request.tool?.name ?? request.toolCall?.name;
47
+ const result = await handler(request);
48
+
49
+ if (!toolName) return result;
50
+
51
+ if (isToolFailure(result)) {
52
+ const tracker = failureTracker.get(toolName) || { count: 0 };
53
+ tracker.count++;
54
+ failureTracker.set(toolName, tracker);
55
+
56
+ // 追加警告到 ToolMessage content
57
+ if (tracker.count >= config.maxConsecutiveFailures) {
58
+ const warning = `\n\n🚫 工具 ${toolName} 已连续失败 ${tracker.count} 次。请停止使用该工具重试相同逻辑,必须换用其他工具或策略。`;
59
+ if (typeof result.content === 'string') {
60
+ result.content += warning;
61
+ }
62
+ } else if (tracker.count >= config.warnAfter) {
63
+ const warning = `\n\n⚠️ 工具 ${toolName} 已连续失败 ${tracker.count} 次(上限 ${config.maxConsecutiveFailures})。如果继续失败将被限制使用,建议考虑替代方案。`;
64
+ if (typeof result.content === 'string') {
65
+ result.content += warning;
66
+ }
67
+ }
68
+ } else if (config.resetOnSuccess) {
69
+ failureTracker.delete(toolName);
70
+ }
71
+
72
+ return result;
73
+ },
74
+ });
75
+ }
76
+
77
+ export default createToolGuardMiddleware;