@szc-ft/mcp-szcd-client 0.34.2 → 0.38.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.
Files changed (117) hide show
  1. package/agents/build.js +59 -1
  2. package/agents/platforms.json +7 -0
  3. package/agents/szcd-component-expert.qoder.md +2 -0
  4. package/opencode-extension/opencode.json +9 -0
  5. package/opencode-extension/skills/local-browser-test/SKILL.md +91 -0
  6. package/opencode-extension/skills/local-browser-test/config.example.yaml +51 -0
  7. package/opencode-extension/skills/local-browser-test/lib/api-call-executor.js +251 -0
  8. package/opencode-extension/skills/local-browser-test/lib/browser-engine.js +95 -21
  9. package/opencode-extension/skills/local-browser-test/lib/config-loader.js +104 -0
  10. package/opencode-extension/skills/local-browser-test/lib/console-archiver.js +100 -0
  11. package/opencode-extension/skills/local-browser-test/lib/global-setup.js +72 -0
  12. package/opencode-extension/skills/local-browser-test/lib/har-collector.js +124 -0
  13. package/opencode-extension/skills/local-browser-test/lib/html-reporter.js +248 -0
  14. package/opencode-extension/skills/local-browser-test/lib/projects.js +46 -0
  15. package/opencode-extension/skills/local-browser-test/lib/raw-script-helpers.js +93 -0
  16. package/opencode-extension/skills/local-browser-test/lib/redact.js +94 -0
  17. package/opencode-extension/skills/local-browser-test/lib/report-retain.js +84 -0
  18. package/opencode-extension/skills/local-browser-test/lib/retry-policy.js +160 -0
  19. package/opencode-extension/skills/local-browser-test/lib/storage-state.js +111 -0
  20. package/opencode-extension/skills/local-browser-test/lib/tag-runner.js +103 -0
  21. package/opencode-extension/skills/local-browser-test/lib/test-plan.js +2 -0
  22. package/opencode-extension/skills/local-browser-test/lib/trace-collector.js +94 -0
  23. package/opencode-extension/skills/local-browser-test/lib/trend-aggregator.js +97 -0
  24. package/opencode-extension/skills/local-browser-test/lib/workspace-init.js +178 -0
  25. package/opencode-extension/skills/local-browser-test/local-browser-executor.js +226 -2
  26. package/opencode-extension/skills/local-browser-test/tests/integration/pr-01.test.js +254 -0
  27. package/opencode-extension/skills/local-browser-test/tests/integration/pr-02.test.js +263 -0
  28. package/opencode-extension/skills/local-browser-test/tests/integration/pr-03.test.js +308 -0
  29. package/opencode-extension/skills/local-browser-test/tests/integration/pr-04.test.js +168 -0
  30. package/opencode-extension/skills/local-browser-test/tests/integration/pr-05.test.js +183 -0
  31. package/opencode-extension/skills/local-browser-test/tests/integration/pr-06.test.js +252 -0
  32. package/opencode-extension/skills/local-browser-test/tests/integration/pr-07.test.js +168 -0
  33. package/opencode-extension/skills/local-browser-test/tests/integration/pr-08.test.js +293 -0
  34. package/opencode-extension/skills/shadcn-design-to-code/SKILL.md +352 -0
  35. package/opencode-extension/skills/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
  36. package/opencode-extension/skills/shadcn-design-to-code/references/install-pitfalls.md +526 -0
  37. package/opencode-extension/skills/shadcn-design-to-code/references/layouts.md +551 -0
  38. package/opencode-extension/skills/shadcn-design-to-code/references/sketch-mapping.md +247 -0
  39. package/opencode-extension/skills/shadcn-design-to-code/references/tokens.md +534 -0
  40. package/opencode-extension/skills/szcd-upload-zip/SKILL.md +45 -0
  41. package/package.json +70 -70
  42. package/qwen-extension/qwen-extension.json +10 -1
  43. package/qwen-extension/skills/local-browser-test/SKILL.md +91 -0
  44. package/qwen-extension/skills/local-browser-test/config.example.yaml +51 -0
  45. package/qwen-extension/skills/local-browser-test/lib/api-call-executor.js +251 -0
  46. package/qwen-extension/skills/local-browser-test/lib/browser-engine.js +95 -21
  47. package/qwen-extension/skills/local-browser-test/lib/config-loader.js +104 -0
  48. package/qwen-extension/skills/local-browser-test/lib/console-archiver.js +100 -0
  49. package/qwen-extension/skills/local-browser-test/lib/global-setup.js +72 -0
  50. package/qwen-extension/skills/local-browser-test/lib/har-collector.js +124 -0
  51. package/qwen-extension/skills/local-browser-test/lib/html-reporter.js +248 -0
  52. package/qwen-extension/skills/local-browser-test/lib/projects.js +46 -0
  53. package/qwen-extension/skills/local-browser-test/lib/raw-script-helpers.js +93 -0
  54. package/qwen-extension/skills/local-browser-test/lib/redact.js +94 -0
  55. package/qwen-extension/skills/local-browser-test/lib/report-retain.js +84 -0
  56. package/qwen-extension/skills/local-browser-test/lib/retry-policy.js +160 -0
  57. package/qwen-extension/skills/local-browser-test/lib/storage-state.js +111 -0
  58. package/qwen-extension/skills/local-browser-test/lib/tag-runner.js +103 -0
  59. package/qwen-extension/skills/local-browser-test/lib/test-plan.js +2 -0
  60. package/qwen-extension/skills/local-browser-test/lib/trace-collector.js +94 -0
  61. package/qwen-extension/skills/local-browser-test/lib/trend-aggregator.js +97 -0
  62. package/qwen-extension/skills/local-browser-test/lib/workspace-init.js +178 -0
  63. package/qwen-extension/skills/local-browser-test/local-browser-executor.js +226 -2
  64. package/qwen-extension/skills/local-browser-test/tests/integration/pr-01.test.js +254 -0
  65. package/qwen-extension/skills/local-browser-test/tests/integration/pr-02.test.js +263 -0
  66. package/qwen-extension/skills/local-browser-test/tests/integration/pr-03.test.js +308 -0
  67. package/qwen-extension/skills/local-browser-test/tests/integration/pr-04.test.js +168 -0
  68. package/qwen-extension/skills/local-browser-test/tests/integration/pr-05.test.js +183 -0
  69. package/qwen-extension/skills/local-browser-test/tests/integration/pr-06.test.js +252 -0
  70. package/qwen-extension/skills/local-browser-test/tests/integration/pr-07.test.js +168 -0
  71. package/qwen-extension/skills/local-browser-test/tests/integration/pr-08.test.js +293 -0
  72. package/qwen-extension/skills/shadcn-design-to-code/SKILL.md +352 -0
  73. package/qwen-extension/skills/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
  74. package/qwen-extension/skills/shadcn-design-to-code/references/install-pitfalls.md +526 -0
  75. package/qwen-extension/skills/shadcn-design-to-code/references/layouts.md +551 -0
  76. package/qwen-extension/skills/shadcn-design-to-code/references/sketch-mapping.md +247 -0
  77. package/qwen-extension/skills/shadcn-design-to-code/references/tokens.md +534 -0
  78. package/qwen-extension/skills/szcd-upload-zip/SKILL.md +45 -0
  79. package/scripts/lib/common.js +8 -1
  80. package/standard-skill/local-browser-test/SKILL.md +91 -0
  81. package/standard-skill/local-browser-test/config.example.yaml +51 -0
  82. package/standard-skill/local-browser-test/lib/api-call-executor.js +251 -0
  83. package/standard-skill/local-browser-test/lib/browser-engine.js +95 -21
  84. package/standard-skill/local-browser-test/lib/config-loader.js +104 -0
  85. package/standard-skill/local-browser-test/lib/console-archiver.js +100 -0
  86. package/standard-skill/local-browser-test/lib/global-setup.js +72 -0
  87. package/standard-skill/local-browser-test/lib/har-collector.js +124 -0
  88. package/standard-skill/local-browser-test/lib/html-reporter.js +248 -0
  89. package/standard-skill/local-browser-test/lib/projects.js +46 -0
  90. package/standard-skill/local-browser-test/lib/raw-script-helpers.js +93 -0
  91. package/standard-skill/local-browser-test/lib/redact.js +94 -0
  92. package/standard-skill/local-browser-test/lib/report-retain.js +84 -0
  93. package/standard-skill/local-browser-test/lib/retry-policy.js +160 -0
  94. package/standard-skill/local-browser-test/lib/storage-state.js +111 -0
  95. package/standard-skill/local-browser-test/lib/tag-runner.js +103 -0
  96. package/standard-skill/local-browser-test/lib/test-plan.js +2 -0
  97. package/standard-skill/local-browser-test/lib/trace-collector.js +94 -0
  98. package/standard-skill/local-browser-test/lib/trend-aggregator.js +97 -0
  99. package/standard-skill/local-browser-test/lib/workspace-init.js +178 -0
  100. package/standard-skill/local-browser-test/local-browser-executor.js +226 -2
  101. package/standard-skill/local-browser-test/tests/integration/pr-01.test.js +254 -0
  102. package/standard-skill/local-browser-test/tests/integration/pr-02.test.js +263 -0
  103. package/standard-skill/local-browser-test/tests/integration/pr-03.test.js +308 -0
  104. package/standard-skill/local-browser-test/tests/integration/pr-04.test.js +168 -0
  105. package/standard-skill/local-browser-test/tests/integration/pr-05.test.js +183 -0
  106. package/standard-skill/local-browser-test/tests/integration/pr-06.test.js +252 -0
  107. package/standard-skill/local-browser-test/tests/integration/pr-07.test.js +168 -0
  108. package/standard-skill/local-browser-test/tests/integration/pr-08.test.js +293 -0
  109. package/standard-skill/shadcn-design-to-code/SKILL.md +352 -0
  110. package/standard-skill/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
  111. package/standard-skill/shadcn-design-to-code/references/install-pitfalls.md +526 -0
  112. package/standard-skill/shadcn-design-to-code/references/layouts.md +551 -0
  113. package/standard-skill/shadcn-design-to-code/references/sketch-mapping.md +247 -0
  114. package/standard-skill/shadcn-design-to-code/references/tokens.md +534 -0
  115. package/standard-skill/szcd-upload-zip/SKILL.md +45 -0
  116. package/opencode-extension/skills/local-browser-test/local-browser-executor.cjs +0 -742
  117. package/qwen-extension/skills/local-browser-test/local-browser-executor.cjs +0 -742
