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,177 @@
1
+ #!/usr/bin/env node
2
+ // HTML -> image / SVG / paged PDF / animation, powered by Takumi (no browser).
3
+ // Self-contained CLI for agent skills: auto-installs takumi-js/takumi-pdf on
4
+ // first run, renders an HTML fragment file to the requested output format.
5
+ //
6
+ // Usage:
7
+ // node render.mjs --html card.html -o card.png --width 1200 --height 630
8
+ // node render.mjs --html badge.html -o badge.svg
9
+ // node render.mjs --html invoice.html --pdf -o invoice.pdf [--size a4] [--title "t"] [--footer '<div>...</div>']
10
+ // node render.mjs --html anim.html --animation webp -o anim.webp [--fps 30] [--duration 1200]
11
+ // node render.mjs --html card.html -o card.png --font /path/Font.ttf [--font ...] | --no-fonts
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { execFileSync } from 'child_process';
15
+ import { fileURLToPath } from 'url';
16
+ import { createRequire } from 'module';
17
+
18
+ const MIN_NODE = 20;
19
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
20
+ if (nodeMajor < MIN_NODE) {
21
+ console.error(`Error: Node.js >= ${MIN_NODE}.19 required, found ${process.versions.node}.`);
22
+ process.exit(1);
23
+ }
24
+
25
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
26
+ const require = createRequire(path.join(SCRIPT_DIR, 'package.json'));
27
+
28
+ function ensureDependencies() {
29
+ try {
30
+ require.resolve('takumi-js');
31
+ require.resolve('takumi-pdf');
32
+ return;
33
+ } catch { /* fall through to install */ }
34
+ console.error('First run: installing takumi-js and takumi-pdf (one-time, needs network)...');
35
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
36
+ execFileSync(npm, ['install', '--no-fund', '--no-audit', 'takumi-js', 'takumi-pdf'], {
37
+ cwd: SCRIPT_DIR, stdio: 'inherit', shell: process.platform === 'win32'
38
+ });
39
+ // Re-check after install so failures surface as a clear error.
40
+ try {
41
+ require.resolve('takumi-js');
42
+ require.resolve('takumi-pdf');
43
+ } catch {
44
+ error('dependency install finished but takumi-js/takumi-pdf still not resolvable; check network / npm registry access.');
45
+ }
46
+ }
47
+ ensureDependencies();
48
+
49
+ const { render, renderSvg, renderAnimation } = await import('takumi-js');
50
+ const { render: renderPdf } = await import('takumi-pdf');
51
+
52
+ // ---- args ----
53
+ const args = process.argv.slice(2);
54
+ const ALIASES = { output: ['o'] };
55
+ function opt(name) {
56
+ for (const key of [name, ...(ALIASES[name] || [])]) {
57
+ const token = key.length === 1 ? `-${key}` : `--${key}`;
58
+ const i = args.indexOf(token);
59
+ if (i !== -1) return args[i + 1];
60
+ }
61
+ return undefined;
62
+ }
63
+ function flag(name) {
64
+ return args.includes(`--${name}`);
65
+ }
66
+ function error(msg) {
67
+ console.error(`Error: ${msg}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ const htmlFile = opt('html');
72
+ const output = opt('output');
73
+ if (!htmlFile) error("'--html <file>' is required (an HTML fragment file to render).");
74
+ if (!output) error("'-o/--output <file>' is required.");
75
+ const template = fs.readFileSync(htmlFile, 'utf-8');
76
+ if (!template.trim()) error(`${htmlFile} is empty.`);
77
+
78
+ const cssFile = opt('css');
79
+ const css = cssFile ? fs.readFileSync(cssFile, 'utf-8') : undefined;
80
+
81
+ // ---- fonts: auto-detect common system fonts unless --no-fonts / --font given ----
82
+ const COMMON_FONT_PATHS = [
83
+ '/usr/share/fonts/noto/NotoSansCJK-Regular.ttc',
84
+ '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
85
+ '/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc',
86
+ '/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc',
87
+ '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
88
+ '/usr/share/fonts/noto/NotoColorEmoji.ttf',
89
+ '/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf',
90
+ '/usr/share/fonts/google-noto-emoji/NotoColorEmoji.ttf',
91
+ 'C:\\Windows\\Fonts\\msyh.ttc',
92
+ 'C:\\Windows\\Fonts\\simhei.ttf',
93
+ 'C:\\Windows\\Fonts\\arial.ttf',
94
+ 'C:\\Windows\\Fonts\\seguiemj.ttf',
95
+ '/System/Library/Fonts/PingFang.ttc',
96
+ '/System/Library/Fonts/Hiragino Sans GB.ttc',
97
+ '/System/Library/Fonts/Helvetica.ttc',
98
+ '/System/Library/Fonts/Apple Color Emoji.ttc',
99
+ ];
100
+
101
+ function resolveFonts() {
102
+ if (flag('no-fonts')) return [];
103
+ const fontOpts = args.flatMap((a, i) => (a === '--font' ? [args[i + 1]] : []));
104
+ const paths = fontOpts.length > 0 ? fontOpts : COMMON_FONT_PATHS.filter(p => fs.existsSync(p));
105
+ const fonts = [];
106
+ for (const p of paths) {
107
+ try {
108
+ fonts.push({ name: path.basename(p).replace(/\.(ttf|otf|ttc|woff2?)$/i, ''), data: fs.readFileSync(p) });
109
+ } catch { /* skip unreadable */ }
110
+ }
111
+ return fonts;
112
+ }
113
+ const fonts = resolveFonts();
114
+
115
+ const width = Number(opt('width')) || 1200;
116
+ const height = Number(opt('height')) || 630;
117
+ const quality = opt('quality') ? Number(opt('quality')) : undefined;
118
+
119
+ try {
120
+ fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
121
+
122
+ // ---- PDF mode ----
123
+ if (flag('pdf')) {
124
+ const options = { size: (opt('size') || 'a4').toLowerCase(), fonts };
125
+ if (css) options.css = css;
126
+ if (flag('landscape')) options.landscape = true;
127
+ if (opt('title')) options.metadata = { title: opt('title') };
128
+ if (opt('footer')) options.footer = opt('footer');
129
+ if (opt('header')) options.header = opt('header');
130
+ if (flag('outline')) options.outline = true;
131
+ const VALID_SIZES = ['a3', 'a4', 'a5', 'b4', 'b5', 'letter', 'legal', 'ledger'];
132
+ if (!VALID_SIZES.includes(options.size)) {
133
+ error(`invalid page size '${options.size}'. Supported: ${VALID_SIZES.join(', ')}.`);
134
+ }
135
+ const bytes = await renderPdf(template, options);
136
+ fs.writeFileSync(output, Buffer.from(bytes));
137
+ console.log(`OK PDF ${bytes.length} bytes -> ${output}`);
138
+ process.exit(0);
139
+ }
140
+
141
+ // ---- animation mode ----
142
+ const animFormat = opt('animation');
143
+ if (animFormat) {
144
+ const VALID_ANIM = ['webp', 'gif', 'apng'];
145
+ const fmt = animFormat.toLowerCase();
146
+ if (!VALID_ANIM.includes(fmt)) error(`invalid animation format '${animFormat}'. Supported: ${VALID_ANIM.join(', ')}.`);
147
+ const bytes = await renderAnimation({
148
+ width, height,
149
+ fps: Number(opt('fps')) || 30,
150
+ format: fmt,
151
+ scenes: [{ node: template, durationMs: Number(opt('duration')) || 1000 }],
152
+ fonts, ...(css ? { css } : {})
153
+ });
154
+ fs.writeFileSync(output, Buffer.from(bytes));
155
+ console.log(`OK animation ${fmt.toUpperCase()} ${bytes.length} bytes -> ${output}`);
156
+ process.exit(0);
157
+ }
158
+
159
+ // ---- static image / SVG mode ----
160
+ const format = (opt('format') || path.extname(output).replace('.', '') || 'png').toLowerCase();
161
+ if (format === 'svg') {
162
+ const svg = await renderSvg(template, { width, height, fonts, ...(css ? { css } : {}) });
163
+ fs.writeFileSync(output, svg, 'utf-8');
164
+ console.log(`OK SVG ${Buffer.byteLength(svg)} bytes -> ${output}`);
165
+ } else {
166
+ const VALID = ['png', 'jpeg', 'webp'];
167
+ if (!VALID.includes(format)) error(`invalid format '${format}'. Supported: png, jpeg, webp, svg (or use --pdf / --animation).`);
168
+ const options = { width, height, format, fonts, ...(css ? { css } : {}) };
169
+ if (quality != null && (format === 'jpeg' || format === 'webp')) options.quality = quality;
170
+ const bytes = await render(template, options);
171
+ fs.writeFileSync(output, Buffer.from(bytes));
172
+ console.log(`OK ${format.toUpperCase()} ${width}x${height} ${bytes.length} bytes -> ${output}`);
173
+ }
174
+ } catch (err) {
175
+ console.error(`Error rendering: ${err?.message || err}`);
176
+ process.exit(1);
177
+ }
@@ -0,0 +1,19 @@
1
+ <style>
2
+ @keyframes pulse-ring {
3
+ 0% { transform: scale(0.55); opacity: 0.9; }
4
+ 100% { transform: scale(1.5); opacity: 0; }
5
+ }
6
+ @keyframes spin { to { transform: rotate(360deg); } }
7
+ @keyframes breathe {
8
+ 0%, 100% { transform: scale(1); }
9
+ 50% { transform: scale(1.12); }
10
+ }
11
+ </style>
12
+ <div tw="w-full h-full flex items-center justify-center bg-slate-900">
13
+ <div style="position:absolute;width:200px;height:200px;border-radius:9999px;background:#3b82f6;animation:pulse-ring 1.2s linear infinite"></div>
14
+ <div style="position:absolute;width:200px;height:200px;border-radius:9999px;background:#3b82f6;animation:pulse-ring 1.2s linear infinite 0.6s"></div>
15
+ <div tw="w-36 h-36 rounded-3xl bg-linear-to-br from-blue-500 to-indigo-600 flex items-center justify-center" style="animation:breathe 1.2s ease-in-out infinite">
16
+ <span tw="text-6xl font-bold text-white" style="font-family:arial">A</span>
17
+ </div>
18
+ <div style="position:absolute;margin-top:220px;width:0;height:0;border-left:18px solid transparent;border-right:18px solid transparent;border-bottom:30px solid #f59e0b;animation:spin 2s linear infinite"></div>
19
+ </div>
@@ -0,0 +1,6 @@
1
+ <div tw="w-full h-full flex items-center justify-center bg-transparent">
2
+ <div tw="flex items-center gap-3 rounded-full px-10 py-5" style="background:#1e293b;border:2px solid #3b82f6">
3
+ <span tw="text-3xl font-bold text-white" style="font-family:arial">render_pdf</span>
4
+ <span tw="text-3xl font-bold px-4 rounded-full" style="background:#2563eb;color:#fff">new</span>
5
+ </div>
6
+ </div>
@@ -0,0 +1,9 @@
1
+ <div tw="w-full h-full flex flex-col items-center justify-center bg-linear-to-b from-amber-50 to-white" style="font-family:msyh,NotoSansCJK-Regular,NotoSansCJKsc-Regular,wqy-zenhei;border:16px solid #d4af37">
2
+ <p tw="text-3xl tracking-widest text-amber-700">C E R T I F I C A T E</p>
3
+ <h1 tw="text-6xl font-bold text-slate-800 mt-10">张伟</h1>
4
+ <p tw="text-2xl text-slate-500 mt-8">完成《AutoClaw 大规模自动化实战》课程</p>
5
+ <div tw="flex items-center gap-16 mt-16 text-lg text-slate-400">
6
+ <span>证书编号: AC-2026-0001</span>
7
+ <span>签发日期: 2026-09-03</span>
8
+ </div>
9
+ </div>
@@ -0,0 +1,25 @@
1
+ <div tw="w-full h-full bg-white p-12 flex flex-col" style="font-family:msyh,NotoSansCJK-Regular,NotoSansCJKsc-Regular,wqy-zenhei">
2
+ <div tw="flex justify-between items-center">
3
+ <h1 tw="text-4xl font-bold text-slate-800">自动化周报</h1>
4
+ <span tw="text-xl text-slate-400">2026-09-01 ~ 09-07</span>
5
+ </div>
6
+ <div tw="flex gap-6 mt-10 flex-1">
7
+ <div tw="flex-1 rounded-2xl bg-blue-50 text-blue-600 p-8 flex flex-col justify-center">
8
+ <span tw="text-2xl opacity-70">任务总数</span>
9
+ <span tw="text-6xl font-bold mt-3">1,284</span>
10
+ </div>
11
+ <div tw="flex-1 rounded-2xl bg-emerald-50 text-emerald-600 p-8 flex flex-col justify-center">
12
+ <span tw="text-2xl opacity-70">成功率</span>
13
+ <span tw="text-6xl font-bold mt-3">99.2%</span>
14
+ </div>
15
+ <div tw="flex-1 rounded-2xl bg-amber-50 text-amber-600 p-8 flex flex-col justify-center">
16
+ <span tw="text-2xl opacity-70">平均耗时</span>
17
+ <span tw="text-6xl font-bold mt-3">3.4s</span>
18
+ </div>
19
+ <div tw="flex-1 rounded-2xl bg-purple-50 text-purple-600 p-8 flex flex-col justify-center">
20
+ <span tw="text-2xl opacity-70">节省工时</span>
21
+ <span tw="text-6xl font-bold mt-3">217h</span>
22
+ </div>
23
+ </div>
24
+ <p tw="text-lg text-slate-400 mt-8">Generated by AutoClaw · deterministic render, no AI involved</p>
25
+ </div>
@@ -0,0 +1,86 @@
1
+ <div tw="w-full h-full flex flex-col p-10" style="font-family:msyh,NotoSansCJK-Regular,NotoSansCJKsc-Regular,wqy-zenhei">
2
+ <div tw="flex justify-between items-center">
3
+ <div>
4
+ <h1 tw="text-3xl font-bold text-slate-900">服务流量周报</h1>
5
+ <p tw="text-sm text-slate-400 mt-1">2026-08-31 ~ 2026-09-06 · 自动生成于每周一 09:00</p>
6
+ </div>
7
+ <span tw="px-4 py-1.5 rounded-full bg-emerald-50 text-emerald-600 text-sm font-semibold">全部指标正常</span>
8
+ </div>
9
+
10
+ <div tw="flex gap-4 mt-6">
11
+ <div tw="flex-1 rounded-xl bg-slate-50 p-4">
12
+ <p tw="text-xs text-slate-400">总请求</p>
13
+ <p tw="text-2xl font-bold text-slate-800 mt-1">2,417,393</p>
14
+ <p tw="text-xs text-emerald-600 mt-1">▲ 8.2% 环比</p>
15
+ </div>
16
+ <div tw="flex-1 rounded-xl bg-slate-50 p-4">
17
+ <p tw="text-xs text-slate-400">平均延迟</p>
18
+ <p tw="text-2xl font-bold text-slate-800 mt-1">86ms</p>
19
+ <p tw="text-xs text-emerald-600 mt-1">▼ 12ms 环比</p>
20
+ </div>
21
+ <div tw="flex-1 rounded-xl bg-slate-50 p-4">
22
+ <p tw="text-xs text-slate-400">错误率</p>
23
+ <p tw="text-2xl font-bold text-slate-800 mt-1">0.03%</p>
24
+ <p tw="text-xs text-slate-400 mt-1">持平</p>
25
+ </div>
26
+ <div tw="flex-1 rounded-xl bg-slate-50 p-4">
27
+ <p tw="text-xs text-slate-400">可用性</p>
28
+ <p tw="text-2xl font-bold text-slate-800 mt-1">99.99%</p>
29
+ <p tw="text-xs text-emerald-600 mt-1">目标 99.9%</p>
30
+ </div>
31
+ </div>
32
+
33
+ <div tw="mt-6">
34
+ <h2 tw="text-lg font-bold text-slate-800">每日请求量(万次)</h2>
35
+ <div tw="flex items-end gap-3 mt-4" style="height:180px">
36
+ <div tw="flex-1 flex flex-col items-center gap-2">
37
+ <span tw="text-xs text-slate-400">286</span>
38
+ <div tw="w-full rounded-t-lg bg-blue-500" style="height:52%"></div>
39
+ <span tw="text-xs text-slate-500">周一</span>
40
+ </div>
41
+ <div tw="flex-1 flex flex-col items-center gap-2">
42
+ <span tw="text-xs text-slate-400">312</span>
43
+ <div tw="w-full rounded-t-lg bg-blue-500" style="height:58%"></div>
44
+ <span tw="text-xs text-slate-500">周二</span>
45
+ </div>
46
+ <div tw="flex-1 flex flex-col items-center gap-2">
47
+ <span tw="text-xs text-slate-400">298</span>
48
+ <div tw="w-full rounded-t-lg bg-blue-500" style="height:55%"></div>
49
+ <span tw="text-xs text-slate-500">周三</span>
50
+ </div>
51
+ <div tw="flex-1 flex flex-col items-center gap-2">
52
+ <span tw="text-xs text-slate-400">405</span>
53
+ <div tw="w-full rounded-t-lg bg-blue-600" style="height:76%"></div>
54
+ <span tw="text-xs text-slate-500">周四</span>
55
+ </div>
56
+ <div tw="flex-1 flex flex-col items-center gap-2">
57
+ <span tw="text-xs text-slate-400">452</span>
58
+ <div tw="w-full rounded-t-lg bg-blue-600" style="height:85%"></div>
59
+ <span tw="text-xs text-slate-500">周五</span>
60
+ </div>
61
+ <div tw="flex-1 flex flex-col items-center gap-2">
62
+ <span tw="text-xs text-slate-400">351</span>
63
+ <div tw="w-full rounded-t-lg bg-blue-400" style="height:65%"></div>
64
+ <span tw="text-xs text-slate-500">周六</span>
65
+ </div>
66
+ <div tw="flex-1 flex flex-col items-center gap-2">
67
+ <span tw="text-xs text-slate-400">313</span>
68
+ <div tw="w-full rounded-t-lg bg-blue-400" style="height:58%"></div>
69
+ <span tw="text-xs text-slate-500">周日</span>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <div tw="mt-6 flex gap-6">
75
+ <div tw="flex-1">
76
+ <h2 tw="text-lg font-bold text-slate-800">异常事件</h2>
77
+ <ul tw="mt-3 space-y-2 text-sm text-slate-600">
78
+ <li>· 周四 14:20 缓存集群扩容,耗时 6 分钟,无请求失败</li>
79
+ <li>· 周五 09:05 上游依赖抖动 3 分钟,错误率瞬时升至 0.4% 后恢复</li>
80
+ </ul>
81
+ </div>
82
+ <div tw="flex-1 rounded-xl bg-blue-50 p-5">
83
+ <p tw="text-sm text-blue-900 leading-relaxed"><b>结论:</b>流量随工作日稳步增长,峰值出现在周五;延迟与错误率均在预算内,下周继续观察周五早高峰容量水位。</p>
84
+ </div>
85
+ </div>
86
+ </div>
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: invoice-maker
3
+ display_name: 发票生成器(报价单/收据/采购单)
4
+ display_name_en: Invoice Maker (quotes, receipts, purchase orders)
5
+ description: Generate formal business documents as paged PDFs — invoices, quotes, purchase orders, receipts, statements — with proper tables, tax lines, page numbers and bilingual layouts. Use when the user wants to 开发票/生成发票/报价单/收据/采购单/对账单/账单/付款通知 or make an invoice/quote/receipt PDF.
6
+ description_zh: 生成正式商务票据 PDF:发票、报价单、收据、采购单、对账单、账单。金额右对齐、税行规范、表格跨页表头重复、页码页脚,中英双语排版可选。离线毫秒级渲染,支持按订单数据批量出票。
7
+ description_en: Generate formal business document PDFs — invoices, quotes, purchase orders, receipts, statements. Right-aligned amounts, tax lines, repeating table headers across pages, page-number footers, optional bilingual layouts. Offline, batch-ready.
8
+ category: office
9
+ version: 1.0.0
10
+ author: AutoClaw
11
+ ---
12
+
13
+ # 发票生成器
14
+
15
+ 把订单/交易数据变成正式的商务票据 PDF:发票、报价单、收据、采购单、对账单、账单。本技能沉淀了**票据的版式结构、金额规范和批量开票模式**,产出的 PDF 文字可选中、字体已内嵌,可直接邮件发送或归档。
16
+
17
+ ## 何时使用
18
+
19
+ - 用户要"开/生成"一张正式单据:发票、报价单、收据、采购单、对账单、账单、付款通知
20
+ - 从订单表(CSV/JSON)批量出票
21
+ - 不适用:非票据类报告(运营周报等用 code2media);证书奖状(用 code2media 模板);艺术排版海报(用 poster-maker)
22
+
23
+ ## 版式结构(票据的骨架)
24
+
25
+ ```
26
+ [抬头区] 公司名 + 单据类型/编号 日期/有效期/联系人(右对齐)
27
+ [客户区] 致:客户名 + 一句话背景说明(浅底色块)
28
+ [明细表] # | 项目 | 规格说明 | 数量 | 单价 | 小计(表头浅灰底)
29
+ [合计区] 小计 → 税 → 含税总计(右对齐,总计最大最粗)
30
+ [条款区] 付款条款/有效期/免责,小号灰字,可中英双语
31
+ ```
32
+
33
+ 参考 `templates/`:`invoice.html`(采购订单,60 行明细跨页示例)、`quote.html`(报价单)。
34
+
35
+ ## 金额与排版规范(硬性)
36
+
37
+ - **金额一律右对齐**,带千分位(`¥ 104,940.00`);数量/单价/小计列都用 `text-align:right`
38
+ - **总计行最大最粗**,小计/税行用灰色小一号;税行单独列出,不和总计混算
39
+ - 表格行加 `style="border-bottom:1px solid #e2e8f0"`,表头浅灰底 `background:#f1f5f9`
40
+ - 表格行加 `break-inside:avoid` 防跨页劈行;`<thead>` 会跨页自动重复,明细多也不怕
41
+ - 页脚固定放页码:`第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页`
42
+ - 编号规则清晰可见(如 `QU-2026-0912`),日期用完整格式;中英双语条款便于跨境
43
+ - 渲染命令:
44
+
45
+ ```bash
46
+ node "<技能目录>/scripts/render.mjs" --html invoice.html --pdf -o invoice.pdf --size a4 \
47
+ --title "发票 INV-2026-0001" \
48
+ --footer '<div style="width:100%;text-align:center;font-size:10px;color:#94a3b8">第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页</div>'
49
+ ```
50
+
51
+ ## 批量开票(swarm 模式)
52
+
53
+ 按订单表出票:循环读每行数据 → 把 `templates/invoice.html` 的抬头/明细/合计替换成该订单 → 渲染为 `invoices/<订单号>.pdf`。单张约几十毫秒,千张订单分钟级完成;输出确定,重跑同数据得到同文件。列宽字段(公司名/品名)超长时截断或换行,不要溢出表格。
54
+
55
+ ## 交付前检查
56
+
57
+ - [ ] 总计 = 明细之和(含税计算正确,税率单列)
58
+ - [ ] 单据编号、双方名称、日期完整
59
+ - [ ] 明细超过一页时:第 2 页起表头是否重复、页码是否正确
60
+ - [ ] 中文无豆腐块(容器需 `font-noto-cjk` 字体包,或 `--font` 指定)
61
+ - [ ] PDF 文字可选中、可搜索(本技能默认满足)
62
+
63
+ ## 环境要求
64
+
65
+ Node.js >= 20.19;首次运行自动安装 takumi-js/takumi-pdf(一次性,需网络)。非票据的自定义文档用 `code2media` 通用技能。
@@ -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": "invoice-maker-scripts",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "Renderer dependencies for the invoice-maker skill (installed automatically on first run).",
6
+ "dependencies": {
7
+ "takumi-js": "^2.13.5",
8
+ "takumi-pdf": "^0.14.1"
9
+ }
10
+ }
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ // HTML -> image / SVG / paged PDF / animation, powered by Takumi (no browser).
3
+ // Self-contained CLI for agent skills: auto-installs takumi-js/takumi-pdf on
4
+ // first run, renders an HTML fragment file to the requested output format.
5
+ //
6
+ // Usage:
7
+ // node render.mjs --html card.html -o card.png --width 1200 --height 630
8
+ // node render.mjs --html badge.html -o badge.svg
9
+ // node render.mjs --html invoice.html --pdf -o invoice.pdf [--size a4] [--title "t"] [--footer '<div>...</div>']
10
+ // node render.mjs --html anim.html --animation webp -o anim.webp [--fps 30] [--duration 1200]
11
+ // node render.mjs --html card.html -o card.png --font /path/Font.ttf [--font ...] | --no-fonts
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { execFileSync } from 'child_process';
15
+ import { fileURLToPath } from 'url';
16
+ import { createRequire } from 'module';
17
+
18
+ const MIN_NODE = 20;
19
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
20
+ if (nodeMajor < MIN_NODE) {
21
+ console.error(`Error: Node.js >= ${MIN_NODE}.19 required, found ${process.versions.node}.`);
22
+ process.exit(1);
23
+ }
24
+
25
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
26
+ const require = createRequire(path.join(SCRIPT_DIR, 'package.json'));
27
+
28
+ function ensureDependencies() {
29
+ try {
30
+ require.resolve('takumi-js');
31
+ require.resolve('takumi-pdf');
32
+ return;
33
+ } catch { /* fall through to install */ }
34
+ console.error('First run: installing takumi-js and takumi-pdf (one-time, needs network)...');
35
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
36
+ execFileSync(npm, ['install', '--no-fund', '--no-audit', 'takumi-js', 'takumi-pdf'], {
37
+ cwd: SCRIPT_DIR, stdio: 'inherit', shell: process.platform === 'win32'
38
+ });
39
+ // Re-check after install so failures surface as a clear error.
40
+ try {
41
+ require.resolve('takumi-js');
42
+ require.resolve('takumi-pdf');
43
+ } catch {
44
+ error('dependency install finished but takumi-js/takumi-pdf still not resolvable; check network / npm registry access.');
45
+ }
46
+ }
47
+ ensureDependencies();
48
+
49
+ const { render, renderSvg, renderAnimation } = await import('takumi-js');
50
+ const { render: renderPdf } = await import('takumi-pdf');
51
+
52
+ // ---- args ----
53
+ const args = process.argv.slice(2);
54
+ const ALIASES = { output: ['o'] };
55
+ function opt(name) {
56
+ for (const key of [name, ...(ALIASES[name] || [])]) {
57
+ const token = key.length === 1 ? `-${key}` : `--${key}`;
58
+ const i = args.indexOf(token);
59
+ if (i !== -1) return args[i + 1];
60
+ }
61
+ return undefined;
62
+ }
63
+ function flag(name) {
64
+ return args.includes(`--${name}`);
65
+ }
66
+ function error(msg) {
67
+ console.error(`Error: ${msg}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ const htmlFile = opt('html');
72
+ const output = opt('output');
73
+ if (!htmlFile) error("'--html <file>' is required (an HTML fragment file to render).");
74
+ if (!output) error("'-o/--output <file>' is required.");
75
+ const template = fs.readFileSync(htmlFile, 'utf-8');
76
+ if (!template.trim()) error(`${htmlFile} is empty.`);
77
+
78
+ const cssFile = opt('css');
79
+ const css = cssFile ? fs.readFileSync(cssFile, 'utf-8') : undefined;
80
+
81
+ // ---- fonts: auto-detect common system fonts unless --no-fonts / --font given ----
82
+ const COMMON_FONT_PATHS = [
83
+ '/usr/share/fonts/noto/NotoSansCJK-Regular.ttc',
84
+ '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
85
+ '/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc',
86
+ '/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc',
87
+ '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
88
+ '/usr/share/fonts/noto/NotoColorEmoji.ttf',
89
+ '/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf',
90
+ '/usr/share/fonts/google-noto-emoji/NotoColorEmoji.ttf',
91
+ 'C:\\Windows\\Fonts\\msyh.ttc',
92
+ 'C:\\Windows\\Fonts\\simhei.ttf',
93
+ 'C:\\Windows\\Fonts\\arial.ttf',
94
+ 'C:\\Windows\\Fonts\\seguiemj.ttf',
95
+ '/System/Library/Fonts/PingFang.ttc',
96
+ '/System/Library/Fonts/Hiragino Sans GB.ttc',
97
+ '/System/Library/Fonts/Helvetica.ttc',
98
+ '/System/Library/Fonts/Apple Color Emoji.ttc',
99
+ ];
100
+
101
+ function resolveFonts() {
102
+ if (flag('no-fonts')) return [];
103
+ const fontOpts = args.flatMap((a, i) => (a === '--font' ? [args[i + 1]] : []));
104
+ const paths = fontOpts.length > 0 ? fontOpts : COMMON_FONT_PATHS.filter(p => fs.existsSync(p));
105
+ const fonts = [];
106
+ for (const p of paths) {
107
+ try {
108
+ fonts.push({ name: path.basename(p).replace(/\.(ttf|otf|ttc|woff2?)$/i, ''), data: fs.readFileSync(p) });
109
+ } catch { /* skip unreadable */ }
110
+ }
111
+ return fonts;
112
+ }
113
+ const fonts = resolveFonts();
114
+
115
+ const width = Number(opt('width')) || 1200;
116
+ const height = Number(opt('height')) || 630;
117
+ const quality = opt('quality') ? Number(opt('quality')) : undefined;
118
+
119
+ try {
120
+ fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
121
+
122
+ // ---- PDF mode ----
123
+ if (flag('pdf')) {
124
+ const options = { size: (opt('size') || 'a4').toLowerCase(), fonts };
125
+ if (css) options.css = css;
126
+ if (flag('landscape')) options.landscape = true;
127
+ if (opt('title')) options.metadata = { title: opt('title') };
128
+ if (opt('footer')) options.footer = opt('footer');
129
+ if (opt('header')) options.header = opt('header');
130
+ if (flag('outline')) options.outline = true;
131
+ const VALID_SIZES = ['a3', 'a4', 'a5', 'b4', 'b5', 'letter', 'legal', 'ledger'];
132
+ if (!VALID_SIZES.includes(options.size)) {
133
+ error(`invalid page size '${options.size}'. Supported: ${VALID_SIZES.join(', ')}.`);
134
+ }
135
+ const bytes = await renderPdf(template, options);
136
+ fs.writeFileSync(output, Buffer.from(bytes));
137
+ console.log(`OK PDF ${bytes.length} bytes -> ${output}`);
138
+ process.exit(0);
139
+ }
140
+
141
+ // ---- animation mode ----
142
+ const animFormat = opt('animation');
143
+ if (animFormat) {
144
+ const VALID_ANIM = ['webp', 'gif', 'apng'];
145
+ const fmt = animFormat.toLowerCase();
146
+ if (!VALID_ANIM.includes(fmt)) error(`invalid animation format '${animFormat}'. Supported: ${VALID_ANIM.join(', ')}.`);
147
+ const bytes = await renderAnimation({
148
+ width, height,
149
+ fps: Number(opt('fps')) || 30,
150
+ format: fmt,
151
+ scenes: [{ node: template, durationMs: Number(opt('duration')) || 1000 }],
152
+ fonts, ...(css ? { css } : {})
153
+ });
154
+ fs.writeFileSync(output, Buffer.from(bytes));
155
+ console.log(`OK animation ${fmt.toUpperCase()} ${bytes.length} bytes -> ${output}`);
156
+ process.exit(0);
157
+ }
158
+
159
+ // ---- static image / SVG mode ----
160
+ const format = (opt('format') || path.extname(output).replace('.', '') || 'png').toLowerCase();
161
+ if (format === 'svg') {
162
+ const svg = await renderSvg(template, { width, height, fonts, ...(css ? { css } : {}) });
163
+ fs.writeFileSync(output, svg, 'utf-8');
164
+ console.log(`OK SVG ${Buffer.byteLength(svg)} bytes -> ${output}`);
165
+ } else {
166
+ const VALID = ['png', 'jpeg', 'webp'];
167
+ if (!VALID.includes(format)) error(`invalid format '${format}'. Supported: png, jpeg, webp, svg (or use --pdf / --animation).`);
168
+ const options = { width, height, format, fonts, ...(css ? { css } : {}) };
169
+ if (quality != null && (format === 'jpeg' || format === 'webp')) options.quality = quality;
170
+ const bytes = await render(template, options);
171
+ fs.writeFileSync(output, Buffer.from(bytes));
172
+ console.log(`OK ${format.toUpperCase()} ${width}x${height} ${bytes.length} bytes -> ${output}`);
173
+ }
174
+ } catch (err) {
175
+ console.error(`Error rendering: ${err?.message || err}`);
176
+ process.exit(1);
177
+ }