pdd-skills 3.1.11 → 3.1.13

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/lib/init.js CHANGED
@@ -35,6 +35,8 @@ const AI_TEST_DIRS = [
35
35
  const AI_TEST_SOURCE_FILES = [
36
36
  'tests/testcase-ai.py',
37
37
  'tests/login_manager.py',
38
+ 'tests/recorder.py',
39
+ 'tests/__init__.py',
38
40
  ];
39
41
 
40
42
  const AI_TEST_EXAMPLE_FILES = [
@@ -498,7 +500,7 @@ export async function initProject(targetPath = '.', options = {}) {
498
500
  console.log(chalk.gray(` [OK] ${file.path}`));
499
501
  }
500
502
 
501
- // Copy AI Test Framework source files (testcase-ai.py, login_manager.py)
503
+ // Copy AI Test Framework source files (testcase-ai.py, login_manager.py, recorder.py, __init__.py)
502
504
  const srcRoot = path.resolve(__dirname, '..');
503
505
  console.log(chalk.blue('\n>> Copying AI Test source files...\n'));
504
506
  for (const rel of AI_TEST_SOURCE_FILES) {
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"pdd-skills","version":"3.1.11","type":"module","description":"PDD Skills - PRD驱动开发框架 | 41+技能: 核心(12)/专家(8)/熵减(4)/OpenSpec(10)/PR管理(7) | AI原生开发工作流 | Bug模式库(14) | PRD规则(30) | 4级门控引擎","main":"index.js","bin":{"pdd":"./bin/pdd.js","pdd-skills":"./bin/pdd.js"},"files":["bin/","lib/","skills/","templates/","scaffolds/","scripts/","config/","hooks/","docs/","index.js","tests/testcase-ai.py","tests/login_manager.py","testcases/examples/"],"scripts":{"start":"node bin/pdd.js","list":"node bin/pdd.js list","lint":"node bin/pdd.js linter --type code prd sql activiti","generate":"node bin/pdd.js generate","verify":"node bin/pdd.js verify","report":"node bin/pdd.js report","config":"node bin/pdd.js config --list","api":"node bin/pdd.js api","api:dev":"node bin/pdd.js api -p 3000 --cors","init":"node bin/pdd.js init","update":"node bin/pdd.js update","cso":"node bin/pdd.js cso","eval":"node bin/pdd.js eval","token":"node bin/pdd.js token","i18n":"node bin/pdd.js i18n"},"keywords":["pdd","prd-driven-development","ai","claude","skills","tdd","code-generation","linter","api-server","cli"],"author":"PDD Team","license":"MIT","engines":{"node":">=18.0.0"},"dependencies":{"chalk":"^5.3.0","commander":"^12.0.0","fs-extra":"^11.2.0","yaml":"^2.3.0"}}
1
+ {"name":"pdd-skills","version":"3.1.13","type":"module","description":"PDD Skills - PRD驱动开发框架 | 41+技能: 核心(12)/专家(8)/熵减(4)/OpenSpec(10)/PR管理(7) | AI原生开发工作流 | Bug模式库(14) | PRD规则(30) | 4级门控引擎","main":"index.js","bin":{"pdd":"./bin/pdd.js","pdd-skills":"./bin/pdd.js"},"files":["bin/","lib/","skills/","templates/","scaffolds/","scripts/","config/","hooks/","docs/","index.js","tests/testcase-ai.py","tests/login_manager.py","tests/recorder.py","tests/__init__.py","testcases/examples/"],"scripts":{"start":"node bin/pdd.js","list":"node bin/pdd.js list","lint":"node bin/pdd.js linter --type code prd sql activiti","generate":"node bin/pdd.js generate","verify":"node bin/pdd.js verify","report":"node bin/pdd.js report","config":"node bin/pdd.js config --list","api":"node bin/pdd.js api","api:dev":"node bin/pdd.js api -p 3000 --cors","init":"node bin/pdd.js init","update":"node bin/pdd.js update","cso":"node bin/pdd.js cso","eval":"node bin/pdd.js eval","token":"node bin/pdd.js token","i18n":"node bin/pdd.js i18n"},"keywords":["pdd","prd-driven-development","ai","claude","skills","tdd","code-generation","linter","api-server","cli"],"author":"PDD Team","license":"MIT","engines":{"node":">=18.0.0"},"dependencies":{"chalk":"^5.3.0","commander":"^12.0.0","fs-extra":"^11.2.0","yaml":"^2.3.0"}}
@@ -449,6 +449,113 @@ Modeler 应了解 testcase-agent 的底层能力边界:
449
449
 
450
450
  ---
451
451
 
452
+ ## 🎬 交互式录制模式(🆕 v2.0 新增)
453
+
454
+ 除了**自然语言描述 → YAML** 的建模方式,testcase-modeler 还支持 **浏览器操作录制 → YAML** 模式。
455
+
456
+ ### 使用方式
457
+
458
+ ```bash
459
+ # 方式1:录制到默认路径
460
+ python tests/testcase-ai.py --record
461
+
462
+ # 方式2:录制到指定路径
463
+ python tests/testcase-ai.py --record testcases/01-系统状况/my-test.yaml
464
+ ```
465
+
466
+ ### 录制流程
467
+
468
+ ```
469
+ ┌─────────────────────────────────────────────────────┐
470
+ │ 1. 启动录制器 │
471
+ │ $ python testcase-ai.py --record │
472
+ │ [1/4] 连接浏览器... ✅ │
473
+ │ [2/4] 注入事件监听器... ✅ │
474
+ └──────────────────┬──────────────────────────────────┘
475
+
476
+ ┌─────────────────────────────────────────────────────┐
477
+ │ 2. 用户在浏览器中操作 │
478
+ │ │
479
+ │ ╔════════════════════════════════════╗ │
480
+ │ ║ 🖥️ 现在可以在浏览器中操作了! ║ │
481
+ │ ║ ║ │
482
+ │ ║ • 点击按钮、链接、输入框等 ║ │
483
+ │ ║ • 填写表单、选择下拉框 ║ │
484
+ │ ║ • 导航到不同页面 ║ │
485
+ │ ║ ║ │
486
+ │ ║ 操作完成后,按 Enter 停止录制 ║ │
487
+ │ ╚════════════════════════════════════╝ │
488
+ │ │
489
+ │ 终端实时显示: │
490
+ │ [01] 👆 点击'用户名输入框' │
491
+ │ [02] ✏️ 填写'用户名输入框'为'test_user' │
492
+ │ [03] ✏️ 填写'密码输入框'为'******' │
493
+ │ [04] 👆 点击'登录按钮' │
494
+ │ [05] 🌐 导航至 'http://...' │
495
+ │ [06] 👆 点击'资产处置及评估系统' │
496
+ │ ... │
497
+ └──────────────────┬──────────────────────────────────┘
498
+
499
+ ┌─────────────────────────────────────────────────────┐
500
+ │ 3. 按 Enter 停止录制 │
501
+ │ [4/4] 停止录制,共捕获 6 个操作步骤 │
502
+ │ │
503
+ │ 自动生成: │
504
+ │ ✅ testcases/recorded/testcase.yaml │
505
+ │ ✅ testcases/recorded/testcase.env │
506
+ │ │
507
+ │ 💡 下一步:编辑 .env → 补充断言 → 执行测试 │
508
+ └─────────────────────────────────────────────────────┘
509
+ ```
510
+
511
+ ### 录制能力
512
+
513
+ | 可录制的事件 | 转换结果 | 说明 |
514
+ |------------|---------|------|
515
+ | **点击** (click) | `action: click` + `target` | 按钮、链接、菜单项 |
516
+ | **输入** (input/change) | `action: fill` + `target` + `value` | 文本框、数字框 |
517
+ | **导航** (pushState) | `action: navigate` + `url` | 页面跳转 |
518
+ | **Enter 键** | 忽略(通常伴随 submit) | 表单提交 |
519
+
520
+ ### 技术原理
521
+
522
+ ```
523
+ 1. evaluate_script 注入 JS 事件监听器到页面
524
+ → document.addEventListener('click', ...)
525
+ → document.addEventListener('input', ...)
526
+ → history.pushState 劫持(捕获导航)
527
+
528
+ 2. 每 1.5 秒轮询 POLL_JS 提取新事件
529
+ → 实时显示操作步骤到终端
530
+
531
+ 3. 用户按 Enter 触发 STOP_JS
532
+ → 收集所有事件 → _event_to_step() 转换
533
+ → yaml.dump() 写入文件 + .env 配对生成
534
+ ```
535
+
536
+ ### 适用场景
537
+
538
+ | 场景 | 推荐方式 |
539
+ |------|---------|
540
+ | 已知业务流程,快速生成用例 | 自然语言描述(Modeler) |
541
+ | **未知流程,边操作边记录** | **🎬 录制模式(推荐)** |
542
+ | 复杂多步操作,难以文字描述 | **🎬 录制模式(推荐)** |
543
+ | 首次接触的新功能探索性测试 | **🎬 录制模式(推荐)** |
544
+ | 需要精确元素定位信息 | **🎬 录制模式(推荐)** |
545
+
546
+ ### 录制后优化建议
547
+
548
+ 录制生成的 YAML 是**初稿**,建议进行以下优化:
549
+
550
+ 1. **补充断言**:每个关键步骤添加 `assertion`
551
+ 2. **优化 target 文本**:录制的 target 可能不够语义化,改为业务语言
552
+ 3. **添加 wait_after**:导航和提交后添加等待策略
553
+ 4. **环境变量化**:将硬编码值替换为 `${VAR}` 引用
554
+ 5. **添加 context_check**:补全前置状态感知配置
555
+ 6. **拆分长用例**:超过 15 步考虑拆分为多个用例
556
+
557
+ ---
558
+
452
559
  ## 🔧 工作流程
453
560
 
454
561
  ### 完整交互流程
File without changes
@@ -0,0 +1,338 @@
1
+ """
2
+ Testcase Recorder v1.0 - 交互式浏览器操作录制器
3
+ ==============================================
4
+ 从 testcase-ai.py 中提取的独立录制模块,遵循 SRP 原则。
5
+
6
+ 功能:
7
+ 🎬 注入 JS 事件监听器,捕获用户在浏览器中的操作
8
+ 📝 将操作事件自动转换为 YAML 测试步骤
9
+ 💾 同时生成 .yaml 和 .env 文件
10
+
11
+ 用法:
12
+ from tests.recorder import run_record_mode
13
+ asyncio.run(run_record_mode("output.yaml"))
14
+
15
+ 或通过 CLI:
16
+ python tests/testcase-ai.py --record [output_path]
17
+ """
18
+
19
+ import asyncio
20
+ import json
21
+ import os
22
+ import sys
23
+ import time
24
+ from typing import Any, Dict, List, Optional
25
+
26
+ import yaml
27
+
28
+
29
+ if sys.platform == "win32":
30
+ import io
31
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
32
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
33
+
34
+
35
+ # ============================================================
36
+ # JS 注入脚本:注入到浏览器页面中捕获用户操作
37
+ # ============================================================
38
+
39
+ RECORDER_JS = """() => {
40
+ if (window.__recorder) {
41
+ return { ok: false, message: 'Already running', count: window.__recorder.events.length };
42
+ }
43
+ window.__recorder = { events: [], start: Date.now(), version: '1.0' };
44
+
45
+ function getXPath(el) {
46
+ const parts = [];
47
+ while (el && el.nodeType === 1) {
48
+ let idx = 1, sib = el.previousSibling;
49
+ while (sib) { if (sib.nodeType === 1) idx++; sib = sib.previousSibling; }
50
+ parts.unshift(el.tagName.toLowerCase() + '[' + idx + ']');
51
+ el = el.parentNode;
52
+ }
53
+ return '/' + parts.join('/');
54
+ }
55
+
56
+ function capture(e, extra) {
57
+ const t = e.target;
58
+ window.__recorder.events.push({
59
+ type: extra.type || e.type,
60
+ ts: Date.now(),
61
+ tag: t.tagName,
62
+ id: t.id || '',
63
+ cls: (t.className || '').toString().slice(0, 60),
64
+ text: (t.textContent || '').trim().slice(0, 80),
65
+ ph: t.placeholder || '',
66
+ role: t.getAttribute('role') || '',
67
+ val: (t.value || '').slice(0, 100),
68
+ href: t.href || '',
69
+ name: t.name || '',
70
+ xpath: getXPath(t),
71
+ url: location.href,
72
+ ...extra
73
+ });
74
+ }
75
+
76
+ document.addEventListener('click', (e) => capture(e, { type: 'click' }), true);
77
+ document.addEventListener('input', (e) => capture(e, { type: 'input' }), true);
78
+ document.addEventListener('change', (e) => capture(e, { type: 'change' }), true);
79
+ document.addEventListener('keydown', (e) => {
80
+ if (e.key === 'Enter') capture(e, { type: 'enter' });
81
+ }, true);
82
+
83
+ const origPush = history.pushState;
84
+ history.pushState = function() {
85
+ window.__recorder.events.push({ type: 'navigate', ts: Date.now(), url: arguments[2] || location.href });
86
+ return origPush.apply(this, arguments);
87
+ };
88
+
89
+ return { ok: true, message: 'Recorder started', pid: performance.now().toString(36) };
90
+ }"""
91
+
92
+ POLL_JS = """() => {
93
+ if (!window.__recorder) return { ok: false };
94
+ const evts = window.__recorder.events;
95
+ const result = [...evts];
96
+ window.__recorder.events = [];
97
+ return { ok: true, elapsed: Math.round((Date.now() - window.__recorder.start) / 1000), events: result };
98
+ }"""
99
+
100
+ STOP_JS = """() => {
101
+ if (!window.__recorder) return { ok: false, events: [] };
102
+ const all = window.__recorder.events;
103
+ window.__recorder = null;
104
+ return { ok: true, totalEvents: all.length, events: all };
105
+ }"""
106
+
107
+
108
+ # ============================================================
109
+ # 事件 → 步骤转换器
110
+ # ============================================================
111
+
112
+ def _event_to_step(evt: Dict, step_num: int) -> Optional[Dict]:
113
+ """将录制事件转换为 YAML 步骤
114
+
115
+ Args:
116
+ evt: 从浏览器捕获的原始事件数据
117
+ step_num: 当前步骤序号
118
+
119
+ Returns:
120
+ YAML 步骤字典,或 None(跳过该事件)
121
+ """
122
+ etype = evt.get("type", "")
123
+
124
+ if etype == "click":
125
+ text = evt.get("text", "") or ""
126
+ ph = evt.get("ph", "") or ""
127
+ href = evt.get("href", "")
128
+ tag = evt.get("tag", "")
129
+
130
+ target = ph or text or href or f"{tag}元素"
131
+ if len(target) > 50:
132
+ target = target[:47] + "..."
133
+
134
+ step = {
135
+ "step": step_num,
136
+ "desc": f"点击'{target}'",
137
+ "action": "click",
138
+ "target": target,
139
+ }
140
+ if href and href.startswith("http"):
141
+ step["wait_after"] = {"type": "navigation", "timeout": 8000}
142
+ return step
143
+
144
+ elif etype in ("input", "change"):
145
+ val = evt.get("val", "")
146
+ ph = evt.get("ph", "") or ""
147
+ target = ph or evt.get("name") or f"{evt.get('tag','')}输入框"
148
+ if not val:
149
+ return None
150
+
151
+ is_number = val.replace(".", "").replace("-", "").isdigit()
152
+ action = "fill"
153
+
154
+ step = {
155
+ "step": step_num,
156
+ "desc": f"填写'{target}'为'{val}'",
157
+ "action": action,
158
+ "target": target,
159
+ "value": val,
160
+ }
161
+ if is_number:
162
+ step["desc"] = f"填写数值'{val}'到{target}"
163
+ return step
164
+
165
+ elif etype == "navigate":
166
+ return {
167
+ "step": step_num,
168
+ "desc": f"导航至 '{evt.get('url', '')}'",
169
+ "action": "navigate",
170
+ "url": evt.get("url", ""),
171
+ "wait_after": {"type": "time", "duration": 2000},
172
+ }
173
+
174
+ elif etype == "enter":
175
+ return None
176
+
177
+ return None
178
+
179
+
180
+ # ============================================================
181
+ # 录制模式主流程
182
+ # ============================================================
183
+
184
+ async def run_record_mode(output_path: str):
185
+ """交互式录制模式:监听用户操作 → 自动生成 YAML + .env
186
+
187
+ 流程:
188
+ 1. 通过 LoginManager 连接 Chrome DevTools MCP
189
+ 2. 向页面注入 RECORDER_JS 事件监听器
190
+ 3. 轮询 POLL_JS 获取捕获的事件,实时转换为步骤显示
191
+ 4. 用户按 Enter 停止,调用 STOP_JS 收尾
192
+ 5. 生成 YAML 用例文件和 .env 环境变量文件
193
+
194
+ Args:
195
+ output_path: 输出 YAML 文件路径(.env 同目录同名生成)
196
+ """
197
+ from .login_manager import LoginManager
198
+
199
+ print("=" * 60)
200
+ print(" Testcase Recorder v1.0")
201
+ print(" Interactive Browser Operation Recorder")
202
+ print("=" * 60)
203
+
204
+ lm = LoginManager()
205
+ session = await lm.create_session()
206
+ if not session:
207
+ print("[ERROR] Cannot connect to Chrome DevTools MCP service")
208
+ sys.exit(1)
209
+
210
+ print(f"\n[1/4] Browser connected... OK")
211
+
212
+ try:
213
+ await session.call_tool("evaluate_script", {"function": RECORDER_JS})
214
+ print("[2/4] Event listeners injected... OK")
215
+ except Exception as e:
216
+ print(f"[ERROR] Injection failed: {e}")
217
+ sys.exit(1)
218
+
219
+ print("""
220
+ +--------------------------------------------------+
221
+ | You can operate in browser now! |
222
+ | |
223
+ | - Click buttons, links, inputs |
224
+ | - Fill forms, select dropdowns |
225
+ | - Navigate to different pages |
226
+ | |
227
+ | Press Enter to stop recording |
228
+ +--------------------------------------------------+
229
+ """)
230
+
231
+ all_events: List[Dict] = []
232
+ step_num = 0
233
+ last_url = ""
234
+
235
+ import msvcrt
236
+ print("\n[3/4] Recording... (Press Enter to stop)\n")
237
+
238
+ while True:
239
+ await asyncio.sleep(1.5)
240
+ try:
241
+ raw = await session.call_tool("evaluate_script", {"function": POLL_JS})
242
+ data = json.loads(raw[0]["text"]) if isinstance(raw, list) else raw
243
+ if data.get("ok") and data.get("events"):
244
+ new_evts = data["events"]
245
+ for evt in new_evts:
246
+ step_num += 1
247
+ step = _event_to_step(evt, step_num)
248
+ if step:
249
+ all_events.append(step)
250
+ action_icon = {"click": "[click]", "fill": "[fill]", "navigate": "[nav]"}.get(step["action"], "[act]")
251
+ val_info = f" -> '{step.get('value','')}'" if step.get("value") else ""
252
+ print(f" [{step_num:02d}] {action_icon} {step['desc']}{val_info}")
253
+
254
+ if new_evts:
255
+ last_url = new_evts[-1].get("url", last_url)
256
+ except Exception:
257
+ pass
258
+
259
+ if msvcrt.kbhit():
260
+ key = msvcrt.getwch()
261
+ if key == '\r':
262
+ break
263
+
264
+ print(f"\n[4/4] Stopped. Captured {len(all_events)} operation steps\n")
265
+
266
+ try:
267
+ raw = await session.call_tool("evaluate_script", {"function": STOP_JS})
268
+ final_data = json.loads(raw[0]["text"]) if isinstance(raw, list) else raw
269
+ remaining = final_data.get("events", [])
270
+ for evt in remaining:
271
+ step_num += 1
272
+ step = _event_to_step(evt, step_num)
273
+ if step:
274
+ all_events.append(step)
275
+ except Exception:
276
+ pass
277
+
278
+ if not all_events:
279
+ print("[WARN] No operations captured, no files generated.")
280
+ return
281
+
282
+ yaml_data = {
283
+ "test_id": f"REC-{int(time.time())}-recorded",
284
+ "title": "Browser Operation Recording (auto-generated)",
285
+ "priority": "P1",
286
+ "tags": ["recorded", "auto-generated"],
287
+ "author": "Testcase Recorder v1.0",
288
+ "context_check": {
289
+ "login_url": last_url.split("/")[0] + "//" + last_url.split("/")[2] if "://" in last_url else "",
290
+ "home_indicator": "",
291
+ "credentials": {"username": "${TEST_USER}", "password": "${TEST_PASS}"},
292
+ "captcha_required": False,
293
+ },
294
+ "steps": all_events,
295
+ "teardown": [
296
+ {"action": "screenshot", "name": "recorded-result-{timestamp}.png", "fullPage": True}
297
+ ],
298
+ }
299
+
300
+ env_vars = set()
301
+ for step in all_events:
302
+ val = str(step.get("value", ""))
303
+ if "${" in val:
304
+ env_vars.add(val[2:-1])
305
+
306
+ out_dir = os.path.dirname(output_path)
307
+ os.makedirs(out_dir, exist_ok=True)
308
+
309
+ with open(output_path, 'w', encoding='utf-8') as f:
310
+ yaml.dump(yaml_data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
311
+ print(f" OK YAML: {output_path}")
312
+
313
+ env_path = output_path.rsplit('.', 1)[0] + ".env"
314
+ if env_vars:
315
+ with open(env_path, 'w', encoding='utf-8') as f:
316
+ f.write("# Recorded test case environment variables (auto-generated)\n")
317
+ for var in sorted(env_vars):
318
+ f.write(f"{var}=<please fill>\n")
319
+ print(f" OK ENV: {env_path} ({len(env_vars)} vars pending)")
320
+ else:
321
+ with open(env_path, 'w', encoding='utf-8') as f:
322
+ f.write("# Recorded test case environment variables (auto-generated)\nTEST_USER=yuanye\nTEST_PASS=yuanye\n")
323
+ print(f" OK ENV: {env_path}")
324
+
325
+ print(f"\n{'-' * 50}")
326
+ print(f"Recording complete!")
327
+ print(f" Case ID : {yaml_data['test_id']}")
328
+ print(f" Steps : {len(all_events)}")
329
+ print(f" Output:")
330
+ print(f" - {output_path}")
331
+ print(f" - {env_path}")
332
+ print(f"\nNext steps:")
333
+ print(f" 1. Edit .env file to fill real test data")
334
+ print(f" 2. Review and optimize YAML step descriptions and assertions")
335
+ print(f" 3. Run: python tests/testcase-ai.py {output_path}")
336
+
337
+
338
+ __all__ = ["run_record_mode", "_event_to_step", "RECORDER_JS", "POLL_JS", "STOP_JS"]
@@ -3178,7 +3178,7 @@ async def run_all(targets: List[Dict], env_overrides: Dict[str, str] = None,
3178
3178
 
3179
3179
  def print_usage():
3180
3180
  print("=" * 70)
3181
- print(" AI Test Framework v2.1 - 通用 AI 测试框架 (含思维链)")
3181
+ print(" AI Test Framework v2.1 - 通用 AI 测试框架 (含录制模式)")
3182
3182
  print("=" * 70)
3183
3183
  print()
3184
3184
  print("用法:")
@@ -3188,6 +3188,10 @@ def print_usage():
3188
3188
  print(f" python testcase-ai.py <yaml路径> 运行单个文件")
3189
3189
  print(f" python testcase-ai.py --continue 失败后继续执行")
3190
3190
  print()
3191
+ print("🎬 录制模式 (交互式操作录制):")
3192
+ print(f" python testcase-ai.py --record 录制到 testcases/recorded/")
3193
+ print(f" python testcase-ai.py --record <路径> 录制到指定路径")
3194
+ print()
3191
3195
  print("思维链选项:")
3192
3196
  print(f" python testcase-ai.py --think <yaml> 启用 LLM 思维链")
3193
3197
  print(f" python testcase-ai.py --think-deep <yaml> 深度思维模式")
@@ -3221,6 +3225,11 @@ def print_usage():
3221
3225
  print(f" └─ {fi['filename']}")
3222
3226
 
3223
3227
 
3228
+ # ============================================================
3229
+ # Recorder Mode - delegated to tests/recorder.py (SRP extraction)
3230
+ # ============================================================
3231
+
3232
+
3224
3233
  if __name__ == "__main__":
3225
3234
  register_builtin_actions()
3226
3235
  register_builtin_assertions()
@@ -3231,6 +3240,19 @@ if __name__ == "__main__":
3231
3240
  print_usage()
3232
3241
  sys.exit(0)
3233
3242
 
3243
+ # ===== 录制模式(优先于其他模式)=====
3244
+ if "--record" in args:
3245
+ args.remove("--record")
3246
+ output = args[0] if args else "testcases/recorded/testcase.yaml"
3247
+ if not output.endswith((".yaml", ".yml")):
3248
+ output = os.path.join(output, "testcase.yaml") if not output.endswith(".yaml") else output
3249
+ import importlib.util
3250
+ _spec = importlib.util.spec_from_file_location("recorder", os.path.join(os.path.dirname(__file__), "recorder.py"))
3251
+ _recorder = importlib.util.module_from_spec(_spec)
3252
+ _spec.loader.exec_module(_recorder)
3253
+ asyncio.run(_recorder.run_record_mode(output))
3254
+ sys.exit(0)
3255
+
3234
3256
  targets = []
3235
3257
  global_config = {}
3236
3258