@@ -0,0 +1,97 @@
1
+ /**
2
+ * trend.json 按日聚合(v4 关键能力)
3
+ *
4
+ * 设计:
5
+ * - schemaVersion: 1
6
+ * - byDay[YYYY-MM-DD]: { runs, total, passed, failed, flaky, skipped,
7
+ * byStrategy: {api, ui, newman, browser},
8
+ * byTag: {}, byProject: {}, byBranch: {} }
9
+ */
10
+
11
+ import fs from "node:fs";
12
+
13
+ const DEFAULTS = {
14
+ schemaVersion: 1,
15
+ updatedAt: null,
16
+ byDay: {}
17
+ };
18
+
19
+ /**
20
+ * 把一次 run 追加到 trend.json
21
+ * @param {object} result
22
+ * @param {string} trendPath
23
+ */
24
+ export function appendToTrend(result, trendPath) {
25
+ const trend = fs.existsSync(trendPath)
26
+ ? JSON.parse(fs.readFileSync(trendPath, "utf8"))
27
+ : structuredClone(DEFAULTS);
28
+
29
+ const day = (result.startedAt ?? new Date().toISOString()).slice(0, 10);
30
+ if (!trend.byDay[day]) {
31
+ trend.byDay[day] = {
32
+ runs: 0, total: 0, passed: 0, failed: 0, flaky: 0, skipped: 0,
33
+ byStrategy: { api: 0, ui: 0, newman: 0, browser: 0 },
34
+ byTag: {},
35
+ byProject: {},
36
+ byBranch: {}
37
+ };
38
+ }
39
+
40
+ const d = trend.byDay[day];
41
+ d.runs += 1;
42
+ d.total += result.stats?.total ?? 0;
43
+ d.passed += result.stats?.passed ?? 0;
44
+ d.failed += result.stats?.failed ?? 0;
45
+ d.flaky += result.stats?.flaky ?? 0;
46
+ d.skipped += result.stats?.skipped ?? 0;
47
+
48
+ // ★ v4 新增:按 strategy 分桶
49
+ const strategy = result.strategy ?? "newman";
50
+ d.byStrategy[strategy] = (d.byStrategy[strategy] ?? 0) + (result.stats?.total ?? 0);
51
+
52
+ // 按 tag 分桶
53
+ if (result.tag) {
54
+ if (!d.byTag[result.tag]) d.byTag[result.tag] = { runs: 0, passed: 0, failed: 0 };
55
+ const t = d.byTag[result.tag];
56
+ t.runs += 1;
57
+ t.passed += result.stats?.passed ?? 0;
58
+ t.failed += result.stats?.failed ?? 0;
59
+ }
60
+
61
+ // 按 project 分桶
62
+ if (result.project) {
63
+ if (!d.byProject[result.project]) d.byProject[result.project] = { runs: 0, passed: 0, failed: 0 };
64
+ const p = d.byProject[result.project];
65
+ p.runs += 1;
66
+ p.passed += result.stats?.passed ?? 0;
67
+ p.failed += result.stats?.failed ?? 0;
68
+ }
69
+
70
+ // 按 branch 分桶
71
+ if (result.branch) {
72
+ if (!d.byBranch[result.branch]) d.byBranch[result.branch] = { runs: 0, passed: 0, failed: 0 };
73
+ const b = d.byBranch[result.branch];
74
+ b.runs += 1;
75
+ b.passed += result.stats?.passed ?? 0;
76
+ b.failed += result.stats?.failed ?? 0;
77
+ }
78
+
79
+ trend.updatedAt = new Date().toISOString();
80
+ fs.writeFileSync(trendPath, JSON.stringify(trend, null, 2));
81
+ }
82
+
83
+ /**
84
+ * 裁剪 trend.json:保留最近 N 天
85
+ * @param {string} trendPath
86
+ * @param {number} days
87
+ */
88
+ export function trimTrendToDays(trendPath, days) {
89
+ if (!fs.existsSync(trendPath)) return;
90
+ const trend = JSON.parse(fs.readFileSync(trendPath, "utf8"));
91
+ const cutoff = new Date(Date.now() - days * 24 * 3600 * 1000).toISOString().slice(0, 10);
92
+ for (const day of Object.keys(trend.byDay)) {
93
+ if (day < cutoff) delete trend.byDay[day];
94
+ }
95
+ trend.updatedAt = new Date().toISOString();
96
+ fs.writeFileSync(trendPath, JSON.stringify(trend, null, 2));
97
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * .lbt/ workspace 自动初始化(v3 多次调用安全)
3
+ *
4
+ * 关键设计:
5
+ * - 缺啥补啥,绝不覆盖任何已有文件
6
+ * - 双层 .gitignore(.lbt/.gitignore + 项目级 .gitignore 追加)
7
+ * - .fingerprint 走 read → merge → write 增量更新
8
+ * - --init-force 显式重置(需 yes 二次确认)
9
+ */
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import readline from "node:readline";
14
+
15
+ // ESM-friendly 方式读 package.json(避免 createRequire 在 lib 内 resolve 失败)
16
+ const PKG_URL = new URL("../package.json", import.meta.url);
17
+ const PKG = JSON.parse(fs.readFileSync(PKG_URL, "utf8"));
18
+
19
+ const INNER_GITIGNORE_CONTENT =
20
+ "auth/\n" +
21
+ "*.log\n" +
22
+ "config.yaml\n" +
23
+ "reports/\n";
24
+
25
+ const PROJECT_GITIGNORE_RULES = [
26
+ ".lbt/auth/",
27
+ ".lbt/config.yaml",
28
+ ".lbt/reports/",
29
+ ".lbt/*.log"
30
+ ];
31
+
32
+ /**
33
+ * 幂等追加到 .gitignore(不破坏用户已有内容)
34
+ * @param {string} projectGi 项目级 .gitignore 路径
35
+ * @param {string[]} rules 要追加的规则
36
+ * @returns {string[]} 实际新增的规则(已存在的跳过)
37
+ */
38
+ export function appendIfMissing(projectGi, rules) {
39
+ if (!fs.existsSync(projectGi)) {
40
+ fs.writeFileSync(projectGi, rules.join("\n") + "\n");
41
+ return rules;
42
+ }
43
+ const content = fs.readFileSync(projectGi, "utf8");
44
+ const missing = rules.filter((r) => !content.includes(r));
45
+ if (missing.length > 0) {
46
+ const header = "\n# szcd local-browser-test workspace\n";
47
+ fs.appendFileSync(projectGi, header + missing.join("\n") + "\n");
48
+ return missing;
49
+ }
50
+ return [];
51
+ }
52
+
53
+ /**
54
+ * ensureWorkspace: 缺啥补啥,绝不覆盖任何已有文件
55
+ * - 子目录:mkdir recursive(已存在则 no-op)
56
+ * - .lbt/.gitignore:已存在则跳过
57
+ * - config.yaml.example:已存在则跳过(用户可能改过)
58
+ * - config.yaml:永远不主动建(避免覆盖用户配置)
59
+ * - 项目 .gitignore:appendIfMissing
60
+ * - .fingerprint:read → merge → write(保留 firstInitAt / initCount)
61
+ *
62
+ * @param {string} SKILL_DIR skill 目录绝对路径
63
+ * @param {string} [lbtDirOverride] 可选,cwd 外的 .lbt/ 路径
64
+ * @returns {string} .lbt 实际路径
65
+ */
66
+ export function ensureWorkspace(SKILL_DIR, lbtDirOverride = null) {
67
+ const lbtDir = lbtDirOverride || path.join(process.cwd(), ".lbt");
68
+ const isNew = !fs.existsSync(lbtDir);
69
+ const actions = [];
70
+
71
+ // 1. 子目录(idempotent:mkdir recursive 已存在则 no-op)
72
+ for (const sub of ["plans/regression", "plans/smoke", "reports", "auth", "api"]) {
73
+ fs.mkdirSync(path.join(lbtDir, sub), { recursive: true });
74
+ }
75
+
76
+ // 2. .lbt/.gitignore(内部自带,双层防御)
77
+ const innerGi = path.join(lbtDir, ".gitignore");
78
+ if (!fs.existsSync(innerGi)) {
79
+ fs.writeFileSync(innerGi, INNER_GITIGNORE_CONTENT);
80
+ actions.push("+ 创建 .lbt/.gitignore");
81
+ } else {
82
+ actions.push("· .lbt/.gitignore 已存在,跳过");
83
+ }
84
+
85
+ // 3. config.yaml.example(已存在则跳过,用户可能改过)
86
+ const examplePath = path.join(lbtDir, "config.yaml.example");
87
+ if (!fs.existsSync(examplePath)) {
88
+ const sourceExample = path.join(SKILL_DIR, "config.example.yaml");
89
+ if (fs.existsSync(sourceExample)) {
90
+ fs.copyFileSync(sourceExample, examplePath);
91
+ actions.push("+ 创建 config.yaml.example");
92
+ }
93
+ } else {
94
+ actions.push("· config.yaml.example 已存在,跳过");
95
+ }
96
+
97
+ // 4. config.yaml — 永远不主动建(避免覆盖用户配置)
98
+
99
+ // 5. 项目级 .gitignore(幂等追加)
100
+ const appended = appendIfMissing(path.join(process.cwd(), ".gitignore"), PROJECT_GITIGNORE_RULES);
101
+ if (appended.length > 0) {
102
+ actions.push(`+ 项目 .gitignore 追加: ${appended.join(", ")}`);
103
+ } else {
104
+ actions.push("· 项目 .gitignore 已包含 .lbt/ 规则,跳过");
105
+ }
106
+
107
+ // 6. .fingerprint(read → merge → write 保留 firstInitAt)
108
+ const fpPath = path.join(lbtDir, ".fingerprint");
109
+ const existing = fs.existsSync(fpPath)
110
+ ? JSON.parse(fs.readFileSync(fpPath, "utf8"))
111
+ : {};
112
+ const now = new Date().toISOString();
113
+ const merged = {
114
+ ...existing,
115
+ skillDir: SKILL_DIR,
116
+ executorVersion: PKG.version,
117
+ lastInitAt: now,
118
+ firstInitAt: existing.firstInitAt ?? now,
119
+ initCount: (existing.initCount ?? 0) + 1,
120
+ runCount: existing.runCount ?? 0, // 由 bumpRunCount 时 +1
121
+ };
122
+ fs.writeFileSync(fpPath, JSON.stringify(merged, null, 2));
123
+
124
+ // 7. 打印动作摘要
125
+ console.log(`${isNew ? "✓" : "↻"} workspace ${lbtDir}:`);
126
+ for (const a of actions) console.log(` ${a}`);
127
+ if (isNew) {
128
+ console.log(` 提示:cp .lbt/config.yaml.example .lbt/config.yaml 并填值`);
129
+ }
130
+
131
+ return lbtDir;
132
+ }
133
+
134
+ /**
135
+ * initForce: 显式强制重置(需 --init-force 显式开启 + yes 二次确认)
136
+ * @param {string} SKILL_DIR
137
+ * @param {string} [lbtDirOverride]
138
+ * @returns {Promise<string>} .lbt 实际路径
139
+ */
140
+ export async function initForce(SKILL_DIR, lbtDirOverride = null) {
141
+ const lbtDir = lbtDirOverride || path.join(process.cwd(), ".lbt");
142
+ if (fs.existsSync(lbtDir)) {
143
+ console.log(`⚠ 将清空 .lbt/ 全部内容(含 plans / reports / auth / config)`);
144
+ console.log(` 如有重要 plan 请先备份到 .lbt/plans/ 之外的位置`);
145
+ const answer = await prompt("\n继续清空?(yes/no): ");
146
+ if (answer !== "yes") {
147
+ console.log("已取消,workspace 保持原样");
148
+ return lbtDir;
149
+ }
150
+ fs.rmSync(lbtDir, { recursive: true, force: true });
151
+ console.log("✓ 已清空 .lbt/");
152
+ }
153
+ return ensureWorkspace(SKILL_DIR, lbtDirOverride);
154
+ }
155
+
156
+ function prompt(question) {
157
+ return new Promise((resolve) => {
158
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
159
+ rl.question(question, (ans) => {
160
+ rl.close();
161
+ resolve(ans.trim().toLowerCase());
162
+ });
163
+ });
164
+ }
165
+
166
+ /**
167
+ * 跑完一次 run 后,更新 .fingerprint 的 runCount 和 lastRunAt
168
+ * @param {string} [lbtDirOverride]
169
+ */
170
+ export function bumpRunCount(lbtDirOverride = null) {
171
+ const lbtDir = lbtDirOverride || path.join(process.cwd(), ".lbt");
172
+ const fpPath = path.join(lbtDir, ".fingerprint");
173
+ if (!fs.existsSync(fpPath)) return;
174
+ const fp = JSON.parse(fs.readFileSync(fpPath, "utf8"));
175
+ fp.runCount = (fp.runCount ?? 0) + 1;
176
+ fp.lastRunAt = new Date().toISOString();
177
+ fs.writeFileSync(fpPath, JSON.stringify(fp, null, 2));
178
+ }
@@ -31,6 +31,18 @@ import fs from "node:fs";
31
31
  import path from "node:path";
32
32
  import { fileURLToPath } from "node:url";
33
33
  import { BrowserEngine } from "./lib/browser-engine.js";
34
+ // PR #1: 配置加载 + 脱敏
35
+ import { loadConfig } from "./lib/config-loader.js";
36
+ import { redact } from "./lib/redact.js";
37
+ // PR #2: workspace + multi-env + storageState + globalSetup + projects
38
+ import { ensureWorkspace, initForce, bumpRunCount } from "./lib/workspace-init.js";
39
+ import { applyStorageState, buildCookieHeader } from "./lib/storage-state.js";
40
+ import { globalSetupTakeover, globalSetupFillForm } from "./lib/global-setup.js";
41
+ import { applyProject, listProjects } from "./lib/projects.js";
42
+ // PR #3: v4 Playwright 风格 HTML 报告
43
+ import { generateRunHtml, appendRunCardToIndex, initIndexHtml } from "./lib/html-reporter.js";
44
+ import { appendToTrend } from "./lib/trend-aggregator.js";
45
+ import { applyRetain, archiveFailedRun } from "./lib/report-retain.js";
34
46
 
35
47
  const __filename = fileURLToPath(import.meta.url);
36
48
  const __dirname = path.dirname(__filename);
@@ -52,6 +64,71 @@ function parseArgs() {
52
64
  case "--plan-file":
53
65
  parsed.planFile = args[++i];
54
66
  break;
67
+ // ★ PR #1 v3: 配置外置
68
+ case "--config":
69
+ parsed.config = args[++i];
70
+ break;
71
+ case "--env":
72
+ parsed.env = args[++i];
73
+ break;
74
+ // ★ PR #2 v3: workspace + multi-env + storageState + globalSetup
75
+ case "--workspace":
76
+ parsed.workspace = args[++i];
77
+ break;
78
+ case "--project":
79
+ parsed.project = args[++i];
80
+ break;
81
+ case "--storage-state":
82
+ parsed.storageState = args[++i];
83
+ break;
84
+ case "--global-setup":
85
+ parsed.globalSetup = true;
86
+ break;
87
+ case "--init-force":
88
+ parsed.initForce = true;
89
+ break;
90
+ // ★ PR #5 v3: 失败定位资料(trace / video / HAR / console)
91
+ case "--screenshot":
92
+ parsed.screenshotPolicy = args[++i] || 'always';
93
+ break;
94
+ case "--video":
95
+ parsed.videoPolicy = args[++i] || 'off';
96
+ break;
97
+ case "--trace":
98
+ parsed.tracePolicy = args[++i] || 'off';
99
+ break;
100
+ case "--record-har":
101
+ parsed.recordHar = true;
102
+ break;
103
+ case "--ci":
104
+ parsed.ci = true;
105
+ // --ci 一键切 CI 模式
106
+ if (!parsed.screenshotPolicy) parsed.screenshotPolicy = 'only-on-failure';
107
+ if (!parsed.videoPolicy) parsed.videoPolicy = 'retain-on-failure';
108
+ if (!parsed.tracePolicy) parsed.tracePolicy = 'retain-on-failure';
109
+ // PR #6 v3: --ci 默认 retries=2
110
+ if (parsed.retries === undefined) parsed.retries = 2;
111
+ break;
112
+ // ★ PR #6 v3: 重试策略(用例级 + 写操作禁重试 + flaky)
113
+ case "--retries":
114
+ parsed.retries = parseInt(args[++i], 10) || 0;
115
+ break;
116
+ case "--flaky-threshold":
117
+ parsed.flakyThreshold = parseInt(args[++i], 10) || 5;
118
+ break;
119
+ case "--no-retry-actions":
120
+ parsed.noRetryActions = true;
121
+ break;
122
+ // ★ PR #7 v3: 用例标签过滤
123
+ case "--tags":
124
+ parsed.tags = args[++i] || "";
125
+ break;
126
+ case "--exclude-tags":
127
+ parsed.excludeTags = args[++i] || "";
128
+ break;
129
+ case "--tag-mode":
130
+ parsed.tagMode = args[++i] || "any";
131
+ break;
55
132
  case "--output":
56
133
  parsed.output = args[++i];
57
134
  break;
@@ -694,7 +771,8 @@ async function executeAction(args) {
694
771
  if (args.testReport) {
695
772
  const dir = path.dirname(args.testReport);
696
773
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
697
- fs.writeFileSync(args.testReport, JSON.stringify(result, null, 2), "utf8");
774
+ // PR #1 v3: 落盘前 redact
775
+ fs.writeFileSync(args.testReport, JSON.stringify(redact(result), null, 2), "utf8");
698
776
  process.stderr.write(`测试报告已写入: ${args.testReport}\n`);
699
777
  }
700
778
 
@@ -894,6 +972,91 @@ async function executeAction(args) {
894
972
  async function main() {
895
973
  const args = parseArgs();
896
974
 
975
+ // ★ PR #1 v3: 加载配置(必须在 args 解析后立即做)
976
+ let config = null;
977
+ try {
978
+ config = loadConfig(args);
979
+ // 注入到 args,供后续 action 使用
980
+ args.config = config;
981
+ // 兼容 baseURL 字段
982
+ if (!args.baseUrl && config.app?.baseURL) {
983
+ args.baseUrl = config.app.baseURL;
984
+ }
985
+ if (config.auth?.type) args.authType = config.auth.type;
986
+ } catch (err) {
987
+ // 配置加载失败:若用户显式传 --config/--env 则报错,否则用默认
988
+ if (args.config || args.env) {
989
+ process.stderr.write(`❌ 配置加载失败: ${err.message}\n`);
990
+ process.exit(2);
991
+ }
992
+ // 隐式加载 + 没指定 → 用默认(不报错)
993
+ process.stderr.write(`ℹ️ 使用默认配置(${err.message})\n`);
994
+ }
995
+
996
+ // ★ PR #2 v3: 加载配置后立即 apply project(多环境)
997
+ if (config && args.project) {
998
+ try {
999
+ const updated = applyProject(config, args.project);
1000
+ args.config = updated;
1001
+ if (updated.app?.baseURL && !args.baseUrl) args.baseUrl = updated.app.baseURL;
1002
+ if (updated.auth?.token) args.authToken = updated.auth.token;
1003
+ if (updated.auth?.type) args.authType = updated.auth.type;
1004
+ } catch (err) {
1005
+ process.stderr.write(`❌ ${err.message}\n`);
1006
+ process.exit(2);
1007
+ }
1008
+ }
1009
+
1010
+ // ★ PR #2 v3: workspace 初始化(仅显式 --workspace 或 --init-force 时)
1011
+ if (args.workspace || args.initForce) {
1012
+ const SKILL_DIR = __dirname;
1013
+ if (args.initForce) {
1014
+ await initForce(SKILL_DIR, args.workspace || null);
1015
+ } else {
1016
+ ensureWorkspace(SKILL_DIR, args.workspace || null);
1017
+ }
1018
+ args.workspace = args.workspace || path.join(process.cwd(), ".lbt");
1019
+ }
1020
+
1021
+ // ★ PR #2 v3: globalSetup 模式(不走 testCase,单独导出 storageState)
1022
+ if (args.globalSetup) {
1023
+ try {
1024
+ // 启动浏览器
1025
+ const engine = new BrowserEngine();
1026
+ const initResult = await engine.initialize({ cdpUrl: args.cdpUrl });
1027
+ if (initResult?.error) {
1028
+ process.stderr.write(`❌ 浏览器初始化失败: ${initResult.error}\n`);
1029
+ process.exit(2);
1030
+ }
1031
+ const page = await engine.newPage();
1032
+
1033
+ const auth = args.config?.auth ?? {};
1034
+ const outputPath = args.storageState || path.join(
1035
+ args.workspace || path.join(process.cwd(), ".lbt"),
1036
+ "auth",
1037
+ `${args.project || "default"}.json`
1038
+ );
1039
+
1040
+ if (auth.username && auth.password) {
1041
+ // 模式 2:自动填表
1042
+ await globalSetupFillForm(page, {
1043
+ loginUrl: (args.baseUrl || "http://localhost:3000") + "/login",
1044
+ username: auth.username,
1045
+ password: auth.password,
1046
+ verifyCode: auth.verifyCode
1047
+ }, outputPath);
1048
+ } else {
1049
+ // 模式 1:接管浏览器(用户在浏览器里已登录)
1050
+ await globalSetupTakeover(page, outputPath);
1051
+ }
1052
+ await engine.close();
1053
+ process.exit(0);
1054
+ } catch (err) {
1055
+ process.stderr.write(`❌ globalSetup 失败: ${err.message}\n`);
1056
+ process.exit(2);
1057
+ }
1058
+ }
1059
+
897
1060
  if (args.action === "assert-api" || args.action === "assertApi") {
898
1061
  try {
899
1062
  const results = executeAssertApiAction(args);
@@ -939,6 +1102,9 @@ async function main() {
939
1102
  // 输出结果
940
1103
  outputResult(results, args.output);
941
1104
 
1105
+ // ★ PR #3 v3: 生成 v4 报告(仅 workspace 模式启用)
1106
+ v4ReportIfEnabled(results, args);
1107
+
942
1108
  // 输出摘要
943
1109
  process.stderr.write(`\n执行完成:\n`);
944
1110
  process.stderr.write(` 模式: ${results.mode || "failed"}\n`);
@@ -960,7 +1126,9 @@ async function main() {
960
1126
  }
961
1127
 
962
1128
  function outputResult(results, outputPath) {
963
- const json = JSON.stringify(results, null, 2);
1129
+ // PR #1 v3: 落盘前必走 redact(password / verifyCode / credentials / token 等)
1130
+ const safe = redact(results);
1131
+ const json = JSON.stringify(safe, null, 2);
964
1132
 
965
1133
  if (outputPath) {
966
1134
  const dir = path.dirname(outputPath);
@@ -974,6 +1142,62 @@ function outputResult(results, outputPath) {
974
1142
  }
975
1143
  }
976
1144
 
1145
+ /**
1146
+ * ★ PR #3 v3: 生成 v4 Playwright 风格 HTML 报告
1147
+ * - 仅在 args.workspace 存在时启用
1148
+ * - 生成 runs/<id>/index.html + trend.json + 入口 index.html + retain
1149
+ * - 失败归档到 runs-failed/
1150
+ */
1151
+ function v4ReportIfEnabled(results, args) {
1152
+ if (!args.workspace) return; // 仅 workspace 模式启用
1153
+ try {
1154
+ const reportsDir = path.join(args.workspace, "reports");
1155
+ const runId = `${(results.startedAt ?? new Date().toISOString()).replace(/[:.]/g, "-")}-pid${process.pid}`;
1156
+ const runDir = path.join(reportsDir, "runs", runId);
1157
+ fs.mkdirSync(runDir, { recursive: true });
1158
+
1159
+ // 1. 补 runId
1160
+ results.runId = runId;
1161
+
1162
+ // 2. 写 result.json(redact 后)
1163
+ fs.writeFileSync(
1164
+ path.join(runDir, "result.json"),
1165
+ JSON.stringify(redact(results), null, 2)
1166
+ );
1167
+
1168
+ // 3. 生成单 run HTML
1169
+ generateRunHtml(results, runDir);
1170
+
1171
+ // 4. 初始化入口 + 追加 run card(仅首次)
1172
+ initIndexHtml(reportsDir);
1173
+ appendRunCardToIndex(results, path.join(reportsDir, "index.html"));
1174
+
1175
+ // 5. 写 trend.json
1176
+ appendToTrend(results, path.join(reportsDir, "trend.json"));
1177
+
1178
+ // 6. 失败归档
1179
+ if ((results.stats?.failed ?? 0) > 0) {
1180
+ archiveFailedRun(runId, runDir, path.join(reportsDir, "runs-failed"));
1181
+ }
1182
+
1183
+ // 7. retain 策略
1184
+ const reportCfg = args.config?.report ?? {};
1185
+ const retainResult = applyRetain(reportsDir, {
1186
+ retainRuns: reportCfg.retainRuns ?? 30,
1187
+ retainRunsByDay: reportCfg.retainDays ?? 7,
1188
+ retainFailedForever: reportCfg.retainFailedForever ?? true
1189
+ });
1190
+
1191
+ // 8. bumpRunCount
1192
+ bumpRunCount(args.workspace);
1193
+
1194
+ process.stderr.write(`✓ v4 报告: ${reportsDir}/runs/${runId}/index.html\n`);
1195
+ process.stderr.write(` (保留 ${retainResult.kept} / 移除 ${retainResult.removed} / 总 ${retainResult.total})\n`);
1196
+ } catch (err) {
1197
+ process.stderr.write(`⚠️ v4 报告生成失败: ${err.message}\n`);
1198
+ }
1199
+ }
1200
+
977
1201
  main().catch((err) => {
978
1202
  process.stderr.write(`Fatal error: ${err.message}\n`);
979
1203
  process.exit(2);