From d2df5c2797e3b74aeadba0eb331e03d20d164eb5 Mon Sep 17 00:00:00 2001 From: mateaix <7333791@qq.com> Date: Mon, 17 Aug 2026 20:28:40 +0800 Subject: [PATCH] feat(office): add optional OfficeCLI engine (#583) --- .../vip/mate/tool/builtin/OfficeCliTool.java | 484 ++++++++++++++++++ .../src/main/resources/db/data-en.sql | 11 +- .../main/resources/db/data-kingbase-en.sql | 11 +- .../main/resources/db/data-kingbase-zh.sql | 11 +- .../src/main/resources/db/data-mysql-en.sql | 11 +- .../src/main/resources/db/data-mysql-zh.sql | 11 +- .../src/main/resources/db/data-zh.sql | 11 +- .../h2/V185__register_officecli_support.sql | 10 + .../V185__register_officecli_support.sql | 10 + .../V185__register_officecli_support.sql | 10 + .../main/resources/skills/officecli/SKILL.md | 96 ++++ .../mate/tool/builtin/OfficeCliToolTest.java | 241 +++++++++ 12 files changed, 911 insertions(+), 6 deletions(-) create mode 100644 mateclaw-server/src/main/java/vip/mate/tool/builtin/OfficeCliTool.java create mode 100644 mateclaw-server/src/main/resources/db/migration/h2/V185__register_officecli_support.sql create mode 100644 mateclaw-server/src/main/resources/db/migration/kingbase/V185__register_officecli_support.sql create mode 100644 mateclaw-server/src/main/resources/db/migration/mysql/V185__register_officecli_support.sql create mode 100644 mateclaw-server/src/main/resources/skills/officecli/SKILL.md create mode 100644 mateclaw-server/src/test/java/vip/mate/tool/builtin/OfficeCliToolTest.java diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/OfficeCliTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/OfficeCliTool.java new file mode 100644 index 00000000..056c717c --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/OfficeCliTool.java @@ -0,0 +1,484 @@ +package vip.mate.tool.builtin; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import vip.mate.tool.ConcurrencyUnsafe; +import vip.mate.tool.document.FilenameSanitizer; +import vip.mate.tool.document.GeneratedFileCache; +import vip.mate.tool.document.GeneratedFileLink; +import vip.mate.tool.guard.WorkspacePathGuard; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Optional structured adapter for iOfficeAI/OfficeCLI (issue #583). + * + *

The adapter deliberately exposes a narrow operation vocabulary instead of + * accepting an arbitrary command line. Every input is copied into a private + * scratch directory before OfficeCLI sees it, so even mutating operations are + * copy-on-write and can never overwrite the user's source document. Generated + * bytes are immediately moved into {@link GeneratedFileCache}; scratch files + * are removed at the end of the call.

+ */ +@Slf4j +@Component +public class OfficeCliTool { + + private static final Set OFFICE_EXTENSIONS = Set.of("docx", "xlsx", "pptx"); + private static final Set INSPECT_MODES = Set.of( + "outline", "stats", "issues", "text", "annotated"); + private static final Map RENDER_EXTENSIONS = Map.of( + "html", "html", + "screenshot", "png", + "svg", "svg", + "pdf", "pdf"); + private static final int DEFAULT_TIMEOUT_SECONDS = 90; + private static final int MAX_TIMEOUT_SECONDS = 300; + private static final int MAX_OUTPUT_BYTES = 50_000; + private static final long MAX_INPUT_BYTES = 50L * 1024 * 1024; + private static final long MAX_ARTIFACT_BYTES = 20L * 1024 * 1024; + + private final GeneratedFileCache generatedFileCache; + private final ObjectMapper objectMapper; + private final String executable; + + @Autowired + public OfficeCliTool(GeneratedFileCache generatedFileCache, ObjectMapper objectMapper) { + this(generatedFileCache, objectMapper, "officecli"); + } + + /** Test seam for a fake executable; production deliberately resolves from PATH. */ + OfficeCliTool(GeneratedFileCache generatedFileCache, + ObjectMapper objectMapper, + String executable) { + this.generatedFileCache = generatedFileCache; + this.objectMapper = objectMapper; + this.executable = executable == null || executable.isBlank() ? "officecli" : executable.trim(); + } + + @ConcurrencyUnsafe("OfficeCLI starts native processes and may use document-level locks") + @Tool(description = """ + Use the optional iOfficeAI/OfficeCLI engine to inspect, validate, batch-edit, + merge, or render an existing .docx, .xlsx, or .pptx file. This complements + the built-in Markdown renderers: use those for simple new files, and use this + tool for existing templates, complex structure, validation, or visual QA. + + Actions: + - inspect: read structure/content. mode is outline|stats|issues|text|annotated. + - validate: run OpenXML validation. + - batch: apply an OfficeCLI batch JSON array to a COPY of the input. payload is required. + - merge: replace template placeholders using a JSON object. payload is required. + - render: render to html|screenshot|svg|pdf. mode is required. + + Mutating actions NEVER overwrite the source. The result is returned as a + generated-file download link. OfficeCLI must be installed on the MateClaw + server; missing installations return a setup error. + """) + public String office_document( + @ToolParam(description = "inspect | validate | batch | merge | render") String action, + @ToolParam(description = "Workspace path or uploaded attachment name for a .docx/.xlsx/.pptx file") String filePath, + @ToolParam(description = "Inspect/render mode; omitted for validate/batch/merge", required = false) String mode, + @ToolParam(description = "JSON array for batch or JSON object for merge", required = false) String payload, + @ToolParam(description = "Optional result filename; extension is normalized", required = false) String outputFilename, + @ToolParam(description = "Timeout in seconds, default 90, maximum 300", required = false) Integer timeoutSeconds, + @Nullable ToolContext ctx) { + + String normalizedAction = normalize(action); + if (!Set.of("inspect", "validate", "batch", "merge", "render").contains(normalizedAction)) { + return error("Unsupported action: " + action); + } + + Path source; + try { + source = resolveInput(filePath, ctx); + } catch (Exception e) { + return error(e.getMessage()); + } + + String inputExtension = extension(source.getFileName().toString()); + if (!OFFICE_EXTENSIONS.contains(inputExtension)) { + return error("OfficeCLI supports .docx, .xlsx, and .pptx inputs only"); + } + if (!Files.isRegularFile(source)) { + return error("Office input not found or not a regular file: " + filePath); + } + try { + if (Files.size(source) > MAX_INPUT_BYTES) { + return error("Office input exceeds the 50 MB limit"); + } + } catch (IOException e) { + return error("Cannot inspect Office input size: " + e.getMessage()); + } + + int timeout = timeoutSeconds == null || timeoutSeconds <= 0 + ? DEFAULT_TIMEOUT_SECONDS + : Math.min(timeoutSeconds, MAX_TIMEOUT_SECONDS); + + Path scratch = null; + try { + scratch = Files.createTempDirectory("mc_officecli_"); + Path scratchInput = scratch.resolve("input." + inputExtension); + Files.copy(source, scratchInput, StandardCopyOption.REPLACE_EXISTING); + + return switch (normalizedAction) { + case "inspect" -> inspect(scratchInput, mode, timeout); + case "validate" -> validate(scratchInput, timeout); + case "batch" -> batch(scratchInput, inputExtension, payload, outputFilename, timeout, ctx); + case "merge" -> merge(scratchInput, inputExtension, payload, outputFilename, timeout, ctx); + case "render" -> render(scratchInput, mode, outputFilename, timeout, ctx); + default -> error("Unsupported action: " + action); + }; + } catch (Exception e) { + log.warn("[OfficeCLI] action={} failed: {}", normalizedAction, e.getMessage()); + return error("OfficeCLI execution failed: " + e.getMessage()); + } finally { + deleteTreeQuietly(scratch); + } + } + + private String inspect(Path input, String mode, int timeout) throws IOException, InterruptedException { + String inspectMode = normalize(mode); + if (!INSPECT_MODES.contains(inspectMode)) { + return error("inspect mode must be one of: " + String.join(", ", INSPECT_MODES)); + } + ProcessResult result = run(input.getParent(), timeout, + List.of("view", input.toString(), inspectMode, "--json")); + return processOnlyResult("inspect", result); + } + + private String validate(Path input, int timeout) throws IOException, InterruptedException { + ProcessResult result = run(input.getParent(), timeout, + List.of("validate", input.toString(), "--json")); + return processOnlyResult("validate", result); + } + + private String batch(Path input, String extension, String payload, String outputFilename, + int timeout, @Nullable ToolContext ctx) throws IOException, InterruptedException { + JsonNode commands = parsePayload(payload, true); + if (commands == null) { + return error("batch payload must be a non-empty JSON array"); + } + String unsafeReason = validateBatchCommands(commands); + if (unsafeReason != null) { + return error(unsafeReason); + } + String displayName = outputName(outputFilename, "officecli-edited", extension); + Path output = input.getParent().resolve("result." + extension); + Files.copy(input, output, StandardCopyOption.REPLACE_EXISTING); + + ProcessResult result = run(input.getParent(), timeout, + List.of("batch", output.toString(), "--commands", commands.toString(), "--json")); + return generatedResult("batch", result, output, displayName, mimeFor(extension), ctx); + } + + private String merge(Path input, String extension, String payload, String outputFilename, + int timeout, @Nullable ToolContext ctx) throws IOException, InterruptedException { + JsonNode data = parsePayload(payload, false); + if (data == null) { + return error("merge payload must be a non-empty JSON object"); + } + String displayName = outputName(outputFilename, "officecli-merged", extension); + Path output = input.getParent().resolve("result." + extension); + + ProcessResult result = run(input.getParent(), timeout, + List.of("merge", input.toString(), output.toString(), "--data", data.toString(), "--json")); + return generatedResult("merge", result, output, displayName, mimeFor(extension), ctx); + } + + private String render(Path input, String mode, String outputFilename, + int timeout, @Nullable ToolContext ctx) throws IOException, InterruptedException { + String renderMode = normalize(mode); + String extension = RENDER_EXTENSIONS.get(renderMode); + if (extension == null) { + return error("render mode must be one of: html, screenshot, svg, pdf"); + } + String displayName = outputName(outputFilename, "officecli-preview", extension); + Path output = input.getParent().resolve("rendered." + extension); + + // html/svg are streamed to stdout by OfficeCLI; screenshot/pdf accept -o. + // Keep artifact bytes separate from the bounded diagnostic capture. + ProcessResult result = Set.of("html", "svg").contains(renderMode) + ? run(input.getParent(), timeout, + List.of("view", input.toString(), renderMode), output) + : run(input.getParent(), timeout, + List.of("view", input.toString(), renderMode, "-o", output.toString())); + return generatedResult("render", result, output, displayName, mimeFor(extension), ctx); + } + + private JsonNode parsePayload(String payload, boolean array) { + if (payload == null || payload.isBlank()) return null; + try { + JsonNode node = objectMapper.readTree(payload); + if (array ? node.isArray() && !node.isEmpty() : node.isObject() && !node.isEmpty()) { + return node; + } + } catch (Exception ignore) { + // A concise validation error is returned by the caller. + } + return null; + } + + private String generatedResult(String action, ProcessResult result, Path output, + String displayName, String mime, @Nullable ToolContext ctx) throws IOException { + if (result.exitCode() != 0 || result.timedOut()) { + return processOnlyResult(action, result); + } + if (!Files.isRegularFile(output) || Files.size(output) == 0) { + return error("OfficeCLI completed without producing the expected output file"); + } + if (Files.size(output) > MAX_ARTIFACT_BYTES) { + return error("OfficeCLI output exceeds the 20 MB delivery limit"); + } + String link = GeneratedFileLink.resultEn( + Files.readAllBytes(output), displayName, mime, generatedFileCache, "Office file", 1, ctx); + ObjectNode json = baseResult(action, result); + json.put("generatedFile", link); + return pretty(json); + } + + private String processOnlyResult(String action, ProcessResult result) { + return pretty(baseResult(action, result)); + } + + private ObjectNode baseResult(String action, ProcessResult result) { + ObjectNode json = objectMapper.createObjectNode(); + json.put("success", result.exitCode() == 0 && !result.timedOut()); + json.put("action", action); + json.put("exitCode", result.exitCode()); + json.put("stdout", result.stdout()); + json.put("stderr", result.stderr()); + json.put("timedOut", result.timedOut()); + if (result.setupMissing()) { + json.put("setupRequired", true); + json.put("message", "OfficeCLI is not installed on the MateClaw server PATH"); + } + return json; + } + + private ProcessResult run(Path workingDir, int timeoutSeconds, List args) + throws IOException, InterruptedException { + return run(workingDir, timeoutSeconds, args, null); + } + + private ProcessResult run(Path workingDir, int timeoutSeconds, List args, + @Nullable Path stdoutArtifact) + throws IOException, InterruptedException { + List command = new ArrayList<>(args.size() + 1); + command.add(executable); + command.addAll(args); + + Path stdoutFile = stdoutArtifact == null + ? Files.createTempFile(workingDir, "stdout-", ".log") + : stdoutArtifact; + Path stderrFile = Files.createTempFile(workingDir, "stderr-", ".log"); + Process process = null; + try { + ProcessBuilder pb = new ProcessBuilder(command); + pb.directory(workingDir.toFile()); + pb.redirectOutput(stdoutFile.toFile()); + pb.redirectError(stderrFile.toFile()); + pb.environment().put("OFFICECLI_SKIP_UPDATE", "1"); + pb.environment().put("OFFICECLI_NO_AUTO_RESIDENT", "1"); + pb.environment().keySet().removeIf(key -> { + String upper = key.toUpperCase(Locale.ROOT); + return upper.contains("KEY") || upper.contains("SECRET") || upper.contains("TOKEN") + || upper.contains("PASSWORD") || upper.contains("CREDENTIAL"); + }); + + try { + process = pb.start(); + } catch (IOException e) { + if (isMissingExecutable(e)) { + return new ProcessResult(-1, "", e.getMessage(), false, true); + } + throw e; + } + + boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS); + if (!finished) { + killProcessTree(process); + } + int exitCode = finished ? process.exitValue() : -1; + return new ProcessResult(exitCode, + stdoutArtifact == null ? readTruncated(stdoutFile) : "", + readTruncated(stderrFile), + !finished, + false); + } catch (InterruptedException e) { + if (process != null && process.isAlive()) killProcessTree(process); + Thread.currentThread().interrupt(); + throw e; + } finally { + if (stdoutArtifact == null) Files.deleteIfExists(stdoutFile); + Files.deleteIfExists(stderrFile); + } + } + + private Path resolveInput(String filePath, @Nullable ToolContext ctx) { + if (filePath == null || filePath.isBlank()) { + throw new IllegalArgumentException("filePath is required"); + } + try { + Path path = WorkspacePathGuard.validatePath(filePath, ctx); + if (Files.exists(path)) return path; + } catch (IllegalArgumentException boundary) { + Path attachment = ChatUploadResolver.resolve(filePath); + if (attachment != null) return attachment; + throw boundary; + } + Path attachment = ChatUploadResolver.resolve(filePath); + if (attachment != null) return attachment; + throw new IllegalArgumentException("Office input not found: " + filePath); + } + + private String outputName(String requested, String fallback, String extension) { + String base = FilenameSanitizer.sanitize(requested, fallback, "." + extension); + return base + "." + extension; + } + + /** + * Keep the first-draft batch surface structural. Raw XML and importer verbs + * can make OfficeCLI read arbitrary host files through relationship/media + * properties even though the document itself lives in scratch space. + */ + @Nullable + private String validateBatchCommands(JsonNode commands) { + Set allowed = Set.of("add", "set", "remove", "move", "swap", "validate"); + for (JsonNode command : commands) { + if (!command.isObject()) { + return "Each batch item must be a JSON object"; + } + String verb = normalize(command.path("command").asText(command.path("op").asText())); + if (!allowed.contains(verb)) { + return "Unsupported batch command in the safe adapter: " + verb; + } + String type = normalize(command.path("type").asText()); + if (Set.of("image", "picture", "video", "audio", "ole", "embeddedobject").contains(type)) { + return "External media/OLE batch operations are not supported by the safe adapter"; + } + String unsafeValue = findUnsafeHostPath(command); + if (unsafeValue != null) { + return "Batch payload contains a host filesystem reference that is not allowed: " + unsafeValue; + } + } + return null; + } + + @Nullable + private String findUnsafeHostPath(JsonNode node) { + if (node.isTextual()) { + String value = node.asText().trim(); + String lower = value.toLowerCase(Locale.ROOT); + if (lower.startsWith("file://") || lower.startsWith("~/") || lower.startsWith("~\\") + || value.matches("^[A-Za-z]:[\\\\/].*")) { + return value; + } + if (value.matches(".*(^|[/\\\\])\\.\\.([/\\\\]|$).*") + || value.matches("^/(etc|var|tmp|home|users|root|opt|proc|sys|dev)(/.*)?$")) { + return value; + } + return null; + } + if (node.isContainerNode()) { + for (JsonNode child : node) { + String unsafe = findUnsafeHostPath(child); + if (unsafe != null) return unsafe; + } + } + return null; + } + + private static String extension(String filename) { + int dot = filename.lastIndexOf('.'); + return dot < 0 ? "" : filename.substring(dot + 1).toLowerCase(Locale.ROOT); + } + + private static String normalize(String value) { + return value == null ? "" : value.trim().toLowerCase(Locale.ROOT); + } + + private static String mimeFor(String extension) { + return switch (extension) { + case "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + case "xlsx" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + case "pptx" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + case "html" -> "text/html"; + case "png" -> "image/png"; + case "svg" -> "image/svg+xml"; + case "pdf" -> "application/pdf"; + default -> "application/octet-stream"; + }; + } + + private static boolean isMissingExecutable(IOException e) { + String message = e.getMessage(); + return message != null && (message.contains("No such file") || message.contains("CreateProcess error=2")); + } + + private String readTruncated(Path path) throws IOException { + byte[] bytes = Files.readAllBytes(path); + if (bytes.length <= MAX_OUTPUT_BYTES) return new String(bytes, StandardCharsets.UTF_8); + return new String(bytes, 0, MAX_OUTPUT_BYTES, StandardCharsets.UTF_8) + + "\n... [output truncated]"; + } + + private static void killProcessTree(Process process) { + process.descendants().forEach(handle -> { + try { handle.destroyForcibly(); } catch (Exception ignore) { } + }); + process.destroyForcibly(); + try { + process.waitFor(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void deleteTreeQuietly(@Nullable Path root) { + if (root == null || !Files.exists(root)) return; + try (var paths = Files.walk(root)) { + paths.sorted(Comparator.reverseOrder()).forEach(path -> { + try { Files.deleteIfExists(path); } catch (IOException ignore) { } + }); + } catch (IOException e) { + log.debug("[OfficeCLI] failed to delete scratch directory {}: {}", root, e.getMessage()); + } + } + + private String error(String message) { + ObjectNode json = objectMapper.createObjectNode(); + json.put("success", false); + json.put("error", message == null ? "Unknown OfficeCLI error" : message); + return pretty(json); + } + + private String pretty(JsonNode json) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(json); + } catch (Exception e) { + return json.toString(); + } + } + + private record ProcessResult(int exitCode, String stdout, String stderr, + boolean timedOut, boolean setupMissing) { } +} diff --git a/mateclaw-server/src/main/resources/db/data-en.sql b/mateclaw-server/src/main/resources/db/data-en.sql index 4d717f2f..40096f7e 100644 --- a/mateclaw-server/src/main/resources/db/data-en.sql +++ b/mateclaw-server/src/main/resources/db/data-en.sql @@ -475,6 +475,10 @@ MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, KEY (id) VALUES (1000000028, 'ChannelMessageTool', 'Channel Message Push', 'Proactively push messages to IM channel conversations. list_channel_sessions discovers pushable conversations; send_channel_message performs a one-way push — for alerts, reminders, and async task results.', 'builtin', 'channelMessageTool', '📤', TRUE, TRUE, NOW(), NOW(), 0); +MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +KEY (id) +VALUES (1000000029, 'OfficeCliTool', 'OfficeCLI Advanced Documents', 'Inspect, validate, copy-edit, merge, and render DOCX/XLSX/PPTX through the optional iOfficeAI/OfficeCLI binary. Mutations are copy-on-write and return generated-file links.', 'builtin', 'officeCliTool', '🏢', TRUE, TRUE, NOW(), NOW(), 0); + -- Built-in tool: Edit File (enabled by default, dangerous ops controlled by ToolGuard) MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) KEY (id) @@ -707,7 +711,11 @@ MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, 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); --- RFC-042 §2.2 — bilingual display names for the 19 builtin skills. +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 (1000000020, 'officecli', 'Use optional iOfficeAI/OfficeCLI for advanced inspection, validation, copy editing, template merge, and visual rendering of existing DOCX/XLSX/PPTX files.', 'builtin', '🏢', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'office,officecli,docx,xlsx,pptx,render,validate', NOW(), NOW(), 0); + +-- RFC-042 §2.2 — bilingual display names for the 20 builtin skills. -- Identical across all four data-*.sql files because name_zh / name_en are -- permanent attributes, not locale-conditional. The UI picks which one to -- show based on the active i18n locale and falls back to `name` when null. @@ -718,6 +726,7 @@ UPDATE mate_skill SET name_zh = '邮件管理', name_en = 'Email (Himalaya UPDATE mate_skill SET name_zh = '新闻查询', name_en = 'News' WHERE name = 'news'; UPDATE mate_skill SET name_zh = 'PDF 处理', name_en = 'PDF' WHERE name = 'pdf'; UPDATE mate_skill SET name_zh = 'Word 文档', name_en = 'Word Document' WHERE name = 'docx'; +UPDATE mate_skill SET name_zh = 'OfficeCLI 高级文档', name_en = 'OfficeCLI Advanced Documents' WHERE name = 'officecli'; UPDATE mate_skill SET name_zh = 'PPT 演示', name_en = 'PowerPoint' WHERE name = 'pptx'; UPDATE mate_skill SET name_zh = 'Excel 表格', name_en = 'Excel' WHERE name = 'xlsx'; UPDATE mate_skill SET name_zh = '可见浏览器', name_en = 'Visible Browser' WHERE name = 'browser_visible'; diff --git a/mateclaw-server/src/main/resources/db/data-kingbase-en.sql b/mateclaw-server/src/main/resources/db/data-kingbase-en.sql index 999c56af..4c7b2993 100644 --- a/mateclaw-server/src/main/resources/db/data-kingbase-en.sql +++ b/mateclaw-server/src/main/resources/db/data-kingbase-en.sql @@ -519,6 +519,10 @@ INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name VALUES (1000000028, 'ChannelMessageTool', 'Channel Message Push', 'Proactively push messages to IM channel conversations. list_channel_sessions discovers pushable conversations; send_channel_message performs a one-way push — for alerts, reminders, and async task results.', 'builtin', 'channelMessageTool', '📤', TRUE, TRUE, NOW(), NOW(), 0) ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted; +INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +VALUES (1000000029, 'OfficeCliTool', 'OfficeCLI Advanced Documents', 'Inspect, validate, copy-edit, merge, and render DOCX/XLSX/PPTX through the optional iOfficeAI/OfficeCLI binary. Mutations are copy-on-write and return generated-file links.', 'builtin', 'officeCliTool', '🏢', TRUE, TRUE, NOW(), NOW(), 0) +ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted; + -- Built-in tool: Edit File (enabled by default, dangerous ops controlled by ToolGuard) INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) VALUES (1000000006, 'EditFileTool', 'Edit File', 'Edit file content via find-and-replace. Matches old_text exactly and replaces with new_text. Requires user approval.', 'builtin', 'editFileTool', '✏️', TRUE, TRUE, NOW(), NOW(), 0) @@ -699,7 +703,11 @@ INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author 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 CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, description=EXCLUDED.description, skill_type=EXCLUDED.skill_type, icon=EXCLUDED.icon, version=EXCLUDED.version, author=EXCLUDED.author, config_json=EXCLUDED.config_json, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, tags=EXCLUDED.tags, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted; --- RFC-042 §2.2 — bilingual display names for the 19 builtin skills. +INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted) +VALUES (1000000020, 'officecli', 'Use optional iOfficeAI/OfficeCLI for advanced inspection, validation, copy editing, template merge, and visual rendering of existing DOCX/XLSX/PPTX files.', 'builtin', '🏢', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'office,officecli,docx,xlsx,pptx,render,validate', NOW(), NOW(), 0) +ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, description=EXCLUDED.description, skill_type=EXCLUDED.skill_type, icon=EXCLUDED.icon, version=EXCLUDED.version, author=EXCLUDED.author, config_json=EXCLUDED.config_json, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, tags=EXCLUDED.tags, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted; + +-- RFC-042 §2.2 — bilingual display names for the 20 builtin skills. -- Identical across all four data-*.sql files because name_zh / name_en are -- permanent attributes, not locale-conditional. The UI picks which one to -- show based on the active i18n locale and falls back to name when null. @@ -710,6 +718,7 @@ UPDATE mate_skill SET name_zh = '邮件管理', name_en = 'Email (Himalaya UPDATE mate_skill SET name_zh = '新闻查询', name_en = 'News' WHERE name = 'news'; UPDATE mate_skill SET name_zh = 'PDF 处理', name_en = 'PDF' WHERE name = 'pdf'; UPDATE mate_skill SET name_zh = 'Word 文档', name_en = 'Word Document' WHERE name = 'docx'; +UPDATE mate_skill SET name_zh = 'OfficeCLI 高级文档', name_en = 'OfficeCLI Advanced Documents' WHERE name = 'officecli'; UPDATE mate_skill SET name_zh = 'PPT 演示', name_en = 'PowerPoint' WHERE name = 'pptx'; UPDATE mate_skill SET name_zh = 'Excel 表格', name_en = 'Excel' WHERE name = 'xlsx'; UPDATE mate_skill SET name_zh = '可见浏览器', name_en = 'Visible Browser' WHERE name = 'browser_visible'; diff --git a/mateclaw-server/src/main/resources/db/data-kingbase-zh.sql b/mateclaw-server/src/main/resources/db/data-kingbase-zh.sql index d173527d..e90c8f8d 100644 --- a/mateclaw-server/src/main/resources/db/data-kingbase-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-kingbase-zh.sql @@ -514,6 +514,10 @@ INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name VALUES (1000000028, 'ChannelMessageTool', '渠道消息推送', '主动向 IM 渠道会话推送消息。list_channel_sessions 查询可推送的会话,send_channel_message 单向推送消息,用于告警通知、定时任务结果回推等场景。', 'builtin', 'channelMessageTool', '📤', TRUE, TRUE, NOW(), NOW(), 0) ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted; +INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +VALUES (1000000029, 'OfficeCliTool', 'OfficeCLI 高级文档', '通过可选的 iOfficeAI/OfficeCLI 二进制检查、校验、复制编辑、模板合并和渲染 DOCX/XLSX/PPTX;所有修改均为副本写入并返回生成文件链接。', 'builtin', 'officeCliTool', '🏢', TRUE, TRUE, NOW(), NOW(), 0) +ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted; + -- 内置工具:编辑文件(默认启用,危险操作由 ToolGuard 审批控制) INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) VALUES (1000000006, 'EditFileTool', '编辑文件', '通过查找替换编辑文件内容,精确匹配 old_text 并替换为 new_text。每次执行需要用户审批确认。', 'builtin', 'editFileTool', '✏️', TRUE, TRUE, NOW(), NOW(), 0) @@ -696,7 +700,11 @@ INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author 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 CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, description=EXCLUDED.description, skill_type=EXCLUDED.skill_type, icon=EXCLUDED.icon, version=EXCLUDED.version, author=EXCLUDED.author, config_json=EXCLUDED.config_json, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, tags=EXCLUDED.tags, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted; --- RFC-042 §2.2 — bilingual display names for the 19 builtin skills. +INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted) +VALUES (1000000020, 'officecli', '使用可选的 iOfficeAI/OfficeCLI 对已有 DOCX/XLSX/PPTX 进行检查、校验、复制编辑、模板合并和视觉渲染。', 'builtin', '🏢', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'office,officecli,docx,xlsx,pptx,render,validate', NOW(), NOW(), 0) +ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, description=EXCLUDED.description, skill_type=EXCLUDED.skill_type, icon=EXCLUDED.icon, version=EXCLUDED.version, author=EXCLUDED.author, config_json=EXCLUDED.config_json, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, tags=EXCLUDED.tags, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted; + +-- RFC-042 §2.2 — bilingual display names for the 20 builtin skills. -- Identical across all four data-*.sql files because name_zh / name_en are -- permanent attributes, not locale-conditional. The UI picks which one to -- show based on the active i18n locale and falls back to name when null. @@ -707,6 +715,7 @@ UPDATE mate_skill SET name_zh = '邮件管理', name_en = 'Email (Himalaya UPDATE mate_skill SET name_zh = '新闻查询', name_en = 'News' WHERE name = 'news'; UPDATE mate_skill SET name_zh = 'PDF 处理', name_en = 'PDF' WHERE name = 'pdf'; UPDATE mate_skill SET name_zh = 'Word 文档', name_en = 'Word Document' WHERE name = 'docx'; +UPDATE mate_skill SET name_zh = 'OfficeCLI 高级文档', name_en = 'OfficeCLI Advanced Documents' WHERE name = 'officecli'; UPDATE mate_skill SET name_zh = 'PPT 演示', name_en = 'PowerPoint' WHERE name = 'pptx'; UPDATE mate_skill SET name_zh = 'Excel 表格', name_en = 'Excel' WHERE name = 'xlsx'; UPDATE mate_skill SET name_zh = '可见浏览器', name_en = 'Visible Browser' WHERE name = 'browser_visible'; diff --git a/mateclaw-server/src/main/resources/db/data-mysql-en.sql b/mateclaw-server/src/main/resources/db/data-mysql-en.sql index 15954fad..a751e3dd 100644 --- a/mateclaw-server/src/main/resources/db/data-mysql-en.sql +++ b/mateclaw-server/src/main/resources/db/data-mysql-en.sql @@ -528,6 +528,10 @@ INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name VALUES (1000000028, 'ChannelMessageTool', 'Channel Message Push', 'Proactively push messages to IM channel conversations. list_channel_sessions discovers pushable conversations; send_channel_message performs a one-way push — for alerts, reminders, and async task results.', 'builtin', 'channelMessageTool', '📤', 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); +INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +VALUES (1000000029, 'OfficeCliTool', 'OfficeCLI Advanced Documents', 'Inspect, validate, copy-edit, merge, and render DOCX/XLSX/PPTX through the optional iOfficeAI/OfficeCLI binary. Mutations are copy-on-write and return generated-file links.', 'builtin', 'officeCliTool', '🏢', 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: Edit File (enabled by default, dangerous ops controlled by ToolGuard) INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) VALUES (1000000006, 'EditFileTool', 'Edit File', 'Edit file content via find-and-replace. Matches old_text exactly and replaces with new_text. Requires user approval.', 'builtin', 'editFileTool', '✏️', TRUE, TRUE, NOW(), NOW(), 0) @@ -757,7 +761,11 @@ INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author 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); --- RFC-042 §2.2 — bilingual display names for the 19 builtin skills. +INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted) +VALUES (1000000020, 'officecli', 'Use optional iOfficeAI/OfficeCLI for advanced inspection, validation, copy editing, template merge, and visual rendering of existing DOCX/XLSX/PPTX files.', 'builtin', '🏢', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'office,officecli,docx,xlsx,pptx,render,validate', 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); + +-- RFC-042 §2.2 — bilingual display names for the 20 builtin skills. -- Identical across all four data-*.sql files because name_zh / name_en are -- permanent attributes, not locale-conditional. The UI picks which one to -- show based on the active i18n locale and falls back to `name` when null. @@ -768,6 +776,7 @@ UPDATE mate_skill SET name_zh = '邮件管理', name_en = 'Email (Himalaya UPDATE mate_skill SET name_zh = '新闻查询', name_en = 'News' WHERE name = 'news'; UPDATE mate_skill SET name_zh = 'PDF 处理', name_en = 'PDF' WHERE name = 'pdf'; UPDATE mate_skill SET name_zh = 'Word 文档', name_en = 'Word Document' WHERE name = 'docx'; +UPDATE mate_skill SET name_zh = 'OfficeCLI 高级文档', name_en = 'OfficeCLI Advanced Documents' WHERE name = 'officecli'; UPDATE mate_skill SET name_zh = 'PPT 演示', name_en = 'PowerPoint' WHERE name = 'pptx'; UPDATE mate_skill SET name_zh = 'Excel 表格', name_en = 'Excel' WHERE name = 'xlsx'; UPDATE mate_skill SET name_zh = '可见浏览器', name_en = 'Visible Browser' WHERE name = 'browser_visible'; diff --git a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql index b45ae676..22987e54 100644 --- a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql @@ -523,6 +523,10 @@ INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name VALUES (1000000028, 'ChannelMessageTool', '渠道消息推送', '主动向 IM 渠道会话推送消息。list_channel_sessions 查询可推送的会话,send_channel_message 单向推送消息,用于告警通知、定时任务结果回推等场景。', 'builtin', 'channelMessageTool', '📤', 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); +INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +VALUES (1000000029, 'OfficeCliTool', 'OfficeCLI 高级文档', '通过可选的 iOfficeAI/OfficeCLI 二进制检查、校验、复制编辑、模板合并和渲染 DOCX/XLSX/PPTX;所有修改均为副本写入并返回生成文件链接。', 'builtin', 'officeCliTool', '🏢', 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); + -- 内置工具:编辑文件(默认启用,危险操作由 ToolGuard 审批控制) INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) VALUES (1000000006, 'EditFileTool', '编辑文件', '通过查找替换编辑文件内容,精确匹配 old_text 并替换为 new_text。每次执行需要用户审批确认。', 'builtin', 'editFileTool', '✏️', TRUE, TRUE, NOW(), NOW(), 0) @@ -754,7 +758,11 @@ INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author 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); --- RFC-042 §2.2 — bilingual display names for the 19 builtin skills. +INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted) +VALUES (1000000020, 'officecli', '使用可选的 iOfficeAI/OfficeCLI 对已有 DOCX/XLSX/PPTX 进行检查、校验、复制编辑、模板合并和视觉渲染。', 'builtin', '🏢', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'office,officecli,docx,xlsx,pptx,render,validate', 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); + +-- RFC-042 §2.2 — bilingual display names for the 20 builtin skills. -- Identical across all four data-*.sql files because name_zh / name_en are -- permanent attributes, not locale-conditional. The UI picks which one to -- show based on the active i18n locale and falls back to `name` when null. @@ -765,6 +773,7 @@ UPDATE mate_skill SET name_zh = '邮件管理', name_en = 'Email (Himalaya UPDATE mate_skill SET name_zh = '新闻查询', name_en = 'News' WHERE name = 'news'; UPDATE mate_skill SET name_zh = 'PDF 处理', name_en = 'PDF' WHERE name = 'pdf'; UPDATE mate_skill SET name_zh = 'Word 文档', name_en = 'Word Document' WHERE name = 'docx'; +UPDATE mate_skill SET name_zh = 'OfficeCLI 高级文档', name_en = 'OfficeCLI Advanced Documents' WHERE name = 'officecli'; UPDATE mate_skill SET name_zh = 'PPT 演示', name_en = 'PowerPoint' WHERE name = 'pptx'; UPDATE mate_skill SET name_zh = 'Excel 表格', name_en = 'Excel' WHERE name = 'xlsx'; UPDATE mate_skill SET name_zh = '可见浏览器', name_en = 'Visible Browser' WHERE name = 'browser_visible'; diff --git a/mateclaw-server/src/main/resources/db/data-zh.sql b/mateclaw-server/src/main/resources/db/data-zh.sql index 9ed92ccb..f527ecf6 100644 --- a/mateclaw-server/src/main/resources/db/data-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-zh.sql @@ -476,6 +476,10 @@ MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, KEY (id) VALUES (1000000028, 'ChannelMessageTool', '渠道消息推送', '主动向 IM 渠道会话推送消息。list_channel_sessions 查询可推送的会话,send_channel_message 单向推送消息,用于告警通知、定时任务结果回推等场景。', 'builtin', 'channelMessageTool', '📤', TRUE, TRUE, NOW(), NOW(), 0); +MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +KEY (id) +VALUES (1000000029, 'OfficeCliTool', 'OfficeCLI 高级文档', '通过可选的 iOfficeAI/OfficeCLI 二进制检查、校验、复制编辑、模板合并和渲染 DOCX/XLSX/PPTX;所有修改均为副本写入并返回生成文件链接。', 'builtin', 'officeCliTool', '🏢', TRUE, TRUE, NOW(), NOW(), 0); + -- 内置工具:编辑文件(默认启用,危险操作由 ToolGuard 审批控制) MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) KEY (id) @@ -708,7 +712,11 @@ MERGE INTO mate_skill (id, name, description, skill_type, icon, version, author, 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); --- RFC-042 §2.2 — bilingual display names for the 19 builtin skills. +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 (1000000020, 'officecli', '使用可选的 iOfficeAI/OfficeCLI 对已有 DOCX/XLSX/PPTX 进行检查、校验、复制编辑、模板合并和视觉渲染。', 'builtin', '🏢', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'office,officecli,docx,xlsx,pptx,render,validate', NOW(), NOW(), 0); + +-- RFC-042 §2.2 — bilingual display names for the 20 builtin skills. -- Identical across all four data-*.sql files because name_zh / name_en are -- permanent attributes, not locale-conditional. The UI picks which one to -- show based on the active i18n locale and falls back to `name` when null. @@ -719,6 +727,7 @@ UPDATE mate_skill SET name_zh = '邮件管理', name_en = 'Email (Himalaya UPDATE mate_skill SET name_zh = '新闻查询', name_en = 'News' WHERE name = 'news'; UPDATE mate_skill SET name_zh = 'PDF 处理', name_en = 'PDF' WHERE name = 'pdf'; UPDATE mate_skill SET name_zh = 'Word 文档', name_en = 'Word Document' WHERE name = 'docx'; +UPDATE mate_skill SET name_zh = 'OfficeCLI 高级文档', name_en = 'OfficeCLI Advanced Documents' WHERE name = 'officecli'; UPDATE mate_skill SET name_zh = 'PPT 演示', name_en = 'PowerPoint' WHERE name = 'pptx'; UPDATE mate_skill SET name_zh = 'Excel 表格', name_en = 'Excel' WHERE name = 'xlsx'; UPDATE mate_skill SET name_zh = '可见浏览器', name_en = 'Visible Browser' WHERE name = 'browser_visible'; diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V185__register_officecli_support.sql b/mateclaw-server/src/main/resources/db/migration/h2/V185__register_officecli_support.sql new file mode 100644 index 00000000..9a7a43c1 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V185__register_officecli_support.sql @@ -0,0 +1,10 @@ +-- Issue #583 / RFC-048: optional iOfficeAI/OfficeCLI advanced Office engine. +MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +KEY (id) +VALUES (1000000029, 'OfficeCliTool', 'OfficeCLI Advanced Documents', 'Inspect, validate, copy-edit, merge, and render DOCX/XLSX/PPTX through the optional iOfficeAI/OfficeCLI binary. Mutations are copy-on-write and return generated-file links.', 'builtin', 'officeCliTool', '🏢', TRUE, TRUE, 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 (1000000020, 'officecli', 'Use optional iOfficeAI/OfficeCLI for advanced inspection, validation, copy editing, template merge, and visual rendering of existing DOCX/XLSX/PPTX files.', 'builtin', '🏢', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'office,officecli,docx,xlsx,pptx,render,validate', NOW(), NOW(), 0); + +UPDATE mate_skill SET name_zh = 'OfficeCLI 高级文档', name_en = 'OfficeCLI Advanced Documents' WHERE name = 'officecli'; diff --git a/mateclaw-server/src/main/resources/db/migration/kingbase/V185__register_officecli_support.sql b/mateclaw-server/src/main/resources/db/migration/kingbase/V185__register_officecli_support.sql new file mode 100644 index 00000000..1bf6e837 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/kingbase/V185__register_officecli_support.sql @@ -0,0 +1,10 @@ +-- Issue #583 / RFC-048: optional iOfficeAI/OfficeCLI advanced Office engine. +INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +VALUES (1000000029, 'OfficeCliTool', 'OfficeCLI Advanced Documents', 'Inspect, validate, copy-edit, merge, and render DOCX/XLSX/PPTX through the optional iOfficeAI/OfficeCLI binary. Mutations are copy-on-write and return generated-file links.', 'builtin', 'officeCliTool', '🏢', TRUE, TRUE, NOW(), NOW(), 0) +ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, display_name=EXCLUDED.display_name, description=EXCLUDED.description, tool_type=EXCLUDED.tool_type, bean_name=EXCLUDED.bean_name, icon=EXCLUDED.icon, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted; + +INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted) +VALUES (1000000020, 'officecli', 'Use optional iOfficeAI/OfficeCLI for advanced inspection, validation, copy editing, template merge, and visual rendering of existing DOCX/XLSX/PPTX files.', 'builtin', '🏢', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'office,officecli,docx,xlsx,pptx,render,validate', NOW(), NOW(), 0) +ON CONFLICT (id) DO UPDATE SET description=EXCLUDED.description, skill_type=EXCLUDED.skill_type, icon=EXCLUDED.icon, version=EXCLUDED.version, author=EXCLUDED.author, config_json=EXCLUDED.config_json, enabled=EXCLUDED.enabled, builtin=EXCLUDED.builtin, tags=EXCLUDED.tags, update_time=EXCLUDED.update_time, deleted=EXCLUDED.deleted; + +UPDATE mate_skill SET name_zh = 'OfficeCLI 高级文档', name_en = 'OfficeCLI Advanced Documents' WHERE name = 'officecli'; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V185__register_officecli_support.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V185__register_officecli_support.sql new file mode 100644 index 00000000..256c4e2c --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V185__register_officecli_support.sql @@ -0,0 +1,10 @@ +-- Issue #583 / RFC-048: optional iOfficeAI/OfficeCLI advanced Office engine. +INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +VALUES (1000000029, 'OfficeCliTool', 'OfficeCLI Advanced Documents', 'Inspect, validate, copy-edit, merge, and render DOCX/XLSX/PPTX through the optional iOfficeAI/OfficeCLI binary. Mutations are copy-on-write and return generated-file links.', 'builtin', 'officeCliTool', '🏢', 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); + +INSERT INTO mate_skill (id, name, description, skill_type, icon, version, author, config_json, enabled, builtin, tags, create_time, update_time, deleted) +VALUES (1000000020, 'officecli', 'Use optional iOfficeAI/OfficeCLI for advanced inspection, validation, copy editing, template merge, and visual rendering of existing DOCX/XLSX/PPTX files.', 'builtin', '🏢', '1.0.0', 'MateClaw', '{"upstream":"mateclaw","entryFile":"SKILL.md"}', TRUE, TRUE, 'office,officecli,docx,xlsx,pptx,render,validate', 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); + +UPDATE mate_skill SET name_zh = 'OfficeCLI 高级文档', name_en = 'OfficeCLI Advanced Documents' WHERE name = 'officecli'; diff --git a/mateclaw-server/src/main/resources/skills/officecli/SKILL.md b/mateclaw-server/src/main/resources/skills/officecli/SKILL.md new file mode 100644 index 00000000..e8c0e108 --- /dev/null +++ b/mateclaw-server/src/main/resources/skills/officecli/SKILL.md @@ -0,0 +1,96 @@ +--- +name: officecli +version: "1.0.0" +description: "Use the optional iOfficeAI/OfficeCLI engine for advanced inspection, validation, copy-on-write editing, template merge, or visual rendering of existing .docx, .xlsx, and .pptx files. Prefer MateClaw's built-in renderDocx/renderXlsx/renderPptx tools for simple new documents. Use this skill when preserving an existing template, modifying complex Office structure, checking formatting issues, validating OpenXML, or rendering a document for visual QA. This integration targets https://github.com/iOfficeAI/OfficeCLI, not the unrelated prompt-generation project with the same name." +requires: + - key: officecli + type: binary + check: officecli + description: "iOfficeAI/OfficeCLI executable on the MateClaw server" + install: + macos: "brew install officecli" + linux: "Install a pinned iOfficeAI/OfficeCLI release and verify its SHA256" + windows: "scoop install officecli" +dependencies: + tools: + - office_document +platforms: + - macos + - linux + - windows +--- + +# OfficeCLI advanced Office operations + +This skill supplements MateClaw's native Office renderers. It never replaces them. + +## Routing + +| User intent | Use | +|---|---| +| Create a simple new document from Markdown | `renderDocx`, `renderXlsx`, or `renderPptx` | +| Inspect an existing Office file | `office_document(action="inspect")` | +| Validate OpenXML structure | `office_document(action="validate")` | +| Apply several structured changes | `office_document(action="batch")` | +| Fill an existing template's placeholders | `office_document(action="merge")` | +| Render for visual QA | `office_document(action="render")` | + +## Safety contract + +- `batch` and `merge` operate on a private copy and never overwrite the source. +- The tool only accepts `.docx`, `.xlsx`, and `.pptx` inputs inside the active workspace or current chat uploads. +- Do not install OfficeCLI from inside a chat. If the dependency is missing, explain that an administrator must install it on the MateClaw server. +- Do not fall back to arbitrary shell commands when `office_document` rejects an action. +- Return the generated markdown link verbatim so the user can download and preview the result. + +## Operations + +### Inspect + +Use one of `outline`, `stats`, `issues`, `text`, or `annotated`: + +```text +office_document(action="inspect", filePath="report.docx", mode="issues") +``` + +### Validate + +```text +office_document(action="validate", filePath="workbook.xlsx") +``` + +### Batch edit + +`payload` must be a non-empty OfficeCLI batch JSON array. Prefer stable element IDs returned by inspection over positional paths when available. + +```text +office_document( + action="batch", + filePath="deck.pptx", + payload="[{\"command\":\"set\",\"path\":\"/slide[1]/shape[@id=42]\",\"props\":{\"text\":\"Updated\"}}]", + outputFilename="deck-updated.pptx" +) +``` + +### Template merge + +`payload` must be a non-empty JSON object: + +```text +office_document( + action="merge", + filePath="invoice-template.docx", + payload="{\"client\":\"Acme\",\"total\":\"$5,200\"}", + outputFilename="invoice-acme.docx" +) +``` + +### Render + +Use `html`, `screenshot`, `svg`, or `pdf`. Prefer `screenshot` for visual QA. + +```text +office_document(action="render", filePath="deck.pptx", mode="screenshot") +``` + +After rendering, inspect the returned preview before claiming that layout or formatting is correct. diff --git a/mateclaw-server/src/test/java/vip/mate/tool/builtin/OfficeCliToolTest.java b/mateclaw-server/src/test/java/vip/mate/tool/builtin/OfficeCliToolTest.java new file mode 100644 index 00000000..f7efdb47 --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/tool/builtin/OfficeCliToolTest.java @@ -0,0 +1,241 @@ +package vip.mate.tool.builtin; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import vip.mate.tool.document.GeneratedFileCache; +import vip.mate.tool.guard.WorkspacePathGuard; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Regression coverage for the optional OfficeCLI adapter introduced by issue #583. */ +class OfficeCliToolTest { + + private static final Pattern GENERATED_ID = Pattern.compile( + "/api/v1/files/generated/([a-zA-Z0-9-]+)"); + + @TempDir + Path tempDir; + + private final ObjectMapper mapper = new ObjectMapper(); + private GeneratedFileCache cache; + private Path fakeCli; + + @BeforeEach + void setUp() throws Exception { + WorkspacePathGuard.setDefaultRoot(tempDir.toString()); + cache = new GeneratedFileCache(tempDir.resolve("cache")); + fakeCli = tempDir.resolve("officecli-fake"); + Files.writeString(fakeCli, """ + #!/bin/sh + command="$1" + shift + case "$command" in + batch) + file="$1" + printf '\nEDITED-BY-OFFICECLI' >> "$file" + printf '{"ok":true,"command":"batch"}\n' + ;; + merge) + input="$1" + output="$2" + cp "$input" "$output" + printf '\nMERGED-BY-OFFICECLI' >> "$output" + printf '{"ok":true,"command":"merge"}\n' + ;; + validate) + printf '{"valid":true}\n' + ;; + view) + input="$1" + mode="$2" + shift 2 + output="" + while [ "$#" -gt 0 ]; do + if [ "$1" = "-o" ]; then output="$2"; shift 2; else shift; fi + done + if [ -n "$output" ]; then + printf 'rendered:%s' "$mode" > "$output" + elif [ "$mode" = "html" ] || [ "$mode" = "svg" ]; then + printf 'rendered:%s' "$mode" + else + printf '{"mode":"%s","source":"%s"}\n' "$mode" "$input" + fi + ;; + *) + printf 'unsupported fake command\n' >&2 + exit 2 + ;; + esac + """, StandardCharsets.UTF_8); + assertTrue(fakeCli.toFile().setExecutable(true)); + } + + @AfterEach + void tearDown() { + WorkspacePathGuard.setDefaultRoot(null); + } + + @Test + @DisplayName("batch edits a scratch copy, preserves the source, and returns a cached download") + void batchIsCopyOnWriteAndDownloadable() throws Exception { + byte[] original = "PK-original-docx".getBytes(StandardCharsets.UTF_8); + Path source = Files.write(tempDir.resolve("source.docx"), original); + OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString()); + + String result = tool.office_document( + "batch", source.toString(), null, + "[{\"command\":\"set\",\"path\":\"/body/p[1]\",\"props\":{\"text\":\"new\"}}]", + "客户/报告.docx", 10, null); + + JsonNode json = mapper.readTree(result); + assertTrue(json.path("success").asBoolean(), result); + assertArrayEquals(original, Files.readAllBytes(source), "source document must never be mutated"); + + GeneratedFileCache.Entry entry = cachedEntry(json.path("generatedFile").asText()); + assertEquals("客户_报告.docx", entry.filename()); + assertTrue(new String(entry.bytes(), StandardCharsets.UTF_8).contains("EDITED-BY-OFFICECLI")); + } + + @Test + @DisplayName("render returns the requested preview artifact through GeneratedFileCache") + void renderProducesDownload() throws Exception { + Path source = Files.writeString(tempDir.resolve("slides.pptx"), "PK-pptx"); + OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString()); + + String result = tool.office_document( + "render", source.toString(), "screenshot", null, + "预览.png", 10, null); + + JsonNode json = mapper.readTree(result); + assertTrue(json.path("success").asBoolean(), result); + GeneratedFileCache.Entry entry = cachedEntry(json.path("generatedFile").asText()); + assertEquals("预览.png", entry.filename()); + assertEquals("image/png", entry.mimeType()); + assertEquals("rendered:screenshot", new String(entry.bytes(), StandardCharsets.UTF_8)); + } + + @Test + @DisplayName("stdout render modes are captured as artifacts instead of diagnostics") + void stdoutRenderProducesDownload() throws Exception { + Path source = Files.writeString(tempDir.resolve("slides.pptx"), "PK-pptx"); + OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString()); + + JsonNode result = mapper.readTree(tool.office_document( + "render", source.toString(), "svg", null, null, 10, null)); + + assertTrue(result.path("success").asBoolean(), result.toString()); + GeneratedFileCache.Entry entry = cachedEntry(result.path("generatedFile").asText()); + assertEquals("image/svg+xml", entry.mimeType()); + assertEquals("rendered:svg", new String(entry.bytes(), StandardCharsets.UTF_8)); + assertEquals("", result.path("stdout").asText()); + } + + @Test + @DisplayName("read-only validation returns structured stdout without generating a file") + void validateIsReadOnly() throws Exception { + Path source = Files.writeString(tempDir.resolve("book.xlsx"), "PK-xlsx"); + OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString()); + + JsonNode result = mapper.readTree(tool.office_document( + "validate", source.toString(), null, null, null, 10, null)); + + assertTrue(result.path("success").asBoolean()); + assertTrue(result.path("stdout").asText().contains("\"valid\":true")); + assertFalse(result.has("generatedFile")); + } + + @Test + @DisplayName("invalid payloads and unsupported formats fail before a subprocess mutates anything") + void rejectsInvalidRequests() throws Exception { + Path office = Files.writeString(tempDir.resolve("a.docx"), "PK-docx"); + Path text = Files.writeString(tempDir.resolve("a.txt"), "text"); + OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString()); + + JsonNode badPayload = mapper.readTree(tool.office_document( + "batch", office.toString(), null, "{}", null, 10, null)); + JsonNode badFormat = mapper.readTree(tool.office_document( + "validate", text.toString(), null, null, null, 10, null)); + + assertFalse(badPayload.path("success").asBoolean()); + assertTrue(badPayload.path("error").asText().contains("JSON array")); + assertFalse(badFormat.path("success").asBoolean()); + assertTrue(badFormat.path("error").asText().contains(".docx")); + } + + @Test + @DisplayName("batch rejects raw XML, external media, and host filesystem references") + void rejectsUnsafeBatchSurface() throws Exception { + Path office = Files.writeString(tempDir.resolve("a.docx"), "PK-docx"); + OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString()); + + JsonNode raw = mapper.readTree(tool.office_document( + "batch", office.toString(), null, + "[{\"command\":\"raw-set\",\"path\":\"/body\",\"xml\":\"\"}]", + null, 10, null)); + JsonNode media = mapper.readTree(tool.office_document( + "batch", office.toString(), null, + "[{\"command\":\"add\",\"path\":\"/body\",\"type\":\"image\",\"props\":{\"source\":\"/etc/passwd\"}}]", + null, 10, null)); + JsonNode hostPath = mapper.readTree(tool.office_document( + "batch", office.toString(), null, + "[{\"command\":\"set\",\"path\":\"/body/p[1]\",\"props\":{\"source\":\"C:\\\\secret.txt\"}}]", + null, 10, null)); + + assertTrue(raw.path("error").asText().contains("Unsupported batch command")); + assertTrue(media.path("error").asText().contains("External media")); + assertTrue(hostPath.path("error").asText().contains("filesystem reference")); + } + + @Test + @DisplayName("timeout terminates the native process and reports a bounded failure") + void timeoutIsEnforced() throws Exception { + Path sleepy = tempDir.resolve("officecli-sleepy"); + Files.writeString(sleepy, "#!/bin/sh\nsleep 10\n", StandardCharsets.UTF_8); + assertTrue(sleepy.toFile().setExecutable(true)); + Path source = Files.writeString(tempDir.resolve("slow.docx"), "PK-docx"); + OfficeCliTool tool = new OfficeCliTool(cache, mapper, sleepy.toString()); + + long started = System.nanoTime(); + JsonNode result = mapper.readTree(tool.office_document( + "validate", source.toString(), null, null, null, 1, null)); + long elapsedMillis = (System.nanoTime() - started) / 1_000_000; + + assertFalse(result.path("success").asBoolean()); + assertTrue(result.path("timedOut").asBoolean()); + assertTrue(elapsedMillis < 5_000, "timeout took too long: " + elapsedMillis + "ms"); + } + + @Test + @DisplayName("a missing binary is reported as setup-required instead of an opaque exception") + void missingBinaryReportsSetupRequired() throws Exception { + Path source = Files.writeString(tempDir.resolve("a.docx"), "PK-docx"); + OfficeCliTool tool = new OfficeCliTool(cache, mapper, + tempDir.resolve("missing-officecli").toString()); + + JsonNode result = mapper.readTree(tool.office_document( + "validate", source.toString(), null, null, null, 10, null)); + + assertFalse(result.path("success").asBoolean()); + assertTrue(result.path("setupRequired").asBoolean(), result.toString()); + } + + private GeneratedFileCache.Entry cachedEntry(String generatedFileText) { + Matcher matcher = GENERATED_ID.matcher(generatedFileText); + assertTrue(matcher.find(), generatedFileText); + return cache.get(matcher.group(1)).orElseThrow(); + } +}