- * Executes schema.sql and tools-sync.sql on every startup.
+ * Loads seed data after Flyway migrations on every startup.
*
diff --git a/mateclaw-server/src/main/java/vip/mate/config/SecurityConfig.java b/mateclaw-server/src/main/java/vip/mate/config/SecurityConfig.java
index 0a07e1d3..ceba7991 100644
--- a/mateclaw-server/src/main/java/vip/mate/config/SecurityConfig.java
+++ b/mateclaw-server/src/main/java/vip/mate/config/SecurityConfig.java
@@ -58,7 +58,9 @@ public class SecurityConfig {
"/api/v1/setup/**",
"/api/v1/channels/webhook/**",
"/api/v1/channels/webchat/**",
- "/api/v1/talk/ws"
+ "/api/v1/talk/ws",
+ // RFC-045: tool-generated files served via unguessable UUID + 10-min TTL
+ "/api/v1/files/generated/**"
).permitAll()
// 所有其他 API 接口需要认证
.requestMatchers("/api/**").authenticated()
diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DocxRenderTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DocxRenderTool.java
new file mode 100644
index 00000000..200c7c20
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DocxRenderTool.java
@@ -0,0 +1,102 @@
+package vip.mate.tool.builtin;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.stereotype.Component;
+import vip.mate.tool.document.GeneratedFileCache;
+import vip.mate.tool.document.MarkdownDocxRenderer;
+
+/**
+ * Render a brand-new .docx from Markdown without ever forking a process.
+ *
+ * The previous path forwarded these requests to {@code skills/docx} which
+ * runs {@code npm install docx} on first use (3-5 minutes). For "create new
+ * document" intents that subprocess is wholly unnecessary; this tool produces
+ * the bytes in the JVM, stashes them in {@link GeneratedFileCache}, and
+ * returns a Markdown link the user can click to download.
+ *
+ *
The skill workflow is still authoritative for editing existing .docx,
+ * tracked changes, and other XML-level operations.
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DocxRenderTool {
+
+ private static final String DOCX_MIME =
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+
+ private final MarkdownDocxRenderer renderer;
+ private final GeneratedFileCache cache;
+
+ @Tool(description = """
+ Render a new .docx file from Markdown text and return a one-time download URL.
+ Use for creating NEW documents: reports, memos, contracts, letters, resumes.
+ Supports: headings (# ## ###), bold (**text**), bullet lists (- item),
+ numbered lists (1. item), tables (| col | col |), plain paragraphs.
+
+ Do NOT use for:
+ - Editing an existing .docx file (use run_skill_script with unpack/edit/pack)
+ - Adding tracked changes or comments (use run_skill_script)
+ - GB/T 9704 official documents (use writeGongwen tool, BmacClaw only)
+
+ Returns a markdown link the user can click to download the file.
+ The link is valid for 10 minutes.
+ """)
+ public String renderDocx(
+ @ToolParam(description = "Document content in Markdown format")
+ String markdown,
+ @ToolParam(description = "Output filename without extension, e.g. 'monthly-report'")
+ String filename,
+ @ToolParam(description = "Page size: A4 or LETTER (default: A4)", required = false)
+ String pageSize) {
+
+ if (markdown == null || markdown.isBlank()) {
+ return "错误:markdown 参数为空,无法生成文档。";
+ }
+
+ String safeName = sanitizeFilename(filename);
+ String displayName = safeName + ".docx";
+ String size = (pageSize == null || pageSize.isBlank()) ? "A4" : pageSize.trim();
+
+ try {
+ long t0 = System.currentTimeMillis();
+ byte[] bytes = renderer.render(markdown, size);
+ String id = cache.put(bytes, displayName, DOCX_MIME);
+ long elapsed = System.currentTimeMillis() - t0;
+ log.info("[DocxRender] generated {} ({} bytes, {}ms, id={})",
+ displayName, bytes.length, elapsed, id);
+
+ String url = "/api/v1/files/generated/" + id;
+ return "文档已生成:[" + displayName + "](" + url + ")(链接 10 分钟内有效)";
+ } catch (Exception e) {
+ log.error("[DocxRender] render failed for {}: {}", displayName, e.getMessage(), e);
+ return "渲染失败:" + e.getMessage();
+ }
+ }
+
+ /**
+ * Strip path separators and other unsafe characters from a user-supplied
+ * filename. Falls back to a generic name when nothing usable remains.
+ */
+ private String sanitizeFilename(String name) {
+ if (name == null) return "document";
+ String trimmed = name.trim();
+ if (trimmed.toLowerCase().endsWith(".docx")) {
+ trimmed = trimmed.substring(0, trimmed.length() - 5);
+ }
+ StringBuilder sb = new StringBuilder(trimmed.length());
+ for (char c : trimmed.toCharArray()) {
+ if (c == '/' || c == '\\' || c == ':' || c == '*' || c == '?'
+ || c == '"' || c == '<' || c == '>' || c == '|' || c < 0x20) {
+ sb.append('_');
+ } else {
+ sb.append(c);
+ }
+ }
+ String cleaned = sb.toString().strip();
+ return cleaned.isEmpty() ? "document" : cleaned;
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/tool/document/GeneratedFileCache.java b/mateclaw-server/src/main/java/vip/mate/tool/document/GeneratedFileCache.java
new file mode 100644
index 00000000..e7bd8547
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/tool/document/GeneratedFileCache.java
@@ -0,0 +1,69 @@
+package vip.mate.tool.document;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * In-memory cache of bytes produced by tools (e.g. {@code DocxRenderTool}) and
+ * served by {@link GeneratedFileController}. Entries expire after {@link #TTL}
+ * and are evicted lazily on every {@link #put} call.
+ *
+ *
The cache is process-local and intentionally not persisted: a JVM restart
+ * invalidates all outstanding download links. The download URL embeds a random
+ * {@link UUID}, which acts as the only access credential.
+ */
+@Slf4j
+@Component
+public class GeneratedFileCache {
+
+ public static final Duration TTL = Duration.ofMinutes(10);
+
+ private final ConcurrentHashMap entries = new ConcurrentHashMap<>();
+
+ public record Entry(byte[] bytes, String filename, String mimeType, long expireAt) {
+
+ public boolean expired() {
+ return System.currentTimeMillis() > expireAt;
+ }
+ }
+
+ /**
+ * Store the given bytes and return a fresh, unguessable identifier.
+ * Callers should embed the id in a URL of the form
+ * {@code /api/v1/files/generated/{id}}.
+ */
+ public String put(byte[] bytes, String filename, String mimeType) {
+ evictExpired();
+ String id = UUID.randomUUID().toString();
+ long expireAt = System.currentTimeMillis() + TTL.toMillis();
+ entries.put(id, new Entry(bytes, filename, mimeType, expireAt));
+ log.debug("Cached generated file id={} filename={} bytes={}", id, filename, bytes.length);
+ return id;
+ }
+
+ /**
+ * Look up an entry. Returns {@link Optional#empty()} if missing or expired
+ * (expired entries are removed as a side-effect).
+ */
+ public Optional get(String id) {
+ Entry entry = entries.get(id);
+ if (entry == null) {
+ return Optional.empty();
+ }
+ if (entry.expired()) {
+ entries.remove(id, entry);
+ return Optional.empty();
+ }
+ return Optional.of(entry);
+ }
+
+ private void evictExpired() {
+ long now = System.currentTimeMillis();
+ entries.entrySet().removeIf(e -> e.getValue().expireAt() <= now);
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/tool/document/GeneratedFileController.java b/mateclaw-server/src/main/java/vip/mate/tool/document/GeneratedFileController.java
new file mode 100644
index 00000000..acd1e3d0
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/tool/document/GeneratedFileController.java
@@ -0,0 +1,59 @@
+package vip.mate.tool.document;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * Serves bytes produced by tools and stashed in {@link GeneratedFileCache}.
+ *
+ * Endpoint is intentionally unauthenticated; the UUID in the URL is the only
+ * access credential. Entries expire after {@link GeneratedFileCache#TTL}.
+ */
+@Tag(name = "Generated Files")
+@RestController
+@RequestMapping("/api/v1/files/generated")
+@RequiredArgsConstructor
+public class GeneratedFileController {
+
+ private final GeneratedFileCache cache;
+
+ @Operation(summary = "Download a tool-generated file by its one-time id")
+ @GetMapping("/{id}")
+ public ResponseEntity> download(@PathVariable String id) {
+ return cache.get(id)
+ .>map(entry -> {
+ String encodedName = URLEncoder.encode(entry.filename(), StandardCharsets.UTF_8)
+ .replace("+", "%20");
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.parseMediaType(entry.mimeType()));
+ // RFC 5987 filename* lets non-ASCII names round-trip in browsers.
+ headers.add(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"" + sanitizeAscii(entry.filename())
+ + "\"; filename*=UTF-8''" + encodedName);
+ headers.setContentLength(entry.bytes().length);
+ return ResponseEntity.ok().headers(headers).body(entry.bytes());
+ })
+ .orElseGet(() -> ResponseEntity.status(404)
+ .body(Map.of("error", "File not found or expired")));
+ }
+
+ private String sanitizeAscii(String name) {
+ StringBuilder sb = new StringBuilder(name.length());
+ for (char c : name.toCharArray()) {
+ sb.append(c < 0x20 || c >= 0x7F || c == '"' || c == '\\' ? '_' : c);
+ }
+ return sb.toString();
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/tool/document/MarkdownDocxRenderer.java b/mateclaw-server/src/main/java/vip/mate/tool/document/MarkdownDocxRenderer.java
new file mode 100644
index 00000000..ac3851e2
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/tool/document/MarkdownDocxRenderer.java
@@ -0,0 +1,350 @@
+package vip.mate.tool.document;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
+import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
+import org.apache.poi.xwpf.usermodel.XWPFAbstractNum;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.apache.poi.xwpf.usermodel.XWPFNumbering;
+import org.apache.poi.xwpf.usermodel.XWPFParagraph;
+import org.apache.poi.xwpf.usermodel.XWPFRun;
+import org.apache.poi.xwpf.usermodel.XWPFTable;
+import org.apache.poi.xwpf.usermodel.XWPFTableCell;
+import org.apache.poi.xwpf.usermodel.XWPFTableRow;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTAbstractNum;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTInd;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTLvl;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPrGeneral;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTShd;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STNumberFormat;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STShd;
+import org.springframework.stereotype.Component;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Render a Markdown string into a Word .docx byte array using Apache POI,
+ * entirely in-process. Replaces the docx-js Node.js subprocess used by
+ * {@code skills/docx} for the "create new document" code path.
+ *
+ * Supported elements: ATX headings (# / ## / ###), bold (**...**),
+ * bullet lists (- / *), numbered lists (1. / 2. ...), pipe-style tables, and
+ * plain paragraphs. Empty Markdown lines are preserved as empty paragraphs.
+ *
+ *
Inline parsing is intentionally minimal: only **bold** is recognized.
+ * For more advanced layouts (images, headers/footers, exact OOXML edits), the
+ * {@code skills/docx} unpack/edit/pack workflow remains the right choice.
+ */
+@Slf4j
+@Component
+public class MarkdownDocxRenderer {
+
+ /** Matches **bold** spans (non-greedy, refuses empty content). */
+ private static final Pattern BOLD = Pattern.compile("\\*\\*(.+?)\\*\\*");
+
+ private static final Pattern UNORDERED_ITEM = Pattern.compile("^\\s*[-*]\\s+(.*)$");
+ private static final Pattern ORDERED_ITEM = Pattern.compile("^\\s*\\d+\\.\\s+(.*)$");
+ private static final Pattern TABLE_SEPARATOR = Pattern.compile("^\\s*\\|?\\s*:?-{3,}:?\\s*(\\|\\s*:?-{3,}:?\\s*)+\\|?\\s*$");
+
+ private static final String LATIN_FONT = "Arial";
+ private static final String CJK_BODY_FONT = "FangSong"; // 仿宋
+ private static final String CJK_HEADING_FONT = "SimHei"; // 黑体
+
+ public byte[] render(String markdown, String pageSize) throws IOException {
+ try (XWPFDocument doc = new XWPFDocument()) {
+ configurePageSize(doc, pageSize);
+ BigInteger bulletNumId = configureNumbering(doc, true);
+ BigInteger decimalNumId = configureNumbering(doc, false);
+
+ List lines = splitLines(markdown == null ? "" : markdown);
+ int i = 0;
+ while (i < lines.size()) {
+ String line = lines.get(i);
+ String stripped = line.strip();
+
+ // Table block: header line + separator + body rows
+ if (stripped.startsWith("|") && i + 1 < lines.size()
+ && TABLE_SEPARATOR.matcher(lines.get(i + 1)).matches()) {
+ int end = i + 2;
+ while (end < lines.size() && lines.get(end).strip().startsWith("|")) {
+ end++;
+ }
+ renderTable(doc, lines.subList(i, end));
+ i = end;
+ continue;
+ }
+
+ if (stripped.startsWith("### ")) {
+ renderHeading(doc, stripped.substring(4), 3);
+ } else if (stripped.startsWith("## ")) {
+ renderHeading(doc, stripped.substring(3), 2);
+ } else if (stripped.startsWith("# ")) {
+ renderHeading(doc, stripped.substring(2), 1);
+ } else {
+ Matcher ul = UNORDERED_ITEM.matcher(line);
+ Matcher ol = ORDERED_ITEM.matcher(line);
+ if (ul.matches()) {
+ renderListItem(doc, ul.group(1), bulletNumId);
+ } else if (ol.matches()) {
+ renderListItem(doc, ol.group(1), decimalNumId);
+ } else if (stripped.isEmpty()) {
+ doc.createParagraph();
+ } else {
+ renderParagraph(doc, line);
+ }
+ }
+ i++;
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ doc.write(baos);
+ return baos.toByteArray();
+ }
+ }
+
+ // ==================== page setup ====================
+
+ private void configurePageSize(XWPFDocument doc, String pageSize) {
+ CTSectPr sectPr = doc.getDocument().getBody().isSetSectPr()
+ ? doc.getDocument().getBody().getSectPr()
+ : doc.getDocument().getBody().addNewSectPr();
+
+ CTPageSz pgSz = sectPr.isSetPgSz() ? sectPr.getPgSz() : sectPr.addNewPgSz();
+ boolean letter = pageSize != null && pageSize.equalsIgnoreCase("LETTER");
+ if (letter) {
+ pgSz.setW(BigInteger.valueOf(12240));
+ pgSz.setH(BigInteger.valueOf(15840));
+ } else {
+ // A4 default
+ pgSz.setW(BigInteger.valueOf(11906));
+ pgSz.setH(BigInteger.valueOf(16838));
+ }
+
+ CTPageMar pgMar = sectPr.isSetPgMar() ? sectPr.getPgMar() : sectPr.addNewPgMar();
+ pgMar.setTop(BigInteger.valueOf(1440));
+ pgMar.setBottom(BigInteger.valueOf(1440));
+ pgMar.setLeft(BigInteger.valueOf(1800));
+ pgMar.setRight(BigInteger.valueOf(1800));
+ pgMar.setHeader(BigInteger.valueOf(720));
+ pgMar.setFooter(BigInteger.valueOf(720));
+ pgMar.setGutter(BigInteger.ZERO);
+ }
+
+ // ==================== numbering ====================
+
+ private BigInteger configureNumbering(XWPFDocument doc, boolean bullet) {
+ XWPFNumbering numbering = doc.createNumbering();
+ CTAbstractNum abstractNum = CTAbstractNum.Factory.newInstance();
+ // Temporary id; XWPFAbstractNum will assign the real one when added.
+ abstractNum.setAbstractNumId(BigInteger.ZERO);
+
+ CTLvl lvl = abstractNum.addNewLvl();
+ lvl.setIlvl(BigInteger.ZERO);
+ lvl.addNewStart().setVal(BigInteger.ONE);
+ if (bullet) {
+ lvl.addNewNumFmt().setVal(STNumberFormat.BULLET);
+ lvl.addNewLvlText().setVal("•");
+ } else {
+ lvl.addNewNumFmt().setVal(STNumberFormat.DECIMAL);
+ lvl.addNewLvlText().setVal("%1.");
+ }
+ CTPPrGeneral ppr = lvl.addNewPPr();
+ CTInd ind = ppr.addNewInd();
+ ind.setLeft(BigInteger.valueOf(720));
+ ind.setHanging(BigInteger.valueOf(360));
+
+ XWPFAbstractNum xwpfAbstractNum = new XWPFAbstractNum(abstractNum);
+ BigInteger absNumId = numbering.addAbstractNum(xwpfAbstractNum);
+ return numbering.addNum(absNumId);
+ }
+
+ // ==================== headings & paragraphs ====================
+
+ private void renderHeading(XWPFDocument doc, String text, int level) {
+ XWPFParagraph p = doc.createParagraph();
+ p.setStyle("Heading" + level);
+ // Spacing before/after, in twentieths of a point.
+ switch (level) {
+ case 1 -> { p.setSpacingBefore(240); p.setSpacingAfter(120); }
+ case 2 -> { p.setSpacingBefore(160); p.setSpacingAfter(80); }
+ default -> { p.setSpacingBefore(120); p.setSpacingAfter(60); }
+ }
+ renderInline(p, text, true, level);
+ }
+
+ private void renderParagraph(XWPFDocument doc, String text) {
+ XWPFParagraph p = doc.createParagraph();
+ p.setAlignment(ParagraphAlignment.LEFT);
+ renderInline(p, text, false, 0);
+ }
+
+ private void renderListItem(XWPFDocument doc, String text, BigInteger numId) {
+ XWPFParagraph p = doc.createParagraph();
+ p.setNumID(numId);
+ renderInline(p, text, false, 0);
+ }
+
+ // ==================== inline (bold) ====================
+
+ private void renderInline(XWPFParagraph p, String text, boolean heading, int headingLevel) {
+ if (text == null || text.isEmpty()) {
+ // Make sure even empty headings still produce a run so style applies.
+ createRun(p, "", heading, headingLevel, false);
+ return;
+ }
+ Matcher m = BOLD.matcher(text);
+ int last = 0;
+ while (m.find()) {
+ if (m.start() > last) {
+ createRun(p, text.substring(last, m.start()), heading, headingLevel, false);
+ }
+ createRun(p, m.group(1), heading, headingLevel, true);
+ last = m.end();
+ }
+ if (last < text.length()) {
+ createRun(p, text.substring(last), heading, headingLevel, false);
+ }
+ }
+
+ private void createRun(XWPFParagraph p, String text, boolean heading, int headingLevel, boolean bold) {
+ XWPFRun run = p.createRun();
+ run.setText(text);
+ run.setUnderline(UnderlinePatterns.NONE);
+
+ // Font sizes per RFC §4.2.
+ int halfPoints;
+ if (heading) {
+ halfPoints = switch (headingLevel) {
+ case 1 -> 40; // 20pt
+ case 2 -> 32; // 16pt
+ default -> 28; // 14pt
+ };
+ run.setBold(true);
+ } else {
+ halfPoints = 24; // 12pt
+ run.setBold(bold);
+ }
+ run.setFontSize(halfPoints / 2);
+
+ // Latin + East-Asian fonts. Each run is freshly created, so we always
+ // append a brand new child rather than try to reuse one.
+ CTRPr rPr = run.getCTR().isSetRPr() ? run.getCTR().getRPr() : run.getCTR().addNewRPr();
+ CTFonts fonts = rPr.sizeOfRFontsArray() > 0 ? rPr.getRFontsArray(0) : rPr.addNewRFonts();
+ fonts.setAscii(LATIN_FONT);
+ fonts.setHAnsi(LATIN_FONT);
+ fonts.setCs(LATIN_FONT);
+ fonts.setEastAsia(heading ? CJK_HEADING_FONT : CJK_BODY_FONT);
+ }
+
+ // ==================== tables ====================
+
+ private void renderTable(XWPFDocument doc, List tableLines) {
+ List rows = new ArrayList<>(tableLines.size());
+ for (int idx = 0; idx < tableLines.size(); idx++) {
+ if (idx == 1) continue; // skip the |---|---| separator
+ rows.add(parseTableRow(tableLines.get(idx)));
+ }
+ if (rows.isEmpty()) return;
+
+ int cols = 0;
+ for (String[] row : rows) cols = Math.max(cols, row.length);
+
+ XWPFTable table = doc.createTable(rows.size(), cols);
+ styleTableBorders(table);
+
+ for (int r = 0; r < rows.size(); r++) {
+ String[] cells = rows.get(r);
+ XWPFTableRow row = table.getRow(r);
+ for (int c = 0; c < cols; c++) {
+ XWPFTableCell cell = row.getCell(c);
+ String value = c < cells.length ? cells[c] : "";
+
+ // POI auto-creates an empty paragraph in each new cell — reuse it.
+ cell.removeParagraph(0);
+ XWPFParagraph p = cell.addParagraph();
+ renderInline(p, value, false, 0);
+
+ if (r == 0) {
+ shadeHeaderCell(cell);
+ for (XWPFRun run : p.getRuns()) {
+ run.setBold(true);
+ }
+ }
+ }
+ }
+ }
+
+ private String[] parseTableRow(String line) {
+ String trimmed = line.strip();
+ if (trimmed.startsWith("|")) trimmed = trimmed.substring(1);
+ if (trimmed.endsWith("|")) trimmed = trimmed.substring(0, trimmed.length() - 1);
+ String[] parts = trimmed.split("\\|", -1);
+ for (int i = 0; i < parts.length; i++) {
+ parts[i] = parts[i].strip();
+ }
+ return parts;
+ }
+
+ private void styleTableBorders(XWPFTable table) {
+ CTTblPr tblPr = table.getCTTbl().getTblPr() != null
+ ? table.getCTTbl().getTblPr()
+ : table.getCTTbl().addNewTblPr();
+ CTTblBorders borders = tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders();
+ applyBorder(borders.isSetTop() ? borders.getTop() : borders.addNewTop());
+ applyBorder(borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom());
+ applyBorder(borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft());
+ applyBorder(borders.isSetRight() ? borders.getRight() : borders.addNewRight());
+ applyBorder(borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH());
+ applyBorder(borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV());
+ }
+
+ private void applyBorder(CTBorder border) {
+ border.setVal(STBorder.SINGLE);
+ border.setSz(BigInteger.valueOf(4));
+ border.setColor("999999");
+ }
+
+ private void shadeHeaderCell(XWPFTableCell cell) {
+ CTTcPr tcPr = cell.getCTTc().getTcPr() != null ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
+ CTShd shd = tcPr.isSetShd() ? tcPr.getShd() : tcPr.addNewShd();
+ shd.setVal(STShd.CLEAR);
+ shd.setColor("auto");
+ shd.setFill("E0E0E0");
+ }
+
+ // ==================== utils ====================
+
+ private List splitLines(String text) {
+ List out = new ArrayList<>();
+ int start = 0;
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if (c == '\n') {
+ int end = i;
+ if (end > start && text.charAt(end - 1) == '\r') end--;
+ out.add(text.substring(start, end));
+ start = i + 1;
+ }
+ }
+ if (start <= text.length()) {
+ out.add(text.substring(start));
+ }
+ return out;
+ }
+}
diff --git a/mateclaw-server/src/main/resources/db/data-en.sql b/mateclaw-server/src/main/resources/db/data-en.sql
index 2d3b62b0..43680cb4 100644
--- a/mateclaw-server/src/main/resources/db/data-en.sql
+++ b/mateclaw-server/src/main/resources/db/data-en.sql
@@ -389,6 +389,11 @@ MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name,
KEY (id)
VALUES (1000000018, 'CronJobTool', 'Scheduled Tasks', 'Create, list, enable/disable, and delete scheduled tasks (cron jobs) through chat. Supports 5-field cron expressions for flexible scheduling.', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0);
+-- Built-in tool: DOCX Render (RFC-045 — in-process Apache POI, millisecond .docx creation)
+MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000019, 'DocxRenderTool', 'DOCX Render', 'Render Markdown directly into a .docx and return a one-time download link. In-process Apache POI implementation, no Node.js subprocess; supports headings, bold, lists, tables. Preferred tool for creating new documents.', 'builtin', 'docxRenderTool', '📝', TRUE, TRUE, NOW(), NOW(), 0);
+
-- Example MCP Server: Filesystem (see MateClaw docs mcpServers.filesystem)
MERGE INTO mate_mcp_server (
id, name, description, transport, url, headers_json, command, args_json, env_json, cwd,
@@ -512,6 +517,22 @@ MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author,
KEY (id)
VALUES (1000000015, 'steve_jobs_perspective', 'Steve Jobs thinking OS. Analyze products, evaluate decisions, and give feedback through Jobs'' perspective, using his six mental models and distinctive expression style.', 'builtin', '🍎', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'persona,jobs,product,strategy,thinking', NOW(), NOW(), 0);
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000016, 'make_plan', 'When a task requires multi-step breakdown or uncertain execution path, request a step-by-step actionable plan from a stronger Agent, then execute it yourself.', 'builtin', '🗺️', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'plan,delegate,agent,collaboration', NOW(), NOW(), 0);
+
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000017, 'chat_with_agent', 'When you need to consult another Agent, seek help, or the user explicitly requests an Agent to participate, use this skill for single or parallel delegation.', 'builtin', '💬', '1.2.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'agent,chat,collaborate,delegate', NOW(), NOW(), 0);
+
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000018, 'channel_message', 'Use when you need to proactively push one-way messages to users, sessions, or channels. For task completion notifications, scheduled reminders, and async result delivery.', 'builtin', '📤', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'channel,message,push,notify,dingtalk,feishu', NOW(), NOW(), 0);
+
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000019, 'multi_agent_collaboration', 'When a task requires the professional capabilities of multiple Agents, orchestrate parallel or serial multi-agent collaboration and integrate results.', 'builtin', '🤝', '1.4.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'multi-agent,collaboration,orchestration,parallel', NOW(), NOW(), 0);
+
-- Populate skill_content for key built-in skills (SKILL.md execution protocol)
-- NOTE: For pdf/docx/pptx/xlsx/himalaya, the authoritative SKILL.md is bundled in
-- classpath:skills/{name}/ and auto-synced to workspace on startup.
diff --git a/mateclaw-server/src/main/resources/db/data-mysql-en.sql b/mateclaw-server/src/main/resources/db/data-mysql-en.sql
index 6905fc70..119b16fd 100644
--- a/mateclaw-server/src/main/resources/db/data-mysql-en.sql
+++ b/mateclaw-server/src/main/resources/db/data-mysql-en.sql
@@ -390,6 +390,11 @@ INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name
VALUES (1000000018, 'CronJobTool', 'Scheduled Tasks', 'Create, list, enable/disable, and delete scheduled tasks (cron jobs) through chat. Supports 5-field cron expressions.', 'builtin', 'cronJobTool', '⏰', 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);
+-- Built-in tool: DOCX Render (RFC-045 — in-process Apache POI, millisecond .docx creation)
+INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
+VALUES (1000000019, 'DocxRenderTool', 'DOCX Render', 'Render Markdown directly into a .docx and return a one-time download link. In-process Apache POI implementation, no Node.js subprocess; supports headings, bold, lists, tables. Preferred tool for creating new documents.', 'builtin', 'docxRenderTool', '📝', 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);
+
-- Example MCP Server: Filesystem (see MateClaw docs mcpServers.filesystem)
INSERT INTO mate_mcp_server (id, name, description, transport, url, headers_json, command, args_json, env_json, cwd,
enabled, connect_timeout_seconds, read_timeout_seconds, last_status, last_error,
@@ -511,6 +516,22 @@ INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author
VALUES (1000000015, 'steve_jobs_perspective', 'Steve Jobs thinking OS. Analyze products, evaluate decisions, and give feedback through Jobs'' perspective, using his six mental models and distinctive expression style.', 'builtin', '🍎', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'persona,jobs,product,strategy,thinking', NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000016, 'make_plan', 'When a task requires multi-step breakdown or uncertain execution path, request a step-by-step actionable plan from a stronger Agent, then execute it yourself.', 'builtin', '🗺️', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'plan,delegate,agent,collaboration', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
+
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000017, 'chat_with_agent', 'When you need to consult another Agent, seek help, or the user explicitly requests an Agent to participate, use this skill for single or parallel delegation.', 'builtin', '💬', '1.2.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'agent,chat,collaborate,delegate', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
+
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000018, 'channel_message', 'Use when you need to proactively push one-way messages to users, sessions, or channels. For task completion notifications, scheduled reminders, and async result delivery.', 'builtin', '📤', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'channel,message,push,notify,dingtalk,feishu', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
+
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000019, 'multi_agent_collaboration', 'When a task requires the professional capabilities of multiple Agents, orchestrate parallel or serial multi-agent collaboration and integrate results.', 'builtin', '🤝', '1.4.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'multi-agent,collaboration,orchestration,parallel', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
+
-- Populate skill_content for key built-in skills (SKILL.md execution protocol)
-- NOTE: For pdf/docx/pptx/xlsx/himalaya, the authoritative SKILL.md is bundled in
-- classpath:skills/{name}/ and auto-synced to workspace on startup.
diff --git a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql
index 150d92c0..9ea9b89b 100644
--- a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql
+++ b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql
@@ -390,6 +390,11 @@ INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name
VALUES (1000000018, 'CronJobTool', '定时任务', '通过对话创建、查看、启停和删除定时任务。支持 5 字段 cron 表达式,灵活设定执行时间。', 'builtin', 'cronJobTool', '⏰', 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);
+-- 内置工具:DOCX 渲染(RFC-045 — 进程内 Apache POI,毫秒级新建 .docx)
+INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
+VALUES (1000000019, 'DocxRenderTool', 'DOCX 渲染', '将 Markdown 直接渲染为 .docx 并返回一次性下载链接。进程内 Apache POI 实现,无需 Node.js 子进程;支持标题、加粗、列表、表格。新建文档场景的首选工具。', 'builtin', 'docxRenderTool', '📝', 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);
+
-- 示例 MCP Server:Filesystem(参考 MateClaw 文档中的 mcpServers.filesystem)
INSERT INTO mate_mcp_server (
id, name, description, transport, url, headers_json, command, args_json, env_json, cwd,
@@ -513,6 +518,22 @@ INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author
VALUES (1000000015, 'steve_jobs_perspective', '史蒂夫·乔布斯思维操作系统。以乔布斯视角审视产品、评估决策、提供反馈,运用其六大心智模型和独特表达风格。', 'builtin', '🍎', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'persona,jobs,product,strategy,thinking', NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000016, 'make_plan', '当任务需要多步拆解或不确定执行路径时,向更强 Agent 请求一份分步可落地的执行计划,由当前 Agent 自己执行。', 'builtin', '🗺️', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'plan,delegate,agent,collaboration', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
+
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000017, 'chat_with_agent', '当需要咨询其他 Agent、寻求帮助或用户明确要求某个 Agent 参与时,使用本技能进行单次或并行委托。', 'builtin', '💬', '1.2.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'agent,chat,collaborate,delegate', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
+
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000018, 'channel_message', '当需要主动向用户、会话或渠道单向推送消息时使用。任务完成通知、定时提醒、异步结果回推等场景。', 'builtin', '📤', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'channel,message,push,notify,dingtalk,feishu', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
+
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000019, 'multi_agent_collaboration', '当任务需要多个 Agent 的专业能力协同完成时,编排多 Agent 并行或串行协作,整合各方结果。', 'builtin', '🤝', '1.4.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'multi-agent,collaboration,orchestration,parallel', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
+
-- 为关键 builtin skill 填充 skill_content(SKILL.md 执行协议)
-- NOTE: For pdf/docx/pptx/xlsx/himalaya, the authoritative SKILL.md is bundled in
-- classpath:skills/{name}/ and auto-synced to workspace on startup.
diff --git a/mateclaw-server/src/main/resources/db/data-zh.sql b/mateclaw-server/src/main/resources/db/data-zh.sql
index e945efdd..bedf88a3 100644
--- a/mateclaw-server/src/main/resources/db/data-zh.sql
+++ b/mateclaw-server/src/main/resources/db/data-zh.sql
@@ -395,6 +395,11 @@ MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name,
KEY (id)
VALUES (1000000018, 'CronJobTool', '定时任务', '通过对话创建、查看、启停和删除定时任务。支持 5 字段 cron 表达式,灵活设定执行时间。', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0);
+-- 内置工具:DOCX 渲染(RFC-045 — 进程内 Apache POI,毫秒级新建 .docx)
+MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000019, 'DocxRenderTool', 'DOCX 渲染', '将 Markdown 直接渲染为 .docx 并返回一次性下载链接。进程内 Apache POI 实现,无需 Node.js 子进程;支持标题、加粗、列表、表格。新建文档场景的首选工具。', 'builtin', 'docxRenderTool', '📝', TRUE, TRUE, NOW(), NOW(), 0);
+
-- 示例 MCP Server:Filesystem(参考 MateClaw 文档中的 mcpServers.filesystem)
MERGE INTO mate_mcp_server (
id, name, description, transport, url, headers_json, command, args_json, env_json, cwd,
@@ -518,6 +523,22 @@ MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author,
KEY (id)
VALUES (1000000015, 'steve_jobs_perspective', '史蒂夫·乔布斯思维操作系统。以乔布斯视角审视产品、评估决策、提供反馈,运用其六大心智模型和独特表达风格。', 'builtin', '🍎', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'persona,jobs,product,strategy,thinking', NOW(), NOW(), 0);
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000016, 'make_plan', '当任务需要多步拆解或不确定执行路径时,向更强 Agent 请求一份分步可落地的执行计划,由当前 Agent 自己执行。', 'builtin', '🗺️', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'plan,delegate,agent,collaboration', NOW(), NOW(), 0);
+
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000017, 'chat_with_agent', '当需要咨询其他 Agent、寻求帮助或用户明确要求某个 Agent 参与时,使用本技能进行单次或并行委托。', 'builtin', '💬', '1.2.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'agent,chat,collaborate,delegate', NOW(), NOW(), 0);
+
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000018, 'channel_message', '当需要主动向用户、会话或渠道单向推送消息时使用。任务完成通知、定时提醒、异步结果回推等场景。', 'builtin', '📤', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'channel,message,push,notify,dingtalk,feishu', NOW(), NOW(), 0);
+
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000019, 'multi_agent_collaboration', '当任务需要多个 Agent 的专业能力协同完成时,编排多 Agent 并行或串行协作,整合各方结果。', 'builtin', '🤝', '1.4.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'multi-agent,collaboration,orchestration,parallel', NOW(), NOW(), 0);
+
-- 为关键 builtin skill 填充 skill_content(SKILL.md 执行协议)
-- NOTE: For pdf/docx/pptx/xlsx/himalaya, the authoritative SKILL.md is bundled in
-- classpath:skills/{name}/ and auto-synced to workspace on startup.
diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V30__register_collab_skills.sql b/mateclaw-server/src/main/resources/db/migration/h2/V30__register_collab_skills.sql
new file mode 100644
index 00000000..45939741
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/h2/V30__register_collab_skills.sql
@@ -0,0 +1,36 @@
+-- Register 4 collaboration skills introduced in RFC-044.
+-- These were previously only in seed data files; this migration ensures they exist
+-- in all environments (including existing installs that have already run seed data).
+-- Ref: rfc-044-skill-md-completion-2026-04-23
+
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000016, 'make_plan',
+ '当任务需要多步拆解或不确定执行路径时,向更强 Agent 请求一份分步可落地的执行计划,由当前 Agent 自己执行。',
+ 'builtin', '🗺️', '1.3.0', 'MateClaw',
+ '{"upstream":"mateclaw","entryFile":"SKILL.md"}',
+ TRUE, TRUE, 'plan,delegate,agent,collaboration', NOW(), NOW(), 0);
+
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000017, 'chat_with_agent',
+ '当需要咨询其他 Agent、寻求帮助或用户明确要求某个 Agent 参与时,使用本技能进行单次或并行委托。',
+ 'builtin', '💬', '1.2.0', 'MateClaw',
+ '{"upstream":"mateclaw","entryFile":"SKILL.md"}',
+ TRUE, TRUE, 'agent,chat,collaborate,delegate', NOW(), NOW(), 0);
+
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000018, 'channel_message',
+ '当需要主动向用户、会话或渠道单向推送消息时使用。任务完成通知、定时提醒、异步结果回推等场景。',
+ 'builtin', '📤', '1.3.0', 'MateClaw',
+ '{"upstream":"mateclaw","entryFile":"SKILL.md"}',
+ TRUE, TRUE, 'channel,message,push,notify,dingtalk,feishu', NOW(), NOW(), 0);
+
+MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000019, 'multi_agent_collaboration',
+ '当任务需要多个 Agent 的专业能力协同完成时,编排多 Agent 并行或串行协作,整合各方结果。',
+ 'builtin', '🤝', '1.4.0', 'MateClaw',
+ '{"upstream":"mateclaw","entryFile":"SKILL.md"}',
+ TRUE, TRUE, 'multi-agent,collaboration,orchestration,parallel', NOW(), NOW(), 0);
diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V31__register_docx_render_tool.sql b/mateclaw-server/src/main/resources/db/migration/h2/V31__register_docx_render_tool.sql
new file mode 100644
index 00000000..e53a35aa
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/h2/V31__register_docx_render_tool.sql
@@ -0,0 +1,5 @@
+-- V31: Register DocxRenderTool as built-in tool (RFC-045)
+-- Idempotent: MERGE INTO updates existing row when id matches.
+MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
+KEY (id)
+VALUES (1000000019, 'DocxRenderTool', 'DOCX Render', 'Render Markdown directly into a .docx and return a one-time download link. In-process Apache POI implementation, no Node.js subprocess; supports headings, bold, lists, tables. Preferred tool for creating new documents.', 'builtin', 'docxRenderTool', '📝', TRUE, TRUE, NOW(), NOW(), 0);
diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V30__register_collab_skills.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V30__register_collab_skills.sql
new file mode 100644
index 00000000..d7c3be0a
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/mysql/V30__register_collab_skills.sql
@@ -0,0 +1,80 @@
+-- Register 4 collaboration skills introduced in RFC-044.
+-- These were previously only in seed data files; this migration ensures they exist
+-- in all environments (including existing installs that have already run seed data).
+-- Ref: rfc-044-skill-md-completion-2026-04-23
+
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000016, 'make_plan',
+ '当任务需要多步拆解或不确定执行路径时,向更强 Agent 请求一份分步可落地的执行计划,由当前 Agent 自己执行。',
+ 'builtin', '🗺️', '1.3.0', 'MateClaw',
+ '{"upstream":"mateclaw","entryFile":"SKILL.md"}',
+ TRUE, TRUE, 'plan,delegate,agent,collaboration', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE
+ description = VALUES(description),
+ skill_type = VALUES(skill_type),
+ icon = VALUES(icon),
+ version = VALUES(version),
+ author = VALUES(author),
+ config_json = VALUES(config_json),
+ enabled = VALUES(enabled),
+ builtin = VALUES(builtin),
+ tags = VALUES(tags),
+ update_time = VALUES(update_time),
+ deleted = VALUES(deleted);
+
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000017, 'chat_with_agent',
+ '当需要咨询其他 Agent、寻求帮助或用户明确要求某个 Agent 参与时,使用本技能进行单次或并行委托。',
+ 'builtin', '💬', '1.2.0', 'MateClaw',
+ '{"upstream":"mateclaw","entryFile":"SKILL.md"}',
+ TRUE, TRUE, 'agent,chat,collaborate,delegate', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE
+ description = VALUES(description),
+ skill_type = VALUES(skill_type),
+ icon = VALUES(icon),
+ version = VALUES(version),
+ author = VALUES(author),
+ config_json = VALUES(config_json),
+ enabled = VALUES(enabled),
+ builtin = VALUES(builtin),
+ tags = VALUES(tags),
+ update_time = VALUES(update_time),
+ deleted = VALUES(deleted);
+
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000018, 'channel_message',
+ '当需要主动向用户、会话或渠道单向推送消息时使用。任务完成通知、定时提醒、异步结果回推等场景。',
+ 'builtin', '📤', '1.3.0', 'MateClaw',
+ '{"upstream":"mateclaw","entryFile":"SKILL.md"}',
+ TRUE, TRUE, 'channel,message,push,notify,dingtalk,feishu', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE
+ description = VALUES(description),
+ skill_type = VALUES(skill_type),
+ icon = VALUES(icon),
+ version = VALUES(version),
+ author = VALUES(author),
+ config_json = VALUES(config_json),
+ enabled = VALUES(enabled),
+ builtin = VALUES(builtin),
+ tags = VALUES(tags),
+ update_time = VALUES(update_time),
+ deleted = VALUES(deleted);
+
+INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
+VALUES (1000000019, 'multi_agent_collaboration',
+ '当任务需要多个 Agent 的专业能力协同完成时,编排多 Agent 并行或串行协作,整合各方结果。',
+ 'builtin', '🤝', '1.4.0', 'MateClaw',
+ '{"upstream":"mateclaw","entryFile":"SKILL.md"}',
+ TRUE, TRUE, 'multi-agent,collaboration,orchestration,parallel', NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE
+ description = VALUES(description),
+ skill_type = VALUES(skill_type),
+ icon = VALUES(icon),
+ version = VALUES(version),
+ author = VALUES(author),
+ config_json = VALUES(config_json),
+ enabled = VALUES(enabled),
+ builtin = VALUES(builtin),
+ tags = VALUES(tags),
+ update_time = VALUES(update_time),
+ deleted = VALUES(deleted);
diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V31__register_docx_render_tool.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V31__register_docx_render_tool.sql
new file mode 100644
index 00000000..4d45e1a5
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/mysql/V31__register_docx_render_tool.sql
@@ -0,0 +1,5 @@
+-- V31: Register DocxRenderTool as built-in tool (RFC-045)
+-- Idempotent: ON DUPLICATE KEY UPDATE keeps the row in sync if it already exists.
+INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
+VALUES (1000000019, 'DocxRenderTool', 'DOCX Render', 'Render Markdown directly into a .docx and return a one-time download link. In-process Apache POI implementation, no Node.js subprocess; supports headings, bold, lists, tables. Preferred tool for creating new documents.', 'builtin', 'docxRenderTool', '📝', TRUE, TRUE, NOW(), NOW(), 0)
+ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), bean_name=VALUES(bean_name), icon=VALUES(icon), update_time=VALUES(update_time);
diff --git a/mateclaw-server/src/main/resources/skills/browser_cdp/SKILL.md b/mateclaw-server/src/main/resources/skills/browser_cdp/SKILL.md
new file mode 100644
index 00000000..43d533b0
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/browser_cdp/SKILL.md
@@ -0,0 +1,100 @@
+---
+name: browser_cdp
+version: "1.2.0"
+description: "通过 Chrome DevTools Protocol (CDP) 连接已运行的浏览器,或扫描本机 CDP 端口,用于远程调试与多工具共享浏览器实例。"
+dependencies:
+ tools:
+ - browser_use
+ - execute_shell_command
+---
+
+# 浏览器 CDP 使用
+
+仅在用户**明确**提出以下需求时使用本技能:
+- 连接到已打开的 Chrome(附着现有实例)
+- 扫描本机有哪些 CDP 端口可用
+- 用固定调试端口启动浏览器
+- 让多个工具 / Agent 共享同一浏览器
+
+**普通"打开浏览器"需求请用 `browser_visible` 技能。**
+
+## 场景一:扫描本机 CDP 端口
+
+```
+browser_use(action="list_cdp_targets")
+```
+
+扫描指定端口:
+```
+browser_use(action="list_cdp_targets", cdpPort=9222)
+```
+
+## 场景二:连接已有 Chrome
+
+用户已手动用调试端口启动了 Chrome:
+
+```
+browser_use(action="connect_cdp", url="http://localhost:9222")
+```
+
+连接后可正常使用 `open` / `snapshot` / `click` / `type` 等操作。
+
+> **注意**:`stop` 只断开连接,**不会关闭**用户的 Chrome 进程。
+
+## 场景三:启动时指定固定 CDP 端口
+
+```
+browser_use(action="start", cdpPort=9222)
+```
+
+可见窗口 + 固定端口:
+```
+browser_use(action="start", headed=true, cdpPort=9222)
+```
+
+> **仅在用户明确要求固定端口时才传 `cdpPort`**,否则让系统自动选择空闲端口以避免冲突。
+
+## 手动启动 Chrome(带调试端口)
+
+若用户需要先手动打开 Chrome 再连接:
+
+**macOS:**
+```
+execute_shell_command(
+ command="\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\" --remote-debugging-port=9222 --no-first-run --no-default-browser-check"
+)
+```
+
+**Windows:**
+```
+execute_shell_command(
+ command="\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --remote-debugging-port=9222 --no-first-run"
+)
+```
+
+**Linux:**
+```
+execute_shell_command(
+ command="google-chrome --remote-debugging-port=9222 --no-first-run &"
+)
+```
+
+## stop 行为区别
+
+| 启动方式 | stop 效果 |
+|---------|----------|
+| `browser_use(action="start")` | 断开连接 + **关闭** Chrome 进程 |
+| `browser_use(action="connect_cdp", ...)` | 仅断开连接,**不关闭**外部 Chrome |
+
+## 与 browser_visible 的分工
+
+| 需求 | 用哪个技能 |
+|------|-----------|
+| 显示 / 隐藏浏览器窗口 | `browser_visible` |
+| 连接 / 暴露 / 扫描 CDP 端口 | `browser_cdp`(本技能) |
+
+## 安全提醒
+
+- CDP 端口暴露后,本机任何进程都可以控制该浏览器(包括读取 Cookies)
+- 不要在公共 / 多用户服务器上暴露 CDP 端口
+- 用完后及时 `stop` 断开连接
diff --git a/mateclaw-server/src/main/resources/skills/browser_visible/SKILL.md b/mateclaw-server/src/main/resources/skills/browser_visible/SKILL.md
new file mode 100644
index 00000000..b6842d6e
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/browser_visible/SKILL.md
@@ -0,0 +1,93 @@
+---
+name: browser_visible
+version: "1.3.0"
+description: "以可见模式启动真实浏览器窗口,适用于演示、调试或需要人工参与的场景。控制 headed/cdpPort 等启动参数。"
+dependencies:
+ tools:
+ - browser_use
+---
+
+# 浏览器启动模式
+
+本技能控制 `browser_use` 的启动方式,核心参数:
+
+| 参数 | 类型 | 说明 |
+|------|------|------|
+| `headed` | Boolean | `true` = 显示窗口;`false`(默认)= 无头模式 |
+| `cdpPort` | Integer | 指定 CDP 调试端口(不填则自动选择) |
+
+## 常见用法
+
+### 无头模式(默认,后台自动化)
+```
+browser_use(action="start")
+```
+
+### 可见窗口(演示 / 需要人工操作)
+```
+browser_use(action="start", headed=true)
+```
+
+### 指定 CDP 端口(与外部工具共享)
+```
+browser_use(action="start", headed=true, cdpPort=9222)
+```
+
+## 标准操作序列
+
+```
+# 1. 启动
+browser_use(action="start", headed=true)
+
+# 2. 打开页面
+browser_use(action="open", url="https://example.com")
+
+# 3. 截取快照(查看页面内容)
+browser_use(action="snapshot")
+
+# 4. 点击元素
+browser_use(action="click", selector="#submit-btn")
+
+# 5. 输入文本
+browser_use(action="type", selector="input[name=email]", text="user@example.com")
+
+# 6. 完成后关闭
+browser_use(action="stop")
+```
+
+## 何时用可见模式
+
+| 场景 | 推荐参数 |
+|------|---------|
+| 向用户演示操作过程 | `headed=true` |
+| 需要用户手动登录(扫码、验证码) | `headed=true`,遇到登录页暂停并提示用户 |
+| 调试自动化脚本 | `headed=true` |
+| 纯自动化、不需要展示 | `headed=false`(默认) |
+
+## 跨平台可执行文件路径(需自定义浏览器时)
+
+MateClaw 的 `browser_use` 会自动检测系统浏览器,无需手动指定路径。如需调试特定 Chrome 安装位置:
+
+**Windows 默认路径:**
+```
+C:\Program Files\Google\Chrome\Application\chrome.exe
+C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
+```
+
+**macOS 默认路径:**
+```
+/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
+```
+
+**Linux 默认路径:**
+```
+/usr/bin/google-chrome
+/usr/bin/chromium-browser
+```
+
+## 注意事项
+
+- 可见模式需要图形环境(GUI);无桌面的服务器无法使用
+- 若浏览器已在运行,须先 `stop` 再重新 `start` 才能切换 `headed` 状态
+- `cdpPort` 被占用时会报错,换端口或先停止占用该端口的进程
+- 用户手动操作可见浏览器不会刷新 Agent 的 idle 超时计时
diff --git a/mateclaw-server/src/main/resources/skills/channel_message/SKILL.md b/mateclaw-server/src/main/resources/skills/channel_message/SKILL.md
new file mode 100644
index 00000000..5ea2050e
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/channel_message/SKILL.md
@@ -0,0 +1,116 @@
+---
+name: channel_message
+version: "1.3.0"
+description: "当需要主动向用户、会话或渠道单向推送消息时使用。适用于任务完成通知、定时提醒、异步结果回推等场景。"
+dependencies:
+ tools:
+ - execute_shell_command
+---
+
+# 渠道消息推送
+
+## 何时使用
+
+仅在以下情况使用,这是**单向推送**,不会收到回复:
+
+### 应该使用
+- 用户明确要求"向某个渠道 / 会话发送消息"
+- 异步任务完成后主动通知用户
+- 定时提醒、告警、状态更新
+- 将后台任务结果推送回指定会话
+
+### 不应使用
+- 当前对话中的正常回复(直接回复即可)
+- 需要等待用户回复的双向交互
+- 目标渠道或会话不明确时(先询问用户)
+
+## 支持渠道
+
+`console`、`dingtalk`、`feishu`、`telegram`、`discord`、`qq`、`slack`
+
+## 工作流程
+
+### 第一步:查询目标会话
+
+**macOS / Linux:**
+```
+execute_shell_command(
+ command="mateclaw chats list --agent-id --channel "
+)
+```
+
+**Windows:**
+```
+execute_shell_command(
+ command="mateclaw.exe chats list --agent-id --channel "
+)
+```
+
+从返回结果中获取 `user_id` 和 `session_id`。有多个会话时,优先选 `updated_at` 最近的。
+
+### 第二步:发送消息
+
+**macOS / Linux:**
+```
+execute_shell_command(
+ command="mateclaw channels send --agent-id --channel --target-user --target-session --text \"消息内容\""
+)
+```
+
+**Windows(PowerShell):**
+```
+execute_shell_command(
+ command="mateclaw.exe channels send --agent-id --channel --target-user --target-session --text '消息内容'"
+)
+```
+
+### 必填参数一览
+
+| 参数 | 说明 |
+|------|------|
+| `--agent-id` | 当前 Agent 的 ID |
+| `--channel` | 目标渠道名称(见支持渠道列表) |
+| `--target-user` | 目标用户 ID(从 `chats list` 获取) |
+| `--target-session` | 目标会话 ID(从 `chats list` 获取) |
+| `--text` | 消息内容 |
+
+## 常见场景示例
+
+### 任务完成通知
+
+```
+execute_shell_command(
+ command="mateclaw chats list --agent-id task-bot --channel dingtalk"
+)
+# 从结果中取 user_id / session_id,然后:
+execute_shell_command(
+ command="mateclaw channels send --agent-id task-bot --channel dingtalk --target-user alice --target-session alice_dt_001 --text \"✅ 数据分析已完成,结果已保存到 report.xlsx\""
+)
+```
+
+### 按用户筛选会话
+
+```
+execute_shell_command(
+ command="mateclaw chats list --agent-id notify-bot --user-id alice"
+)
+```
+
+## mateclaw CLI 未安装时的降级处理
+
+若 `mateclaw` 命令不可用:
+
+1. 检测:
+```
+execute_shell_command(command="which mateclaw || where mateclaw")
+```
+
+2. 如果未安装,告知用户:
+> mateclaw CLI 未找到,无法主动推送消息。请确认 MateClaw 已正确安装并将 CLI 加入 PATH。安装后重试。
+
+## 常见错误
+
+- **缺少必填参数**:5 个参数(agent-id、channel、target-user、target-session、text)缺一不可
+- **没有先查 session 就发送**:不要猜 target-user 和 target-session,必须先查
+- **把正常对话回复当成推送**:当前会话直接回复不需要用本技能
+- **期望收到回复**:`channels send` 是单向推送,不返回用户回复
diff --git a/mateclaw-server/src/main/resources/skills/chat_with_agent/SKILL.md b/mateclaw-server/src/main/resources/skills/chat_with_agent/SKILL.md
new file mode 100644
index 00000000..059c67af
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/chat_with_agent/SKILL.md
@@ -0,0 +1,87 @@
+---
+name: chat_with_agent
+version: "1.2.0"
+description: "当需要咨询其他 Agent、寻求帮助,或用户明确要求某个 Agent 参与时使用。支持单次委托和多任务并行委托。"
+dependencies:
+ tools:
+ - listAvailableAgents
+ - delegateToAgent
+ - delegateParallel
+---
+
+# 与 Agent 对话
+
+## 何时使用
+
+当你需要**向另一个 Agent 询问问题、寻求帮助、请求方案、请求复核**,或用户明确要求某个 Agent 参与时使用。
+
+### 应该使用
+- 需要另一个 Agent 的专长、判断或第二意见
+- 需要向某个 Agent 请求方案、复核或建议
+- 用户明确要求某个 Agent 参与或协助
+- 多个独立子任务需要并行分配给不同 Agent
+
+### 不应使用
+- 你自己可以直接完成,且用户没有明确要求调用其他 Agent
+- 只是普通问答,不需要专门 Agent
+- 刚收到某个 Agent 的消息,**不要立刻回调同一个 Agent**(防止死循环)
+
+## 工作流程
+
+### 第一步:查询可用 Agent
+
+```
+listAvailableAgents()
+```
+
+返回所有已启用 Agent 的名称、类型和描述,根据描述选择最合适的 Agent。
+
+### 第二步A:单次委托(串行)
+
+```
+delegateToAgent(
+ agentName="data-analyst",
+ task="[来自 Agent my-agent 的请求] 请帮我分析以下销售数据,给出环比趋势摘要:..."
+)
+```
+
+- `agentName`:目标 Agent 的名称(从 `listAvailableAgents()` 返回值中取)
+- `task`:发送给目标 Agent 的完整任务描述
+- 建议在 `task` 开头加 `[来自 Agent <自身名称> 的请求]` 便于对方识别来源
+
+### 第二步B:并行委托(多个独立任务同时进行)
+
+最多同时委托 3 个 Agent:
+
+```
+delegateParallel(
+ tasksJson="[
+ {\"agentName\": \"research-agent\", \"task\": \"[来自 Agent coordinator 的请求] 搜索 AI 行业最新融资动态\"},
+ {\"agentName\": \"data-analyst\", \"task\": \"[来自 Agent coordinator 的请求] 分析上季度销售数据趋势\"}
+ ]"
+)
+```
+
+`tasksJson` 是 JSON 数组字符串,每个元素包含 `agentName` 和 `task`。
+
+## 决策规则
+
+1. **用户明确要求调用某 Agent** → 先 `listAvailableAgents()` 确认名称,不要猜
+2. **能自己完成** → 不调用
+3. **多个互相独立的子任务** → 用 `delegateParallel`,不要串行逐个调用
+4. **不超过 3 个并行** → 超过时按优先级分批
+5. **收到 Agent B 回复后** → 不要立刻回调 Agent B
+
+## 与 make_plan 的区别
+
+| 技能 | 用途 |
+|------|------|
+| `chat_with_agent` | 向 Agent 咨询、委托、获取结果 |
+| `make_plan` | 专门向更强 Agent 索要执行计划(由自己执行) |
+
+## 注意事项
+
+- `delegateToAgent` 是同步阻塞调用,等待目标 Agent 完成后返回结果
+- `delegateParallel` 并发执行,等所有任务完成后一次性返回所有结果
+- MateClaw 当前不支持跨调用的会话 session 续接,每次 `delegateToAgent` 是独立对话
+- 如需上下文连贯,在 `task` 参数中附带上一次的关键结论
diff --git a/mateclaw-server/src/main/resources/skills/cron/SKILL.md b/mateclaw-server/src/main/resources/skills/cron/SKILL.md
new file mode 100644
index 00000000..e406116c
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/cron/SKILL.md
@@ -0,0 +1,106 @@
+---
+name: cron
+version: "1.4.0"
+description: "仅在需要未来定时执行或周期执行任务时使用。通过 create_cron_job / list_cron_jobs / toggle_cron_job / delete_cron_job 管理定时任务。"
+dependencies:
+ tools:
+ - create_cron_job
+ - list_cron_jobs
+ - toggle_cron_job
+ - delete_cron_job
+---
+
+# 定时任务管理
+
+## 何时使用
+
+只有在需要**未来某个时间自动执行**,或**按周期重复执行**时使用本技能。
+
+### 应该使用
+- 用户要求"每天 / 每周 / 每小时"执行某件事
+- 用户要求"明天 9 点 / 下周一 / 某个时间"自动提醒或执行
+- 需要长期周期性通知、检查、汇报
+
+### 不应使用
+- 只是**现在立即执行一次**
+- 只是当前会话中的正常回复
+- 用户没有明确执行时间或周期
+
+## 工具速查
+
+| 操作 | 工具调用 |
+|------|---------|
+| 查看所有任务 | `list_cron_jobs()` |
+| 创建任务 | `create_cron_job(name, cronExpression, triggerMessage, timezone)` |
+| 暂停任务 | `toggle_cron_job(jobId, enabled=false)` |
+| 恢复任务 | `toggle_cron_job(jobId, enabled=true)` |
+| 删除任务 | `delete_cron_job(jobId)` |
+
+## 工作流程
+
+### 第一步:确认必要信息
+
+创建前**必须**确认以下信息,缺一不可:
+
+- 任务名称(`name`)
+- 执行周期(`cronExpression`,5 段 cron 表达式)
+- 触发消息(`triggerMessage`,任务触发时 Agent 收到的提示词)
+- 时区(`timezone`,可选,默认 `Asia/Shanghai`)
+
+信息不全时先追问用户,不要用占位符创建任务。
+
+### 第二步:创建任务
+
+```
+create_cron_job(
+ name="每日早报",
+ cronExpression="0 9 * * *",
+ triggerMessage="请获取今日财经新闻并发送摘要给用户",
+ timezone="Asia/Shanghai"
+)
+```
+
+### 第三步:管理任务
+
+查看所有任务:
+```
+list_cron_jobs()
+```
+
+暂停(从返回的任务列表中获取 jobId):
+```
+toggle_cron_job(jobId=123, enabled=false)
+```
+
+恢复:
+```
+toggle_cron_job(jobId=123, enabled=true)
+```
+
+删除:
+```
+delete_cron_job(jobId=123)
+```
+
+## Cron 表达式参考(5 段格式)
+
+```
+格式:分 时 日 月 周
+0 9 * * * 每天 09:00
+0 */2 * * * 每 2 小时整点
+30 8 * * 1-5 工作日 08:30
+0 0 * * 0 每周日零点
+*/15 * * * * 每 15 分钟
+0 9,18 * * * 每天 09:00 和 18:00
+```
+
+## 常见错误
+
+- **缺少信息就创建**:必须先确认周期、名称和触发消息
+- **只是立即执行一次**:不需要创建 cron,直接执行即可
+- **时区混淆**:默认 `Asia/Shanghai`;若用户在其他时区,明确指定
+
+## 安全须知
+
+- `triggerMessage` 是 Agent 触发时收到的提示词,不是发给用户的消息
+- 避免创建执行频率极高(< 5 分钟)的任务,除非用户明确需要
diff --git a/mateclaw-server/src/main/resources/skills/dingtalk_channel_connect/SKILL.md b/mateclaw-server/src/main/resources/skills/dingtalk_channel_connect/SKILL.md
new file mode 100644
index 00000000..8a125843
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/dingtalk_channel_connect/SKILL.md
@@ -0,0 +1,139 @@
+---
+name: dingtalk_channel_connect
+version: "1.3.0"
+description: "使用可见浏览器自动完成 MateClaw 钉钉渠道接入。遇到登录页必须暂停等待用户手动登录后继续。"
+dependencies:
+ tools:
+ - browser_use
+ - execute_shell_command
+ - read_file
+---
+
+# 钉钉渠道接入(可见浏览器)
+
+通过可见浏览器自动化完成钉钉应用创建与 MateClaw 渠道绑定。
+
+## 强制规则
+
+1. **必须使用可见浏览器**:`browser_use(action="start", headed=true)`
+2. **遇到登录页必须暂停**:检测到登录界面立即停止,提示用户手动登录,收到"继续"后再执行
+3. **配置变更后必须发布**:任何机器人配置修改都要"创建新版本 + 发布",否则不生效
+
+## 执行前确认(必须先做)
+
+开始自动化前向用户确认以下可定制项(未指定则使用默认值):
+
+| 配置项 | 默认值 |
+|--------|--------|
+| 应用名称 | `MateClaw` |
+| 应用描述 | `Your personal AI assistant` |
+| 机器人图标 | `https://img.alicdn.com/imgextra/i4/O1CN01M0iyHF1FVNzM9qjC0_!!6000000000492-2-tps-254-254.png` |
+| 机器人消息预览图 | 同上 |
+
+**图片规范(务必告知用户)**:
+- 机器人图标:JPG/PNG,240×240px 以上,1:1 比例,2MB 以内
+- 消息预览图:PNG/JPEG/JPG,不超过 2MB
+
+## 图片上传策略
+
+1. 用户提供本地路径 → 直接上传
+2. 用户提供图片链接 → 先下载到本地临时文件,再上传
+
+**下载图片(跨平台):**
+
+macOS / Linux:
+```
+execute_shell_command(
+ command="curl -L -o /tmp/bot_icon.png \"<图片URL>\""
+)
+```
+
+Windows:
+```
+execute_shell_command(
+ command="powershell Invoke-WebRequest -Uri '<图片URL>' -OutFile 'C:\\Temp\\bot_icon.png'"
+)
+```
+
+**上传步骤**(必须按此顺序):
+1. 先 `browser_use(action="click", selector="<上传入口>")` 触发文件选择器
+2. 再用文件上传操作(MateClaw browser_use 支持 file input 的 `type` 操作传入路径)
+
+## 自动化流程
+
+### 步骤 1:打开钉钉开发者后台
+
+```
+browser_use(action="start", headed=true)
+browser_use(action="open", url="https://open-dev.dingtalk.com/")
+browser_use(action="snapshot")
+```
+
+若页面显示登录界面,**立即暂停**:
+> 检测到需要登录钉钉开发者后台。请在弹出的浏览器中完成登录,完成后回复"继续"。
+
+### 步骤 2:创建企业内部应用
+
+用户确认登录后:
+
+1. 导航路径:应用开发 → 企业内部应用 → 钉钉应用 → 创建应用
+2. 填写应用名称、应用描述
+3. 保存创建
+
+```
+# 每次关键操作后都要截快照确认状态
+browser_use(action="snapshot")
+```
+
+### 步骤 3:添加机器人能力
+
+1. 进入「应用能力」→「添加应用能力」→ 找到「机器人」并添加
+2. 打开机器人配置开关
+3. 填写机器人名称、简介、描述
+4. 上传机器人图标(见图片上传策略)
+5. 上传消息预览图
+6. 确认消息接收模式为 **Stream 模式**
+7. 点击发布 → 确认发布弹窗
+
+**发布是必须步骤,未发布前配置不生效。**
+
+### 步骤 4:创建版本并发布
+
+1. 进入「应用发布」→「版本管理与发布」
+2. 创建新版本,填写版本说明
+3. 应用可见范围选「全部员工」
+4. 确认发布(有二次确认弹窗,选确认)
+5. 看到「发布成功」状态才继续
+
+### 步骤 5:获取凭证并引导绑定
+
+1. 进入「基础信息」→「凭证与基础信息」
+2. 告知用户 `Client ID`(AppKey)和 `Client Secret`(AppSecret)的位置
+3. 引导用户在 MateClaw 控制台绑定:
+
+**方式 A — 控制台前端:**
+> 进入 MateClaw 管理界面 → 渠道 → 新建渠道 → 选择钉钉 → 填入 Client ID 和 Client Secret
+
+**方式 B — 配置文件:**
+```json
+"dingtalk": {
+ "enabled": true,
+ "client_id": "你的 Client ID",
+ "client_secret": "你的 Client Secret"
+}
+```
+
+**Agent 不主动修改 MateClaw 配置文件,只引导用户操作。**
+
+## 稳定性策略
+
+- 优先使用 `snapshot` 返回的 `ref` 定位元素
+- 每次关键点击 / 页面跳转后重新 `snapshot` 确认状态
+- 页面结构与预期不符时重新 `snapshot`,按可见文本重新定位
+- 租户权限、管理员审批等阻塞时,说明卡点,请用户手动完成该步骤
+
+## 完成后关闭浏览器
+
+```
+browser_use(action="stop")
+```
diff --git a/mateclaw-server/src/main/resources/skills/docx/LICENSE.txt b/mateclaw-server/src/main/resources/skills/docx/LICENSE.txt
new file mode 100644
index 00000000..c55ab422
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/docx/LICENSE.txt
@@ -0,0 +1,30 @@
+© 2025 Anthropic, PBC. All rights reserved.
+
+LICENSE: Use of these materials (including all code, prompts, assets, files,
+and other components of this Skill) is governed by your agreement with
+Anthropic regarding use of Anthropic's services. If no separate agreement
+exists, use is governed by Anthropic's Consumer Terms of Service or
+Commercial Terms of Service, as applicable:
+https://www.anthropic.com/legal/consumer-terms
+https://www.anthropic.com/legal/commercial-terms
+Your applicable agreement is referred to as the "Agreement." "Services" are
+as defined in the Agreement.
+
+ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
+contrary, users may not:
+
+- Extract these materials from the Services or retain copies of these
+ materials outside the Services
+- Reproduce or copy these materials, except for temporary copies created
+ automatically during authorized use of the Services
+- Create derivative works based on these materials
+- Distribute, sublicense, or transfer these materials to any third party
+- Make, offer to sell, sell, or import any inventions embodied in these
+ materials
+- Reverse engineer, decompile, or disassemble these materials
+
+The receipt, viewing, or possession of these materials does not convey or
+imply any license or right beyond those expressly granted above.
+
+Anthropic retains all right, title, and interest in these materials,
+including all copyrights, patents, and other intellectual property rights.
diff --git a/mateclaw-server/src/main/resources/skills/docx/SKILL.md b/mateclaw-server/src/main/resources/skills/docx/SKILL.md
index e464d55d..f4eb621f 100644
--- a/mateclaw-server/src/main/resources/skills/docx/SKILL.md
+++ b/mateclaw-server/src/main/resources/skills/docx/SKILL.md
@@ -19,8 +19,39 @@ platforms:
# DOCX creation, editing, and analysis
+## Quick Start — Pick the Right Tool
+
+| Task | Recommended Tool |
+|------|------------------|
+| **Create** a new document (report / résumé / contract / memo) | `renderDocx()` — millisecond render, no subprocess |
+| **Edit** an existing .docx (content / formatting) | unpack → edit XML → pack workflow below |
+| Add tracked changes / comments | unpack → edit XML → pack workflow below |
+| GB/T 9704 official document | `writeGongwen()` (BmacClaw only) |
+
+### Create a new document (recommended path)
+
+Call the in-process Java tool — no Node.js install, no fork, no disk round-trip:
+
+```
+renderDocx(
+ markdown="# Title\n\nBody paragraph...",
+ filename="monthly-report",
+ pageSize="A4"
+)
+```
+
+Returns a clickable link of the form
+`[monthly-report.docx](/api/v1/files/generated/)` valid for 10 minutes.
+The user clicks it to download — no follow-up Agent step needed.
+
+`renderDocx` supports headings (`#` `##` `###`), bold (`**text**`), bullet
+lists (`- item`), numbered lists (`1. item`), pipe-style tables, and plain
+paragraphs. For images, headers/footers, or precise OOXML control, fall back
+to the docx-js workflow below.
+
## Prerequisites
+- **python-docx** (`pip install python-docx`): direct structure reading and light editing (paragraphs, styles, tables)
- **docx** (`npm install -g docx`): new document creation
- **LibreOffice** (`soffice`): `.doc` -> `.docx` conversion, tracked-changes acceptance, and PDF export
- **pandoc**: text extraction
@@ -50,6 +81,34 @@ python scripts/office/soffice.py --headless --convert-to docx document.doc
### Reading Content
+**Option A: python-docx (recommended for structured access)**
+
+Install: `pip install python-docx`. Gives direct access to paragraphs, styles, tables, and metadata without unpacking ZIP.
+
+```python
+from docx import Document
+
+doc = Document("document.docx")
+
+# Paragraphs with styles
+for para in doc.paragraphs:
+ print(f"[{para.style.name}] {para.text}")
+
+# Tables
+for i, table in enumerate(doc.tables):
+ print(f"Table {i+1}:")
+ for row in table.rows:
+ print([cell.text for cell in row.cells])
+
+# Inline styles within a paragraph
+for para in doc.paragraphs:
+ for run in para.runs:
+ print(f" run: bold={run.bold} italic={run.italic} text={run.text!r}")
+```
+
+Use python-docx when you need to read or lightly modify content. Fall back to the unpack/XML workflow for complex structural changes.
+
+**Option B: pandoc (plain text extraction)**
```bash
# Text extraction with tracked changes
pandoc --track-changes=all document.docx -o output.md
diff --git a/mateclaw-server/src/main/resources/skills/file_reader/SKILL.md b/mateclaw-server/src/main/resources/skills/file_reader/SKILL.md
new file mode 100644
index 00000000..93f8d15d
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/file_reader/SKILL.md
@@ -0,0 +1,90 @@
+---
+name: file_reader
+version: "1.2.0"
+description: "读取与摘要文本类文件(txt、md、json、yaml、csv、log、代码文件等)。PDF 与 Office 文件由专用技能处理。"
+dependencies:
+ tools:
+ - read_file
+ - execute_shell_command
+---
+
+# 文件读取
+
+当用户要求读取或摘要本地文本文件时使用本技能。
+
+**不在范围内**:PDF、Word (.docx)、Excel (.xlsx)、PPT (.pptx)、图片、音视频 — 这些由专用技能处理。
+
+## 工作流程
+
+### 第一步:类型探测(可选)
+
+不确定文件类型时,先探测:
+
+**macOS / Linux:**
+```
+execute_shell_command(command="file -b --mime-type \"/path/to/file\"")
+```
+
+**Windows:**
+```
+execute_shell_command(command="cmd /c \"echo %~x1\" & exit", timeoutSeconds=10)
+```
+或直接根据扩展名判断。
+
+### 第二步:读取文件
+
+```
+read_file(filePath="/absolute/or/relative/path/to/file.txt")
+```
+
+读取特定行范围(大文件时):
+```
+read_file(filePath="/path/to/large.log", startLine=1, endLine=200)
+```
+
+### 第三步:处理内容
+
+根据文件类型采用对应策略:
+
+| 类型 | 处理方式 |
+|------|---------|
+| `.txt` / `.md` | 直接摘要或按用户需求处理 |
+| `.json` / `.yaml` | 先列出顶层键,再展开用户关注的字段 |
+| `.csv` / `.tsv` | 展示表头 + 前 5 行,再描述各列含义和数据规模 |
+| `.log` | 读取最后 200 行,聚焦错误/警告模式 |
+| 源代码 | 说明文件作用,摘要核心逻辑,不逐行复述 |
+
+## 大文件策略
+
+文件超过 500 行时分段读取:
+
+```
+# 先读前 100 行了解结构
+read_file(filePath="/path/to/big.log", startLine=1, endLine=100)
+
+# 再读末尾 200 行看最新内容
+read_file(filePath="/path/to/big.log", startLine=-200)
+```
+
+日志文件也可用系统命令读取末尾:
+
+**macOS / Linux:**
+```
+execute_shell_command(command="tail -n 200 \"/path/to/file.log\"")
+```
+
+**Windows:**
+```
+execute_shell_command(command="powershell Get-Content -Tail 200 \"/path/to/file.log\"")
+```
+
+## 跨平台路径注意事项
+
+- **Windows**:路径使用反斜杠 `\` 或正斜杠 `/` 均可,但含空格时须加引号
+- **macOS / Linux**:使用正斜杠 `/`,含空格时须加引号或转义
+
+## 安全规范
+
+- 只读取文件,不执行其内容
+- 优先读取所需的最小部分,避免加载超大文件到上下文
+- 如文件包含敏感信息(密码、密钥),提醒用户注意但不拒绝读取
diff --git a/mateclaw-server/src/main/resources/skills/guidance/SKILL.md b/mateclaw-server/src/main/resources/skills/guidance/SKILL.md
new file mode 100644
index 00000000..adfb6c3b
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/guidance/SKILL.md
@@ -0,0 +1,66 @@
+---
+name: guidance
+version: "1.2.0"
+description: "回答用户关于 MateClaw 安装与配置的问题。优先定位并阅读本地文档,再提炼答案;文档不足时访问官网。"
+dependencies:
+ tools:
+ - readMateClawDoc
+ - read_file
+ - search
+---
+
+# MateClaw 安装与配置问答
+
+当用户询问 MateClaw 的安装、初始化、环境配置、依赖要求、常见配置项时使用本技能。
+
+**核心原则**:先查文档,再回答;不臆测;回答语言与提问语言一致。
+
+## 工作流程
+
+### 第一步:查文档目录
+
+```
+readMateClawDoc(action="list")
+```
+
+浏览返回的文件列表,找到与用户问题最相关的文档(如 `zh/quickstart.md`、`en/config.md`)。
+
+### 第二步:读取相关文档
+
+```
+readMateClawDoc(action="read", path="zh/quickstart.md")
+```
+
+文档较长时只读相关章节;如多个文档都相关,按优先级依次读取。
+
+### 第三步:提炼答案
+
+从文档中提取关键信息,组织成可执行答案:
+1. 先给直接结论
+2. 再给步骤 / 命令 / 配置示例
+3. 补充必要前置条件和常见坑
+
+### 第四步(兜底):搜索官网
+
+如本地文档信息不足:
+```
+search(query="MateClaw 安装配置 <关键词>", language="zh-CN", count=5)
+```
+
+参考搜索结果补充回答,并注明信息来自官网搜索。
+
+## 文档路径速查
+
+| 内容 | 路径 |
+|------|------|
+| 快速开始(中文) | `zh/quickstart.md` |
+| 配置说明(中文) | `zh/config.md` |
+| Quick Start (EN) | `en/quickstart.md` |
+| Config Reference (EN) | `en/config.md` |
+
+## 输出质量要求
+
+- 不编造不存在的配置项或命令
+- 涉及版本差异时标注"请以当前版本文档为准"
+- 涉及路径、命令、配置键时给可复制的原文片段
+- 若信息仍不足,明确告知用户缺少哪类信息(操作系统、安装方式、报错日志等)
diff --git a/mateclaw-server/src/main/resources/skills/make_plan/SKILL.md b/mateclaw-server/src/main/resources/skills/make_plan/SKILL.md
new file mode 100644
index 00000000..38df78a2
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/make_plan/SKILL.md
@@ -0,0 +1,133 @@
+---
+name: make_plan
+version: "1.3.0"
+description: "当任务需要多步拆解或不确定执行路径时,向更强 Agent 请求一份分步可落地的执行计划,由当前 Agent 自己执行。"
+dependencies:
+ tools:
+ - listAvailableAgents
+ - delegateToAgent
+ - skillFileTool
+---
+
+# 制定计划
+
+本技能的目标:**向更强 Agent 要计划,自己来执行**。不是把任务外包出去,而是获得一份可落地的执行路径。
+
+## 何时使用
+
+### 应该使用
+- 任务需要多步拆解,步骤之间有依赖关系
+- 不确定执行顺序或关键检查点
+- 涉及多个模块、文件、系统或角色
+- 用户明确要求先给出计划
+- 想在动手前获得更完整、更稳妥的执行路径
+
+### 不应使用
+- 任务很简单,一步就能完成
+- 真正缺的是一个小事实,而不是计划
+- 连任务目标都没理解清楚(先理解目标,再要计划)
+- 其实是想让对方直接替你执行任务(用 `chat_with_agent`)
+
+## 工作流程
+
+### 第一步:查询可用 Agent
+
+```
+listAvailableAgents()
+```
+
+根据描述选择能力最强或最匹配任务领域的 Agent(没有合适的就用 default)。
+
+### 第二步:请求计划
+
+```
+delegateToAgent(
+ agentName="strong-agent",
+ task="[来自 Agent my-agent 的请求] 请为以下任务制定执行计划。你不需要执行任务,只需要输出计划。
+
+任务:
+<描述要做什么>
+
+目标:
+<最终想达到什么结果>
+
+约束:
+- <限制条件1>
+- <限制条件2>
+
+计划要求:
+1. 拆成明确、可执行的步骤
+2. 标明推荐顺序
+3. 指出关键依赖和检查点
+4. 包含验证方式
+
+输出格式:请输出 4-8 个编号步骤,每步具体说明。"
+)
+```
+
+**关键原则**:
+- 明确说明"只要计划,不要代执行"
+- 步骤必须具体,不接受"先分析,再实现"这类泛泛建议
+
+### 第三步:保存计划文件(推荐)
+
+将收到的计划写入工作区,方便后续追踪和恢复:
+
+```
+skillFileTool(
+ action="write",
+ path="plans/{YYYY-MM-DD}-{task-slug}.md",
+ content="# 计划:{任务标题}\n\n{计划正文}"
+)
+```
+
+文件名规范:`plans/2026-04-23-migrate-database.md`
+
+好处:
+- 执行中断后可恢复进度
+- 用户可以审阅和修改计划
+- 多步任务完成后作为执行记录
+
+### 第四步:提炼并执行
+
+收到计划后:
+1. 提炼出真正可执行的步骤
+2. 按照当前环境做必要微调
+3. **由当前 Agent 自己执行这些步骤**
+
+**收到计划不等于任务完成,计划是输入,执行才是输出。**
+
+### 第五步(如需细化):追问
+
+如果计划某步骤不够具体,再次调用:
+
+```
+delegateToAgent(
+ agentName="strong-agent",
+ task="[来自 Agent my-agent 的请求] 请基于刚才的计划,细化第 3 步。仍然只需要补充计划细节,不需要代执行。
+
+刚才的计划摘要:
+<粘贴上次回复的关键内容>
+
+需要细化的问题:
+<具体的疑问>"
+)
+```
+
+## 计划质量标准
+
+合格的计划必须满足:
+- 有明确步骤,不是泛泛建议
+- 步骤顺序清楚
+- 每步是可执行的具体动作
+- 关键依赖被点明
+- 有必要的验证点
+
+若收到的是空泛建议,继续追问细化,直到满足以上标准。
+
+## 护栏
+
+- 不要把"请帮我规划"说成"请你直接做完"
+- 不要请求对方执行代码、命令或变更
+- 拿到计划后仍要结合当前环境判断,不要照单全收
+- 目标不清楚时,先弄清楚目标,再要计划
diff --git a/mateclaw-server/src/main/resources/skills/mateclaw_source_index/SKILL.md b/mateclaw-server/src/main/resources/skills/mateclaw_source_index/SKILL.md
new file mode 100644
index 00000000..0c8ff044
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/mateclaw_source_index/SKILL.md
@@ -0,0 +1,108 @@
+---
+name: mateclaw_source_index
+version: "1.0.0"
+description: "将用户问题映射到 MateClaw 文档路径与源码入口,减少盲目搜索。回答'XX 功能在哪里实现'、'怎么修改 YY 逻辑'等源码定位问题。"
+dependencies:
+ tools:
+ - readMateClawDoc
+ - read_file
+---
+
+# MateClaw 源码导航
+
+当用户询问"XX 功能在哪里实现"、"Agent 流程入口在哪"、"如何修改 YY 逻辑"等源码定位问题时使用本技能。
+
+## 工作流程
+
+### 第一步:查文档索引
+
+```
+readMateClawDoc(action="list")
+```
+
+根据用户问题关键词在文件列表中找到最相关文档。
+
+### 第二步:读取架构文档
+
+```
+readMateClawDoc(action="read", path="zh/architecture.md")
+```
+
+或直接读 CLAUDE.md(项目根目录,包含最完整的包结构说明):
+```
+read_file(filePath="CLAUDE.md")
+```
+
+### 第三步:返回定位结果
+
+回答格式:**文件路径 : 行号范围 + 一句话说明入口作用**
+
+示例:
+> `agent/graph/StateGraphReActAgent.java` — ReAct 循环图的组装入口,连接 ReasoningNode → ActionNode → ObservationNode
+
+## 核心路径速查表
+
+### Agent 运行时
+
+| 功能 | 文件路径 |
+|------|---------|
+| ReAct 图组装 | `agent/graph/StateGraphReActAgent.java` |
+| Plan-Execute 图组装 | `agent/graph/plan/StateGraphPlanExecuteAgent.java` |
+| 推理节点 | `agent/graph/node/ReasoningNode.java` |
+| 动作节点 | `agent/graph/node/ActionNode.java` |
+| 观察节点 | `agent/graph/node/ObservationNode.java` |
+| 最终答案节点 | `agent/graph/node/FinalAnswerNode.java` |
+| Agent 图构建器 | `agent/AgentGraphBuilder.java` |
+| 上下文注入 | `agent/context/RuntimeContextInjector.java` |
+| Token 估算 / 裁剪 | `agent/context/TokenEstimator.java` |
+
+### 工具 & 审批
+
+| 功能 | 文件路径 |
+|------|---------|
+| 工具注册中心 | `tool/ToolRegistry.java` |
+| MCP 适配器 | `tool/mcp/` |
+| 工具守卫规则 | `tool/guard/` |
+| 人工审批流程 | `approval/ApprovalWorkflowService.java` |
+| 审批 Controller | `approval/ApprovalController.java` |
+
+### 渠道 & 对话
+
+| 功能 | 文件路径 |
+|------|---------|
+| 渠道适配器接口 | `channel/ChannelAdapter.java` |
+| Web SSE 聊天 | `channel/web/ChatController.java` |
+| 渠道 Webhook | `channel/ChannelWebhookController.java` |
+| 流追踪器 | `channel/web/ChatStreamTracker.java` |
+
+### 记忆 & Wiki
+
+| 功能 | 文件路径 |
+|------|---------|
+| 记忆生命周期协调者 | `memory/MemoryLifecycleMediator.java` |
+| Dream 引擎 | `memory/dream/DreamService.java` |
+| Dream 报告 Controller | `memory/controller/DreamController.java` |
+| Wiki 处理流水线 | `wiki/service/WikiProcessingService.java` |
+| Wiki Controller | `wiki/controller/WikiController.java` |
+
+### 技能运行时
+
+| 功能 | 文件路径 |
+|------|---------|
+| 技能运行时服务 | `skill/runtime/SkillRuntimeService.java` |
+| 技能安全扫描 | `skill/security/SkillSecurityService.java` |
+| 技能 Controller | `skill/controller/SkillController.java` |
+
+### 数据库 & 配置
+
+| 功能 | 文件路径 |
+|------|---------|
+| Flyway 迁移(H2) | `resources/db/migration/h2/` |
+| Flyway 迁移(MySQL) | `resources/db/migration/mysql/` |
+| 种子数据(中文) | `resources/db/data-mysql-zh.sql` |
+| 系统设置 | `system/service/SystemSettingService.java` |
+
+## 注意
+
+- 路径均相对于 `mateclaw-server/src/main/java/vip/mate/`(Java 文件)或 `mateclaw-server/src/main/resources/`(资源文件)
+- 如找不到精确文件,先用 `readMateClawDoc` 搜索,再用 `read_file` 读取 CLAUDE.md 获取最新架构描述
diff --git a/mateclaw-server/src/main/resources/skills/multi_agent_collaboration/SKILL.md b/mateclaw-server/src/main/resources/skills/multi_agent_collaboration/SKILL.md
new file mode 100644
index 00000000..22b24821
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/multi_agent_collaboration/SKILL.md
@@ -0,0 +1,104 @@
+---
+name: multi_agent_collaboration
+version: "1.4.0"
+description: "当任务需要多个 Agent 的专业能力协同完成时,编排多 Agent 并行或串行协作,整合各方结果。"
+dependencies:
+ tools:
+ - listAvailableAgents
+ - delegateToAgent
+ - delegateParallel
+---
+
+# 多 Agent 协作
+
+## 何时使用
+
+当任务明显需要多个专业 Agent 共同完成,或用户明确要求多 Agent 协作时使用。
+
+### 应该使用
+- 任务可拆分为多个专业子域,每个子域有对应 Agent
+- 多个独立子任务可以并行执行(节省时间)
+- 需要来自不同 Agent 的结果进行综合分析
+- 用户明确要求"让 A 和 B 一起做"
+
+### 不应使用
+- 一个 Agent 可以完成,无需分工
+- 只是简单咨询,用 `chat_with_agent` 即可
+- 刚收到某 Agent 的消息,不要立刻回调它(防死循环)
+
+## 两种协作模式
+
+### 模式一:串行(有依赖关系)
+
+B 的任务需要 A 的结果时使用:
+
+```
+# 第一阶段:A 完成
+result_a = delegateToAgent(
+ agentName="research-agent",
+ task="[来自 Agent coordinator 的请求] 收集最新 AI 大模型基准测试数据,返回原始数据表格。"
+)
+
+# 第二阶段:B 基于 A 的结果处理
+result_b = delegateToAgent(
+ agentName="data-analyst",
+ task="[来自 Agent coordinator 的请求] 基于以下数据生成分析报告和可视化建议:\n\n" + result_a
+)
+```
+
+### 模式二:并行(互相独立)
+
+多个子任务之间没有依赖时使用,最多同时 3 个:
+
+```
+results = delegateParallel(
+ tasksJson="[
+ {\"agentName\": \"research-agent\", \"task\": \"[来自 Agent coordinator 的请求] 搜索竞品 A 的最新功能更新\"},
+ {\"agentName\": \"data-analyst\", \"task\": \"[来自 Agent coordinator 的请求] 分析我们产品上月用户留存数据\"},
+ {\"agentName\": \"writer-agent\", \"task\": \"[来自 Agent coordinator 的请求] 起草本次竞品分析报告的大纲\"}
+ ]"
+)
+```
+
+所有任务完成后一次性返回全部结果,再由当前 Agent 整合。
+
+## 完整工作流程
+
+### 第一步:查询可用 Agent
+
+```
+listAvailableAgents()
+```
+
+根据各 Agent 的描述分配任务。
+
+### 第二步:判断串行 or 并行
+
+| 判断条件 | 模式 |
+|---------|------|
+| 子任务 B 依赖子任务 A 的结果 | 串行 |
+| 子任务互相独立,可同时进行 | 并行 |
+| 混合(部分有依赖) | 先并行无依赖任务,再串行有依赖任务 |
+
+### 第三步:分配并执行
+
+使用对应模式(见上)。
+
+### 第四步:整合结果
+
+由当前 Agent(编排者)负责整合所有 Agent 的返回结果,形成最终回复。**不要把整合工作再委托给某个子 Agent。**
+
+## 关键规则
+
+- 任务说明中加 `[来自 Agent <名称> 的请求]` 帮助目标 Agent 识别来源
+- 并行任务数量不超过 3 个;超过时按优先级分批
+- 不让两个 Agent 互相调用对方(会形成死循环)
+- 整合由编排者负责,不再向下委托
+- 如需上下文连贯,在 `task` 中附带前一阶段的关键结论
+
+## 与 chat_with_agent 的区别
+
+| 技能 | 场景 |
+|------|------|
+| `chat_with_agent` | 一对一,咨询或单任务委托 |
+| `multi_agent_collaboration` | 一对多,多 Agent 分工、并行或串行编排 |
diff --git a/mateclaw-server/src/main/resources/skills/news/SKILL.md b/mateclaw-server/src/main/resources/skills/news/SKILL.md
new file mode 100644
index 00000000..e3f56966
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/news/SKILL.md
@@ -0,0 +1,116 @@
+---
+name: news
+version: "2.0.0"
+description: "从互联网查询最新新闻。支持政治、财经、社会、国际、科技、体育、娱乐等分类,自动适配搜索工具与浏览器工具。"
+dependencies:
+ tools:
+ - search
+ - browser_use
+---
+
+# 新闻查询
+
+当用户询问"最新新闻"、"今天发生了什么"或某类别新闻时使用本技能。
+
+## 工作流程
+
+### 方式一:搜索工具(推荐,速度快)
+
+```
+search(
+ query="今日财经新闻",
+ freshness="day",
+ language="zh-CN",
+ count=8
+)
+```
+
+参数说明:
+- `freshness`:`day`(24h)/ `week` / `month` / `year`
+- `language`:`zh-CN`(中文)/ `en`(英文)
+- `count`:返回结果数,1-10,默认 5
+
+### 方式二:浏览器直接访问权威来源
+
+当搜索结果质量不佳或用户需要更权威来源时:
+
+```
+browser_use(action="start")
+browser_use(action="open", url="https://www.chinanews.com/society/")
+browser_use(action="snapshot")
+```
+
+| 类别 | 来源 | URL |
+|------|------|-----|
+| 政治 | 人民网 · 党报 | https://cpc.people.com.cn/ |
+| 财经 | 中国经济网 | http://www.ce.cn/ |
+| 社会 | 中新网 · 社会 | https://www.chinanews.com/society/ |
+| 国际 | CGTN | https://www.cgtn.com/ |
+| 科技 | 科技日报 | https://www.stdaily.com/ |
+| 体育 | 央视体育 | https://sports.cctv.com/ |
+| 娱乐 | 新浪娱乐 | https://ent.sina.com.cn/ |
+
+浏览器用完后关闭:
+```
+browser_use(action="stop")
+```
+
+## 输出格式
+
+以要点列表呈现,每条包含:
+- 标题(加粗)
+- 一两句摘要
+- 来源 + 发布时间
+
+示例:
+```
+**经济数据:3 月 CPI 同比上涨 0.1%**
+国家统计局今日发布数据,环比下降 0.4%,低于市场预期。
+来源:中国经济网 · 2026-04-23
+```
+
+## 方式三:持续监控 RSS/Atom(blogwatcher)
+
+当用户需要**定期追踪**某个来源而非单次查询时,使用 `blogwatcher-cli`。
+
+安装:`pip install blogwatcher-cli`(SQLite 后端,无需服务器)
+
+```bash
+# 添加 RSS 源
+blogwatcher add --name "科技日报" --url https://www.stdaily.com/rss.xml
+
+# 添加 Atom 源(也支持)
+blogwatcher add --name "MIT Tech Review" --url https://www.technologyreview.com/feed/
+
+# 列出已订阅来源
+blogwatcher list
+
+# 拉取所有来源的新文章(增量,只返回未见过的)
+blogwatcher fetch --all
+
+# 拉取指定来源
+blogwatcher fetch --name "科技日报"
+
+# 查看最近 N 条条目
+blogwatcher entries --limit 20
+
+# 搜索历史条目
+blogwatcher search "人工智能 大模型"
+
+# 删除来源
+blogwatcher remove --name "科技日报"
+```
+
+`blogwatcher fetch` 是**增量**的:只返回自上次 fetch 以来的新条目,不重复推送旧内容。
+
+**适用场景**:
+- 用户说"每天帮我看看某网站有什么新文章"
+- 需要追踪多个来源、避免重复浏览
+- 配合 `cron` 技能设置定时抓取任务
+
+## 注意事项
+
+- 搜索结果以新鲜度为优先,优先选 `freshness="day"`
+- 多个类别时分别搜索,避免混淆
+- 网站无法访问时说明原因并提供备用来源链接
+- 不编造新闻,所有内容来自实际搜索/浏览结果
diff --git a/mateclaw-server/src/main/resources/skills/pdf/LICENSE.txt b/mateclaw-server/src/main/resources/skills/pdf/LICENSE.txt
new file mode 100644
index 00000000..c55ab422
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/pdf/LICENSE.txt
@@ -0,0 +1,30 @@
+© 2025 Anthropic, PBC. All rights reserved.
+
+LICENSE: Use of these materials (including all code, prompts, assets, files,
+and other components of this Skill) is governed by your agreement with
+Anthropic regarding use of Anthropic's services. If no separate agreement
+exists, use is governed by Anthropic's Consumer Terms of Service or
+Commercial Terms of Service, as applicable:
+https://www.anthropic.com/legal/consumer-terms
+https://www.anthropic.com/legal/commercial-terms
+Your applicable agreement is referred to as the "Agreement." "Services" are
+as defined in the Agreement.
+
+ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
+contrary, users may not:
+
+- Extract these materials from the Services or retain copies of these
+ materials outside the Services
+- Reproduce or copy these materials, except for temporary copies created
+ automatically during authorized use of the Services
+- Create derivative works based on these materials
+- Distribute, sublicense, or transfer these materials to any third party
+- Make, offer to sell, sell, or import any inventions embodied in these
+ materials
+- Reverse engineer, decompile, or disassemble these materials
+
+The receipt, viewing, or possession of these materials does not convey or
+imply any license or right beyond those expressly granted above.
+
+Anthropic retains all right, title, and interest in these materials,
+including all copyrights, patents, and other intellectual property rights.
diff --git a/mateclaw-server/src/main/resources/skills/pdf/SKILL.md b/mateclaw-server/src/main/resources/skills/pdf/SKILL.md
index a7b38d1f..8679081a 100644
--- a/mateclaw-server/src/main/resources/skills/pdf/SKILL.md
+++ b/mateclaw-server/src/main/resources/skills/pdf/SKILL.md
@@ -27,25 +27,108 @@ platforms:
- **pdftoppm** (poppler-utils): PDF-to-image conversion
- **qpdf**: PDF manipulation (merge, split, rotate, decrypt)
+## Tool Selection Decision Table
+
+Choose the right approach before starting:
+
+| Input | Condition | Recommended Tool |
+|-------|-----------|-----------------|
+| URL | PDF accessible via URL | `web_extract(url)` — fastest, no download needed |
+| Local file | Text-native PDF (generated by software) | `pymupdf` — ~25 MB install, instant extraction |
+| Local file | Scanned/image-only PDF (no selectable text) | `marker-pdf` — OCR with layout preservation (~5 GB, needs GPU or CPU) |
+| Local file | Form filling or page manipulation | `pypdf` / `pdfplumber` + form scripts |
+| Local file | NLP editing or semantic search | `nano-pdf` — sentence-level operations |
+
+**URL-first rule**: If the user provides a URL, always try URL extraction first before downloading.
+
## Overview
This guide covers essential PDF processing operations using Python libraries and command-line tools.
-## Quick Start
+## URL-First Extraction
+
+If the user provides a URL pointing to a PDF, extract it without downloading:
+
+```
+web_extract(url="https://example.com/report.pdf")
+```
+
+Fall back to download + local processing only if `web_extract` returns empty or errors.
+
+---
+
+## Fast Extraction: pymupdf (fitz)
+
+**Best for**: Text-native PDFs (digital, not scanned). Install: `pip install pymupdf` (~25 MB).
```python
-from pypdf import PdfReader, PdfWriter
+import fitz # pymupdf
-# Read a PDF
-reader = PdfReader("document.pdf")
-print(f"Pages: {len(reader.pages)}")
+doc = fitz.open("document.pdf")
+print(f"Pages: {doc.page_count}")
-# Extract text
-text = ""
-for page in reader.pages:
- text += page.extract_text()
+# Extract all text (fast)
+full_text = "\n".join(page.get_text() for page in doc)
+
+# Extract with layout blocks (tables, columns)
+for page in doc:
+ blocks = page.get_text("blocks") # (x0,y0,x1,y1,text,block_no,block_type)
+ for block in blocks:
+ print(block[4]) # text content
+
+# Extract images
+for page in doc:
+ for img in page.get_images():
+ xref = img[0]
+ base = doc.extract_image(xref)
+ with open(f"img_{xref}.{base['ext']}", "wb") as f:
+ f.write(base["image"])
```
+pymupdf is 5-10× faster than pypdf for text extraction and preserves layout better.
+
+---
+
+## OCR Extraction: marker-pdf
+
+**Best for**: Scanned PDFs, image-only PDFs, or documents where `pymupdf` returns garbled text.
+Install: `pip install marker-pdf` (~5 GB with models).
+
+```bash
+# Single file
+marker_single document.pdf output_dir/ --batch_multiplier 2
+
+# Batch
+marker input_dir/ output_dir/ --workers 4
+```
+
+Outputs Markdown with preserved headings, tables, and code blocks.
+
+**Decision signal**: Run `pymupdf` first. If extracted text has <50% printable characters or looks like garbage, switch to `marker-pdf`.
+
+---
+
+## NLP Editing: nano-pdf
+
+**Best for**: Semantic search, sentence-level edits, keyword replacement in text-native PDFs.
+Install: `pip install nano-pdf`.
+
+```python
+from nano_pdf import NanoPDF
+
+doc = NanoPDF("document.pdf")
+
+# Search sentences
+results = doc.search("termination clause", top_k=5)
+for r in results:
+ print(r.page, r.text, r.score)
+
+# Replace text (produces new PDF)
+doc.replace("old phrase", "new phrase", output="modified.pdf")
+```
+
+---
+
## Python Libraries
### pypdf - Basic Operations
@@ -268,10 +351,13 @@ with open("encrypted.pdf", "wb") as output:
| Task | Best Tool | Command/Code |
|------|-----------|--------------|
+| URL → text | web_extract | `web_extract(url=...)` |
+| Fast text extraction | pymupdf | `fitz.open(...).get_text()` |
+| Scanned / OCR | marker-pdf | `marker_single doc.pdf out/` |
+| Semantic search/edit | nano-pdf | `NanoPDF(...).search(...)` |
| Merge PDFs | pypdf | `writer.add_page(page)` |
| Split PDFs | pypdf | One page per file |
-| Extract text | pdfplumber | `page.extract_text()` |
+| Extract text (layout) | pdfplumber | `page.extract_text()` |
| Extract tables | pdfplumber | `page.extract_tables()` |
| Create PDFs | reportlab | Canvas or Platypus |
| Fill forms | scripts | `fill_fillable_fields.py` |
-| OCR scanned PDFs | pytesseract | Convert to image first |
diff --git a/mateclaw-server/src/main/resources/skills/pdf/forms.md b/mateclaw-server/src/main/resources/skills/pdf/forms.md
new file mode 100644
index 00000000..f8c1dec1
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/pdf/forms.md
@@ -0,0 +1,298 @@
+> **Important:** All `scripts/` paths are relative to the skill directory (where SKILL.md is).
+> Run with: `cd {this_skill_dir} && python scripts/...`
+> Or use the `cwd` parameter of `execute_shell_command`.
+
+**CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.**
+
+If you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file's directory:
+ `python scripts/check_fillable_fields `, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions.
+
+# Fillable fields
+If the PDF has fillable form fields:
+- Run this script from this file's directory: `python scripts/extract_form_field_info.py `. It will create a JSON file with a list of fields in this format:
+```
+[
+ {
+ "field_id": (unique ID for the field),
+ "page": (page number, 1-based),
+ "rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page),
+ "type": ("text", "checkbox", "radio_group", or "choice"),
+ },
+ // Checkboxes have "checked_value" and "unchecked_value" properties:
+ {
+ "field_id": (unique ID for the field),
+ "page": (page number, 1-based),
+ "type": "checkbox",
+ "checked_value": (Set the field to this value to check the checkbox),
+ "unchecked_value": (Set the field to this value to uncheck the checkbox),
+ },
+ // Radio groups have a "radio_options" list with the possible choices.
+ {
+ "field_id": (unique ID for the field),
+ "page": (page number, 1-based),
+ "type": "radio_group",
+ "radio_options": [
+ {
+ "value": (set the field to this value to select this radio option),
+ "rect": (bounding box for the radio button for this option)
+ },
+ // Other radio options
+ ]
+ },
+ // Multiple choice fields have a "choice_options" list with the possible choices:
+ {
+ "field_id": (unique ID for the field),
+ "page": (page number, 1-based),
+ "type": "choice",
+ "choice_options": [
+ {
+ "value": (set the field to this value to select this option),
+ "text": (display text of the option)
+ },
+ // Other choice options
+ ],
+ }
+]
+```
+- Convert the PDF to PNGs (one image for each page) with this script (run from this file's directory):
+`python scripts/convert_pdf_to_images.py `
+Then analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates).
+- Create a `field_values.json` file in this format with the values to be entered for each field:
+```
+[
+ {
+ "field_id": "last_name", // Must match the field_id from `extract_form_field_info.py`
+ "description": "The user's last name",
+ "page": 1, // Must match the "page" value in field_info.json
+ "value": "Simpson"
+ },
+ {
+ "field_id": "Checkbox12",
+ "description": "Checkbox to be checked if the user is 18 or over",
+ "page": 1,
+ "value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it's a radio button group, use one of the "value" values in "radio_options".
+ },
+ // more fields
+]
+```
+- Run the `fill_fillable_fields.py` script from this file's directory to create a filled-in PDF:
+`python scripts/fill_fillable_fields.py `
+This script will verify that the field IDs and values you provide are valid; if it prints error messages, correct the appropriate fields and try again.
+
+# Non-fillable fields
+If the PDF doesn't have fillable form fields, you'll add text annotations. First try to extract coordinates from the PDF structure (more accurate), then fall back to visual estimation if needed.
+
+## Step 1: Try Structure Extraction First
+
+Run this script to extract text labels, lines, and checkboxes with their exact PDF coordinates:
+`python scripts/extract_form_structure.py form_structure.json`
+
+This creates a JSON file containing:
+- **labels**: Every text element with exact coordinates (x0, top, x1, bottom in PDF points)
+- **lines**: Horizontal lines that define row boundaries
+- **checkboxes**: Small square rectangles that are checkboxes (with center coordinates)
+- **row_boundaries**: Row top/bottom positions calculated from horizontal lines
+
+**Check the results**: If `form_structure.json` has meaningful labels (text elements that correspond to form fields), use **Approach A: Structure-Based Coordinates**. If the PDF is scanned/image-based and has few or no labels, use **Approach B: Visual Estimation**.
+
+---
+
+## Approach A: Structure-Based Coordinates (Preferred)
+
+Use this when `extract_form_structure.py` found text labels in the PDF.
+
+### A.1: Analyze the Structure
+
+Read form_structure.json and identify:
+
+1. **Label groups**: Adjacent text elements that form a single label (e.g., "Last" + "Name")
+2. **Row structure**: Labels with similar `top` values are in the same row
+3. **Field columns**: Entry areas start after label ends (x0 = label.x1 + gap)
+4. **Checkboxes**: Use the checkbox coordinates directly from the structure
+
+**Coordinate system**: PDF coordinates where y=0 is at TOP of page, y increases downward.
+
+### A.2: Check for Missing Elements
+
+The structure extraction may not detect all form elements. Common cases:
+- **Circular checkboxes**: Only square rectangles are detected as checkboxes
+- **Complex graphics**: Decorative elements or non-standard form controls
+- **Faded or light-colored elements**: May not be extracted
+
+If you see form fields in the PDF images that aren't in form_structure.json, you'll need to use **visual analysis** for those specific fields (see "Hybrid Approach" below).
+
+### A.3: Create fields.json with PDF Coordinates
+
+For each field, calculate entry coordinates from the extracted structure:
+
+**Text fields:**
+- entry x0 = label x1 + 5 (small gap after label)
+- entry x1 = next label's x0, or row boundary
+- entry top = same as label top
+- entry bottom = row boundary line below, or label bottom + row_height
+
+**Checkboxes:**
+- Use the checkbox rectangle coordinates directly from form_structure.json
+- entry_bounding_box = [checkbox.x0, checkbox.top, checkbox.x1, checkbox.bottom]
+
+Create fields.json using `pdf_width` and `pdf_height` (signals PDF coordinates):
+```json
+{
+ "pages": [
+ {"page_number": 1, "pdf_width": 612, "pdf_height": 792}
+ ],
+ "form_fields": [
+ {
+ "page_number": 1,
+ "description": "Last name entry field",
+ "field_label": "Last Name",
+ "label_bounding_box": [43, 63, 87, 73],
+ "entry_bounding_box": [92, 63, 260, 79],
+ "entry_text": {"text": "Smith", "font_size": 10}
+ },
+ {
+ "page_number": 1,
+ "description": "US Citizen Yes checkbox",
+ "field_label": "Yes",
+ "label_bounding_box": [260, 200, 280, 210],
+ "entry_bounding_box": [285, 197, 292, 205],
+ "entry_text": {"text": "X"}
+ }
+ ]
+}
+```
+
+**Important**: Use `pdf_width`/`pdf_height` and coordinates directly from form_structure.json.
+
+### A.4: Validate Bounding Boxes
+
+Before filling, check your bounding boxes for errors:
+`python scripts/check_bounding_boxes.py fields.json`
+
+This checks for intersecting bounding boxes and entry boxes that are too small for the font size. Fix any reported errors before filling.
+
+---
+
+## Approach B: Visual Estimation (Fallback)
+
+Use this when the PDF is scanned/image-based and structure extraction found no usable text labels (e.g., all text shows as "(cid:X)" patterns).
+
+### B.1: Convert PDF to Images
+
+`python scripts/convert_pdf_to_images.py `
+
+### B.2: Initial Field Identification
+
+Examine each page image to identify form sections and get **rough estimates** of field locations:
+- Form field labels and their approximate positions
+- Entry areas (lines, boxes, or blank spaces for text input)
+- Checkboxes and their approximate locations
+
+For each field, note approximate pixel coordinates (they don't need to be precise yet).
+
+### B.3: Zoom Refinement (CRITICAL for accuracy)
+
+For each field, crop a region around the estimated position to refine coordinates precisely.
+
+**Create a zoomed crop using ImageMagick:**
+```bash
+magick -crop x++ +repage
+```
+
+Where:
+- `, ` = top-left corner of crop region (use your rough estimate minus padding)
+- `, ` = size of crop region (field area plus ~50px padding on each side)
+
+**Example:** To refine a "Name" field estimated around (100, 150):
+```bash
+magick images_dir/page_1.png -crop 300x80+50+120 +repage crops/name_field.png
+```
+
+(Note: if the `magick` command isn't available, try `convert` with the same arguments).
+
+**Examine the cropped image** to determine precise coordinates:
+1. Identify the exact pixel where the entry area begins (after the label)
+2. Identify where the entry area ends (before next field or edge)
+3. Identify the top and bottom of the entry line/box
+
+**Convert crop coordinates back to full image coordinates:**
+- full_x = crop_x + crop_offset_x
+- full_y = crop_y + crop_offset_y
+
+Example: If the crop started at (50, 120) and the entry box starts at (52, 18) within the crop:
+- entry_x0 = 52 + 50 = 102
+- entry_top = 18 + 120 = 138
+
+**Repeat for each field**, grouping nearby fields into single crops when possible.
+
+### B.4: Create fields.json with Refined Coordinates
+
+Create fields.json using `image_width` and `image_height` (signals image coordinates):
+```json
+{
+ "pages": [
+ {"page_number": 1, "image_width": 1700, "image_height": 2200}
+ ],
+ "form_fields": [
+ {
+ "page_number": 1,
+ "description": "Last name entry field",
+ "field_label": "Last Name",
+ "label_bounding_box": [120, 175, 242, 198],
+ "entry_bounding_box": [255, 175, 720, 218],
+ "entry_text": {"text": "Smith", "font_size": 10}
+ }
+ ]
+}
+```
+
+**Important**: Use `image_width`/`image_height` and the refined pixel coordinates from the zoom analysis.
+
+### B.5: Validate Bounding Boxes
+
+Before filling, check your bounding boxes for errors:
+`python scripts/check_bounding_boxes.py fields.json`
+
+This checks for intersecting bounding boxes and entry boxes that are too small for the font size. Fix any reported errors before filling.
+
+---
+
+## Hybrid Approach: Structure + Visual
+
+Use this when structure extraction works for most fields but misses some elements (e.g., circular checkboxes, unusual form controls).
+
+1. **Use Approach A** for fields that were detected in form_structure.json
+2. **Convert PDF to images** for visual analysis of missing fields
+3. **Use zoom refinement** (from Approach B) for the missing fields
+4. **Combine coordinates**: For fields from structure extraction, use `pdf_width`/`pdf_height`. For visually-estimated fields, you must convert image coordinates to PDF coordinates:
+ - pdf_x = image_x * (pdf_width / image_width)
+ - pdf_y = image_y * (pdf_height / image_height)
+5. **Use a single coordinate system** in fields.json - convert all to PDF coordinates with `pdf_width`/`pdf_height`
+
+---
+
+## Step 2: Validate Before Filling
+
+**Always validate bounding boxes before filling:**
+`python scripts/check_bounding_boxes.py fields.json`
+
+This checks for:
+- Intersecting bounding boxes (which would cause overlapping text)
+- Entry boxes that are too small for the specified font size
+
+Fix any reported errors in fields.json before proceeding.
+
+## Step 3: Fill the Form
+
+The fill script auto-detects the coordinate system and handles conversion:
+`python scripts/fill_pdf_form_with_annotations.py fields.json `
+
+## Step 4: Verify Output
+
+Convert the filled PDF to images and verify text placement:
+`python scripts/convert_pdf_to_images.py `
+
+If text is mispositioned:
+- **Approach A**: Check that you're using PDF coordinates from form_structure.json with `pdf_width`/`pdf_height`
+- **Approach B**: Check that image dimensions match and coordinates are accurate pixels
+- **Hybrid**: Ensure coordinate conversions are correct for visually-estimated fields
diff --git a/mateclaw-server/src/main/resources/skills/pdf/reference.md b/mateclaw-server/src/main/resources/skills/pdf/reference.md
new file mode 100644
index 00000000..41400bf4
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/pdf/reference.md
@@ -0,0 +1,612 @@
+# PDF Processing Advanced Reference
+
+This document contains advanced PDF processing features, detailed examples, and additional libraries not covered in the main skill instructions.
+
+## pypdfium2 Library (Apache/BSD License)
+
+### Overview
+pypdfium2 is a Python binding for PDFium (Chromium's PDF library). It's excellent for fast PDF rendering, image generation, and serves as a PyMuPDF replacement.
+
+### Render PDF to Images
+```python
+import pypdfium2 as pdfium
+from PIL import Image
+
+# Load PDF
+pdf = pdfium.PdfDocument("document.pdf")
+
+# Render page to image
+page = pdf[0] # First page
+bitmap = page.render(
+ scale=2.0, # Higher resolution
+ rotation=0 # No rotation
+)
+
+# Convert to PIL Image
+img = bitmap.to_pil()
+img.save("page_1.png", "PNG")
+
+# Process multiple pages
+for i, page in enumerate(pdf):
+ bitmap = page.render(scale=1.5)
+ img = bitmap.to_pil()
+ img.save(f"page_{i+1}.jpg", "JPEG", quality=90)
+```
+
+### Extract Text with pypdfium2
+```python
+import pypdfium2 as pdfium
+
+pdf = pdfium.PdfDocument("document.pdf")
+for i, page in enumerate(pdf):
+ text = page.get_text()
+ print(f"Page {i+1} text length: {len(text)} chars")
+```
+
+## JavaScript Libraries
+
+### pdf-lib (MIT License)
+
+pdf-lib is a powerful JavaScript library for creating and modifying PDF documents in any JavaScript environment.
+
+#### Load and Manipulate Existing PDF
+```javascript
+import { PDFDocument } from 'pdf-lib';
+import fs from 'fs';
+
+async function manipulatePDF() {
+ // Load existing PDF
+ const existingPdfBytes = fs.readFileSync('input.pdf');
+ const pdfDoc = await PDFDocument.load(existingPdfBytes);
+
+ // Get page count
+ const pageCount = pdfDoc.getPageCount();
+ console.log(`Document has ${pageCount} pages`);
+
+ // Add new page
+ const newPage = pdfDoc.addPage([600, 400]);
+ newPage.drawText('Added by pdf-lib', {
+ x: 100,
+ y: 300,
+ size: 16
+ });
+
+ // Save modified PDF
+ const pdfBytes = await pdfDoc.save();
+ fs.writeFileSync('modified.pdf', pdfBytes);
+}
+```
+
+#### Create Complex PDFs from Scratch
+```javascript
+import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
+import fs from 'fs';
+
+async function createPDF() {
+ const pdfDoc = await PDFDocument.create();
+
+ // Add fonts
+ const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
+ const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
+
+ // Add page
+ const page = pdfDoc.addPage([595, 842]); // A4 size
+ const { width, height } = page.getSize();
+
+ // Add text with styling
+ page.drawText('Invoice #12345', {
+ x: 50,
+ y: height - 50,
+ size: 18,
+ font: helveticaBold,
+ color: rgb(0.2, 0.2, 0.8)
+ });
+
+ // Add rectangle (header background)
+ page.drawRectangle({
+ x: 40,
+ y: height - 100,
+ width: width - 80,
+ height: 30,
+ color: rgb(0.9, 0.9, 0.9)
+ });
+
+ // Add table-like content
+ const items = [
+ ['Item', 'Qty', 'Price', 'Total'],
+ ['Widget', '2', '$50', '$100'],
+ ['Gadget', '1', '$75', '$75']
+ ];
+
+ let yPos = height - 150;
+ items.forEach(row => {
+ let xPos = 50;
+ row.forEach(cell => {
+ page.drawText(cell, {
+ x: xPos,
+ y: yPos,
+ size: 12,
+ font: helveticaFont
+ });
+ xPos += 120;
+ });
+ yPos -= 25;
+ });
+
+ const pdfBytes = await pdfDoc.save();
+ fs.writeFileSync('created.pdf', pdfBytes);
+}
+```
+
+#### Advanced Merge and Split Operations
+```javascript
+import { PDFDocument } from 'pdf-lib';
+import fs from 'fs';
+
+async function mergePDFs() {
+ // Create new document
+ const mergedPdf = await PDFDocument.create();
+
+ // Load source PDFs
+ const pdf1Bytes = fs.readFileSync('doc1.pdf');
+ const pdf2Bytes = fs.readFileSync('doc2.pdf');
+
+ const pdf1 = await PDFDocument.load(pdf1Bytes);
+ const pdf2 = await PDFDocument.load(pdf2Bytes);
+
+ // Copy pages from first PDF
+ const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
+ pdf1Pages.forEach(page => mergedPdf.addPage(page));
+
+ // Copy specific pages from second PDF (pages 0, 2, 4)
+ const pdf2Pages = await mergedPdf.copyPages(pdf2, [0, 2, 4]);
+ pdf2Pages.forEach(page => mergedPdf.addPage(page));
+
+ const mergedPdfBytes = await mergedPdf.save();
+ fs.writeFileSync('merged.pdf', mergedPdfBytes);
+}
+```
+
+### pdfjs-dist (Apache License)
+
+PDF.js is Mozilla's JavaScript library for rendering PDFs in the browser.
+
+#### Basic PDF Loading and Rendering
+```javascript
+import * as pdfjsLib from 'pdfjs-dist';
+
+// Configure worker (important for performance)
+pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.js';
+
+async function renderPDF() {
+ // Load PDF
+ const loadingTask = pdfjsLib.getDocument('document.pdf');
+ const pdf = await loadingTask.promise;
+
+ console.log(`Loaded PDF with ${pdf.numPages} pages`);
+
+ // Get first page
+ const page = await pdf.getPage(1);
+ const viewport = page.getViewport({ scale: 1.5 });
+
+ // Render to canvas
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ canvas.height = viewport.height;
+ canvas.width = viewport.width;
+
+ const renderContext = {
+ canvasContext: context,
+ viewport: viewport
+ };
+
+ await page.render(renderContext).promise;
+ document.body.appendChild(canvas);
+}
+```
+
+#### Extract Text with Coordinates
+```javascript
+import * as pdfjsLib from 'pdfjs-dist';
+
+async function extractText() {
+ const loadingTask = pdfjsLib.getDocument('document.pdf');
+ const pdf = await loadingTask.promise;
+
+ let fullText = '';
+
+ // Extract text from all pages
+ for (let i = 1; i <= pdf.numPages; i++) {
+ const page = await pdf.getPage(i);
+ const textContent = await page.getTextContent();
+
+ const pageText = textContent.items
+ .map(item => item.str)
+ .join(' ');
+
+ fullText += `\n--- Page ${i} ---\n${pageText}`;
+
+ // Get text with coordinates for advanced processing
+ const textWithCoords = textContent.items.map(item => ({
+ text: item.str,
+ x: item.transform[4],
+ y: item.transform[5],
+ width: item.width,
+ height: item.height
+ }));
+ }
+
+ console.log(fullText);
+ return fullText;
+}
+```
+
+#### Extract Annotations and Forms
+```javascript
+import * as pdfjsLib from 'pdfjs-dist';
+
+async function extractAnnotations() {
+ const loadingTask = pdfjsLib.getDocument('annotated.pdf');
+ const pdf = await loadingTask.promise;
+
+ for (let i = 1; i <= pdf.numPages; i++) {
+ const page = await pdf.getPage(i);
+ const annotations = await page.getAnnotations();
+
+ annotations.forEach(annotation => {
+ console.log(`Annotation type: ${annotation.subtype}`);
+ console.log(`Content: ${annotation.contents}`);
+ console.log(`Coordinates: ${JSON.stringify(annotation.rect)}`);
+ });
+ }
+}
+```
+
+## Advanced Command-Line Operations
+
+### poppler-utils Advanced Features
+
+#### Extract Text with Bounding Box Coordinates
+```bash
+# Extract text with bounding box coordinates (essential for structured data)
+pdftotext -bbox-layout document.pdf output.xml
+
+# The XML output contains precise coordinates for each text element
+```
+
+#### Advanced Image Conversion
+```bash
+# Convert to PNG images with specific resolution
+pdftoppm -png -r 300 document.pdf output_prefix
+
+# Convert specific page range with high resolution
+pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages
+
+# Convert to JPEG with quality setting
+pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_output
+```
+
+#### Extract Embedded Images
+```bash
+# Extract all embedded images with metadata
+pdfimages -j -p document.pdf page_images
+
+# List image info without extracting
+pdfimages -list document.pdf
+
+# Extract images in their original format
+pdfimages -all document.pdf images/img
+```
+
+### qpdf Advanced Features
+
+#### Complex Page Manipulation
+```bash
+# Split PDF into groups of pages
+qpdf --split-pages=3 input.pdf output_group_%02d.pdf
+
+# Extract specific pages with complex ranges
+qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf
+
+# Merge specific pages from multiple PDFs
+qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdf
+```
+
+#### PDF Optimization and Repair
+```bash
+# Optimize PDF for web (linearize for streaming)
+qpdf --linearize input.pdf optimized.pdf
+
+# Remove unused objects and compress
+qpdf --optimize-level=all input.pdf compressed.pdf
+
+# Attempt to repair corrupted PDF structure
+qpdf --check input.pdf
+qpdf --fix-qdf damaged.pdf repaired.pdf
+
+# Show detailed PDF structure for debugging
+qpdf --show-all-pages input.pdf > structure.txt
+```
+
+#### Advanced Encryption
+```bash
+# Add password protection with specific permissions
+qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf
+
+# Check encryption status
+qpdf --show-encryption encrypted.pdf
+
+# Remove password protection (requires password)
+qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf
+```
+
+## Advanced Python Techniques
+
+### pdfplumber Advanced Features
+
+#### Extract Text with Precise Coordinates
+```python
+import pdfplumber
+
+with pdfplumber.open("document.pdf") as pdf:
+ page = pdf.pages[0]
+
+ # Extract all text with coordinates
+ chars = page.chars
+ for char in chars[:10]: # First 10 characters
+ print(f"Char: '{char['text']}' at x:{char['x0']:.1f} y:{char['y0']:.1f}")
+
+ # Extract text by bounding box (left, top, right, bottom)
+ bbox_text = page.within_bbox((100, 100, 400, 200)).extract_text()
+```
+
+#### Advanced Table Extraction with Custom Settings
+```python
+import pdfplumber
+import pandas as pd
+
+with pdfplumber.open("complex_table.pdf") as pdf:
+ page = pdf.pages[0]
+
+ # Extract tables with custom settings for complex layouts
+ table_settings = {
+ "vertical_strategy": "lines",
+ "horizontal_strategy": "lines",
+ "snap_tolerance": 3,
+ "intersection_tolerance": 15
+ }
+ tables = page.extract_tables(table_settings)
+
+ # Visual debugging for table extraction
+ img = page.to_image(resolution=150)
+ img.save("debug_layout.png")
+```
+
+### reportlab Advanced Features
+
+#### Create Professional Reports with Tables
+```python
+from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib import colors
+
+# Sample data
+data = [
+ ['Product', 'Q1', 'Q2', 'Q3', 'Q4'],
+ ['Widgets', '120', '135', '142', '158'],
+ ['Gadgets', '85', '92', '98', '105']
+]
+
+# Create PDF with table
+doc = SimpleDocTemplate("report.pdf")
+elements = []
+
+# Add title
+styles = getSampleStyleSheet()
+title = Paragraph("Quarterly Sales Report", styles['Title'])
+elements.append(title)
+
+# Add table with advanced styling
+table = Table(data)
+table.setStyle(TableStyle([
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
+ ('FONTSIZE', (0, 0), (-1, 0), 14),
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
+ ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
+ ('GRID', (0, 0), (-1, -1), 1, colors.black)
+]))
+elements.append(table)
+
+doc.build(elements)
+```
+
+## Complex Workflows
+
+### Extract Figures/Images from PDF
+
+#### Method 1: Using pdfimages (fastest)
+```bash
+# Extract all images with original quality
+pdfimages -all document.pdf images/img
+```
+
+#### Method 2: Using pypdfium2 + Image Processing
+```python
+import pypdfium2 as pdfium
+from PIL import Image
+import numpy as np
+
+def extract_figures(pdf_path, output_dir):
+ pdf = pdfium.PdfDocument(pdf_path)
+
+ for page_num, page in enumerate(pdf):
+ # Render high-resolution page
+ bitmap = page.render(scale=3.0)
+ img = bitmap.to_pil()
+
+ # Convert to numpy for processing
+ img_array = np.array(img)
+
+ # Simple figure detection (non-white regions)
+ mask = np.any(img_array != [255, 255, 255], axis=2)
+
+ # Find contours and extract bounding boxes
+ # (This is simplified - real implementation would need more sophisticated detection)
+
+ # Save detected figures
+ # ... implementation depends on specific needs
+```
+
+### Batch PDF Processing with Error Handling
+```python
+import os
+import glob
+from pypdf import PdfReader, PdfWriter
+import logging
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+def batch_process_pdfs(input_dir, operation='merge'):
+ pdf_files = glob.glob(os.path.join(input_dir, "*.pdf"))
+
+ if operation == 'merge':
+ writer = PdfWriter()
+ for pdf_file in pdf_files:
+ try:
+ reader = PdfReader(pdf_file)
+ for page in reader.pages:
+ writer.add_page(page)
+ logger.info(f"Processed: {pdf_file}")
+ except Exception as e:
+ logger.error(f"Failed to process {pdf_file}: {e}")
+ continue
+
+ with open("batch_merged.pdf", "wb") as output:
+ writer.write(output)
+
+ elif operation == 'extract_text':
+ for pdf_file in pdf_files:
+ try:
+ reader = PdfReader(pdf_file)
+ text = ""
+ for page in reader.pages:
+ text += page.extract_text()
+
+ output_file = pdf_file.replace('.pdf', '.txt')
+ with open(output_file, 'w', encoding='utf-8') as f:
+ f.write(text)
+ logger.info(f"Extracted text from: {pdf_file}")
+
+ except Exception as e:
+ logger.error(f"Failed to extract text from {pdf_file}: {e}")
+ continue
+```
+
+### Advanced PDF Cropping
+```python
+from pypdf import PdfWriter, PdfReader
+
+reader = PdfReader("input.pdf")
+writer = PdfWriter()
+
+# Crop page (left, bottom, right, top in points)
+page = reader.pages[0]
+page.mediabox.left = 50
+page.mediabox.bottom = 50
+page.mediabox.right = 550
+page.mediabox.top = 750
+
+writer.add_page(page)
+with open("cropped.pdf", "wb") as output:
+ writer.write(output)
+```
+
+## Performance Optimization Tips
+
+### 1. For Large PDFs
+- Use streaming approaches instead of loading entire PDF in memory
+- Use `qpdf --split-pages` for splitting large files
+- Process pages individually with pypdfium2
+
+### 2. For Text Extraction
+- `pdftotext -bbox-layout` is fastest for plain text extraction
+- Use pdfplumber for structured data and tables
+- Avoid `pypdf.extract_text()` for very large documents
+
+### 3. For Image Extraction
+- `pdfimages` is much faster than rendering pages
+- Use low resolution for previews, high resolution for final output
+
+### 4. For Form Filling
+- pdf-lib maintains form structure better than most alternatives
+- Pre-validate form fields before processing
+
+### 5. Memory Management
+```python
+# Process PDFs in chunks
+def process_large_pdf(pdf_path, chunk_size=10):
+ reader = PdfReader(pdf_path)
+ total_pages = len(reader.pages)
+
+ for start_idx in range(0, total_pages, chunk_size):
+ end_idx = min(start_idx + chunk_size, total_pages)
+ writer = PdfWriter()
+
+ for i in range(start_idx, end_idx):
+ writer.add_page(reader.pages[i])
+
+ # Process chunk
+ with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output:
+ writer.write(output)
+```
+
+## Troubleshooting Common Issues
+
+### Encrypted PDFs
+```python
+# Handle password-protected PDFs
+from pypdf import PdfReader
+
+try:
+ reader = PdfReader("encrypted.pdf")
+ if reader.is_encrypted:
+ reader.decrypt("password")
+except Exception as e:
+ print(f"Failed to decrypt: {e}")
+```
+
+### Corrupted PDFs
+```bash
+# Use qpdf to repair
+qpdf --check corrupted.pdf
+qpdf --replace-input corrupted.pdf
+```
+
+### Text Extraction Issues
+```python
+# Fallback to OCR for scanned PDFs
+import pytesseract
+from pdf2image import convert_from_path
+
+def extract_text_with_ocr(pdf_path):
+ images = convert_from_path(pdf_path)
+ text = ""
+ for i, image in enumerate(images):
+ text += pytesseract.image_to_string(image)
+ return text
+```
+
+## License Information
+
+- **pypdf**: BSD License
+- **pdfplumber**: MIT License
+- **pypdfium2**: Apache/BSD License
+- **reportlab**: BSD License
+- **poppler-utils**: GPL-2 License
+- **qpdf**: Apache License
+- **pdf-lib**: MIT License
+- **pdfjs-dist**: Apache License
\ No newline at end of file
diff --git a/mateclaw-server/src/main/resources/skills/pptx/LICENSE.txt b/mateclaw-server/src/main/resources/skills/pptx/LICENSE.txt
new file mode 100644
index 00000000..c55ab422
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/pptx/LICENSE.txt
@@ -0,0 +1,30 @@
+© 2025 Anthropic, PBC. All rights reserved.
+
+LICENSE: Use of these materials (including all code, prompts, assets, files,
+and other components of this Skill) is governed by your agreement with
+Anthropic regarding use of Anthropic's services. If no separate agreement
+exists, use is governed by Anthropic's Consumer Terms of Service or
+Commercial Terms of Service, as applicable:
+https://www.anthropic.com/legal/consumer-terms
+https://www.anthropic.com/legal/commercial-terms
+Your applicable agreement is referred to as the "Agreement." "Services" are
+as defined in the Agreement.
+
+ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
+contrary, users may not:
+
+- Extract these materials from the Services or retain copies of these
+ materials outside the Services
+- Reproduce or copy these materials, except for temporary copies created
+ automatically during authorized use of the Services
+- Create derivative works based on these materials
+- Distribute, sublicense, or transfer these materials to any third party
+- Make, offer to sell, sell, or import any inventions embodied in these
+ materials
+- Reverse engineer, decompile, or disassemble these materials
+
+The receipt, viewing, or possession of these materials does not convey or
+imply any license or right beyond those expressly granted above.
+
+Anthropic retains all right, title, and interest in these materials,
+including all copyrights, patents, and other intellectual property rights.
diff --git a/mateclaw-server/src/main/resources/skills/pptx/SKILL.md b/mateclaw-server/src/main/resources/skills/pptx/SKILL.md
index cfe2ad05..0a8a6329 100644
--- a/mateclaw-server/src/main/resources/skills/pptx/SKILL.md
+++ b/mateclaw-server/src/main/resources/skills/pptx/SKILL.md
@@ -7,6 +7,7 @@ dependencies:
tools:
- skillScriptTool
- skillFileTool
+ - delegateToAgent
platforms:
- macos
- linux
@@ -199,6 +200,34 @@ pdftoppm -jpeg -r 150 output.pdf slide
Look for: overlapping elements, text overflow, low-contrast text, uneven gaps, insufficient margins.
+### Subagent Visual QA (Fresh Eyes)
+
+For high-stakes presentations, delegate a visual inspection to a separate agent that has NOT seen the creation process. A fresh pair of eyes catches issues the author missed.
+
+```
+delegateToAgent(
+ agentName="strong-agent",
+ task="[Visual QA Request] Inspect the attached presentation slides as a fresh reviewer.
+You have no context about how these were made — treat it as if seeing them for the first time.
+
+Slides location:
+
+Check for:
+1. Any slide where text is cut off or overflows the frame
+2. Low contrast (e.g., light text on light background)
+3. Repeated layouts — more than 2 slides with identical structure
+4. Text-only slides with no visual element
+5. Accent lines under slide titles (hallmark of AI-generated slides)
+6. Any leftover placeholder text (XXXX, lorem, [insert here])
+7. Font size below 14pt in body text
+
+For each issue, state: slide number, issue type, what you see.
+If everything looks clean, say so explicitly."
+)
+```
+
+Act on the subagent's findings before declaring the presentation complete.
+
### Verification Loop
1. Generate slides -> Convert to images -> Inspect
@@ -206,6 +235,7 @@ Look for: overlapping elements, text overflow, low-contrast text, uneven gaps, i
3. Fix issues
4. Re-verify affected slides
5. Repeat until clean
+6. (High-stakes) Run subagent visual QA for a fresh-eyes check
---
diff --git a/mateclaw-server/src/main/resources/skills/pptx/editing.md b/mateclaw-server/src/main/resources/skills/pptx/editing.md
new file mode 100644
index 00000000..60a18b68
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/pptx/editing.md
@@ -0,0 +1,209 @@
+> **Important:** All `scripts/` paths are relative to the skill directory (where SKILL.md is).
+> Run with: `cd {this_skill_dir} && python scripts/...`
+> Or use the `cwd` parameter of `execute_shell_command`.
+
+# Editing Presentations
+
+## Template-Based Workflow
+
+When using an existing presentation as a template:
+
+1. **Analyze existing slides**:
+ ```bash
+ python scripts/thumbnail.py template.pptx
+ python -m markitdown template.pptx
+ ```
+ Review `thumbnails.jpg` to see layouts, and markitdown output to see placeholder text.
+
+2. **Plan slide mapping**: For each content section, choose a template slide.
+
+ ⚠️ **USE VARIED LAYOUTS** — monotonous presentations are a common failure mode. Don't default to basic title + bullet slides. Actively seek out:
+ - Multi-column layouts (2-column, 3-column)
+ - Image + text combinations
+ - Full-bleed images with text overlay
+ - Quote or callout slides
+ - Section dividers
+ - Stat/number callouts
+ - Icon grids or icon + text rows
+
+ **Avoid:** Repeating the same text-heavy layout for every slide.
+
+ Match content type to layout style (e.g., key points → bullet slide, team info → multi-column, testimonials → quote slide).
+
+3. **Unpack**: `python scripts/office/unpack.py template.pptx unpacked/`
+
+4. **Build presentation** (do this yourself, not with subagents):
+ - Delete unwanted slides (remove from ``)
+ - Duplicate slides you want to reuse (`add_slide.py`)
+ - Reorder slides in ``
+ - **Complete all structural changes before step 5**
+
+5. **Edit content**: Update text in each `slide{N}.xml`.
+ **Use subagents here if available** — slides are separate XML files, so subagents can edit in parallel.
+
+6. **Clean**: `python scripts/clean.py unpacked/`
+
+7. **Pack**: `python scripts/office/pack.py unpacked/ output.pptx --original template.pptx`
+
+---
+
+## Scripts
+
+| Script | Purpose |
+|--------|---------|
+| `unpack.py` | Extract and pretty-print PPTX |
+| `add_slide.py` | Duplicate slide or create from layout |
+| `clean.py` | Remove orphaned files |
+| `pack.py` | Repack with validation |
+| `thumbnail.py` | Create visual grid of slides |
+
+### unpack.py
+
+```bash
+python scripts/office/unpack.py input.pptx unpacked/
+```
+
+Extracts PPTX, pretty-prints XML, escapes smart quotes.
+
+### add_slide.py
+
+```bash
+python scripts/add_slide.py unpacked/ slide2.xml # Duplicate slide
+python scripts/add_slide.py unpacked/ slideLayout2.xml # From layout
+```
+
+Prints `` to add to `` at desired position.
+
+### clean.py
+
+```bash
+python scripts/clean.py unpacked/
+```
+
+Removes slides not in ``, unreferenced media, orphaned rels.
+
+### pack.py
+
+```bash
+python scripts/office/pack.py unpacked/ output.pptx --original input.pptx
+```
+
+Validates, repairs, condenses XML, re-encodes smart quotes.
+
+### thumbnail.py
+
+```bash
+python scripts/thumbnail.py input.pptx [output_prefix] [--cols N]
+```
+
+Creates `thumbnails.jpg` with slide filenames as labels. Default 3 columns, max 12 per grid.
+
+**Use for template analysis only** (choosing layouts). For visual QA, use `soffice` + `pdftoppm` to create full-resolution individual slide images—see SKILL.md.
+
+---
+
+## Slide Operations
+
+Slide order is in `ppt/presentation.xml` → ``.
+
+**Reorder**: Rearrange `` elements.
+
+**Delete**: Remove ``, then run `clean.py`.
+
+**Add**: Use `add_slide.py`. Never manually copy slide files—the script handles notes references, Content_Types.xml, and relationship IDs that manual copying misses.
+
+---
+
+## Editing Content
+
+**Subagents:** If available, use them here (after completing step 4). Each slide is a separate XML file, so subagents can edit in parallel. In your prompt to subagents, include:
+- The slide file path(s) to edit
+- **"Use the Edit tool for all changes"**
+- The formatting rules and common pitfalls below
+
+For each slide:
+1. Read the slide's XML
+2. Identify ALL placeholder content—text, images, charts, icons, captions
+3. Replace each placeholder with final content
+
+**Use the Edit tool, not sed or Python scripts.** The Edit tool forces specificity about what to replace and where, yielding better reliability.
+
+### Formatting Rules
+
+- **Bold all headers, subheadings, and inline labels**: Use `b="1"` on ``. This includes:
+ - Slide titles
+ - Section headers within a slide
+ - Inline labels like (e.g.: "Status:", "Description:") at the start of a line
+- **Never use unicode bullets (•)**: Use proper list formatting with `` or ``
+- **Bullet consistency**: Let bullets inherit from the layout. Only specify `` or ``.
+
+---
+
+## Common Pitfalls
+
+### Template Adaptation
+
+When source content has fewer items than the template:
+- **Remove excess elements entirely** (images, shapes, text boxes), don't just clear text
+- Check for orphaned visuals after clearing text content
+- Run visual QA to catch mismatched counts
+
+When replacing text with different length content:
+- **Shorter replacements**: Usually safe
+- **Longer replacements**: May overflow or wrap unexpectedly
+- Test with visual QA after text changes
+- Consider truncating or splitting content to fit the template's design constraints
+
+**Template slots ≠ Source items**: If template has 4 team members but source has 3 users, delete the 4th member's entire group (image + text boxes), not just the text.
+
+### Multi-Item Content
+
+If source has multiple items (numbered lists, multiple sections), create separate `` elements for each — **never concatenate into one string**.
+
+**❌ WRONG** — all items in one paragraph:
+```xml
+
+ Step 1: Do the first thing. Step 2: Do the second thing.
+
+```
+
+**✅ CORRECT** — separate paragraphs with bold headers:
+```xml
+
+
+ Step 1
+
+
+
+ Do the first thing.
+
+
+
+ Step 2
+
+
+```
+
+Copy `` from the original paragraph to preserve line spacing. Use `b="1"` on headers.
+
+### Smart Quotes
+
+Handled automatically by unpack/pack. But the Edit tool converts smart quotes to ASCII.
+
+**When adding new text with quotes, use XML entities:**
+
+```xml
+the “Agreement”
+```
+
+| Character | Name | Unicode | XML Entity |
+|-----------|------|---------|------------|
+| `“` | Left double quote | U+201C | `“` |
+| `”` | Right double quote | U+201D | `”` |
+| `‘` | Left single quote | U+2018 | `‘` |
+| `’` | Right single quote | U+2019 | `’` |
+
+### Other
+
+- **Whitespace**: Use `xml:space="preserve"` on `` with leading/trailing spaces
+- **XML parsing**: Use `defusedxml.minidom`, not `xml.etree.ElementTree` (corrupts namespaces)
diff --git a/mateclaw-server/src/main/resources/skills/pptx/pptxgenjs.md b/mateclaw-server/src/main/resources/skills/pptx/pptxgenjs.md
new file mode 100644
index 00000000..6bfed908
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/pptx/pptxgenjs.md
@@ -0,0 +1,420 @@
+# PptxGenJS Tutorial
+
+## Setup & Basic Structure
+
+```javascript
+const pptxgen = require("pptxgenjs");
+
+let pres = new pptxgen();
+pres.layout = 'LAYOUT_16x9'; // or 'LAYOUT_16x10', 'LAYOUT_4x3', 'LAYOUT_WIDE'
+pres.author = 'Your Name';
+pres.title = 'Presentation Title';
+
+let slide = pres.addSlide();
+slide.addText("Hello World!", { x: 0.5, y: 0.5, fontSize: 36, color: "363636" });
+
+pres.writeFile({ fileName: "Presentation.pptx" });
+```
+
+## Layout Dimensions
+
+Slide dimensions (coordinates in inches):
+- `LAYOUT_16x9`: 10" × 5.625" (default)
+- `LAYOUT_16x10`: 10" × 6.25"
+- `LAYOUT_4x3`: 10" × 7.5"
+- `LAYOUT_WIDE`: 13.3" × 7.5"
+
+---
+
+## Text & Formatting
+
+```javascript
+// Basic text
+slide.addText("Simple Text", {
+ x: 1, y: 1, w: 8, h: 2, fontSize: 24, fontFace: "Arial",
+ color: "363636", bold: true, align: "center", valign: "middle"
+});
+
+// Character spacing (use charSpacing, not letterSpacing which is silently ignored)
+slide.addText("SPACED TEXT", { x: 1, y: 1, w: 8, h: 1, charSpacing: 6 });
+
+// Rich text arrays
+slide.addText([
+ { text: "Bold ", options: { bold: true } },
+ { text: "Italic ", options: { italic: true } }
+], { x: 1, y: 3, w: 8, h: 1 });
+
+// Multi-line text (requires breakLine: true)
+slide.addText([
+ { text: "Line 1", options: { breakLine: true } },
+ { text: "Line 2", options: { breakLine: true } },
+ { text: "Line 3" } // Last item doesn't need breakLine
+], { x: 0.5, y: 0.5, w: 8, h: 2 });
+
+// Text box margin (internal padding)
+slide.addText("Title", {
+ x: 0.5, y: 0.3, w: 9, h: 0.6,
+ margin: 0 // Use 0 when aligning text with other elements like shapes or icons
+});
+```
+
+**Tip:** Text boxes have internal margin by default. Set `margin: 0` when you need text to align precisely with shapes, lines, or icons at the same x-position.
+
+---
+
+## Lists & Bullets
+
+```javascript
+// ✅ CORRECT: Multiple bullets
+slide.addText([
+ { text: "First item", options: { bullet: true, breakLine: true } },
+ { text: "Second item", options: { bullet: true, breakLine: true } },
+ { text: "Third item", options: { bullet: true } }
+], { x: 0.5, y: 0.5, w: 8, h: 3 });
+
+// ❌ WRONG: Never use unicode bullets
+slide.addText("• First item", { ... }); // Creates double bullets
+
+// Sub-items and numbered lists
+{ text: "Sub-item", options: { bullet: true, indentLevel: 1 } }
+{ text: "First", options: { bullet: { type: "number" }, breakLine: true } }
+```
+
+---
+
+## Shapes
+
+```javascript
+slide.addShape(pres.shapes.RECTANGLE, {
+ x: 0.5, y: 0.8, w: 1.5, h: 3.0,
+ fill: { color: "FF0000" }, line: { color: "000000", width: 2 }
+});
+
+slide.addShape(pres.shapes.OVAL, { x: 4, y: 1, w: 2, h: 2, fill: { color: "0000FF" } });
+
+slide.addShape(pres.shapes.LINE, {
+ x: 1, y: 3, w: 5, h: 0, line: { color: "FF0000", width: 3, dashType: "dash" }
+});
+
+// With transparency
+slide.addShape(pres.shapes.RECTANGLE, {
+ x: 1, y: 1, w: 3, h: 2,
+ fill: { color: "0088CC", transparency: 50 }
+});
+
+// Rounded rectangle (rectRadius only works with ROUNDED_RECTANGLE, not RECTANGLE)
+// ⚠️ Don't pair with rectangular accent overlays — they won't cover rounded corners. Use RECTANGLE instead.
+slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
+ x: 1, y: 1, w: 3, h: 2,
+ fill: { color: "FFFFFF" }, rectRadius: 0.1
+});
+
+// With shadow
+slide.addShape(pres.shapes.RECTANGLE, {
+ x: 1, y: 1, w: 3, h: 2,
+ fill: { color: "FFFFFF" },
+ shadow: { type: "outer", color: "000000", blur: 6, offset: 2, angle: 135, opacity: 0.15 }
+});
+```
+
+Shadow options:
+
+| Property | Type | Range | Notes |
+|----------|------|-------|-------|
+| `type` | string | `"outer"`, `"inner"` | |
+| `color` | string | 6-char hex (e.g. `"000000"`) | No `#` prefix, no 8-char hex — see Common Pitfalls |
+| `blur` | number | 0-100 pt | |
+| `offset` | number | 0-200 pt | **Must be non-negative** — negative values corrupt the file |
+| `angle` | number | 0-359 degrees | Direction the shadow falls (135 = bottom-right, 270 = upward) |
+| `opacity` | number | 0.0-1.0 | Use this for transparency, never encode in color string |
+
+To cast a shadow upward (e.g. on a footer bar), use `angle: 270` with a positive offset — do **not** use a negative offset.
+
+**Note**: Gradient fills are not natively supported. Use a gradient image as a background instead.
+
+---
+
+## Images
+
+### Image Sources
+
+```javascript
+// From file path
+slide.addImage({ path: "images/chart.png", x: 1, y: 1, w: 5, h: 3 });
+
+// From URL
+slide.addImage({ path: "https://example.com/image.jpg", x: 1, y: 1, w: 5, h: 3 });
+
+// From base64 (faster, no file I/O)
+slide.addImage({ data: "image/png;base64,iVBORw0KGgo...", x: 1, y: 1, w: 5, h: 3 });
+```
+
+### Image Options
+
+```javascript
+slide.addImage({
+ path: "image.png",
+ x: 1, y: 1, w: 5, h: 3,
+ rotate: 45, // 0-359 degrees
+ rounding: true, // Circular crop
+ transparency: 50, // 0-100
+ flipH: true, // Horizontal flip
+ flipV: false, // Vertical flip
+ altText: "Description", // Accessibility
+ hyperlink: { url: "https://example.com" }
+});
+```
+
+### Image Sizing Modes
+
+```javascript
+// Contain - fit inside, preserve ratio
+{ sizing: { type: 'contain', w: 4, h: 3 } }
+
+// Cover - fill area, preserve ratio (may crop)
+{ sizing: { type: 'cover', w: 4, h: 3 } }
+
+// Crop - cut specific portion
+{ sizing: { type: 'crop', x: 0.5, y: 0.5, w: 2, h: 2 } }
+```
+
+### Calculate Dimensions (preserve aspect ratio)
+
+```javascript
+const origWidth = 1978, origHeight = 923, maxHeight = 3.0;
+const calcWidth = maxHeight * (origWidth / origHeight);
+const centerX = (10 - calcWidth) / 2;
+
+slide.addImage({ path: "image.png", x: centerX, y: 1.2, w: calcWidth, h: maxHeight });
+```
+
+### Supported Formats
+
+- **Standard**: PNG, JPG, GIF (animated GIFs work in Microsoft 365)
+- **SVG**: Works in modern PowerPoint/Microsoft 365
+
+---
+
+## Icons
+
+Use react-icons to generate SVG icons, then rasterize to PNG for universal compatibility.
+
+### Setup
+
+```javascript
+const React = require("react");
+const ReactDOMServer = require("react-dom/server");
+const sharp = require("sharp");
+const { FaCheckCircle, FaChartLine } = require("react-icons/fa");
+
+function renderIconSvg(IconComponent, color = "#000000", size = 256) {
+ return ReactDOMServer.renderToStaticMarkup(
+ React.createElement(IconComponent, { color, size: String(size) })
+ );
+}
+
+async function iconToBase64Png(IconComponent, color, size = 256) {
+ const svg = renderIconSvg(IconComponent, color, size);
+ const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer();
+ return "image/png;base64," + pngBuffer.toString("base64");
+}
+```
+
+### Add Icon to Slide
+
+```javascript
+const iconData = await iconToBase64Png(FaCheckCircle, "#4472C4", 256);
+
+slide.addImage({
+ data: iconData,
+ x: 1, y: 1, w: 0.5, h: 0.5 // Size in inches
+});
+```
+
+**Note**: Use size 256 or higher for crisp icons. The size parameter controls the rasterization resolution, not the display size on the slide (which is set by `w` and `h` in inches).
+
+### Icon Libraries
+
+Install: `npm install -g react-icons react react-dom sharp`
+
+Popular icon sets in react-icons:
+- `react-icons/fa` - Font Awesome
+- `react-icons/md` - Material Design
+- `react-icons/hi` - Heroicons
+- `react-icons/bi` - Bootstrap Icons
+
+---
+
+## Slide Backgrounds
+
+```javascript
+// Solid color
+slide.background = { color: "F1F1F1" };
+
+// Color with transparency
+slide.background = { color: "FF3399", transparency: 50 };
+
+// Image from URL
+slide.background = { path: "https://example.com/bg.jpg" };
+
+// Image from base64
+slide.background = { data: "image/png;base64,iVBORw0KGgo..." };
+```
+
+---
+
+## Tables
+
+```javascript
+slide.addTable([
+ ["Header 1", "Header 2"],
+ ["Cell 1", "Cell 2"]
+], {
+ x: 1, y: 1, w: 8, h: 2,
+ border: { pt: 1, color: "999999" }, fill: { color: "F1F1F1" }
+});
+
+// Advanced with merged cells
+let tableData = [
+ [{ text: "Header", options: { fill: { color: "6699CC" }, color: "FFFFFF", bold: true } }, "Cell"],
+ [{ text: "Merged", options: { colspan: 2 } }]
+];
+slide.addTable(tableData, { x: 1, y: 3.5, w: 8, colW: [4, 4] });
+```
+
+---
+
+## Charts
+
+```javascript
+// Bar chart
+slide.addChart(pres.charts.BAR, [{
+ name: "Sales", labels: ["Q1", "Q2", "Q3", "Q4"], values: [4500, 5500, 6200, 7100]
+}], {
+ x: 0.5, y: 0.6, w: 6, h: 3, barDir: 'col',
+ showTitle: true, title: 'Quarterly Sales'
+});
+
+// Line chart
+slide.addChart(pres.charts.LINE, [{
+ name: "Temp", labels: ["Jan", "Feb", "Mar"], values: [32, 35, 42]
+}], { x: 0.5, y: 4, w: 6, h: 3, lineSize: 3, lineSmooth: true });
+
+// Pie chart
+slide.addChart(pres.charts.PIE, [{
+ name: "Share", labels: ["A", "B", "Other"], values: [35, 45, 20]
+}], { x: 7, y: 1, w: 5, h: 4, showPercent: true });
+```
+
+### Better-Looking Charts
+
+Default charts look dated. Apply these options for a modern, clean appearance:
+
+```javascript
+slide.addChart(pres.charts.BAR, chartData, {
+ x: 0.5, y: 1, w: 9, h: 4, barDir: "col",
+
+ // Custom colors (match your presentation palette)
+ chartColors: ["0D9488", "14B8A6", "5EEAD4"],
+
+ // Clean background
+ chartArea: { fill: { color: "FFFFFF" }, roundedCorners: true },
+
+ // Muted axis labels
+ catAxisLabelColor: "64748B",
+ valAxisLabelColor: "64748B",
+
+ // Subtle grid (value axis only)
+ valGridLine: { color: "E2E8F0", size: 0.5 },
+ catGridLine: { style: "none" },
+
+ // Data labels on bars
+ showValue: true,
+ dataLabelPosition: "outEnd",
+ dataLabelColor: "1E293B",
+
+ // Hide legend for single series
+ showLegend: false,
+});
+```
+
+**Key styling options:**
+- `chartColors: [...]` - hex colors for series/segments
+- `chartArea: { fill, border, roundedCorners }` - chart background
+- `catGridLine/valGridLine: { color, style, size }` - grid lines (`style: "none"` to hide)
+- `lineSmooth: true` - curved lines (line charts)
+- `legendPos: "r"` - legend position: "b", "t", "l", "r", "tr"
+
+---
+
+## Slide Masters
+
+```javascript
+pres.defineSlideMaster({
+ title: 'TITLE_SLIDE', background: { color: '283A5E' },
+ objects: [{
+ placeholder: { options: { name: 'title', type: 'title', x: 1, y: 2, w: 8, h: 2 } }
+ }]
+});
+
+let titleSlide = pres.addSlide({ masterName: "TITLE_SLIDE" });
+titleSlide.addText("My Title", { placeholder: "title" });
+```
+
+---
+
+## Common Pitfalls
+
+⚠️ These issues cause file corruption, visual bugs, or broken output. Avoid them.
+
+1. **NEVER use "#" with hex colors** - causes file corruption
+ ```javascript
+ color: "FF0000" // ✅ CORRECT
+ color: "#FF0000" // ❌ WRONG
+ ```
+
+2. **NEVER encode opacity in hex color strings** - 8-char colors (e.g., `"00000020"`) corrupt the file. Use the `opacity` property instead.
+ ```javascript
+ shadow: { type: "outer", blur: 6, offset: 2, color: "00000020" } // ❌ CORRUPTS FILE
+ shadow: { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.12 } // ✅ CORRECT
+ ```
+
+3. **Use `bullet: true`** - NEVER unicode symbols like "•" (creates double bullets)
+
+4. **Use `breakLine: true`** between array items or text runs together
+
+5. **Avoid `lineSpacing` with bullets** - causes excessive gaps; use `paraSpaceAfter` instead
+
+6. **Each presentation needs fresh instance** - don't reuse `pptxgen()` objects
+
+7. **NEVER reuse option objects across calls** - PptxGenJS mutates objects in-place (e.g. converting shadow values to EMU). Sharing one object between multiple calls corrupts the second shape.
+ ```javascript
+ const shadow = { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 };
+ slide.addShape(pres.shapes.RECTANGLE, { shadow, ... }); // ❌ second call gets already-converted values
+ slide.addShape(pres.shapes.RECTANGLE, { shadow, ... });
+
+ const makeShadow = () => ({ type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 });
+ slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... }); // ✅ fresh object each time
+ slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... });
+ ```
+
+8. **Don't use `ROUNDED_RECTANGLE` with accent borders** - rectangular overlay bars won't cover rounded corners. Use `RECTANGLE` instead.
+ ```javascript
+ // ❌ WRONG: Accent bar doesn't cover rounded corners
+ slide.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } });
+ slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 0.08, h: 1.5, fill: { color: "0891B2" } });
+
+ // ✅ CORRECT: Use RECTANGLE for clean alignment
+ slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } });
+ slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 0.08, h: 1.5, fill: { color: "0891B2" } });
+ ```
+
+---
+
+## Quick Reference
+
+- **Shapes**: RECTANGLE, OVAL, LINE, ROUNDED_RECTANGLE
+- **Charts**: BAR, LINE, PIE, DOUGHNUT, SCATTER, BUBBLE, RADAR
+- **Layouts**: LAYOUT_16x9 (10"×5.625"), LAYOUT_16x10, LAYOUT_4x3, LAYOUT_WIDE
+- **Alignment**: "left", "center", "right"
+- **Chart data labels**: "outEnd", "inEnd", "center"
diff --git a/mateclaw-server/src/main/resources/skills/xlsx/LICENSE.txt b/mateclaw-server/src/main/resources/skills/xlsx/LICENSE.txt
new file mode 100644
index 00000000..c55ab422
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skills/xlsx/LICENSE.txt
@@ -0,0 +1,30 @@
+© 2025 Anthropic, PBC. All rights reserved.
+
+LICENSE: Use of these materials (including all code, prompts, assets, files,
+and other components of this Skill) is governed by your agreement with
+Anthropic regarding use of Anthropic's services. If no separate agreement
+exists, use is governed by Anthropic's Consumer Terms of Service or
+Commercial Terms of Service, as applicable:
+https://www.anthropic.com/legal/consumer-terms
+https://www.anthropic.com/legal/commercial-terms
+Your applicable agreement is referred to as the "Agreement." "Services" are
+as defined in the Agreement.
+
+ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
+contrary, users may not:
+
+- Extract these materials from the Services or retain copies of these
+ materials outside the Services
+- Reproduce or copy these materials, except for temporary copies created
+ automatically during authorized use of the Services
+- Create derivative works based on these materials
+- Distribute, sublicense, or transfer these materials to any third party
+- Make, offer to sell, sell, or import any inventions embodied in these
+ materials
+- Reverse engineer, decompile, or disassemble these materials
+
+The receipt, viewing, or possession of these materials does not convey or
+imply any license or right beyond those expressly granted above.
+
+Anthropic retains all right, title, and interest in these materials,
+including all copyrights, patents, and other intellectual property rights.