howone 0.1.50 → 0.1.52

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 (52) hide show
  1. package/package.json +1 -1
  2. package/templates/vite/.howone/skills/howone/03-ai-capabilities/01-ai-capability-architecture.md +16 -2
  3. package/templates/vite/.howone/skills/howone/03-ai-capabilities/02-workflow-contract-rules.md +107 -0
  4. package/templates/vite/.howone/skills/howone/03-ai-capabilities/03-service-capability-catalog.md +98 -22
  5. package/templates/vite/.howone/skills/howone/03-ai-capabilities/04-workflow-operations.md +8 -0
  6. package/templates/vite/.howone/skills/howone/03-ai-capabilities/05-ai-feature-playbooks.md +66 -1
  7. package/templates/vite/.howone/skills/web-clone/LICENSE +0 -21
  8. package/templates/vite/.howone/skills/web-clone/README.md +0 -179
  9. package/templates/vite/.howone/skills/web-clone/SKILL.md +0 -243
  10. package/templates/vite/.howone/skills/web-clone/references/assessment.md +0 -77
  11. package/templates/vite/.howone/skills/web-clone/references/complex-playbooks.md +0 -46
  12. package/templates/vite/.howone/skills/web-clone/references/deliverables.md +0 -144
  13. package/templates/vite/.howone/skills/web-clone/references/design-dna.md +0 -125
  14. package/templates/vite/.howone/skills/web-clone/references/effect-extraction.md +0 -73
  15. package/templates/vite/.howone/skills/web-clone/references/marbles-case.md +0 -31
  16. package/templates/vite/.howone/skills/web-clone/references/reverse-engineering.md +0 -34
  17. package/templates/vite/.howone/skills/web-clone/references/static-mirror.md +0 -72
  18. package/templates/vite/.howone/skills/web-clone/scripts/asset-harvest.mjs +0 -101
  19. package/templates/vite/.howone/skills/web-clone/scripts/audit-clone.mjs +0 -151
  20. package/templates/vite/.howone/skills/web-clone/scripts/compare-recon.mjs +0 -265
  21. package/templates/vite/.howone/skills/web-clone/scripts/dna-scaffold.mjs +0 -214
  22. package/templates/vite/.howone/skills/web-clone/scripts/init-clone.mjs +0 -136
  23. package/templates/vite/.howone/skills/web-clone/scripts/interaction-probe.mjs +0 -314
  24. package/templates/vite/.howone/skills/web-clone/scripts/lib/playwright-loader.mjs +0 -39
  25. package/templates/vite/.howone/skills/web-clone/scripts/mirror-site.mjs +0 -121
  26. package/templates/vite/.howone/skills/web-clone/scripts/network-capture.mjs +0 -127
  27. package/templates/vite/.howone/skills/web-clone/scripts/recon-site.mjs +0 -235
  28. package/templates/vite/.howone/skills/web-clone/scripts/route-crawl.mjs +0 -228
  29. package/templates/vite/.howone/skills/web-clone/scripts/sourcemap-hunt.mjs +0 -112
  30. package/templates/vite/.howone/skills/web-clone/scripts/visual-diff.mjs +0 -161
  31. package/templates/vite/.howone/skills/web-shader-extractor/SKILL.md +0 -123
  32. package/templates/vite/.howone/skills/web-shader-extractor/references/capture-backends.md +0 -201
  33. package/templates/vite/.howone/skills/web-shader-extractor/references/evidence-policy.md +0 -93
  34. package/templates/vite/.howone/skills/web-shader-extractor/references/operating-contract.md +0 -82
  35. package/templates/vite/.howone/skills/web-shader-extractor/references/qa-failure-policy.md +0 -100
  36. package/templates/vite/.howone/skills/web-shader-extractor/references/recon-kernel.md +0 -206
  37. package/templates/vite/.howone/skills/web-shader-extractor/references/replay-policy.md +0 -145
  38. package/templates/vite/.howone/skills/web-shader-extractor/references/shaders-com.md +0 -212
  39. package/templates/vite/.howone/skills/web-shader-extractor/references/source-analysis.md +0 -112
  40. package/templates/vite/.howone/skills/web-shader-extractor/references/surface-discovery.md +0 -113
  41. package/templates/vite/.howone/skills/web-shader-extractor/references/target-lock.md +0 -205
  42. package/templates/vite/.howone/skills/web-shader-extractor/references/three-shader-reconstruction.md +0 -155
  43. package/templates/vite/.howone/skills/web-shader-extractor/references/tool-capability-matrix.md +0 -57
  44. package/templates/vite/.howone/skills/web-shader-extractor/references/unicorn-studio.md +0 -387
  45. package/templates/vite/.howone/skills/web-shader-extractor/scripts/fetch-rendered-dom.mjs +0 -178
  46. package/templates/vite/.howone/skills/web-shader-extractor/scripts/scan-bundle.sh +0 -80
  47. package/templates/vite/.howone/skills/web-shader-extractor/templates/extraction-report.md +0 -41
  48. package/templates/vite/.howone/skills/web-shader-extractor/templates/known-gaps.md +0 -14
  49. package/templates/vite/.howone/skills/web-shader-extractor/templates/qa-report.md +0 -74
  50. package/templates/vite/.howone/skills/web-shader-extractor/templates/replay-manifest.json +0 -147
  51. package/templates/vite/.howone/skills/web-shader-extractor/templates/run-state.json +0 -75
  52. package/templates/vite/.howone/skills/web-shader-extractor/templates/scout-card.json +0 -106
