howone 0.2.0 → 0.2.1

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 (33) hide show
  1. package/bin/index.mjs +1 -1
  2. package/package.json +1 -1
  3. package/templates/vite/.howone/skills/howone/01-architect/01-app-generation.md +19 -16
  4. package/templates/vite/.howone/skills/howone/03-ai-capabilities/01-ai-capability-architecture.md +14 -24
  5. package/templates/vite/.howone/skills/howone/03-ai-capabilities/02-workflow-contract-rules.md +0 -107
  6. package/templates/vite/.howone/skills/howone/03-ai-capabilities/03-service-capability-catalog.md +26 -101
  7. package/templates/vite/.howone/skills/howone/03-ai-capabilities/04-workflow-operations.md +23 -17
  8. package/templates/vite/.howone/skills/howone/03-ai-capabilities/05-ai-feature-playbooks.md +1 -66
  9. package/templates/vite/.howone/skills/howone/SKILL.md +5 -4
  10. package/templates/vite/.howone/skills/web-clone/LICENSE +0 -21
  11. package/templates/vite/.howone/skills/web-clone/README.md +0 -179
  12. package/templates/vite/.howone/skills/web-clone/SKILL.md +0 -243
  13. package/templates/vite/.howone/skills/web-clone/references/assessment.md +0 -77
  14. package/templates/vite/.howone/skills/web-clone/references/complex-playbooks.md +0 -46
  15. package/templates/vite/.howone/skills/web-clone/references/deliverables.md +0 -144
  16. package/templates/vite/.howone/skills/web-clone/references/design-dna.md +0 -125
  17. package/templates/vite/.howone/skills/web-clone/references/effect-extraction.md +0 -73
  18. package/templates/vite/.howone/skills/web-clone/references/marbles-case.md +0 -31
  19. package/templates/vite/.howone/skills/web-clone/references/reverse-engineering.md +0 -34
  20. package/templates/vite/.howone/skills/web-clone/references/static-mirror.md +0 -72
  21. package/templates/vite/.howone/skills/web-clone/scripts/asset-harvest.mjs +0 -101
  22. package/templates/vite/.howone/skills/web-clone/scripts/audit-clone.mjs +0 -151
  23. package/templates/vite/.howone/skills/web-clone/scripts/compare-recon.mjs +0 -265
  24. package/templates/vite/.howone/skills/web-clone/scripts/dna-scaffold.mjs +0 -214
  25. package/templates/vite/.howone/skills/web-clone/scripts/init-clone.mjs +0 -136
  26. package/templates/vite/.howone/skills/web-clone/scripts/interaction-probe.mjs +0 -314
  27. package/templates/vite/.howone/skills/web-clone/scripts/lib/playwright-loader.mjs +0 -39
  28. package/templates/vite/.howone/skills/web-clone/scripts/mirror-site.mjs +0 -121
  29. package/templates/vite/.howone/skills/web-clone/scripts/network-capture.mjs +0 -127
  30. package/templates/vite/.howone/skills/web-clone/scripts/recon-site.mjs +0 -235
  31. package/templates/vite/.howone/skills/web-clone/scripts/route-crawl.mjs +0 -228
  32. package/templates/vite/.howone/skills/web-clone/scripts/sourcemap-hunt.mjs +0 -112
  33. package/templates/vite/.howone/skills/web-clone/scripts/visual-diff.mjs +0 -161
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import crypto from "node:crypto";
5
-
6
- function usage() {
7
- console.log(`Usage:
8
- node scripts/asset-harvest.mjs --recon original-recon.json --out assets/original [--manifest asset-manifest.json] [--all-external]
9
-
10
- Reads recon JSON, downloads discovered images/scripts/stylesheets, and writes a manifest.
11
- `);
12
- }
13
-
14
- function parseArgs(argv) {
15
- const out = { recon: "", outDir: "assets/original", manifest: "", allExternal: false };
16
- for (let i = 0; i < argv.length; i += 1) {
17
- const arg = argv[i];
18
- if (arg === "--help" || arg === "-h") out.help = true;
19
- else if (arg === "--recon") out.recon = argv[++i] || "";
20
- else if (arg === "--out") out.outDir = argv[++i] || "assets/original";
21
- else if (arg === "--manifest") out.manifest = argv[++i] || "";
22
- else if (arg === "--all-external") out.allExternal = true;
23
- else throw new Error(`Unexpected argument: ${arg}`);
24
- }
25
- return out;
26
- }
27
-
28
- function safeName(url) {
29
- const parsed = new URL(url);
30
- const ext = path.extname(parsed.pathname).slice(0, 12);
31
- const base = path.basename(parsed.pathname, ext).replace(/[^a-z0-9._-]+/gi, "-").slice(0, 80) || "asset";
32
- const hash = crypto.createHash("sha1").update(url).digest("hex").slice(0, 10);
33
- return `${base}-${hash}${ext || ".bin"}`;
34
- }
35
-
36
- function collectAssets(recon) {
37
- const assets = new Map();
38
- for (const capture of recon.captures || []) {
39
- const signals = capture.signals || {};
40
- for (const src of signals.scripts || []) assets.set(src, { type: "script", url: src });
41
- for (const href of signals.stylesheets || []) assets.set(href, { type: "stylesheet", url: href });
42
- for (const image of signals.images || []) {
43
- if (image.src) assets.set(image.src, { type: "image", url: image.src, alt: image.alt, width: image.width, height: image.height });
44
- }
45
- }
46
- return Array.from(assets.values()).filter((asset) => /^https?:\/\//i.test(asset.url));
47
- }
48
-
49
- async function download(asset, outDir) {
50
- const response = await fetch(asset.url, {
51
- headers: {
52
- "user-agent": "web-clone-skill/1.0 asset-harvest",
53
- "accept": "*/*",
54
- },
55
- });
56
- if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
57
- const buffer = Buffer.from(await response.arrayBuffer());
58
- const hostDir = path.join(outDir, new URL(asset.url).hostname);
59
- fs.mkdirSync(hostDir, { recursive: true });
60
- const file = path.join(hostDir, safeName(asset.url));
61
- fs.writeFileSync(file, buffer);
62
- return { ...asset, status: "ok", bytes: buffer.length, file };
63
- }
64
-
65
- try {
66
- const args = parseArgs(process.argv.slice(2));
67
- if (args.help || !args.recon) {
68
- usage();
69
- process.exit(args.help ? 0 : 1);
70
- }
71
-
72
- const recon = JSON.parse(fs.readFileSync(args.recon, "utf8"));
73
- const outDir = path.resolve(args.outDir);
74
- const manifestFile = path.resolve(args.manifest || path.join(outDir, "asset-manifest.json"));
75
- const originHost = recon.url ? new URL(recon.url).hostname : "";
76
- const assets = collectAssets(recon).filter((asset) => args.allExternal || new URL(asset.url).hostname === originHost);
77
- const results = [];
78
-
79
- for (const asset of assets) {
80
- try {
81
- results.push(await download(asset, outDir));
82
- } catch (error) {
83
- results.push({ ...asset, status: "error", error: error.message });
84
- }
85
- }
86
-
87
- fs.mkdirSync(path.dirname(manifestFile), { recursive: true });
88
- fs.writeFileSync(manifestFile, `${JSON.stringify({
89
- source: args.recon,
90
- url: recon.url,
91
- allExternal: args.allExternal,
92
- total: results.length,
93
- ok: results.filter((item) => item.status === "ok").length,
94
- error: results.filter((item) => item.status === "error").length,
95
- assets: results,
96
- }, null, 2)}\n`);
97
- console.log(manifestFile);
98
- } catch (error) {
99
- console.error(`asset-harvest failed: ${error.message}`);
100
- process.exit(1);
101
- }
@@ -1,151 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
-
5
- function usage() {
6
- console.log(`Usage:
7
- node scripts/audit-clone.mjs --project <clone-dir> [--brand "KOKUYO,Original Brand"] [--out CLONE_AUDIT.md]
8
-
9
- Scans clone source files for tracking scripts, original-brand residue, Japanese residue, TODOs, and risky external dependencies.
10
- `);
11
- }
12
-
13
- function parseArgs(argv) {
14
- const out = { project: process.cwd(), brand: [], out: "CLONE_AUDIT.md" };
15
- for (let i = 0; i < argv.length; i += 1) {
16
- const arg = argv[i];
17
- if (arg === "--help" || arg === "-h") out.help = true;
18
- else if (arg === "--project") out.project = argv[++i] || process.cwd();
19
- else if (arg === "--brand") out.brand = (argv[++i] || "").split(",").map((s) => s.trim()).filter(Boolean);
20
- else if (arg === "--out") out.out = argv[++i] || "CLONE_AUDIT.md";
21
- else throw new Error(`Unexpected argument: ${arg}`);
22
- }
23
- return out;
24
- }
25
-
26
- const includeExt = new Set([".html", ".css", ".js", ".jsx", ".ts", ".tsx", ".json", ".md", ".txt", ".svg"]);
27
- const skipDirs = new Set([".git", "node_modules", "dist", "build", ".next", ".nuxt", "coverage", "RECON"]);
28
- const skipFiles = new Set(["NOTES.md", "TEARDOWN.md", "CLONE_REPORT.md", "CLONE_AUDIT.md", "REPLACE_GUIDE.md"]);
29
-
30
- function walk(dir, files = []) {
31
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
32
- if (skipDirs.has(entry.name)) continue;
33
- const full = path.join(dir, entry.name);
34
- if (entry.isDirectory()) {
35
- if (entry.name === "screenshots") continue;
36
- walk(full, files);
37
- } else if (!skipFiles.has(entry.name) && includeExt.has(path.extname(entry.name).toLowerCase())) {
38
- files.push(full);
39
- }
40
- }
41
- return files;
42
- }
43
-
44
- function lineNumber(text, index) {
45
- return text.slice(0, index).split("\n").length;
46
- }
47
-
48
- function collectMatches(file, text, checks) {
49
- const findings = [];
50
- for (const check of checks) {
51
- const regex = new RegExp(check.pattern, check.flags || "gi");
52
- for (const match of text.matchAll(regex)) {
53
- const matchedText = String(match[0]);
54
- if (check.type === "external" && /^https?:\/\/(www\.)?w3\.org\//i.test(matchedText)) continue;
55
- findings.push({
56
- type: check.type,
57
- label: check.label,
58
- file,
59
- line: lineNumber(text, match.index || 0),
60
- match: matchedText.slice(0, 160),
61
- });
62
- }
63
- }
64
- return findings;
65
- }
66
-
67
- function markdown(findings, project, scannedFiles) {
68
- const byType = new Map();
69
- for (const finding of findings) {
70
- if (!byType.has(finding.type)) byType.set(finding.type, []);
71
- byType.get(finding.type).push(finding);
72
- }
73
- const types = [
74
- ["tracking", "追踪脚本 / 统计像素"],
75
- ["brand", "原站品牌残留"],
76
- ["japanese", "日文残留"],
77
- ["todo", "TODO / 占位内容"],
78
- ["external", "外部依赖 / 外链风险"],
79
- ];
80
- const lines = [
81
- `# Clone Audit`,
82
- "",
83
- `- Project: ${project}`,
84
- `- Scanned files: ${scannedFiles}`,
85
- `- Findings: ${findings.length}`,
86
- "",
87
- ];
88
-
89
- for (const [type, title] of types) {
90
- const items = byType.get(type) || [];
91
- lines.push(`## ${title}`);
92
- if (!items.length) {
93
- lines.push("- 未发现");
94
- lines.push("");
95
- continue;
96
- }
97
- for (const item of items.slice(0, 200)) {
98
- lines.push(`- ${path.relative(project, item.file)}:${item.line} · ${item.label} · \`${item.match.replaceAll("`", "'")}\``);
99
- }
100
- if (items.length > 200) lines.push(`- 还有 ${items.length - 200} 条未展开`);
101
- lines.push("");
102
- }
103
-
104
- lines.push("## 结论");
105
- lines.push(findings.length ? "- 需要处理上面的残留项后再声明可部署。" : "- 未发现明显残留项;仍需人工核查素材授权和视觉截图。");
106
- return `${lines.join("\n")}\n`;
107
- }
108
-
109
- try {
110
- const args = parseArgs(process.argv.slice(2));
111
- if (args.help) {
112
- usage();
113
- process.exit(0);
114
- }
115
-
116
- const project = path.resolve(args.project);
117
- if (!fs.existsSync(project)) throw new Error(`Project not found: ${project}`);
118
-
119
- const brandPatterns = args.brand.map((brand) => ({
120
- type: "brand",
121
- label: `brand residue: ${brand}`,
122
- pattern: brand.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
123
- flags: "gi",
124
- }));
125
-
126
- const checks = [
127
- { type: "tracking", label: "Google Tag Manager", pattern: "googletagmanager|GTM-[A-Z0-9]+", flags: "gi" },
128
- { type: "tracking", label: "Google Analytics / gtag", pattern: "google-analytics|gtag\\s*\\(|ga\\s*\\(", flags: "gi" },
129
- { type: "tracking", label: "Meta Pixel / fbq", pattern: "connect\\.facebook\\.net|fbq\\s*\\(", flags: "gi" },
130
- { type: "tracking", label: "Hotjar / Clarity", pattern: "hotjar|clarity\\.ms|hj\\s*\\(", flags: "gi" },
131
- { type: "japanese", label: "Japanese kana residue", pattern: "[\\u3040-\\u30ff]{2,}", flags: "g" },
132
- { type: "todo", label: "TODO / placeholder content", pattern: "TODO|FIXME|lorem ipsum|待补|这里填写", flags: "gi" },
133
- { type: "external", label: "external URL", pattern: "https?://[^\\s\"')<>]+", flags: "gi" },
134
- ...brandPatterns,
135
- ];
136
-
137
- const files = walk(project);
138
- const findings = [];
139
- for (const file of files) {
140
- const text = fs.readFileSync(file, "utf8");
141
- findings.push(...collectMatches(file, text, checks));
142
- }
143
-
144
- const output = path.resolve(args.out);
145
- fs.mkdirSync(path.dirname(output), { recursive: true });
146
- fs.writeFileSync(output, markdown(findings, project, files.length));
147
- console.log(output);
148
- } catch (error) {
149
- console.error(`audit-clone failed: ${error.message}`);
150
- process.exit(1);
151
- }
@@ -1,265 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
-
5
- function usage() {
6
- console.log(`Usage:
7
- node scripts/compare-recon.mjs --original <original-recon.json> --clone <clone-recon.json> [--visual-diff visual-diff.json] [--original-routes route-map.json] [--clone-routes route-map.json] [--original-interactions interactions.json] [--clone-interactions interactions.json] [--out CLONE_REPORT.md]
8
- `);
9
- }
10
-
11
- function parseArgs(argv) {
12
- const out = {
13
- original: "",
14
- clone: "",
15
- visualDiff: "",
16
- originalRoutes: "",
17
- cloneRoutes: "",
18
- originalInteractions: "",
19
- cloneInteractions: "",
20
- out: "CLONE_REPORT.md",
21
- };
22
- for (let i = 0; i < argv.length; i += 1) {
23
- const arg = argv[i];
24
- if (arg === "--help" || arg === "-h") out.help = true;
25
- else if (arg === "--original") out.original = argv[++i] || "";
26
- else if (arg === "--clone") out.clone = argv[++i] || "";
27
- else if (arg === "--visual-diff") out.visualDiff = argv[++i] || "";
28
- else if (arg === "--original-routes") out.originalRoutes = argv[++i] || "";
29
- else if (arg === "--clone-routes") out.cloneRoutes = argv[++i] || "";
30
- else if (arg === "--original-interactions") out.originalInteractions = argv[++i] || "";
31
- else if (arg === "--clone-interactions") out.cloneInteractions = argv[++i] || "";
32
- else if (arg === "--out") out.out = argv[++i] || "CLONE_REPORT.md";
33
- else throw new Error(`Unexpected argument: ${arg}`);
34
- }
35
- return out;
36
- }
37
-
38
- function readJson(file) {
39
- return JSON.parse(fs.readFileSync(file, "utf8"));
40
- }
41
-
42
- function firstSignals(recon) {
43
- return recon.captures?.[0]?.signals || {};
44
- }
45
-
46
- function boolList(flags = {}) {
47
- return Object.entries(flags).filter(([, value]) => value).map(([key]) => key);
48
- }
49
-
50
- function ratioScore(a, b) {
51
- if (a === 0 && b === 0) return 5;
52
- if (a === 0 || b === 0) return 1;
53
- const ratio = Math.min(a, b) / Math.max(a, b);
54
- if (ratio > 0.9) return 5;
55
- if (ratio > 0.75) return 4;
56
- if (ratio > 0.55) return 3;
57
- if (ratio > 0.3) return 2;
58
- return 1;
59
- }
60
-
61
- function sequenceSimilarity(a, b) {
62
- const left = a.map((item) => `${item.tag}:${item.text}`).filter(Boolean);
63
- const right = b.map((item) => `${item.tag}:${item.text}`).filter(Boolean);
64
- if (!left.length && !right.length) return 1;
65
- if (!left.length || !right.length) return 0;
66
- const rightSet = new Set(right);
67
- const hits = left.filter((item) => rightSet.has(item)).length;
68
- return hits / Math.max(left.length, right.length);
69
- }
70
-
71
- function inferComplexity(signals) {
72
- const frameworks = boolList(signals.frameworks);
73
- const counts = signals.counts || {};
74
- if ((counts.forms || 0) > 2 && (counts.inputs || 0) > 10) return "L6";
75
- if ((counts.canvas || 0) > 0 || signals.frameworks?.three) return "L5";
76
- if (signals.frameworks?.gsap || signals.frameworks?.lenis || (counts.video || 0) > 2) return "L4";
77
- if (frameworks.some((name) => ["react", "next", "vue", "nuxt", "svelte", "astro"].includes(name))) return "L3";
78
- if ((counts.links || 0) > 80 || (counts.images || 0) > 40) return "L2";
79
- return "L1";
80
- }
81
-
82
- function score(original, clone, visualDiff) {
83
- const o = firstSignals(original);
84
- const c = firstSignals(clone);
85
- const structureSimilarity = sequenceSimilarity(o.headings || [], c.headings || []);
86
- const structure = Math.max(1, Math.round(structureSimilarity * 5));
87
- const responsive = original.captures?.length === clone.captures?.length ? 4 : 2;
88
- const functionCounts = ["links", "forms", "buttons", "inputs"].map((key) => ratioScore(o.counts?.[key] || 0, c.counts?.[key] || 0));
89
- const functional = Math.round(functionCounts.reduce((sum, value) => sum + value, 0) / functionCounts.length);
90
- const motionCounts = ["canvas", "video"].map((key) => ratioScore(o.counts?.[key] || 0, c.counts?.[key] || 0));
91
- const interaction = Math.round(motionCounts.reduce((sum, value) => sum + value, 0) / motionCounts.length);
92
- return {
93
- sourceEvidence: 3,
94
- structure,
95
- visual: visualDiff ? `${visualDiff.visualScore}/5` : "需人工看截图或传 --visual-diff",
96
- interaction,
97
- responsive,
98
- functional,
99
- contentReplacement: "需人工看文案残留",
100
- legalRisk: "需人工核查 license / 素材",
101
- };
102
- }
103
-
104
- function line(value) {
105
- if (Array.isArray(value)) return value.join(", ") || "none";
106
- return value ?? "";
107
- }
108
-
109
- function routePath(url) {
110
- try {
111
- const parsed = new URL(url);
112
- return `${parsed.pathname}${parsed.search}` || "/";
113
- } catch {
114
- return url;
115
- }
116
- }
117
-
118
- function routesSection(files, evidence) {
119
- if (!evidence.originalRoutes || !evidence.cloneRoutes) {
120
- return `## 路由覆盖
121
- - 未提供 route-crawl 结果。多页面站需要传 --original-routes / --clone-routes。
122
- `;
123
- }
124
- const originalSet = new Set((evidence.originalRoutes.routes || []).map((route) => routePath(route.url)));
125
- const cloneSet = new Set((evidence.cloneRoutes.routes || []).map((route) => routePath(route.url)));
126
- const matched = Array.from(originalSet).filter((item) => cloneSet.has(item));
127
- const missing = Array.from(originalSet).filter((item) => !cloneSet.has(item));
128
- const extra = Array.from(cloneSet).filter((item) => !originalSet.has(item));
129
- const coverage = originalSet.size ? Math.round((matched.length / originalSet.size) * 100) : 100;
130
- return `## 路由覆盖
131
- - 原站路由: ${originalSet.size}
132
- - 克隆路由: ${cloneSet.size}
133
- - 覆盖率: ${coverage}%
134
- - 原站 route map: ${files.originalRoutes}
135
- - 克隆 route map: ${files.cloneRoutes}
136
- - 缺失路由: ${missing.join(", ") || "无"}
137
- - 额外路由: ${extra.join(", ") || "无"}
138
- `;
139
- }
140
-
141
- function changedActionCount(interactions) {
142
- return (interactions?.actions || []).filter((action) => action.changed).length;
143
- }
144
-
145
- function interactionSection(files, evidence) {
146
- if (!evidence.originalInteractions || !evidence.cloneInteractions) {
147
- return `## 交互覆盖
148
- - 未提供 interaction-probe 结果。交互站需要传 --original-interactions / --clone-interactions。
149
- `;
150
- }
151
- const originalActions = evidence.originalInteractions.actions || [];
152
- const cloneActions = evidence.cloneInteractions.actions || [];
153
- const originalChanged = changedActionCount(evidence.originalInteractions);
154
- const cloneChanged = changedActionCount(evidence.cloneInteractions);
155
- const originalCanvas = evidence.originalInteractions.discovered?.canvases?.length || 0;
156
- const cloneCanvas = evidence.cloneInteractions.discovered?.canvases?.length || 0;
157
- const originalInteractive = evidence.originalInteractions.discovered?.interactive?.length || 0;
158
- const cloneInteractive = evidence.cloneInteractions.discovered?.interactive?.length || 0;
159
- return `## 交互覆盖
160
- - 原站可见交互目标: ${originalInteractive}
161
- - 克隆可见交互目标: ${cloneInteractive}
162
- - 原站 canvas 目标: ${originalCanvas}
163
- - 克隆 canvas 目标: ${cloneCanvas}
164
- - 原站 changed actions: ${originalChanged}/${originalActions.length}
165
- - 克隆 changed actions: ${cloneChanged}/${cloneActions.length}
166
- - 原站 interaction probe: ${files.originalInteractions}
167
- - 克隆 interaction probe: ${files.cloneInteractions}
168
- - 判断: ${originalChanged === cloneChanged && originalCanvas === cloneCanvas ? "交互数量信号接近,仍需看截图确认状态质量。" : "交互数量信号不一致,需要检查缺失状态或过度实现。"}
169
- `;
170
- }
171
-
172
- function report(files, original, clone, evidence) {
173
- const o = firstSignals(original);
174
- const c = firstSignals(clone);
175
- const scores = score(original, clone, evidence.visualDiff);
176
- const complexity = inferComplexity(o);
177
- const originalFlags = boolList(o.frameworks);
178
- const cloneFlags = boolList(c.frameworks);
179
- const counts = ["sections", "links", "images", "video", "canvas", "forms", "buttons", "inputs", "interactive", "scripts"];
180
-
181
- return `# ${original.label || "original"} vs ${clone.label || "clone"} · 克隆评估报告
182
-
183
- ## 结论
184
- - 原站 URL: ${original.url}
185
- - 克隆 URL: ${clone.url}
186
- - 自动推断复杂度: ${complexity}
187
- - 复刻模式建议: ${complexity === "L5" ? "技术拆解 / 忠实复刻优先" : complexity === "L6" ? "展示层视觉复刻" : "视觉复刻 / 内容爆改"}
188
- - 自动报告边界: 结构、数量、框架、console 可自动比;传入 visual-diff 后可纳入像素差异分。内容残留和法务仍需审计。
189
-
190
- ## 技术信号
191
- | 项目 | 原站 | 克隆站 |
192
- |---|---|---|
193
- | title | ${o.title || ""} | ${c.title || ""} |
194
- | lang | ${o.lang || ""} | ${c.lang || ""} |
195
- | frameworks | ${line(originalFlags)} | ${line(cloneFlags)} |
196
- | scrollHeight | ${o.scrollHeight || 0} | ${c.scrollHeight || 0} |
197
- | h1 | ${line(o.h1)} | ${line(c.h1)} |
198
-
199
- ## 数量对比
200
- | 指标 | 原站 | 克隆站 | 自动评分 |
201
- |---|---:|---:|---:|
202
- ${counts.map((key) => `| ${key} | ${o.counts?.[key] || 0} | ${c.counts?.[key] || 0} | ${ratioScore(o.counts?.[key] || 0, c.counts?.[key] || 0)}/5 |`).join("\n")}
203
-
204
- ## 复刻评分
205
- - 源证据: ${scores.sourceEvidence}/5
206
- - 结构保真: ${scores.structure}/5
207
- - 视觉保真: ${scores.visual}
208
- - 动效/交互: ${scores.interaction}/5
209
- - 响应式: ${scores.responsive}/5
210
- - 功能完整: ${scores.functional}/5
211
- - 内容替换: ${scores.contentReplacement}
212
- - 法务/部署风险: ${scores.legalRisk}
213
-
214
- ## Console
215
- - 原站 console errors: ${original.console?.errors?.length || 0}
216
- - 克隆 console errors: ${clone.console?.errors?.length || 0}
217
- - 原站 page errors: ${original.console?.pageErrors?.length || 0}
218
- - 克隆 page errors: ${clone.console?.pageErrors?.length || 0}
219
-
220
- ${routesSection(files, evidence)}
221
-
222
- ${interactionSection(files, evidence)}
223
-
224
- ## 截图证据
225
- - 原站侦察: ${files.original}
226
- - 克隆侦察: ${files.clone}
227
- - 像素差异: ${files.visualDiff || "未提供"}
228
- - 像素差异率: ${evidence.visualDiff ? evidence.visualDiff.diffPixelRatio : "未提供"}
229
- - 原站截图: ${(original.captures || []).map((capture) => capture.screenshot).join(", ")}
230
- - 克隆截图: ${(clone.captures || []).map((capture) => capture.screenshot).join(", ")}
231
-
232
- ## 已知缺口
233
- - 未传入 visual-diff 时,视觉保真需要打开截图人工确认。
234
- - 法务、素材授权、品牌替换完整度需要人工核查。
235
- `;
236
- }
237
-
238
- try {
239
- const args = parseArgs(process.argv.slice(2));
240
- if (args.help || !args.original || !args.clone) {
241
- usage();
242
- process.exit(args.help ? 0 : 1);
243
- }
244
-
245
- const original = readJson(args.original);
246
- const clone = readJson(args.clone);
247
- const visualDiff = args.visualDiff ? readJson(args.visualDiff) : null;
248
- const originalRoutes = args.originalRoutes ? readJson(args.originalRoutes) : null;
249
- const cloneRoutes = args.cloneRoutes ? readJson(args.cloneRoutes) : null;
250
- const originalInteractions = args.originalInteractions ? readJson(args.originalInteractions) : null;
251
- const cloneInteractions = args.cloneInteractions ? readJson(args.cloneInteractions) : null;
252
- const output = path.resolve(args.out);
253
- fs.mkdirSync(path.dirname(output), { recursive: true });
254
- fs.writeFileSync(output, report(args, original, clone, {
255
- visualDiff,
256
- originalRoutes,
257
- cloneRoutes,
258
- originalInteractions,
259
- cloneInteractions,
260
- }));
261
- console.log(output);
262
- } catch (error) {
263
- console.error(`compare-recon failed: ${error.message}`);
264
- process.exit(1);
265
- }