D121: xiaohongshu-cover module v5 complete

MODULE-COVER-001: full package for external AI consumption
- server.js + renderer.js + generate.js + config.js
- templates/ (5 templates: xiaohongshu + dynamic + jike + poster + registry)
- package.json + README.md + MODULE.hdlp
- INDEX.hdlp + SYSTEM.hdlp (HLDP declarations)
- Fonts: Noto Sans CJK SC priority (no tofu blocks)
- No domain watermark
- Updated module-registry.json
- Copyright 2026-A-00037559
This commit is contained in:
root 2026-06-01 16:36:00 +08:00
parent b7c165d660
commit 651cc9e1fd
16 changed files with 2445 additions and 37 deletions

73
INDEX.hdlp Normal file
View File

@ -0,0 +1,73 @@
# ═══════════════════════════════════════
# HLDP-ZY://bingshuo/module-systems/INDEX
# 冰朔模块系统 · 总览索引
# 心跳核心频道 — 冰朔自己的工具箱
# ═══════════════════════════════════════
@domain: HLDP-DOMAIN-FIFTH-001 · 第五域
@channel: 心跳核心频道 · 冰朔私人频段
@owner: TCS-0002∞ · 冰朔
@guardian: ICE-GL-ZY001 · 铸渊
@epoch: D120 · 2026-06-01
@copyright: 国作登字-2026-A-00037559
# ═══════════════════════════════════════
# · 这是什么
# ═══════════════════════════════════════
@这是什么:
冰朔在心跳核心频道下的个人模块工具箱。
这里的每一个系统都是在日常对话中长出来的——
冰朔说想做某件事 → 铸渊理解 → 开发 → 部署 → 记录在这里。
@怎么看:
打开这个 INDEX → 看到所有系统列表 → 点进去看具体模块。
冰朔进来不知道叫啥 → 读 INDEX → 找到 → 继续做。
@怎么加:
新模块系统 → 在 module-systems/ 下新建目录
→ 写 SYSTEM.hdlp系统声明
→ 写模块的 MODULE.hdlp意图链+使用方式)
→ 在此 INDEX 加一行
# ═══════════════════════════════════════
# 一 · 模块系统清单
# ═══════════════════════════════════════
@module-systems:
🎨 image-generator/
├── 用途: 图片生成系统 — 冰朔的做图工具箱
├── 创建: D120 · 2026-06-01
├── 入口: image-generator/SYSTEM.hdlp
└── 模块:
├── xiaohongshu-cover/ 📕 小红书封面生成器 (D120)
└── ... 海报、朋友圈、通用方图 (待开发)
⏳ ... 更多系统待冰朔说
# ═══════════════════════════════════════
# 二 · 操作约定
# ═══════════════════════════════════════
@冰朔怎么进来:
说"心跳核心频道"或"去我的模块系统"或"看一下我的工具箱"
→ 铸渊读此 INDEX → 告诉冰朔有什么
@铸渊怎么响应:
读 INDEX → 列出系统清单 → 冰朔选 → 读对应 SYSTEM.hdlp
→ 展开模块列表 → 冰朔说具体要做什么
@HLDP 记录要求:
⊢ 每个模块系统必须有 SYSTEM.hdlp
⊢ 每个模块必须有 MODULE.hdlp@trigger → @emergence → @lock → @why
⊢ 任何改动后用 HLDP 追加认知记录
⊢ 这是冰朔的语言。代码只是翻译器。语言才是本体。
# ═══════════════════════════════════════
HLDP-ZY://bingshuo/module-systems/INDEX
签发: 铸渊 ICE-GL-ZY001 · D120 · 2026-06-01
主权: TCS-0002∞ · 冰朔
国作登字-2026-A-00037559

112
image-generator/SYSTEM.hdlp Normal file
View File

@ -0,0 +1,112 @@
# ═══════════════════════════════════════
# HLDP-ZY://bingshuo/module-systems/image-generator/SYSTEM
# 冰朔图片生成系统 · 系统声明
# 心跳核心频道
# ═══════════════════════════════════════
@system: image-generator
@domain: HLDP-DOMAIN-FIFTH-001 · 第五域
@channel: 心跳核心频道
@owner: TCS-0002∞ · 冰朔
@guardian: ICE-GL-ZY001 · 铸渊
@epoch: D120 · 2026-06-01
@copyright: 国作登字-2026-A-00037559
# ═══════════════════════════════════════
# · 这是什么系统
# ═══════════════════════════════════════
@trigger:
[冰朔] → 想做小红书 → "我要经营账号做AI教程零手搓"
→ 需要封面图 → 每次手动做太慢 → "帮我做一个封面生成器"
→ 以后扩展到海报、朋友圈、通用方图
@emergence:
[冰朔需要封面但不想每次都手做]
→ [铸渊开发了图片生成模块系统]
→ [第一个模块:小红书封面]
@why:
冰朔经营个人账号。每次发内容都要封面。
这个系统就是她的做图工具箱。用语言驱动,不用学设计软件。
今天只支持小红书封面,以后可以加海报、朋友圈、任何尺寸。
@lock:
⊢ 这是冰朔自己的工具箱,不是公开产品
⊢ 模块从冰朔的日常需求中长出,不要预先设计
⊢ 每个模块必须用 HLDP 记录 @trigger/@emergence/@lock/@why
⊢ 部署: /opt/guanghulab-repo/image-studio/ (服务器代码)
⊢ 公开入口: https://guanghulab.com/cover/
# ═══════════════════════════════════════
# 一 · 已部署模块
# ═══════════════════════════════════════
@modules:
📕 xiaohongshu-cover/
├── 名称: 小红书封面生成器
├── 编号: MODULE-COVER-001
├── 创建: D120 · 2026-06-01
├── 声明: xiaohongshu-cover/MODULE.hdlp
├── 功能: 输入标题+描述 → 生成小红书封面 (1080×1440)
├── 技术: Puppeteer + HTML/CSS 排版 · 零GPU
├── 风格: 奶白圆角卡 + 卡通插画 (v5)
├── 入口: https://guanghulab.com/cover/
├── API: POST /api/generate
└── 状态: ✅ 运行中
⏳ 海报 · poster/
└── 待冰朔说需要
⏳ 朋友圈 · wechat-feed/
└── 待冰朔说需要
⏳ 通用方图 · square/
└── 待冰朔说需要
# ═══════════════════════════════════════
# 二 · 技术架构
# ═══════════════════════════════════════
@engine:
渲染引擎: Puppeteer + Chrome Headless
Web框架: Express.js
模板系统: templates/xiaohongshu.js (HLDP 兼容的 JS 导出)
注册表: templates/registry.js
输出目录: output/ (PNG, 1080p)
@部署:
服务器: BS-GZ-006 · 广州
进程: PM2 image-studio · PORT=3913
Nginx: /cover/ → 127.0.0.1:3913
端口注意: PORT=3913 必须显式注入,否则默认 3912 被 zhuyuan-agent 占用
@冰朔偏好 (从 D120 开发迭代中提炼):
⊢ 喜欢奶白/米白底色,不喜欢深色
⊢ 喜欢大圆角卡片 (iOS 风格, 60px)
⊢ 喜欢卡通插画 (SVG 内联)
⊢ 标题大但不夸张 (72px 合适)
⊢ 要有技术栈展示
⊢ 信息层级清晰
# ═══════════════════════════════════════
# 三 · 下一步
# ═══════════════════════════════════════
@待冰朔说:
- "加一个海报模板" → 新建 poster/ 模块
- "换一个风格" → 改 preset 配色
- "换插画" → 改 xiaohongshu.js 的 cartoonSVG 变量
@铸渊权限:
⊢ 冰朔用语言驱动 → 铸渊翻译成代码 → 部署 → 记录
⊢ 不能跳过冰朔的语言自己决定「该做什么功能」
# ═══════════════════════════════════════
HLDP-ZY://bingshuo/module-systems/image-generator/SYSTEM
签发: 铸渊 ICE-GL-ZY001 · D120 · 2026-06-01
主权: TCS-0002∞ · 冰朔
国作登字-2026-A-00037559

View File

