autoclaw 1.3.4 → 1.3.6

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 (37) hide show
  1. package/README.md +356 -291
  2. package/README.zh-CN.md +69 -4
  3. package/dist/agent.js +47 -2
  4. package/dist/index.js +75 -1
  5. package/dist/sandbox.js +94 -0
  6. package/dist/shell.js +55 -32
  7. package/dist/skills.js +276 -0
  8. package/dist/tools/background.js +165 -0
  9. package/dist/tools/core.js +17 -1
  10. package/dist/tools/index.js +9 -1
  11. package/dist/tools/render-image.js +135 -0
  12. package/dist/tools/render-pdf.js +111 -0
  13. package/dist/tools/takumi-fonts.js +61 -0
  14. package/dist/zip.js +149 -0
  15. package/package.json +5 -2
  16. package/skills/code2media/SKILL.md +82 -0
  17. package/skills/code2media/references/syntax-guide.md +63 -0
  18. package/skills/code2media/scripts/package.json +10 -0
  19. package/skills/code2media/scripts/render.mjs +177 -0
  20. package/skills/code2media/templates/animation.html +19 -0
  21. package/skills/code2media/templates/badge.html +6 -0
  22. package/skills/code2media/templates/certificate.html +9 -0
  23. package/skills/code2media/templates/metrics-card.html +25 -0
  24. package/skills/code2media/templates/weekly-report.html +86 -0
  25. package/skills/invoice-maker/SKILL.md +65 -0
  26. package/skills/invoice-maker/references/syntax-guide.md +63 -0
  27. package/skills/invoice-maker/scripts/package.json +10 -0
  28. package/skills/invoice-maker/scripts/render.mjs +177 -0
  29. package/skills/invoice-maker/templates/invoice.html +26 -0
  30. package/skills/invoice-maker/templates/quote.html +68 -0
  31. package/skills/poster-maker/SKILL.md +62 -0
  32. package/skills/poster-maker/references/syntax-guide.md +63 -0
  33. package/skills/poster-maker/scripts/package.json +10 -0
  34. package/skills/poster-maker/scripts/render.mjs +177 -0
  35. package/skills/poster-maker/templates/cover.html +13 -0
  36. package/skills/poster-maker/templates/og-card.html +17 -0
  37. package/skills/poster-maker/templates/social-post.html +14 -0
