feat(tool): native Java DocxRender tool — eliminate Node.js subprocess

This commit is contained in:
matevip 2026-04-23 16:31:13 +08:00
parent 9740d46fbc
commit edaf762878
40 changed files with 4008 additions and 17 deletions

View File

@ -280,6 +280,17 @@
<version>2.2.0</version>
</dependency>
<!-- ===== Apache POI (in-process .docx generation) ===== -->
<!--
Used by DocxRenderTool to render Markdown into a .docx in-JVM,
replacing the Node.js docx-js subprocess (3-5 min cold install).
-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.4.1</version>
</dependency>
<!-- ===== Database Migration (Flyway) ===== -->
<dependency>
<groupId>org.flywaydb</groupId>

View File

@ -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<Message> 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<Message> 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<String> callIds = new java.util.HashSet<>();
Set<String> 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;
}

View File

@ -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<Message> 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<String, Object> extra) {
if (streamTracker == null || conversationId == null || conversationId.isEmpty()) {
return;

View File

@ -20,7 +20,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
/**
* Database bootstrap runner.
* <p>
* Executes schema.sql and tools-sync.sql on every startup.
* Loads seed data after Flyway migrations on every startup.
* <p>
* For data.sql (seed data with locale-specific content):
* <ul>

View File

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

View File

@ -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.
*
* <p>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.
*
* <p>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;
}
}

View File

@ -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.
*
* <p>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<String, Entry> 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<Entry> 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);
}
}

View File

@ -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}.
*
* <p>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)
.<ResponseEntity<?>>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();
}
}

View File

@ -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.
*
* <p>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.
*
* <p>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<String> 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 <w:rFonts/> 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<String> tableLines) {
List<String[]> 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<String> splitLines(String text) {
List<String> 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;
}
}

View File

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

View File

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

View File

@ -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 ServerFilesystem参考 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_contentSKILL.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.

View File

@ -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 ServerFilesystem参考 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_contentSKILL.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.

View File

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

View File

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

View File

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

View File

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

View File

@ -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` 断开连接

View File

@ -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 超时计时

View File

@ -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 <agentId> --channel <channel>"
)
```
**Windows**
```
execute_shell_command(
command="mateclaw.exe chats list --agent-id <agentId> --channel <channel>"
)
```
从返回结果中获取 `user_id``session_id`。有多个会话时,优先选 `updated_at` 最近的。
### 第二步:发送消息
**macOS / Linux**
```
execute_shell_command(
command="mateclaw channels send --agent-id <agentId> --channel <channel> --target-user <userId> --target-session <sessionId> --text \"消息内容\""
)
```
**WindowsPowerShell**
```
execute_shell_command(
command="mateclaw.exe channels send --agent-id <agentId> --channel <channel> --target-user <userId> --target-session <sessionId> --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` 是单向推送,不返回用户回复

View File

@ -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` 参数中附带上一次的关键结论

View File

@ -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 分钟的任务除非用户明确需要

View File

@ -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/PNG240×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")
```

View File

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

View File

@ -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/<uuid>)` 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

View File

@ -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**:使用正斜杠 `/`,含空格时须加引号或转义
## 安全规范
- 只读取文件,不执行其内容
- 优先读取所需的最小部分,避免加载超大文件到上下文
- 如文件包含敏感信息(密码、密钥),提醒用户注意但不拒绝读取

View File

@ -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` |
## 输出质量要求
- 不编造不存在的配置项或命令
- 涉及版本差异时标注"请以当前版本文档为准"
- 涉及路径、命令、配置键时给可复制的原文片段
- 若信息仍不足,明确告知用户缺少哪类信息(操作系统、安装方式、报错日志等)

View File

@ -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 步。仍然只需要补充计划细节,不需要代执行。
刚才的计划摘要:
<粘贴上次回复的关键内容>
需要细化的问题:
<具体的疑问>"
)
```
## 计划质量标准
合格的计划必须满足:
- 有明确步骤,不是泛泛建议
- 步骤顺序清楚
- 每步是可执行的具体动作
- 关键依赖被点明
- 有必要的验证点
若收到的是空泛建议,继续追问细化,直到满足以上标准。
## 护栏
- 不要把"请帮我规划"说成"请你直接做完"
- 不要请求对方执行代码、命令或变更
- 拿到计划后仍要结合当前环境判断,不要照单全收
- 目标不清楚时,先弄清楚目标,再要计划

View File

@ -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 获取最新架构描述

View File

@ -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 分工、并行或串行编排 |

View File

@ -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/Atomblogwatcher
当用户需要**定期追踪**某个来源而非单次查询时,使用 `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"`
- 多个类别时分别搜索,避免混淆
- 网站无法访问时说明原因并提供备用来源链接
- 不编造新闻,所有内容来自实际搜索/浏览结果

View File

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

View File

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

View File

@ -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 <file.pdf>`, 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 <input.pdf> <field_info.json>`. 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 <file.pdf> <output_directory>`
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 <input pdf> <field_values.json> <output pdf>`
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 <input.pdf> 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 <input.pdf> <images_dir/>`
### 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 <page_image> -crop <width>x<height>+<x>+<y> +repage <crop_output.png>
```
Where:
- `<x>, <y>` = top-left corner of crop region (use your rough estimate minus padding)
- `<width>, <height>` = 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 <input.pdf> fields.json <output.pdf>`
## Step 4: Verify Output
Convert the filled PDF to images and verify text placement:
`python scripts/convert_pdf_to_images.py <output.pdf> <verify_images/>`
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

View File

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

View File

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

View File

@ -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: <path to slide images or pptx>
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
---

View File

@ -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 `<p:sldIdLst>`)
- Duplicate slides you want to reuse (`add_slide.py`)
- Reorder slides in `<p:sldIdLst>`
- **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 `<p:sldId>` to add to `<p:sldIdLst>` at desired position.
### clean.py
```bash
python scripts/clean.py unpacked/
```
Removes slides not in `<p:sldIdLst>`, 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``<p:sldIdLst>`.
**Reorder**: Rearrange `<p:sldId>` elements.
**Delete**: Remove `<p:sldId>`, 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 `<a:rPr>`. 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 `<a:buChar>` or `<a:buAutoNum>`
- **Bullet consistency**: Let bullets inherit from the layout. Only specify `<a:buChar>` or `<a:buNone>`.
---
## 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 `<a:p>` elements for each — **never concatenate into one string**.
**❌ WRONG** — all items in one paragraph:
```xml
<a:p>
<a:r><a:rPr .../><a:t>Step 1: Do the first thing. Step 2: Do the second thing.</a:t></a:r>
</a:p>
```
**✅ CORRECT** — separate paragraphs with bold headers:
```xml
<a:p>
<a:pPr algn="l"><a:lnSpc><a:spcPts val="3919"/></a:lnSpc></a:pPr>
<a:r><a:rPr lang="en-US" sz="2799" b="1" .../><a:t>Step 1</a:t></a:r>
</a:p>
<a:p>
<a:pPr algn="l"><a:lnSpc><a:spcPts val="3919"/></a:lnSpc></a:pPr>
<a:r><a:rPr lang="en-US" sz="2799" .../><a:t>Do the first thing.</a:t></a:r>
</a:p>
<a:p>
<a:pPr algn="l"><a:lnSpc><a:spcPts val="3919"/></a:lnSpc></a:pPr>
<a:r><a:rPr lang="en-US" sz="2799" b="1" .../><a:t>Step 2</a:t></a:r>
</a:p>
<!-- continue pattern -->
```
Copy `<a:pPr>` 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
<a:t>the &#x201C;Agreement&#x201D;</a:t>
```
| Character | Name | Unicode | XML Entity |
|-----------|------|---------|------------|
| `“` | Left double quote | U+201C | `&#x201C;` |
| `”` | Right double quote | U+201D | `&#x201D;` |
| `` | Left single quote | U+2018 | `&#x2018;` |
| `` | Right single quote | U+2019 | `&#x2019;` |
### Other
- **Whitespace**: Use `xml:space="preserve"` on `<a:t>` with leading/trailing spaces
- **XML parsing**: Use `defusedxml.minidom`, not `xml.etree.ElementTree` (corrupts namespaces)

View File

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

View File

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