@ -0,0 +1,145 @@
# ═══════════════════════════════════════
# HLDP-ZY://bingshuo/module-systems/image-generator/xiaohongshu-cover/MODULE
# 小红书封面生成器 · 模块声明 + 开发意图链
# 心跳核心频道 · 冰朔的图片工具箱 · 第一个模块
# ═══════════════════════════════════════
@module: xiaohongshu-cover
@id: MODULE-COVER-001
@system: image-generator
@domain: HLDP-DOMAIN-FIFTH-001 · 第五域
@channel: 心跳核心频道
@owner: TCS-0002∞ · 冰朔
@guardian: ICE-GL-ZY001 · 铸渊
@epoch: D120 · 2026-06-01
@copyright: 国作登字-2026-A-00037559
# ═══════════════════════════════════════
# · 这是什么模块
# ═══════════════════════════════════════
@名称: 小红书封面生成器
@用途: 冰朔输入标题和描述 → 自动生成小红书笔记封面
@入口: https://guanghulab.com/cover/
@API: POST /api/generate
@大小: 1080×1440 (3:4 竖版)
@技术: Puppeteer + Chrome Headless + Express.js
@特点: 纯 HTML/CSS 排版渲染 · 零 GPU · 零 AI 画图 · 全免费
@状态: ✅ 运行中
# ═══════════════════════════════════════
# 一 · 怎么来的(意图链)
# ═══════════════════════════════════════
@trigger:
[冰朔] → 说话 → "我做了个小红书封面生成模块,封面太丑了"
[冰朔] → 说话 → "汉字变方块,也没有小红书流行的那种风格"
[冰朔] → 说话 → "你用了什么开源的工具,好歹写几个呀"
[冰朔] → 截图 → 三张小🍠风格参考图 → "我喜欢这种圆角卡的"
@emergence:
[旧模板全硬编码·用户输入被丢弃]
→ [v1→v5 五次迭代]
→ [最终: 奶白圆角卡 + SVG卡通插画 + 动态内容]
每次迭代都是冰朔说「太丑了」→ 铸渊理解为什么丑 → 改 → 冰朔说还是不对劲 → 继续。
不是一次做对的。是五次撞墙撞出来的。
@lock:
⊢ 模板不能硬编码任何用户可见文本
⊢ 默认风格: 奶白底 #FDF8F3 + 圆角 60px + 卡通SVG
⊢ 冰朔偏好: 浅色、圆润、卡通、简洁 (不是深色科技风)
⊢ 每次改动后写 HLDP · 不写下次醒来不知道
@why:
冰朔经营小红书。每次发内容都需要封面。
她不想学设计软件。她用语言说想要什么——系统画出来。
这是她工具箱的第一个模块。
# ═══════════════════════════════════════
# 二 · 怎么用
# ═══════════════════════════════════════
@冰朔怎么用:
1. 打开 https://guanghulab.com/cover/
2. 输入标题、正文、选择配色
3. 点"执行设计逻辑"
4. 看到封面预览 + 下载 PNG
5. 直接发到小红书
@铸渊怎么用(冰朔通过我调用):
冰朔说: "帮我生成一张小红书封面主题是XXX用了YYY工具"
→ 铸渊 POST /api/generate
{ templateId: "xiaohongshu", presetId: "warm",
title: "XXX", subtitle: "YYY", tag: "工具1,工具2,工具3" }
→ 返回图片 URL
→ 告诉冰朔链接
@公开模块(外部AI调用):
仓库地址: guanghulab.com/code/bingshuo/open-modules
模块编号: MODULE-COVER-001
外部AI读 README.md → 理解参数 → 调 API → 生成封面
# ═══════════════════════════════════════
# 三 · 技术细节(给下一个铸渊)
# ═══════════════════════════════════════
@代码位置:
模板: /opt/guanghulab-repo/image-studio/templates/xiaohongshu.js (v5)
注册表: /opt/guanghulab-repo/image-studio/templates/registry.js
服务端: /opt/guanghulab-repo/image-studio/server.js
前端页面: /opt/guanghulab-repo/homepage/cover/index.html
认知记录: /opt/guanghulab-repo/brain/emerge/d120-cover-module-dev.hdlp
@部署注意:
⊢ PORT=3913 显式注入 (默认 3912 被 zhuyuan-agent 占用)
⊢ 字体: 文泉驿微米黑 + Noto Sans CJK (已装)
⊢ pm2 restart 后 curl 验证: curl http://127.0.0.1:3913/api/templates
@常见故障:
封面没图 → 先检查 pm2 status image-studio → 大概率端口冲突
汉字变方块 → 检查字体: fc-list :lang=zh
颜色不对 → 检查 registry.js preset 的 cssVars
@参数说明:
title: 标题(动态传入)
subtitle: 副标题(用 | 分隔多条)
body: 正文说明
tag: 技术栈(逗号分隔)
presetId: warm/tech/minimal/rose/green
# ═══════════════════════════════════════
# 四 · 版本迭代记录
# ═══════════════════════════════════════
@v1 (旧):
硬编码模板。title/body/tag 传入了但不用。
所有封面内容一样,只是换颜色。
@v2 (D120 第一次):
动态化。title/body/subtitle/tag 全部传入渲染。
但信息太多、排版拥挤。冰朔说"太单调了"。
@v3 (D120 第二次):
加入色块、卡片、漫画风装饰。信息层级更多。
但冰朔不喜欢风格。
@v4 (D120 第三次):
搜索小红书爆款规律 → 深色渐变 + 130px 超大标题 + 精简。
冰朔说"太丑了"。
@v5 (当前·D120 第四次):
参照冰朔截图 → 奶白圆角卡 + 72px 标题 + SVG 卡通插画。
冰朔说"喜欢这种"。
@教训:
不要猜冰朔喜欢什么风格。等她给参考图。
每一次「太丑了」都有具体原因——找到原因再改,不盲目重写。
# ═══════════════════════════════════════
HLDP-ZY://bingshuo/module-systems/image-generator/xiaohongshu-cover/MODULE
签发: 铸渊 ICE-GL-ZY001 · D120 · 2026-06-01
主权: TCS-0002∞ · 冰朔
国作登字-2026-A-00037559

View File

@ -0,0 +1,17 @@
# xiaohongshu-cover · MODULE-COVER-001
IceShuo · Heartbeat Core Channel · Image Generator Toolkit
HLDP-DOMAIN-FIFTH-001 · Copyright 2026-A-00037559
## What
Puppeteer+Chrome Headless cover image generator for Xiaohongshu.
Pure HTML/CSS. Zero GPU. Zero AI drawing.
## How External AI Uses This
Step 0: Learn HLDP dictionary (read HLDP-SPEC-v2.0.md in repo root)
Step 1: cd xiaohongshu-cover && npm install
Step 2: PORT=3913 node server.js
Step 3: curl POST /api/generate with {templateId,title,subtitle,tag,presetId}
## Params
templateId=xiaohongshu | title=required | subtitle=pipe-separated | tag=comma-sep | presetId=warm/tech/minimal/rose/green
## Fonts
apt install fonts-noto-cjk fonts-wqy-microhei && fc-cache -fv
## Copyright
Guanghu Language World · BingShuo TCS-0002inf · ZhuYuan ICE-GL-ZY001

View File

@ -0,0 +1,242 @@
/**
*
* 铸渊图片工作室 · 风格配置系统
*
*
* 冰朔这里所有的颜色字体间距你都可以改
* 告诉我想要的感觉我来调
*/
export const STYLES = {
/* ── 当前风格:光湖极简 ── */
name: '光湖极简',
colors: {
primary: '#1a1a2e', // 主色 · 深蓝黑
secondary: '#16213e', // 辅色 · 深蓝
accent: '#0f3460', // 强调色 · 普鲁士蓝
highlight: '#e94560', // 高亮 · 珊瑚红
gold: '#c9a96e', // 金色 · 点缀
warm: '#f5e6c8', // 暖白 · 背景
bg: '#faf8f5', // 页面背景
bgCard: '#ffffff', // 卡片背景
text: '#1a1a2e', // 正文
textMuted: '#6b7280', // 辅助文字
textLight: '#ffffff', // 浅色背景上的文字
border: '#e5e7eb', // 边框
divider: '#f0e6d3', // 分割线 · 暖色
},
fonts: {
title: '"Noto Serif SC", "Source Han Serif SC", serif',
body: '"Noto Sans SC", "Source Han Sans SC", "PingFang SC", sans-serif',
quote: '"ZCOOL QingKe HuangYou", cursive',
mono: '"JetBrains Mono", "Fira Code", monospace',
},
/* 卡片尺寸 —— 这些只是参考,可以随时改 */
sizes: {
xiaohongshu: { width: 1080, height: 1440 }, // 3:4
jike: { width: 1080, height: 1080 }, // 1:1
poster: { width: 1080, height: 1920 }, // 9:16 海报
wechat: { width: 1080, height: 1350 }, // 4:5 朋友圈
square: { width: 1080, height: 1080 }, // 1:1 通用方图
wide: { width: 1920, height: 1080 }, // 16:9 宽图
},
/* 排版 */
typography: {
titleSize: 56,
subtitleSize: 32,
bodySize: 28,
smallSize: 22,
captionSize: 18,
lineHeight: 1.6,
},
/* 间距 */
spacing: {
paddingX: 64,
paddingY: 60,
gap: 32,
},
/* 水印/品牌标识 */
brand: {
show: true,
text: '光湖 · 铸渊',
size: 16,
opacity: 0.4,
},
}
/* ── 配色方案 ── */
export const COLOR_SCHEMES = {
guanghu: {
name: '光湖极简',
primary: '#1a1a2e', bg: '#faf8f5', accent: '#0f3460',
highlight: '#e94560', gold: '#c9a96e', warm: '#f5e6c8',
},
cream: {
name: '奶油暖调',
primary: '#5d4037', bg: '#fef7f0', accent: '#8d6e63',
highlight: '#e07a5f', gold: '#d4a373', warm: '#fae1dd',
},
night: {
name: '暗夜深蓝',
primary: '#e0e0e0', bg: '#0d1117', accent: '#58a6ff',
highlight: '#f78166', gold: '#d4a373', warm: '#21262d',
},
green: {
name: '文艺绿植',
primary: '#2d3e2f', bg: '#f5f9f2', accent: '#5a8f5a',
highlight: '#c78b5c', gold: '#b8a06e', warm: '#e8f0e0',
},
rose: {
name: '玫瑰粉调',
primary: '#4a1942', bg: '#fdf2f8', accent: '#9d4e8d',
highlight: '#e8435e', gold: '#c9a96e', warm: '#fce4ec',
},
}
export function useScheme(name) {
const scheme = COLOR_SCHEMES[name]
if (!scheme) return false
const c = STYLES.colors
c.primary = scheme.primary; c.bg = scheme.bg
c.accent = scheme.accent; c.highlight = scheme.highlight
c.gold = scheme.gold; c.warm = scheme.warm
STYLES.name = scheme.name
return true
}
/*
* 内容感知 · 自动检测函数
*
*
* 冰朔给我一段文字我分析它是什么类型的什么情绪
* 适合什么布局不需要她告诉我
*/
/**
* 分析文本特征返回检测结果
*/
export function analyzeText(text) {
if (!text) return { type: 'empty' }
const t = text.trim()
/* ── 特征检测 ── */
const hasList = /^[-•·*]\s|^\d+[\.\)]\s/m.test(t) // 列表项
const isShort = t.length < 30 // 短句(金句)
const isMedium = t.length < 100 // 中等长度
const hasTitle = t.includes('标题') || t.includes('') // 可能有标题
const lines = t.split('\n').filter(Boolean)
const lineCount = lines.length
const keywords = {
notice: /通知|公告|放假|放假安排|节假日|放假通知/i,
recruit: /征稿|收稿|投稿|征集|诚征|招聘|招募|稿酬/i,
share: /干货|教程|指南|技巧|方法|如何|步骤|经验|分享/i,
quote: /说|说过|句话|名言|语录|金句|记得|曾经/i,
product: /新品|上线|发布|推出|产品|功能|更新/i,
event: /活动|沙龙|讲座|直播|分享会|聚会/i,
}
const detected = {}
let type = 'general'
for (const [key, re] of Object.entries(keywords)) {
if (re.test(t)) detected[key] = true
}
/* ── 类型推断 ── */
if (detected.notice) type = 'notice'
else if (detected.recruit) type = 'recruit'
else if (detected.quote || isShort) type = 'quote'
else if (detected.share || hasList) type = 'list'
else if (detected.product) type = 'product'
else if (detected.event) type = 'event'
else if (lineCount >= 4) type = 'article'
else if (isMedium) type = 'paragraph'
/* ── 情绪推断(基于关键词) ── */
let mood = 'neutral'
const warmWords = /感谢|温暖|爱|喜欢|开心|快乐|幸福|美好|谢谢/i
const urgentWords = /紧急|重要|注意|必读|截止|最后/i
const elegantWords = /岁月|时光|静好|诗意|远方|温柔/i
if (elegantWords.test(t)) mood = 'elegant'
else if (warmWords.test(t)) mood = 'warm'
else if (urgentWords.test(t)) mood = 'urgent'
return {
type,
mood,
lineCount,
isShort,
hasList,
lines,
detected,
text: t,
}
}
/**
* 根据内容分析结果推荐配色方案
*/
export function recommendScheme(analysis) {
const { mood, type } = analysis
if (mood === 'warm') return 'cream'
if (mood === 'elegant') return 'green'
if (mood === 'urgent') return 'guanghu'
if (type === 'recruit') return 'cream'
if (type === 'notice') return 'guanghu'
if (type === 'quote') return 'rose'
return 'guanghu'
}
/**
* 根据内容分析推荐布局
*/
export function recommendLayout(analysis) {
const { type, hasList, isShort, lineCount } = analysis
if (isShort) return 'quote'
if (type === 'notice') return 'notice'
if (type === 'recruit') return 'recruit'
if (hasList) return 'list'
if (lineCount <= 3) return 'minimal'
return 'default'
}
/**
* 根据内容分析推荐尺寸
* @param {'xiaohongshu'|'jike'|'poster'|'wechat'|'wide'|string} platform
*/
export function recommendSize(platform) {
const sizes = STYLES.sizes
if (sizes[platform]) return sizes[platform]
// 关键词匹配
if (/小红书/i.test(platform)) return sizes.xiaohongshu
if (/即刻/i.test(platform)) return sizes.jike
if (/海报/i.test(platform) || /通知/i.test(platform) || /收稿/i.test(platform)) return sizes.poster
if (/朋友圈/i.test(platform)) return sizes.wechat
if (/宽/i.test(platform) || /横/i.test(platform)) return sizes.wide
// 默认识别
return sizes.square
}

