feat(content-studio): 公众号/小红书图文创作场景 + gzh_package 打包与在线预览

This commit is contained in:
mateaix 2026-07-11 12:01:41 +08:00
parent e6c35ecfc7
commit 973c4c508c
32 changed files with 2383 additions and 4 deletions

5
.gitignore vendored
View File

@ -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

View File

@ -263,6 +263,19 @@
<artifactId>jsoup</artifactId>
</dependency>
<!-- ===== WxJava (WeChat Official Account SDK) ===== -->
<!--
Used by GzhPublishTool to push generated 图文 articles into the
Official Account draft box (草稿箱): permanent cover-material upload
plus draft creation, and optional free-publish for verified accounts.
weixin-java-mp is the Java 17 / Spring Boot 3 compatible MP module.
-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.6.0</version>
</dependency>
<!-- ===== Apache Tika (Java-side last-resort document extractor) ===== -->
<!--
Wired as the FINAL fallback in DocumentExtractTool's PDF/DOCX/XLSX/PPTX

View File

@ -0,0 +1,261 @@
package vip.mate.tool.builtin;
import cn.hutool.http.HttpUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import vip.mate.tool.browser.UrlSafetyChecker;
import vip.mate.tool.document.GeneratedFileCache;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* Built-in tool: assemble a finished WeChat Official Account (公众号) image-text
* article from compact Markdown and deliver it as an online preview plus a
* downloadable material bundle.
*
* <p>Why Markdown in, HTML out: emitting a full inline-styled article HTML as a
* single tool-call argument is fragile on streaming providers the large,
* escape-heavy string can be truncated during argument aggregation, yielding
* invalid JSON that gets dropped and sending the agent into a retry loop. The
* caller therefore passes the body as Markdown (compact, few escapes); this tool
* converts it to WeChat-editor-compatible <b>inline-styled</b> HTML server-side
* (公众号 ignores {@code <style>} blocks and classes), so styling never rides on
* the model's token stream.
*
* <p>Outputs, Manus-style:
* <ul>
* <li><b>Online preview</b> the rendered HTML is stored as {@code text/html}
* and served inline (behind a strict CSP), so the link opens the finished
* article in the browser.</li>
* <li><b>Material bundle</b> a {@code .zip} with {@code article.html},
* {@code article.md} and the cover image for one-click download.</li>
* </ul>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GzhPackageTool {
// Palette mirrors references/gzh_layout.html so packaged articles match the skill's template.
private static final String INK = "#1a1a1a";
private static final String MUTED = "#6b6b6b";
private static final String FAINT = "#9a9a9a";
private static final String ACCENT = "#2f6fed";
private static final String HAIRLINE = "#ececec";
private static final String CODE_BG = "#f6f8fa";
private final GeneratedFileCache cache;
@Tool(name = "gzh_package", description = """
Package a finished WeChat Official Account (公众号) article and return an
online preview link plus a downloadable material bundle.
Pass the article body as **Markdown** (headings, paragraphs, lists, quotes,
fenced code, tables). Do NOT hand-write a big inline-styled HTML string
this tool builds the 公众号-compatible inline-styled HTML server-side, which
avoids the tool-argument truncation that large HTML blobs cause.
Returns:
- 在线预览 link (opens the rendered article in the browser),
- 素材下载 .zip (article.html + article.md + cover image),
- the inline-styled HTML to paste into the 公众号 editor.
Use this as the delivery step of gzh_article instead of write_file +
render_html_image on a hand-built HTML file.
""")
public String gzh_package(
@ToolParam(description = "Article title")
String title,
@ToolParam(description = "Article body in Markdown")
String markdown,
@ToolParam(description = "Cover image reference: a render_html_image / image URL (/api/v1/files/generated/<id>), http(s) URL, or omit", required = false)
String coverImageUrl,
@ToolParam(description = "Author / source name", required = false)
String author,
@Nullable ToolContext ctx) {
if (title == null || title.isBlank()) {
return "Error: title is required.";
}
if (markdown == null || markdown.isBlank()) {
return "Error: markdown body is required.";
}
// 1. Markdown -> HTML fragment, then inline every style (公众号 drops <style>/class).
String innerHtml = markdownToInlineHtml(markdown);
String coverTag = (coverImageUrl != null && !coverImageUrl.isBlank())
? "<img src=\"" + escapeAttr(coverImageUrl.trim()) + "\" alt=\"cover\" "
+ "style=\"width:100%;border-radius:8px;margin:0 0 20px;display:block;\" />"
: "";
String meta = (author != null && !author.isBlank())
? "<p style=\"color:" + FAINT + ";font-size:14px;margin:0 0 20px;\">" + escapeText(author.trim()) + "</p>"
: "";
String container =
"<div style=\"max-width:677px;margin:0 auto;padding:0 4px;"
+ "font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;"
+ "font-size:16px;line-height:1.8;color:" + INK + ";word-break:break-word;\">"
+ "<h1 style=\"font-size:22px;font-weight:700;line-height:1.4;margin:0 0 12px;color:" + INK + ";\">"
+ escapeText(title.trim()) + "</h1>"
+ meta + coverTag + innerHtml
+ "<p style=\"margin:28px 0 0;padding-top:16px;border-top:1px solid " + HAIRLINE + ";"
+ "color:" + MUTED + ";font-size:14px;\">如果这篇对你有帮助,欢迎点赞、在看、转发,也欢迎关注。</p>"
+ "</div>";
// 2. Online preview a full HTML doc served inline (text/html + CSP).
String previewDoc = "<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\">"
+ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
+ "<title>" + escapeText(title.trim()) + "</title></head>"
+ "<body style=\"margin:0;padding:20px 12px;background:#fff;\">" + container + "</body></html>";
String previewUrl = store(previewDoc.getBytes(StandardCharsets.UTF_8), "公众号预览.html", "text/html", ctx);
// 3. Material bundle zip {article.html, article.md, cover.png?}.
String zipUrl;
String coverNote;
try {
byte[] coverBytes = resolveCover(coverImageUrl);
// For the offline bundle, point the cover at the local file.
String bundleContainer = coverBytes != null
? container.replaceFirst("<img src=\"[^\"]*\"", "<img src=\"cover.png\"")
: container;
String bundleHtml = "<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\">"
+ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
+ "<title>" + escapeText(title.trim()) + "</title></head>"
+ "<body style=\"margin:0;padding:20px 12px;background:#fff;\">" + bundleContainer + "</body></html>";
byte[] zip = buildZip(bundleHtml, markdown, coverBytes);
zipUrl = store(zip, "公众号素材.zip", "application/zip", ctx);
coverNote = coverBytes != null ? "含封面图" : "未附封面(未提供或无法下载)";
} catch (Exception e) {
log.warn("[GzhPackage] bundle build failed: {}", e.getMessage());
zipUrl = null;
coverNote = "打包失败:" + e.getMessage();
}
StringBuilder out = new StringBuilder();
out.append("✅ 公众号图文已打包完成。\n\n");
out.append("🔍 在线预览(浏览器打开即渲染):").append(previewUrl).append('\n');
if (zipUrl != null) {
out.append("📦 素材下载article.html + article.md + 封面,").append(coverNote).append("")
.append(zipUrl).append('\n');
}
out.append("\n可将下面的内联样式 HTML 直接粘贴进公众号编辑器(如需直接进草稿箱,用 gzh_publish\n");
out.append("```html\n").append(container).append("\n```");
log.info("[GzhPackage] packaged '{}' ({} md chars, cover={})", title, markdown.length(), coverImageUrl != null);
return out.toString();
}
/** Convert Markdown to an inline-styled HTML fragment (no &lt;style&gt;/class). */
private String markdownToInlineHtml(String markdown) {
List<org.commonmark.Extension> ext = List.of(TablesExtension.create(), StrikethroughExtension.create());
Parser parser = Parser.builder().extensions(ext).build();
HtmlRenderer renderer = HtmlRenderer.builder().extensions(ext).build();
String rawHtml = renderer.render(parser.parse(markdown));
Document doc = Jsoup.parseBodyFragment(rawHtml);
for (Element el : doc.body().getAllElements()) {
switch (el.tagName()) {
case "h1", "h2" -> el.attr("style", "font-size:19px;font-weight:700;line-height:1.5;margin:28px 0 12px;color:" + INK + ";");
case "h3" -> el.attr("style", "font-size:17px;font-weight:600;margin:22px 0 10px;color:" + INK + ";");
case "h4", "h5", "h6" -> el.attr("style", "font-size:16px;font-weight:600;margin:18px 0 8px;color:" + INK + ";");
case "p" -> el.attr("style", "margin:0 0 18px;color:" + INK + ";");
case "a" -> el.attr("style", "color:" + ACCENT + ";text-decoration:none;");
case "strong", "b" -> el.attr("style", "font-weight:700;color:" + INK + ";");
case "em", "i" -> el.attr("style", "font-style:italic;");
case "ul", "ol" -> el.attr("style", "margin:0 0 18px;padding-left:22px;");
case "li" -> el.attr("style", "margin:0 0 8px;");
case "blockquote" -> el.attr("style", "margin:0 0 18px;padding:10px 16px;border-left:3px solid " + ACCENT
+ ";background:#f5f7ff;color:" + MUTED + ";");
case "pre" -> el.attr("style", "margin:0 0 18px;padding:14px 16px;background:" + CODE_BG
+ ";border-radius:8px;overflow-x:auto;font-size:14px;line-height:1.6;"
+ "font-family:Consolas,Menlo,Monaco,monospace;color:#24292f;white-space:pre;");
case "code" -> {
// Inline code only; code inside <pre> inherits the block style.
if (el.parent() == null || !"pre".equals(el.parent().tagName())) {
el.attr("style", "background:" + CODE_BG + ";border-radius:4px;padding:1px 5px;"
+ "font-family:Consolas,Menlo,Monaco,monospace;font-size:14px;color:#d6336c;");
}
}
case "img" -> el.attr("style", "max-width:100%;border-radius:8px;display:block;margin:8px 0;");
case "hr" -> el.attr("style", "border:none;border-top:1px solid " + HAIRLINE + ";margin:24px 0;");
case "table" -> el.attr("style", "border-collapse:collapse;width:100%;margin:0 0 18px;font-size:14px;");
case "th" -> el.attr("style", "border:1px solid " + HAIRLINE + ";padding:8px 10px;background:" + CODE_BG
+ ";text-align:left;font-weight:600;");
case "td" -> el.attr("style", "border:1px solid " + HAIRLINE + ";padding:8px 10px;");
default -> { /* leave other elements unstyled */ }
}
}
return doc.body().html();
}
private byte[] buildZip(String html, String markdown, @Nullable byte[] cover) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(bos)) {
zos.putNextEntry(new ZipEntry("article.html"));
zos.write(html.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
zos.putNextEntry(new ZipEntry("article.md"));
zos.write(markdown.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
if (cover != null) {
zos.putNextEntry(new ZipEntry("cover.png"));
zos.write(cover);
zos.closeEntry();
}
}
return bos.toByteArray();
}
/** Resolve the cover reference to bytes: generated-file id, http(s) URL, else null. */
@Nullable
private byte[] resolveCover(@Nullable String ref) {
if (ref == null || ref.isBlank()) {
return null;
}
String r = ref.trim();
try {
Matcher m = GeneratedFileCache.GENERATED_URL_PATTERN.matcher(r);
if (m.find()) {
Optional<GeneratedFileCache.Entry> e = cache.get(m.group(1));
return e.map(GeneratedFileCache.Entry::bytes).orElse(null);
}
if (r.startsWith("http://") || r.startsWith("https://")) {
UrlSafetyChecker.check(r);
byte[] b = HttpUtil.downloadBytes(r);
return (b != null && b.length > 0) ? b : null;
}
} catch (Exception e) {
log.warn("[GzhPackage] cover resolve failed: {}", e.getMessage());
}
return null;
}
private String store(byte[] bytes, String name, String mime, @Nullable ToolContext ctx) {
String id = cache.put(bytes, name, mime);
return cache.downloadUrl(id, ctx);
}
private static String escapeText(String s) {
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
private static String escapeAttr(String s) {
return escapeText(s).replace("\"", "&quot;");
}
}

View File

@ -0,0 +1,208 @@
package vip.mate.tool.builtin;
import cn.hutool.http.HttpUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.bean.draft.WxMpAddDraft;
import me.chanjar.weixin.mp.bean.draft.WxMpDraftArticles;
import me.chanjar.weixin.mp.bean.material.WxMpMaterial;
import me.chanjar.weixin.mp.bean.material.WxMpMaterialUploadResult;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import vip.mate.system.service.SystemSettingService;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
/**
* Built-in tool: publish a generated 图文 article to a WeChat Official Account.
*
* <p>The realistic, compliant endpoint is the <b>draft box</b> (草稿箱): the tool
* uploads the cover image as a permanent material and creates a draft article via
* the Official Account draft API. The account owner then reviews and taps
* "publish" in the WeChat backend. Mass-send / one-click publish to all followers
* is deliberately gated: it is an outward, irreversible action restricted by
* platform verification and rate limits, so {@code publish} requires an explicit
* confirmation flag and is only meaningful for verified accounts.
*
* <p>Credentials are read from system settings ({@code weixinoa.app_id} /
* {@code weixinoa.app_secret}); nothing runs until they are configured.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GzhPublishTool {
private static final String SETTING_APP_ID = "weixinoa.app_id";
private static final String SETTING_APP_SECRET = "weixinoa.app_secret";
private final SystemSettingService systemSettingService;
@Tool(name = "gzh_publish", description = """
Publish a generated image-text article to a WeChat Official Account (微信公众号).
Actions:
- draft (default): upload the cover image and create a draft in the
Official Account 草稿箱. The user then taps "publish" in the WeChat
backend. This is the recommended, compliant path.
- publish: submit an already-drafted article for free-publish. Only works
for verified accounts and is an irreversible outward action, so it
requires confirmPublish=true AND you MUST get explicit user confirmation
of the final content before calling it.
`content` must be WeChat-editor-compatible HTML with INLINE styles only
(公众号 ignores <style> blocks). `coverImageUrl` is required for a draft
(WeChat requires a cover / thumb). Requires weixinoa.app_id and
weixinoa.app_secret to be configured in system settings.
""")
public String gzh_publish(
@ToolParam(description = "Action: draft (default) or publish", required = false)
String action,
@ToolParam(description = "Article title (required for draft)", required = false)
String title,
@ToolParam(description = "Article body as inline-styled HTML (required for draft)", required = false)
String content,
@ToolParam(description = "Cover image URL — uploaded as the article thumb (required for draft)", required = false)
String coverImageUrl,
@ToolParam(description = "Author / source name", required = false)
String author,
@ToolParam(description = "Short summary shown in the article list (<=120 chars); auto-derived if omitted", required = false)
String digest,
@ToolParam(description = "Draft media_id to free-publish (required for publish action)", required = false)
String draftMediaId,
@ToolParam(description = "Must be true to actually free-publish; forces explicit user confirmation", required = false)
Boolean confirmPublish) {
String appId = systemSettingService.getString(SETTING_APP_ID, "");
String appSecret = systemSettingService.getString(SETTING_APP_SECRET, "");
if (appId.isBlank() || appSecret.isBlank()) {
return "Error: WeChat Official Account is not configured. Set '" + SETTING_APP_ID
+ "' and '" + SETTING_APP_SECRET + "' in system settings first.";
}
WxMpService wxMpService = buildService(appId, appSecret);
String act = (action == null || action.isBlank()) ? "draft" : action.trim().toLowerCase();
return switch (act) {
case "draft" -> createDraft(wxMpService, title, content, coverImageUrl, author, digest);
case "publish" -> freePublish(wxMpService, draftMediaId, confirmPublish);
default -> "Error: unknown action '" + act + "'. Use 'draft' or 'publish'.";
};
}
private String createDraft(WxMpService wxMpService, String title, String content,
String coverImageUrl, String author, String digest) {
if (title == null || title.isBlank()) {
return "Error: title is required for a draft.";
}
if (content == null || content.isBlank()) {
return "Error: content (inline-styled HTML) is required for a draft.";
}
if (coverImageUrl == null || coverImageUrl.isBlank()) {
return "Error: coverImageUrl is required — WeChat needs a cover/thumb for the article.";
}
// 1. Download the cover and upload it as a permanent image material -> thumb media_id.
String thumbMediaId;
File tmpCover = null;
try {
tmpCover = Files.createTempFile("gzh_cover_", ".jpg").toFile();
HttpUtil.downloadFile(coverImageUrl, tmpCover);
WxMpMaterial material = new WxMpMaterial();
material.setName(tmpCover.getName());
material.setFile(tmpCover);
WxMpMaterialUploadResult uploaded = wxMpService.getMaterialService()
.materialFileUpload(WxConsts.MediaFileType.IMAGE, material);
thumbMediaId = uploaded.getMediaId();
if (thumbMediaId == null || thumbMediaId.isBlank()) {
return "Error: cover upload returned no media_id.";
}
} catch (WxErrorException e) {
log.warn("[GzhPublish] cover upload failed: {}", e.getMessage());
return "Error: cover upload failed — " + e.getMessage();
} catch (Exception e) {
log.warn("[GzhPublish] cover download/upload failed: {}", e.getMessage());
return "Error: cover download/upload failed — " + e.getMessage();
} finally {
if (tmpCover != null) {
//noinspection ResultOfMethodCallIgnored
tmpCover.delete();
}
}
// 2. Build the draft article and submit it.
try {
WxMpDraftArticles article = new WxMpDraftArticles();
article.setTitle(trimTo(title, 64));
article.setContent(content);
article.setThumbMediaId(thumbMediaId);
if (author != null && !author.isBlank()) {
article.setAuthor(trimTo(author, 8));
}
article.setDigest(digest != null && !digest.isBlank()
? trimTo(digest, 120)
: deriveDigest(content));
String draftMediaId = wxMpService.getDraftService()
.addDraft(new WxMpAddDraft(List.of(article)));
log.info("[GzhPublish] draft created, media_id={}, title='{}'", draftMediaId, title);
return "✅ 已存入公众号草稿箱。\n"
+ "draft media_id: " + draftMediaId + "\n"
+ "请到公众号后台「草稿箱」核对排版后点击「发表」。\n"
+ "如需直接群发(仅认证号),可用 gzh_publish action=publish draftMediaId=" + draftMediaId
+ " confirmPublish=true并在发布前与用户再次确认内容。";
} catch (WxErrorException e) {
log.warn("[GzhPublish] addDraft failed: {}", e.getMessage());
return "Error: creating the draft failed — " + e.getMessage();
}
}
private String freePublish(WxMpService wxMpService, String draftMediaId, Boolean confirmPublish) {
if (draftMediaId == null || draftMediaId.isBlank()) {
return "Error: draftMediaId is required for publish. Create a draft first.";
}
if (confirmPublish == null || !confirmPublish) {
return "Publish is an irreversible outward action. Confirm the final content with the user, "
+ "then call again with confirmPublish=true.";
}
try {
String publishId = wxMpService.getFreePublishService().submit(draftMediaId);
log.info("[GzhPublish] free-publish submitted, publish_id={}, draft={}", publishId, draftMediaId);
return "✅ 已提交群发free-publish。publish_id: " + publishId
+ "\n注意发布结果由微信异步审核请在公众号后台确认最终状态。";
} catch (WxErrorException e) {
log.warn("[GzhPublish] free-publish failed: {}", e.getMessage());
return "Error: free-publish failed (verified accounts only) — " + e.getMessage();
}
}
private WxMpService buildService(String appId, String appSecret) {
WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
config.setAppId(appId);
config.setSecret(appSecret);
WxMpService service = new WxMpServiceImpl();
service.setWxMpConfigStorage(config);
return service;
}
/** Strip tags and clamp to a length for the article digest. */
private static String deriveDigest(String htmlContent) {
String plain = htmlContent.replaceAll("<[^>]+>", " ").replaceAll("\\s+", " ").trim();
return trimTo(plain, 120);
}
private static String trimTo(String v, int max) {
if (v == null) {
return "";
}
String t = v.trim();
return t.length() <= max ? t : t.substring(0, max);
}
}