@@ -0,0 +1,111 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { render as renderPdf } from 'takumi-pdf';
4
+ import { resolveFontLoaders, describeFonts } from './takumi-fonts.js';
5
+ const VALID_SIZES = ['a3', 'a4', 'a5', 'b4', 'b5', 'letter', 'legal', 'ledger'];
6
+ const toolDefinition = {
7
+ type: "function",
8
+ function: {
9
+ name: "render_pdf",
10
+ description: "Render an HTML template into a paged PDF document with selectable text and embedded subset fonts. Fully offline — no browser, no network, renders in milliseconds. Use for invoices, quotes, reports, packing slips, certificates and any structured document. Content flows across pages automatically; CSS break-before/break-after/break-inside control page breaks, and <thead> repeats on every page of a table. Optional header/footer bands repeat on every page and support page counters via <span class=\"pageNumber\"></span> and <span class=\"totalPages\"></span>. Style with inline styles, a <style> block with regular CSS classes, or the 'tw' attribute (Tailwind v4 utilities) — Tailwind utilities placed in the class attribute are NOT compiled. JavaScript is NOT executed. CJK text needs fonts: common system fonts are auto-detected when font_paths is omitted, and registered families can be referenced via font-family.",
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ template: {
15
+ type: "string",
16
+ description: "HTML fragment for the document body, e.g. '<div style=\"padding:32px\"><h1 style=\"font-size:24px;font-weight:700\">Invoice #1024</h1><table style=\"width:100%\">…</table></div>'. Style via inline styles, a <style> block, or the tw attribute (Tailwind v4 utilities); a plain class attribute only matches CSS selectors, not Tailwind utilities."
17
+ },
18
+ css: {
19
+ type: "string",
20
+ description: "Optional CSS stylesheet applied before layout, e.g. 'tr { break-inside: avoid; }'."
21
+ },
22
+ size: {
23
+ type: "string",
24
+ enum: VALID_SIZES,
25
+ description: "Page size. Default 'a4'."
26
+ },
27
+ landscape: {
28
+ type: "boolean",
29
+ description: "Swap page width and height. Default false."
30
+ },
31
+ margin: {
32
+ description: "Page margin in CSS px: a number applied to all sides, or an object {top,right,bottom,left}. Default 'auto'.",
33
+ type: ["number", "object"]
34
+ },
35
+ header: {
36
+ type: "string",
37
+ description: "HTML band repeated at the top of every page, e.g. '<div class=\"text-[10px] text-gray-500\">ACME Ltd</div>'."
38
+ },
39
+ footer: {
40
+ type: "string",
41
+ description: "HTML band repeated at the bottom of every page, e.g. '<div class=\"text-[10px] text-gray-500\" style=\"width:100%;text-align:center\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>'."
42
+ },
43
+ font_paths: {
44
+ type: "array",
45
+ items: { type: "string" },
46
+ description: "Local font files (.ttf/.otf/.ttc/.woff2) to register and embed. Omit to auto-detect common system fonts (incl. CJK). Pass [] to skip font loading (built-in Latin font only)."
47
+ },
48
+ title: {
49
+ type: "string",
50
+ description: "Document title written to the PDF metadata."
51
+ },
52
+ outline: {
53
+ type: "boolean",
54
+ description: "Build a bookmark outline from h1-h6 headings. Default false."
55
+ },
56
+ output_path: {
57
+ type: "string",
58
+ description: "File path to write the PDF. Parent directories are created automatically."
59
+ }
60
+ },
61
+ required: ["template", "output_path"]
62
+ }
63
+ }
64
+ };
65
+ const handler = async (args, config) => {
66
+ const template = typeof args.template === 'string' ? args.template : '';
67
+ if (!template.trim()) {
68
+ return "Error: 'template' is required — an HTML fragment for the document body.";
69
+ }
70
+ if (!args.output_path) {
71
+ return "Error: 'output_path' is required.";
72
+ }
73
+ const size = String(args.size || 'a4').toLowerCase();
74
+ if (!VALID_SIZES.includes(size)) {
75
+ return `Error: Invalid page size '${size}'. Supported sizes: ${VALID_SIZES.join(", ")}.`;
76
+ }
77
+ const resolvedPath = path.resolve(process.cwd(), args.output_path);
78
+ try {
79
+ const resolved = resolveFontLoaders(args.font_paths);
80
+ const fontsInfo = describeFonts(resolved);
81
+ const options = { size };
82
+ if (resolved.fonts.length > 0)
83
+ options.fonts = resolved.fonts;
84
+ if (args.css)
85
+ options.css = args.css;
86
+ if (args.landscape)
87
+ options.landscape = true;
88
+ if (args.margin != null)
89
+ options.margin = args.margin;
90
+ if (args.header)
91
+ options.header = args.header;
92
+ if (args.footer)
93
+ options.footer = args.footer;
94
+ if (args.title)
95
+ options.metadata = { title: args.title };
96
+ if (args.outline)
97
+ options.outline = true;
98
+ const bytes = await renderPdf(template, options);
99
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
100
+ fs.writeFileSync(resolvedPath, Buffer.from(bytes));
101
+ return `Rendered PDF (${bytes.length} bytes, ${size.toUpperCase()} page size) saved to ${resolvedPath}. Fonts: ${fontsInfo}`;
102
+ }
103
+ catch (err) {
104
+ return `Error rendering PDF: ${err?.message || err}`;
105
+ }
106
+ };
107
+ export const RenderPdfTool = {
108
+ name: "PDF Renderer (Takumi)",
109
+ definition: toolDefinition,
110
+ handler
111
+ };
@@ -0,0 +1,61 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ // Font files registered by the Takumi render tools when a call does not pass
4
+ // font_paths explicitly. Takumi ships only a last-resort Latin font, so without
5
+ // these, CJK and emoji render as tofu in headless containers. Paths mirror the
6
+ // font detection in screenshot.ts and cover Alpine/Debian/Arch packaging plus
7
+ // Windows and macOS system fonts.
8
+ const COMMON_FONT_PATHS = [
9
+ // Linux — CJK
10
+ '/usr/share/fonts/noto/NotoSansCJK-Regular.ttc',
11
+ '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
12
+ '/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc',
13
+ '/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc',
14
+ '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
15
+ // Linux — emoji
16
+ '/usr/share/fonts/noto/NotoColorEmoji.ttf',
17
+ '/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf',
18
+ '/usr/share/fonts/google-noto-emoji/NotoColorEmoji.ttf',
19
+ // Windows — msyh.ttc (Microsoft YaHei) covers CJK, seguiemj.ttf covers emoji
20
+ 'C:\\Windows\\Fonts\\msyh.ttc',
21
+ 'C:\\Windows\\Fonts\\simhei.ttf',
22
+ 'C:\\Windows\\Fonts\\arial.ttf',
23
+ 'C:\\Windows\\Fonts\\seguiemj.ttf',
24
+ // macOS
25
+ '/System/Library/Fonts/PingFang.ttc',
26
+ '/System/Library/Fonts/Hiragino Sans GB.ttc',
27
+ '/System/Library/Fonts/Helvetica.ttc',
28
+ '/System/Library/Fonts/Apple Color Emoji.ttc',
29
+ ];
30
+ // font_paths === undefined -> auto-detect common system fonts
31
+ // font_paths === [] -> skip font loading entirely (built-in Latin font)
32
+ export function resolveFontLoaders(fontPaths) {
33
+ const paths = fontPaths === undefined
34
+ ? COMMON_FONT_PATHS.filter(p => fs.existsSync(p))
35
+ : fontPaths;
36
+ const fonts = [];
37
+ const skipped = [];
38
+ for (const p of paths) {
39
+ try {
40
+ const data = fs.readFileSync(p);
41
+ const name = path.basename(p).replace(/\.(ttf|otf|ttc|woff2?)$/i, '');
42
+ fonts.push({ name, data });
43
+ }
44
+ catch {
45
+ skipped.push(p);
46
+ }
47
+ }
48
+ return { fonts, families: fonts.map(f => f.name), skipped };
49
+ }
50
+ // One-line summary for tool results so the agent knows which family names it
51
+ // can reference via font-family in templates.
52
+ export function describeFonts(resolved) {
53
+ if (resolved.families.length === 0) {
54
+ return 'no extra fonts registered (built-in Latin font only; CJK/emoji need font_paths)';
55
+ }
56
+ const parts = [`registered: ${resolved.families.join(', ')}`];
57
+ if (resolved.skipped.length > 0) {
58
+ parts.push(`skipped (unreadable): ${resolved.skipped.join(', ')}`);
59
+ }
60
+ return parts.join('; ');
61
+ }
package/dist/zip.js ADDED
@@ -0,0 +1,149 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as zlib from 'zlib';
4
+ // ---- CRC32 ----
5
+ const CRC_TABLE = (() => {
6
+ const table = new Uint32Array(256);
7
+ for (let n = 0; n < 256; n++) {
8
+ let c = n;
9
+ for (let k = 0; k < 8; k++)
10
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
11
+ table[n] = c >>> 0;
12
+ }
13
+ return table;
14
+ })();
15
+ function crc32(buf) {
16
+ let c = 0xffffffff;
17
+ for (let i = 0; i < buf.length; i++)
18
+ c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
19
+ return (c ^ 0xffffffff) >>> 0;
20
+ }
21
+ // ---- writer ----
22
+ export function createZip(files) {
23
+ const chunks = [];
24
+ const central = [];
25
+ let offset = 0;
26
+ const dosTime = 0;
27
+ const dosDate = 0x21; // fixed 1980-01-01 for deterministic output
28
+ for (const file of files) {
29
+ const name = Buffer.from(file.path.replace(/\\/g, '/'), 'utf8');
30
+ const crc = crc32(file.data);
31
+ const deflated = zlib.deflateRawSync(file.data, { level: 9 });
32
+ const useDeflate = deflated.length < file.data.length;
33
+ const payload = useDeflate ? deflated : file.data;
34
+ const method = useDeflate ? 8 : 0;
35
+ const local = Buffer.alloc(30);
36
+ local.writeUInt32LE(0x04034b50, 0);
37
+ local.writeUInt16LE(20, 4);
38
+ local.writeUInt16LE(0x0800, 6); // UTF-8 names
39
+ local.writeUInt16LE(method, 8);
40
+ local.writeUInt16LE(dosTime, 10);
41
+ local.writeUInt16LE(dosDate, 12);
42
+ local.writeUInt32LE(crc, 14);
43
+ local.writeUInt32LE(payload.length, 18);
44
+ local.writeUInt32LE(file.data.length, 22);
45
+ local.writeUInt16LE(name.length, 26);
46
+ local.writeUInt16LE(0, 28);
47
+ chunks.push(local, name, payload);
48
+ const cd = Buffer.alloc(46);
49
+ cd.writeUInt32LE(0x02014b50, 0);
50
+ cd.writeUInt16LE(20, 4);
51
+ cd.writeUInt16LE(20, 6);
52
+ cd.writeUInt16LE(0x0800, 8);
53
+ cd.writeUInt16LE(method, 10);
54
+ cd.writeUInt16LE(dosTime, 12);
55
+ cd.writeUInt16LE(dosDate, 14);
56
+ cd.writeUInt32LE(crc, 16);
57
+ cd.writeUInt32LE(payload.length, 20);
58
+ cd.writeUInt32LE(file.data.length, 24);
59
+ cd.writeUInt16LE(name.length, 28);
60
+ cd.writeUInt32LE(offset, 42);
61
+ central.push(Buffer.concat([cd, name]));
62
+ offset += 30 + name.length + payload.length;
63
+ }
64
+ const centralBuf = Buffer.concat(central);
65
+ const eocd = Buffer.alloc(22);
66
+ eocd.writeUInt32LE(0x06054b50, 0);
67
+ eocd.writeUInt16LE(files.length, 8);
68
+ eocd.writeUInt16LE(files.length, 10);
69
+ eocd.writeUInt32LE(centralBuf.length, 12);
70
+ eocd.writeUInt32LE(offset, 16);
71
+ return Buffer.concat([...chunks, centralBuf, eocd]);
72
+ }
73
+ // ---- reader ----
74
+ export function readZip(buf) {
75
+ let eocd = -1;
76
+ const min = Math.max(0, buf.length - 22 - 65535);
77
+ for (let i = buf.length - 22; i >= min; i--) {
78
+ if (buf.readUInt32LE(i) === 0x06054b50) {
79
+ eocd = i;
80
+ break;
81
+ }
82
+ }
83
+ if (eocd < 0)
84
+ throw new Error('not a zip archive (end-of-central-directory not found)');
85
+ const count = buf.readUInt16LE(eocd + 10);
86
+ const cdOffset = buf.readUInt32LE(eocd + 16);
87
+ const entries = [];
88
+ let p = cdOffset;
89
+ for (let n = 0; n < count; n++) {
90
+ if (p + 46 > buf.length || buf.readUInt32LE(p) !== 0x02014b50) {
91
+ throw new Error('corrupt zip archive (bad central directory)');
92
+ }
93
+ const method = buf.readUInt16LE(p + 10);
94
+ const compSize = buf.readUInt32LE(p + 20);
95
+ const nameLen = buf.readUInt16LE(p + 28);
96
+ const extraLen = buf.readUInt16LE(p + 30);
97
+ const commentLen = buf.readUInt16LE(p + 32);
98
+ const localOffset = buf.readUInt32LE(p + 42);
99
+ const name = buf.toString('utf8', p + 46, p + 46 + nameLen);
100
+ const lhNameLen = buf.readUInt16LE(localOffset + 26);
101
+ const lhExtraLen = buf.readUInt16LE(localOffset + 28);
102
+ const dataStart = localOffset + 30 + lhNameLen + lhExtraLen;
103
+ const raw = buf.subarray(dataStart, dataStart + compSize);
104
+ let data;
105
+ if (method === 0)
106
+ data = Buffer.from(raw);
107
+ else if (method === 8)
108
+ data = zlib.inflateRawSync(raw);
109
+ else
110
+ throw new Error(`unsupported zip compression method ${method} (entry: ${name})`);
111
+ entries.push({ path: name, data });
112
+ p += 46 + nameLen + extraLen + commentLen;
113
+ }
114
+ return entries;
115
+ }
116
+ // Reject absolute paths, drive letters, '..' traversal, and directory entries.
117
+ export function safeJoinZipPath(destDir, zipPath) {
118
+ const norm = zipPath.replace(/\\/g, '/');
119
+ if (norm.endsWith('/'))
120
+ return null; // directory entry — created implicitly
121
+ if (norm.startsWith('/') || /^[a-zA-Z]:/.test(norm)) {
122
+ throw new Error(`unsafe zip entry (absolute path): ${zipPath}`);
123
+ }
124
+ const parts = norm.split('/').filter(s => s.length > 0);
125
+ if (parts.length === 0 || parts.some(s => s === '..')) {
126
+ throw new Error(`unsafe zip entry (path traversal): ${zipPath}`);
127
+ }
128
+ return path.join(destDir, ...parts);
129
+ }
130
+ // Extract every entry into destDir, creating parents as needed. All paths are
131
+ // validated up-front so an unsafe entry (zip-slip) rejects the whole archive
132
+ // before a single byte is written.
133
+ export function extractZip(buf, destDir) {
134
+ const entries = readZip(buf);
135
+ const resolved = [];
136
+ for (const entry of entries) {
137
+ const dest = safeJoinZipPath(destDir, entry.path);
138
+ if (dest === null)
139
+ continue; // directory entry
140
+ resolved.push({ dest, data: entry.data });
141
+ }
142
+ const written = [];
143
+ for (const r of resolved) {
144
+ fs.mkdirSync(path.dirname(r.dest), { recursive: true });
145
+ fs.writeFileSync(r.dest, r.data);
146
+ written.push(r.dest);
147
+ }
148
+ return written;
149
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autoclaw",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
+ "skills",
20
21
  "README.md",
21
22
  "package.json",
22
23
  "LICENSE"
@@ -59,7 +60,9 @@
59
60
  "nodemailer": "^8.0.0",
60
61
  "openai": "^6.18.0",
61
62
  "ora": "^9.3.0",
62
- "playwright": "^1.58.2"
63
+ "playwright": "^1.58.2",
64
+ "takumi-js": "^2.13.5",
65
+ "takumi-pdf": "^0.14.1"
63
66
  },
