mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(tool): native Java DocxRender tool — eliminate Node.js subprocess
This commit is contained in:
parent
9740d46fbc
commit
edaf762878
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -390,6 +390,11 @@ INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name
|
||||
VALUES (1000000018, 'CronJobTool', '定时任务', '通过对话创建、查看、启停和删除定时任务。支持 5 字段 cron 表达式,灵活设定执行时间。', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
-- 内置工具:DOCX 渲染(RFC-045 — 进程内 Apache POI,毫秒级新建 .docx)
|
||||
INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
|
||||
VALUES (1000000019, 'DocxRenderTool', 'DOCX 渲染', '将 Markdown 直接渲染为 .docx 并返回一次性下载链接。进程内 Apache POI 实现,无需 Node.js 子进程;支持标题、加粗、列表、表格。新建文档场景的首选工具。', 'builtin', 'docxRenderTool', '📝', TRUE, TRUE, NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
-- 示例 MCP Server:Filesystem(参考 MateClaw 文档中的 mcpServers.filesystem)
|
||||
INSERT INTO mate_mcp_server (
|
||||
id, name, description, transport, url, headers_json, command, args_json, env_json, cwd,
|
||||
@ -513,6 +518,22 @@ INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author
|
||||
VALUES (1000000015, 'steve_jobs_perspective', '史蒂夫·乔布斯思维操作系统。以乔布斯视角审视产品、评估决策、提供反馈,运用其六大心智模型和独特表达风格。', 'builtin', '🍎', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'persona,jobs,product,strategy,thinking', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
|
||||
VALUES (1000000016, 'make_plan', '当任务需要多步拆解或不确定执行路径时,向更强 Agent 请求一份分步可落地的执行计划,由当前 Agent 自己执行。', 'builtin', '🗺️', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'plan,delegate,agent,collaboration', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
|
||||
VALUES (1000000017, 'chat_with_agent', '当需要咨询其他 Agent、寻求帮助或用户明确要求某个 Agent 参与时,使用本技能进行单次或并行委托。', 'builtin', '💬', '1.2.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'agent,chat,collaborate,delegate', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
|
||||
VALUES (1000000018, 'channel_message', '当需要主动向用户、会话或渠道单向推送消息时使用。任务完成通知、定时提醒、异步结果回推等场景。', 'builtin', '📤', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'channel,message,push,notify,dingtalk,feishu', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
|
||||
VALUES (1000000019, 'multi_agent_collaboration', '当任务需要多个 Agent 的专业能力协同完成时,编排多 Agent 并行或串行协作,整合各方结果。', 'builtin', '🤝', '1.4.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'multi-agent,collaboration,orchestration,parallel', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), skill_type=VALUES(skill_type), icon=VALUES(icon), version=VALUES(version), author=VALUES(author), config_json=VALUES(config_json), enabled=VALUES(enabled), builtin=VALUES(builtin), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
-- 为关键 builtin skill 填充 skill_content(SKILL.md 执行协议)
|
||||
-- NOTE: For pdf/docx/pptx/xlsx/himalaya, the authoritative SKILL.md is bundled in
|
||||
-- classpath:skills/{name}/ and auto-synced to workspace on startup.
|
||||
|
||||
@ -395,6 +395,11 @@ MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name,
|
||||
KEY (id)
|
||||
VALUES (1000000018, 'CronJobTool', '定时任务', '通过对话创建、查看、启停和删除定时任务。支持 5 字段 cron 表达式,灵活设定执行时间。', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0);
|
||||
|
||||
-- 内置工具:DOCX 渲染(RFC-045 — 进程内 Apache POI,毫秒级新建 .docx)
|
||||
MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted)
|
||||
KEY (id)
|
||||
VALUES (1000000019, 'DocxRenderTool', 'DOCX 渲染', '将 Markdown 直接渲染为 .docx 并返回一次性下载链接。进程内 Apache POI 实现,无需 Node.js 子进程;支持标题、加粗、列表、表格。新建文档场景的首选工具。', 'builtin', 'docxRenderTool', '📝', TRUE, TRUE, NOW(), NOW(), 0);
|
||||
|
||||
-- 示例 MCP Server:Filesystem(参考 MateClaw 文档中的 mcpServers.filesystem)
|
||||
MERGE INTO mate_mcp_server (
|
||||
id, name, description, transport, url, headers_json, command, args_json, env_json, cwd,
|
||||
@ -518,6 +523,22 @@ MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author,
|
||||
KEY (id)
|
||||
VALUES (1000000015, 'steve_jobs_perspective', '史蒂夫·乔布斯思维操作系统。以乔布斯视角审视产品、评估决策、提供反馈,运用其六大心智模型和独特表达风格。', 'builtin', '🍎', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'persona,jobs,product,strategy,thinking', NOW(), NOW(), 0);
|
||||
|
||||
MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
|
||||
KEY (id)
|
||||
VALUES (1000000016, 'make_plan', '当任务需要多步拆解或不确定执行路径时,向更强 Agent 请求一份分步可落地的执行计划,由当前 Agent 自己执行。', 'builtin', '🗺️', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'plan,delegate,agent,collaboration', NOW(), NOW(), 0);
|
||||
|
||||
MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
|
||||
KEY (id)
|
||||
VALUES (1000000017, 'chat_with_agent', '当需要咨询其他 Agent、寻求帮助或用户明确要求某个 Agent 参与时,使用本技能进行单次或并行委托。', 'builtin', '💬', '1.2.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'agent,chat,collaborate,delegate', NOW(), NOW(), 0);
|
||||
|
||||
MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
|
||||
KEY (id)
|
||||
VALUES (1000000018, 'channel_message', '当需要主动向用户、会话或渠道单向推送消息时使用。任务完成通知、定时提醒、异步结果回推等场景。', 'builtin', '📤', '1.3.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'channel,message,push,notify,dingtalk,feishu', NOW(), NOW(), 0);
|
||||
|
||||
MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted)
|
||||
KEY (id)
|
||||
VALUES (1000000019, 'multi_agent_collaboration', '当任务需要多个 Agent 的专业能力协同完成时,编排多 Agent 并行或串行协作,整合各方结果。', 'builtin', '🤝', '1.4.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'multi-agent,collaboration,orchestration,parallel', NOW(), NOW(), 0);
|
||||
|
||||
-- 为关键 builtin skill 填充 skill_content(SKILL.md 执行协议)
|
||||
-- NOTE: For pdf/docx/pptx/xlsx/himalaya, the authoritative SKILL.md is bundled in
|
||||
-- classpath:skills/{name}/ and auto-synced to workspace on startup.
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
100
mateclaw-server/src/main/resources/skills/browser_cdp/SKILL.md
Normal file
100
mateclaw-server/src/main/resources/skills/browser_cdp/SKILL.md
Normal 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` 断开连接
|
||||
@ -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 超时计时
|
||||
@ -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 \"消息内容\""
|
||||
)
|
||||
```
|
||||
|
||||
**Windows(PowerShell):**
|
||||
```
|
||||
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` 是单向推送,不返回用户回复
|
||||
@ -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` 参数中附带上一次的关键结论
|
||||
106
mateclaw-server/src/main/resources/skills/cron/SKILL.md
Normal file
106
mateclaw-server/src/main/resources/skills/cron/SKILL.md
Normal 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 分钟)的任务,除非用户明确需要
|
||||
@ -0,0 +1,139 @@
|
||||
---
|
||||
name: dingtalk_channel_connect
|
||||
version: "1.3.0"
|
||||
description: "使用可见浏览器自动完成 MateClaw 钉钉渠道接入。遇到登录页必须暂停等待用户手动登录后继续。"
|
||||
dependencies:
|
||||
tools:
|
||||
- browser_use
|
||||
- execute_shell_command
|
||||
- read_file
|
||||
---
|
||||
|
||||
# 钉钉渠道接入(可见浏览器)
|
||||
|
||||
通过可见浏览器自动化完成钉钉应用创建与 MateClaw 渠道绑定。
|
||||
|
||||
## 强制规则
|
||||
|
||||
1. **必须使用可见浏览器**:`browser_use(action="start", headed=true)`
|
||||
2. **遇到登录页必须暂停**:检测到登录界面立即停止,提示用户手动登录,收到"继续"后再执行
|
||||
3. **配置变更后必须发布**:任何机器人配置修改都要"创建新版本 + 发布",否则不生效
|
||||
|
||||
## 执行前确认(必须先做)
|
||||
|
||||
开始自动化前向用户确认以下可定制项(未指定则使用默认值):
|
||||
|
||||
| 配置项 | 默认值 |
|
||||
|--------|--------|
|
||||
| 应用名称 | `MateClaw` |
|
||||
| 应用描述 | `Your personal AI assistant` |
|
||||
| 机器人图标 | `https://img.alicdn.com/imgextra/i4/O1CN01M0iyHF1FVNzM9qjC0_!!6000000000492-2-tps-254-254.png` |
|
||||
| 机器人消息预览图 | 同上 |
|
||||
|
||||
**图片规范(务必告知用户)**:
|
||||
- 机器人图标:JPG/PNG,240×240px 以上,1:1 比例,2MB 以内
|
||||
- 消息预览图:PNG/JPEG/JPG,不超过 2MB
|
||||
|
||||
## 图片上传策略
|
||||
|
||||
1. 用户提供本地路径 → 直接上传
|
||||
2. 用户提供图片链接 → 先下载到本地临时文件,再上传
|
||||
|
||||
**下载图片(跨平台):**
|
||||
|
||||
macOS / Linux:
|
||||
```
|
||||
execute_shell_command(
|
||||
command="curl -L -o /tmp/bot_icon.png \"<图片URL>\""
|
||||
)
|
||||
```
|
||||
|
||||
Windows:
|
||||
```
|
||||
execute_shell_command(
|
||||
command="powershell Invoke-WebRequest -Uri '<图片URL>' -OutFile 'C:\\Temp\\bot_icon.png'"
|
||||
)
|
||||
```
|
||||
|
||||
**上传步骤**(必须按此顺序):
|
||||
1. 先 `browser_use(action="click", selector="<上传入口>")` 触发文件选择器
|
||||
2. 再用文件上传操作(MateClaw browser_use 支持 file input 的 `type` 操作传入路径)
|
||||
|
||||
## 自动化流程
|
||||
|
||||
### 步骤 1:打开钉钉开发者后台
|
||||
|
||||
```
|
||||
browser_use(action="start", headed=true)
|
||||
browser_use(action="open", url="https://open-dev.dingtalk.com/")
|
||||
browser_use(action="snapshot")
|
||||
```
|
||||
|
||||
若页面显示登录界面,**立即暂停**:
|
||||
> 检测到需要登录钉钉开发者后台。请在弹出的浏览器中完成登录,完成后回复"继续"。
|
||||
|
||||
### 步骤 2:创建企业内部应用
|
||||
|
||||
用户确认登录后:
|
||||
|
||||
1. 导航路径:应用开发 → 企业内部应用 → 钉钉应用 → 创建应用
|
||||
2. 填写应用名称、应用描述
|
||||
3. 保存创建
|
||||
|
||||
```
|
||||
# 每次关键操作后都要截快照确认状态
|
||||
browser_use(action="snapshot")
|
||||
```
|
||||
|
||||
### 步骤 3:添加机器人能力
|
||||
|
||||
1. 进入「应用能力」→「添加应用能力」→ 找到「机器人」并添加
|
||||
2. 打开机器人配置开关
|
||||
3. 填写机器人名称、简介、描述
|
||||
4. 上传机器人图标(见图片上传策略)
|
||||
5. 上传消息预览图
|
||||
6. 确认消息接收模式为 **Stream 模式**
|
||||
7. 点击发布 → 确认发布弹窗
|
||||
|
||||
**发布是必须步骤,未发布前配置不生效。**
|
||||
|
||||
### 步骤 4:创建版本并发布
|
||||
|
||||
1. 进入「应用发布」→「版本管理与发布」
|
||||
2. 创建新版本,填写版本说明
|
||||
3. 应用可见范围选「全部员工」
|
||||
4. 确认发布(有二次确认弹窗,选确认)
|
||||
5. 看到「发布成功」状态才继续
|
||||
|
||||
### 步骤 5:获取凭证并引导绑定
|
||||
|
||||
1. 进入「基础信息」→「凭证与基础信息」
|
||||
2. 告知用户 `Client ID`(AppKey)和 `Client Secret`(AppSecret)的位置
|
||||
3. 引导用户在 MateClaw 控制台绑定:
|
||||
|
||||
**方式 A — 控制台前端:**
|
||||
> 进入 MateClaw 管理界面 → 渠道 → 新建渠道 → 选择钉钉 → 填入 Client ID 和 Client Secret
|
||||
|
||||
**方式 B — 配置文件:**
|
||||
```json
|
||||
"dingtalk": {
|
||||
"enabled": true,
|
||||
"client_id": "你的 Client ID",
|
||||
"client_secret": "你的 Client Secret"
|
||||
}
|
||||
```
|
||||
|
||||
**Agent 不主动修改 MateClaw 配置文件,只引导用户操作。**
|
||||
|
||||
## 稳定性策略
|
||||
|
||||
- 优先使用 `snapshot` 返回的 `ref` 定位元素
|
||||
- 每次关键点击 / 页面跳转后重新 `snapshot` 确认状态
|
||||
- 页面结构与预期不符时重新 `snapshot`,按可见文本重新定位
|
||||
- 租户权限、管理员审批等阻塞时,说明卡点,请用户手动完成该步骤
|
||||
|
||||
## 完成后关闭浏览器
|
||||
|
||||
```
|
||||
browser_use(action="stop")
|
||||
```
|
||||
30
mateclaw-server/src/main/resources/skills/docx/LICENSE.txt
Normal file
30
mateclaw-server/src/main/resources/skills/docx/LICENSE.txt
Normal 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.
|
||||
@ -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
|
||||
|
||||
@ -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**:使用正斜杠 `/`,含空格时须加引号或转义
|
||||
|
||||
## 安全规范
|
||||
|
||||
- 只读取文件,不执行其内容
|
||||
- 优先读取所需的最小部分,避免加载超大文件到上下文
|
||||
- 如文件包含敏感信息(密码、密钥),提醒用户注意但不拒绝读取
|
||||
66
mateclaw-server/src/main/resources/skills/guidance/SKILL.md
Normal file
66
mateclaw-server/src/main/resources/skills/guidance/SKILL.md
Normal 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` |
|
||||
|
||||
## 输出质量要求
|
||||
|
||||
- 不编造不存在的配置项或命令
|
||||
- 涉及版本差异时标注"请以当前版本文档为准"
|
||||
- 涉及路径、命令、配置键时给可复制的原文片段
|
||||
- 若信息仍不足,明确告知用户缺少哪类信息(操作系统、安装方式、报错日志等)
|
||||
133
mateclaw-server/src/main/resources/skills/make_plan/SKILL.md
Normal file
133
mateclaw-server/src/main/resources/skills/make_plan/SKILL.md
Normal 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 步。仍然只需要补充计划细节,不需要代执行。
|
||||
|
||||
刚才的计划摘要:
|
||||
<粘贴上次回复的关键内容>
|
||||
|
||||
需要细化的问题:
|
||||
<具体的疑问>"
|
||||
)
|
||||
```
|
||||
|
||||
## 计划质量标准
|
||||
|
||||
合格的计划必须满足:
|
||||
- 有明确步骤,不是泛泛建议
|
||||
- 步骤顺序清楚
|
||||
- 每步是可执行的具体动作
|
||||
- 关键依赖被点明
|
||||
- 有必要的验证点
|
||||
|
||||
若收到的是空泛建议,继续追问细化,直到满足以上标准。
|
||||
|
||||
## 护栏
|
||||
|
||||
- 不要把"请帮我规划"说成"请你直接做完"
|
||||
- 不要请求对方执行代码、命令或变更
|
||||
- 拿到计划后仍要结合当前环境判断,不要照单全收
|
||||
- 目标不清楚时,先弄清楚目标,再要计划
|
||||
@ -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 获取最新架构描述
|
||||
@ -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 分工、并行或串行编排 |
|
||||
116
mateclaw-server/src/main/resources/skills/news/SKILL.md
Normal file
116
mateclaw-server/src/main/resources/skills/news/SKILL.md
Normal 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/Atom(blogwatcher)
|
||||
|
||||
当用户需要**定期追踪**某个来源而非单次查询时,使用 `blogwatcher-cli`。
|
||||
|
||||
安装:`pip install blogwatcher-cli`(SQLite 后端,无需服务器)
|
||||
|
||||
```bash
|
||||
# 添加 RSS 源
|
||||
blogwatcher add --name "科技日报" --url https://www.stdaily.com/rss.xml
|
||||
|
||||
# 添加 Atom 源(也支持)
|
||||
blogwatcher add --name "MIT Tech Review" --url https://www.technologyreview.com/feed/
|
||||
|
||||
# 列出已订阅来源
|
||||
blogwatcher list
|
||||
|
||||
# 拉取所有来源的新文章(增量,只返回未见过的)
|
||||
blogwatcher fetch --all
|
||||
|
||||
# 拉取指定来源
|
||||
blogwatcher fetch --name "科技日报"
|
||||
|
||||
# 查看最近 N 条条目
|
||||
blogwatcher entries --limit 20
|
||||
|
||||
# 搜索历史条目
|
||||
blogwatcher search "人工智能 大模型"
|
||||
|
||||
# 删除来源
|
||||
blogwatcher remove --name "科技日报"
|
||||
```
|
||||
|
||||
`blogwatcher fetch` 是**增量**的:只返回自上次 fetch 以来的新条目,不重复推送旧内容。
|
||||
|
||||
**适用场景**:
|
||||
- 用户说"每天帮我看看某网站有什么新文章"
|
||||
- 需要追踪多个来源、避免重复浏览
|
||||
- 配合 `cron` 技能设置定时抓取任务
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 搜索结果以新鲜度为优先,优先选 `freshness="day"`
|
||||
- 多个类别时分别搜索,避免混淆
|
||||
- 网站无法访问时说明原因并提供备用来源链接
|
||||
- 不编造新闻,所有内容来自实际搜索/浏览结果
|
||||
30
mateclaw-server/src/main/resources/skills/pdf/LICENSE.txt
Normal file
30
mateclaw-server/src/main/resources/skills/pdf/LICENSE.txt
Normal 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.
|
||||
@ -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 |
|
||||
|
||||
298
mateclaw-server/src/main/resources/skills/pdf/forms.md
Normal file
298
mateclaw-server/src/main/resources/skills/pdf/forms.md
Normal 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
|
||||
612
mateclaw-server/src/main/resources/skills/pdf/reference.md
Normal file
612
mateclaw-server/src/main/resources/skills/pdf/reference.md
Normal 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
|
||||
30
mateclaw-server/src/main/resources/skills/pptx/LICENSE.txt
Normal file
30
mateclaw-server/src/main/resources/skills/pptx/LICENSE.txt
Normal 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.
|
||||
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
209
mateclaw-server/src/main/resources/skills/pptx/editing.md
Normal file
209
mateclaw-server/src/main/resources/skills/pptx/editing.md
Normal 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 “Agreement”</a:t>
|
||||
```
|
||||
|
||||
| Character | Name | Unicode | XML Entity |
|
||||
|-----------|------|---------|------------|
|
||||
| `“` | Left double quote | U+201C | `“` |
|
||||
| `”` | Right double quote | U+201D | `”` |
|
||||
| `‘` | Left single quote | U+2018 | `‘` |
|
||||
| `’` | Right single quote | U+2019 | `’` |
|
||||
|
||||
### Other
|
||||
|
||||
- **Whitespace**: Use `xml:space="preserve"` on `<a:t>` with leading/trailing spaces
|
||||
- **XML parsing**: Use `defusedxml.minidom`, not `xml.etree.ElementTree` (corrupts namespaces)
|
||||
420
mateclaw-server/src/main/resources/skills/pptx/pptxgenjs.md
Normal file
420
mateclaw-server/src/main/resources/skills/pptx/pptxgenjs.md
Normal 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"
|
||||
30
mateclaw-server/src/main/resources/skills/xlsx/LICENSE.txt
Normal file
30
mateclaw-server/src/main/resources/skills/xlsx/LICENSE.txt
Normal 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.
|
||||
Loading…
Reference in New Issue
Block a user