@@ -1,136 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
-
6
- function usage() {
7
- console.log(`Usage:
8
- node scripts/init-clone.mjs <slug> [--url <url>] [--mode <mode>] [--level <L1-L6>]
9
-
10
- Creates:
11
- ~/projects/website-clones/<slug>-clone/
12
- ~/projects/website-clones/<slug>-clone/NOTES.md
13
- ~/projects/website-clones/<slug>-clone/RECON/screenshots/
14
- `);
15
- }
16
-
17
- function parseArgs(argv) {
18
- const out = { slug: null, url: "", mode: "", level: "" };
19
- for (let i = 0; i < argv.length; i += 1) {
20
- const arg = argv[i];
21
- if (arg === "--help" || arg === "-h") out.help = true;
22
- else if (arg === "--url") out.url = argv[++i] || "";
23
- else if (arg === "--mode") out.mode = argv[++i] || "";
24
- else if (arg === "--level") out.level = argv[++i] || "";
25
- else if (!out.slug) out.slug = arg;
26
- else throw new Error(`Unexpected argument: ${arg}`);
27
- }
28
- return out;
29
- }
30
-
31
- function cleanSlug(input) {
32
- return input
33
- .trim()
34
- .toLowerCase()
35
- .replace(/https?:\/\//g, "")
36
- .replace(/[^a-z0-9]+/g, "-")
37
- .replace(/^-+|-+$/g, "");
38
- }
39
-
40
- function notesTemplate({ name, url, mode, level }) {
41
- return `# ${name} · 克隆笔记
42
-
43
- ## 源信息
44
- - 原站 URL: ${url}
45
- - 源码仓库:
46
- - 原作者:
47
- - 许可证:
48
- - 致谢要求:
49
-
50
- ## 技术栈
51
- - 框架 / 关键库 / Node 版本:
52
-
53
- ## 复刻前预判
54
- - 复杂度等级: ${level}
55
- - 推荐模式: ${mode}
56
- - 可高保真的部分:
57
- - 需要近似或替代的部分:
58
- - 不克隆的部分:
59
- - 主要风险:
60
-
61
- ## 跑起来
62
- \`\`\`bash
63
- cd ~/projects/website-clones/${name}
64
- python3 -m http.server 8123
65
- \`\`\`
66
-
67
- ## 改了什么(对照原版)
68
- -
69
-
70
- ## 原站 vs 克隆站
71
- | 模块 | 原站表现 | 克隆实现 | 差异 / 取舍 | 证据 |
72
- |---|---|---|---|---|
73
- | 首屏 | | | | |
74
- | 导航 | | | | |
75
- | 核心动效 | | | | |
76
- | 内容区块 | | | | |
77
- | 移动端 | | | | |
78
-
79
- ## 复刻评分
80
- - 源证据: /5
81
- - 结构保真: /5
82
- - 视觉保真: /5
83
- - 动效/交互: /5
84
- - 响应式: /5
85
- - 功能完整: /5
86
- - 内容替换: /5
87
- - 法务/部署风险: /5
88
- - 总评:
89
-
90
- ## 替换地图(要换什么改哪)
91
- - 文字 -> 文件 行
92
- - 图片/媒体 -> 目录
93
- - 配色 -> CSS 变量 / theme
94
- - 3D 模型 / 字体 ->
95
-
96
- ## 验证
97
- - [ ] 本地跑通、console 0 error
98
- - [ ] 截图对照原站(RECON/screenshots/)
99
- - 验证不了的点(如实记,别伪造):
100
- `;
101
- }
102
-
103
- try {
104
- const args = parseArgs(process.argv.slice(2));
105
- if (args.help || !args.slug) {
106
- usage();
107
- process.exit(args.help ? 0 : 1);
108
- }
109
-
110
- const slug = cleanSlug(args.slug);
111
- if (!slug) throw new Error("Slug is empty after normalization.");
112
- const name = slug.endsWith("-clone") ? slug : `${slug}-clone`;
113
- const root = path.join(os.homedir(), "projects", "website-clones");
114
- const project = path.join(root, name);
115
-
116
- if (fs.existsSync(project)) {
117
- throw new Error(`Project already exists: ${project}`);
118
- }
119
-
120
- fs.mkdirSync(path.join(project, "RECON", "screenshots"), { recursive: true });
121
- fs.writeFileSync(
122
- path.join(project, "NOTES.md"),
123
- notesTemplate({
124
- name,
125
- url: args.url,
126
- mode: args.mode,
127
- level: args.level,
128
- })
129
- );
130
- fs.writeFileSync(path.join(project, ".gitignore"), "node_modules/\n.DS_Store\n");
131
-
132
- console.log(project);
133
- } catch (error) {
134
- console.error(`init-clone failed: ${error.message}`);
135
- process.exit(1);
136
- }
@@ -1,314 +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
- import { loadPlaywright, launchChromium } from "./lib/playwright-loader.mjs";
6
-
7
- function usage() {
8
- console.log(`Usage:
9
- node scripts/interaction-probe.mjs --url <url> --out RECON/interactions [--label original] [--max-clicks 12] [--max-hovers 8] [--wait 800] [--width 1440]
10
-
11
- Exercises scroll, hover, safe clicks, and canvas drag gestures, then saves screenshots plus state/network evidence.
12
- `);
13
- }
14
-
15
- function parseArgs(argv) {
16
- const out = {
17
- url: "",
18
- outDir: "RECON/interactions",
19
- label: "site",
20
- maxClicks: 12,
21
- maxHovers: 8,
22
- waitMs: 800,
23
- width: 1440,
24
- };
25
- for (let i = 0; i < argv.length; i += 1) {
26
- const arg = argv[i];
27
- if (arg === "--help" || arg === "-h") out.help = true;
28
- else if (arg === "--url") out.url = argv[++i] || "";
29
- else if (arg === "--out") out.outDir = argv[++i] || "RECON/interactions";
30
- else if (arg === "--label") out.label = argv[++i] || "site";
31
- else if (arg === "--max-clicks") out.maxClicks = Number(argv[++i] || "12");
32
- else if (arg === "--max-hovers") out.maxHovers = Number(argv[++i] || "8");
33
- else if (arg === "--wait") out.waitMs = Number(argv[++i] || "800");
34
- else if (arg === "--width") out.width = Number(argv[++i] || "1440");
35
- else throw new Error(`Unexpected argument: ${arg}`);
36
- }
37
- return out;
38
- }
39
-
40
- function shortHash(value) {
41
- return crypto.createHash("sha1").update(value).digest("hex").slice(0, 10);
42
- }
43
-
44
- function safeFileName(label) {
45
- return label.replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 90) || "step";
46
- }
47
-
48
- function markdown(result) {
49
- const lines = [
50
- `# ${result.label} interaction probe`,
51
- "",
52
- `- URL: ${result.url}`,
53
- `- Actions: ${result.actions.length}`,
54
- `- Console errors: ${result.consoleErrors.length}`,
55
- `- Network events: ${result.network.length}`,
56
- "",
57
- "## Actions",
58
- "| # | Type | Target | Changed | URL after | Screenshot |",
59
- "|---:|---|---|---:|---|---|",
60
- ];
61
- result.actions.forEach((action, index) => {
62
- lines.push(`| ${index + 1} | ${action.type} | ${String(action.target || "").replaceAll("|", "\\|")} | ${action.changed ? "yes" : "no"} | ${action.after.url.replaceAll("|", "\\|")} | ${action.screenshot || ""} |`);
63
- });
64
- if (result.findings.length) {
65
- lines.push("");
66
- lines.push("## Findings");
67
- for (const finding of result.findings) lines.push(`- ${finding}`);
68
- }
69
- return `${lines.join("\n")}\n`;
70
- }
71
-
72
- async function snapshot(page) {
73
- return page.evaluate(() => {
74
- const text = (node) => (node?.textContent || "").trim().replace(/\s+/g, " ");
75
- const visible = (selector) => Array.from(document.querySelectorAll(selector)).filter((node) => {
76
- const rect = node.getBoundingClientRect();
77
- const style = getComputedStyle(node);
78
- return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
79
- }).length;
80
- const bodyText = document.body?.innerText || "";
81
- const html = document.documentElement.outerHTML;
82
- return {
83
- url: location.href,
84
- title: document.title || "",
85
- activeElement: document.activeElement?.tagName?.toLowerCase() || "",
86
- scrollY: Math.round(window.scrollY),
87
- bodyTextChars: bodyText.length,
88
- bodyTextStart: bodyText.trim().replace(/\s+/g, " ").slice(0, 500),
89
- counts: {
90
- dialogs: visible("dialog,[role='dialog'],[aria-modal='true']"),
91
- popovers: visible("[popover],.popover,.modal,.drawer,.menu,[role='menu']"),
92
- canvases: document.querySelectorAll("canvas").length,
93
- videos: document.querySelectorAll("video").length,
94
- buttons: document.querySelectorAll("button,[role='button']").length,
95
- forms: document.forms.length,
96
- },
97
- htmlHash: "__HASH_PLACEHOLDER__",
98
- htmlLength: html.length,
99
- };
100
- });
101
- }
102
-
103
- async function hashSnapshot(page, state) {
104
- const htmlHash = await page.evaluate(() => {
105
- const html = document.documentElement.outerHTML;
106
- let hash = 0;
107
- for (let i = 0; i < html.length; i += 1) {
108
- hash = ((hash << 5) - hash + html.charCodeAt(i)) | 0;
109
- }
110
- return String(hash);
111
- });
112
- return { ...state, htmlHash };
113
- }
114
-
115
- async function candidates(page) {
116
- return page.evaluate(() => {
117
- function selectorFor(node) {
118
- if (node.id && !/\s/.test(node.id)) return `#${CSS.escape(node.id)}`;
119
- const parts = [];
120
- let current = node;
121
- while (current && current.nodeType === Node.ELEMENT_NODE && parts.length < 6) {
122
- const tag = current.tagName.toLowerCase();
123
- const parent = current.parentElement;
124
- if (!parent) {
125
- parts.unshift(tag);
126
- break;
127
- }
128
- const sameTag = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
129
- const index = sameTag.indexOf(current) + 1;
130
- parts.unshift(`${tag}:nth-of-type(${index})`);
131
- current = parent;
132
- }
133
- return parts.join(" > ");
134
- }
135
- const text = (node) => (node.innerText || node.textContent || node.getAttribute("aria-label") || "").trim().replace(/\s+/g, " ");
136
- const visible = (node) => {
137
- const rect = node.getBoundingClientRect();
138
- const style = getComputedStyle(node);
139
- return rect.width >= 8 && rect.height >= 8 && style.visibility !== "hidden" && style.display !== "none";
140
- };
141
- const interactive = Array.from(document.querySelectorAll("button,a[href],summary,input,textarea,select,[role='button'],[tabindex]"))
142
- .filter(visible)
143
- .slice(0, 80)
144
- .map((node) => {
145
- const rect = node.getBoundingClientRect();
146
- const tag = node.tagName.toLowerCase();
147
- const href = node.href || node.getAttribute("href") || "";
148
- const safeClick = tag === "button" || tag === "summary" || node.getAttribute("role") === "button" || href.startsWith("#") || href.startsWith(location.origin);
149
- return {
150
- selector: selectorFor(node),
151
- tag,
152
- type: node.getAttribute("type") || "",
153
- role: node.getAttribute("role") || "",
154
- href,
155
- text: text(node).slice(0, 120),
156
- ariaLabel: node.getAttribute("aria-label") || "",
157
- safeClick,
158
- rect: {
159
- x: Math.round(rect.x),
160
- y: Math.round(rect.y),
161
- width: Math.round(rect.width),
162
- height: Math.round(rect.height),
163
- },
164
- };
165
- });
166
- const canvases = Array.from(document.querySelectorAll("canvas")).filter(visible).slice(0, 4).map((node) => {
167
- const rect = node.getBoundingClientRect();
168
- return {
169
- selector: selectorFor(node),
170
- rect: {
171
- x: Math.round(rect.x),
172
- y: Math.round(rect.y),
173
- width: Math.round(rect.width),
174
- height: Math.round(rect.height),
175
- },
176
- };
177
- });
178
- return { interactive, canvases };
179
- });
180
- }
181
-
182
- function hasChanged(before, after) {
183
- if (before.url !== after.url) return true;
184
- if (before.htmlHash !== after.htmlHash) return true;
185
- if (before.scrollY !== after.scrollY) return true;
186
- return JSON.stringify(before.counts) !== JSON.stringify(after.counts);
187
- }
188
-
189
- async function freshPage(browser, args, network, consoleErrors) {
190
- const page = await browser.newPage({ viewport: { width: args.width, height: 900 }, deviceScaleFactor: 1 });
191
- page.on("console", (message) => {
192
- if (message.type() === "error") consoleErrors.push(message.text());
193
- });
194
- page.on("response", (response) => {
195
- const request = response.request();
196
- if (["xhr", "fetch"].includes(request.resourceType())) {
197
- network.push({ url: response.url(), status: response.status(), method: request.method(), resourceType: request.resourceType() });
198
- }
199
- });
200
- await page.goto(args.url, { waitUntil: "domcontentloaded", timeout: 45000 });
201
- await page.waitForLoadState("networkidle", { timeout: 6000 }).catch(() => {});
202
- if (args.waitMs > 0) await page.waitForTimeout(args.waitMs);
203
- return page;
204
- }
205
-
206
- async function captureAction(page, outDir, index, action, before) {
207
- if (action.type === "scroll") {
208
- await page.evaluate((ratio) => window.scrollTo({ top: Math.round((document.documentElement.scrollHeight - window.innerHeight) * ratio), behavior: "instant" }), action.ratio);
209
- } else if (action.type === "hover") {
210
- await page.locator(action.selector).first().hover({ timeout: 3000 });
211
- } else if (action.type === "click") {
212
- await page.locator(action.selector).first().click({ timeout: 4000 });
213
- } else if (action.type === "canvas-drag") {
214
- const rect = action.rect;
215
- const x = rect.x + rect.width / 2;
216
- const y = rect.y + rect.height / 2;
217
- await page.mouse.move(x, y);
218
- await page.mouse.down();
219
- await page.mouse.move(x + Math.min(180, rect.width * 0.25), y + Math.min(120, rect.height * 0.2), { steps: 8 });
220
- await page.mouse.up();
221
- }
222
- await page.waitForTimeout(450);
223
- const after = await hashSnapshot(page, await snapshot(page));
224
- const screenshotName = `${String(index + 1).padStart(2, "0")}-${safeFileName(`${action.type}-${action.target || action.selector || "page"}`)}-${shortHash(JSON.stringify(action))}.png`;
225
- const screenshotPath = path.join(outDir, "screenshots", screenshotName);
226
- await page.screenshot({ path: screenshotPath, fullPage: true });
227
- return {
228
- ...action,
229
- before,
230
- after,
231
- changed: hasChanged(before, after),
232
- screenshot: path.relative(outDir, screenshotPath),
233
- };
234
- }
235
-
236
- try {
237
- const args = parseArgs(process.argv.slice(2));
238
- if (args.help || !args.url) {
239
- usage();
240
- process.exit(args.help ? 0 : 1);
241
- }
242
-
243
- const outDir = path.resolve(args.outDir);
244
- fs.mkdirSync(path.join(outDir, "screenshots"), { recursive: true });
245
-
246
- const { chromium } = loadPlaywright();
247
- const browser = await launchChromium(chromium);
248
- const network = [];
249
- const consoleErrors = [];
250
-
251
- const seedPage = await freshPage(browser, args, network, consoleErrors);
252
- const initial = await hashSnapshot(seedPage, await snapshot(seedPage));
253
- const discovered = await candidates(seedPage);
254
- await seedPage.screenshot({ path: path.join(outDir, "screenshots", "00-initial.png"), fullPage: true });
255
- await seedPage.close();
256
-
257
- const actions = [
258
- { type: "scroll", target: "middle", ratio: 0.5 },
259
- { type: "scroll", target: "bottom", ratio: 1 },
260
- ...discovered.interactive.slice(0, args.maxHovers).map((item) => ({ type: "hover", target: item.text || item.ariaLabel || item.selector, selector: item.selector, meta: item })),
261
- ...discovered.interactive.filter((item) => item.safeClick).slice(0, args.maxClicks).map((item) => ({ type: "click", target: item.text || item.ariaLabel || item.selector, selector: item.selector, meta: item })),
262
- ...discovered.canvases.map((item) => ({ type: "canvas-drag", target: item.selector, selector: item.selector, rect: item.rect })),
263
- ];
264
-
265
- const results = [];
266
- for (let index = 0; index < actions.length; index += 1) {
267
- const page = await freshPage(browser, args, network, consoleErrors);
268
- const before = await hashSnapshot(page, await snapshot(page));
269
- try {
270
- results.push(await captureAction(page, outDir, index, actions[index], before));
271
- } catch (error) {
272
- results.push({
273
- ...actions[index],
274
- before,
275
- after: before,
276
- changed: false,
277
- screenshot: "",
278
- error: error.message,
279
- });
280
- } finally {
281
- await page.close();
282
- }
283
- }
284
-
285
- await browser.close();
286
-
287
- const changedCount = results.filter((action) => action.changed).length;
288
- const findings = [
289
- `${discovered.interactive.length} visible interactive candidates discovered`,
290
- `${discovered.canvases.length} visible canvas targets discovered`,
291
- `${changedCount}/${results.length} actions changed DOM, URL, scroll, or visible overlay counts`,
292
- ];
293
- if (discovered.canvases.length) findings.push("Canvas drag evidence exists; inspect screenshots before simplifying WebGL/Canvas behavior.");
294
-
295
- const result = {
296
- label: args.label,
297
- url: args.url,
298
- capturedAt: new Date().toISOString(),
299
- initial,
300
- discovered,
301
- actions: results,
302
- network,
303
- consoleErrors,
304
- findings,
305
- };
306
- const jsonFile = path.join(outDir, `${args.label}-interactions.json`);
307
- const mdFile = path.join(outDir, `${args.label}-interactions.md`);
308
- fs.writeFileSync(jsonFile, `${JSON.stringify(result, null, 2)}\n`);
309
- fs.writeFileSync(mdFile, markdown(result));
310
- console.log(jsonFile);
311
- } catch (error) {
312
- console.error(`interaction-probe failed: ${error.message}`);
313
- process.exit(1);
314
- }
@@ -1,39 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import { existsSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const require = createRequire(import.meta.url);
7
- const here = dirname(fileURLToPath(import.meta.url));
8
- const workspaceRoot = join(here, "..", "..", "..", "..");
9
-
10
- export function loadPlaywright() {
11
- const candidates = [
12
- "playwright",
13
- join(workspaceRoot, "node_modules/playwright"),
14
- join(workspaceRoot, "node_modules/@playwright/test/node_modules/playwright"),
15
- "/Users/erkelost/.codex/plugins/cache/openai-bundled/browser/node_modules/playwright",
16
- "/Users/erkelost/.codex/plugins/cache/openai-primary-runtime/node_modules/playwright",
17
- ];
18
- for (const candidate of candidates) {
19
- try {
20
- if (candidate.startsWith("/") && !existsSync(candidate)) continue;
21
- return require(candidate);
22
- } catch {
23
- // Try next candidate.
24
- }
25
- }
26
- throw new Error("Playwright not found. Run `npm install -D playwright` in the clone project, or install the Browser skill dependencies.");
27
- }
28
-
29
- export async function launchChromium(chromium) {
30
- try {
31
- return await chromium.launch({ headless: true });
32
- } catch (firstError) {
33
- try {
34
- return await chromium.launch({ headless: true, channel: "chrome" });
35
- } catch {
36
- throw firstError;
37
- }
38
- }
39
- }
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env node
2
- // mirror-site.mjs — 把"静态构建站"(Astro / Vite SSG / Hugo 等)的部署资产整套镜像下来,做 1:1 忠实复刻。
3
- // 原理: 这类站的"真源码"不在 GitHub,但部署出来的静态资产(HTML + bundle + CSS + 运行时 fetch 的
4
- // .sog/.buf/.wasm/.riv/字体/图)就是真相。用真浏览器全程滚动一遍捕获每一个真实请求,按路径镜像同源资产。
5
- // 用法:
6
- // node scripts/mirror-site.mjs --url <URL> --out <dir> [--scroll-step 700] [--settle 2500] [--max-ms 90000]
7
- // 产物:
8
- // <dir>/site/... 镜像的同源资产(保留路径;目录 URL 存为 index.html)
9
- // <dir>/mirror-manifest.json 全部请求(同源+第三方) + 每项状态
10
- // <dir>/own-asset-urls.txt 同源资产路径清单
11
- // <dir>/third-party.json 第三方 host + 需自托管的 webfont CSS(typekit/google) 提示
12
- // 纪律: 只搬"真实请求到的"资产,不臆造路径。第三方 CDN(字体/wasm/视频)不自动改写——按 third-party.json 人工处理。
13
- // 后续手工: 自托管锁域名字体(典型 Typekit @import) → 改写 CSS @import 为本地 → 删追踪 → 从 site/ 作 web 根服务。
14
- // 完整配方见 references/static-mirror.md。
15
-
16
- import { loadPlaywright, launchChromium } from "./lib/playwright-loader.mjs";
17
- import fs from "node:fs";
18
- import path from "node:path";
19
-
20
- function parseArgs(argv) {
21
- const o = { url: "", out: "", scrollStep: 700, settle: 2500, maxMs: 90000, help: false };
22
- for (let i = 0; i < argv.length; i++) {
23
- const a = argv[i];
24
- if (a === "--help" || a === "-h") o.help = true;
25
- else if (a === "--url") o.url = argv[++i] || "";
26
- else if (a === "--out") o.out = argv[++i] || "";
27
- else if (a === "--scroll-step") o.scrollStep = parseInt(argv[++i] || "700", 10);
28
- else if (a === "--settle") o.settle = parseInt(argv[++i] || "2500", 10);
29
- else if (a === "--max-ms") o.maxMs = parseInt(argv[++i] || "90000", 10);
30
- }
31
- return o;
32
- }
33
-
34
- function usage() {
35
- console.log(`mirror-site.mjs — 静态构建站全量资产镜像(1:1 忠实复刻)
36
-
37
- node scripts/mirror-site.mjs --url <URL> --out <dir> [--scroll-step 700] [--settle 2500] [--max-ms 90000]
38
-
39
- 适用: Astro / Vite SSG / Hugo / 任何把客户端运行时输出成可下载静态资产的站(含 WebGL/Canvas 重前端)。
40
- 不适用: 真·服务端渲染/数据驱动 SPA(需 network-capture.mjs 做 API 替身)。
41
- 配方与后续改写步骤(自托管字体/删追踪/服务) → references/static-mirror.md`);
42
- }
43
-
44
- // 同源资产 URL → 本地相对路径(去 query;目录结尾存 index.html)
45
- function urlToLocalPath(u, origin) {
46
- let p = u.slice(origin.length);
47
- const q = p.indexOf("?");
48
- if (q >= 0) p = p.slice(0, q);
49
- if (p === "" || p.endsWith("/")) p += "index.html";
50
- return p.replace(/^\/+/, "");
51
- }
52
-
53
- const args = parseArgs(process.argv.slice(2));
54
- if (args.help || !args.url || !args.out) {
55
- usage();
56
- process.exit(args.help ? 0 : 1);
57
- }
58
-
59
- const origin = new URL(args.url).origin;
60
- const siteDir = path.join(path.resolve(args.out), "site");
61
- fs.mkdirSync(siteDir, { recursive: true });
62
-
63
- const responses = new Map(); // url -> {status, type, ct}
64
- const pw = loadPlaywright();
65
- const browser = await launchChromium(pw.chromium);
66
- const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 });
67
- const page = await ctx.newPage();
68
- page.on("response", (resp) => {
69
- try {
70
- const h = resp.headers();
71
- responses.set(resp.url(), { status: resp.status(), type: resp.request().resourceType(), ct: h["content-type"] || "" });
72
- } catch {}
73
- });
74
-
75
- console.log(`▸ 加载 + 全程滚动捕获: ${args.url}`);
76
- await page.goto(args.url, { waitUntil: "networkidle", timeout: args.maxMs }).catch((e) => console.warn(" goto:", e.message));
77
- const total = await page.evaluate(() => document.documentElement.scrollHeight);
78
- for (let y = 0; y <= total; y += args.scrollStep) {
79
- await page.evaluate((yy) => window.scrollTo(0, yy), y);
80
- await page.waitForTimeout(180);
81
- }
82
- await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
83
- await page.waitForTimeout(args.settle);
84
- await page.evaluate(() => window.scrollTo(0, 0));
85
- await page.waitForTimeout(1200);
86
-
87
- const all = [...responses.entries()].map(([url, m]) => ({ url, ...m }));
88
- const ownUrls = all.filter((r) => r.url.startsWith(origin + "/") || r.url === origin || r.url === origin + "/");
89
-
90
- console.log(`▸ 捕获请求 ${all.length} 个;同源 ${ownUrls.length} 个,开始下载…`);
91
- let ok = 0, fail = 0;
92
- const failed = [];
93
- for (const r of ownUrls) {
94
- const rel = urlToLocalPath(r.url, origin);
95
- const dest = path.join(siteDir, rel);
96
- try {
97
- const resp = await ctx.request.get(r.url); // 复用浏览器网络栈(cookie/TUN/代理一致)
98
- if (!resp.ok()) { fail++; failed.push(`HTTP${resp.status()} ${rel}`); continue; }
99
- const buf = await resp.body();
100
- fs.mkdirSync(path.dirname(dest), { recursive: true });
101
- fs.writeFileSync(dest, buf);
102
- ok++;
103
- } catch (e) {
104
- fail++; failed.push(`${e.message} ${rel}`);
105
- }
106
- }
107
-
108
- // 第三方 + webfont 提示
109
- const thirdHosts = [...new Set(all.filter((r) => !r.url.startsWith(origin)).map((r) => { try { return new URL(r.url).host; } catch { return r.url; } }))];
110
- const webfontCss = all.map((r) => r.url).filter((u) => /use\.typekit\.net\/[a-z0-9]+\.css|fonts\.googleapis\.com\/css/i.test(u));
111
- const outRoot = path.resolve(args.out);
112
- fs.writeFileSync(path.join(outRoot, "mirror-manifest.json"), JSON.stringify(all, null, 2));
113
- fs.writeFileSync(path.join(outRoot, "own-asset-urls.txt"), ownUrls.map((r) => urlToLocalPath(r.url, origin)).sort().join("\n") + "\n");
114
- fs.writeFileSync(path.join(outRoot, "third-party.json"), JSON.stringify({ hosts: thirdHosts, webfont_css_to_selfhost: webfontCss }, null, 2));
115
-
116
- console.log(`✅ 镜像完成: ${ok} 成功 / ${fail} 失败 → ${siteDir}`);
117
- if (failed.length) console.log(" ⚠️ 失败:\n " + failed.slice(0, 20).join("\n "));
118
- console.log(`▸ 第三方 host: ${thirdHosts.join(", ") || "(无)"}`);
119
- if (webfontCss.length) console.log(`▸ 需自托管的 webfont CSS(锁域名,见 static-mirror.md): \n ${webfontCss.join("\n ")}`);
120
- console.log(`▸ 下一步: 自托管字体 + 改写 CSS @import + 删追踪 → cd ${siteDir} && python3 -m http.server 8124`);
121
- await browser.close();