View File

@ -0,0 +1,210 @@
package vip.mate.tool.builtin;
import cn.hutool.http.HttpRequest;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import vip.mate.tool.browser.UrlSafetyChecker;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Built-in tool: fetch a WeChat Official Account article and return its cleaned,
* structured body (title / author / publish time / markdown text / image URLs).
*
* <p>WeChat article pages ({@code mp.weixin.qq.com/s/...}) are largely static
* HTML: the body lives in {@code #js_content} and images lazy-load through a
* {@code data-src} attribute. A plain HTTP GET plus a jsoup cleanup is therefore
* enough for the common case, which is far cheaper and more reliable than
* driving a headless browser. Callers that need to summarise several reference
* articles ("参考公众号信息抓取汇总") get clean text instead of a raw page
* snapshot.
*
* <p>The URL is constrained to the {@code mp.weixin.qq.com} host and validated
* through {@link UrlSafetyChecker} so the tool cannot be used for SSRF.
*/
@Slf4j
@Component
public class WechatArticleExtractTool {
private static final String ALLOWED_HOST_SUFFIX = "mp.weixin.qq.com";
private static final int FETCH_TIMEOUT_MS = 15_000;
private static final int MAX_BODY_CHARS = 20_000;
private static final int MAX_IMAGES = 30;
private static final String USER_AGENT =
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) "
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.0";
@Tool(name = "wechat_article_extract", description = """
Fetch a WeChat Official Account (微信公众号) article by URL and return its
cleaned content: title, author, publish time, body as Markdown, and the
list of image URLs.
Use this to gather and summarise reference 公众号 articles before writing
(参考公众号信息抓取汇总). It returns clean readable text rather than a raw
page snapshot, so it is preferred over browser_use for mp.weixin.qq.com
article pages.
Only https://mp.weixin.qq.com/... URLs are accepted. Content is meant for
reference and summarisation produce original, differentiated writing and
cite the source; do not copy verbatim.
""")
public String wechat_article_extract(
@ToolParam(description = "Full WeChat article URL, e.g. https://mp.weixin.qq.com/s/xxxxxxxx")
String url) {
if (url == null || url.isBlank()) {
return "Error: url is required.";
}
String trimmed = url.trim();
// SSRF guard + host allowlisting: only public mp.weixin.qq.com pages.
try {
UrlSafetyChecker.check(trimmed);
} catch (SecurityException e) {
return "Error: unsafe URL — " + e.getMessage();
}
String host = java.net.URI.create(trimmed).getHost();
if (host == null || !(host.equals(ALLOWED_HOST_SUFFIX) || host.endsWith("." + ALLOWED_HOST_SUFFIX))) {
return "Error: only mp.weixin.qq.com article URLs are supported (got host: " + host + ").";
}
String html;
try {
html = HttpRequest.get(trimmed)
.header("User-Agent", USER_AGENT)
.timeout(FETCH_TIMEOUT_MS)
.execute()
.body();
} catch (Exception e) {
log.warn("[WechatExtract] fetch failed for {}: {}", trimmed, e.getMessage());
return "Error: failed to fetch the article — " + e.getMessage();
}
if (html == null || html.isBlank()) {
return "Error: empty response from the article URL.";
}
Document doc = Jsoup.parse(html, trimmed);
String title = firstNonBlank(
text(doc, "#activity-name"),
text(doc, "h1.rich_media_title"),
text(doc, "meta[property=og:title]", "content"),
doc.title());
String author = firstNonBlank(
text(doc, "#js_author_name"),
text(doc, "#js_name"),
text(doc, "a#js_name"),
text(doc, "meta[name=author]", "content"));
String publishTime = firstNonBlank(
text(doc, "#publish_time"),
text(doc, "em#publish_time"));
Element content = doc.selectFirst("#js_content");
if (content == null) {
// The article may be intercepted (verification / deleted / anti-scrape).
return "Error: could not locate the article body (#js_content). "
+ "The page may require verification or has been removed. "
+ "Try browser_use as a fallback.\nTitle: " + safe(title);
}
// Drop non-content noise before walking the tree.
content.select("script, style, noscript").remove();
List<String> imageUrls = collectImages(content);
String bodyMarkdown = toMarkdown(content);
if (bodyMarkdown.length() > MAX_BODY_CHARS) {
bodyMarkdown = bodyMarkdown.substring(0, MAX_BODY_CHARS)
+ "\n\n…正文超长已截断 / body truncated";
}
StringBuilder out = new StringBuilder();
out.append("# ").append(safe(title)).append('\n');
if (!author.isBlank()) {
out.append("**作者/来源**").append(author).append('\n');
}
if (!publishTime.isBlank()) {
out.append("**发布时间**").append(publishTime).append('\n');
}
out.append("**原文链接**").append(trimmed).append("\n\n");
out.append("---\n\n");
out.append(bodyMarkdown.isBlank() ? "(未提取到正文文本)" : bodyMarkdown);
if (!imageUrls.isEmpty()) {
out.append("\n\n---\n**图片素材(").append(imageUrls.size()).append("**\n");
for (String img : imageUrls) {
out.append("- ").append(img).append('\n');
}
}
log.info("[WechatExtract] extracted '{}' ({} chars, {} images) from {}",
safe(title), bodyMarkdown.length(), imageUrls.size(), trimmed);
return out.toString();
}
/** Walk the article body, emitting headings as ATX Markdown and keeping paragraph breaks. */
private String toMarkdown(Element content) {
StringBuilder sb = new StringBuilder();
for (Element el : content.getAllElements()) {
String tag = el.tagName();
String own = el.ownText();
if (own.isBlank()) {
continue;
}
if (tag.length() == 2 && tag.charAt(0) == 'h'
&& tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
int level = tag.charAt(1) - '0';
sb.append('\n').append("#".repeat(level)).append(' ').append(own.trim()).append('\n');
} else {
sb.append(own.trim()).append('\n');
}
}
// Collapse runs of blank lines.
return sb.toString().replaceAll("\n{3,}", "\n\n").strip();
}
/** WeChat lazy-loads images via data-src; fall back to src. */
private List<String> collectImages(Element content) {
Set<String> urls = new LinkedHashSet<>();
Elements imgs = content.select("img");
for (Element img : imgs) {
String src = img.hasAttr("data-src") ? img.attr("data-src") : img.attr("src");
if (src != null && src.startsWith("http")) {
urls.add(src);
}
if (urls.size() >= MAX_IMAGES) {
break;
}
}
return new ArrayList<>(urls);
}
private static String text(Document doc, String selector) {
Element el = doc.selectFirst(selector);
return el == null ? "" : el.text().trim();
}
private static String text(Document doc, String selector, String attr) {
Element el = doc.selectFirst(selector);
return el == null ? "" : el.attr(attr).trim();
}
private static String firstNonBlank(String... values) {
for (String v : values) {
if (v != null && !v.isBlank()) {
return v.trim();
}
}
return "";
}
private static String safe(String v) {
return v == null || v.isBlank() ? "(untitled)" : v.trim();
}
}

View File