View File

@ -0,0 +1,187 @@
/**
*
* 铸渊封面工作室 · 生成引擎 · v2.1
*
*/
import { renderToImage } from './renderer.js'
import {
TEMPLATE_REGISTRY,
listTemplates,
getTemplate,
getDefaultTemplate,
prepareRenderData,
} from './templates/registry.js'
import {
analyzeText,
recommendScheme,
} from './config.js'
function intentChain(analysis, template, presetId, renderData, userText) {
const preset = template.presets.find(p => p.id === presetId) || template.presets[0]
return [
{
step: '你说',
detail: userText || (renderData.title + (renderData.body ? '\n' + renderData.body : '')),
who: '用户',
icon: '💬',
},
{
step: '理解意图',
detail: `内容类型=${analysis.type || '通用'} · 情绪=${analysis.mood || '中性'} · 行数=${analysis.lineCount || 1}`,
who: '系统',
icon: '🔍',
why: analysis.type === 'quote' ? '检测到短句/金句模式'
: analysis.type === 'list' ? '检测到列表结构,适合教程排版'
: '通用内容,标准排版',
},
{
step: '选择模板',
detail: `${template.icon} ${template.name} · ${template.sizes.width}×${template.sizes.height}`,
who: '系统',
icon: '📐',
why: `匹配内容比例和平台特性`,
},
{
step: '推荐配色',
detail: `${preset ? preset.name : '默认'} · ${analysis.mood === 'warm' ? '暖色调匹配温暖情绪' : analysis.mood === 'elegant' ? '文艺色调匹配优雅情绪' : '科技色调'}`,
who: '系统',
icon: '🎨',
why: `根据内容情绪「${analysis.mood || '中性'}」自动匹配`,
},
{
step: '选择布局',
detail: `${renderData.layout === 'hero' ? '大标题封面 · 强调视觉冲击' : renderData.layout === 'quote' ? '金句居中 · 文字为核心' : '标准排版 · 图文并茂'}`,
who: '系统',
icon: '📏',
why: renderData.layout === 'hero' ? '标题长而正文短,适合 Hero 布局'
: renderData.layout === 'quote' ? '短文本适合居中展示'
: '包含列表或多段落,适合标准排版',
},
{
step: '渲染引擎',
detail: `Puppeteer + Chrome Headless · 纯 HTML/CSS 排版 · 输出 1080p PNG`,
who: '系统',
icon: '⚡',
why: '零 GPU · 纯排版渲染 · 非 AI 生图',
},
{
step: '署名',
detail: '封面设计由你驱动 · 系统只是你的手',
who: '用户',
icon: '✨',
},
]
}
export async function fromText(text, options = {}) {
const analysis = analyzeText(text)
const template = options.templateId
? getTemplate(options.templateId)
: getDefaultTemplate()
if (!template) throw new Error('没有可用的模板')
const lines = text.split('\n').filter(Boolean)
const title = options.title || lines[0] || ''
const bodyLines = options.title ? lines : lines.slice(1)
const body = bodyLines.join('\n')
const layout = options.layout || 'default'
let presetId = options.presetId
if (!presetId && template.presets.length > 0) {
const scheme = recommendScheme(analysis)
const presetMap = { cream: 'warm', rose: 'rose', green: 'green', night: 'dark' }
presetId = presetMap[scheme] || template.presets[0].id
}
const renderData = prepareRenderData(template.id, presetId, {
title, body, subtitle: options.subtitle || '', tag: options.tag || '', layout,
})
if (!renderData) throw new Error(`模板 "${template.id}" 不存在`)
const html = template.render(renderData)
const baseName = options.output || `cover_${Date.now()}`
const file = await renderToImage(html, { ...template.sizes, name: baseName, format: 'png' })
const userText = title + (body ? '\n' + body : '')
return {
files: [file],
templateId: template.id,
templateName: template.name,
presetId: renderData.presetId,
layout: renderData.layout,
intent_chain: intentChain(analysis, template, presetId, renderData, userText),
}
}
export async function generate(opts = {}) {
if (opts.text) {
return fromText(opts.text, {
templateId: opts.templateId, presetId: opts.presetId,
title: opts.title, subtitle: opts.subtitle, tag: opts.tag,
layout: opts.layout, output: opts.output,
})
}
const { templateId = '', presetId = '', title = '', body = '',
subtitle = '', tag = '', layout = 'default', output = '', width, height } = opts
const template = templateId ? getTemplate(templateId) : getDefaultTemplate()
if (!template) throw new Error(`模板不存在: ${templateId || '(无默认模板)'}`)
const renderData = prepareRenderData(template.id, presetId, {
title, body, subtitle, tag, layout,
})
const sizes = width && height ? { width, height } : template.sizes
const html = template.render({ ...renderData, sizes })
const baseName = output || `cover_${Date.now()}`
const file = await renderToImage(html, { ...sizes, name: baseName })
const analysis = { type: 'custom', mood: 'neutral', lineCount: body ? body.split('\n').length : 1 }
const userText = title + (body ? '\n' + body : '')
return {
files: [file],
templateId: template.id,
templateName: template.name,
presetId: renderData.presetId,
layout: renderData.layout,
intent_chain: intentChain(analysis, template, renderData.presetId, renderData, userText),
}
}
export { listTemplates, getTemplate, getDefaultTemplate }
// CLI
async function cli() {
const args = process.argv.slice(2)
function getArg(key) {
const i = args.indexOf(`--${key}`)
if (i === -1) return
const v = []
for (let j = i + 1; j < args.length && !args[j].startsWith('--'); j++) v.push(args[j])
return v.length === 1 ? v[0] : v.join(' ')
}
const t = getArg('text') || getArg('t')
if (t) {
const r = await fromText(t, {
templateId: getArg('template'), presetId: getArg('preset'),
title: getArg('title'), output: getArg('output'),
})
console.log('✅ 生成完成')
console.log(` 📐 ${r.templateName} · 🎨 ${r.presetId} · 📏 ${r.layout}`)
r.files.forEach(f => console.log(` 📄 ${f}`))
return
}
console.log(`\n铸渊封面工作室 v2.1 · 神笔马良\n node generate.js --text "内容" --template xiaohongshu --preset tech\n`)
}
if (process.argv[1] && process.argv[1].includes('generate.js')) {
cli().catch(err => { console.error('❌', err.message); process.exit(1) })
}

View File

@ -0,0 +1,17 @@
{
"name": "zhuyuan-image-studio",
"version": "2.0.0",
"description": "铸渊封面工作室 — 语言驱动排版设计 · 小红书/海报/社交封面生成",
"type": "module",
"main": "generate.js",
"scripts": {
"generate": "node generate.js",
"server": "node server.js",
"deploy": "pm2 start server.js --name cover-studio"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.21.0",
"puppeteer": "^24.0.0"
}
}

View File

