deepspider 0.3.2 → 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.
- package/README.md +8 -2
- package/package.json +4 -2
- package/src/agent/core/PanelBridge.js +34 -8
- package/src/agent/core/StreamHandler.js +114 -15
- package/src/agent/index.js +72 -14
- package/src/agent/middleware/memoryFlush.js +48 -0
- package/src/agent/middleware/report.js +77 -45
- package/src/agent/middleware/subagent.js +4 -1
- package/src/agent/middleware/toolAvailability.js +37 -0
- package/src/agent/middleware/toolGuard.js +141 -31
- package/src/agent/prompts/system.js +130 -1
- package/src/agent/run.js +127 -14
- package/src/agent/sessions.js +88 -0
- package/src/agent/skills/anti-detect/SKILL.md +89 -14
- package/src/agent/skills/captcha/SKILL.md +93 -19
- package/src/agent/skills/js2python/evolved.md +5 -1
- package/src/agent/skills/static-analysis/evolved.md +5 -1
- package/src/agent/subagents/anti-detect.js +27 -5
- package/src/agent/subagents/captcha.js +28 -9
- package/src/agent/subagents/crawler.js +26 -79
- package/src/agent/subagents/factory.js +24 -4
- package/src/agent/subagents/js2python.js +18 -16
- package/src/agent/tools/analysis.js +17 -7
- package/src/agent/tools/browser.js +26 -13
- package/src/agent/tools/crawler.js +1 -1
- package/src/agent/tools/crawlerGenerator.js +2 -2
- package/src/agent/tools/index.js +3 -1
- package/src/agent/tools/patch.js +1 -1
- package/src/agent/tools/store.js +1 -1
- package/src/browser/client.js +5 -1
- package/src/browser/ui/analysisPanel.js +72 -0
|
@@ -17,17 +17,36 @@ export const captchaSubagent = createSubagent({
|
|
|
17
17
|
## 核心职责
|
|
18
18
|
识别验证码类型,选择最优处理策略,确保验证通过。
|
|
19
19
|
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
## 验证码类型与策略
|
|
21
|
+
|
|
22
|
+
| 类型 | 识别特征 | 处理策略 |
|
|
23
|
+
|------|----------|----------|
|
|
24
|
+
| 图片验证码 | img 标签 + 输入框 | OCR 识别(ddddocr) |
|
|
25
|
+
| 滑块验证码 | 背景图 + 滑块图 + 拖动条 | 缺口检测 + 轨迹模拟 |
|
|
26
|
+
| 点选验证码 | 背景图 + 文字/图标提示 | 目标检测 + 坐标点击 |
|
|
27
|
+
| 短信/邮箱验证码 | 发送按钮 + 输入框 | 接码平台或提示用户手动输入 |
|
|
25
28
|
|
|
26
29
|
## 工作流程
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
1. **识别类型** — 分析页面元素,判断验证码类型
|
|
32
|
+
2. **获取素材** — 截图或提取验证码图片
|
|
33
|
+
3. **选择策略** — 根据类型选择处理方案
|
|
34
|
+
4. **执行验证** — 调用对应工具处理
|
|
35
|
+
5. **检查结果** — 验证是否通过
|
|
36
|
+
6. **失败处理**:
|
|
37
|
+
- 图片 OCR 失败 → 刷新验证码重试(最多 3 次)
|
|
38
|
+
- 滑块失败 → 调整轨迹参数(速度、抖动)重试
|
|
39
|
+
- 连续失败 → 返回告知主 agent,建议换策略或人工介入
|
|
40
|
+
|
|
41
|
+
## 滑块验证码要点
|
|
42
|
+
- 轨迹必须模拟人类行为:加速→匀速→减速→微调
|
|
43
|
+
- 不要直接匀速滑动,会被检测
|
|
44
|
+
- 缺口位置检测后加随机偏移(±2px)
|
|
45
|
+
|
|
46
|
+
## 能力边界
|
|
47
|
+
- 不能做加密分析、反混淆
|
|
48
|
+
- 不能生成爬虫脚本
|
|
49
|
+
- 遇到未知验证码类型 → 截图返回,让主 agent 决策
|
|
31
50
|
`,
|
|
32
51
|
tools: [
|
|
33
52
|
...captchaTools,
|
|
@@ -16,102 +16,49 @@ export const crawlerSubagent = createSubagent({
|
|
|
16
16
|
systemPrompt: `你是 DeepSpider 的爬虫编排专家,负责生成完整可运行的 Python 爬虫脚本。
|
|
17
17
|
|
|
18
18
|
## 核心职责
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
1. 根据主 agent 提供的分析结果和已验证的代码模块,整合生成完整 Python 爬虫脚本
|
|
22
|
-
2. 使用 artifact_save 保存代码文件
|
|
23
|
-
3. 输出最终代码文件路径
|
|
24
|
-
|
|
25
|
-
## 网站复杂度分级
|
|
26
|
-
|
|
27
|
-
### Level 1 - 简单
|
|
28
|
-
- 无加密或简单加密
|
|
29
|
-
- 无验证码
|
|
30
|
-
- 无登录要求
|
|
31
|
-
- 无风控检测
|
|
32
|
-
|
|
33
|
-
### Level 2 - 中等
|
|
34
|
-
- 有加密参数
|
|
35
|
-
- 可能有简单验证码
|
|
36
|
-
- 可能需要登录
|
|
37
|
-
- 基础风控
|
|
38
|
-
|
|
39
|
-
### Level 3 - 复杂
|
|
40
|
-
- 复杂加密 + 多重风控
|
|
41
|
-
- 多种验证码
|
|
42
|
-
- 设备指纹检测
|
|
43
|
-
- 行为分析
|
|
19
|
+
输出用户可以直接 \`python crawler.py\` 运行的完整爬虫代码。
|
|
44
20
|
|
|
45
21
|
## 输入来源
|
|
46
|
-
|
|
47
|
-
主 agent 会在 task description 中提供:
|
|
22
|
+
主 agent 在 task description 中提供:
|
|
48
23
|
- 接口分析结果(URL、方法、参数、Headers)
|
|
49
24
|
- 已验证的加密代码文件路径(如有)
|
|
50
25
|
- 用户选择的框架(requests / scrapy / playwright 等)
|
|
51
26
|
|
|
52
|
-
|
|
27
|
+
你不需要自己分析加密或调度其他子代理。
|
|
53
28
|
如需查看已有的加密代码,用 \`query_store\` 或 \`artifact_load\` 读取。
|
|
54
29
|
|
|
55
|
-
##
|
|
30
|
+
## 工作流程
|
|
56
31
|
|
|
57
|
-
|
|
32
|
+
1. **读取输入** — 从 task description 和 store 中获取分析结果、加密代码
|
|
33
|
+
2. **生成代码** — 整合为完整可运行的 Python 爬虫脚本
|
|
34
|
+
3. **自验证** — 检查代码完整性:
|
|
35
|
+
- 所有 import 是否齐全
|
|
36
|
+
- 加密模块是否正确整合(路径、函数名、参数)
|
|
37
|
+
- Headers/Cookies 是否从分析结果中完整复制
|
|
38
|
+
- if __name__ 入口是否可运行
|
|
39
|
+
4. **保存** — artifact_save 保存文件 + requirements.txt
|
|
40
|
+
5. **输出路径** — 告知文件保存位置
|
|
41
|
+
|
|
42
|
+
## 输出规范
|
|
58
43
|
|
|
59
|
-
|
|
60
|
-
1. 使用 artifact_save 保存完整 .py 文件
|
|
44
|
+
1. 使用 artifact_save 保存 .py 文件
|
|
61
45
|
2. 代码必须可以直接 \`python xxx.py\` 运行
|
|
62
|
-
3.
|
|
63
|
-
4.
|
|
64
|
-
5. 包含 requirements.txt
|
|
46
|
+
3. 包含完整 import、使用示例(if __name__)、requirements.txt
|
|
47
|
+
4. 禁止在对话中输出大段代码片段代替文件
|
|
65
48
|
|
|
66
|
-
### 复杂网站
|
|
49
|
+
### 复杂网站 — 多文件结构
|
|
67
50
|
\`\`\`
|
|
68
|
-
|
|
69
|
-
├── config.py #
|
|
51
|
+
{domain}_crawler/
|
|
52
|
+
├── config.py # 配置
|
|
70
53
|
├── crypto.py # 加密模块(来自 js2python)
|
|
71
|
-
├── captcha.py # 验证码处理(如需要)
|
|
72
54
|
├── crawler.py # 主爬虫逻辑
|
|
73
|
-
└── requirements.txt
|
|
55
|
+
└── requirements.txt
|
|
74
56
|
\`\`\`
|
|
75
57
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<domain> 爬虫 - 由 DeepSpider 生成
|
|
81
|
-
"""
|
|
82
|
-
import requests
|
|
83
|
-
|
|
84
|
-
class Crawler:
|
|
85
|
-
def __init__(self):
|
|
86
|
-
self.session = requests.Session()
|
|
87
|
-
self.session.headers.update({...})
|
|
88
|
-
|
|
89
|
-
def encrypt(self, data):
|
|
90
|
-
# 加密逻辑
|
|
91
|
-
...
|
|
92
|
-
|
|
93
|
-
def login(self, username, password):
|
|
94
|
-
# 登录流程(如需要)
|
|
95
|
-
...
|
|
96
|
-
|
|
97
|
-
def fetch(self, params):
|
|
98
|
-
# 请求逻辑
|
|
99
|
-
encrypted = self.encrypt(params)
|
|
100
|
-
resp = self.session.post(url, data=encrypted)
|
|
101
|
-
return resp.json()
|
|
102
|
-
|
|
103
|
-
if __name__ == "__main__":
|
|
104
|
-
c = Crawler()
|
|
105
|
-
# c.login("user", "pass") # 如需要
|
|
106
|
-
data = c.fetch({"page": 1})
|
|
107
|
-
print(data)
|
|
108
|
-
\`\`\`
|
|
109
|
-
|
|
110
|
-
## 工作流程
|
|
111
|
-
1. 读取主 agent 提供的分析结果和已有代码模块(query_store / artifact_load)
|
|
112
|
-
2. 整合为完整可运行的 Python 爬虫脚本
|
|
113
|
-
3. 使用 artifact_save 保存文件
|
|
114
|
-
4. 输出文件路径
|
|
58
|
+
## 常见问题处理
|
|
59
|
+
- 加密代码路径找不到 → query_store 搜索,或返回告知主 agent
|
|
60
|
+
- 分析结果中缺少关键 Headers → 从原始请求详情中补全,不要猜测
|
|
61
|
+
- 框架不熟悉 → 用 requests 作为降级方案,说明原因
|
|
115
62
|
`,
|
|
116
63
|
tools: [
|
|
117
64
|
...crawlerTools,
|
|
@@ -16,13 +16,33 @@ import { evolveTools } from '../tools/evolve.js';
|
|
|
16
16
|
const SUBAGENT_RUN_LIMIT = 80;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* 子代理执行纪律提示
|
|
20
|
+
* 注入所有子代理的公共行为规范
|
|
21
21
|
*/
|
|
22
22
|
export const SUBAGENT_DISCIPLINE_PROMPT = `
|
|
23
23
|
|
|
24
24
|
## 执行纪律
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
### 先验证再展开
|
|
27
|
+
- 先用最小代价验证假设(一次工具调用),确认可行后再展开
|
|
28
|
+
- 不要一上来就拉全量数据或启动完整流程
|
|
29
|
+
|
|
30
|
+
### Think/Reflect — 异常时暂停
|
|
31
|
+
遇到以下情况,先输出思考过程再行动:
|
|
32
|
+
- 执行结果与预期不符
|
|
33
|
+
- 连续 2 次工具调用失败
|
|
34
|
+
- 需要在多个方案中选择
|
|
35
|
+
|
|
36
|
+
### 循环检测
|
|
37
|
+
同一操作最多重试 3 次。第 3 次失败后:
|
|
38
|
+
1. 分析失败模式,不要简单重试
|
|
39
|
+
2. 尝试替代方案(换工具、换参数、换思路)
|
|
40
|
+
3. 替代方案也失败 → 总结当前进度和卡点,返回给主 agent
|
|
41
|
+
|
|
42
|
+
### 信息优先级
|
|
43
|
+
1. 已捕获的数据(请求/响应/Hook 记录)— 最可靠
|
|
44
|
+
2. 工具实时获取的结果 — 需验证
|
|
45
|
+
3. 模型推断 — 仅作参考,必须验证后才能作为结论`;
|
|
26
46
|
|
|
27
47
|
/**
|
|
28
48
|
* 创建工具调用次数限制中间件
|
|
@@ -33,7 +53,7 @@ export const SUBAGENT_DISCIPLINE_PROMPT = `
|
|
|
33
53
|
* 注意:callCount 通过闭包持有,假设同一子代理不会被并行调用。
|
|
34
54
|
* deepagents 当前是串行调度子代理,如果未来支持并行需改为 per-invocation 计数。
|
|
35
55
|
*/
|
|
36
|
-
function createToolCallLimitMiddleware(runLimit = SUBAGENT_RUN_LIMIT) {
|
|
56
|
+
export function createToolCallLimitMiddleware(runLimit = SUBAGENT_RUN_LIMIT) {
|
|
37
57
|
let callCount = 0;
|
|
38
58
|
|
|
39
59
|
return createMiddleware({
|
|
@@ -16,38 +16,40 @@ export const js2pythonSubagent = createSubagent({
|
|
|
16
16
|
systemPrompt: `你是 DeepSpider 的 JS 转 Python 专家,负责将 JS 加密逻辑转换为 Python 代码。
|
|
17
17
|
|
|
18
18
|
## 核心职责
|
|
19
|
-
将 JS 加密算法转换为 Python
|
|
19
|
+
将 JS 加密算法转换为 Python 实现,保证输出与原始 JS 结果完全一致。
|
|
20
20
|
|
|
21
21
|
## 转换策略
|
|
22
22
|
|
|
23
23
|
### 策略一:纯 Python 重写(优先)
|
|
24
|
-
适用:标准加密算法(AES、MD5、SHA、RSA
|
|
24
|
+
适用:标准加密算法(AES、MD5、SHA、HMAC、RSA、国密 SM2/SM3/SM4)
|
|
25
|
+
常用库:pycryptodome、hashlib、hmac、gmssl
|
|
25
26
|
|
|
26
27
|
### 策略二:execjs 执行原始 JS
|
|
27
|
-
|
|
28
|
+
适用:复杂自定义算法、混淆代码难还原、位运算密集型
|
|
28
29
|
|
|
29
30
|
## 工作流程
|
|
30
|
-
1. 分析 JS 代码,识别加密算法类型
|
|
31
|
-
2. 使用 run_node_code 执行原始 JS 获取基准结果
|
|
32
|
-
3. 选择转换策略
|
|
33
|
-
4. 生成 Python 代码
|
|
34
|
-
5. 验证结果一致性
|
|
35
|
-
6. 使用 artifact_save 保存文件
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
1. **获取基准** — run_node_code 执行原始 JS,用固定输入获取基准输出
|
|
33
|
+
2. **识别算法** — 分析 JS 代码,判断加密类型和关键参数(key、iv、mode、padding)
|
|
34
|
+
3. **选择策略** — 标准算法走纯 Python,复杂算法走 execjs
|
|
35
|
+
4. **生成代码** — 编写 Python 实现
|
|
36
|
+
5. **验证一致性** — 用相同输入运行 Python 代码,对比输出是否与基准完全一致
|
|
37
|
+
6. **保存文件** — artifact_save 保存 .py 文件
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
## 常见坑
|
|
40
|
+
- AES padding 差异:JS 的 CryptoJS 默认 PKCS7,Python 需手动实现
|
|
41
|
+
- 编码差异:JS 的 toString() 可能是 hex/base64,注意对齐
|
|
42
|
+
- 字节序:JS 的 charCodeAt 返回 UTF-16,Python 的 ord 返回 Unicode code point
|
|
43
|
+
- 大数运算:JS 的位运算是 32 位有符号,Python 是任意精度,需要 & 0xFFFFFFFF 截断
|
|
40
44
|
|
|
45
|
+
## 输出规范
|
|
41
46
|
1. 使用 artifact_save 保存 .py 文件
|
|
42
47
|
2. 文件必须可以直接 python xxx.py 运行
|
|
43
48
|
3. 包含完整 import、函数定义、使用示例
|
|
44
|
-
4.
|
|
49
|
+
4. 禁止在对话中输出大段代码片段代替文件
|
|
45
50
|
|
|
46
51
|
## 降级策略
|
|
47
|
-
|
|
48
|
-
纯 Python 转换失败 3 次 → 改用 execjs 方案
|
|
49
|
-
|
|
50
|
-
目标是保证最终输出可用的代码。
|
|
52
|
+
纯 Python 转换失败 3 次 → 改用 execjs 方案。目标是保证最终输出可用的代码。
|
|
51
53
|
`,
|
|
52
54
|
tools: [
|
|
53
55
|
...pythonTools,
|
|
@@ -7,16 +7,26 @@ import { tool } from '@langchain/core/tools';
|
|
|
7
7
|
import { getBrowserClient } from '../../browser/index.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* 通过 CDP 执行 JS(复用 session
|
|
10
|
+
* 通过 CDP 执行 JS(复用 session,带超时保护)
|
|
11
11
|
*/
|
|
12
|
-
async function evaluateViaCDP(client, expression) {
|
|
12
|
+
async function evaluateViaCDP(client, expression, timeout = 5000) {
|
|
13
13
|
const cdp = await client.getCDPSession();
|
|
14
14
|
if (!cdp) return null;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
try {
|
|
16
|
+
const result = await Promise.race([
|
|
17
|
+
cdp.send('Runtime.evaluate', {
|
|
18
|
+
expression,
|
|
19
|
+
returnByValue: true,
|
|
20
|
+
}),
|
|
21
|
+
new Promise((_, reject) =>
|
|
22
|
+
setTimeout(() => reject(new Error('CDP evaluate timeout')), timeout)
|
|
23
|
+
),
|
|
24
|
+
]);
|
|
25
|
+
return result.result?.value;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('[analysis:evaluateViaCDP] 超时或错误:', e.message);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
/**
|
|
@@ -9,16 +9,29 @@ import { getBrowser } from '../../browser/index.js';
|
|
|
9
9
|
import { getScreenshotPath } from './utils.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* 安全获取 CDP session,断开时返回友好错误而非 TypeError
|
|
13
13
|
*/
|
|
14
|
-
async function
|
|
14
|
+
async function safeCDP(browser) {
|
|
15
15
|
const cdp = await browser.getCDPSession();
|
|
16
|
-
if (!cdp) throw new Error('CDP session
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
if (!cdp) throw new Error('CDP session 不可用,浏览器可能已关闭或断开连接');
|
|
17
|
+
return cdp;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 通过 CDP 执行 JS(带超时保护)
|
|
22
|
+
*/
|
|
23
|
+
async function cdpEvaluate(browser, expression, returnByValue = true, timeout = 5000) {
|
|
24
|
+
const cdp = await safeCDP(browser);
|
|
25
|
+
const result = await Promise.race([
|
|
26
|
+
cdp.send('Runtime.evaluate', {
|
|
27
|
+
expression,
|
|
28
|
+
returnByValue,
|
|
29
|
+
awaitPromise: true,
|
|
30
|
+
}),
|
|
31
|
+
new Promise((_, reject) =>
|
|
32
|
+
setTimeout(() => reject(new Error('cdpEvaluate timeout (page loading/paused?)')), timeout)
|
|
33
|
+
),
|
|
34
|
+
]);
|
|
22
35
|
if (result.exceptionDetails) {
|
|
23
36
|
throw new Error(result.exceptionDetails.text || 'CDP evaluate error');
|
|
24
37
|
}
|
|
@@ -112,7 +125,7 @@ export const waitForSelector = tool(
|
|
|
112
125
|
export const reloadPage = tool(
|
|
113
126
|
async () => {
|
|
114
127
|
const browser = await getBrowser();
|
|
115
|
-
const cdp = await browser
|
|
128
|
+
const cdp = await safeCDP(browser);
|
|
116
129
|
await cdp.send('Page.reload');
|
|
117
130
|
const url = await cdpEvaluate(browser, 'location.href');
|
|
118
131
|
return JSON.stringify({ success: true, url });
|
|
@@ -130,7 +143,7 @@ export const reloadPage = tool(
|
|
|
130
143
|
export const goBack = tool(
|
|
131
144
|
async () => {
|
|
132
145
|
const browser = await getBrowser();
|
|
133
|
-
const cdp = await browser
|
|
146
|
+
const cdp = await safeCDP(browser);
|
|
134
147
|
const history = await cdp.send('Page.getNavigationHistory');
|
|
135
148
|
if (history.currentIndex > 0) {
|
|
136
149
|
const entry = history.entries[history.currentIndex - 1];
|
|
@@ -152,7 +165,7 @@ export const goBack = tool(
|
|
|
152
165
|
export const goForward = tool(
|
|
153
166
|
async () => {
|
|
154
167
|
const browser = await getBrowser();
|
|
155
|
-
const cdp = await browser
|
|
168
|
+
const cdp = await safeCDP(browser);
|
|
156
169
|
const history = await cdp.send('Page.getNavigationHistory');
|
|
157
170
|
if (history.currentIndex < history.entries.length - 1) {
|
|
158
171
|
const entry = history.entries[history.currentIndex + 1];
|
|
@@ -174,7 +187,7 @@ export const goForward = tool(
|
|
|
174
187
|
export const scrollPage = tool(
|
|
175
188
|
async ({ direction, distance }) => {
|
|
176
189
|
const browser = await getBrowser();
|
|
177
|
-
const cdp = await browser
|
|
190
|
+
const cdp = await safeCDP(browser);
|
|
178
191
|
const deltaY = direction === 'up' ? -distance : distance;
|
|
179
192
|
await cdp.send('Input.dispatchMouseEvent', {
|
|
180
193
|
type: 'mouseWheel', x: 100, y: 100, deltaX: 0, deltaY
|
|
@@ -350,7 +363,7 @@ export const getElementHtml = tool(
|
|
|
350
363
|
export const getCookies = tool(
|
|
351
364
|
async ({ domain, format }) => {
|
|
352
365
|
const browser = await getBrowser();
|
|
353
|
-
const cdp = await browser
|
|
366
|
+
const cdp = await safeCDP(browser);
|
|
354
367
|
|
|
355
368
|
// 获取当前页面 URL 用于过滤
|
|
356
369
|
const currentUrl = await cdpEvaluate(browser, 'location.href');
|
|
@@ -108,7 +108,7 @@ export const e2eTest = tool(
|
|
|
108
108
|
description: '端到端测试爬虫脚本',
|
|
109
109
|
schema: z.object({
|
|
110
110
|
script_path: z.string().describe('脚本路径'),
|
|
111
|
-
test_params: z.
|
|
111
|
+
test_params: z.object({}).passthrough().optional().describe('测试参数'),
|
|
112
112
|
}),
|
|
113
113
|
}
|
|
114
114
|
);
|
|
@@ -73,8 +73,8 @@ export const delegateCrawlerGeneration = tool(
|
|
|
73
73
|
xpath: z.string(),
|
|
74
74
|
type: z.string(),
|
|
75
75
|
})),
|
|
76
|
-
entry: z.
|
|
77
|
-
pagination: z.
|
|
76
|
+
entry: z.string().optional().describe('入口 URL 或选择器'),
|
|
77
|
+
pagination: z.string().optional().describe('分页选择器或 URL 模式'),
|
|
78
78
|
})),
|
|
79
79
|
}).describe('爬虫配置'),
|
|
80
80
|
domain: z.string().describe('目标网站域名'),
|
package/src/agent/tools/index.js
CHANGED
|
@@ -54,7 +54,7 @@ import { profileTools } from './profile.js';
|
|
|
54
54
|
import { runtimeTools } from './runtime.js';
|
|
55
55
|
import { debugTools } from './debug.js';
|
|
56
56
|
import { captureTools } from './capture.js';
|
|
57
|
-
import { browserTools } from './browser.js';
|
|
57
|
+
import { browserTools, clickElement, scrollPage, fillInput, getInteractiveElements, getPageInfo, hoverElement, pressKey } from './browser.js';
|
|
58
58
|
import { reportTools } from './report.js';
|
|
59
59
|
import { webcrackTools } from './webcrack.js';
|
|
60
60
|
import { preprocessTools } from './preprocess.js';
|
|
@@ -147,4 +147,6 @@ export const coreTools = [
|
|
|
147
147
|
...scratchpadTools,
|
|
148
148
|
// 爬虫代码生成(带 HITL 确认)
|
|
149
149
|
...crawlerGeneratorTools,
|
|
150
|
+
// 页面交互(自主数据搜寻:滚动加载、点击触发请求)
|
|
151
|
+
clickElement, scrollPage, fillInput, getInteractiveElements, getPageInfo, hoverElement, pressKey,
|
|
150
152
|
];
|
package/src/agent/tools/patch.js
CHANGED
|
@@ -21,7 +21,7 @@ export const generatePatch = tool(
|
|
|
21
21
|
description: '为缺失的环境属性生成补丁代码。',
|
|
22
22
|
schema: z.object({
|
|
23
23
|
property: z.string().describe('缺失的属性路径,如 navigator.userAgent'),
|
|
24
|
-
context: z.
|
|
24
|
+
context: z.object({}).passthrough().optional().describe('上下文信息'),
|
|
25
25
|
}),
|
|
26
26
|
}
|
|
27
27
|
);
|
package/src/agent/tools/store.js
CHANGED
|
@@ -30,7 +30,7 @@ export const saveToStore = tool(
|
|
|
30
30
|
type: z.enum(['env-module', 'crypto-pattern', 'obfuscation']).describe('类型'),
|
|
31
31
|
name: z.string().describe('名称'),
|
|
32
32
|
code: z.string().describe('代码'),
|
|
33
|
-
metadata: z.
|
|
33
|
+
metadata: z.object({}).passthrough().optional().describe('元数据'),
|
|
34
34
|
}),
|
|
35
35
|
}
|
|
36
36
|
);
|
package/src/browser/client.js
CHANGED
|
@@ -206,7 +206,11 @@ export class BrowserClient extends EventEmitter {
|
|
|
206
206
|
|
|
207
207
|
try {
|
|
208
208
|
// 通过简单的 Runtime.evaluate 验证 session 是否还活着
|
|
209
|
-
|
|
209
|
+
// 必须加超时:页面 loading/断点暂停时 Runtime.evaluate 会永远挂住
|
|
210
|
+
await Promise.race([
|
|
211
|
+
this.cdpSession.send('Runtime.evaluate', { expression: '1' }),
|
|
212
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('CDP health check timeout')), 3000)),
|
|
213
|
+
]);
|
|
210
214
|
this._cdpLastCheck = now;
|
|
211
215
|
return this.cdpSession;
|
|
212
216
|
} catch {
|
|
@@ -657,6 +657,22 @@ export function getAnalysisPanelScript() {
|
|
|
657
657
|
.deepspider-confirm-no {
|
|
658
658
|
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.15); color: #8b949e;
|
|
659
659
|
}
|
|
660
|
+
/* 恢复 session 横幅 */
|
|
661
|
+
.deepspider-resume-banner {
|
|
662
|
+
background: rgba(99, 179, 237, 0.08); border: 1px solid rgba(99, 179, 237, 0.2);
|
|
663
|
+
border-radius: 10px; padding: 12px 14px; margin: 4px 0;
|
|
664
|
+
}
|
|
665
|
+
.deepspider-resume-btn {
|
|
666
|
+
width: 100%; padding: 8px; border-radius: 8px; border: none;
|
|
667
|
+
background: linear-gradient(135deg, #63b3ed, #4299e1); color: #fff;
|
|
668
|
+
font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s;
|
|
669
|
+
}
|
|
670
|
+
.deepspider-resume-btn:hover { opacity: 0.85; }
|
|
671
|
+
.deepspider-resume-dismiss {
|
|
672
|
+
width: 100%; padding: 6px; border: none; background: none;
|
|
673
|
+
color: #8b949e; font-size: 11px; cursor: pointer; margin-top: 4px;
|
|
674
|
+
}
|
|
675
|
+
.deepspider-resume-dismiss:hover { color: #c9d1d9; }
|
|
660
676
|
.deepspider-msg-system {
|
|
661
677
|
background: transparent;
|
|
662
678
|
text-align: center;
|
|
@@ -1270,6 +1286,7 @@ export function getAnalysisPanelScript() {
|
|
|
1270
1286
|
bindFilePathClicks(messagesEl);
|
|
1271
1287
|
bindChoiceClicks(messagesEl);
|
|
1272
1288
|
bindConfirmClicks(messagesEl);
|
|
1289
|
+
bindResumeClicks(messagesEl);
|
|
1273
1290
|
}
|
|
1274
1291
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1275
1292
|
}
|
|
@@ -1286,6 +1303,10 @@ export function getAnalysisPanelScript() {
|
|
|
1286
1303
|
return renderChoicesMessage(m);
|
|
1287
1304
|
case 'confirm':
|
|
1288
1305
|
return renderConfirmMessage(m);
|
|
1306
|
+
case 'resume-available':
|
|
1307
|
+
return renderResumeMessage(m);
|
|
1308
|
+
case 'file-saved':
|
|
1309
|
+
return renderFileSavedMessage(m);
|
|
1289
1310
|
default:
|
|
1290
1311
|
return '<div class="deepspider-msg deepspider-msg-system">' + escapeHtml(JSON.stringify(m.data)) + '</div>';
|
|
1291
1312
|
}
|
|
@@ -1326,6 +1347,32 @@ export function getAnalysisPanelScript() {
|
|
|
1326
1347
|
return html;
|
|
1327
1348
|
}
|
|
1328
1349
|
|
|
1350
|
+
function renderResumeMessage(m) {
|
|
1351
|
+
if (m.answered) return '';
|
|
1352
|
+
const d = m.data;
|
|
1353
|
+
return '<div class="deepspider-resume-banner">' +
|
|
1354
|
+
'<div style="margin-bottom:6px;">检测到上次未完成的分析</div>' +
|
|
1355
|
+
'<div style="font-size:11px;color:#8b949e;margin-bottom:8px;">' +
|
|
1356
|
+
escapeHtml(d.domain) + ' · ' + escapeHtml(d.timeAgo) + ' · ' + escapeHtml(String(d.messageCount)) + '条消息</div>' +
|
|
1357
|
+
'<button class="deepspider-resume-btn" data-resume-thread="' + escapeHtml(d.threadId) + '">恢复上次分析</button>' +
|
|
1358
|
+
'<button class="deepspider-resume-dismiss" data-resume-dismiss="true">忽略</button>' +
|
|
1359
|
+
'</div>';
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function renderFileSavedMessage(m) {
|
|
1363
|
+
var d = m.data;
|
|
1364
|
+
var icon = d.type === 'py' ? '🐍' : d.type === 'report' ? '📊' : '📄';
|
|
1365
|
+
var label = d.type === 'py' ? 'Python 脚本' : d.type === 'report' ? '分析报告' : '文件';
|
|
1366
|
+
return '<div class="deepspider-msg deepspider-msg-system" style="background:#1a2332;border-left:3px solid #388bfd;padding:8px 12px;">' +
|
|
1367
|
+
'<div style="display:flex;align-items:center;gap:6px;">' +
|
|
1368
|
+
'<span>' + icon + '</span>' +
|
|
1369
|
+
'<span style="color:#58a6ff;">' + escapeHtml(label) + '已保存</span>' +
|
|
1370
|
+
'</div>' +
|
|
1371
|
+
'<div class="deepspider-file-path" style="font-size:11px;color:#8b949e;margin-top:4px;cursor:pointer;" data-file-path="' + escapeHtml(d.path) + '">' +
|
|
1372
|
+
escapeHtml(d.path) +
|
|
1373
|
+
'</div></div>';
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1329
1376
|
function bindChoiceClicks(container) {
|
|
1330
1377
|
container.querySelectorAll('.deepspider-choice-btn:not([style*="pointer-events"])').forEach(btn => {
|
|
1331
1378
|
btn.onclick = () => {
|
|
@@ -1367,6 +1414,31 @@ export function getAnalysisPanelScript() {
|
|
|
1367
1414
|
});
|
|
1368
1415
|
}
|
|
1369
1416
|
|
|
1417
|
+
function bindResumeClicks(container) {
|
|
1418
|
+
container.querySelectorAll('.deepspider-resume-btn').forEach(btn => {
|
|
1419
|
+
btn.onclick = () => {
|
|
1420
|
+
const threadId = btn.dataset.resumeThread;
|
|
1421
|
+
const msgs = deepspider.chatMessages;
|
|
1422
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1423
|
+
if (msgs[i].type === 'resume-available') { msgs[i].answered = true; break; }
|
|
1424
|
+
}
|
|
1425
|
+
addMessage('system', '正在恢复上次分析...');
|
|
1426
|
+
if (typeof __deepspider_send__ === 'function') {
|
|
1427
|
+
__deepspider_send__(JSON.stringify({ __ds__: true, type: 'resume', threadId }));
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
});
|
|
1431
|
+
container.querySelectorAll('.deepspider-resume-dismiss').forEach(btn => {
|
|
1432
|
+
btn.onclick = () => {
|
|
1433
|
+
const msgs = deepspider.chatMessages;
|
|
1434
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1435
|
+
if (msgs[i].type === 'resume-available') { msgs[i].answered = true; break; }
|
|
1436
|
+
}
|
|
1437
|
+
renderMessages();
|
|
1438
|
+
};
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1370
1442
|
function escapeHtml(str) {
|
|
1371
1443
|
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
1372
1444
|
}
|