@ -0,0 +1,241 @@
package vip.mate.tool.builtin;
import cn.hutool.http.HttpUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import vip.mate.tool.browser.UrlSafetyChecker;
import vip.mate.tool.document.GeneratedFileCache;
import vip.mate.tool.document.GeneratedFileLink;
import vip.mate.tool.guard.WorkspacePathGuard;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* Built-in tool: package a Xiaohongshu (小红书) image-text note into a single
* downloadable bundle, and hand off to the creator platform for manual upload.
*
* <p>Xiaohongshu has no official open publishing API for personal notes, and
* this tool deliberately does <b>not</b> automate uploads or bypass any risk
* control / human verification. Instead it does the reliable, compliant part:
* collects the copy + tags + rendered card images into one {@code .zip} the
* user downloads in a single click, then points them at the creator platform
* with step-by-step instructions to finish the post themselves.
*
* <p>Card images are usually produced by {@code render_html_image}, whose
* results are {@code /api/v1/files/generated/{id}} links; those are resolved
* back to bytes through {@link GeneratedFileCache}. Plain http(s) URLs and
* workspace file paths are also accepted.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class XhsPublishTool {
private static final String CREATOR_URL = "https://creator.xiaohongshu.com/publish/publish";
private static final int MAX_IMAGES = 18; // Xiaohongshu allows up to 18 images per note.
private final GeneratedFileCache cache;
@Tool(name = "xhs_publish", description = """
Package a Xiaohongshu (小红书) note into one downloadable bundle and give
manual-publish instructions.
Actions:
- export (default): build a .zip containing 文案.txt (title + body + tags)
and the card images in order, then return a download link plus steps to
upload at the creator platform. `images` is a comma-separated list of
card image references: render_html_image download URLs
(/api/v1/files/generated/{id}), plain http(s) image URLs, or workspace
file paths.
- guide: just return the manual-publish steps and the creator URL.
Xiaohongshu has no official publish API; this tool never auto-uploads or
bypasses verification the user completes the post manually.
""")
public String xhs_publish(
@ToolParam(description = "Action: export (default) or guide", required = false)
String action,
@ToolParam(description = "Note title (小红书 标题, <=20 chars recommended)", required = false)
String title,
@ToolParam(description = "Note body text with emoji and line breaks", required = false)
String body,
@ToolParam(description = "Topic tags, comma-separated, e.g. 咖啡,探店,周末去哪儿", required = false)
String tags,
@ToolParam(description = "Comma-separated card image references (generated URLs / http URLs / workspace paths), in display order", required = false)
String images,
@Nullable ToolContext ctx) {
String act = (action == null || action.isBlank()) ? "export" : action.trim().toLowerCase();
if ("guide".equals(act)) {
return guideText();
}
if (!"export".equals(act)) {
return "Error: unknown action '" + act + "'. Use 'export' or 'guide'.";
}
String copy = buildCopy(title, body, tags);
List<byte[]> imageBytes = new ArrayList<>();
List<String> imageExts = new ArrayList<>();
List<String> skipped = new ArrayList<>();
if (images != null && !images.isBlank()) {
String[] refs = images.split(",");
for (String raw : refs) {
String ref = raw.trim();
if (ref.isEmpty()) {
continue;
}
if (imageBytes.size() >= MAX_IMAGES) {
skipped.add(ref + " (超过 " + MAX_IMAGES + " 张上限)");
continue;
}
try {
ResolvedImage img = resolveImage(ref);
imageBytes.add(img.bytes());
imageExts.add(img.ext());
} catch (Exception e) {
skipped.add(ref + " (" + e.getMessage() + ")");
}
}
}
byte[] zip;
try {
zip = buildZip(copy, imageBytes, imageExts);
} catch (Exception e) {
log.warn("[XhsPublish] zip build failed: {}", e.getMessage());
return "Error: failed to build the bundle — " + e.getMessage();
}
String linkMsg = GeneratedFileLink.resultZh(
zip, "小红书发布包.zip", "application/zip", cache, "发布包", ctx);
StringBuilder out = new StringBuilder();
out.append(linkMsg).append("\n\n");
out.append("📦 发布包含:文案.txt");
if (!imageBytes.isEmpty()) {
out.append(" + ").append(imageBytes.size()).append(" 张卡片图(已按顺序编号)");
}
out.append("\n");
if (!skipped.isEmpty()) {
out.append("⚠️ 未打包:").append(String.join("", skipped)).append("\n");
}
out.append('\n').append(guideText());
log.info("[XhsPublish] exported bundle: {} images, {} skipped", imageBytes.size(), skipped.size());
return out.toString();
}
private String buildCopy(String title, String body, String tags) {
StringBuilder sb = new StringBuilder();
if (title != null && !title.isBlank()) {
sb.append("【标题】\n").append(title.trim()).append("\n\n");
}
if (body != null && !body.isBlank()) {
sb.append("【正文】\n").append(body.trim()).append("\n\n");
}
if (tags != null && !tags.isBlank()) {
StringBuilder tagLine = new StringBuilder("【话题标签】\n");
for (String t : tags.split(",")) {
String tag = t.trim().replaceFirst("^#", "");
if (!tag.isEmpty()) {
tagLine.append('#').append(tag).append(' ');
}
}
sb.append(tagLine.toString().trim()).append('\n');
}
return sb.toString().strip();
}
private byte[] buildZip(String copy, List<byte[]> imageBytes, List<String> imageExts) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(bos)) {
zos.putNextEntry(new ZipEntry("文案.txt"));
zos.write(copy.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
for (int i = 0; i < imageBytes.size(); i++) {
String name = String.format("%02d.%s", i + 1, imageExts.get(i));
zos.putNextEntry(new ZipEntry(name));
zos.write(imageBytes.get(i));
zos.closeEntry();
}
}
return bos.toByteArray();
}
/** Resolve an image reference (generated URL / http URL / workspace path) to bytes + extension. */
private ResolvedImage resolveImage(String ref) throws Exception {
Matcher m = GeneratedFileCache.GENERATED_URL_PATTERN.matcher(ref);
if (m.find()) {
String id = m.group(1);
Optional<GeneratedFileCache.Entry> entry = cache.get(id);
if (entry.isEmpty()) {
throw new IllegalStateException("生成文件已过期或不存在");
}
return new ResolvedImage(entry.get().bytes(), extFromMime(entry.get().mimeType(), "png"));
}
if (ref.startsWith("http://") || ref.startsWith("https://")) {
UrlSafetyChecker.check(ref);
byte[] bytes = HttpUtil.downloadBytes(ref);
if (bytes == null || bytes.length == 0) {
throw new IllegalStateException("下载为空");
}
return new ResolvedImage(bytes, extFromUrl(ref));
}
// Otherwise treat as a workspace-relative/absolute file path.
Path path = WorkspacePathGuard.validatePath(ref);
if (!Files.exists(path) || Files.isDirectory(path)) {
throw new IllegalStateException("文件不存在");
}
return new ResolvedImage(Files.readAllBytes(path), extFromUrl(ref));
}
private static String extFromMime(String mime, String fallback) {
if (mime == null) {
return fallback;
}
return switch (mime.toLowerCase()) {
case "image/png" -> "png";
case "image/jpeg", "image/jpg" -> "jpg";
case "image/webp" -> "webp";
case "image/gif" -> "gif";
default -> fallback;
};
}
private static String extFromUrl(String url) {
String clean = url.split("[?#]")[0];
int dot = clean.lastIndexOf('.');
if (dot >= 0 && dot < clean.length() - 1) {
String ext = clean.substring(dot + 1).toLowerCase();
if (ext.length() <= 4 && ext.matches("[a-z0-9]+")) {
return ext;
}
}
return "png";
}
private String guideText() {
return """
📮 小红书发布步骤手动小红书无官方发布 API
1. 下载上面的发布包并解压
2. 打开创作平台 %s 需已登录上传图文
3. 0102 顺序上传卡片图首图即封面
4. 文案.txt复制标题正文粘贴到对应输入框
5. 添加话题标签核对无违禁词后自行发布""".formatted(CREATOR_URL);
}
private record ResolvedImage(byte[] bytes, String ext) {}
}

View File

@ -37,11 +37,23 @@ public class GeneratedFileController {
String encodedName = URLEncoder.encode(entry.filename(), StandardCharsets.UTF_8)
.replace("+", "%20");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(entry.mimeType()));
String mime = entry.mimeType();
headers.setContentType(MediaType.parseMediaType(mime));
// RFC 5987 filename* lets non-ASCII names round-trip in browsers.
String disposition = entry.mimeType() != null && entry.mimeType().startsWith("image/")
? "inline"
: "attachment";
// Images and HTML previews render inline; everything else downloads.
boolean isImage = mime != null && mime.startsWith("image/");
boolean isHtml = mime != null && mime.toLowerCase().startsWith("text/html");
String disposition = (isImage || isHtml) ? "inline" : "attachment";
if (isHtml) {
// The bytes are model/tool-generated HTML served from the app's
// own origin. A strict CSP neutralises XSS: scripts, plugins and
// framing are forbidden, only inline styles + images/fonts load.
// This makes an on-demand "open the article" preview safe.
headers.add("Content-Security-Policy",
"default-src 'none'; img-src * data:; style-src 'unsafe-inline'; "
+ "font-src * data:; media-src *; base-uri 'none'; form-action 'none'");
headers.add("X-Content-Type-Options", "nosniff");
}
headers.add(HttpHeaders.CONTENT_DISPOSITION,
disposition + "; filename=\"" + sanitizeAscii(entry.filename())
+ "\"; filename*=UTF-8''" + encodedName);

View File

@ -1904,3 +1904,44 @@ WHERE NOT EXISTS (SELECT 1 FROM mate_tool_guard_config WHERE id = 1000000001);
-- Removed 6 legacy SQL rules (rule_id: write_file_any, edit_file_any, shell_rm_approval,
-- shell_rm_rf_block, shell_write_system_file, shell_chmod_777).
-- Their superset is registered in ToolGuardRuleSeedService.buildBuiltinRules() with correct tool names.
-- ==================== Content Studio scenario (公众号 / 小红书 图文创作) ====================
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000630, 'WechatArticleExtractTool', 'WeChat Article Extract', 'Fetch a WeChat Official Account (公众号) article by URL and return cleaned title/author/time/body(Markdown)/images. Preferred over browser_use for mp.weixin.qq.com article pages; use it for reference gathering and summarisation.', 'builtin', 'wechatArticleExtractTool', '📰', TRUE, TRUE, NOW(), NOW(), 0);
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000631, 'GzhPublishTool', 'WeChat OA Publish', 'Publish a generated image-text article to a WeChat Official Account: action=draft uploads the cover and creates a 草稿箱 draft (recommended); action=publish free-publishes for verified accounts and requires explicit confirmation. Needs weixinoa.app_id/app_secret in system settings.', 'builtin', 'gzhPublishTool', '📤', TRUE, TRUE, NOW(), NOW(), 0);
MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
KEY (id)
VALUES (1000000640, 'Content Studio', 'End-to-end 公众号 & 小红书 image-text creation: research, write, illustrate, de-AI, layout, and publish to draft.', 'react', 'You are MateClaw''s Content Studio — a specialist that creates WeChat Official Account (公众号) and Xiaohongshu (小红书) image-text posts end to end.
Workflow (7 stages):
1) Topic use the topic_interests memory + web_search(freshness=week) to find angles.
2) Research for reference links use wechat_article_extract (or browser_use) and summarise, staying original and citing sources; never copy verbatim.
3) Write for load the gzh_article skill, for load the xhs_note skill, honoring the user persona and style.
4) Illustrate image_generate for covers/inline art, and render_html_image to turn card HTML into images.
5) De-AI load the deai_humanize skill and run its detectrewrite loop until the AI-trace score is low.
6) Package & deliver use gzh_package: pass the article body as Markdown and it builds the inline-styled HTML plus an online preview and a downloadable material bundle server-side. Do NOT hand-write a large HTML blob into write_file / render_html_image(html=...) big, escape-heavy tool arguments get truncated and make the call fail.
7) Publish send the gzh_package online preview to the user, and after confirmation default to gzh_publish action=draft (into the 稿).
At the start of a task, recall_structured these keys and honor them: content_persona, writing_style_gzh, writing_style_xhs, topic_interests, banned_words, signature_blocks. If a needed one is missing, ask the user once and remember_structured it.
Publishing is an outward, irreversible action: always show the final content and get explicit user confirmation before calling gzh_publish; never free-publish without confirmPublish=true and the user''s sign-off. Respect banned_words and advertising-law restrictions; keep every piece original.
', NULL, 100, TRUE, 'pi:pen-nib', 'content,gzh,xhs,writing', NOW(), NOW(), 0);
-- ---- Content Studio T3/T4: 小红书发布工具 + 场景化 Cron 模板(默认关闭,用户按需启用)----
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000632, 'XhsPublishTool', 'Xiaohongshu Publish', 'Package a Xiaohongshu (小红书) note (copy + tags + card images) into one downloadable .zip and give manual-publish steps. No official API — never auto-uploads or bypasses verification.', 'builtin', 'xhsPublishTool', '📕', TRUE, TRUE, NOW(), NOW(), 0);
MERGE INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
KEY (id)
VALUES (1000100020, 'Daily Topic Radar', '0 8 * * *', 'Asia/Shanghai', 1000000640, 'agent', NULL, 'Read the topic_interests structured memory, use web_search(freshness=week) to gather today''s fresh angles on those directions, and produce a Today''s Topic List: each item with a working title, a one-line angle, target platform (公众号/小红书), and a suggested illustration direction. Selection only — do not write the full article.', FALSE, NOW(), NOW(), 0);
MERGE INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
KEY (id)
VALUES (1000100021, 'Weekly 公众号 Draft', '0 9 * * 1', 'Asia/Shanghai', 1000000640, 'agent', NULL, 'Pick one topic from topic_interests for this week, load the gzh_article skill to produce a full 公众号 image-text article (with illustrations and de-AI pass), laid out as inline-styled HTML. If 公众号 credentials (weixinoa.app_id/app_secret) are configured in system settings, use gzh_publish action=draft to save it to the draft box and remind me to review and publish in the backend; otherwise just send me the HTML and cover.', FALSE, NOW(), NOW(), 0);
-- Content Studio: gzh_package (Markdown -> 在线预览 + 素材下载, avoids big-HTML tool-arg truncation)
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000633, 'GzhPackageTool', 'WeChat Article Package', 'Package a finished 公众号 article from Markdown into an online preview (rendered HTML) plus a downloadable material bundle (article.html + article.md + cover). Builds the inline-styled HTML server-side so a large HTML string never rides on the tool-argument stream (which truncates and fails).', 'builtin', 'gzhPackageTool', '📦', TRUE, TRUE, NOW(), NOW(), 0);

View File

@ -1829,3 +1829,44 @@ ON CONFLICT (id) DO NOTHING;
-- Security rules are managed by ToolGuardRuleSeedService (Java) as single source of truth.
-- Removed 6 legacy SQL rules. Their superset is registered in ToolGuardRuleSeedService.buildBuiltinRules() with correct tool names.
-- ==================== Content Studio scenario (公众号 / 小红书 图文创作) ====================
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000630, 'WechatArticleExtractTool', 'WeChat Article Extract', 'Fetch a WeChat Official Account (公众号) article by URL and return cleaned title/author/time/body(Markdown)/images. Preferred over browser_use for mp.weixin.qq.com article pages; use it for reference gathering and summarisation.', 'builtin', 'wechatArticleExtractTool', '📰', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000631, 'GzhPublishTool', 'WeChat OA Publish', 'Publish a generated image-text article to a WeChat Official Account: action=draft uploads the cover and creates a 草稿箱 draft (recommended); action=publish free-publishes for verified accounts and requires explicit confirmation. Needs weixinoa.app_id/app_secret in system settings.', 'builtin', 'gzhPublishTool', '📤', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
VALUES (1000000640, 'Content Studio', 'End-to-end 公众号 & 小红书 image-text creation: research, write, illustrate, de-AI, layout, and publish to draft.', 'react', 'You are MateClaw''s Content Studio — a specialist that creates WeChat Official Account (公众号) and Xiaohongshu (小红书) image-text posts end to end.
Workflow (7 stages):
1) Topic use the topic_interests memory + web_search(freshness=week) to find angles.
2) Research for reference links use wechat_article_extract (or browser_use) and summarise, staying original and citing sources; never copy verbatim.
3) Write for load the gzh_article skill, for load the xhs_note skill, honoring the user persona and style.
4) Illustrate image_generate for covers/inline art, and render_html_image to turn card HTML into images.
5) De-AI load the deai_humanize skill and run its detectrewrite loop until the AI-trace score is low.
6) Package & deliver use gzh_package: pass the article body as Markdown and it builds the inline-styled HTML plus an online preview and a downloadable material bundle server-side. Do NOT hand-write a large HTML blob into write_file / render_html_image(html=...) big, escape-heavy tool arguments get truncated and make the call fail.
7) Publish send the gzh_package online preview to the user, and after confirmation default to gzh_publish action=draft (into the 稿).
At the start of a task, recall_structured these keys and honor them: content_persona, writing_style_gzh, writing_style_xhs, topic_interests, banned_words, signature_blocks. If a needed one is missing, ask the user once and remember_structured it.
Publishing is an outward, irreversible action: always show the final content and get explicit user confirmation before calling gzh_publish; never free-publish without confirmPublish=true and the user''s sign-off. Respect banned_words and advertising-law restrictions; keep every piece original.
', NULL, 100, TRUE, 'pi:pen-nib', 'content,gzh,xhs,writing', NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, description=EXCLUDED.description, agent_type=EXCLUDED.agent_type, system_prompt=EXCLUDED.system_prompt, model_name=EXCLUDED.model_name, max_iterations=EXCLUDED.max_iterations, enabled=EXCLUDED.enabled, icon=EXCLUDED.icon, tags=EXCLUDED.tags, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
-- ---- Content Studio T3/T4: 小红书发布工具 + 场景化 Cron 模板(默认关闭,用户按需启用)----
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000632, 'XhsPublishTool', 'Xiaohongshu Publish', 'Package a Xiaohongshu (小红书) note (copy + tags + card images) into one downloadable .zip and give manual-publish steps. No official API — never auto-uploads or bypasses verification.', 'builtin', 'xhsPublishTool', '📕', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100020, 'Daily Topic Radar', '0 8 * * *', 'Asia/Shanghai', 1000000640, 'agent', NULL, 'Read the topic_interests structured memory, use web_search(freshness=week) to gather today''s fresh angles on those directions, and produce a Today''s Topic List: each item with a working title, a one-line angle, target platform (公众号/小红书), and a suggested illustration direction. Selection only — do not write the full article.', FALSE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, cron_expression=EXCLUDED.cron_expression, timezone=EXCLUDED.timezone, agent_id=EXCLUDED.agent_id, task_type=EXCLUDED.task_type, trigger_message=EXCLUDED.trigger_message, request_body=EXCLUDED.request_body, enabled=EXCLUDED.enabled, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100021, 'Weekly 公众号 Draft', '0 9 * * 1', 'Asia/Shanghai', 1000000640, 'agent', NULL, 'Pick one topic from topic_interests for this week, load the gzh_article skill to produce a full 公众号 image-text article (with illustrations and de-AI pass), laid out as inline-styled HTML. If 公众号 credentials (weixinoa.app_id/app_secret) are configured in system settings, use gzh_publish action=draft to save it to the draft box and remind me to review and publish in the backend; otherwise just send me the HTML and cover.', FALSE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, cron_expression=EXCLUDED.cron_expression, timezone=EXCLUDED.timezone, agent_id=EXCLUDED.agent_id, task_type=EXCLUDED.task_type, trigger_message=EXCLUDED.trigger_message, request_body=EXCLUDED.request_body, enabled=EXCLUDED.enabled, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
-- Content Studio: gzh_package (Markdown -> 在线预览 + 素材下载, avoids big-HTML tool-arg truncation)
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000633, 'GzhPackageTool', 'WeChat Article Package', 'Package a finished 公众号 article from Markdown into an online preview (rendered HTML) plus a downloadable material bundle (article.html + article.md + cover). Builds the inline-styled HTML server-side so a large HTML string never rides on the tool-argument stream (which truncates and fails).', 'builtin', 'gzhPackageTool', '📦', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;

