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_ALT}} +
{{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_ALT}} +
{{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_ALT}} +
{{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 `${alt}` + } + return `${innerHtml}` }, }