diff --git a/mateclaw-server/pom.xml b/mateclaw-server/pom.xml index 303980ab..4bc5932a 100644 --- a/mateclaw-server/pom.xml +++ b/mateclaw-server/pom.xml @@ -280,6 +280,17 @@ 2.2.0 + + + + org.apache.poi + poi-ooxml + 5.4.1 + + org.flywaydb diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java index 58ff362c..50cef765 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java @@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.ToolResponseMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.prompt.ChatOptions; @@ -174,14 +175,79 @@ public class ReasoningNode implements NodeAction { String systemPrompt = accessor.systemPrompt(); List messages = accessor.messages(); - // 消息列表膨胀防护 + // Guard against runaway message list growth. + // + // CRITICAL: a naive head+tail cut can break the OpenAI-compatible protocol invariant + // that requires tool_call / tool_response pairs to be complete: + // + // P0 (originally observed): AssistantMessage(tool_calls) falls into the dropped gap, + // its ToolResponseMessage lands in the kept tail → provider sees an orphaned + // ToolResponseMessage → kimi-code 400 "tool_call_id is not found". + // + // P1 (symmetric): AssistantMessage(tool_calls) is kept in the head at the boundary, + // its ToolResponseMessage falls into the dropped gap → provider sees an assistant + // tool_call with no matching response → also a 400 on strict providers. + // + // Fix: perform the normal cut, then run an iterative bidirectional integrity pass until + // the list is stable: + // • Remove any ToolResponseMessage whose parent AssistantMessage.tool_calls id was + // dropped (P0). + // • Remove any AssistantMessage whose tool_calls have no matching ToolResponseMessage + // (P1). + // Iterate because a P1 removal could expose a new P0 orphan (and vice versa, though that + // is pathological in practice). With ≤40 messages convergence is always fast. + // Dropping incomplete pairs is safe — prior iterations already processed those + // observations; the LLM needs the summary context, not the raw tool I/O. final int MAX_LOOP_MESSAGES = 40; if (messages.size() > MAX_LOOP_MESSAGES) { log.warn("[ReasoningNode] Messages list too large ({} messages), trimming to {} for conversation {}", messages.size(), MAX_LOOP_MESSAGES, conversationId); + int headKeep = Math.min(4, messages.size()); + int tailKeep = MAX_LOOP_MESSAGES - headKeep; + int tailStart = messages.size() - tailKeep; + List trimmed = new ArrayList<>(MAX_LOOP_MESSAGES); - trimmed.addAll(messages.subList(0, Math.min(4, messages.size()))); - trimmed.addAll(messages.subList(messages.size() - (MAX_LOOP_MESSAGES - 4), messages.size())); + trimmed.addAll(messages.subList(0, headKeep)); + trimmed.addAll(messages.subList(tailStart, messages.size())); + + // Iterative bidirectional integrity pass. + int totalRemoved = 0; + boolean changed; + do { + // Snapshot current tool_call ids and response ids. + Set callIds = new java.util.HashSet<>(); + Set respIds = new java.util.HashSet<>(); + for (Message m : trimmed) { + if (m instanceof AssistantMessage am && am.getToolCalls() != null) { + for (AssistantMessage.ToolCall tc : am.getToolCalls()) callIds.add(tc.id()); + } + if (m instanceof ToolResponseMessage trm) { + for (ToolResponseMessage.ToolResponse r : trm.getResponses()) respIds.add(r.id()); + } + } + int before = trimmed.size(); + trimmed.removeIf(m -> { + // P0: ToolResponseMessage whose parent tool_call was dropped + if (m instanceof ToolResponseMessage trm) { + return trm.getResponses().stream().anyMatch(r -> !callIds.contains(r.id())); + } + // P1: AssistantMessage whose tool_call has no ToolResponseMessage + if (m instanceof AssistantMessage am && am.getToolCalls() != null + && !am.getToolCalls().isEmpty()) { + return am.getToolCalls().stream().anyMatch(tc -> !respIds.contains(tc.id())); + } + return false; + }); + int removed = before - trimmed.size(); + totalRemoved += removed; + changed = removed > 0; + } while (changed); + + if (totalRemoved > 0) { + log.warn("[ReasoningNode] Removed {} message(s) with broken tool_call/response pairs " + + "after trim (bidirectional integrity guard), conv={}", totalRemoved, conversationId); + } + messages = trimmed; } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/SummarizingNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/SummarizingNode.java index 018c9589..22fd8d6b 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/SummarizingNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/SummarizingNode.java @@ -3,12 +3,16 @@ package vip.mate.agent.graph.node; import com.alibaba.cloud.ai.graph.OverAllState; import com.alibaba.cloud.ai.graph.action.NodeAction; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.anthropic.AnthropicChatModel; +import org.springframework.ai.anthropic.AnthropicChatOptions; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatOptions; import vip.mate.agent.GraphEventPublisher; import vip.mate.agent.graph.NodeStreamingChatHelper; import vip.mate.agent.graph.state.MateClawStateAccessor; @@ -101,9 +105,15 @@ public class SummarizingNode implements NodeAction { promptMessages.add(new SystemMessage(SYSTEM_PROMPT)); promptMessages.add(new UserMessage(userPrompt)); + // Summarization is mechanical text compression — disable thinking/reasoning to avoid + // inheriting the user's thinkingLevel=high from the model's default options. + // Without this override, a plain Prompt would inherit extended thinking from chatModel + // defaults, causing 100+ second delays for a task that needs no deep reasoning. + Prompt summarizePrompt = buildNoThinkingPrompt(promptMessages); + // 流式调用 LLM,实时推送 content/thinking NodeStreamingChatHelper.StreamResult result = streamingHelper.streamCall( - chatModel, new Prompt(promptMessages), conversationId, "summarizing"); + chatModel, summarizePrompt, conversationId, "summarizing"); // 错误处理:摘要失败时用原始观察的前 500 字符作为 fallback if (result.hasFatalError()) { @@ -181,6 +191,26 @@ public class SummarizingNode implements NodeAction { .build(); } + /** + * Build a Prompt with thinking/reasoning explicitly disabled. + * Summarization is mechanical compression — it never needs extended reasoning, + * and inheriting the user's thinkingLevel=high from model defaults wastes 100+ seconds. + */ + private Prompt buildNoThinkingPrompt(List messages) { + ChatOptions opts; + if (chatModel instanceof AnthropicChatModel) { + opts = AnthropicChatOptions.builder() + .thinking(org.springframework.ai.anthropic.api.AnthropicApi.ThinkingType.DISABLED, 0) + .build(); + } else { + // OpenAI / DashScope / other: omit reasoningEffort to disable chain-of-thought + OpenAiChatOptions oaiOpts = OpenAiChatOptions.builder().build(); + oaiOpts.setStreamUsage(true); + opts = oaiOpts; + } + return new Prompt(messages, opts); + } + private void pushPhase(String conversationId, String phase, Map extra) { if (streamTracker == null || conversationId == null || conversationId.isEmpty()) { return; diff --git a/mateclaw-server/src/main/java/vip/mate/config/DatabaseBootstrapRunner.java b/mateclaw-server/src/main/java/vip/mate/config/DatabaseBootstrapRunner.java index 91d2ec62..b4b2d723 100644 --- a/mateclaw-server/src/main/java/vip/mate/config/DatabaseBootstrapRunner.java +++ b/mateclaw-server/src/main/java/vip/mate/config/DatabaseBootstrapRunner.java @@ -20,7 +20,7 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * Database bootstrap runner. *

- * Executes schema.sql and tools-sync.sql on every startup. + * Loads seed data after Flyway migrations on every startup. *

* For data.sql (seed data with locale-specific content): *

    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.