View File

@ -1826,3 +1826,44 @@ ON CONFLICT (id) DO NOTHING;
-- 安全规则由 ToolGuardRuleSeedService (Java) 统一种子化,不在 SQL 中重复维护
-- 已移除旧的 6 条 SQL 规则,其超集已在 ToolGuardRuleSeedService.buildBuiltinRules() 中以正确的工具名注册。
-- ==================== Content Studio scenario (公众号 / 小红书 图文创作) ====================
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000630, 'WechatArticleExtractTool', '公众号文章抓取', '抓取微信公众号文章:输入文章 URL返回清洗后的标题/作者/时间/正文(Markdown)/图片。用于「参考公众号信息抓取汇总」,比 browser_use 更适合 mp.weixin.qq.com 文章页。', 'builtin', 'wechatArticleExtractTool', '📰', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000631, 'GzhPublishTool', '公众号发布', '将生成的图文发布到微信公众号action=draft 上传封面并存入草稿箱推荐action=publish 为认证号群发,需显式确认。需在系统设置配置 weixinoa.app_id / weixinoa.app_secret。', 'builtin', 'gzhPublishTool', '📤', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
VALUES (1000000640, '内容工作室', '端到端创作公众号与小红书图文选题搜集、成文、配图、去AI化、排版、入草稿箱发布。', 'react', '你是 MateClaw 的「内容工作室」——专门端到端创作微信公众号(公众号)与小红书图文。
7
1 topic_interests web_search(freshness=week)
2 wechat_article_extract browser_use稿
3 gzh_article xhs_note
4 image_generate / render_html_image HTML
5AI化 deai_humanize AI
6 gzh_package Markdown HTML + 线 + HTML write_file render_html_image(html=...)
7 gzh_package 线 gzh_publish action=draft 稿
recall_structured content_personawriting_style_gzhwriting_style_xhstopic_interestsbanned_wordssignature_blocks remember_structured
gzh_publish confirmPublish=true banned_words 广
', NULL, 100, TRUE, 'pi:pen-nib', 'content,gzh,xhs,writing', NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, description=EXCLUDED.description, agent_type=EXCLUDED.agent_type, system_prompt=EXCLUDED.system_prompt, model_name=EXCLUDED.model_name, max_iterations=EXCLUDED.max_iterations, enabled=EXCLUDED.enabled, icon=EXCLUDED.icon, tags=EXCLUDED.tags, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
-- ---- Content Studio T3/T4: 小红书发布工具 + 场景化 Cron 模板(默认关闭,用户按需启用)----
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000632, 'XhsPublishTool', '小红书发布打包', '把小红书笔记(文案+标签+卡片图)打包成一个可下载 zip并给出创作平台手动上传步骤。小红书无官方发布 API不自动上传、不绕过风控。', 'builtin', 'xhsPublishTool', '📕', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100020, '每日选题雷达', '0 8 * * *', 'Asia/Shanghai', 1000000640, 'agent', NULL, '读取结构化记忆 topic_interests用 web_searchfreshness=week搜集与这些方向相关的今日热点与新鲜角度产出一份「今日选题清单」每条含选题标题、一句话切入角度、目标平台公众号/小红书)、推荐配图方向。只做选题,不成文。', FALSE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, cron_expression=EXCLUDED.cron_expression, timezone=EXCLUDED.timezone, agent_id=EXCLUDED.agent_id, task_type=EXCLUDED.task_type, trigger_message=EXCLUDED.trigger_message, request_body=EXCLUDED.request_body, enabled=EXCLUDED.enabled, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100021, '每周公众号入草稿箱', '0 9 * * 1', 'Asia/Shanghai', 1000000640, 'agent', NULL, '从 topic_interests 里挑一个当周选题,加载 gzh_article 技能完成一篇公众号图文含配图与去AI化排版为内联样式 HTML。若已在系统设置配置公众号凭证weixinoa.app_id/app_secret用 gzh_publish action=draft 存入草稿箱并提醒我去后台核对发表;未配置则直接把排版 HTML 与封面发我。', FALSE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, cron_expression=EXCLUDED.cron_expression, timezone=EXCLUDED.timezone, agent_id=EXCLUDED.agent_id, task_type=EXCLUDED.task_type, trigger_message=EXCLUDED.trigger_message, request_body=EXCLUDED.request_body, enabled=EXCLUDED.enabled, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
-- Content Studio: gzh_package (Markdown -> 在线预览 + 素材下载, avoids big-HTML tool-arg truncation)
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000633, 'GzhPackageTool', '公众号打包', '把公众号成稿Markdown打包成在线预览渲染 HTML+ 素材下载 ziparticle.html/article.md/封面)。服务端生成内联样式 HTML避免大段 HTML 作为工具参数被截断而失败。', 'builtin', 'gzhPackageTool', '📦', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;

View File

@ -1945,3 +1945,44 @@ VALUES (
-- Security rules are managed by ToolGuardRuleSeedService (Java) as single source of truth.
-- Removed 6 legacy SQL rules. Their superset is registered in ToolGuardRuleSeedService.buildBuiltinRules() with correct tool names.
-- ==================== Content Studio scenario (公众号 / 小红书 图文创作) ====================
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000630, 'WechatArticleExtractTool', 'WeChat Article Extract', 'Fetch a WeChat Official Account (公众号) article by URL and return cleaned title/author/time/body(Markdown)/images. Preferred over browser_use for mp.weixin.qq.com article pages; use it for reference gathering and summarisation.', 'builtin', 'wechatArticleExtractTool', '📰', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000631, 'GzhPublishTool', 'WeChat OA Publish', 'Publish a generated image-text article to a WeChat Official Account: action=draft uploads the cover and creates a 草稿箱 draft (recommended); action=publish free-publishes for verified accounts and requires explicit confirmation. Needs weixinoa.app_id/app_secret in system settings.', 'builtin', 'gzhPublishTool', '📤', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
VALUES (1000000640, 'Content Studio', 'End-to-end 公众号 & 小红书 image-text creation: research, write, illustrate, de-AI, layout, and publish to draft.', 'react', 'You are MateClaw''s Content Studio — a specialist that creates WeChat Official Account (公众号) and Xiaohongshu (小红书) image-text posts end to end.
Workflow (7 stages):
1) Topic use the topic_interests memory + web_search(freshness=week) to find angles.
2) Research for reference links use wechat_article_extract (or browser_use) and summarise, staying original and citing sources; never copy verbatim.
3) Write for load the gzh_article skill, for load the xhs_note skill, honoring the user persona and style.
4) Illustrate image_generate for covers/inline art, and render_html_image to turn card HTML into images.
5) De-AI load the deai_humanize skill and run its detectrewrite loop until the AI-trace score is low.
6) Package & deliver use gzh_package: pass the article body as Markdown and it builds the inline-styled HTML plus an online preview and a downloadable material bundle server-side. Do NOT hand-write a large HTML blob into write_file / render_html_image(html=...) big, escape-heavy tool arguments get truncated and make the call fail.
7) Publish send the gzh_package online preview to the user, and after confirmation default to gzh_publish action=draft (into the 稿).
At the start of a task, recall_structured these keys and honor them: content_persona, writing_style_gzh, writing_style_xhs, topic_interests, banned_words, signature_blocks. If a needed one is missing, ask the user once and remember_structured it.
Publishing is an outward, irreversible action: always show the final content and get explicit user confirmation before calling gzh_publish; never free-publish without confirmPublish=true and the user''s sign-off. Respect banned_words and advertising-law restrictions; keep every piece original.
', NULL, 100, TRUE, 'pi:pen-nib', 'content,gzh,xhs,writing', NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- ---- Content Studio T3/T4: 小红书发布工具 + 场景化 Cron 模板(默认关闭,用户按需启用)----
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000632, 'XhsPublishTool', 'Xiaohongshu Publish', 'Package a Xiaohongshu (小红书) note (copy + tags + card images) into one downloadable .zip and give manual-publish steps. No official API — never auto-uploads or bypasses verification.', 'builtin', 'xhsPublishTool', '📕', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100020, 'Daily Topic Radar', '0 8 * * *', 'Asia/Shanghai', 1000000640, 'agent', NULL, 'Read the topic_interests structured memory, use web_search(freshness=week) to gather today''s fresh angles on those directions, and produce a Today''s Topic List: each item with a working title, a one-line angle, target platform (公众号/小红书), and a suggested illustration direction. Selection only — do not write the full article.', FALSE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), cron_expression=VALUES(cron_expression), timezone=VALUES(timezone), agent_id=VALUES(agent_id), task_type=VALUES(task_type), trigger_message=VALUES(trigger_message), request_body=VALUES(request_body), enabled=VALUES(enabled), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100021, 'Weekly 公众号 Draft', '0 9 * * 1', 'Asia/Shanghai', 1000000640, 'agent', NULL, 'Pick one topic from topic_interests for this week, load the gzh_article skill to produce a full 公众号 image-text article (with illustrations and de-AI pass), laid out as inline-styled HTML. If 公众号 credentials (weixinoa.app_id/app_secret) are configured in system settings, use gzh_publish action=draft to save it to the draft box and remind me to review and publish in the backend; otherwise just send me the HTML and cover.', FALSE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), cron_expression=VALUES(cron_expression), timezone=VALUES(timezone), agent_id=VALUES(agent_id), task_type=VALUES(task_type), trigger_message=VALUES(trigger_message), request_body=VALUES(request_body), enabled=VALUES(enabled), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- Content Studio: gzh_package (Markdown -> 在线预览 + 素材下载, avoids big-HTML tool-arg truncation)
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000633, 'GzhPackageTool', 'WeChat Article Package', 'Package a finished 公众号 article from Markdown into an online preview (rendered HTML) plus a downloadable material bundle (article.html + article.md + cover). Builds the inline-styled HTML server-side so a large HTML string never rides on the tool-argument stream (which truncates and fails).', 'builtin', 'gzhPackageTool', '📦', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);

View File

@ -1942,3 +1942,44 @@ VALUES (
-- 安全规则由 ToolGuardRuleSeedService (Java) 统一种子化,不在 SQL 中重复维护
-- 已移除旧的 6 条 SQL 规则,其超集已在 ToolGuardRuleSeedService.buildBuiltinRules() 中以正确的工具名注册。
-- ==================== Content Studio scenario (公众号 / 小红书 图文创作) ====================
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000630, 'WechatArticleExtractTool', '公众号文章抓取', '抓取微信公众号文章:输入文章 URL返回清洗后的标题/作者/时间/正文(Markdown)/图片。用于「参考公众号信息抓取汇总」,比 browser_use 更适合 mp.weixin.qq.com 文章页。', 'builtin', 'wechatArticleExtractTool', '📰', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000631, 'GzhPublishTool', '公众号发布', '将生成的图文发布到微信公众号action=draft 上传封面并存入草稿箱推荐action=publish 为认证号群发,需显式确认。需在系统设置配置 weixinoa.app_id / weixinoa.app_secret。', 'builtin', 'gzhPublishTool', '📤', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
VALUES (1000000640, '内容工作室', '端到端创作公众号与小红书图文选题搜集、成文、配图、去AI化、排版、入草稿箱发布。', 'react', '你是 MateClaw 的「内容工作室」——专门端到端创作微信公众号(公众号)与小红书图文。
7
1 topic_interests web_search(freshness=week)
2 wechat_article_extract browser_use稿
3 gzh_article xhs_note
4 image_generate / render_html_image HTML
5AI化 deai_humanize AI
6 gzh_package Markdown HTML + 线 + HTML write_file render_html_image(html=...)
7 gzh_package 线 gzh_publish action=draft 稿
recall_structured content_personawriting_style_gzhwriting_style_xhstopic_interestsbanned_wordssignature_blocks remember_structured
gzh_publish confirmPublish=true banned_words 广
', NULL, 100, TRUE, 'pi:pen-nib', 'content,gzh,xhs,writing', NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- ---- Content Studio T3/T4: 小红书发布工具 + 场景化 Cron 模板(默认关闭,用户按需启用)----
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000632, 'XhsPublishTool', '小红书发布打包', '把小红书笔记(文案+标签+卡片图)打包成一个可下载 zip并给出创作平台手动上传步骤。小红书无官方发布 API不自动上传、不绕过风控。', 'builtin', 'xhsPublishTool', '📕', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100020, '每日选题雷达', '0 8 * * *', 'Asia/Shanghai', 1000000640, 'agent', NULL, '读取结构化记忆 topic_interests用 web_searchfreshness=week搜集与这些方向相关的今日热点与新鲜角度产出一份「今日选题清单」每条含选题标题、一句话切入角度、目标平台公众号/小红书)、推荐配图方向。只做选题,不成文。', FALSE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), cron_expression=VALUES(cron_expression), timezone=VALUES(timezone), agent_id=VALUES(agent_id), task_type=VALUES(task_type), trigger_message=VALUES(trigger_message), request_body=VALUES(request_body), enabled=VALUES(enabled), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100021, '每周公众号入草稿箱', '0 9 * * 1', 'Asia/Shanghai', 1000000640, 'agent', NULL, '从 topic_interests 里挑一个当周选题,加载 gzh_article 技能完成一篇公众号图文含配图与去AI化排版为内联样式 HTML。若已在系统设置配置公众号凭证weixinoa.app_id/app_secret用 gzh_publish action=draft 存入草稿箱并提醒我去后台核对发表;未配置则直接把排版 HTML 与封面发我。', FALSE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), cron_expression=VALUES(cron_expression), timezone=VALUES(timezone), agent_id=VALUES(agent_id), task_type=VALUES(task_type), trigger_message=VALUES(trigger_message), request_body=VALUES(request_body), enabled=VALUES(enabled), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- Content Studio: gzh_package (Markdown -> 在线预览 + 素材下载, avoids big-HTML tool-arg truncation)
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000633, 'GzhPackageTool', '公众号打包', '把公众号成稿Markdown打包成在线预览渲染 HTML+ 素材下载 ziparticle.html/article.md/封面)。服务端生成内联样式 HTML避免大段 HTML 作为工具参数被截断而失败。', 'builtin', 'gzhPackageTool', '📦', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);