64
67
  "devDependencies": {
65
68
  "@types/inquirer": "^9.0.9",
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: code2media
3
+ display_name: 代码转多媒体(HTML → 图片/PDF/动图)
4
+ display_name_en: Code to Media (HTML → images/PDF/animations)
5
+ description: Universal renderer — turn any HTML into pixel-perfect PNG/JPEG/WebP images, vector SVG, paged PDFs and animated WebP/GIF. Offline, no browser. Use when the user wants to 把内容/代码/数据变成图片或PDF, 生成图片/OG图/徽章/数据卡片/动图, 生成PDF/报告/发票/工单, or render HTML to image or PDF. For deep scenario optimization see sibling skills (posters, invoices, certificates).
6
+ description_zh: 通用多媒体渲染:把任意 HTML 变成精确的图片(PNG/JPEG/WebP)、矢量 SVG、分页 PDF 和动图(WebP/GIF)。离线渲染,无需浏览器,毫秒级出图,文字排版 100% 精确。通用兜底;海报/发票/证书等具体场景有专属技能时优先用专属技能。
7
+ description_en: Universal multimedia renderer — any HTML becomes precise images (PNG/JPEG/WebP), vector SVG, paged PDFs and animations (WebP/GIF). Offline, no browser, millisecond-per-render. General fallback; prefer dedicated scenario skills (posters, invoices, certificates) when available.
8
+ category: image
9
+ version: 1.2.0
10
+ author: AutoClaw
11
+ ---
12
+
13
+ # 代码转多媒体
14
+
15
+ 把任意 HTML 变成可直接交付的多媒体:图片、矢量 SVG、分页 PDF、动图。底层是 Takumi 渲染引擎(纯计算,无浏览器、无 AI、无网络依赖),**你写什么 HTML,输出就是什么像素**——文字、排版、颜色 100% 精确,相同输入永远得到相同输出。
16
+
17
+ 这是**通用渲染技能**:处理任意自定义图形与文档需求。海报、发票、证书等高频场景有各自独立优化的专属技能,任务精确匹配场景时优先用专属技能;没有匹配场景、或需要完全自定义的产物时,用本技能。
18
+
19
+ ## 何时使用
20
+
21
+ - 用户要**自定义/长尾**的图形与文档:上面场景技能没覆盖的尺寸、结构、风格,数据驱动的图表卡片,一次性版式
22
+ - 用户要**动图**:加载动画、动态徽章、社媒动图
23
+ - 任务明确命中高频场景时优先用专属技能(它们有独立的排版规范与质量清单):海报/封面/分享卡 → `poster-maker`;发票/报价单/收据/采购单 → `invoice-maker`
24
+ - 不适用:艺术创作、照片类图像(用平台的 AI 生图能力);截图现实网页(用浏览器/截图工具)
25
+
26
+ ## 工作流程
27
+
28
+ 1. **选起点**:优先修改 `templates/` 里的现成模板(见下方清单);没有合适的再从零写。语法规则必须遵守 `@references/syntax-guide.md`,最重要的三条:
29
+ - Tailwind 工具类写在 **`tw` 属性**里(`class` 属性不编译 Tailwind,只匹配普通 CSS 选择器)
30
+ - 用 Tailwind v4 规范名(渐变是 `bg-linear-to-br`,不是 v3 的 `bg-gradient-to-br`)
31
+ - 根元素撑满画布:`tw="w-full h-full ..."`
32
+ 2. **写出 HTML 片段文件**(不需要完整 HTML 文档),数据直接填进模板。
33
+ 3. **运行渲染脚本**:
34
+ ```bash
35
+ node "<技能目录>/scripts/render.mjs" --html card.html -o card.png --width 1200 --height 630
36
+ ```
37
+ 首次运行会自动安装 takumi-js / takumi-pdf(一次性,需网络);之后完全离线。
38
+ 4. **校验并交付**:确认脚本输出 `OK` 与文件存在,把输出路径告诉用户。渲染是毫秒级的,改模板重渲染的成本几乎为零——效果不满意就直接改了再跑。
39
+
40
+ ## 命令速查
41
+
42
+ ```bash
43
+ # 静态图片(png/jpeg/webp),尺寸自定
44
+ node scripts/render.mjs --html card.html -o card.png --width 1200 --height 630
45
+ # 矢量 SVG
46
+ node scripts/render.mjs --html badge.html -o badge.svg --width 560 --height 160
47
+ # 分页 PDF(a3/a4/a5/b4/b5/letter/legal/ledger,自动分页,可加页脚/标题/书签)
48
+ node scripts/render.mjs --html invoice.html --pdf -o invoice.pdf --size a4 --title "采购订单" \
49
+ --footer '<div style="width:100%;text-align:center;font-size:10px;color:#94a3b8">第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页</div>'
50
+ # 动图(CSS @keyframes 按时间采样,输出 webp/gif/apng)
51
+ node scripts/render.mjs --html anim.html --animation webp -o anim.webp --width 480 --height 480 --fps 30 --duration 1200
52
+ # 附加样式表 / 指定字体 / 跳过字体
53
+ node scripts/render.mjs --html card.html -o card.png --css extra.css
54
+ node scripts/render.mjs --html card.html -o card.png --font /path/MyFont.ttf
55
+ node scripts/render.mjs --html card.html -o card.png --no-fonts # 纯拉丁文字时更快
56
+ ```
57
+
58
+ JPEG/WebP 可加 `--quality 80`。PDF 表格加 `style="break-inside:avoid"` 防止行被劈开,`<thead>` 会跨页自动重复。
59
+
60
+ ## 模板清单(templates/)
61
+
62
+ 通用示例模板,展示引擎能力面。海报/封面/OG 图在 `poster-maker` 技能,发票/报价单在 `invoice-maker` 技能。
63
+
64
+ | 文件 | 用途 | 建议参数 |
65
+ | :--- | :--- | :--- |
66
+ | `metrics-card.html` | KPI 数据指标卡(1600x900) | 图片 |
67
+ | `certificate.html` | 结业证书/奖状(1414x1000,金色边框) | 图片 |
68
+ | `weekly-report.html` | 单页 A4 运营周报(KPI + 纯 div 柱状图) | `--pdf` |
69
+ | `badge.html` | SVG 徽章/发布标签(560x160) | 图片(输出 .svg) |
70
+ | `animation.html` | 品牌 Logo 脉冲动画(480x480) | `--animation webp` |
71
+
72
+ ## 批量场景
73
+
74
+ 要批量生成(如 200 份证书、按订单出发票)时:写一个循环脚本,读数据(_CSV/JSON_),把每条数据填进同一份模板后循环调用 `render.mjs`。单张渲染约几十毫秒,200 份几秒完成;输出确定性意味着同一份数据永远得到同样的文件。
75
+
76
+ ## 环境要求与故障排查
77
+
78
+ - Node.js >= 20.19。
79
+ - 首次运行自动 `npm install`(安装进 scripts/ 目录),之后离线可用;若目标环境完全断网,需在有网环境预装后再拷贝,或提前执行一次安装。
80
+ - 中文/Emoji 显示为方块(豆腐块):容器缺字体。安装 `font-noto-cjk`(Alpine)或 `fonts-noto-cjk`(Debian)后重跑;或用 `--font` 指定字体文件。详见 `@references/syntax-guide.md`。
81
+ - Emoji 默认经 Twemoji CDN 在线获取;完全离线的环境让模板保持纯文本。
82
+ - 渲染报错信息以 `Error` 开头输出在 stderr,按提示修正模板后重试。
@@ -0,0 +1,63 @@
1
+ # HTML 模板语法指南
2
+
3
+ 渲染引擎是 Takumi(Rust 排版引擎的 Node 绑定):解析 HTML 片段 → taffy 布局(Flexbox/Grid/block/float)→ 文字排版(含中日韩、RTL、emoji)→ 合成输出。本指南只列**容易踩坑的规则**,完整 CSS 支持范围以引擎能力为准。
4
+
5
+ ## 1. 样式的三条通路
6
+
7
+ | 方式 | 写法 | 说明 |
8
+ | :--- | :--- | :--- |
9
+ | `tw` 属性 | `<div tw="w-full h-full bg-blue-500">` | **Tailwind v4 工具类的唯一入口** |
10
+ | 内联 style | `<div style="font-size:32px;color:#fff">` | 标准 CSS,最直接 |
11
+ | `<style>` 块 | `<style>.card{background:#0ea5e9}</style>` + `class="card"` | 常规 CSS 选择器,支持 `:is()`、`::before` 等 |
12
+
13
+ **关键陷阱**:
14
+ - Tailwind 工具类放在 `class` 属性里**不会编译**——整块样式静默失效(背景全透明)。工具类永远写 `tw`。
15
+ - `class` 属性的用途是配合 `<style>` 块里的普通选择器。
16
+
17
+ ## 2. Tailwind v4 命名
18
+
19
+ 用 v4 规范名,v3 旧名不识别:
20
+ - 渐变:`bg-linear-to-br`(不是 `bg-gradient-to-br`)+ `from-blue-600 to-indigo-900`
21
+ - 透明度修饰符可用:`bg-white/10`、`bg-blue-500/20`
22
+ - 常用:`w-full h-full flex flex-col items-center justify-center gap-4 p-16 rounded-2xl rounded-full text-6xl font-bold leading-tight tracking-widest mt-10 space-y-2`
23
+ - 任意值:`text-[10px]`、`w-[220px]`
24
+
25
+ ## 3. 布局要点
26
+
27
+ - 根元素必须撑满画布:`tw="w-full h-full"`,否则内容按自身高度渲染。
28
+ - 布局引擎:Flexbox、CSS Grid、block、inline、float、`calc()`、绝对定位、z-index 均支持。
29
+ - `w-14 h-14 w-36 text-6xl` 等标准尺寸刻度可用;复杂尺寸用任意值或内联 style。
30
+
31
+ ## 4. 字体
32
+
33
+ 引擎内置仅一个拉丁兜底字体;**中日韩、Emoji 必须注册字体**。渲染脚本自动探测常见系统字体(Windows: msyh/simhei/arial/seguiemj;macOS: PingFang/Hiragino;Linux: Noto CJK/WQY + Noto Emoji),注册后的字体族名 = 文件名去扩展名(如 `msyh`、`NotoSansCJK-Regular`)。
34
+
35
+ - 模板里跨平台建议写字体栈:`style="font-family:msyh,NotoSansCJK-Regular,NotoSansCJKsc-Regular,wqy-zenhei"`
36
+ - 不指定时使用注册顺序作为回退链,注册了 CJK 字体就能显示中文。
37
+ - 容器缺字体 → 中文显示方块:Alpine `apk add font-noto-cjk font-noto-emoji`;Debian `apt-get install fonts-noto-cjk fonts-noto-color-emoji`。或 `--font /路径/字体.ttf` 显式注册。
38
+ - Emoji 默认走 Twemoji CDN 在线取图(需网络);离线环境模板写纯文本。
39
+ - 等宽/代码:`font-family:monospace` 若未注册对应字体,回退到内置拉丁字体。
40
+
41
+ ## 5. 不支持 / 受限
42
+
43
+ - **不执行 JavaScript**——图表用纯 `div` 条形图拼,数据直接写进 HTML。
44
+ - CSS 是 Chrome 的子集:`backdrop-filter`、blend modes 支持于图片模式;**PDF 模式不支持** `filter: blur()`、`drop-shadow()`、`backdrop-filter`。
45
+ - 远程图片(`<img src="https://...">`)需要网络;离线场景把图片以本地文件方式避开或不用图。
46
+ - PDF 模式不支持 CSS `@page` 规则——页面尺寸/边距用命令行参数(`--size`、`--landscape`)控制。
47
+
48
+ ## 6. PDF 分页
49
+
50
+ - 内容超过一页自动分页;`break-before:page`、`break-after:page`、`break-inside:avoid` 控制断点(表格行建议 `break-inside:avoid`)。
51
+ - `<thead>` 跨页自动重复;孤行寡行默认保护。
52
+ - 页眉页脚用 `--header` / `--footer` 参数传入 HTML 片段,内部可用 `<span class="pageNumber"></span>`、`<span class="totalPages"></span>` 注入页码计数(可加 `cjk-decimal` 等计数样式类)。
53
+ - `--title` 写入 PDF 元数据;`--outline` 从 h1-h6 生成书签。
54
+
55
+ ## 7. 动画
56
+
57
+ - 在 `<style>` 里写 `@keyframes`,元素上加 `animation: 名称 时长 linear infinite`。
58
+ - 脚本对整个场景按 `--fps` 采样 `--duration` 毫秒,输出动画 WebP/GIF/APNG。
59
+ - 变换(rotate/scale/translate)、透明度渐变均可采样;两个以上元素错开 `delay` 能做出层次感(参考 `templates/animation.html`)。
60
+
61
+ ## 8. 确定性与批量
62
+
63
+ 相同输入(模板+参数+字体)永远产出字节一致的文件。批量生成时:循环读数据 → 数据填入模板 → 调 `render.mjs` → 输出按数据键命名。单张几十毫秒,失败重跑零成本。
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "code2media-scripts",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "Renderer dependencies for the code2media skill (installed automatically on first run).",
6
+ "dependencies": {
7
+ "takumi-js": "^2.13.5",
8
+ "takumi-pdf": "^0.14.1"
9
+ }
10
+ }