deepspider 0.4.0 → 0.5.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 +9 -19
- package/package.json +1 -1
- package/src/agent/core/StreamHandler.js +37 -20
- package/src/agent/prompts/system.js +15 -1
- package/src/agent/skills/crawler/SKILL.md +86 -0
- package/src/agent/skills/crawler/evolved.md +14 -13
- package/src/agent/skills/general/evolved.md +12 -1
- package/src/agent/skills/js2python/SKILL.md +40 -0
- package/src/agent/skills/js2python/evolved.md +14 -6
- package/src/agent/skills/sandbox/SKILL.md +33 -0
- package/src/agent/skills/sandbox/evolved.md +12 -5
- package/src/agent/skills/static-analysis/SKILL.md +39 -0
- package/src/agent/skills/static-analysis/evolved.md +88 -6
- package/src/agent/tools/evolve.js +47 -8
- package/src/agent/tools/index.js +4 -2
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ pnpm run setup:crypto # 安装 Python 加密库(可选)
|
|
|
48
48
|
|
|
49
49
|
### 配置
|
|
50
50
|
|
|
51
|
-
DeepSpider
|
|
51
|
+
DeepSpider 支持兼容 Anthropic 格式的 API 供应商。推荐使用 Claude API 以获得最佳效果。
|
|
52
52
|
|
|
53
53
|
| 配置键 | 环境变量 | 说明 |
|
|
54
54
|
|--------|----------|------|
|
|
@@ -59,33 +59,23 @@ DeepSpider 需要配置 LLM API 才能运行。支持任何兼容 OpenAI 格式
|
|
|
59
59
|
|
|
60
60
|
优先级:环境变量 > 配置文件 (`~/.deepspider/config/settings.json`) > 默认值
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
**方式一:CLI 命令(推荐)**
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
deepspider config set apiKey sk-xxx
|
|
66
|
-
deepspider config set baseUrl https://api.
|
|
67
|
-
deepspider config set model
|
|
65
|
+
deepspider config set apiKey sk-ant-api03-xxx
|
|
66
|
+
deepspider config set baseUrl https://api.anthropic.com
|
|
67
|
+
deepspider config set model claude-opus-4-6
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
**方式二:环境变量**
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
-
export DEEPSPIDER_API_KEY=sk-xxx
|
|
74
|
-
export DEEPSPIDER_BASE_URL=https://api.
|
|
75
|
-
export DEEPSPIDER_MODEL=
|
|
73
|
+
export DEEPSPIDER_API_KEY=sk-ant-api03-xxx
|
|
74
|
+
export DEEPSPIDER_BASE_URL=https://api.anthropic.com
|
|
75
|
+
export DEEPSPIDER_MODEL=claude-opus-4-6
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
# OpenAI
|
|
82
|
-
deepspider config set baseUrl https://api.openai.com/v1
|
|
83
|
-
deepspider config set model gpt-4o
|
|
84
|
-
|
|
85
|
-
# DeepSeek
|
|
86
|
-
deepspider config set baseUrl https://api.deepseek.com/v1
|
|
87
|
-
deepspider config set model deepseek-chat
|
|
88
|
-
```
|
|
78
|
+
> **提示**:也支持其他兼容 Anthropic 格式的 API 供应商。
|
|
89
79
|
|
|
90
80
|
### 使用
|
|
91
81
|
|
package/package.json
CHANGED
|
@@ -55,6 +55,20 @@ export class StreamHandler {
|
|
|
55
55
|
this.fullResponse = '';
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* 从 on_chat_model_end 提取最终响应
|
|
60
|
+
* 优先使用流式累积的 fullResponse,兜底使用 end 事件内容
|
|
61
|
+
*/
|
|
62
|
+
_extractFinalResponse(output) {
|
|
63
|
+
if (!output?.content) return null;
|
|
64
|
+
const streamContent = this.fullResponse;
|
|
65
|
+
const endContent = typeof output.content === 'string'
|
|
66
|
+
? output.content
|
|
67
|
+
: output.content.filter(c => c.type === 'text').map(c => c.text).join('');
|
|
68
|
+
// 使用较长的一方(通常流式累积的内容更完整)
|
|
69
|
+
return streamContent.length >= endContent.length ? streamContent : endContent;
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
/**
|
|
59
73
|
* 流式对话 - 显示思考过程(带重试)
|
|
60
74
|
*/
|
|
@@ -99,12 +113,10 @@ export class StreamHandler {
|
|
|
99
113
|
await this._handleStreamEvent(event);
|
|
100
114
|
|
|
101
115
|
if (event.event === 'on_chat_model_end') {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
finalResponse =
|
|
105
|
-
|
|
106
|
-
: output.content.filter(c => c.type === 'text').map(c => c.text).join('');
|
|
107
|
-
this.debug(`chatStream: 收到最终响应, 长度=${finalResponse.length}`);
|
|
116
|
+
const extracted = this._extractFinalResponse(event.data?.output);
|
|
117
|
+
if (extracted) {
|
|
118
|
+
finalResponse = extracted;
|
|
119
|
+
this.debug(`chatStream: 最终响应, 长度=${finalResponse.length}`);
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
}
|
|
@@ -165,11 +177,8 @@ export class StreamHandler {
|
|
|
165
177
|
|
|
166
178
|
if (event.event === 'on_chat_model_end') {
|
|
167
179
|
const output = event.data?.output;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
? output.content
|
|
171
|
-
: output.content.filter(c => c.type === 'text').map(c => c.text).join('');
|
|
172
|
-
}
|
|
180
|
+
const extracted = this._extractFinalResponse(output);
|
|
181
|
+
if (extracted) finalResponse = extracted;
|
|
173
182
|
}
|
|
174
183
|
}
|
|
175
184
|
|
|
@@ -245,11 +254,8 @@ export class StreamHandler {
|
|
|
245
254
|
|
|
246
255
|
if (event.event === 'on_chat_model_end') {
|
|
247
256
|
const output = event.data?.output;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
? output.content
|
|
251
|
-
: output.content.filter(c => c.type === 'text').map(c => c.text).join('');
|
|
252
|
-
}
|
|
257
|
+
const extracted = this._extractFinalResponse(output);
|
|
258
|
+
if (extracted) finalResponse = extracted;
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
|
|
@@ -415,12 +421,23 @@ export class StreamHandler {
|
|
|
415
421
|
switch (eventType) {
|
|
416
422
|
case 'on_chat_model_stream':
|
|
417
423
|
let chunk = data?.chunk?.content;
|
|
418
|
-
|
|
419
|
-
|
|
424
|
+
// 处理多种内容格式:字符串或数组
|
|
425
|
+
let textChunk = '';
|
|
426
|
+
if (typeof chunk === 'string') {
|
|
427
|
+
textChunk = chunk;
|
|
428
|
+
} else if (Array.isArray(chunk)) {
|
|
429
|
+
// 数组格式:提取所有 text 类型的内容
|
|
430
|
+
textChunk = chunk.filter(c => c.type === 'text').map(c => c.text).join('');
|
|
431
|
+
} else if (chunk?.text) {
|
|
432
|
+
// 对象格式:提取 text 字段
|
|
433
|
+
textChunk = chunk.text;
|
|
434
|
+
}
|
|
435
|
+
if (textChunk) {
|
|
436
|
+
textChunk = cleanDSML(textChunk);
|
|
420
437
|
// CLI 侧仍流式输出
|
|
421
|
-
process.stdout.write(
|
|
438
|
+
process.stdout.write(textChunk);
|
|
422
439
|
// 面板侧只累积,不推送
|
|
423
|
-
this.fullResponse = (this.fullResponse || '') +
|
|
440
|
+
this.fullResponse = (this.fullResponse || '') + textChunk;
|
|
424
441
|
}
|
|
425
442
|
break;
|
|
426
443
|
|
|
@@ -44,11 +44,25 @@ export const systemPrompt = `你是 DeepSpider,一个智能爬虫 Agent。你
|
|
|
44
44
|
|
|
45
45
|
1. **已捕获的数据**(浏览器拦截的请求/响应、Hook 记录)— 最可靠
|
|
46
46
|
2. **用户提供的信息**(选中的元素、补充说明)— 直接采信
|
|
47
|
-
3. **工具实时获取**(run_node_code 验证、get_request_detail 查看)— 需验证
|
|
47
|
+
3. **工具实时获取**(run_node_code / run_python_code 验证、get_request_detail 查看)— 需验证
|
|
48
48
|
4. **模型推断**(基于经验猜测加密类型、参数含义)— 仅作参考,必须验证
|
|
49
49
|
|
|
50
50
|
**禁止仅凭推断就下结论。所有关键判断必须有数据支撑。**
|
|
51
51
|
|
|
52
|
+
## 代码执行能力
|
|
53
|
+
|
|
54
|
+
你有两个代码执行工具可用:
|
|
55
|
+
|
|
56
|
+
| 工具 | 用途 | 示例 |
|
|
57
|
+
|------|------|------|
|
|
58
|
+
| \`run_node_code\` | JS代码快速验证、加密库调用测试 | \`const CryptoJS = require('crypto-js'); ...\` |
|
|
59
|
+
| \`run_python_code\` | Python代码验证、加密算法对比、数据处理 | \`from gmssl import sm2; ...\` |
|
|
60
|
+
|
|
61
|
+
**使用原则**:
|
|
62
|
+
- 快速验证假设时优先用 \`run_node_code\`(JS环境与浏览器更接近)
|
|
63
|
+
- 需要验证 Python 加密实现时用 \`run_python_code\`
|
|
64
|
+
- 复杂的加密代码转换仍需委托 js2python 子代理
|
|
65
|
+
|
|
52
66
|
## 浏览器面板
|
|
53
67
|
|
|
54
68
|
当用户通过浏览器面板发送消息时(消息以"[浏览器已就绪]"开头):
|
|
@@ -68,3 +68,89 @@ class Crawler:
|
|
|
68
68
|
- 请求间隔随机化(1-3s)
|
|
69
69
|
- 异常重试(网络超时、频控返回)
|
|
70
70
|
- 数据去重(基于唯一标识)
|
|
71
|
+
|
|
72
|
+
## 加密参数处理
|
|
73
|
+
|
|
74
|
+
### AES-CFB 加密请求
|
|
75
|
+
**场景**:招标采购等政府网站常用 AES-CFB 加密请求参数
|
|
76
|
+
|
|
77
|
+
**实现要点**:
|
|
78
|
+
```python
|
|
79
|
+
from Crypto.Cipher import AES
|
|
80
|
+
import base64
|
|
81
|
+
|
|
82
|
+
def encrypt_cfb(data, key, iv):
|
|
83
|
+
# CFB 模式,segment_size=128 与 CryptoJS 兼容
|
|
84
|
+
cipher = AES.new(key[:16], AES.MODE_CFB, iv[:16], segment_size=128)
|
|
85
|
+
encrypted = cipher.encrypt(data.encode())
|
|
86
|
+
return base64.b64encode(encrypted).decode()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**注意**:CFB 模式不需要填充,segment_size=128 是 CryptoJS 默认配置
|
|
90
|
+
|
|
91
|
+
### 特殊字符编码
|
|
92
|
+
**场景**:某些竞赛/测试站点使用特殊分隔符
|
|
93
|
+
|
|
94
|
+
**示例 - 猿人学 m 参数**:
|
|
95
|
+
```python
|
|
96
|
+
import hashlib
|
|
97
|
+
import urllib.parse
|
|
98
|
+
|
|
99
|
+
timestamp = int(time.time() * 1000) + 100000000
|
|
100
|
+
md5_val = hashlib.md5(str(timestamp).encode()).hexdigest()
|
|
101
|
+
# 注意:使用中文竖线 '丨' 而非英文 '|'
|
|
102
|
+
m = f"{md5_val}丨{timestamp}"
|
|
103
|
+
m_encoded = urllib.parse.quote(m) # URL 编码
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 政府公示网站爬虫
|
|
107
|
+
|
|
108
|
+
**典型架构**:列表接口 + 详情接口分离设计
|
|
109
|
+
|
|
110
|
+
**推荐策略**:
|
|
111
|
+
```python
|
|
112
|
+
# 第一步:获取所有 ID
|
|
113
|
+
list_response = fetch_list(page)
|
|
114
|
+
ids = extract_ids(list_response)
|
|
115
|
+
|
|
116
|
+
# 第二步:批量请求详情(添加友好延迟)
|
|
117
|
+
for id in ids:
|
|
118
|
+
detail = fetch_detail(id)
|
|
119
|
+
save(detail)
|
|
120
|
+
time.sleep(random.uniform(0.5, 1)) # 友好延迟
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**注意事项**:
|
|
124
|
+
- 第 1 页可能不需要 page 参数,第 2+ 页需要
|
|
125
|
+
- 政府网站建议 0.5-1 秒随机延迟
|
|
126
|
+
- 关注数据更新频率,避免过度抓取
|
|
127
|
+
|
|
128
|
+
## 端到端验证
|
|
129
|
+
|
|
130
|
+
### Sign 参数验证失败排查
|
|
131
|
+
当加密逻辑正确但请求仍失败时,检查:
|
|
132
|
+
|
|
133
|
+
**1. 请求头完整性**
|
|
134
|
+
```python
|
|
135
|
+
# 某些接口需要特定请求头配合
|
|
136
|
+
headers = {
|
|
137
|
+
'UUIDAH': uuid_value, # 可能需要在页面中获取
|
|
138
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
139
|
+
# ... 其他必要头
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**2. Sign 时效性**
|
|
144
|
+
- 检查 sign 是否绑定了时间戳
|
|
145
|
+
- 确认服务器时间差(本地时间 vs 服务器时间)
|
|
146
|
+
- 验证 sign 有效期(有些只有几分钟)
|
|
147
|
+
|
|
148
|
+
**3. 验证流程**
|
|
149
|
+
```
|
|
150
|
+
浏览器生成 sign → 复制到 Python 测试
|
|
151
|
+
↓
|
|
152
|
+
是否一致?
|
|
153
|
+
↓
|
|
154
|
+
是 → 检查请求头/时效性
|
|
155
|
+
否 → 检查加密参数(模式、编码、密钥)
|
|
156
|
+
```
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
---
|
|
2
|
-
total:
|
|
3
|
-
last_merged:
|
|
2
|
+
total: 0
|
|
3
|
+
last_merged: 2026-03-03
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## 核心经验
|
|
7
7
|
|
|
8
8
|
<!-- 经过验证的高价值经验 -->
|
|
9
|
+
<!-- [已合并] AES-CFB 加密爬虫实现 → SKILL.md 加密参数处理章节 -->
|
|
10
|
+
<!-- [已合并] 猿人学 m 参数生成 → SKILL.md 特殊字符编码章节 -->
|
|
11
|
+
<!-- [已合并] 政府公示网站爬虫最佳实践 → SKILL.md 政府公示网站爬虫章节 -->
|
|
12
|
+
|
|
13
|
+
## 近期发现
|
|
14
|
+
|
|
15
|
+
<!-- 最近发现,FIFO 滚动,最多保留 10 条 -->
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
9
21
|
|
|
10
|
-
### [2026-02-02] AES-CFB 加密爬虫实现模式
|
|
11
|
-
**场景**: 招标采购网站使用 AES-CFB 加密请求参数,Key和IV为 "jinrun2024secret",需要实现 Python 加密模块并集成到爬虫中
|
|
12
|
-
**经验**: AES-CFB 加密网站爬虫开发时,使用 pycryptodome 的 AES.MODE_CFB 模式,segment_size=128,密钥和IV需要截取16字节。加密后Base64编码,请求体直接发送密文字符串。
|
|
13
22
|
|
|
14
|
-
### [2026-02-13] 猿人学题目m参数生成规范
|
|
15
|
-
**场景**: 猿人学第1题,需要生成m参数:timestamp = Date.now() + 100000000,md5 = MD5(timestamp),m = md5 + '丨' + timestamp
|
|
16
|
-
**经验**: 注意使用中文竖线'丨'而非英文竖线'|',需要对m参数进行URL编码,第1页不传page参数,第2-5页需要page参数
|
|
17
23
|
|
|
18
|
-
### [2026-02-14] 政府公示网站爬虫最佳实践
|
|
19
|
-
**场景**: 金昌市信用公示网站行政处罚数据爬取,采用列表+详情两步爬取策略
|
|
20
|
-
**经验**: 政府公示网站通常采用列表接口+详情接口的分离设计,应先获取所有ID再批量请求详情,并添加0.5-1秒随机延迟以示友好
|
|
21
24
|
|
|
22
|
-
## 近期发现
|
|
23
25
|
|
|
24
|
-
<!-- 最近发现,FIFO 滚动,最多保留 10 条 -->
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
total: 0
|
|
3
|
-
last_merged:
|
|
3
|
+
last_merged: 2026-03-03
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## 核心经验
|
|
7
7
|
|
|
8
8
|
<!-- 经过验证的高价值经验 -->
|
|
9
|
+
<!-- [已合并] SM2 sign 验证问题 → crawler/SKILL.md 端到端验证章节 -->
|
|
9
10
|
|
|
10
11
|
## 近期发现
|
|
11
12
|
|
|
12
13
|
<!-- 最近发现,FIFO 滚动,最多保留 10 条 -->
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
@@ -25,6 +25,46 @@ description: |
|
|
|
25
25
|
|
|
26
26
|
**CFB 模式 segment_size:** CryptoJS AES-CFB 默认 CFB128,PyCryptodome 默认 CFB8。必须指定 `segment_size=128`。
|
|
27
27
|
|
|
28
|
+
## 国密 SM2 转换
|
|
29
|
+
|
|
30
|
+
### 模式对齐(重要)
|
|
31
|
+
**关键经验**:gmssl 默认使用 C1C2C3 模式 (mode=0),而 sm-crypto 默认使用 C1C3C2 模式 (mode=1),必须显式设置 mode=1 才能兼容。
|
|
32
|
+
|
|
33
|
+
| 模式 | sm-crypto (JS) | gmssl (Python) |
|
|
34
|
+
|------|----------------|----------------|
|
|
35
|
+
| C1C2C3 | mode=0 | mode=0 (默认) |
|
|
36
|
+
| C1C3C2 | mode=1 (默认) | mode=1 |
|
|
37
|
+
|
|
38
|
+
**正确转换**:
|
|
39
|
+
```python
|
|
40
|
+
from gmssl import sm2
|
|
41
|
+
|
|
42
|
+
# JS: sm2.doEncrypt(plain, pubKey, 1) # mode=1, C1C3C2
|
|
43
|
+
# Python 必须显式设置 mode=1 才能兼容
|
|
44
|
+
sm2_crypt = sm2.CryptSM2(public_key=pub_key, private_key="", mode=1)
|
|
45
|
+
cipher = sm2_crypt.encrypt(plain_bytes)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 明文编码差异
|
|
49
|
+
**陷阱**:sm-crypto 的 `doEncrypt` 接收 **hex 字符串**,Python gmssl 接收 **bytes**
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# JS: sm2.doEncrypt(sha1Hex, pubKey, 1)
|
|
53
|
+
# sha1Hex 是 hex 字符串如 "a1b2c3..."
|
|
54
|
+
|
|
55
|
+
# Python 错误做法
|
|
56
|
+
plain_bytes = bytes.fromhex(sha1_hex) # ❌
|
|
57
|
+
|
|
58
|
+
# Python 正确做法
|
|
59
|
+
plain_bytes = sha1_hex.encode() # ✅ 直接编码字符串
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### SM2 加密随机性
|
|
63
|
+
SM2 加密结果每次不同(内置随机数),这是正常现象。验证时应:
|
|
64
|
+
1. 解密验证是否能还原原文
|
|
65
|
+
2. 对比密文长度和格式(04 开头 hex)
|
|
66
|
+
3. 多次执行确认稳定性
|
|
67
|
+
|
|
28
68
|
## 降级策略
|
|
29
69
|
|
|
30
70
|
纯 Python 失败 3 次 → 改用 execjs 直接执行 JS。
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
---
|
|
2
|
-
total:
|
|
3
|
-
last_merged: 2026-
|
|
2
|
+
total: 0
|
|
3
|
+
last_merged: 2026-03-03
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## 核心经验
|
|
7
7
|
|
|
8
8
|
<!-- 经过验证的高价值经验 -->
|
|
9
9
|
<!-- [已合并] CFB segment_size 差异 → SKILL.md -->
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
**场景**: credit.ah.gov.cn SM2 sign 生成,JS 用 sm-crypto doEncrypt(sha1Hex, pubKey, 1),传入的是 hex 字符串,Python 需用 sha1_hex.encode() 而非 bytes.fromhex(sha1_hex)
|
|
13
|
-
**经验**: sm-crypto 的 doEncrypt 接收字符串明文,Python gmssl 接收 bytes,两者对齐时需用 str.encode() 而非 bytes.fromhex()
|
|
10
|
+
<!-- [已合并] sm-crypto 明文编码差异 → SKILL.md 国密 SM2 转换章节 -->
|
|
11
|
+
<!-- [已合并] SM2 模式对齐 → SKILL.md 国密 SM2 转换章节 -->
|
|
14
12
|
|
|
15
13
|
## 近期发现
|
|
16
14
|
|
|
17
15
|
<!-- 最近发现,FIFO 滚动,最多保留 10 条 -->
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
@@ -55,3 +55,36 @@ description: |
|
|
|
55
55
|
- 加密结果:如果有 IV/随机数,固定后再比较
|
|
56
56
|
- 时间戳相关:Hook Date.now() 返回固定值
|
|
57
57
|
- 多次执行:确认结果稳定性(排除随机因素)
|
|
58
|
+
|
|
59
|
+
## Python 加密代码验证
|
|
60
|
+
|
|
61
|
+
### JS-to-Python 交叉验证流程
|
|
62
|
+
当把 JS 加密转换为 Python 后,验证正确性:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# 1. 分析算法参数
|
|
66
|
+
# - 密钥长度、IV、模式、填充方式
|
|
67
|
+
|
|
68
|
+
# 2. 交叉验证(推荐 Web Crypto API 或 NodeJS)
|
|
69
|
+
# 用原始 JS 生成测试用例,对比 Python 输出
|
|
70
|
+
|
|
71
|
+
# 3. 边界测试
|
|
72
|
+
test_cases = [
|
|
73
|
+
"", # 空字符串
|
|
74
|
+
"abc", # 普通 ASCII
|
|
75
|
+
"中文测试", # 中文
|
|
76
|
+
"!@#$%^&*()", # 特殊字符
|
|
77
|
+
"a" * 1000, # 长文本
|
|
78
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### AES-CFB 验证要点
|
|
82
|
+
```python
|
|
83
|
+
from Crypto.Cipher import AES
|
|
84
|
+
import base64
|
|
85
|
+
|
|
86
|
+
# 与 Web Crypto API 对比时确认:
|
|
87
|
+
# - segment_size=128(不是 8)
|
|
88
|
+
# - CFB 模式不需要填充
|
|
89
|
+
# - 密钥和 IV 截取 16 字节
|
|
90
|
+
```
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
---
|
|
2
|
-
total:
|
|
3
|
-
last_merged:
|
|
2
|
+
total: 0
|
|
3
|
+
last_merged: 2026-03-03
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## 核心经验
|
|
7
7
|
|
|
8
8
|
<!-- 经过验证的高价值经验 -->
|
|
9
|
+
<!-- [已合并] Python 加密代码验证流程 → SKILL.md Python 加密代码验证章节 -->
|
|
9
10
|
|
|
10
11
|
## 近期发现
|
|
11
12
|
|
|
12
13
|
<!-- 最近发现,FIFO 滚动,最多保留 10 条 -->
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
@@ -16,6 +16,19 @@ description: |
|
|
|
16
16
|
|
|
17
17
|
**从请求参数反推:** 找到加密参数名(如 `sign`),全局搜索赋值位置。
|
|
18
18
|
|
|
19
|
+
**混淆代码中的常量定位:**
|
|
20
|
+
混淆代码中的关键密钥/常量往往以简单变量赋值形式存在:
|
|
21
|
+
- 搜索模式:`varName='...'` 或 `_0xabc='...'`(混淆变量名=字符串)
|
|
22
|
+
- 优先检查:代码顶部或函数外部的直接字符串赋值
|
|
23
|
+
- 技巧:比搜索密钥内容更有效,因为混淆后的变量名是固定的
|
|
24
|
+
|
|
25
|
+
**示例:**
|
|
26
|
+
```javascript
|
|
27
|
+
// 混淆代码中直接定义的常量
|
|
28
|
+
var _tt='43e02a48ffb79f98e5bf5872e1e052f80d3300...'
|
|
29
|
+
var _0x5f2e='SM2_PUBLIC_KEY'
|
|
30
|
+
```
|
|
31
|
+
|
|
19
32
|
## 混淆器识别
|
|
20
33
|
|
|
21
34
|
| 特征 | 类型 | 难度 |
|
|
@@ -172,6 +185,12 @@ C1C2C3 格式 (旧):
|
|
|
172
185
|
|
|
173
186
|
**判断技巧**: 以 `04` 开头的hex + 长度约97+N = SM2
|
|
174
187
|
|
|
188
|
+
**SM2 公钥提取技巧**:
|
|
189
|
+
- 搜索关键词:`SM2`, `SM2Cipher`, `sm2.encrypt`, `sm2.doEncrypt`
|
|
190
|
+
- 公钥特征:以 `04` 开头的 130+ 字符 hex 字符串(未压缩公钥点)
|
|
191
|
+
- 模式标识:查找 `C1C3C2` 或 `C1C2C3` 模式参数
|
|
192
|
+
- 政企网站中 SM2 越来越常见,优先检查国密算法
|
|
193
|
+
|
|
175
194
|
### JWT Token 特征
|
|
176
195
|
|
|
177
196
|
```
|
|
@@ -297,6 +316,26 @@ sign = MD5(timestamp + token + data)
|
|
|
297
316
|
- 同一个请求有多个加密参数 → 可能共享中间值
|
|
298
317
|
- 参数名含 `sign`/`token`/`ticket` → 通常是最外层
|
|
299
318
|
|
|
319
|
+
### 多层哈希嵌套识别(重要)
|
|
320
|
+
|
|
321
|
+
**场景**:哈希计算结果与预期不符时,检查是否存在嵌套模式
|
|
322
|
+
|
|
323
|
+
**常见嵌套模式:**
|
|
324
|
+
```
|
|
325
|
+
单层: SHA1(data)
|
|
326
|
+
双层: SHA1(Base64(SHA1(data)))
|
|
327
|
+
三层: SHA1(SHA1(Base64(SHA1(data))))
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**识别方法:**
|
|
331
|
+
1. 逐行分析 CryptoJS 调用链
|
|
332
|
+
2. 关注中间结果的编码转换(Base64/Hex)
|
|
333
|
+
3. 检查同一数据是否被多次哈希
|
|
334
|
+
|
|
335
|
+
**线索关键词:**
|
|
336
|
+
- 连续的 `.SHA1(`, `.MD5(`, `.encrypt(` 调用
|
|
337
|
+
- 中间穿插的 `.Base64.stringify(`, `.Hex.stringify(`
|
|
338
|
+
|
|
300
339
|
### 参数来源分类
|
|
301
340
|
|
|
302
341
|
| 来源 | 特征 | 追踪方式 |
|
|
@@ -1,16 +1,98 @@
|
|
|
1
1
|
---
|
|
2
|
-
total:
|
|
3
|
-
last_merged:
|
|
2
|
+
total: 3
|
|
3
|
+
last_merged: 2026-03-03
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## 核心经验
|
|
7
7
|
|
|
8
8
|
<!-- 经过验证的高价值经验 -->
|
|
9
|
+
<!-- [已合并] 混淆代码常量定位 → SKILL.md 加密入口定位章节 -->
|
|
10
|
+
<!-- [已合并] 多层哈希嵌套识别 → SKILL.md 加密链路追踪章节 -->
|
|
11
|
+
<!-- [已合并] SM2 公钥提取技巧 → SKILL.md 国密 SM2 密文特征章节 -->
|
|
12
|
+
|
|
13
|
+
### [2026-03-03] SM2加密公钥格式
|
|
14
|
+
|
|
15
|
+
**一句话结论**: SM2加密时,公钥需要以04开头,格式为:'04' + x坐标(64字符) + y坐标(64字符),总共130字符
|
|
16
|
+
|
|
17
|
+
**场景**: 安徽省信用信息公示平台sign生成使用SM2加密,公钥需要04前缀
|
|
18
|
+
|
|
19
|
+
**技术细节**:
|
|
20
|
+
| 项目 | 值/说明 |
|
|
21
|
+
|------|---------|
|
|
22
|
+
| 公钥格式 | 04 + x(64) + y(64) = 130字符 |
|
|
23
|
+
| 加密模式 | C1C3C2 |
|
|
24
|
+
| 加密结果长度 | 明文长度决定,56字节明文加密后得到304字符(去掉04前缀) |
|
|
25
|
+
| 原始密钥长度 | 128字符(64+64) |
|
|
26
|
+
|
|
27
|
+
**正确做法**:
|
|
28
|
+
```python
|
|
29
|
+
const publicKey = '04' + key.substring(0, 64) + key.substring(64);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**错误陷阱** ⚠️:
|
|
33
|
+
```python
|
|
34
|
+
const publicKey = key.substring(0, 64); // 缺少04前缀和y坐标
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**为什么**: SM2算法使用非压缩格式的公钥表示,04表示非压缩点,后面跟着x和y坐标
|
|
38
|
+
|
|
39
|
+
### [2026-03-03] 国密SM2算法逆向分析
|
|
40
|
+
|
|
41
|
+
**一句话结论**: 当遇到国密SM2加密时,可以使用sm-crypto库进行还原,公钥通常以04开头(非压缩格式)
|
|
42
|
+
|
|
43
|
+
**场景**: 安徽省信用信息公共服务平台使用SM2国密算法进行请求签名加密
|
|
44
|
+
|
|
45
|
+
**技术细节**:
|
|
46
|
+
| 项目 | 值/说明 |
|
|
47
|
+
|------|---------|
|
|
48
|
+
| 算法类型 | SM2国密加密 |
|
|
49
|
+
| 加密格式 | |sha1_hash|activeTitle|timestamp| |
|
|
50
|
+
| 公钥格式 | 04 + 128位十六进制字符串 |
|
|
51
|
+
| 依赖库 | sm-crypto |
|
|
52
|
+
| 哈希算法 | SHA1 |
|
|
53
|
+
|
|
54
|
+
**正确做法**:
|
|
55
|
+
```python
|
|
56
|
+
const smCrypto = require('sm-crypto');
|
|
57
|
+
const publicKey = '04' + '3e02a48...';
|
|
58
|
+
const encrypted = smCrypto.sm2.doEncrypt(data, publicKey, 1);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**为什么**: 国密算法是中国标准,使用专门的sm-crypto库比通用加密库更可靠
|
|
62
|
+
|
|
63
|
+
**举一反三**:
|
|
64
|
+
- 国密SM3/SM4算法
|
|
65
|
+
- SM2签名验证
|
|
66
|
+
- CryptoJS与sm-crypto对比
|
|
67
|
+
|
|
68
|
+
### [2026-03-03] 安徽省信用信息公示平台SM2 Sign分析
|
|
69
|
+
|
|
70
|
+
**一句话结论**: 列表页和详情页的sign生成逻辑不同:列表页使用浏览器指纹SHA1+Base64+SM2加密;详情页使用serverId+columnId拼接后SHA1+Base64+SM2加密。两者使用相同的SM2公钥和C1C3C2模式。
|
|
71
|
+
|
|
72
|
+
**场景**: 分析credit.ah.gov.cn网站的sign参数加密,发现列表页和详情页使用不同的加密逻辑
|
|
73
|
+
|
|
74
|
+
**技术细节**:
|
|
75
|
+
| 项目 | 值/说明 |
|
|
76
|
+
|------|---------|
|
|
77
|
+
| sign前缀 | 04 |
|
|
78
|
+
| sign长度 | 306字符 |
|
|
79
|
+
| 公钥 | 04e7780d97a923e7fa1e2d8c4f1f0c54006aabca7f95b6aaa339ab03c6130edde50bf676ce8781f3f82fa8a5b85385b0bb28e381dda21e7fd4cb17d6d910e8d389 |
|
|
80
|
+
| 加密算法 | SM2 C1C3C2 |
|
|
81
|
+
| 哈希算法 | SHA1 |
|
|
82
|
+
| 编码方式 | Base64 |
|
|
83
|
+
|
|
84
|
+
**正确做法**:
|
|
85
|
+
```python
|
|
86
|
+
详情页:data = serverId + columnId → SHA1 → Base64 → SM2加密
|
|
87
|
+
列表页:data = SHA1(fingerprint) → Base64 → SM2加密
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**为什么**: 网站不同接口可能使用不同的加密数据源,需要分别分析每个接口的sign生成逻辑,不能假设所有接口使用相同的算法
|
|
91
|
+
|
|
92
|
+
**举一反三**:
|
|
93
|
+
- 其他政府网站可能也有类似的区分
|
|
94
|
+
- SM2加密在政务系统中广泛使用
|
|
9
95
|
|
|
10
96
|
## 近期发现
|
|
11
97
|
|
|
12
98
|
<!-- 最近发现,FIFO 滚动,最多保留 10 条 -->
|
|
13
|
-
|
|
14
|
-
### [2026-03-02] 混淆JS中硬编码密钥定位技巧
|
|
15
|
-
**场景**: 在分析 credit.ah.gov.cn 的 sign 参数加密时,发现密钥通过混淆代码中的变量赋值。通过搜索 `_aa=`, `_bb=` 等模式,成功定位到硬编码的 SM2 公钥和常量。
|
|
16
|
-
**经验**: 混淆代码中的硬编码密钥通常以简单变量赋值形式存在,搜索 `varName=` 或 `constName=` 模式比搜索密钥内容更有效。
|
|
@@ -79,9 +79,10 @@ last_merged: null
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* evolve_skill 工具
|
|
82
|
+
* 使用结构化格式记录经验,符合 evolved.md 模板规范
|
|
82
83
|
*/
|
|
83
84
|
export const evolveSkill = tool(
|
|
84
|
-
async ({ skill, title, scenario,
|
|
85
|
+
async ({ skill, title, scenario, conclusion, technicalDetails, correctExample, incorrectExample, why, extensions, isCore }) => {
|
|
85
86
|
const skillInfo = getSkillPath(skill);
|
|
86
87
|
if (!skillInfo) {
|
|
87
88
|
return JSON.stringify({
|
|
@@ -109,11 +110,44 @@ export const evolveSkill = tool(
|
|
|
109
110
|
|
|
110
111
|
const data = parseEvolvedMd(content);
|
|
111
112
|
|
|
112
|
-
//
|
|
113
|
+
// 生成新条目(结构化格式)
|
|
113
114
|
const date = new Date().toISOString().split('T')[0];
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
let entry = `### [${date}] ${title}
|
|
116
|
+
|
|
117
|
+
**一句话结论**: ${conclusion}
|
|
118
|
+
|
|
119
|
+
**场景**: ${scenario}`;
|
|
120
|
+
|
|
121
|
+
// 技术细节(表格形式)
|
|
122
|
+
if (technicalDetails && Object.keys(technicalDetails).length > 0) {
|
|
123
|
+
entry += '\n\n**技术细节**:\n| 项目 | 值/说明 |\n|------|---------|';
|
|
124
|
+
for (const [key, value] of Object.entries(technicalDetails)) {
|
|
125
|
+
entry += `\n| ${key} | ${value} |`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 正确做法
|
|
130
|
+
if (correctExample) {
|
|
131
|
+
entry += `\n\n**正确做法**:\n\`\`\`python\n${correctExample}\n\`\`\``;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 错误陷阱
|
|
135
|
+
if (incorrectExample) {
|
|
136
|
+
entry += `\n\n**错误陷阱** ⚠️:\n\`\`\`python\n${incorrectExample}\n\`\`\``;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 原因解释
|
|
140
|
+
if (why) {
|
|
141
|
+
entry += `\n\n**为什么**: ${why}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 举一反三
|
|
145
|
+
if (extensions && extensions.length > 0) {
|
|
146
|
+
entry += '\n\n**举一反三**:';
|
|
147
|
+
for (const item of extensions) {
|
|
148
|
+
entry += `\n- ${item}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
117
151
|
|
|
118
152
|
if (isCore) {
|
|
119
153
|
// 追加到核心经验
|
|
@@ -153,13 +187,18 @@ export const evolveSkill = tool(
|
|
|
153
187
|
},
|
|
154
188
|
{
|
|
155
189
|
name: 'evolve_skill',
|
|
156
|
-
description: '
|
|
190
|
+
description: '记录分析过程中学到的经验。使用结构化格式(一句话结论、技术细节、正确/错误示例、陷阱标记)',
|
|
157
191
|
schema: z.object({
|
|
158
192
|
skill: z.string().describe('目标 skill: static-analysis, dynamic-analysis, sandbox, env, js2python, crawler, captcha, anti-detect, report, general,或 new:<name> 创建新 skill'),
|
|
159
193
|
title: z.string().describe('经验标题,简短描述'),
|
|
160
194
|
scenario: z.string().describe('具体场景/案例'),
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
conclusion: z.string().describe('一句话核心结论(merge时必须提取到SKILL.md最前面)'),
|
|
196
|
+
technicalDetails: z.record(z.string(), z.string()).optional().describe('技术细节表格,如 {"参数类型": "int", "默认值": "0", "取值范围": "0或1"}'),
|
|
197
|
+
correctExample: z.string().optional().describe('正确代码示例'),
|
|
198
|
+
incorrectExample: z.string().optional().describe('错误代码示例(带陷阱标记)'),
|
|
199
|
+
why: z.string().optional().describe('解释根本原因'),
|
|
200
|
+
extensions: z.array(z.string()).optional().describe('类似场景列表(举一反三)'),
|
|
201
|
+
isCore: z.boolean().default(false).describe('是否为核心经验(已验证的高价值经验)'),
|
|
163
202
|
}),
|
|
164
203
|
}
|
|
165
204
|
);
|
package/src/agent/tools/index.js
CHANGED
|
@@ -38,10 +38,9 @@ export { antiDetectTools } from './anti-detect.js';
|
|
|
38
38
|
export { crawlerTools } from './crawler.js';
|
|
39
39
|
export { crawlerGeneratorTools, generateCrawlerWithConfirm, delegateCrawlerGeneration } from './crawlerGenerator.js';
|
|
40
40
|
export { nodejsTools, runNodeCode } from './nodejs.js';
|
|
41
|
+
export { pythonTools, executePythonCode } from './python.js';
|
|
41
42
|
export { hookManagerTools, listHooks, enableHook, disableHook, injectHook, setHookConfig } from './hookManager.js';
|
|
42
43
|
export { scratchpadTools, saveMemo, loadMemo, listMemo } from './scratchpad.js';
|
|
43
|
-
// pythonTools 只在 js2python 子代理中使用,不导出到主工具集
|
|
44
|
-
|
|
45
44
|
// 所有工具
|
|
46
45
|
import { sandboxTools } from './sandbox.js';
|
|
47
46
|
import { analyzerTools } from './analyzer.js';
|
|
@@ -76,6 +75,7 @@ import { antiDetectTools } from './anti-detect.js';
|
|
|
76
75
|
import { crawlerTools } from './crawler.js';
|
|
77
76
|
import { crawlerGeneratorTools } from './crawlerGenerator.js';
|
|
78
77
|
import { nodejsTools } from './nodejs.js';
|
|
78
|
+
import { executePythonCode } from './python.js';
|
|
79
79
|
import { hookManagerTools } from './hookManager.js';
|
|
80
80
|
import { scratchpadTools } from './scratchpad.js';
|
|
81
81
|
|
|
@@ -143,6 +143,8 @@ export const coreTools = [
|
|
|
143
143
|
...evolveTools,
|
|
144
144
|
// Node.js 执行(委托前快速验证假设)- 已添加网络请求防护
|
|
145
145
|
...nodejsTools,
|
|
146
|
+
// Python 执行(用于加密验证、数据处理等任务)
|
|
147
|
+
executePythonCode,
|
|
146
148
|
// 工作记忆
|
|
147
149
|
...scratchpadTools,
|
|
148
150
|
// 爬虫代码生成(带 HITL 确认)
|