@creatoria/miniapp-mcp 0.1.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 +469 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +144 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/defaults.d.ts +73 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +118 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/loader.d.ts +50 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +189 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/core/element-ref.d.ts +44 -0
- package/dist/core/element-ref.d.ts.map +1 -0
- package/dist/core/element-ref.js +213 -0
- package/dist/core/element-ref.js.map +1 -0
- package/dist/core/logger.d.ts +55 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +378 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/output.d.ts +21 -0
- package/dist/core/output.d.ts.map +1 -0
- package/dist/core/output.js +56 -0
- package/dist/core/output.js.map +1 -0
- package/dist/core/report-generator.d.ts +24 -0
- package/dist/core/report-generator.d.ts.map +1 -0
- package/dist/core/report-generator.js +212 -0
- package/dist/core/report-generator.js.map +1 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +306 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/timeout.d.ts +49 -0
- package/dist/core/timeout.d.ts.map +1 -0
- package/dist/core/timeout.js +67 -0
- package/dist/core/timeout.js.map +1 -0
- package/dist/core/tool-logger.d.ts +83 -0
- package/dist/core/tool-logger.d.ts.map +1 -0
- package/dist/core/tool-logger.js +453 -0
- package/dist/core/tool-logger.js.map +1 -0
- package/dist/core/validation.d.ts +39 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +93 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +85 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/assert.d.ts +108 -0
- package/dist/tools/assert.d.ts.map +1 -0
- package/dist/tools/assert.js +291 -0
- package/dist/tools/assert.js.map +1 -0
- package/dist/tools/automator.d.ts +45 -0
- package/dist/tools/automator.d.ts.map +1 -0
- package/dist/tools/automator.js +186 -0
- package/dist/tools/automator.js.map +1 -0
- package/dist/tools/element.d.ts +253 -0
- package/dist/tools/element.d.ts.map +1 -0
- package/dist/tools/element.js +615 -0
- package/dist/tools/element.js.map +1 -0
- package/dist/tools/index.d.ts +97 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +1565 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/miniprogram.d.ts +79 -0
- package/dist/tools/miniprogram.d.ts.map +1 -0
- package/dist/tools/miniprogram.js +245 -0
- package/dist/tools/miniprogram.js.map +1 -0
- package/dist/tools/network.d.ts +65 -0
- package/dist/tools/network.d.ts.map +1 -0
- package/dist/tools/network.js +205 -0
- package/dist/tools/network.js.map +1 -0
- package/dist/tools/page.d.ts +108 -0
- package/dist/tools/page.d.ts.map +1 -0
- package/dist/tools/page.js +307 -0
- package/dist/tools/page.js.map +1 -0
- package/dist/tools/record.d.ts +86 -0
- package/dist/tools/record.d.ts.map +1 -0
- package/dist/tools/record.js +316 -0
- package/dist/tools/record.js.map +1 -0
- package/dist/tools/snapshot.d.ts +82 -0
- package/dist/tools/snapshot.d.ts.map +1 -0
- package/dist/tools/snapshot.js +258 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/types.d.ts +240 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/docs/SIMPLE_USAGE.md +210 -0
- package/docs/api/README.md +244 -0
- package/docs/api/assert.md +1015 -0
- package/docs/api/automator.md +345 -0
- package/docs/api/element.md +1454 -0
- package/docs/api/miniprogram.md +558 -0
- package/docs/api/network.md +883 -0
- package/docs/api/page.md +909 -0
- package/docs/api/record.md +963 -0
- package/docs/api/snapshot.md +792 -0
- package/docs/architecture.E-Docs.md +1359 -0
- package/docs/architecture.F1.md +720 -0
- package/docs/architecture.F2.md +871 -0
- package/docs/architecture.F3.md +905 -0
- package/docs/architecture.md +90 -0
- package/docs/charter.A1.align.yaml +170 -0
- package/docs/charter.A2.align.yaml +199 -0
- package/docs/charter.A3.align.yaml +242 -0
- package/docs/charter.A4.align.yaml +227 -0
- package/docs/charter.B1.align.yaml +179 -0
- package/docs/charter.B2.align.yaml +200 -0
- package/docs/charter.B3.align.yaml +200 -0
- package/docs/charter.B4.align.yaml +188 -0
- package/docs/charter.C1.align.yaml +190 -0
- package/docs/charter.C2.align.yaml +202 -0
- package/docs/charter.C3.align.yaml +211 -0
- package/docs/charter.C4.align.yaml +263 -0
- package/docs/charter.C5.align.yaml +220 -0
- package/docs/charter.D1.align.yaml +190 -0
- package/docs/charter.D2.align.yaml +234 -0
- package/docs/charter.D3.align.yaml +206 -0
- package/docs/charter.E-Docs.align.yaml +294 -0
- package/docs/charter.F1.align.yaml +193 -0
- package/docs/charter.F2.align.yaml +248 -0
- package/docs/charter.F3.align.yaml +287 -0
- package/docs/charter.G.align.yaml +174 -0
- package/docs/charter.align.yaml +111 -0
- package/docs/examples/session-report-usage.md +449 -0
- package/docs/maintenance.md +682 -0
- package/docs/playwright-mcp/350/260/203/347/240/224.md +53 -0
- package/docs/setup-guide.md +775 -0
- package/docs/tasks.A1.atomize.md +296 -0
- package/docs/tasks.A2.atomize.md +408 -0
- package/docs/tasks.A3.atomize.md +564 -0
- package/docs/tasks.A4.atomize.md +496 -0
- package/docs/tasks.B1.atomize.md +352 -0
- package/docs/tasks.B2.atomize.md +561 -0
- package/docs/tasks.B3.atomize.md +508 -0
- package/docs/tasks.B4.atomize.md +504 -0
- package/docs/tasks.C1.atomize.md +540 -0
- package/docs/tasks.C2.atomize.md +665 -0
- package/docs/tasks.C3.atomize.md +745 -0
- package/docs/tasks.C4.atomize.md +908 -0
- package/docs/tasks.C5.atomize.md +755 -0
- package/docs/tasks.D1.atomize.md +547 -0
- package/docs/tasks.D2.atomize.md +619 -0
- package/docs/tasks.D3.atomize.md +790 -0
- package/docs/tasks.E-Docs.atomize.md +1204 -0
- package/docs/tasks.atomize.md +189 -0
- package/docs/troubleshooting.md +855 -0
- package/docs//345/256/214/346/225/264/345/256/236/347/216/260/346/226/271/346/241/210.md +155 -0
- package/docs//345/274/200/345/217/221/344/273/273/345/212/241/350/256/241/345/210/222.md +110 -0
- package/docs//345/276/256/344/277/241/345/260/217/347/250/213/345/272/217/350/207/252/345/212/250/345/214/226API/345/256/214/346/225/264/346/226/207/346/241/243.md +894 -0
- package/docs//345/276/256/344/277/241/345/260/217/347/250/213/345/272/217/350/207/252/345/212/250/345/214/226/345/256/214/346/225/264/346/223/215/344/275/234/346/211/213/345/206/214.md +1885 -0
- package/docs//346/216/245/345/217/243/346/226/271/346/241/210.md +565 -0
- package/docs//347/254/254/344/270/200/347/211/210/346/234/254/346/226/271/346/241/210.md +380 -0
- package/package.json +87 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
# Architecture: F2 - 失败时自动快照收集
|
|
2
|
+
|
|
3
|
+
## 文档信息
|
|
4
|
+
- **项目**: creatoria-miniapp-mcp
|
|
5
|
+
- **阶段**: F - 可观测性与产物输出
|
|
6
|
+
- **任务**: F2 - 失败时自动收集截图和数据快照
|
|
7
|
+
- **作者**: ClaudeCode
|
|
8
|
+
- **日期**: 2025-10-03
|
|
9
|
+
- **版本**: 1.0
|
|
10
|
+
|
|
11
|
+
## 目录
|
|
12
|
+
1. [系统概述](#1-系统概述)
|
|
13
|
+
2. [架构设计](#2-架构设计)
|
|
14
|
+
3. [详细设计](#3-详细设计)
|
|
15
|
+
4. [接口设计](#4-接口设计)
|
|
16
|
+
5. [数据模型](#5-数据模型)
|
|
17
|
+
6. [错误处理](#6-错误处理)
|
|
18
|
+
7. [性能考量](#7-性能考量)
|
|
19
|
+
8. [安全性](#8-安全性)
|
|
20
|
+
9. [测试策略](#9-测试策略)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. 系统概述
|
|
25
|
+
|
|
26
|
+
### 1.1 背景
|
|
27
|
+
在自动化测试执行过程中,工具调用失败时开发者往往需要手动重现问题以获取失败现场。F2 任务旨在实现失败时的自动快照收集,保存失败瞬间的页面状态、截图和错误上下文,显著提升调试效率。
|
|
28
|
+
|
|
29
|
+
### 1.2 目标
|
|
30
|
+
- **自动化诊断收集**:工具失败时无需人工介入,自动保存现场
|
|
31
|
+
- **完整上下文**:包含错误信息、工具参数、页面数据、截图
|
|
32
|
+
- **可配置**:用户可选择启用/禁用,避免产生过多文件
|
|
33
|
+
- **无侵入**:快照收集失败不影响原始错误流程
|
|
34
|
+
|
|
35
|
+
### 1.3 关键约束
|
|
36
|
+
- 依赖 D2 快照工具(已实现)
|
|
37
|
+
- 依赖 F1 日志系统和 ToolLogger
|
|
38
|
+
- Fire-and-forget 模式,不阻塞错误抛出
|
|
39
|
+
- 默认关闭,显式启用
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. 架构设计
|
|
44
|
+
|
|
45
|
+
### 2.1 C4 模型
|
|
46
|
+
|
|
47
|
+
#### Level 1: 系统上下文图
|
|
48
|
+
```
|
|
49
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
50
|
+
│ │
|
|
51
|
+
│ MCP Client (Claude) │
|
|
52
|
+
│ │
|
|
53
|
+
└────────────────────────┬────────────────────────────────────────┘
|
|
54
|
+
│ MCP Protocol
|
|
55
|
+
│ (stdio transport)
|
|
56
|
+
v
|
|
57
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
58
|
+
│ │
|
|
59
|
+
│ MCP Server (miniapp-mcp) │
|
|
60
|
+
│ │
|
|
61
|
+
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
62
|
+
│ │ ToolLogger with Failure Snapshots │ │
|
|
63
|
+
│ │ - Wraps tool handlers │ │
|
|
64
|
+
│ │ - Captures failures automatically │ │
|
|
65
|
+
│ │ - Saves: screenshot + data + error context │ │
|
|
66
|
+
│ └─────────────────────────────────────────────────────────┘ │
|
|
67
|
+
│ │ │
|
|
68
|
+
└─────────────────────────┼────────────────────────────────────────┘
|
|
69
|
+
│
|
|
70
|
+
v
|
|
71
|
+
┌───────────────────────┐
|
|
72
|
+
│ File System │
|
|
73
|
+
│ .mcp-artifacts/ │
|
|
74
|
+
│ failures/ │
|
|
75
|
+
└───────────────────────┘
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Level 2: 容器图
|
|
79
|
+
```
|
|
80
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
81
|
+
│ MCP Server Process │
|
|
82
|
+
│ │
|
|
83
|
+
│ ┌────────────────┐ ┌────────────────┐ ┌─────────────────┐ │
|
|
84
|
+
│ │ ToolLogger │────>│ SnapshotTools │────>│ OutputManager │ │
|
|
85
|
+
│ │ │ │ (D2) │ │ │ │
|
|
86
|
+
│ │ - wrap() │ │ - snapshotPage │ │ - writeFile() │ │
|
|
87
|
+
│ │ - capture...() │ │ - screenshot │ │ - ensureDir() │ │
|
|
88
|
+
│ └────────────────┘ └────────────────┘ └─────────────────┘ │
|
|
89
|
+
│ │ │
|
|
90
|
+
│ v │
|
|
91
|
+
│ ┌────────────────┐ │
|
|
92
|
+
│ │ Logger │ │
|
|
93
|
+
│ │ (F1) │ │
|
|
94
|
+
│ │ - info() │ │
|
|
95
|
+
│ │ - error() │ │
|
|
96
|
+
│ └────────────────┘ │
|
|
97
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
98
|
+
│
|
|
99
|
+
v
|
|
100
|
+
┌─────────────────────────────┐
|
|
101
|
+
│ Artifacts Directory │
|
|
102
|
+
│ │
|
|
103
|
+
│ failures/ │
|
|
104
|
+
│ {tool}-{timestamp}/ │
|
|
105
|
+
│ ├─ snapshot.json │
|
|
106
|
+
│ ├─ snapshot.png │
|
|
107
|
+
│ └─ error-context.json │
|
|
108
|
+
└─────────────────────────────┘
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Level 3: 组件图
|
|
112
|
+
```
|
|
113
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
114
|
+
│ ToolLogger │
|
|
115
|
+
│ │
|
|
116
|
+
│ ┌────────────────────────────────────────────────────────────────┐ │
|
|
117
|
+
│ │ wrap<TArgs, TResult>(toolName, handler) │ │
|
|
118
|
+
│ │ │ │
|
|
119
|
+
│ │ try { │ │
|
|
120
|
+
│ │ const result = await handler(session, args) │ │
|
|
121
|
+
│ │ logger.info('Tool call completed') │ │
|
|
122
|
+
│ │ return result │ │
|
|
123
|
+
│ │ } catch (error) { │ │
|
|
124
|
+
│ │ logger.error('Tool call failed') │ │
|
|
125
|
+
│ │ │ │
|
|
126
|
+
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
|
127
|
+
│ │ │ if (config.enableFailureSnapshot) { │ │ │
|
|
128
|
+
│ │ │ void this.captureFailureSnapshot({ │ │ │
|
|
129
|
+
│ │ │ session, toolName, args, error, duration │ │ │
|
|
130
|
+
│ │ │ }).catch(e => { │ │ │
|
|
131
|
+
│ │ │ logger.warn('Snapshot capture failed', e) │ │ │
|
|
132
|
+
│ │ │ }) │ │ │
|
|
133
|
+
│ │ │ } │ │ │
|
|
134
|
+
│ │ └────────────────────────────────────────────────────┘ │ │
|
|
135
|
+
│ │ │ │
|
|
136
|
+
│ │ throw error // Re-throw original error │ │
|
|
137
|
+
│ │ } │ │
|
|
138
|
+
│ └────────────────────────────────────────────────────────────────┘ │
|
|
139
|
+
│ │
|
|
140
|
+
│ ┌────────────────────────────────────────────────────────────────┐ │
|
|
141
|
+
│ │ private async captureFailureSnapshot(context) │ │
|
|
142
|
+
│ │ │ │
|
|
143
|
+
│ │ 1. Check prerequisites │ │
|
|
144
|
+
│ │ - session.miniProgram exists? │ │
|
|
145
|
+
│ │ - session.outputManager exists? │ │
|
|
146
|
+
│ │ - config.enableFailureSnapshot === true? │ │
|
|
147
|
+
│ │ │ │
|
|
148
|
+
│ │ 2. Create failure directory │ │
|
|
149
|
+
│ │ - Path: failures/{toolName}-{timestamp}/ │ │
|
|
150
|
+
│ │ │ │
|
|
151
|
+
│ │ 3. Capture snapshot │ │
|
|
152
|
+
│ │ - Call snapshotPage() → snapshot.json + snapshot.png │ │
|
|
153
|
+
│ │ │ │
|
|
154
|
+
│ │ 4. Save error context │ │
|
|
155
|
+
│ │ - File: error-context.json │ │
|
|
156
|
+
│ │ - Content: { toolName, timestamp, error, args, duration } │ │
|
|
157
|
+
│ │ │ │
|
|
158
|
+
│ │ 5. Log result │ │
|
|
159
|
+
│ │ - logger.info('Failure snapshot captured', { path }) │ │
|
|
160
|
+
│ └────────────────────────────────────────────────────────────────┘ │
|
|
161
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 2.2 数据流图
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
┌───────────────┐
|
|
168
|
+
│ Tool Call │
|
|
169
|
+
│ Execution │
|
|
170
|
+
└───────┬───────┘
|
|
171
|
+
│
|
|
172
|
+
v
|
|
173
|
+
┌─────────────┐
|
|
174
|
+
│ Success? │
|
|
175
|
+
└─────┬───┬───┘
|
|
176
|
+
│ │
|
|
177
|
+
YES │ │ NO
|
|
178
|
+
│ v
|
|
179
|
+
│ ┌───────────────────────┐
|
|
180
|
+
│ │ ToolLogger.wrap() │
|
|
181
|
+
│ │ catch block │
|
|
182
|
+
│ └───────┬───────────────┘
|
|
183
|
+
│ │
|
|
184
|
+
│ v
|
|
185
|
+
│ ┌────────────────────────────┐
|
|
186
|
+
│ │ config.enableFailure │
|
|
187
|
+
│ │ Snapshot === true? │
|
|
188
|
+
│ └───────┬────────────────────┘
|
|
189
|
+
│ │
|
|
190
|
+
│ YES │
|
|
191
|
+
│ v
|
|
192
|
+
│ ┌────────────────────────────┐
|
|
193
|
+
│ │ captureFailureSnapshot() │
|
|
194
|
+
│ │ (async, fire-and-forget) │
|
|
195
|
+
│ └───────┬────────────────────┘
|
|
196
|
+
│ │
|
|
197
|
+
│ │ ┌─ Check prerequisites
|
|
198
|
+
│ │ │ (miniProgram, outputManager)
|
|
199
|
+
│ │ │
|
|
200
|
+
│ │ ├─ Create failure directory
|
|
201
|
+
│ │ │ failures/{tool}-{timestamp}/
|
|
202
|
+
│ │ │
|
|
203
|
+
│ │ ├─ Call snapshotPage()
|
|
204
|
+
│ │ │ → snapshot.json
|
|
205
|
+
│ │ │ → snapshot.png
|
|
206
|
+
│ │ │
|
|
207
|
+
│ │ └─ Save error-context.json
|
|
208
|
+
│ │ { toolName, timestamp, error, args }
|
|
209
|
+
│ v
|
|
210
|
+
│ ┌────────────────────────────┐
|
|
211
|
+
│ │ Snapshot saved to: │
|
|
212
|
+
│ │ failures/{tool}-{ts}/ │
|
|
213
|
+
│ └────────────────────────────┘
|
|
214
|
+
│
|
|
215
|
+
v
|
|
216
|
+
┌─────────────────┐
|
|
217
|
+
│ Throw Error │
|
|
218
|
+
│ (original) │
|
|
219
|
+
└─────────────────┘
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 3. 详细设计
|
|
225
|
+
|
|
226
|
+
### 3.1 配置扩展
|
|
227
|
+
|
|
228
|
+
#### 3.1.1 类型定义 (src/types.ts)
|
|
229
|
+
```typescript
|
|
230
|
+
/**
|
|
231
|
+
* Logger configuration
|
|
232
|
+
*/
|
|
233
|
+
export interface LoggerConfig {
|
|
234
|
+
level?: LogLevel
|
|
235
|
+
enableFileLog?: boolean
|
|
236
|
+
outputDir?: string
|
|
237
|
+
bufferSize?: number
|
|
238
|
+
flushInterval?: number
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Enable automatic failure snapshot capture
|
|
242
|
+
* When a tool call fails, automatically save:
|
|
243
|
+
* - Page screenshot (PNG)
|
|
244
|
+
* - Page data snapshot (JSON)
|
|
245
|
+
* - Error context (JSON)
|
|
246
|
+
*
|
|
247
|
+
* Output: {outputDir}/failures/{toolName}-{timestamp}/
|
|
248
|
+
*
|
|
249
|
+
* @default false
|
|
250
|
+
*/
|
|
251
|
+
enableFailureSnapshot?: boolean
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### 3.1.2 默认配置 (src/config/defaults.ts)
|
|
256
|
+
```typescript
|
|
257
|
+
export const DEFAULT_LOGGER_CONFIG: Required<LoggerConfig> = {
|
|
258
|
+
level: 'info',
|
|
259
|
+
enableFileLog: false,
|
|
260
|
+
outputDir: '.mcp-artifacts',
|
|
261
|
+
bufferSize: 100,
|
|
262
|
+
flushInterval: 5000,
|
|
263
|
+
enableFailureSnapshot: false, // 🆕 默认关闭
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 3.2 ToolLogger 扩展
|
|
268
|
+
|
|
269
|
+
#### 3.2.1 构造函数修改
|
|
270
|
+
```typescript
|
|
271
|
+
export class ToolLogger {
|
|
272
|
+
private config: LoggerConfig
|
|
273
|
+
|
|
274
|
+
constructor(
|
|
275
|
+
private logger: Logger,
|
|
276
|
+
config?: LoggerConfig
|
|
277
|
+
) {
|
|
278
|
+
this.config = config || {}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ... existing wrap() method
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### 3.2.2 失败快照捕获方法
|
|
286
|
+
```typescript
|
|
287
|
+
/**
|
|
288
|
+
* Capture failure snapshot when tool call fails
|
|
289
|
+
*
|
|
290
|
+
* Creates a failure directory with:
|
|
291
|
+
* - snapshot.json: Page data
|
|
292
|
+
* - snapshot.png: Screenshot
|
|
293
|
+
* - error-context.json: Error details + tool context
|
|
294
|
+
*
|
|
295
|
+
* @param context Failure context
|
|
296
|
+
* @returns Promise that resolves when snapshot is saved (or fails silently)
|
|
297
|
+
*/
|
|
298
|
+
private async captureFailureSnapshot(context: {
|
|
299
|
+
session: SessionState
|
|
300
|
+
toolName: string
|
|
301
|
+
args: any
|
|
302
|
+
error: Error
|
|
303
|
+
duration: number
|
|
304
|
+
}): Promise<void> {
|
|
305
|
+
const { session, toolName, args, error, duration } = context
|
|
306
|
+
const logger = this.logger
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
// 1. Check prerequisites
|
|
310
|
+
if (!this.config.enableFailureSnapshot) {
|
|
311
|
+
return // Feature disabled
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!session.miniProgram) {
|
|
315
|
+
logger?.debug('Skipping failure snapshot: miniProgram not connected')
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!session.outputManager) {
|
|
320
|
+
logger?.debug('Skipping failure snapshot: outputManager not available')
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 2. Create failure directory
|
|
325
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
|
|
326
|
+
const failureDirName = `${toolName}-${timestamp}`
|
|
327
|
+
const failureDir = `failures/${failureDirName}`
|
|
328
|
+
|
|
329
|
+
const outputManager = session.outputManager
|
|
330
|
+
await outputManager.ensureOutputDir()
|
|
331
|
+
|
|
332
|
+
// Create failures subdirectory
|
|
333
|
+
const { mkdir } = await import('fs/promises')
|
|
334
|
+
const { join } = await import('path')
|
|
335
|
+
const failurePath = join(outputManager.getOutputDir(), failureDir)
|
|
336
|
+
await mkdir(failurePath, { recursive: true })
|
|
337
|
+
|
|
338
|
+
logger?.info('Capturing failure snapshot', { path: failurePath })
|
|
339
|
+
|
|
340
|
+
// 3. Capture page snapshot
|
|
341
|
+
const snapshotFilename = join(failureDir, 'snapshot.json')
|
|
342
|
+
const screenshotFilename = join(failureDir, 'snapshot.png')
|
|
343
|
+
|
|
344
|
+
const snapshotTools = await import('../tools/snapshot.js')
|
|
345
|
+
await snapshotTools.snapshotPage(session, {
|
|
346
|
+
filename: snapshotFilename,
|
|
347
|
+
includeScreenshot: true,
|
|
348
|
+
fullPage: false,
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// 4. Save error context
|
|
352
|
+
const errorContext = {
|
|
353
|
+
toolName,
|
|
354
|
+
timestamp: new Date().toISOString(),
|
|
355
|
+
error: {
|
|
356
|
+
message: error.message,
|
|
357
|
+
stack: error.stack,
|
|
358
|
+
code: (error as any).code,
|
|
359
|
+
},
|
|
360
|
+
args: this.sanitizeArgs(args), // Reuse existing sanitization
|
|
361
|
+
duration,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const contextFilename = join(failureDir, 'error-context.json')
|
|
365
|
+
await outputManager.writeFile(
|
|
366
|
+
contextFilename,
|
|
367
|
+
Buffer.from(JSON.stringify(errorContext, null, 2))
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
logger?.info('Failure snapshot captured successfully', {
|
|
371
|
+
path: failurePath,
|
|
372
|
+
files: ['snapshot.json', 'snapshot.png', 'error-context.json'],
|
|
373
|
+
})
|
|
374
|
+
} catch (snapshotError) {
|
|
375
|
+
// Snapshot capture failed - log but don't throw
|
|
376
|
+
logger?.warn('Failed to capture failure snapshot', {
|
|
377
|
+
error: snapshotError instanceof Error ? snapshotError.message : String(snapshotError),
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
#### 3.2.3 wrap() 方法集成
|
|
384
|
+
```typescript
|
|
385
|
+
wrap<TArgs, TResult>(
|
|
386
|
+
toolName: string,
|
|
387
|
+
handler: (session: SessionState, args: TArgs) => Promise<TResult>
|
|
388
|
+
): (session: SessionState, args: TArgs) => Promise<TResult> {
|
|
389
|
+
return async (session: SessionState, args: TArgs): Promise<TResult> => {
|
|
390
|
+
const startTime = Date.now()
|
|
391
|
+
const childLogger = this.logger.child(toolName)
|
|
392
|
+
|
|
393
|
+
// Log START
|
|
394
|
+
childLogger.info('Tool call started', {
|
|
395
|
+
phase: 'START',
|
|
396
|
+
args: this.sanitizeArgs(args),
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
// Execute tool
|
|
401
|
+
const result = await handler(session, args)
|
|
402
|
+
const duration = Date.now() - startTime
|
|
403
|
+
|
|
404
|
+
// Log END
|
|
405
|
+
childLogger.info('Tool call completed', {
|
|
406
|
+
phase: 'END',
|
|
407
|
+
duration,
|
|
408
|
+
result: this.sanitizeResult(result),
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
return result
|
|
412
|
+
} catch (error) {
|
|
413
|
+
const duration = Date.now() - startTime
|
|
414
|
+
|
|
415
|
+
// Log ERROR
|
|
416
|
+
childLogger.error('Tool call failed', {
|
|
417
|
+
phase: 'ERROR',
|
|
418
|
+
duration,
|
|
419
|
+
error: error instanceof Error ? error.message : String(error),
|
|
420
|
+
stackTrace: error instanceof Error ? error.stack : undefined,
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// 🆕 Capture failure snapshot (fire-and-forget)
|
|
424
|
+
if (this.config.enableFailureSnapshot) {
|
|
425
|
+
void this.captureFailureSnapshot({
|
|
426
|
+
session,
|
|
427
|
+
toolName,
|
|
428
|
+
args,
|
|
429
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
430
|
+
duration,
|
|
431
|
+
}).catch((e) => {
|
|
432
|
+
childLogger.warn('Snapshot capture failed', {
|
|
433
|
+
error: e instanceof Error ? e.message : String(e),
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
throw error // Re-throw to preserve error handling
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## 4. 接口设计
|
|
447
|
+
|
|
448
|
+
### 4.1 公共接口
|
|
449
|
+
|
|
450
|
+
#### 4.1.1 配置接口
|
|
451
|
+
```typescript
|
|
452
|
+
// 用户在 server.ts 或 .mcp.json 中配置
|
|
453
|
+
const server = await startServer({
|
|
454
|
+
logLevel: 'info',
|
|
455
|
+
enableFileLog: true,
|
|
456
|
+
enableFailureSnapshot: true, // 🆕 启用失败快照
|
|
457
|
+
outputDir: '.mcp-artifacts',
|
|
458
|
+
})
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### 4.1.2 输出目录结构
|
|
462
|
+
```
|
|
463
|
+
.mcp-artifacts/
|
|
464
|
+
└── session-{sessionId}-{timestamp}/
|
|
465
|
+
├── logs/
|
|
466
|
+
│ └── session-{sessionId}.log
|
|
467
|
+
└── failures/ # 🆕 失败快照目录
|
|
468
|
+
├── element_click-20251003-123456/
|
|
469
|
+
│ ├── snapshot.json # 页面数据
|
|
470
|
+
│ ├── snapshot.png # 页面截图
|
|
471
|
+
│ └── error-context.json # 错误上下文
|
|
472
|
+
└── page_navigate-20251003-123500/
|
|
473
|
+
├── snapshot.json
|
|
474
|
+
├── snapshot.png
|
|
475
|
+
└── error-context.json
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### 4.2 内部接口
|
|
479
|
+
|
|
480
|
+
#### 4.2.1 ToolLogger 构造函数
|
|
481
|
+
```typescript
|
|
482
|
+
class ToolLogger {
|
|
483
|
+
constructor(logger: Logger, config?: LoggerConfig)
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
#### 4.2.2 失败快照捕获
|
|
488
|
+
```typescript
|
|
489
|
+
private async captureFailureSnapshot(context: {
|
|
490
|
+
session: SessionState
|
|
491
|
+
toolName: string
|
|
492
|
+
args: any
|
|
493
|
+
error: Error
|
|
494
|
+
duration: number
|
|
495
|
+
}): Promise<void>
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## 5. 数据模型
|
|
501
|
+
|
|
502
|
+
### 5.1 错误上下文文件 (error-context.json)
|
|
503
|
+
|
|
504
|
+
```json
|
|
505
|
+
{
|
|
506
|
+
"toolName": "element_click",
|
|
507
|
+
"timestamp": "2025-10-03T12:34:56.789Z",
|
|
508
|
+
"error": {
|
|
509
|
+
"message": "Element not found: .submit-btn",
|
|
510
|
+
"stack": "Error: Element not found...\n at element_click (...)",
|
|
511
|
+
"code": "ELEMENT_NOT_FOUND"
|
|
512
|
+
},
|
|
513
|
+
"args": {
|
|
514
|
+
"selector": ".submit-btn",
|
|
515
|
+
"pagePath": "pages/checkout/checkout"
|
|
516
|
+
},
|
|
517
|
+
"duration": 1234
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### 5.2 页面快照文件 (snapshot.json)
|
|
522
|
+
|
|
523
|
+
```json
|
|
524
|
+
{
|
|
525
|
+
"timestamp": "2025-10-03T12:34:56.789Z",
|
|
526
|
+
"pagePath": "pages/checkout/checkout",
|
|
527
|
+
"pageData": {
|
|
528
|
+
"items": [...],
|
|
529
|
+
"total": 299.99,
|
|
530
|
+
"userInfo": "[REDACTED]"
|
|
531
|
+
},
|
|
532
|
+
"pageQuery": {
|
|
533
|
+
"from": "cart",
|
|
534
|
+
"orderid": "123456"
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### 5.3 文件命名规范
|
|
540
|
+
|
|
541
|
+
| 文件类型 | 命名格式 | 示例 |
|
|
542
|
+
|---------|----------|------|
|
|
543
|
+
| 失败目录 | `{toolName}-{timestamp}/` | `element_click-20251003-123456/` |
|
|
544
|
+
| 页面数据 | `snapshot.json` | `snapshot.json` |
|
|
545
|
+
| 页面截图 | `snapshot.png` | `snapshot.png` |
|
|
546
|
+
| 错误上下文 | `error-context.json` | `error-context.json` |
|
|
547
|
+
|
|
548
|
+
**时间戳格式**: `YYYYMMDD-HHmmss` (ISO 8601 移除符号)
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
## 6. 错误处理
|
|
553
|
+
|
|
554
|
+
### 6.1 错误分级
|
|
555
|
+
|
|
556
|
+
| 错误类型 | 处理策略 | 日志级别 |
|
|
557
|
+
|---------|----------|---------|
|
|
558
|
+
| miniProgram 未连接 | 跳过快照,DEBUG 日志 | DEBUG |
|
|
559
|
+
| outputManager 不可用 | 跳过快照,DEBUG 日志 | DEBUG |
|
|
560
|
+
| 快照文件创建失败 | 捕获异常,WARN 日志 | WARN |
|
|
561
|
+
| 目录创建失败 | 捕获异常,WARN 日志 | WARN |
|
|
562
|
+
| 原始工具错误 | 正常抛出,不受影响 | ERROR |
|
|
563
|
+
|
|
564
|
+
### 6.2 错误处理原则
|
|
565
|
+
|
|
566
|
+
1. **非侵入性**: 快照收集失败不影响原始错误
|
|
567
|
+
2. **Fail Silently**: 快照失败仅记录警告日志
|
|
568
|
+
3. **Fire-and-Forget**: 不阻塞错误抛出流程
|
|
569
|
+
4. **详细日志**: 记录失败原因便于调试
|
|
570
|
+
|
|
571
|
+
### 6.3 错误处理示例
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
try {
|
|
575
|
+
await this.captureFailureSnapshot(context)
|
|
576
|
+
} catch (snapshotError) {
|
|
577
|
+
// 🔴 不抛出,仅记录
|
|
578
|
+
logger?.warn('Failed to capture failure snapshot', {
|
|
579
|
+
error: snapshotError instanceof Error
|
|
580
|
+
? snapshotError.message
|
|
581
|
+
: String(snapshotError),
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## 7. 性能考量
|
|
589
|
+
|
|
590
|
+
### 7.1 性能目标
|
|
591
|
+
|
|
592
|
+
| 指标 | 目标值 | 实际值 | 备注 |
|
|
593
|
+
|-----|--------|--------|------|
|
|
594
|
+
| 快照收集耗时 | < 500ms | TBD | 异步,不阻塞 |
|
|
595
|
+
| 额外内存占用 | < 10MB | TBD | 临时缓冲 |
|
|
596
|
+
| 磁盘空间占用 | ~100KB-5MB/次 | TBD | 取决于截图大小 |
|
|
597
|
+
| 错误抛出延迟 | 0ms | 0ms | Fire-and-forget |
|
|
598
|
+
|
|
599
|
+
### 7.2 性能优化策略
|
|
600
|
+
|
|
601
|
+
#### 7.2.1 异步非阻塞
|
|
602
|
+
```typescript
|
|
603
|
+
// ❌ 错误:阻塞错误抛出
|
|
604
|
+
await this.captureFailureSnapshot(context)
|
|
605
|
+
throw error
|
|
606
|
+
|
|
607
|
+
// ✅ 正确:Fire-and-forget
|
|
608
|
+
void this.captureFailureSnapshot(context).catch(...)
|
|
609
|
+
throw error // 立即抛出
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
#### 7.2.2 条件触发
|
|
613
|
+
```typescript
|
|
614
|
+
// 只在配置启用时执行
|
|
615
|
+
if (this.config.enableFailureSnapshot) {
|
|
616
|
+
void this.captureFailureSnapshot(context)
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
#### 7.2.3 前提条件检查
|
|
621
|
+
```typescript
|
|
622
|
+
// 快速退出,避免不必要的操作
|
|
623
|
+
if (!session.miniProgram || !session.outputManager) {
|
|
624
|
+
return
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### 7.3 性能监控
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
// 记录快照收集耗时
|
|
632
|
+
const snapshotStart = Date.now()
|
|
633
|
+
await snapshotTools.snapshotPage(...)
|
|
634
|
+
const snapshotDuration = Date.now() - snapshotStart
|
|
635
|
+
|
|
636
|
+
logger?.debug('Snapshot capture performance', {
|
|
637
|
+
duration: snapshotDuration,
|
|
638
|
+
size: fileSize,
|
|
639
|
+
})
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## 8. 安全性
|
|
645
|
+
|
|
646
|
+
### 8.1 安全威胁模型
|
|
647
|
+
|
|
648
|
+
| 威胁 | 风险等级 | 缓解措施 |
|
|
649
|
+
|-----|----------|---------|
|
|
650
|
+
| 路径遍历攻击 | 🔴 高 | 使用 `join()` + 禁止 `..` |
|
|
651
|
+
| 敏感数据泄漏 | 🟡 中 | 复用 F1 脱敏逻辑 |
|
|
652
|
+
| 磁盘空间耗尽 | 🟢 低 | 默认关闭 + 文档提醒 |
|
|
653
|
+
| 恶意工具参数 | 🟡 中 | 参数脱敏 + 大小限制 |
|
|
654
|
+
|
|
655
|
+
### 8.2 安全措施
|
|
656
|
+
|
|
657
|
+
#### 8.2.1 路径验证
|
|
658
|
+
```typescript
|
|
659
|
+
// ✅ 安全:使用 join() 防止路径遍历
|
|
660
|
+
const failurePath = join(outputManager.getOutputDir(), failureDir)
|
|
661
|
+
|
|
662
|
+
// ❌ 危险:直接拼接
|
|
663
|
+
const failurePath = `${outputDir}/${failureDir}` // 可能被 ../攻击
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
#### 8.2.2 参数脱敏
|
|
667
|
+
```typescript
|
|
668
|
+
// 复用 F1 的 sanitizeArgs() 方法
|
|
669
|
+
args: this.sanitizeArgs(args) // 移除 password, token 等
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
#### 8.2.3 文件大小限制
|
|
673
|
+
```typescript
|
|
674
|
+
// 限制 error-context.json 大小
|
|
675
|
+
const contextJson = JSON.stringify(errorContext, null, 2)
|
|
676
|
+
if (contextJson.length > 100 * 1024) { // 100KB
|
|
677
|
+
throw new Error('Error context too large')
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### 8.3 安全最佳实践
|
|
682
|
+
|
|
683
|
+
1. **最小权限原则**: 快照文件权限 0644
|
|
684
|
+
2. **数据脱敏**: 所有用户数据必须脱敏
|
|
685
|
+
3. **输入验证**: 验证所有文件名和路径
|
|
686
|
+
4. **审计日志**: 记录快照创建事件
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## 9. 测试策略
|
|
691
|
+
|
|
692
|
+
### 9.1 单元测试
|
|
693
|
+
|
|
694
|
+
#### 9.1.1 配置开关测试
|
|
695
|
+
```typescript
|
|
696
|
+
describe('ToolLogger Failure Snapshot', () => {
|
|
697
|
+
it('should capture snapshot when enabled', async () => {
|
|
698
|
+
const logger = new ToolLogger(mockLogger, {
|
|
699
|
+
enableFailureSnapshot: true,
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
const handler = jest.fn().mockRejectedValue(new Error('Test error'))
|
|
703
|
+
const wrappedHandler = logger.wrap('test_tool', handler)
|
|
704
|
+
|
|
705
|
+
await expect(wrappedHandler(mockSession, {})).rejects.toThrow('Test error')
|
|
706
|
+
|
|
707
|
+
// 验证快照文件创建
|
|
708
|
+
expect(fs.existsSync('failures/test_tool-*/snapshot.json')).toBe(true)
|
|
709
|
+
expect(fs.existsSync('failures/test_tool-*/snapshot.png')).toBe(true)
|
|
710
|
+
expect(fs.existsSync('failures/test_tool-*/error-context.json')).toBe(true)
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
it('should NOT capture snapshot when disabled', async () => {
|
|
714
|
+
const logger = new ToolLogger(mockLogger, {
|
|
715
|
+
enableFailureSnapshot: false, // 关闭
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
const handler = jest.fn().mockRejectedValue(new Error('Test error'))
|
|
719
|
+
const wrappedHandler = logger.wrap('test_tool', handler)
|
|
720
|
+
|
|
721
|
+
await expect(wrappedHandler(mockSession, {})).rejects.toThrow('Test error')
|
|
722
|
+
|
|
723
|
+
// 验证没有创建快照
|
|
724
|
+
expect(fs.existsSync('failures/')).toBe(false)
|
|
725
|
+
})
|
|
726
|
+
})
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
#### 9.1.2 错误不阻塞测试
|
|
730
|
+
```typescript
|
|
731
|
+
it('should not block error when snapshot fails', async () => {
|
|
732
|
+
const logger = new ToolLogger(mockLogger, {
|
|
733
|
+
enableFailureSnapshot: true,
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
// Mock snapshotPage to fail
|
|
737
|
+
jest.spyOn(snapshotTools, 'snapshotPage').mockRejectedValue(
|
|
738
|
+
new Error('Snapshot failed')
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
const handler = jest.fn().mockRejectedValue(new Error('Original error'))
|
|
742
|
+
const wrappedHandler = logger.wrap('test_tool', handler)
|
|
743
|
+
|
|
744
|
+
// 应该抛出原始错误,而非快照错误
|
|
745
|
+
await expect(wrappedHandler(mockSession, {})).rejects.toThrow('Original error')
|
|
746
|
+
})
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
#### 9.1.3 前提条件测试
|
|
750
|
+
```typescript
|
|
751
|
+
it('should skip snapshot if miniProgram not connected', async () => {
|
|
752
|
+
const sessionWithoutMP = { ...mockSession, miniProgram: undefined }
|
|
753
|
+
const logger = new ToolLogger(mockLogger, {
|
|
754
|
+
enableFailureSnapshot: true,
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
const handler = jest.fn().mockRejectedValue(new Error('Test error'))
|
|
758
|
+
const wrappedHandler = logger.wrap('test_tool', handler)
|
|
759
|
+
|
|
760
|
+
await expect(wrappedHandler(sessionWithoutMP, {})).rejects.toThrow('Test error')
|
|
761
|
+
|
|
762
|
+
// 验证没有创建快照(因为前提条件不满足)
|
|
763
|
+
expect(fs.existsSync('failures/')).toBe(false)
|
|
764
|
+
})
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### 9.2 集成测试
|
|
768
|
+
|
|
769
|
+
```typescript
|
|
770
|
+
describe('Failure Snapshot Integration', () => {
|
|
771
|
+
it('should capture real failure snapshot', async () => {
|
|
772
|
+
// 启动真实 MCP 服务器
|
|
773
|
+
const server = await startServer({
|
|
774
|
+
enableFailureSnapshot: true,
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
// 连接到 miniProgram
|
|
778
|
+
await server.call('miniprogram_connect', { port: 9420 })
|
|
779
|
+
|
|
780
|
+
// 触发一个会失败的工具调用
|
|
781
|
+
try {
|
|
782
|
+
await server.call('element_click', {
|
|
783
|
+
selector: '.nonexistent-element',
|
|
784
|
+
})
|
|
785
|
+
} catch (error) {
|
|
786
|
+
// 验证快照已创建
|
|
787
|
+
const files = fs.readdirSync('failures/')
|
|
788
|
+
expect(files.length).toBeGreaterThan(0)
|
|
789
|
+
|
|
790
|
+
const snapshotDir = files[0]
|
|
791
|
+
expect(fs.existsSync(`failures/${snapshotDir}/snapshot.json`)).toBe(true)
|
|
792
|
+
expect(fs.existsSync(`failures/${snapshotDir}/snapshot.png`)).toBe(true)
|
|
793
|
+
expect(fs.existsSync(`failures/${snapshotDir}/error-context.json`)).toBe(true)
|
|
794
|
+
|
|
795
|
+
// 验证 error-context.json 内容
|
|
796
|
+
const errorContext = JSON.parse(
|
|
797
|
+
fs.readFileSync(`failures/${snapshotDir}/error-context.json`, 'utf-8')
|
|
798
|
+
)
|
|
799
|
+
expect(errorContext.toolName).toBe('element_click')
|
|
800
|
+
expect(errorContext.error.message).toContain('nonexistent-element')
|
|
801
|
+
}
|
|
802
|
+
})
|
|
803
|
+
})
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### 9.3 性能测试
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
describe('Failure Snapshot Performance', () => {
|
|
810
|
+
it('should not block error throwing', async () => {
|
|
811
|
+
const logger = new ToolLogger(mockLogger, {
|
|
812
|
+
enableFailureSnapshot: true,
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
const handler = jest.fn().mockRejectedValue(new Error('Test error'))
|
|
816
|
+
const wrappedHandler = logger.wrap('test_tool', handler)
|
|
817
|
+
|
|
818
|
+
const start = Date.now()
|
|
819
|
+
await expect(wrappedHandler(mockSession, {})).rejects.toThrow('Test error')
|
|
820
|
+
const errorThrowTime = Date.now() - start
|
|
821
|
+
|
|
822
|
+
// 错误抛出应该立即发生(< 10ms)
|
|
823
|
+
expect(errorThrowTime).toBeLessThan(10)
|
|
824
|
+
|
|
825
|
+
// 等待快照收集完成(异步)
|
|
826
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
827
|
+
|
|
828
|
+
// 验证快照已创建
|
|
829
|
+
expect(fs.existsSync('failures/')).toBe(true)
|
|
830
|
+
})
|
|
831
|
+
})
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
## 10. 实现清单
|
|
837
|
+
|
|
838
|
+
### Phase 1: 核心实现
|
|
839
|
+
- [ ] 扩展 `LoggerConfig` 类型 (`src/types.ts`)
|
|
840
|
+
- [ ] 添加默认配置 (`src/config/defaults.ts`)
|
|
841
|
+
- [ ] 实现 `ToolLogger.captureFailureSnapshot()` 方法
|
|
842
|
+
- [ ] 修改 `ToolLogger.wrap()` 集成快照逻辑
|
|
843
|
+
- [ ] 传递 config 到 ToolLogger 构造函数
|
|
844
|
+
|
|
845
|
+
### Phase 2: 测试验证
|
|
846
|
+
- [ ] 单元测试:配置开关测试
|
|
847
|
+
- [ ] 单元测试:错误不阻塞测试
|
|
848
|
+
- [ ] 单元测试:前提条件测试
|
|
849
|
+
- [ ] 集成测试:端到端快照收集
|
|
850
|
+
|
|
851
|
+
### Phase 3: 文档完善
|
|
852
|
+
- [ ] 更新 README 配置示例
|
|
853
|
+
- [ ] 更新使用文档
|
|
854
|
+
- [ ] 添加故障排查文档
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
## 11. 附录
|
|
859
|
+
|
|
860
|
+
### 11.1 相关文档
|
|
861
|
+
- [Charter F2](./charter.F2.align.yaml)
|
|
862
|
+
- [Architecture F1](./architecture.F1.md) - 日志系统
|
|
863
|
+
- [Architecture D2](./architecture.D2.md) - 快照工具
|
|
864
|
+
|
|
865
|
+
### 11.2 参考资料
|
|
866
|
+
- MCP Protocol Specification
|
|
867
|
+
- miniprogram-automator SDK
|
|
868
|
+
- Node.js fs/promises API
|
|
869
|
+
|
|
870
|
+
### 11.3 版本历史
|
|
871
|
+
- v1.0 (2025-10-03): 初始设计
|