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. * *
Xiaohongshu has no official open publishing API for personal notes, and * this tool deliberately does not 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. * *
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