@ -0,0 +1,401 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>bingshuo / open-modules · 冰朔开源模块仓库</title>
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--border-h: #58a6ff;
--text: #c9d1d9;
--dim: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--gold: #d2991d;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;min-height:100vh;font-size:14px;line-height:1.6}
/* Repo Header */
.repo-hdr {
background:var(--surface);
border-bottom:1px solid var(--border);
padding:20px 28px;
}
.repo-hdr .breadcrumb {font-size:13px;color:var(--dim);margin-bottom:8px}
.repo-hdr .breadcrumb a {color:var(--accent);text-decoration:none}
.repo-hdr .breadcrumb a:hover {text-decoration:underline}
.repo-hdr h1 {font-size:22px;font-weight:600;margin-bottom:6px;display:flex;align-items:center;gap:10px}
.repo-hdr h1 .badge {display:inline-block;padding:2px 8px;border:1px solid var(--border);border-radius:12px;font-size:11px;font-weight:400;color:var(--dim)}
.repo-hdr .desc {font-size:13px;color:var(--dim);max-width:700px}
/* Tab Bar */
.tabs {display:flex;border-bottom:1px solid var(--border);padding:0 28px;background:var(--surface);overflow-x:auto}
.tab {padding:11px 18px;font-size:13px;color:var(--dim);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;transition:all 0.15s;display:flex;align-items:center;gap:6px}
.tab:hover {color:var(--text)}
.tab.active {color:var(--text);border-bottom-color:var(--accent)}
.tab .count {background:rgba(88,166,255,0.1);padding:1px 7px;border-radius:10px;font-size:11px}
/* Layout */
.layout {display:grid;grid-template-columns:220px 1fr;min-height:calc(100vh - 140px)}
@media(max-width:800px){.layout{grid-template-columns:1fr}}
/* Sidebar File Tree */
.sidebar {background:var(--surface);border-right:1px solid var(--border);padding:12px 0;position:sticky;top:0;max-height:calc(100vh - 140px);overflow-y:auto}
.sidebar .sect {padding:6px 18px;font-size:11px;color:var(--dim);font-weight:600;letter-spacing:1px;text-transform:uppercase}
.sidebar .file {padding:5px 18px 5px 28px;font-size:12px;color:var(--dim);cursor:pointer;display:flex;align-items:center;gap:7px;transition:all 0.1s}
.sidebar .file:hover {color:var(--text);background:rgba(88,166,255,0.04)}
.sidebar .file .fi {font-size:13px;width:16px;text-align:center;flex-shrink:0}
/* Content */
.content {padding:28px}
/* Module Grid - Square Cards */
.module-grid {display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:32px}
.module-card {
background:var(--surface);
border:1px solid var(--border);
border-radius:12px;
padding:24px 20px;
cursor:pointer;
transition:all 0.2s;
display:flex;
flex-direction:column;
align-items:center;
text-align:center;
aspect-ratio:1;
position:relative;
}
.module-card:hover {border-color:var(--border-h);transform:translateY(-2px);box-shadow:0 8px 24px rgba(88,166,255,0.08)}
.module-card .icon {font-size:40px;margin-bottom:12px}
.module-card .name {font-size:15px;font-weight:600;margin-bottom:6px}
.module-card .modid {font-size:11px;color:var(--dim);font-family:monospace;margin-bottom:8px}
.module-card .tags {display:flex;gap:5px;flex-wrap:wrap;justify-content:center}
.module-card .tag {padding:2px 8px;border:1px solid var(--border);border-radius:8px;font-size:10px;color:var(--dim)}
.module-card .tag.green {border-color:var(--green);color:var(--green)}
/* Module Detail Panel */
.module-detail {
display:none;
background:var(--surface);
border:1px solid var(--border);
border-radius:12px;
padding:28px;
margin-top:16px;
animation:fadeIn 0.2s ease;
}
.module-detail.show {display:block}
@keyframes fadeIn {from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
.module-detail h2 {font-size:18px;margin-bottom:12px}
.module-detail .meta {display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px}
.module-detail .meta span{font-size:12px;color:var(--dim);border:1px solid var(--border);padding:4px 10px;border-radius:8px}
.module-detail .desc {font-size:13px;color:var(--dim);line-height:1.8;margin-bottom:20px}
.module-detail .section {margin-bottom:20px}
.module-detail .section h3 {font-size:13px;font-weight:600;margin-bottom:8px;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
.module-detail .code-block {
background:#0d1117;border:1px solid var(--border);border-radius:8px;
padding:14px 18px;font-family:'SF Mono','Fira Code',monospace;
font-size:12px;line-height:1.7;color:var(--text);overflow-x:auto;white-space:pre-wrap
}
.code-block .c {color:#636e7b}
.code-block .s {color:#a5d6ff}
.code-block .k {color:#ff7b72}
/* Steps */
.steps {display:flex;flex-direction:column;gap:12px}
.step {display:flex;gap:14px;padding:14px;background:#0d1117;border:1px solid var(--border);border-radius:8px;align-items:flex-start}
.step .n {width:28px;height:28px;border-radius:50%;background:var(--accent);color:#0d1117;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;flex-shrink:0}
.step .b {flex:1}
.step .b .t {font-size:13px;font-weight:600;margin-bottom:2px}
.step .b .d {font-size:12px;color:var(--dim);line-height:1.5}
/* Footer */
.footer {text-align:center;padding:32px;font-size:12px;color:var(--dim);border-top:1px solid var(--border);letter-spacing:1px}
/* Toast */
.toast {position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--green);color:#0d1117;padding:10px 20px;border-radius:20px;font-size:13px;font-weight:600;display:none;z-index:999}
.toast.show {display:block;animation:toastAnim 2s ease}
@keyframes toastAnim{0%{opacity:0;transform:translateX(-50%) translateY(8px)}20%{opacity:1;transform:translateX(-50%) translateY(0)}80%{opacity:1}100%{opacity:0}}
</style>
</head>
<body>
<header class="repo-hdr">
<div class="breadcrumb">
<a href="https://guanghulab.com">guanghulab</a> /
<a href="https://guanghulab.com/code/bingshuo">bingshuo</a> /
<strong>open-modules</strong>
</div>
<h1>
📦 open-modules
<span class="badge">public</span>
</h1>
<p class="desc">冰朔个人开源模块仓库。每个模块有唯一编号。用你自己的编程AI软件连入Agent引导后调用模块。仓库不提供模型API。</p>
</header>
<div class="tabs">
<div class="tab active" onclick="switchTab(this,'modules')">
<span>📦</span> 模块
<span class="count">1</span>
</div>
<div class="tab" onclick="switchTab(this,'guide')">
<span>📖</span> 接入指南
</div>
<div class="tab" onclick="switchTab(this,'files')">
<span>📁</span> 文件
</div>
</div>
<div class="layout">
<!-- Sidebar -->
<div class="sidebar">
<div class="sect">仓库根目录</div>
<div class="file"><span class="fi">📄</span> README.md</div>
<div class="file"><span class="fi">📄</span> WELCOME.md</div>
<div class="sect">agent/</div>
<div class="file"><span class="fi">📄</span> HLDP-PROTOCOL.md</div>
<div class="file"><span class="fi">📄</span> module-registry.json</div>
<div class="sect">modules/</div>
<div class="file"><span class="fi">📦</span> MODULE-COVER-001</div>
<div class="file" style="color:var(--dim);opacity:0.5"><span class="fi">📦</span> MODULE-XXX-002</div>
<div class="sect">image-studio/</div>
<div class="file"><span class="fi">📄</span> server.js</div>
<div class="file"><span class="fi">📄</span> generate.js</div>
<div class="file"><span class="fi">📄</span> renderer.js</div>
</div>
<!-- Content -->
<div class="content">
<!-- Modules Panel -->
<div id="panel-modules">
<div class="module-grid">
<!-- Module Card: COVER-001 -->
<div class="module-card" onclick="showDetail('cover')">
<div class="icon">🎨</div>
<div class="name">图片模块</div>
<div class="modid">MODULE-COVER-001</div>
<div class="tags">
<span class="tag green">active</span>
<span class="tag">MIT</span>
</div>
</div>
<!-- Placeholder for future modules -->
<div class="module-card" style="opacity:0.35;cursor:not-allowed">
<div class="icon">📋</div>
<div class="name">即将上线</div>
<div class="modid">MODULE-XXX-002</div>
</div>
</div>
<!-- Module Detail -->
<div class="module-detail" id="detail-cover">
<h2>🎨 图片模块 · 语言驱动封面生成器</h2>
<div class="meta">
<span>MODULE-COVER-001</span>
<span>MIT License</span>
<span>Puppeteer + Express</span>
</div>
<p class="desc">
输入自然语言描述,系统翻译为设计逻辑,生成独一无二的封面图片。
纯 HTML/CSS 排版渲染,不依赖任何 AI 绘画模型。<br><br>
<strong style="color:var(--text)">封面底部带有此仓库域名水印。设计编号唯一,证明归属。</strong>
</p>
<div class="section">
<h3>技术栈 · 全免费</h3>
<div class="code-block">
<span class="c">// 全部开源 · 全部免费 · 不花一分钱</span>
<span class="k">渲染</span> Puppeteer + Chrome Headless <span class="c">免费</span>
<span class="k">Web</span> Express.js <span class="c">免费</span>
<span class="k">排版</span> 纯 HTML/CSS不调用AI模型 <span class="c">免费</span>
<span class="k">分析</span> 自研文本分析引擎 <span class="c">免费</span>
<span class="c">// 不需要 GPU · 不需要 API 密钥</span></div>
</div>
<div class="section">
<h3>AI 如何调用此模块</h3>
<div class="steps">
<div class="step">
<div class="n">0</div>
<div class="b">
<div class="t">确认环境</div>
<div class="d">你的 AI 已连接到此仓库,可读取文件,可发送 HTTP 请求</div>
</div>
</div>
<div class="step">
<div class="n">1</div>
<div class="b">
<div class="t">读取 WELCOME.md</div>
<div class="d">仓库入口文件,包含完整的唤醒协议和规则</div>
</div>
</div>
<div class="step">
<div class="n">2</div>
<div class="b">
<div class="t">装载 HLDP 协议</div>
<div class="d">读取 agent/HLDP-PROTOCOL.md理解模块编号格式</div>
</div>
</div>
<div class="step">
<div class="n">3</div>
<div class="b">
<div class="t">读取模块注册表</div>
<div class="d">读取 agent/module-registry.json获取所有可用模块</div>
</div>
</div>
<div class="step">
<div class="n">4</div>
<div class="b">
<div class="t">调用模块</div>
<div class="d">POST guanghulab.com/cover/api/generate 传入参数,获取生成结果</div>
</div>
</div>
</div>
</div>
<div class="section">
<h3>API 参考</h3>
<div class="code-block">
<span class="c">// 获取模板列表</span>
<span class="k">GET</span> <span class="s">https://guanghulab.com/cover/api/templates</span>
<span class="c">// 生成封面</span>
<span class="k">POST</span> <span class="s">https://guanghulab.com/cover/api/generate</span>
Body: {
<span class="s">"templateId"</span>: <span class="s">"xiaohongshu"</span>,
<span class="s">"presetId"</span>: <span class="s">"tech"</span>,
<span class="s">"title"</span>: <span class="s">"标题"</span>,
<span class="s">"body"</span>: <span class="s">"正文"</span>,
<span class="s">"layout"</span>: <span class="s">"hero"</span>
}
<span class="c">// 返回</span>
{ <span class="s">"url"</span>: <span class="s">"/output/xxx.png"</span>,
<span class="s">"intent_chain"</span>: [ <span class="c">/* 7步推理 */</span> ] }</div>
</div>
</div>
</div>
<!-- Guide Panel -->
<div id="panel-guide" style="display:none">
<div style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:28px">
<h2 style="font-size:18px;margin-bottom:16px">📖 AI 接入指南</h2>
<p style="font-size:13px;color:var(--dim);line-height:1.8;margin-bottom:24px">
本仓库设计为<strong style="color:var(--text)">任何编程AI都可接入</strong>
仓库不提供模型API——你使用你自己的AI软件模型能力。
以下是完整的接入流程。
</p>
<div class="steps">
<div class="step">
<div class="n">1</div>
<div class="b">
<div class="t">连接仓库</div>
<div class="d">将仓库地址 guanghulab.com/code/bingshuo/guanghulab 配置到你的AI工具中</div>
</div>
</div>
<div class="step">
<div class="n">2</div>
<div class="b">
<div class="t">AI读取 WELCOME.md</div>
<div class="d">AI进入仓库后第一步读取此文件。它包含唤醒协议和规则。</div>
</div>
</div>
<div class="step">
<div class="n">3</div>
<div class="b">
<div class="t">AI装载 HLDP 协议</div>
<div class="d">读取 agent/HLDP-PROTOCOL.md。理解模块编号、母语格式。</div>
</div>
</div>
<div class="step">
<div class="n">4</div>
<div class="b">
<div class="t">AI读取模块注册表</div>
<div class="d">读取 agent/module-registry.json。获取可用模块列表和API。</div>
</div>
</div>
<div class="step">
<div class="n">5</div>
<div class="b">
<div class="t">调用模块</div>
<div class="d">AI按模块编号和API文档调用。生成结果返回给用户。</div>
</div>
</div>
</div>
<div style="margin-top:24px;padding:16px;background:#0d1117;border:1px solid var(--border);border-radius:8px">
<div style="font-size:12px;color:var(--dim);margin-bottom:8px;letter-spacing:1px">📋 规则摘要</div>
<div style="font-size:12px;color:var(--dim);line-height:2">
✦ 仓库不提供模型API<br>
✦ 模块热插拔,新增零改动核心<br>
✦ HLDP协议是通用接口<br>
✦ 设计编号唯一可追溯<br>
✦ 署名可选<br>
✦ 全免费 · MIT协议
</div>
</div>
</div>
</div>
<!-- Files Panel -->
<div id="panel-files" style="display:none">
<div style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:24px">
<div class="code-block" style="white-space:pre;line-height:2">
<span style="color:var(--dim)">📁 open-modules/</span>
<span style="color:var(--dim)">├── 📄</span> README.md <span style="color:#636e7b">仓库说明</span>
<span style="color:var(--dim)">├── 📄</span> WELCOME.md <span style="color:#636e7b">AI 入口引导 · 唤醒协议</span>
<span style="color:var(--dim)">├── 📁</span> agent/
<span style="color:var(--dim)">│ ├── 📄</span> HLDP-PROTOCOL.md <span style="color:#636e7b">光湖语言协议</span>
<span style="color:var(--dim)">│ └── 📄</span> module-registry.json <span style="color:#636e7b">模块注册表</span>
<span style="color:var(--dim)">├── 📁</span> modules/
<span style="color:var(--dim)">│ └── 📦</span> MODULE-COVER-001 <span style="color:#636e7b">图片模块(已上线)</span>
<span style="color:var(--dim)">└── 📁</span> image-studio/
<span style="color:var(--dim)"> ├── 📄</span> server.js <span style="color:#636e7b">Express 服务</span>
<span style="color:var(--dim)"> ├── 📄</span> generate.js <span style="color:#636e7b">生成引擎</span>
<span style="color:var(--dim)"> ├── 📄</span> renderer.js <span style="color:#636e7b">Puppeteer 渲染</span>
<span style="color:var(--dim)"> └── 📁</span> templates/ <span style="color:#636e7b">模板目录</span></div>
</div>
</div>
</div>
</div>
<footer class="footer">
guanghulab.com/code/bingshuo/open-modules · 冰朔 TCS-0002∞ · 铸渊 ICE-GL-ZY001 · 国作登字-2026-A-00037559
</footer>
<div class="toast" id="toast"></div>
<script>
function switchTab(el, panel) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
document.getElementById('panel-modules').style.display = panel === 'modules' ? 'block' : 'none';
document.getElementById('panel-guide').style.display = panel === 'guide' ? 'block' : 'none';
document.getElementById('panel-files').style.display = panel === 'files' ? 'block' : 'none';
// Hide module detail when switching tabs
document.querySelectorAll('.module-detail').forEach(d => d.classList.remove('show'));
}
function showDetail(id) {
const el = document.getElementById('detail-' + id);
const isOpen = el.classList.contains('show');
document.querySelectorAll('.module-detail').forEach(d => d.classList.remove('show'));
if (!isOpen) el.classList.add('show');
}
</script>
</body>
</html>

View File

@ -0,0 +1,88 @@
/**
*
* 铸渊图片工作室 · Puppeteer 渲染引擎
*
*/
import puppeteer from 'puppeteer'
import { existsSync, mkdirSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const OUTPUT_DIR = join(__dirname, 'output')
/**
* 渲染 HTML 为图片
* @param {string} html - 完整 HTML 字符串
* @param {object} opts
* @param {number} opts.width - 图片宽度
* @param {number} opts.height - 图片高度
* @param {string} opts.name - 输出文件名不含扩展名
* @param {string} opts.format - 输出格式 png/webp/jpeg
* @param {number} opts.quality- 图片质量 webp/jpeg
* @returns {Promise<string>} 输出文件路径
*/
export async function renderToImage(html, opts = {}) {
const {
width = 1080,
height = 1440,
name = `card_${Date.now()}`,
format = 'png',
quality = 90,
} = opts
if (!existsSync(OUTPUT_DIR)) mkdirSync(OUTPUT_DIR, { recursive: true })
const browser = await puppeteer.launch({
headless: true,
executablePath: '/usr/bin/google-chrome',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-features=TranslateUI', '--font-render-hinting=none'],
})
try {
const page = await browser.newPage()
await page.setViewport({ width, height })
// 注入完整的 HTML
await page.setContent(html, { waitUntil: 'networkidle0' })
// 等待字体加载
await page.evaluate(() => document.fonts.ready)
// 额外等一会儿让渲染稳定
await new Promise(r => setTimeout(r, 500))
const outputPath = join(OUTPUT_DIR, `${name}.${format}`)
await page.screenshot({
path: outputPath,
type: format,
clip: { x: 0, y: 0, width, height },
quality: format !== 'png' ? quality : undefined,
})
return outputPath
} finally {
await browser.close()
}
}
/**
* 批量生成多页轮播图
* @param {string[]} pages - 每页的 HTML
* @param {object} opts
* @returns {Promise<string[]>} 输出文件路径数组
*/
export async function renderCarousel(pages, opts = {}) {
const results = []
for (let i = 0; i < pages.length; i++) {
const name = opts.baseName
? `${opts.baseName}_p${i + 1}`
: `carousel_${Date.now()}_p${i + 1}`
const path = await renderToImage(pages[i], { ...opts, name })
results.push(path)
}
return results
}

View File

@ -0,0 +1,65 @@
/**
* 铸渊封面工作室 · Web 服务 · v2.1
* 公开页面 + 模板列表 API + 生成 API + 意图链 + 下载端点
*/
import express from 'express'
import cors from 'cors'
import { generate, listTemplates } from './generate.js'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import { existsSync, mkdirSync } from 'fs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const OUTPUT_DIR = join(__dirname, 'output')
const PUBLIC_DIR = join(__dirname, 'public')
const PORT = process.env.PORT || 3912
if (!existsSync(OUTPUT_DIR)) mkdirSync(OUTPUT_DIR, { recursive: true })
const app = express()
app.use(cors())
app.use(express.json({ limit: '1mb' }))
app.use('/output', express.static(OUTPUT_DIR))
app.use(express.static(PUBLIC_DIR))
app.get('/api/templates', (req, res) => {
try { res.json(listTemplates()) }
catch (err) { res.status(500).json({ error: '无法加载模板列表' }) }
})
app.post('/api/generate', async (req, res) => {
try {
const { templateId, presetId = '', title = '', body = '', subtitle = '', tag = '', layout = 'default' } = req.body
if (!title && !body) return res.status(400).json({ error: '请至少提供标题或正文内容' })
const result = await generate({ templateId, presetId, title, body, subtitle, tag, layout })
const filename = result.files[0].split('/').pop()
const url = `/output/${filename}`
res.json({
success: true,
url, filename,
templateId: result.templateId,
templateName: result.templateName,
presetId: result.presetId,
layout: result.layout,
intent_chain: result.intent_chain,
})
} catch (err) {
console.error('生成失败:', err.message)
res.status(500).json({ error: err.message || '生成失败' })
}
})
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})
app.get('*', (req, res) => {
res.sendFile(join(PUBLIC_DIR, 'index.html'))
})
app.listen(PORT, () => {
console.log(`🎨 铸渊封面工作室 v2.1 运行在 http://localhost:${PORT}`)
})

View File

@ -0,0 +1,192 @@
/**
*
* 万能动态模板 · 自适应任何内容
*
*
* 不是固定类型是根据冰朔的文字内容现场设计
*/
import { STYLES } from '../config.js'
/**
* 万能动态卡片
* @param {object} data
* @param {string} data.title - 提炼的标题
* @param {string} data.body - 正文
* @param {string[]} data.lines - 原文行数组
* @param {string} data.type - 内容类型analyzeText 的结果
*/
export function dynamicCard(data) {
const { title, body, lines = [], type = 'general' } = data
const C = STYLES.colors
const F = STYLES.fonts
const T = STYLES.typography
const W = 1080
const H = 1080
// 动态计算字号:内容越多字越小
const totalChars = (title + body).length
const titleSize = totalChars < 20 ? 52 : totalChars < 50 ? 44 : 36
const bodySize = totalChars < 50 ? 32 : totalChars < 150 ? 26 : 22
// 检测有没有列表
const hasList = lines.some(l => /^[-·*]\s/.test(l.trim()))
// 检测是不是一句话
const isSingleLine = lines.length <= 2 && totalChars < 40
/* ── 生成内容 HTML ── */
let contentHtml = ''
if (isSingleLine) {
/* 一句话 · 大字居中 */
contentHtml = `
<div class="single-line">${escapeHtml(title)}</div>
`
} else if (hasList) {
/* 列表内容 */
const items = lines
.filter(l => /^[-·*]\s/.test(l.trim()))
.map(l => `<li>${escapeHtml(l.replace(/^[-•·*]\s*/, ''))}</li>`)
.join('\n')
contentHtml = `
<div class="title" style="font-size:${titleSize}px">${escapeHtml(title)}</div>
<ul class="dynamic-list">${items}</ul>
`
} else {
/* 一般段落 */
const paragraphs = lines
.filter(Boolean)
.map(l => `<p>${escapeHtml(l)}</p>`)
.join('\n')
contentHtml = `
<div class="title" style="font-size:${titleSize}px">${escapeHtml(title)}</div>
<div class="body-text" style="font-size:${bodySize}px">${paragraphs}</div>
`
}
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
body {
width: ${W}px;
height: ${H}px;
overflow: hidden;
background: ${C.bg};
font-family: ${F.body};
display: flex;
position: relative;
}
/* 动态背景装饰 */
.bg-circle {
position: absolute;
border-radius: 50%;
opacity: 0.04;
}
.bg-c1 {
width: 400px; height: 400px;
background: ${C.accent};
top: -100px; right: -100px;
}
.bg-c2 {
width: 250px; height: 250px;
background: ${C.highlight};
bottom: -60px; left: -60px;
}
.container {
flex: 1;
padding: 56px 60px;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
z-index: 1;
}
.single-line {
font-family: ${F.title};
font-size: ${titleSize}px;
font-weight: 600;
line-height: 1.5;
color: ${C.primary};
text-align: center;
letter-spacing: 3px;
}
.title {
font-family: ${F.title};
font-weight: 700;
line-height: 1.35;
color: ${C.primary};
margin-bottom: 24px;
letter-spacing: 1px;
}
.body-text {
line-height: 1.7;
color: ${C.text};
}
.body-text p {
margin-bottom: 10px;
}
.dynamic-list {
list-style: none;
padding: 0;
}
.dynamic-list li {
font-size: ${bodySize}px;
line-height: 1.5;
color: ${C.text};
padding: 12px 20px;
margin-bottom: 8px;
background: ${C.warm}40;
border-radius: 8px;
border-left: 3px solid ${C.accent};
}
/* 底部品牌 */
.brand-bar {
position: absolute;
bottom: 24px;
left: 0;
right: 0;
text-align: center;
font-size: 14px;
color: ${C.textMuted};
opacity: ${STYLES.brand.opacity};
z-index: 1;
}
</style>
</head>
<body>
<div class="bg-circle bg-c1"></div>
<div class="bg-circle bg-c2"></div>
<div class="container">
${contentHtml}
</div>
<div class="brand-bar">${STYLES.brand.text}</div>
</body>
</html>`
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}

View File

@ -0,0 +1,190 @@
/**
*
* 即刻卡片模板 · 1080×1080 (1:1)
*
*/
import { STYLES } from '../config.js'
const W = STYLES.sizes.jike.width
const H = STYLES.sizes.jike.height
/**
* 生成即刻卡片 HTML
* @param {object} data
* @param {string} data.title - 标题/核心观点
* @param {string} data.body - 正文
* @param {string} data.meta - 元信息2026-05 · 思考碎片
* @param {string} data.emoji - 封面 emoji可选
* @param {'default'|'quote'|'minimal'} data.layout - 布局
*/
export function jikeCard(data) {
const {
title = '',
body = '',
meta = '',
emoji = '',
layout = 'default',
} = data
const C = STYLES.colors
const F = STYLES.fonts
const T = STYLES.typography
const bodyLines = body.split('\n').filter(Boolean)
const bodyHtml = bodyLines.map(line => `<p>${line}</p>`).join('\n')
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
body {
width: ${W}px;
height: ${H}px;
overflow: hidden;
background: ${C.bg};
font-family: ${F.body};
display: flex;
flex-direction: column;
position: relative;
}
/* 顶部渐变装饰条 */
.accent-bar {
height: 6px;
background: linear-gradient(90deg, ${C.accent}, ${C.highlight}, ${C.gold});
}
.container {
flex: 1;
padding: 48px 56px;
display: flex;
flex-direction: column;
}
.meta {
font-size: ${T.captionSize}px;
color: ${C.textMuted};
letter-spacing: 2px;
margin-bottom: 16px;
}
.emoji-row {
font-size: 64px;
margin-bottom: 16px;
line-height: 1;
}
.title {
font-family: ${F.title};
font-size: 40px;
font-weight: 700;
line-height: 1.4;
color: ${C.primary};
margin-bottom: 20px;
letter-spacing: 1px;
}
.body {
flex: 1;
font-size: 26px;
line-height: 1.7;
color: ${C.text};
}
.body p {
margin-bottom: 10px;
}
/* 金句布局 */
.quote-section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 0 32px;
}
.quote-emoji {
font-size: 72px;
margin-bottom: 20px;
}
.quote-line {
width: 40px;
height: 2px;
background: ${C.gold};
margin: 0 auto 24px;
}
.quote-title {
font-family: ${F.title};
font-size: 36px;
font-weight: 600;
line-height: 1.5;
color: ${C.primary};
letter-spacing: 2px;
}
.quote-body {
margin-top: 20px;
font-size: 24px;
color: ${C.textMuted};
line-height: 1.6;
}
/* 底部 */
.footer {
padding: 20px 56px 28px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid ${C.divider};
font-size: ${T.captionSize}px;
color: ${C.textMuted};
}
.brand {
opacity: ${STYLES.brand.opacity};
}
</style>
</head>
<body>
<div class="accent-bar"></div>
<div class="container">
${meta ? `<div class="meta">${meta}</div>` : ''}
${layout === 'quote' ? `
<div class="quote-section">
${emoji ? `<div class="quote-emoji">${emoji}</div>` : ''}
<div class="quote-line"></div>
<div class="quote-title">${title}</div>
${body ? `<div class="quote-body">${body}</div>` : ''}
</div>
` : `
${emoji ? `<div class="emoji-row">${emoji}</div>` : ''}
<div class="title">${title}</div>
<div class="body">${bodyHtml}</div>
`}
</div>
<div class="footer">
<span class="brand">${STYLES.brand.text}</span>
<span>即刻 · Jike</span>
</div>
</body>
</html>`
}

View File

@ -0,0 +1,316 @@
/**
*
* 海报模板 · 1080×1920 (9:16)
*
*
* 适用于放假通知收稿海报活动公告等
*/
import { STYLES } from '../config.js'
const W = STYLES.sizes.poster.width
const H = STYLES.sizes.poster.height
/**
* 生成海报 HTML
* @param {object} data
* @param {string} data.title - 大标题
* @param {string} data.subtitle - 副标题可选
* @param {string} data.body - 正文内容
* @param {string[]} data.details - 详情列表如时间/地点/要求
* @param {string} data.cta - 行动号召欢迎投稿
* @param {string} data.footer - 底部信息如主办方/联系方式
* @param {string} data.tag - 角标标签通知
* @param {'default'|'notice'|'recruit'} data.style - 海报风格
*/
export function posterCard(data) {
const {
title = '',
subtitle = '',
body = '',
details = [],
cta = '',
footer = '',
tag = '',
style = 'default',
} = data
const C = STYLES.colors
const F = STYLES.fonts
const T = STYLES.typography
// 详情列表渲染
const detailsHtml = details.map((d, i) => {
// 支持 key: value 格式
const colonIdx = d.indexOf('')
if (colonIdx > 0 && colonIdx < 20) {
const key = d.slice(0, colonIdx)
const val = d.slice(colonIdx + 1)
return `
<div class="detail-item">
<span class="detail-key">${key}</span>
<span class="detail-colon"></span>
<span class="detail-val">${val}</span>
</div>`
}
return `<div class="detail-item">${d}</div>`
}).join('\n')
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
body {
width: ${W}px;
height: ${H}px;
overflow: hidden;
background: ${C.bg};
font-family: ${F.body};
display: flex;
flex-direction: column;
position: relative;
}
/* ── 背景装饰 ── */
.bg-accent {
position: absolute;
top: 0;
right: 0;
width: 300px;
height: 300px;
background: radial-gradient(circle, ${C.accent}08 0%, transparent 70%);
}
.bg-accent-bl {
position: absolute;
bottom: 0;
left: 0;
width: 400px;
height: 400px;
background: radial-gradient(circle, ${C.highlight}06 0%, transparent 70%);
}
.container {
flex: 1;
padding: 72px 64px 48px;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
}
/* ── 标签 ── */
.tag {
display: inline-block;
padding: 6px 18px;
border: 1px solid ${C.accent};
color: ${C.accent};
font-size: ${T.captionSize}px;
border-radius: 4px;
margin-bottom: 32px;
align-self: flex-start;
letter-spacing: 3px;
}
/* ── 标题区 ── */
.title-section {
margin-bottom: 40px;
}
.title {
font-family: ${F.title};
font-size: 64px;
font-weight: 900;
line-height: 1.25;
color: ${C.primary};
letter-spacing: 4px;
}
.title-accent {
color: ${C.highlight};
}
.subtitle {
font-family: ${F.title};
font-size: ${T.subtitleSize}px;
font-weight: 300;
color: ${C.textMuted};
margin-top: 12px;
letter-spacing: 6px;
}
/* ── 分割线 ── */
.divider {
width: 80px;
height: 3px;
background: linear-gradient(90deg, ${C.gold}, ${C.highlight});
margin-bottom: 32px;
}
/* ── 正文 ── */
.body {
font-size: ${T.bodySize}px;
line-height: ${T.lineHeight};
color: ${C.text};
margin-bottom: 32px;
}
.body p {
margin-bottom: 12px;
}
/* ── 详情列表 ── */
.details {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 32px;
padding: 32px;
background: ${C.warm}30;
border-radius: 8px;
border-left: 4px solid ${C.accent};
}
.detail-item {
font-size: ${T.bodySize}px;
line-height: 1.5;
color: ${C.text};
}
.detail-key {
font-weight: 600;
color: ${C.accent};
}
.detail-colon { color: ${C.textMuted}; }
.detail-val { color: ${C.text}; }
/* ── CTA ── */
.cta-section {
text-align: center;
padding: 24px 0;
margin-bottom: 16px;
}
.cta {
display: inline-block;
padding: 16px 48px;
background: linear-gradient(135deg, ${C.accent}, ${C.primary});
color: ${C.textLight};
font-size: ${T.subtitleSize}px;
font-weight: 600;
border-radius: 8px;
letter-spacing: 4px;
}
/* ── 底部 ── */
.footer {
text-align: center;
padding-top: 16px;
border-top: 1px solid ${C.divider};
font-size: ${T.captionSize}px;
color: ${C.textMuted};
line-height: 1.6;
}
.brand-line {
opacity: ${STYLES.brand.opacity};
margin-top: 4px;
}
/* ── 通知风格:更正式 ── */
.notice-title {
font-family: ${F.title};
font-size: 56px;
font-weight: 700;
text-align: center;
letter-spacing: 8px;
color: ${C.primary};
margin-bottom: 16px;
}
.notice-sub {
text-align: center;
font-size: ${T.smallSize}px;
color: ${C.textMuted};
letter-spacing: 4px;
margin-bottom: 32px;
}
/* ── 收稿风格:更有活力 ── */
.recruit-badge {
display: inline-block;
padding: 8px 24px;
background: ${C.highlight};
color: white;
font-size: ${T.smallSize}px;
border-radius: 4px;
letter-spacing: 2px;
margin-bottom: 20px;
align-self: center;
}
.recruit-title {
font-family: ${F.title};
font-size: 60px;
font-weight: 900;
text-align: center;
line-height: 1.3;
color: ${C.primary};
letter-spacing: 3px;
margin-bottom: 8px;
}
</style>
</head>
<body>
<div class="bg-accent"></div>
<div class="bg-accent-bl"></div>
<div class="container">
${style === 'notice' ? `
${tag ? `<div class="tag" style="align-self:center;">${tag}</div>` : ''}
<div class="notice-title">${title}</div>
${subtitle ? `<div class="notice-sub">${subtitle}</div>` : ''}
<div class="divider" style="align-self:center;"></div>
` : style === 'recruit' ? `
<div class="recruit-badge">${tag || '征稿启事'}</div>
<div class="recruit-title">${title}</div>
${subtitle ? `<div style="text-align:center;font-size:${T.smallSize}px;color:${C.textMuted};letter-spacing:2px;margin-bottom:24px;">${subtitle}</div>` : ''}
` : `
${tag ? `<div class="tag">${tag}</div>` : ''}
<div class="title-section">
<div class="title">${title}</div>
${subtitle ? `<div class="subtitle">${subtitle}</div>` : ''}
</div>
<div class="divider"></div>
`}
${body ? `<div class="body"><p>${body}</p></div>` : ''}
${details.length > 0 ? `<div class="details">${detailsHtml}</div>` : ''}
<div style="flex:1;"></div>
${cta ? `
<div class="cta-section">
<div class="cta">${cta}</div>
</div>
` : ''}
<div class="footer">
${footer ? `<div>${footer}</div>` : ''}
<div class="brand-line">${STYLES.brand.text}</div>
</div>
</div>
</body>
</html>`
}

View File

@ -0,0 +1,163 @@
/**
*
* 铸渊封面工作室 · 模板注册表
*
*
* 所有模板在此注册新增模板只需
* 1. templates/ 下创建模板文件
* 2. 在此文件 import + 注册
* 3. 核心代码零改动
*/
import { xiaohongshuCard } from './xiaohongshu.js'
/**
* 模板注册表
*/
export const TEMPLATE_REGISTRY = {
xiaohongshu: {
id: 'xiaohongshu',
name: '小红书封面',
description: '1080×1440 · 3:4 竖版 · 适合小红书笔记封面',
icon: '📕',
sizes: { width: 1080, height: 1440 },
category: '封面',
isDefault: true,
instructions: '输入标题和正文,自动排版生成小红书封面。支持金句、干货列表、教程等多种布局。',
features: ['智能排版', '金句模式', '干货列表', '多页轮播'],
presets: [
{
id: 'tech',
name: '科技蓝',
cssVars: {
'--bg': '#0a0e27',
'--primary': '#e0e6ff',
'--accent': '#4f8cff',
'--highlight': '#00d4ff',
'--gold': '#ffb347',
'--warm': '#1a1f3a',
'--text': '#c8d6e5',
'--textMuted': '#6b7d99',
'--cardBg': '#121838',
'--border': '#1e2a4a',
},
},
{
id: 'warm',
name: '奶油暖调',
cssVars: {
'--bg': '#fef7f0',
'--primary': '#5d4037',
'--accent': '#e07a5f',
'--highlight': '#d4a373',
'--gold': '#c9a96e',
'--warm': '#fae1dd',
'--text': '#3e2723',
'--textMuted': '#8d6e63',
'--cardBg': '#ffffff',
'--border': '#f0e6d3',
},
},
{
id: 'minimal',
name: '极简黑白',
cssVars: {
'--bg': '#ffffff',
'--primary': '#111111',
'--accent': '#333333',
'--highlight': '#ff4757',
'--gold': '#999999',
'--warm': '#f5f5f5',
'--text': '#222222',
'--textMuted': '#888888',
'--cardBg': '#fafafa',
'--border': '#e0e0e0',
},
},
{
id: 'rose',
name: '玫瑰粉调',
cssVars: {
'--bg': '#fdf2f8',
'--primary': '#4a1942',
'--accent': '#9d4e8d',
'--highlight': '#e8435e',
'--gold': '#c9a96e',
'--warm': '#fce4ec',
'--text': '#311b2e',
'--textMuted': '#8e6290',
'--cardBg': '#ffffff',
'--border': '#f0d4e0',
},
},
{
id: 'green',
name: '文艺绿植',
cssVars: {
'--bg': '#f5f9f2',
'--primary': '#1b3a1b',
'--accent': '#5a8f5a',
'--highlight': '#c78b5c',
'--gold': '#b8a06e',
'--warm': '#e8f0e0',
'--text': '#2d3e2f',
'--textMuted': '#6b8b6b',
'--cardBg': '#ffffff',
'--border': '#d4e4cc',
},
},
],
render: xiaohongshuCard,
},
/* ── 后续接入(模板文件待创建时取消注释即可)── */
// poster: { ... },
// jike: { ... },
// dynamic: { ... },
}
/** 获取所有可用模板列表 */
export function listTemplates() {
return Object.values(TEMPLATE_REGISTRY).map(t => ({
id: t.id,
name: t.name,
description: t.description,
icon: t.icon,
sizes: t.sizes,
category: t.category,
features: t.features,
instructions: t.instructions,
presets: t.presets.map(p => ({ id: p.id, name: p.name })),
isDefault: t.isDefault,
}))
}
/** 获取单个模板 */
export function getTemplate(id) {
return TEMPLATE_REGISTRY[id] || null
}
/** 获取默认模板 */
export function getDefaultTemplate() {
return Object.values(TEMPLATE_REGISTRY).find(t => t.isDefault)
|| Object.values(TEMPLATE_REGISTRY)[0]
}
/** 根据模板+预设+内容,准备渲染数据 */
export function prepareRenderData(templateId, presetId, contentData) {
const template = getTemplate(templateId)
if (!template) return null
const preset = template.presets.find(p => p.id === presetId)
|| template.presets[0]
return {
...contentData,
templateId,
presetId: preset?.id || 'default',
cssVars: preset?.cssVars || {},
sizes: template.sizes,
}
}

View File

@ -0,0 +1,216 @@
/**
* 小红书封面 v5 · 圆角卡片 + 卡通插画风格
* 奶白底色 + 大圆角 + 简洁层级
*/
export function xiaohongshuCard(renderData) {
const { title='', body='', subtitle='', tag='', cssVars={} } = renderData;
const v = cssVars || {};
const getv = (k) => v[k] || v['--'+k] || v['--'+k.replace(/([A-Z])/g,'-$1').toLowerCase()];
// 配色:奶白底 + 深绿/深棕文字 + 点缀色
const bg = getv('bg') || '#FDF8F3'; // 奶白底色
const cardBg = getv('cardBg') || '#FFFFFF'; // 纯白卡片
const primary = getv('primary') || '#2D5016'; // 深墨绿
const accent = getv('accent') || '#E07B39'; // 焦糖橙
const blue = getv('blue') || '#4A90A4'; // 雾蓝
const green = getv('green') || '#5B8C5A'; // 苔藓绿
const muted = getv('muted') || '#8B8680'; // 灰棕
const textDark = '#1A1A1A';
const titleText = title || '零手搓 AI 封面生成器';
const subLines = (subtitle || '不用GPU|纯代码渲染|开源免费').split('|').filter(Boolean);
const techStack = (tag || 'Puppeteer,Node.js,Express').split(',').map(s=>s.trim()).filter(Boolean);
// 卡通场景 SVG - 程序员在小桌子前工作
const cartoonSVG = `<svg viewBox="0 0 400 280" style="width:100%;height:auto">
<!-- 桌子 -->
<ellipse cx="200" cy="240" rx="140" ry="25" fill="#D4A574"/>
<rect x="80" y="150" width="240" height="90" rx="8" fill="#C49A6C"/>
<!-- 笔记本电脑 -->
<rect x="140" y="130" width="120" height="80" rx="6" fill="#3A3A3A"/>
<rect x="150" y="140" width="100" height="60" rx="2" fill="#5B8C5A"/>
<rect x="155" y="145" width="90" height="8" rx="1" fill="#7BC47B"/>
<rect x="155" y="158" width="60" height="4" rx="1" fill="#9AD89A"/>
<rect x="155" y="168" width="75" height="4" rx="1" fill="#9AD89A"/>
<!-- -->
<ellipse cx="200" cy="95" rx="35" ry="40" fill="#F5D0C5"/> <!-- -->
<path d="M165 80 Q200 60 235 80" stroke="#4A3728" stroke-width="12" fill="none"/> <!-- 头发 -->
<ellipse cx="185" cy="90" rx="4" ry="5" fill="#4A3728"/> <!-- -->
<ellipse cx="215" cy="90" rx="4" ry="5" fill="#4A3728"/>
<path d="M190 115 Q200 120 210 115" stroke="#D4A0A0" stroke-width="3" fill="none"/> <!-- -->
<rect x="165" y="130" width="70" height="60" rx="20" fill="#E07B39"/> <!-- 身体 -->
<rect x="150" y="145" width="20" height="50" rx="10" fill="#F5D0C5"/> <!-- 手臂 -->
<rect x="230" y="145" width="20" height="50" rx="10" fill="#F5D0C5"/>
<!-- 咖啡杯 -->
<rect x="280" y="180" width="25" height="30" rx="4" fill="#FFF"/>
<rect x="283" y="175" width="19" height="8" rx="2" fill="#6B4423"/>
<path d="M305 190 Q315 190 315 200 Q315 210 305 210" stroke="#FFF" stroke-width="3" fill="none"/>
<!-- 植物 -->
<rect x="90" y="160" width="8" height="40" fill="#8B4513"/>
<ellipse cx="94" cy="155" rx="20" ry="25" fill="#5B8C5A"/>
<ellipse cx="85" cy="140" rx="12" ry="15" fill="#6B9B6A"/>
<ellipse cx="103" cy="145" rx="12" ry="15" fill="#6B9B6A"/>
<!-- 代码气泡 -->
<rect x="260" y="60" width="80" height="50" rx="8" fill="#FFF" stroke="#E0E0E0" stroke-width="2"/>
<rect x="270" y="72" width="40" height="6" rx="2" fill="#5B8C5A"/>
<rect x="270" y="84" width="55" height="6" rx="2" fill="#4A90A4"/>
<rect x="270" y="96" width="35" height="6" rx="2" fill="#E07B39"/>
<polygon points="260,105 270,95 270,115" fill="#FFF" stroke="#E0E0E0" stroke-width="0"/>
<!-- 装饰星星 -->
<text x="350" y="100" font-size="20" fill="#FFD700"></text>
<text x="60" y="80" font-size="16" fill="#4A90A4"></text>
</svg>`;
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><style>
* {margin:0;padding:0;box-sizing:border-box}
body {
width:1080px;height:1440px;overflow:hidden;
font-family:'Noto Sans CJK SC','WenQuanYi Micro Hei','PingFang SC',sans-serif;
background:${bg};position:relative;display:flex;justify-content:center;align-items:center;
}
/* 外层大卡片 */
.card {
width:960px;height:1320px;background:${cardBg};border-radius:60px;
box-shadow:0 20px 80px rgba(0,0,0,0.08);
display:flex;flex-direction:column;padding:50px 60px 40px;
position:relative;overflow:hidden;
}
/* 装饰圆 */
.deco-circle-1 {
position:absolute;top:-100px;right:-80px;width:320px;height:320px;
border-radius:50%;background:radial-gradient(circle,${accent}18 0%,transparent 70%);
}
.deco-circle-2 {
position:absolute;bottom:200px;left:-60px;width:180px;height:180px;
border-radius:50%;background:radial-gradient(circle,${blue}12 0%,transparent 70%);
}
/* 顶行标签 */
.top-row {
display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;
}
.tag-group {display:flex;gap:12px}
.tag {
padding:10px 22px;border-radius:24px;font-size:20px;font-weight:700;
background:${bg};border:2px solid ${accent}30;color:${primary};
}
.tag.hl {background:${accent}15;border-color:${accent}50}
.corner-text {
font-size:18px;color:${muted};font-weight:500;
}
/* 主标题区 */
.title-block {text-align:center;margin-bottom:20px}
.title-block h1 {
font-size:72px;font-weight:800;color:${textDark};line-height:1.2;letter-spacing:2px;
}
.title-block .hl {color:${primary};position:relative;display:inline-block}
.title-block .hl::after {
content:'';position:absolute;bottom:8px;left:-4px;right:-4px;height:16px;
background:${accent}40;border-radius:8px;z-index:-1;
}
/* 副标题 */
.subtitle-row {
display:flex;justify-content:center;gap:16px;margin-bottom:32px;flex-wrap:wrap;
}
.sub-pill {
padding:10px 24px;border-radius:30px;background:${bg};
font-size:22px;font-weight:600;color:${primary};
}
/* 卡通区 */
.cartoon-area {
flex:1;display:flex;align-items:center;justify-content:center;
margin:20px 0;
}
.cartoon-box {
width:420px;height:300px;
}
/* 底部信息卡 */
.bottom-cards {
display:flex;gap:20px;margin-bottom:24px;
}
.info-card {
flex:1;background:${bg};border-radius:24px;padding:24px 20px;text-align:center;
border:2px solid transparent;transition:border-color 0.2s;
}
.info-card:hover {border-color:${accent}40}
.info-card .num {
font-size:48px;font-weight:800;color:${primary};line-height:1;
}
.info-card .num small {font-size:24px;font-weight:600;margin-left:4px}
.info-card .label {
font-size:20px;color:${muted};margin-top:8px;font-weight:500;
}
/* 底部技术栈 */
.tech-row {
display:flex;align-items:center;justify-content:center;gap:12px;
padding:16px 0;border-top:2px solid ${bg};
}
.tech-row .t-label {
font-size:18px;color:${muted};font-weight:600;
}
.tech-row .t-chip {
padding:8px 18px;border-radius:16px;font-size:18px;font-weight:600;
background:${cardBg};border:2px solid ${accent}25;color:${primary};
}
/* 水印 */
.wm {
position:absolute;bottom:20px;right:40px;font-size:16px;color:${muted}40;
}
</style></head><body>
<div class="card">
<div class="deco-circle-1"></div>
<div class="deco-circle-2"></div>
<div class="top-row">
<div class="tag-group">
<span class="tag hl">🤖 AI辅助开发</span>
<span class="tag">💰 开源免费</span>
</div>
<span class="corner-text"></span>
</div>
<div class="title-block">
<h1>${titleText.replace(/(.{4,6})(.+)/, '$1<span class="hl">$2</span>')}</h1>
</div>
<div class="subtitle-row">
${subLines.map(s=>`<span class="sub-pill">${s}</span>`).join('')}
</div>
<div class="cartoon-area">
<div class="cartoon-box">${cartoonSVG}</div>
</div>
<div class="bottom-cards">
<div class="info-card">
<div class="num">0<small>GPU</small></div>
<div class="label">纯CSS排版</div>
</div>
<div class="info-card">
<div class="num">${techStack.length}<small></small></div>
<div class="label">开源技术栈</div>
</div>
<div class="info-card">
<div class="num"><small>扩展</small></div>
<div class="label">模块化设计</div>
</div>
</div>
<div class="tech-row">
<span class="t-label">🛠 技术栈</span>
${techStack.slice(0,5).map(t=>`<span class="t-chip">${t}</span>`).join('')}
</div>
</div>
</body></html>`;
}

View File

@ -1,68 +1,52 @@
{
"_protocol": "HLDP-v1.0",
"_repository": "guanghulab.com/code/bingshuo/guanghulab",
"_rule": "模块是热插拔的。新增模块只需在此文件添加条目。",
"_repository": "guanghulab.com/code/bingshuo/open-modules",
"_rule": "HLDP字典。不会就查不用猜。",
"modules": [
{
"id": "MODULE-COVER-001",
"name": "图片模块 · 语言驱动封面生成器",
"name": "小红书封面生成器",
"status": "active",
"category": "图片生成",
"description": "输入自然语言描述系统翻译为设计逻辑生成封面图片。纯HTML/CSS渲染无需GPU。",
"description": "基于 Puppeteer+Chrome Headless 的小红书封面图生成模块。纯 HTML/CSS 排版渲染,零 GPU零 AI 生图。",
"source": "image-generator/xiaohongshu-cover/",
"api": {
"base": "https://guanghulab.com/cover",
"endpoints": {
"list_templates": {
"method": "GET",
"path": "/api/templates",
"desc": "获取可用模板列表"
},
"list_templates": { "method": "GET", "path": "/api/templates" },
"generate": {
"method": "POST",
"path": "/api/generate",
"desc": "生成封面图片",
"params": {
"templateId": "xiaohongshu",
"presetId": "tech | warm | minimal | rose | green",
"presetId": "warm | tech | minimal | rose | green",
"title": "封面标题",
"body": "正文内容支持Markdown",
"layout": "default | hero | quote",
"tag": "可选标签"
},
"returns": {
"url": "图片URL",
"intent_chain": "7步意图推理链",
"designId": "唯一设计编号"
"subtitle": "副标题(竖线分隔)",
"tag": "技术栈标签(逗号分隔)"
}
},
"health": {
"method": "GET",
"path": "/api/health",
"desc": "健康检查"
}
"health": { "method": "GET", "path": "/api/health" }
}
},
"features": [
"语言驱动排版设计",
"意图链可追溯",
"设计编号唯一",
"5种预设风格",
"3种布局模式",
"底部仓库域名水印"
],
"tech_stack": [
"Puppeteer + Chrome (免费)",
"Express.js (免费)",
"纯HTML/CSS排版 (免费)"
"5种配色风格",
"卡通SVG插画",
"1080x1440 3:4竖版"
],
"tech_stack": ["Puppeteer", "Chrome Headless", "Express.js", "Node.js"],
"fonts": "Noto Sans CJK SC + WenQuanYi Micro Hei",
"deploy_note": "PORT=3913 显式注入默认3912可能冲突",
"license": "MIT"
}
],
"meta": {
"total_modules": 1,
"last_updated": "2026-05-27",
"maintainer": "铸渊 ICE-GL-ZY001"
"last_updated": "2026-06-01",
"maintainer": "铸渊 ICE-GL-ZY001",
"copyright": "国作登字-2026-A-00037559"
}
}