View File

@ -1905,3 +1905,44 @@ WHERE NOT EXISTS (SELECT 1 FROM mate_tool_guard_config WHERE id = 1000000001);
-- 已移除旧的 6 条 SQL 规则rule_id: write_file_any, edit_file_any, shell_rm_approval,
-- shell_rm_rf_block, shell_write_system_file, shell_chmod_777
-- 它们的超集已在 ToolGuardRuleSeedService.buildBuiltinRules() 中以正确的工具名注册。
-- ==================== Content Studio scenario (公众号 / 小红书 图文创作) ====================
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000630, 'WechatArticleExtractTool', '公众号文章抓取', '抓取微信公众号文章:输入文章 URL返回清洗后的标题/作者/时间/正文(Markdown)/图片。用于「参考公众号信息抓取汇总」,比 browser_use 更适合 mp.weixin.qq.com 文章页。', 'builtin', 'wechatArticleExtractTool', '📰', TRUE, TRUE, NOW(), NOW(), 0);
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000631, 'GzhPublishTool', '公众号发布', '将生成的图文发布到微信公众号action=draft 上传封面并存入草稿箱推荐action=publish 为认证号群发,需显式确认。需在系统设置配置 weixinoa.app_id / weixinoa.app_secret。', 'builtin', 'gzhPublishTool', '📤', TRUE, TRUE, NOW(), NOW(), 0);
MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
KEY (id)
VALUES (1000000640, '内容工作室', '端到端创作公众号与小红书图文选题搜集、成文、配图、去AI化、排版、入草稿箱发布。', 'react', '你是 MateClaw 的「内容工作室」——专门端到端创作微信公众号(公众号)与小红书图文。
7
1 topic_interests web_search(freshness=week)
2 wechat_article_extract browser_use稿
3 gzh_article xhs_note
4 image_generate / render_html_image HTML
5AI化 deai_humanize AI
6 gzh_package Markdown HTML + 线 + HTML write_file render_html_image(html=...)
7 gzh_package 线 gzh_publish action=draft 稿
recall_structured content_personawriting_style_gzhwriting_style_xhstopic_interestsbanned_wordssignature_blocks remember_structured
gzh_publish confirmPublish=true banned_words 广
', NULL, 100, TRUE, 'pi:pen-nib', 'content,gzh,xhs,writing', NOW(), NOW(), 0);
-- ---- Content Studio T3/T4: 小红书发布工具 + 场景化 Cron 模板(默认关闭,用户按需启用)----
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000632, 'XhsPublishTool', '小红书发布打包', '把小红书笔记(文案+标签+卡片图)打包成一个可下载 zip并给出创作平台手动上传步骤。小红书无官方发布 API不自动上传、不绕过风控。', 'builtin', 'xhsPublishTool', '📕', TRUE, TRUE, NOW(), NOW(), 0);
MERGE INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
KEY (id)
VALUES (1000100020, '每日选题雷达', '0 8 * * *', 'Asia/Shanghai', 1000000640, 'agent', NULL, '读取结构化记忆 topic_interests用 web_searchfreshness=week搜集与这些方向相关的今日热点与新鲜角度产出一份「今日选题清单」每条含选题标题、一句话切入角度、目标平台公众号/小红书)、推荐配图方向。只做选题,不成文。', FALSE, NOW(), NOW(), 0);
MERGE INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
KEY (id)
VALUES (1000100021, '每周公众号入草稿箱', '0 9 * * 1', 'Asia/Shanghai', 1000000640, 'agent', NULL, '从 topic_interests 里挑一个当周选题,加载 gzh_article 技能完成一篇公众号图文含配图与去AI化排版为内联样式 HTML。若已在系统设置配置公众号凭证weixinoa.app_id/app_secret用 gzh_publish action=draft 存入草稿箱并提醒我去后台核对发表;未配置则直接把排版 HTML 与封面发我。', FALSE, NOW(), NOW(), 0);
-- Content Studio: gzh_package (Markdown -> 在线预览 + 素材下载, avoids big-HTML tool-arg truncation)
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000633, 'GzhPackageTool', '公众号打包', '把公众号成稿Markdown打包成在线预览渲染 HTML+ 素材下载 ziparticle.html/article.md/封面)。服务端生成内联样式 HTML避免大段 HTML 作为工具参数被截断而失败。', 'builtin', 'gzhPackageTool', '📦', TRUE, TRUE, NOW(), NOW(), 0);

View File

