diff --git a/.gitignore b/.gitignore
index 5e5ab1e8..e946f6b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -119,3 +119,8 @@ outputs/
# Ignore stray npm/yarn lockfiles so they are not committed by mistake.
package-lock.json
yarn.lock
+
+# Python bytecode caches generated when skill scripts (e.g. skills/*/scripts/*.py)
+# are executed. Never commit or sync these.
+__pycache__/
+*.pyc
diff --git a/mateclaw-server/pom.xml b/mateclaw-server/pom.xml
index e04bb74f..f25d542c 100644
--- a/mateclaw-server/pom.xml
+++ b/mateclaw-server/pom.xml
@@ -263,6 +263,19 @@
jsoup
+
+
+
+ com.github.binarywang
+ weixin-java-mp
+ 4.6.0
+
+
+
+
+
+ {{TITLE}}
+
+
+ {{AUTHOR}} · {{DATE}}
+
+
+ {{LEAD_PARAGRAPH}}
+
+
+ {{SECTION_HEADING}}
+
+
+ {{BODY_PARAGRAPH}}
+
+
+
+
+ {{IMAGE_CAPTION}}
+
+
+
+
+ {{PULL_QUOTE}}
+
+
+
+ {{SECTION_HEADING_2}}
+ {{BODY_PARAGRAPH_2}}
+
+
+
+
+
+
+ {{CTA_LINE}}
+ {{FOLLOW_LINE}}
+
+
+
diff --git a/mateclaw-server/src/main/resources/skills/gzh_article/references/gzh_layout_business.html b/mateclaw-server/src/main/resources/skills/gzh_article/references/gzh_layout_business.html
new file mode 100644
index 00000000..59086609
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/gzh_article/references/gzh_layout_business.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+ {{CATEGORY_LABEL}}
+ {{TITLE}}
+ {{AUTHOR}} · {{DATE}}
+
+
+
+ {{LEAD_PARAGRAPH}}
+
+
+
+
+
+ | 01 |
+ {{SECTION_HEADING}} |
+
+
+
+ {{BODY_PARAGRAPH}}
+
+
+
+ {{KEYPOINT_LABEL}}
+ {{KEYPOINT_TEXT}}
+
+
+
+
+
+
+ | 02 |
+ {{SECTION_HEADING_2}} |
+
+
+
+ {{BODY_PARAGRAPH_2}}
+
+
+
+
+ |
+ {{STAT_1_VALUE}}
+ {{STAT_1_LABEL}}
+ |
+
+ {{STAT_2_VALUE}}
+ {{STAT_2_LABEL}}
+ |
+
+ {{STAT_3_VALUE}}
+ {{STAT_3_LABEL}}
+ |
+
+
+
+
+
+
+ {{IMAGE_CAPTION}}
+
+
+
+
+ {{CTA_LINE}}
+ {{FOLLOW_LINE}}
+
+
+
diff --git a/mateclaw-server/src/main/resources/skills/gzh_article/references/gzh_layout_minimal.html b/mateclaw-server/src/main/resources/skills/gzh_article/references/gzh_layout_minimal.html
new file mode 100644
index 00000000..cb4579f6
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/gzh_article/references/gzh_layout_minimal.html
@@ -0,0 +1,59 @@
+
+
+
+
+ {{KICKER}}
+
+
+ {{TITLE}}
+
+
+ {{AUTHOR}} · {{DATE}}
+
+
+
+
+
+ {{LEAD_PARAGRAPH}}
+
+
+ {{SECTION_HEADING}}
+
+
+
+ {{BODY_PARAGRAPH}}
+
+
+
+
+ {{IMAGE_CAPTION}}
+
+
+
+
+ “
+ {{PULL_QUOTE}}
+
+
+
+ {{SECTION_HEADING_2}}
+
+ {{BODY_PARAGRAPH_2}}
+
+
+
+
+
+
+ {{CTA_LINE}}
+ {{FOLLOW_LINE}}
+
+
+
diff --git a/mateclaw-server/src/main/resources/skills/gzh_article/references/gzh_structure.md b/mateclaw-server/src/main/resources/skills/gzh_article/references/gzh_structure.md
new file mode 100644
index 00000000..94000322
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/gzh_article/references/gzh_structure.md
@@ -0,0 +1,54 @@
+# 公众号文章结构与钩子 / 金句方法论
+
+## 整体结构模板
+
+```
+┌─ 钩子引言(1–2 段)
+│ 抓注意力:具体场景 / 反常识数据 / 一个扎心的问题
+│
+├─ 小节 1(小标题)
+│ 论点 + 具体案例或数据 + 一句金句
+├─ 小节 2(小标题)
+│ ……
+├─ 小节 3–5(小标题)
+│ ……
+│
+└─ 结尾
+ 升华(克制,别喊口号)+ 行动号召 + 引导关注(signature_blocks)
+```
+
+## 钩子引言:4 种开法
+
+1. **场景代入**:"周五晚上十点,我还在改第 7 版方案。"
+2. **反常识数据**:"90% 的人以为自己在'深度工作',其实平均每 6 分钟就被打断一次。"
+3. **一个问题**:"你有没有过这种感觉——忙了一整天,却说不出自己干了啥?"
+4. **冲突 / 反转**:"我一直以为多任务是效率,直到它把我拖垮。"
+
+避免用"在……的今天""随着……的发展"这类套话开头(去 AI 化会直接扣分)。
+
+## 小标题写法
+
+- 每个小标题是一个**可独立成立的观点**,不是"背景 / 现状 / 对策"这种流水账标签。
+- 好:「先把大石头放进罐子」;差:「第一部分:方法概述」。
+- 3–5 个为宜,太多显得散。
+
+## 每节内容:论点 → 证据 → 金句
+
+- **论点**:一句话讲清这节要说什么。
+- **证据**:**具体**案例、数字、亲身经历、对话。抽象论断必须落地。
+- **金句**:一句可被读者截图 / 转发的话。金句要短、有画面或反差感。
+ - 例:"计划不是用来完成的,是用来对齐的。"
+
+## 结尾
+
+- 升华要**克制**——点到为止,别喊"让我们一起……"这类口号。
+- 明确的**行动号召**:让读者做一件具体的小事(留言 / 试一个方法 / 收藏)。
+- **引导关注**:用 `signature_blocks` 里既定的固定段,保持人设一致。
+
+## 金句仓库(结构示例,非套用)
+
+金句靠"具体 + 反差 + 短",不是靠华丽词藻。写作时现场生成,别硬塞不贴题的名言。
+
+- 反差型:「越想抓住时间,越留不住。」
+- 定义型:「所谓专业,就是把无聊的事重复做对。」
+- 行动型:「先完成,再完美。」
diff --git a/mateclaw-server/src/main/resources/skills/xhs_note/SKILL.md b/mateclaw-server/src/main/resources/skills/xhs_note/SKILL.md
new file mode 100644
index 00000000..69f2f025
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/xhs_note/SKILL.md
@@ -0,0 +1,113 @@
+---
+name: xhs_note
+description: '小红书图文创作 / 笔记 / 种草文案 (xiaohongshu / red note) — 端到端:成文→图文卡片(HTML→图)→去AI化→交付。标题四件套 + 碎句正文 + 话题标签,配 3:4 竖版卡片。honors user persona & style memory.'
+version: 1.0.0
+tags:
+- 小红书
+- 图文
+- 笔记
+- 内容创作
+- xiaohongshu
+platforms:
+ - macos
+ - linux
+ - windows
+---
+
+# 小红书图文创作
+
+把一个主题做成可直接发布的小红书笔记:文案 + 竖版图文卡片。
+
+## 开工前:读取共享人设记忆
+
+先用 `recall_structured` 取回并全程遵守:
+
+- `content_persona` — 人设 / 口吻
+- `writing_style_xhs` — 小红书文风
+- `topic_interests` — 选题方向
+- `banned_words` — 禁用 / 敏感词
+- `signature_blocks` — 固定开场 / 结尾段
+
+取不到就用中性默认,不要编造。
+
+## SOP
+
+### 1. 成文(小红书文案公式)
+
+**标题(≤20 字,四件套任选组合)**
+
+- **数字**:「30天」「省了800块」「3个动作」
+- **悬念**:「原来一直做错了……」
+- **情绪**:😭 😮💨 🤯 直给情绪
+- **对比 / 反转**:「从烂脸到裸妆出门」
+
+**正文**
+
+- 碎句、一句一断,用 emoji 分段。
+- 开头**痛点共鸣**,戳中读者才往下看。
+- 中段**干货清单**:可操作、具体、有数字。
+- 全程 `writing_style_xhs` + `content_persona`,大量第一人称和"姐妹们 / 你"。
+
+**话题标签(3–8 个)**
+
+大词 + 中词 + 长尾组合,例:`#护肤` `#敏感肌护肤` `#学生党平价护肤`。
+
+### 2. 图文卡片(HTML → 图)
+
+**卡片模板库**(竖版 3:4,挑选组合或据用户口味自创):
+- `references/xhs_card_cover.html` — 封面 / 大标题。
+- `references/xhs_card_content.html` — 干货清单。
+- `references/xhs_card_end.html` — 关注 CTA。
+- `references/xhs_card_quote.html` — 金句 / 大字引用卡。
+
+想要别的视觉风格时,直接生成一份新的自包含 HTML 卡片(可参考现有模板结构),不必局限于现成几款。`render_html_image` 渲染出的 PNG **本身就是预览**——先把图给用户看,满意再进入第 4 步打包。
+
+**填充占位符**:每个模板里有 `{{TITLE}}` `{{SUBTITLE}}` `{{POINTS}}` `{{CTA}}` 等占位 token。把第 1 步的文案填进去——`{{POINTS}}` 是清单,按模板注释里的格式(每条一个 `
`)注入。
+
+**渲染成图**:用 `render_html_image` 把填好的 HTML 渲染成 PNG。两种传法:
+
+- 写入临时文件后传 `filePath`:
+ ```
+ render_html_image(filePath="<填好的卡片.html>", filename="xhs_cover",
+ width=1080, height=1440, fullPage=false)
+ ```
+- 或直接内联传 `html="<填好的完整HTML字符串>"`,其余参数同上。
+
+竖版 3:4 用 `width=1080 height=1440`。`fullPage=false` 保证输出严格 3:4,不因内容溢出而拉长。
+
+**可选封面底图**:想要更精致的封面,可先用 `image_generate`(`aspectRatio=portrait`)生成一张背景图,再把其链接填进封面模板的背景占位处。
+
+### 3. 去 AI 化
+
+`load_skill deai_humanize`,对正文跑"打分→改写→复检"循环,`platform=xhs`,目标 `score ≤ 55`。小红书口吻要碎、要有情绪,别写成公众号。
+
+### 4. 交付
+
+用 `xhs_publish`(action=export)把**卡片图 + 正文 + 话题标签**打成一个 `.zip` 发布包,一键下载:
+
+```
+xhs_publish(action="export", title="<标题>", body="<正文>",
+ tags="标签1,标签2,标签3",
+ images="<封面卡的 render_html_image 下载链接>,<内容卡链接>,<结尾卡链接>")
+```
+
+`images` 按顺序传每张卡片的 `render_html_image` 返回链接(首图即封面)。工具会返回发布包下载链接 + 手动上传步骤。
+
+小红书**没有官方发布 API**:由用户下载后到创作平台手动上传完成发布,**不自动上传、不绕过任何风控/人机验证**。发布属于对外动作,必须用户明确同意。
+
+发布前对照 `banned_words` 扫一遍正文和标题,命中即标注替换。
+
+## 保存自定义卡片模板 / 对话升级技能
+
+用户满意某个自创卡片、想复用时,用 `skill_manage` 存成**自定义技能**(`builtin=false` 才能写):
+- `skill_manage(action="create", name="my_xhs_cards", content="<一份 SKILL.md>")`(首次)。
+- `skill_manage(action="write_file", name="my_xhs_cards", filePath="references/<卡片名>.html", content="")`。
+
+> 本技能 `xhs_note` 是内置技能、不能被直接编辑;自定义卡片一律存到用户自己的自定义技能里。写入都会过安全扫描。
+
+## 参考
+
+- `references/xhs_card_cover.html` — 封面大标题卡(bold hero)。
+- `references/xhs_card_content.html` — 干货清单卡(clean list)。
+- `references/xhs_card_end.html` — 关注引导卡(follow CTA)。
+- `references/xhs_card_quote.html` — 金句 / 大字引用卡。
diff --git a/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_content.html b/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_content.html
new file mode 100644
index 00000000..2aee097e
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_content.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+ {{TITLE}}
+ {{SUBTITLE}}
+
+
+
diff --git a/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_cover.html b/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_cover.html
new file mode 100644
index 00000000..7d5648d9
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_cover.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+ {{BADGE}}
+ {{TITLE}}
+ {{SUBTITLE}}
+
+
+
diff --git a/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_end.html b/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_end.html
new file mode 100644
index 00000000..43c3fceb
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_end.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+ 💛
+ {{TITLE}}
+ {{CTA}}
+ {{TAGS}}
+ {{HANDLE}}
+
+
diff --git a/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_quote.html b/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_quote.html
new file mode 100644
index 00000000..7ca8e612
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/xhs_note/references/xhs_card_quote.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+ {{TAG}}
+ “
+ {{QUOTE}}
+
+ {{SOURCE}}
+
+
+
diff --git a/mateclaw-ui/src/assets/main.css b/mateclaw-ui/src/assets/main.css
index 072bb1d6..29b22af7 100644
--- a/mateclaw-ui/src/assets/main.css
+++ b/mateclaw-ui/src/assets/main.css
@@ -401,6 +401,20 @@ html.dark body::before {
================================================================ */
.markdown-body { line-height: 1.75; }
+/* Inline preview for tool-generated images (render_html_image / image gen).
+ Rendered by useMarkdownRenderer.link(); click opens full-size in a new tab. */
+.markdown-body .markdown-generated-image {
+ display: block;
+ max-width: min(560px, 100%);
+ max-height: 640px;
+ height: auto;
+ margin: 12px 0;
+ border-radius: 10px;
+ border: 1px solid var(--mc-code-header-border);
+ object-fit: contain;
+ cursor: zoom-in;
+}
+
/* headings */
.markdown-body h1,
.markdown-body h2,
diff --git a/mateclaw-ui/src/composables/useGlobalFileDownloadClick.ts b/mateclaw-ui/src/composables/useGlobalFileDownloadClick.ts
index 9ed56ed1..6682f3f2 100644
--- a/mateclaw-ui/src/composables/useGlobalFileDownloadClick.ts
+++ b/mateclaw-ui/src/composables/useGlobalFileDownloadClick.ts
@@ -46,6 +46,21 @@ export function useGlobalFileDownloadClick() {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
const target = e.target as HTMLElement | null
if (!target) return
+
+ // Inline generated images (rendered by useMarkdownRenderer.link() as
+ //
) open full-size in a new tab on click, so the
+ // user can zoom without the picture ever becoming a download.
+ const genImg = target.closest('img[data-generated-image]')
+ if (genImg) {
+ const src = genImg.getAttribute('src')
+ if (src) {
+ e.preventDefault()
+ e.stopPropagation()
+ window.open(src, '_blank', 'noopener,noreferrer')
+ return
+ }
+ }
+
const anchor = target.closest('a[href]')
if (!anchor) return
diff --git a/mateclaw-ui/src/composables/useMarkdownRenderer.ts b/mateclaw-ui/src/composables/useMarkdownRenderer.ts
index c5914198..16fbba44 100644
--- a/mateclaw-ui/src/composables/useMarkdownRenderer.ts
+++ b/mateclaw-ui/src/composables/useMarkdownRenderer.ts
@@ -437,6 +437,23 @@ const customRenderer = {
// Malformed URL — treat as same-origin (relative link path).
}
const titleAttr = title ? ` title="${escapeHtml(title)}"` : ''
+
+ // Inline-preview tool-generated image files instead of showing a
+ // download-only link. render_html_image / image generation return
+ // `[cover.png](/api/v1/files/generated/)`; without this the chat only
+ // offers a download and the user can never *see* the picture. The
+ // generated-file endpoint is permitAll, so a same-origin
loads
+ // without an auth header. Detection is by the link label's extension
+ // (the URL itself carries only a UUID). Clicking the image opens it
+ // full-size in a new tab (see useGlobalFileDownloadClick).
+ const labelText = innerHtml.replace(/<[^>]*>/g, '').trim()
+ const isFileApi = /^\/api\/v1\/(files|chat\/files)\//.test(safeHref)
+ if (isFileApi && /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(labelText)) {
+ const alt = escapeHtml(labelText)
+ return `
`
+ }
+
return `${innerHtml}`
},
}