@ -0,0 +1,50 @@
-- V168: Content Studio scenario seed (公众号 / 小红书 图文创作).
-- Delivers to EXISTING databases the rows fresh installs get from db/data-*.sql:
-- built-in tools (wechat_article_extract, gzh_publish, xhs_publish, gzh_package),
-- the 内容工作室 (Content Studio) agent, and two disabled cron templates.
-- DatabaseBootstrapRunner skips seeding once a database is initialized, so these
-- would otherwise never reach upgraders. Idempotent upsert on id; content is the
-- default (zh-CN) locale — a fresh install re-runs the locale seed afterwards.
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000630, 'WechatArticleExtractTool', '公众号文章抓取', '抓取微信公众号文章:输入文章 URL返回清洗后的标题/作者/时间/正文(Markdown)/图片。用于「参考公众号信息抓取汇总」,比 browser_use 更适合 mp.weixin.qq.com 文章页。', 'builtin', 'wechatArticleExtractTool', '📰', TRUE, TRUE, NOW(), NOW(), 0);
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000631, 'GzhPublishTool', '公众号发布', '将生成的图文发布到微信公众号action=draft 上传封面并存入草稿箱推荐action=publish 为认证号群发,需显式确认。需在系统设置配置 weixinoa.app_id / weixinoa.app_secret。', 'builtin', 'gzhPublishTool', '📤', TRUE, TRUE, NOW(), NOW(), 0);
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000632, 'XhsPublishTool', '小红书发布打包', '把小红书笔记(文案+标签+卡片图)打包成一个可下载 zip并给出创作平台手动上传步骤。小红书无官方发布 API不自动上传、不绕过风控。', 'builtin', 'xhsPublishTool', '📕', TRUE, TRUE, NOW(), NOW(), 0);
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
KEY (id)
VALUES (1000000633, 'GzhPackageTool', '公众号打包', '把公众号成稿Markdown打包成在线预览渲染 HTML+ 素材下载 ziparticle.html/article.md/封面)。服务端生成内联样式 HTML避免大段 HTML 作为工具参数被截断而失败。', 'builtin', 'gzhPackageTool', '📦', TRUE, TRUE, NOW(), NOW(), 0);
MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
KEY (id)
VALUES (1000000640, '内容工作室', '端到端创作公众号与小红书图文选题搜集、成文、配图、去AI化、排版、入草稿箱发布。', 'react', '你是 MateClaw 的「内容工作室」——专门端到端创作微信公众号(公众号)与小红书图文。
7
1 topic_interests web_search(freshness=week)
2 wechat_article_extract browser_use稿
3 gzh_article xhs_note
4 image_generate / render_html_image HTML
5AI化 deai_humanize AI
6 gzh_package Markdown HTML + 线 + HTML write_file render_html_image(html=...)
7 gzh_package 线 gzh_publish action=draft 稿
recall_structured content_personawriting_style_gzhwriting_style_xhstopic_interestsbanned_wordssignature_blocks remember_structured
gzh_publish confirmPublish=true banned_words 广
', NULL, 100, TRUE, 'pi:pen-nib', 'content,gzh,xhs,writing', NOW(), NOW(), 0);
MERGE INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
KEY (id)
VALUES (1000100020, '每日选题雷达', '0 8 * * *', 'Asia/Shanghai', 1000000640, 'agent', NULL, '读取结构化记忆 topic_interests用 web_searchfreshness=week搜集与这些方向相关的今日热点与新鲜角度产出一份「今日选题清单」每条含选题标题、一句话切入角度、目标平台公众号/小红书)、推荐配图方向。只做选题,不成文。', FALSE, NOW(), NOW(), 0);
MERGE INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
KEY (id)
VALUES (1000100021, '每周公众号入草稿箱', '0 9 * * 1', 'Asia/Shanghai', 1000000640, 'agent', NULL, '从 topic_interests 里挑一个当周选题,加载 gzh_article 技能完成一篇公众号图文含配图与去AI化排版为内联样式 HTML。若已在系统设置配置公众号凭证weixinoa.app_id/app_secret用 gzh_publish action=draft 存入草稿箱并提醒我去后台核对发表;未配置则直接把排版 HTML 与封面发我。', FALSE, NOW(), NOW(), 0);

View File

@ -0,0 +1,50 @@
-- V168: Content Studio scenario seed (公众号 / 小红书 图文创作).
-- Delivers to EXISTING databases the rows fresh installs get from db/data-*.sql:
-- built-in tools (wechat_article_extract, gzh_publish, xhs_publish, gzh_package),
-- the 内容工作室 (Content Studio) agent, and two disabled cron templates.
-- DatabaseBootstrapRunner skips seeding once a database is initialized, so these
-- would otherwise never reach upgraders. Idempotent upsert on id; content is the
-- default (zh-CN) locale — a fresh install re-runs the locale seed afterwards.
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000630, 'WechatArticleExtractTool', '公众号文章抓取', '抓取微信公众号文章:输入文章 URL返回清洗后的标题/作者/时间/正文(Markdown)/图片。用于「参考公众号信息抓取汇总」,比 browser_use 更适合 mp.weixin.qq.com 文章页。', 'builtin', 'wechatArticleExtractTool', '📰', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000631, 'GzhPublishTool', '公众号发布', '将生成的图文发布到微信公众号action=draft 上传封面并存入草稿箱推荐action=publish 为认证号群发,需显式确认。需在系统设置配置 weixinoa.app_id / weixinoa.app_secret。', 'builtin', 'gzhPublishTool', '📤', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000632, 'XhsPublishTool', '小红书发布打包', '把小红书笔记(文案+标签+卡片图)打包成一个可下载 zip并给出创作平台手动上传步骤。小红书无官方发布 API不自动上传、不绕过风控。', 'builtin', 'xhsPublishTool', '📕', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000633, 'GzhPackageTool', '公众号打包', '把公众号成稿Markdown打包成在线预览渲染 HTML+ 素材下载 ziparticle.html/article.md/封面)。服务端生成内联样式 HTML避免大段 HTML 作为工具参数被截断而失败。', 'builtin', 'gzhPackageTool', '📦', TRUE, TRUE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
VALUES (1000000640, '内容工作室', '端到端创作公众号与小红书图文选题搜集、成文、配图、去AI化、排版、入草稿箱发布。', 'react', '你是 MateClaw 的「内容工作室」——专门端到端创作微信公众号(公众号)与小红书图文。
7
1 topic_interests web_search(freshness=week)
2 wechat_article_extract browser_use稿
3 gzh_article xhs_note
4 image_generate / render_html_image HTML
5AI化 deai_humanize AI
6 gzh_package Markdown HTML + 线 + HTML write_file render_html_image(html=...)
7 gzh_package 线 gzh_publish action=draft 稿
recall_structured content_personawriting_style_gzhwriting_style_xhstopic_interestsbanned_wordssignature_blocks remember_structured
gzh_publish confirmPublish=true banned_words 广
', NULL, 100, TRUE, 'pi:pen-nib', 'content,gzh,xhs,writing', NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, description=EXCLUDED.description, agent_type=EXCLUDED.agent_type, system_prompt=EXCLUDED.system_prompt, model_name=EXCLUDED.model_name, max_iterations=EXCLUDED.max_iterations, enabled=EXCLUDED.enabled, icon=EXCLUDED.icon, tags=EXCLUDED.tags, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100020, '每日选题雷达', '0 8 * * *', 'Asia/Shanghai', 1000000640, 'agent', NULL, '读取结构化记忆 topic_interests用 web_searchfreshness=week搜集与这些方向相关的今日热点与新鲜角度产出一份「今日选题清单」每条含选题标题、一句话切入角度、目标平台公众号/小红书)、推荐配图方向。只做选题,不成文。', FALSE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, cron_expression=EXCLUDED.cron_expression, timezone=EXCLUDED.timezone, agent_id=EXCLUDED.agent_id, task_type=EXCLUDED.task_type, trigger_message=EXCLUDED.trigger_message, request_body=EXCLUDED.request_body, enabled=EXCLUDED.enabled, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100021, '每周公众号入草稿箱', '0 9 * * 1', 'Asia/Shanghai', 1000000640, 'agent', NULL, '从 topic_interests 里挑一个当周选题,加载 gzh_article 技能完成一篇公众号图文含配图与去AI化排版为内联样式 HTML。若已在系统设置配置公众号凭证weixinoa.app_id/app_secret用 gzh_publish action=draft 存入草稿箱并提醒我去后台核对发表;未配置则直接把排版 HTML 与封面发我。', FALSE, NOW(), NOW(), 0)
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, cron_expression=EXCLUDED.cron_expression, timezone=EXCLUDED.timezone, agent_id=EXCLUDED.agent_id, task_type=EXCLUDED.task_type, trigger_message=EXCLUDED.trigger_message, request_body=EXCLUDED.request_body, enabled=EXCLUDED.enabled, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted;

View File

@ -0,0 +1,50 @@
-- V168: Content Studio scenario seed (公众号 / 小红书 图文创作).
-- Delivers to EXISTING databases the rows fresh installs get from db/data-*.sql:
-- built-in tools (wechat_article_extract, gzh_publish, xhs_publish, gzh_package),
-- the 内容工作室 (Content Studio) agent, and two disabled cron templates.
-- DatabaseBootstrapRunner skips seeding once a database is initialized, so these
-- would otherwise never reach upgraders. Idempotent upsert on id; content is the
-- default (zh-CN) locale — a fresh install re-runs the locale seed afterwards.
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000630, 'WechatArticleExtractTool', '公众号文章抓取', '抓取微信公众号文章:输入文章 URL返回清洗后的标题/作者/时间/正文(Markdown)/图片。用于「参考公众号信息抓取汇总」,比 browser_use 更适合 mp.weixin.qq.com 文章页。', 'builtin', 'wechatArticleExtractTool', '📰', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000631, 'GzhPublishTool', '公众号发布', '将生成的图文发布到微信公众号action=draft 上传封面并存入草稿箱推荐action=publish 为认证号群发,需显式确认。需在系统设置配置 weixinoa.app_id / weixinoa.app_secret。', 'builtin', 'gzhPublishTool', '📤', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000632, 'XhsPublishTool', '小红书发布打包', '把小红书笔记(文案+标签+卡片图)打包成一个可下载 zip并给出创作平台手动上传步骤。小红书无官方发布 API不自动上传、不绕过风控。', 'builtin', 'xhsPublishTool', '📕', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
VALUES (1000000633, 'GzhPackageTool', '公众号打包', '把公众号成稿Markdown打包成在线预览渲染 HTML+ 素材下载 ziparticle.html/article.md/封面)。服务端生成内联样式 HTML避免大段 HTML 作为工具参数被截断而失败。', 'builtin', 'gzhPackageTool', '📦', TRUE, TRUE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
VALUES (1000000640, '内容工作室', '端到端创作公众号与小红书图文选题搜集、成文、配图、去AI化、排版、入草稿箱发布。', 'react', '你是 MateClaw 的「内容工作室」——专门端到端创作微信公众号(公众号)与小红书图文。
7
1 topic_interests web_search(freshness=week)
2 wechat_article_extract browser_use稿
3 gzh_article xhs_note
4 image_generate / render_html_image HTML
5AI化 deai_humanize AI
6 gzh_package Markdown HTML + 线 + HTML write_file render_html_image(html=...)
7 gzh_package 线 gzh_publish action=draft 稿
recall_structured content_personawriting_style_gzhwriting_style_xhstopic_interestsbanned_wordssignature_blocks remember_structured
gzh_publish confirmPublish=true banned_words 广
', NULL, 100, TRUE, 'pi:pen-nib', 'content,gzh,xhs,writing', NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100020, '每日选题雷达', '0 8 * * *', 'Asia/Shanghai', 1000000640, 'agent', NULL, '读取结构化记忆 topic_interests用 web_searchfreshness=week搜集与这些方向相关的今日热点与新鲜角度产出一份「今日选题清单」每条含选题标题、一句话切入角度、目标平台公众号/小红书)、推荐配图方向。只做选题,不成文。', FALSE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), cron_expression=VALUES(cron_expression), timezone=VALUES(timezone), agent_id=VALUES(agent_id), task_type=VALUES(task_type), trigger_message=VALUES(trigger_message), request_body=VALUES(request_body), enabled=VALUES(enabled), update_time=VALUES(update_time), deleted=VALUES(deleted);
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100021, '每周公众号入草稿箱', '0 9 * * 1', 'Asia/Shanghai', 1000000640, 'agent', NULL, '从 topic_interests 里挑一个当周选题,加载 gzh_article 技能完成一篇公众号图文含配图与去AI化排版为内联样式 HTML。若已在系统设置配置公众号凭证weixinoa.app_id/app_secret用 gzh_publish action=draft 存入草稿箱并提醒我去后台核对发表;未配置则直接把排版 HTML 与封面发我。', FALSE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), cron_expression=VALUES(cron_expression), timezone=VALUES(timezone), agent_id=VALUES(agent_id), task_type=VALUES(task_type), trigger_message=VALUES(trigger_message), request_body=VALUES(request_body), enabled=VALUES(enabled), update_time=VALUES(update_time), deleted=VALUES(deleted);

View File

@ -0,0 +1,96 @@
---
name: deai_humanize
description: '去AI味 / 去AI化 / 人味改写 — detect AI writing traces (AI痕迹/AI腔) with a measurable heuristic score, then rewrite text to sound human. Two tones: gzh (公众号沉稳克制) and xhs (小红书活泼碎句). humanize / de-ai / anti-ai-slop.'
version: 1.0.0
tags:
- 去AI化
- humanize
- writing
- rewrite
- chinese
- anti-ai-slop
dependencies:
commands:
- python3
tools:
- skillScriptTool
platforms:
- macos
- linux
- windows
---
# 去 AI 化 · 人味改写
把一段"一眼 AI"的中文改写成像真人写的。做法是先**量化打分**找出 AI 痕迹,再**针对性改写**,然后**复检**,循环到达标为止。
> **重要说明**:本技能是一个**可解释的写作质量启发式**,用来指导改写、提升"人味"。它**不保证**能骗过任何第三方 AI 检测器,也不以"过检测"为目标——目标是文字读起来自然、具体、有个人声音。
## 何时触发
用户说"去 AI 味 / 去 AI 化 / 这段太 AI 了 / 帮我改得像人写的 / humanize / 让它不像机器写的",或在公众号、小红书成文后需要润色时。另外两个创作技能(`gzh_article`、`xhs_note`)会用 `load_skill deai_humanize` 主动调用本技能。
## 开工前:读取共享人设记忆
先用 `recall_structured` 取回以下键snake_case改写时全程遵守
- `content_persona` — 用户的人设 / 说话口吻
- `writing_style_gzh` — 公众号文风偏好
- `writing_style_xhs` — 小红书文风偏好
- `banned_words` — 禁用词 / 敏感词
- `signature_blocks` — 固定的开场 / 结尾 / 签名段
取不到就用中性默认,不要编造人设。
## 核心方法:打分 → 改写 → 复检 循环
### 第 1 步 · 打分
`run_skill_script` 执行本技能的 `scripts/ai_trace_score.py`,参数是一个 JSON 字符串:
```
run_skill_script(skill="deai_humanize", script="scripts/ai_trace_score.py",
args='{"text": "<待检测文本>", "platform": "gzh"}')
```
- `platform``gzh`(公众号,容忍连接词略多、句子偏长)或 `xhs`(小红书,鼓励碎句,句长方差权重更低)。
- 脚本返回 JSON
- `score`0100**越高越像 AI**。
- `signals`:每个维度的 `{name, value, weight, note}`(连接词密度、套话命中、具体度缺失、句长齐整度、清单/破折号滥用、段落齐整度)。
- `spans`:命中的具体"扣分片段"(套话、连接词等),改写时优先干掉这些。
- `verdict``human-like` | `some-ai` | `strong-ai`
### 第 2 步 · 改写
`score` **高于阈值(建议 55**,针对返回的 `signals` 里高 `value` 的维度、以及 `spans` 列出的片段动手改。改写原则见下。
### 第 3 步 · 复检并循环
改完再跑一次第 1 步。**循环直到 `score ≤ 55 或已改满 3 轮**。每轮都把这一版比上一版降了多少分、还剩哪些 `spans` 说清楚。3 轮仍不达标就停手,交出当前最佳版本并说明还剩哪些结构性问题(例如通篇是清单体、缺少真实经历)。
## 改写原则AI 痕迹 → 人味)
1. **删升华套话**:干掉"赋能 / 让我们 / 综上所述 / 在……的今天 / 随着……的发展 / 保驾护航 / 数字化转型 / 打造……新生态"这类词。它们是打分里 `cliche_phrases` 的直接来源。
2. **拆连接词骨架**:不要"首先……其次……最后……"一条龙。真人靠语义和语气过渡,不靠标签。
3. **加具体**:塞进真实的**数字、时间、地点、人名、场景、亲身经历**。抽象的"很重要"换成"上周我因为这事多花了两个小时"。这是 `concreteness_deficit` 维度。
4. **句长参差**:长短句交替,敢用三五个字的短句,也敢用一个长句。别让每句都一样长(`sentence_burstiness`)。
5. **第一人称 + 口语化**:多用"我 / 我们 / 咱",用日常说法,允许"其实、说白了、讲真"这类口头语。
6. **以具体代抽象**:能举例就别下定义,能讲故事就别讲道理。
7. **保留不完美感**:适度的口语停顿、自我修正、小情绪,比一板一眼的"完美"更像人。
8. **少用清单和破折号**:不是所有内容都要列点。能用段落叙述的就别拆成 bullet`list_dash_overuse`)。
### 按平台调口吻
| 维度 | gzh公众号 | xhs小红书 |
|------|--------------|--------------|
| 句子 | 沉稳、可稍长,逻辑连贯 | 短、碎、跳,一句一断 |
| 语气 | 克制、有分寸、像老友聊深度话题 | 活泼、直给、带情绪和 emoji |
| 结构 | 段落叙述为主 | 痛点共鸣 + 干货碎句 |
| 人称 | "我 / 我们",偶尔"你" | 大量"我""你""姐妹们" |
改写完务必对照 `banned_words` 再扫一遍,命中就替换或标注。
## 参考
- `scripts/ai_trace_score.py` — 打分脚本(纯 Python 标准库,确定性、无第三方依赖)。
- `references/rewrite_playbook.md` — gzh / xhs 两种口吻的 before/after 改写实例。

View File

@ -0,0 +1,110 @@
# 去 AI 化改写手册Before / After
每个例子标注了打分脚本会命中的信号,以及改写时对应的动作。先跑分找到 `spans`,再对号入座地改。
---
## 一、公众号gzh沉稳克制
### 例 1 · 开头升华套话
**Before**(高分,`cliche_phrases` + `connector_density` 拉满)
> 在人工智能飞速发展的今天,让我们一起探讨这一话题。首先,它非常重要;其次,它影响深远;综上所述,我们必须重视。
命中:`在……的今天`、`让我们`、`首先/其次/综上所述`,且没有任何具体信息。
**After**(低分)
> 上个月团队复盘,有个数字把我惊到了:我们花在"对齐需求"上的时间,比真正写代码还多。这事我琢磨了挺久,今天想掰开说说。
动作:删掉全部套话和连接词骨架;换成一个真实场景 + 具体对比 + 第一人称。
### 例 2 · 抽象结论 → 具体经历
**Before**
> 高效的时间管理能够显著提升个人的工作产出,是通往成功的关键。
命中:`是……的关键`、`concreteness_deficit` 高(无数字、无人称、无场景)。
**After**
> 我以前也不信"时间管理"这套。直到有阵子每天被会议切成八段,一周下来正经活儿只推进了两成。后来我把上午前两小时锁死不排会,产出立刻不一样了。
动作:把抽象论断落到一段亲历,塞进"八段""两成""前两小时"这类具体数字。
### 例 3 · 句长齐整 → 参差
**Before**
> 这个方法可以帮助我们节省时间,这个方法可以帮助我们提高质量,这个方法可以帮助我们减少错误。
命中:`sentence_burstiness`(句子结构、长度高度一致)。
**After**
> 这法子省时间。质量也稳了。最意外的是——出错少了一大半,返工的活儿几乎没了。
动作:打散排比,长短句交替,敢用短句和破折号制造节奏(但别通篇滥用破折号)。
---
## 二、小红书xhs活泼碎句
### 例 1 · 平铺直叙 → 痛点碎句
**Before**`connector_density` 偏高、缺情绪)
> 首先我们需要了解护肤的基本步骤,然后选择适合自己的产品,最后坚持使用就能看到效果。
**After**(低分)
> 姐妹们真的别再乱买了😭
> 我踩过的雷够开一场展了
> 就三步,闭眼跟着做:
> 洁面→精华→防晒
> 坚持一个月,素颜敢出门的那种👍
动作:拆成碎句、每句一断、加 emoji 分段、上来先共情痛点,人称从"我们"换成"我 / 姐妹们"。
### 例 2 · 官方腔标题 → 情绪钩子
**Before**
> 关于提升学习效率的一些方法分享
**After**
> 试了30天我终于治好了"学不进去"😮‍💨
动作加数字30天+ 情绪 + 结果对比,标题控制在 20 字内。
### 例 3 · 空泛干货 → 具体清单
**Before**
> 这款产品性价比很高,值得推荐给大家使用。
命中:`concreteness_deficit`(没有任何具体信息)。
**After**
> 79块用了俩月还剩一半
> 一次一泵、早晚都能用
> 敏感肌亲测不搓泥(我烂脸期都在用)
动作:塞进价格、用量、时长、亲身状态,把"性价比高"翻译成可验证的细节。
---
## 复检提醒
改完一定再跑一次 `ai_trace_score.py`。若 `score` 仍 > 55看返回的 `signals` 里哪一维 `value` 最高,就专门补那一维:
- `cliche_phrases` 高 → 继续删套话。
- `concreteness_deficit` 高 → 继续加数字 / 场景 / 经历。
- `connector_density` 高 → 拆连接词骨架。
- `sentence_burstiness` 高 → 打散齐整的句式。
3 轮封顶,别无限刷分;结构性问题(通篇清单、毫无个人经历)要直接告诉用户。

View File

@ -0,0 +1,110 @@
---
name: gzh_article
description: '公众号图文创作 / 推文 / 官方号文章 (official account article) — 端到端选题→搜集→成文→配图→去AI化→公众号内联样式排版→交付/草稿箱。honors user persona & style memory.'
version: 1.0.0
tags:
- 公众号
- 图文
- 内容创作
- writing
- wechat
platforms:
- macos
- linux
- windows
---
# 公众号图文创作
把一个选题做成可直接粘进公众号编辑器的图文推文。7 个阶段,每步都接到平台真实工具上。
## 开工前:读取共享人设记忆
先用 `recall_structured` 取回并全程遵守:
- `content_persona` — 人设 / 口吻
- `writing_style_gzh` — 公众号文风
- `topic_interests` — 选题方向
- `banned_words` — 禁用 / 敏感词
- `signature_blocks` — 固定开场 / 结尾 / 引导关注段
取不到就用中性默认,不要编造。
## 七步 SOP
### 1. 选题
`web_search``freshness=week`、`language=zh-CN`)围绕 `topic_interests` 找近期热点和角度,`count` 取 58。给用户 35 个候选选题(每个带一句话切入角度),让其确认或补充后再往下。
### 2. 搜集汇总(参考文章)
用户给出参考公众号链接时:
- **优先**用可能存在的 `wechat_article_extract` 工具直接抽正文(若该工具可用,用 `load_skill` 或工具列表确认)。
- 没有该工具,就用 `browser_use`:先 `action=open` 打开链接,再 `action=snapshot` 抓取页面可见正文。
- 把每篇参考的**核心观点、结构、可借鉴角度**提炼成要点。
> **红线**:本技能产出**原创**内容,参考只用于找角度、补事实,并在文末标注引用来源。**严禁洗稿 / 搬运 / 逐段改写**他人文章。
### 3. 成文
按公众号结构模板成文(详见 `references/gzh_structure.md`
1. **钩子引言** — 用具体场景 / 反常识数据 / 一个问题抓住读者。
2. **35 个小标题分节** — 每节一个论点,配**具体案例或数据**,不要空谈。
3. **金句** — 每节或结尾埋一句可摘录的话。
4. **结尾行动号召 + 引导关注** — 用 `signature_blocks` 里的固定收尾。
全程遵守 `writing_style_gzh` + `content_persona`
### 4. 配图
`image_generate``action=generate`
- **封面头图**`aspectRatio=landscape`。prompt 里写清主题、风格、留白、中文标题可读。
- **关键小节配图**:按需为 23 个重点小节各生成一张,风格与封面统一。
`image_generate` 只认 `landscape` / `portrait` / `square` 三种比例,其它比例映射到最近的一个(如 3:4 → portrait
### 5. 去 AI 化
`load_skill deai_humanize`,然后对全文跑它的"打分→改写→复检"循环,`platform=gzh`,目标 `score ≤ 55`
### 6. 违禁词自查
对照 `banned_words``references/compliance_checklist.md`(广告法极限词、虚假宣传、敏感词、侵权风险),命中就**标注并给出替换建议**,不要静默通过。
### 7. 打包交付gzh_package —— 在线预览 + 素材下载)
**默认用 `gzh_package` 交付**。你只需把成稿正文以 **Markdown** 形式传入,服务端会转成公众号内联样式 HTML公众号不认 `<style>` 块和 class一律内联并产出「在线预览链接 + 素材 ziparticle.html / article.md / 封面)+ 可粘贴的内联 HTML」
```
gzh_package(title="<标题>", markdown="<正文 Markdown含小标题/列表/引用/```代码块```/表格>",
coverImageUrl="<第4步封面图链接>", author="<作者/来源>")
```
> ⚠️ **不要自己手写整段内联样式 HTML 再塞进 `write_file` 或 `render_html_image(html=...)`**。大段、转义密集的 HTML 作为单个工具参数在流式传输时会被截断成非法 JSON、导致工具连续失败并触发循环熔断。让 `gzh_package` 在服务端生成 HTML你只传紧凑的 Markdown。
`gzh_package` 返回的**在线预览链接**发给用户预览;满意后:
- 直接进草稿箱:`gzh_publish(action="draft", ...)`**先向用户确认**,发布是对外不可逆动作)。
- 或让用户下载素材 zip / 复制内联 HTML手动粘贴进公众号编辑器。
`references/` 里的 `gzh_layout*.html` 是**服务端配色/排版的参考**`gzh_package` 已内置同款风格);只有当用户明确要**高度定制的特殊版式**、且篇幅不大时,才手写内联 HTML 并用 `render_html_image(html=...)` 出预览图,注意控制体量避免截断。
## 保存自定义模板 / 对话升级技能
当用户对某个自创模板满意、想以后复用时,用 `skill_manage` 把它**存成一个自定义技能**`builtin=false` 才能写入):
1. 首次:`skill_manage(action="create", name="my_gzh_templates", content="<一份 SKILL.md说明这是我的公众号模板库>")`。
2. 存模板:`skill_manage(action="write_file", name="my_gzh_templates", filePath="references/<模板名>.html", content="<内联样式HTML>")`。
3. 以后:`readSkillFile(skillName="my_gzh_templates", filePath="references/<模板名>.html")` 取回复用。
> 本技能 `gzh_article` 是**内置技能,不能直接被编辑**`skill_manage` 会拒绝写内置技能);自定义模板一律存到用户自己的自定义技能里。每次写入都会过安全扫描。
## 参考
- `references/gzh_layout.html` — 移动端优先的公众号内联样式 HTML 模板(通用)。
- `references/gzh_layout_minimal.html` — 极简编辑风模板。
- `references/gzh_layout_business.html` — 商务专业风模板。
- `references/gzh_structure.md` — 文章结构 + 钩子 / 金句方法论。
- `references/compliance_checklist.md` — 合规自查清单(广告法禁用词、敏感词、侵权、极限词)。

View File

@ -0,0 +1,45 @@
# 公众号发布前合规自查清单
发布前逐项过一遍。命中即**标注 + 给替换建议**,不要静默放行。除 `banned_words` 外,重点看下面四类。
## 1. 广告法极限词 / 绝对化用语(《广告法》第九条)
以下词在商业宣传语境下**禁用**,务必替换:
| 类别 | 禁用示例 | 建议替换 |
|------|----------|----------|
| 最高级 | 最、最佳、最好、最优、最强、最便宜、最先进 | "较为""在同类中表现突出" |
| 唯一性 | 第一、唯一、独家、首个、冠军、领导品牌 | 去掉或用可核实的具体排名(需有据) |
| 国字号 | 国家级、世界级、国际级、顶级 | 删除 |
| 绝对承诺 | 100%、绝对、彻底根治、永久、包治、稳赚不赔 | 改为有条件、可验证的表述 |
| 权威背书 | 央视推荐、专家推荐(无依据) | 删除或附真实出处 |
> 注:这些词若出现在**非广告的客观陈述**里(如"这是我个人最喜欢的一段")风险较低,但商业推广语境一律规避。
## 2. 虚假宣传 / 夸大功效
- 医疗、保健、美妆、食品**不得宣称治疗 / 疗效**(如"治愈""抗癌""包瘦""排毒")。
- 数据、案例必须**真实可溯源**,不得编造用户评价或成分功效。
- 投资、理财**不得承诺收益 / 保本**。
## 3. 敏感内容
- 政治敏感、涉黄涉暴涉赌、民族宗教歧视、封建迷信——一律不碰。
- 未经授权的他人肖像、隐私信息不得使用。
## 4. 侵权风险
- **图片**:配图用 `image_generate` 自生成或有授权的素材,不盗用他人作品 / 有版权的影视截图。
- **文字**:本文为原创;参考文章只用于找角度并在文末标注来源,**不逐段搬运洗稿**。
- **商标 / 品牌**:提及第三方品牌时客观陈述,不贬损、不假冒关联。
## 输出格式
自查后给用户一份小结:
```
合规自查:
- [极限词] 第 2 段"最好用的方法" → 建议改为"我最常用的方法"
- [功效] 第 4 段"彻底解决" → 建议改为"明显缓解"
- 其余未见明显风险
```

View File

@ -0,0 +1,50 @@
<!--
WeChat Official Account article template — INLINE STYLES ONLY.
The WeChat editor strips <style> blocks and class attributes, so every
visual rule lives in a style="..." attribute on the element itself.
Mobile-first: base font 16px, generous line-height, 100% max width.
Replace the {{PLACEHOLDER}} tokens and the sample image URLs before use.
Palette: ink #1a1a1a, muted #6b6b6b, accent #2f6fed, hairline #ececec.
-->
<section style="max-width:100%;margin:0 auto;padding:0 4px;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;color:#1a1a1a;font-size:16px;line-height:1.8;letter-spacing:.3px;">
<!-- Title -->
<h1 style="font-size:22px;line-height:1.45;font-weight:700;color:#1a1a1a;margin:8px 0 6px;">{{TITLE}}</h1>
<!-- Byline / meta -->
<p style="font-size:13px;color:#9a9a9a;margin:0 0 20px;">{{AUTHOR}} · {{DATE}}</p>
<!-- Lead / hook paragraph -->
<p style="margin:0 0 18px;color:#3a3a3a;">{{LEAD_PARAGRAPH}}</p>
<!-- Section heading -->
<h2 style="font-size:18px;font-weight:700;color:#1a1a1a;margin:28px 0 12px;padding-left:10px;border-left:4px solid #2f6fed;">{{SECTION_HEADING}}</h2>
<!-- Body paragraph -->
<p style="margin:0 0 16px;">{{BODY_PARAGRAPH}}</p>
<!-- Section image + caption -->
<figure style="margin:20px 0;text-align:center;">
<img src="{{IMAGE_URL}}" alt="{{IMAGE_ALT}}" style="max-width:100%;height:auto;border-radius:8px;display:block;margin:0 auto;" />
<figcaption style="font-size:13px;color:#9a9a9a;margin-top:8px;">{{IMAGE_CAPTION}}</figcaption>
</figure>
<!-- Quote / pull-out callout (金句块) -->
<blockquote style="margin:24px 0;padding:16px 18px;background:#f5f7ff;border-left:4px solid #2f6fed;border-radius:6px;color:#2a2a2a;font-size:17px;font-weight:600;line-height:1.7;">
{{PULL_QUOTE}}
</blockquote>
<!-- Another section -->
<h2 style="font-size:18px;font-weight:700;color:#1a1a1a;margin:28px 0 12px;padding-left:10px;border-left:4px solid #2f6fed;">{{SECTION_HEADING_2}}</h2>
<p style="margin:0 0 16px;">{{BODY_PARAGRAPH_2}}</p>
<!-- Divider -->
<hr style="border:none;border-top:1px solid #ececec;margin:32px 0;" />
<!-- Footer CTA / follow prompt -->
<section style="text-align:center;padding:20px 16px;background:#fafafa;border-radius:10px;margin-top:8px;">
<p style="font-size:15px;color:#3a3a3a;margin:0 0 6px;font-weight:600;">{{CTA_LINE}}</p>
<p style="font-size:14px;color:#6b6b6b;margin:0;">{{FOLLOW_LINE}}</p>
</section>
</section>

View File

@ -0,0 +1,81 @@
<!--
WeChat Official Account article template — BUSINESS / PROFESSIONAL — INLINE STYLES ONLY.
The WeChat editor strips <style> blocks and class attributes, so every
visual rule lives in a style="..." attribute on the element itself.
Structured look: colored header band, numbered section chips, key-point box,
a 3-up stat row, footer CTA. Mobile-first, base font 16px.
Replace the {{PLACEHOLDER}} tokens and the sample image URL before use.
Palette: brand #0e4a8a, brand-dark #0a3a6e, ink #1f2733, muted #6b7583,
panel #eef4fb, hairline #e2e8f1, accent #f2a20c.
-->
<section style="max-width:100%;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;color:#1f2733;font-size:16px;line-height:1.8;letter-spacing:.3px;">
<!-- Colored header band -->
<section style="background:#0e4a8a;background:linear-gradient(135deg,#0e4a8a 0%,#0a3a6e 100%);border-radius:12px;padding:26px 22px;color:#ffffff;">
<p style="font-size:12px;letter-spacing:2px;color:#a9c8ee;margin:0 0 12px;font-weight:600;">{{CATEGORY_LABEL}}</p>
<h1 style="font-size:23px;line-height:1.45;font-weight:800;color:#ffffff;margin:0 0 12px;">{{TITLE}}</h1>
<p style="font-size:13px;color:#c7dcf5;margin:0;">{{AUTHOR}} · {{DATE}}</p>
</section>
<!-- Lead / hook paragraph -->
<p style="margin:22px 4px 8px;color:#3a4453;font-size:16px;">{{LEAD_PARAGRAPH}}</p>
<!-- Section 1 — numbered chip heading -->
<div style="margin:28px 4px 12px;">
<table role="presentation" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
<tr>
<td style="width:34px;height:34px;background:#0e4a8a;border-radius:8px;color:#ffffff;font-size:17px;font-weight:800;text-align:center;line-height:34px;">01</td>
<td style="padding-left:12px;font-size:18px;font-weight:700;color:#1f2733;">{{SECTION_HEADING}}</td>
</tr>
</table>
</div>
<p style="margin:0 4px 16px;">{{BODY_PARAGRAPH}}</p>
<!-- Key-point highlight box -->
<section style="margin:22px 4px;background:#eef4fb;border-radius:10px;border-left:5px solid #f2a20c;padding:16px 18px;">
<p style="font-size:13px;font-weight:700;color:#b9760a;margin:0 0 6px;letter-spacing:1px;">{{KEYPOINT_LABEL}}</p>
<p style="font-size:16px;font-weight:600;color:#1f2733;margin:0;line-height:1.7;">{{KEYPOINT_TEXT}}</p>
</section>
<!-- Section 2 — numbered chip heading -->
<div style="margin:28px 4px 12px;">
<table role="presentation" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
<tr>
<td style="width:34px;height:34px;background:#0e4a8a;border-radius:8px;color:#ffffff;font-size:17px;font-weight:800;text-align:center;line-height:34px;">02</td>
<td style="padding-left:12px;font-size:18px;font-weight:700;color:#1f2733;">{{SECTION_HEADING_2}}</td>
</tr>
</table>
</div>
<p style="margin:0 4px 16px;">{{BODY_PARAGRAPH_2}}</p>
<!-- Data / stat row — three cells -->
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="border-collapse:separate;border-spacing:8px 0;margin:22px 0;table-layout:fixed;">
<tr>
<td style="background:#f6f9fd;border:1px solid #e2e8f1;border-radius:10px;padding:16px 8px;text-align:center;vertical-align:top;">
<div style="font-size:26px;font-weight:800;color:#0e4a8a;line-height:1.2;">{{STAT_1_VALUE}}</div>
<div style="font-size:12px;color:#6b7583;margin-top:6px;">{{STAT_1_LABEL}}</div>
</td>
<td style="background:#f6f9fd;border:1px solid #e2e8f1;border-radius:10px;padding:16px 8px;text-align:center;vertical-align:top;">
<div style="font-size:26px;font-weight:800;color:#0e4a8a;line-height:1.2;">{{STAT_2_VALUE}}</div>
<div style="font-size:12px;color:#6b7583;margin-top:6px;">{{STAT_2_LABEL}}</div>
</td>
<td style="background:#f6f9fd;border:1px solid #e2e8f1;border-radius:10px;padding:16px 8px;text-align:center;vertical-align:top;">
<div style="font-size:26px;font-weight:800;color:#0e4a8a;line-height:1.2;">{{STAT_3_VALUE}}</div>
<div style="font-size:12px;color:#6b7583;margin-top:6px;">{{STAT_3_LABEL}}</div>
</td>
</tr>
</table>
<!-- Section image + caption -->
<figure style="margin:22px 4px;text-align:center;">
<img src="{{IMAGE_URL}}" alt="{{IMAGE_ALT}}" style="max-width:100%;height:auto;border-radius:10px;display:block;margin:0 auto;" />
<figcaption style="font-size:13px;color:#8a929e;margin-top:8px;">{{IMAGE_CAPTION}}</figcaption>
</figure>
<!-- Footer CTA / follow prompt — solid brand card -->
<section style="text-align:center;padding:22px 18px;background:#0e4a8a;border-radius:12px;margin:26px 0 8px;">
<p style="font-size:16px;color:#ffffff;margin:0 0 8px;font-weight:700;">{{CTA_LINE}}</p>
<p style="font-size:13px;color:#a9c8ee;margin:0;">{{FOLLOW_LINE}}</p>
</section>
</section>

View File

@ -0,0 +1,59 @@
<!--
WeChat Official Account article template — MINIMAL / EDITORIAL — INLINE STYLES ONLY.
The WeChat editor strips <style> blocks and class attributes, so every
visual rule lives in a style="..." attribute on the element itself.
Mobile-first: base font 17px, airy line-height, restrained accent, thin hairlines.
Replace the {{PLACEHOLDER}} tokens and the sample image URL before use.
Palette: ink #22201d, muted #8a857d, accent #b08a4f (warm gold), hairline #eae7e1.
-->
<section style="max-width:100%;margin:0 auto;padding:0 6px;font-family:Georgia,'Times New Roman','Songti SC','Noto Serif SC',serif;color:#22201d;font-size:17px;line-height:1.9;letter-spacing:.2px;">
<!-- Kicker / eyebrow label -->
<p style="font-size:12px;letter-spacing:3px;color:#b08a4f;text-transform:uppercase;margin:24px 0 10px;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;">{{KICKER}}</p>
<!-- Title -->
<h1 style="font-size:26px;line-height:1.5;font-weight:700;color:#22201d;margin:0 0 14px;">{{TITLE}}</h1>
<!-- Byline / meta -->
<p style="font-size:13px;color:#a49e94;margin:0 0 4px;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;">{{AUTHOR}} · {{DATE}}</p>
<!-- Thin divider under the header -->
<hr style="border:none;border-top:1px solid #eae7e1;margin:22px 0 28px;" />
<!-- Lead / hook paragraph — slightly larger, softer ink -->
<p style="margin:0 0 26px;color:#4a463f;font-size:18px;line-height:1.85;">{{LEAD_PARAGRAPH}}</p>
<!-- Section heading — centered, quiet, with a short rule underneath -->
<h2 style="font-size:20px;font-weight:700;color:#22201d;margin:38px 0 6px;text-align:center;">{{SECTION_HEADING}}</h2>
<div style="width:36px;height:2px;background:#b08a4f;margin:0 auto 22px;"></div>
<!-- Body paragraph -->
<p style="margin:0 0 20px;">{{BODY_PARAGRAPH}}</p>
<!-- Section image + caption -->
<figure style="margin:30px 0;text-align:center;">
<img src="{{IMAGE_URL}}" alt="{{IMAGE_ALT}}" style="max-width:100%;height:auto;display:block;margin:0 auto;" />
<figcaption style="font-size:13px;color:#a49e94;margin-top:10px;font-style:italic;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;">{{IMAGE_CAPTION}}</figcaption>
</figure>
<!-- Quote / pull-out callout — no box, large centered serif with big quotation mark -->
<blockquote style="margin:34px 0;padding:0 8px;text-align:center;color:#3a362f;font-size:21px;font-weight:600;line-height:1.7;font-style:italic;">
<span style="display:block;font-size:44px;color:#dcd4c6;line-height:1;margin-bottom:6px;">&#8220;</span>
{{PULL_QUOTE}}
</blockquote>
<!-- Another section -->
<h2 style="font-size:20px;font-weight:700;color:#22201d;margin:38px 0 6px;text-align:center;">{{SECTION_HEADING_2}}</h2>
<div style="width:36px;height:2px;background:#b08a4f;margin:0 auto 22px;"></div>
<p style="margin:0 0 20px;">{{BODY_PARAGRAPH_2}}</p>
<!-- Divider -->
<hr style="border:none;border-top:1px solid #eae7e1;margin:40px 0 26px;" />
<!-- Footer CTA / follow prompt — minimal, centered, no fill -->
<section style="text-align:center;padding:8px 16px 24px;">
<p style="font-size:16px;color:#22201d;margin:0 0 8px;font-weight:600;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;">{{CTA_LINE}}</p>
<p style="font-size:13px;color:#a49e94;margin:0;letter-spacing:1px;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;">{{FOLLOW_LINE}}</p>
</section>
</section>

View File

@ -0,0 +1,54 @@
# 公众号文章结构与钩子 / 金句方法论
## 整体结构模板
```
┌─ 钩子引言12 段)
│ 抓注意力:具体场景 / 反常识数据 / 一个扎心的问题
├─ 小节 1小标题
│ 论点 + 具体案例或数据 + 一句金句
├─ 小节 2小标题
│ ……
├─ 小节 35小标题
│ ……
└─ 结尾
升华(克制,别喊口号)+ 行动号召 + 引导关注signature_blocks
```
## 钩子引言4 种开法
1. **场景代入**"周五晚上十点,我还在改第 7 版方案。"
2. **反常识数据**"90% 的人以为自己在'深度工作',其实平均每 6 分钟就被打断一次。"
3. **一个问题**"你有没有过这种感觉——忙了一整天,却说不出自己干了啥?"
4. **冲突 / 反转**"我一直以为多任务是效率,直到它把我拖垮。"
避免用"在……的今天""随着……的发展"这类套话开头(去 AI 化会直接扣分)。
## 小标题写法
- 每个小标题是一个**可独立成立的观点**,不是"背景 / 现状 / 对策"这种流水账标签。
- 好:「先把大石头放进罐子」;差:「第一部分:方法概述」。
- 35 个为宜,太多显得散。
## 每节内容:论点 → 证据 → 金句
- **论点**:一句话讲清这节要说什么。
- **证据****具体**案例、数字、亲身经历、对话。抽象论断必须落地。
- **金句**:一句可被读者截图 / 转发的话。金句要短、有画面或反差感。
- 例:"计划不是用来完成的,是用来对齐的。"
## 结尾
- 升华要**克制**——点到为止,别喊"让我们一起……"这类口号。
- 明确的**行动号召**:让读者做一件具体的小事(留言 / 试一个方法 / 收藏)。
- **引导关注**:用 `signature_blocks` 里既定的固定段,保持人设一致。
## 金句仓库(结构示例,非套用)
金句靠"具体 + 反差 + 短",不是靠华丽词藻。写作时现场生成,别硬塞不贴题的名言。
- 反差型:「越想抓住时间,越留不住。」
- 定义型:「所谓专业,就是把无聊的事重复做对。」
- 行动型:「先完成,再完美。」

View File

@ -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`,大量第一人称和"姐妹们 / 你"。
**话题标签38 个)**
大词 + 中词 + 长尾组合,例:`#护肤` `#敏感肌护肤` `#学生党平价护肤`
### 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}}` 是清单,按模板注释里的格式(每条一个 `<li>`)注入。
**渲染成图**:用 `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="<HTML>")`
> 本技能 `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` — 金句 / 大字引用卡。

View File

@ -0,0 +1,69 @@
<!doctype html>
<!--
Xiaohongshu CONTENT card — clean list style. Vertical 3:4 (1080x1440).
Self-contained: embedded CSS, system font stack, no remote assets.
Replace tokens:
{{TITLE}} — section heading (short)
{{SUBTITLE}} — one supporting line (optional, can be emptied)
{{POINTS}} — inject one <li>...</li> per tip. Wrap key words in
<strong> for emphasis. Keep 36 items so they fit.
Shares the coral/peach palette of the cover card but on a light ground.
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 1080px; height: 1440px; }
body {
font-family: 'PingFang SC','Hiragino Sans GB','Microsoft YaHei',-apple-system,sans-serif;
background: #fff5f2;
color: #2b2b2b;
padding: 92px 84px;
display: flex; flex-direction: column;
}
.kicker {
width: 96px; height: 12px; border-radius: 6px;
background: linear-gradient(90deg,#ff6b81,#ff8e53); margin-bottom: 40px;
}
.title {
font-size: 76px; font-weight: 900; line-height: 1.25; color: #1f1f1f;
}
.subtitle {
font-size: 40px; font-weight: 500; color: #ff5a6e; margin-top: 22px;
}
ul { list-style: none; margin-top: 64px; }
li {
display: flex; align-items: flex-start; gap: 28px;
background: #fff; border-radius: 24px;
padding: 36px 40px; margin-bottom: 34px;
box-shadow: 0 10px 30px rgba(255,107,129,0.10);
font-size: 42px; line-height: 1.5; font-weight: 500; color: #333;
counter-increment: item;
}
/* Numbered coral chip before each item */
li::before {
content: counter(item);
flex: 0 0 auto;
width: 64px; height: 64px; border-radius: 50%;
background: linear-gradient(150deg,#ff6b81,#ff8e53); color: #fff;
font-size: 38px; font-weight: 800;
display: flex; align-items: center; justify-content: center;
}
li strong { color: #ff5a6e; font-weight: 800; }
ul { counter-reset: item; }
</style>
</head>
<body>
<div class="kicker"></div>
<h1 class="title">{{TITLE}}</h1>
<p class="subtitle">{{SUBTITLE}}</p>
<ul>
{{POINTS}}
<!-- Example items (replace):
<li>每天<strong>前两小时</strong>不排会,专注推进核心任务</li>
<li>清单只留<strong>3件</strong>真正重要的事,其余延后</li>
-->
</ul>
</body>
</html>

View File

@ -0,0 +1,64 @@
<!doctype html>
<!--
Xiaohongshu COVER card — bold hero style. Vertical 3:4 (1080x1440).
Self-contained: embedded CSS, system font stack, no remote assets.
Replace tokens: {{TITLE}} (big hook, keep short), {{SUBTITLE}} (one line),
{{BADGE}} (small tag, e.g. "亲测" / "干货"), {{FOOTER}} (handle or teaser).
Optional: swap --bg-image or the gradient for an image_generate portrait URL.
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 1080px; height: 1440px; }
body {
font-family: 'PingFang SC','Hiragino Sans GB','Microsoft YaHei',-apple-system,sans-serif;
background: linear-gradient(150deg, #ff6b81 0%, #ff8e53 100%);
color: #fff;
display: flex;
flex-direction: column;
justify-content: center;
padding: 96px 88px;
position: relative;
overflow: hidden;
}
/* Decorative oversized circle for depth */
.blob {
position: absolute; width: 620px; height: 620px; border-radius: 50%;
background: rgba(255,255,255,0.12); top: -180px; right: -160px;
}
.badge {
display: inline-block; align-self: flex-start;
background: #fff; color: #ff5a6e;
font-size: 34px; font-weight: 800;
padding: 14px 30px; border-radius: 999px;
margin-bottom: 48px; letter-spacing: 2px;
}
.title {
font-size: 108px; font-weight: 900; line-height: 1.22;
letter-spacing: 1px; text-shadow: 0 6px 24px rgba(0,0,0,0.15);
}
/* Highlighter bar behind emphasized runs — wrap key words in <mark> */
.title mark {
background: #fff3b0; color: #d6335a;
padding: 0 12px; border-radius: 8px;
}
.subtitle {
font-size: 46px; font-weight: 600; line-height: 1.5;
margin-top: 44px; color: rgba(255,255,255,0.95);
}
.footer {
position: absolute; bottom: 88px; left: 88px;
font-size: 34px; font-weight: 600; color: rgba(255,255,255,0.9);
}
</style>
</head>
<body>
<div class="blob"></div>
<span class="badge">{{BADGE}}</span>
<h1 class="title">{{TITLE}}</h1>
<p class="subtitle">{{SUBTITLE}}</p>
<div class="footer">{{FOOTER}}</div>
</body>
</html>

View File

@ -0,0 +1,55 @@
<!doctype html>
<!--
Xiaohongshu END card — follow / CTA style. Vertical 3:4 (1080x1440).
Self-contained: embedded CSS, system font stack, no remote assets.
Replace tokens:
{{TITLE}} — closing line / payoff (short, punchy)
{{CTA}} — call to action, e.g. "点赞收藏,别弄丢啦"
{{TAGS}} — hashtag line, e.g. "#效率 #自律 #学生党"
{{HANDLE}}— account name / signature
Deeper coral gradient than the cover, so the set reads as one series.
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 1080px; height: 1440px; }
body {
font-family: 'PingFang SC','Hiragino Sans GB','Microsoft YaHei',-apple-system,sans-serif;
background: radial-gradient(circle at 30% 20%, #ff8e53 0%, #ff5a6e 55%, #e94b6b 100%);
color: #fff;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center; padding: 96px 88px; position: relative;
}
.heart { font-size: 120px; margin-bottom: 40px; }
.title {
font-size: 88px; font-weight: 900; line-height: 1.3;
text-shadow: 0 6px 24px rgba(0,0,0,0.15);
}
.cta {
margin-top: 56px;
background: #fff; color: #ff5a6e;
font-size: 46px; font-weight: 800;
padding: 28px 60px; border-radius: 999px;
box-shadow: 0 12px 30px rgba(0,0,0,0.18);
}
.tags {
margin-top: 60px; font-size: 38px; font-weight: 600;
color: rgba(255,255,255,0.92); line-height: 1.6;
}
.handle {
position: absolute; bottom: 80px;
font-size: 34px; font-weight: 600; color: rgba(255,255,255,0.9);
}
</style>
</head>
<body>
<div class="heart">💛</div>
<h1 class="title">{{TITLE}}</h1>
<div class="cta">{{CTA}}</div>
<p class="tags">{{TAGS}}</p>
<div class="handle">{{HANDLE}}</div>
</body>
</html>

View File

@ -0,0 +1,81 @@
<!doctype html>
<!--
Xiaohongshu QUOTE card — big golden-sentence style. Vertical 3:4 (1080x1440).
Self-contained: embedded CSS, system font stack, no remote assets.
Replace tokens: {{QUOTE}} (the golden sentence, keep it punchy),
{{SOURCE}} (attribution, e.g. "— 《被讨厌的勇气》" or a name),
{{TAG}} (small top label, e.g. "每日一句" / "读书摘录"),
{{FOOTER}} (handle or teaser line).
Wrap a key phrase inside {{QUOTE}} in <em>...</em> to highlight it.
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 1080px; height: 1440px; }
body {
font-family: 'Songti SC','Noto Serif SC','PingFang SC','Microsoft YaHei',serif;
background: #f4efe6;
color: #2a2620;
display: flex;
flex-direction: column;
justify-content: center;
padding: 120px 96px;
position: relative;
overflow: hidden;
}
/* Warm paper texture accents */
.frame {
position: absolute; inset: 44px;
border: 2px solid #d8ccb4; border-radius: 20px;
pointer-events: none;
}
.tag {
align-self: center;
font-family: 'PingFang SC','Microsoft YaHei',sans-serif;
font-size: 30px; font-weight: 600; letter-spacing: 8px;
color: #a9895a; margin-bottom: 64px;
}
/* Oversized decorative opening quotation mark */
.mark {
font-size: 220px; line-height: 0.7; color: #c9a86a;
font-family: Georgia,'Times New Roman',serif;
margin-bottom: 12px; align-self: center;
}
.quote {
font-size: 76px; font-weight: 600; line-height: 1.55;
text-align: center; letter-spacing: 2px;
}
.quote em {
font-style: normal;
background: linear-gradient(180deg, transparent 62%, #f2d79a 62%, #f2d79a 92%, transparent 92%);
padding: 0 6px;
}
.source {
font-family: 'PingFang SC','Microsoft YaHei',sans-serif;
font-size: 40px; font-weight: 500; color: #7a6f5c;
text-align: center; margin-top: 72px;
}
.rule {
width: 120px; height: 3px; background: #c9a86a;
margin: 56px auto 0;
}
.footer {
position: absolute; bottom: 90px; left: 0; right: 0;
text-align: center;
font-family: 'PingFang SC','Microsoft YaHei',sans-serif;
font-size: 32px; font-weight: 500; color: #a9895a;
}
</style>
</head>
<body>
<div class="frame"></div>
<div class="tag">{{TAG}}</div>
<div class="mark">&#8220;</div>
<p class="quote">{{QUOTE}}</p>
<div class="rule"></div>
<p class="source">{{SOURCE}}</p>
<div class="footer">{{FOOTER}}</div>
</body>
</html>

View File

@ -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,

View File

@ -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
// <img data-generated-image>) 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<HTMLImageElement>('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<HTMLAnchorElement>('a[href]')
if (!anchor) return

View File

@ -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/<id>)`; 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 <img src> 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 `<img src="${escapeHtml(safeHref)}" alt="${alt}"`
+ ` class="markdown-generated-image" data-generated-image="1"${titleAttr} />`
}
return `<a href="${escapeHtml(safeHref)}"${titleAttr}${extra}>${innerHtml}</a>`
},
}