mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(office): add optional OfficeCLI engine (#583)
This commit is contained in:
parent
81c6f4aece
commit
d2df5c2797
@ -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).
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class OfficeCliTool {
|
||||
|
||||
private static final Set<String> OFFICE_EXTENSIONS = Set.of("docx", "xlsx", "pptx");
|
||||
private static final Set<String> INSPECT_MODES = Set.of(
|
||||
"outline", "stats", "issues", "text", "annotated");
|
||||
private static final Map<String, String> 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<String> args)
|
||||
throws IOException, InterruptedException {
|
||||
return run(workingDir, timeoutSeconds, args, null);
|
||||
}
|
||||
|
||||
private ProcessResult run(Path workingDir, int timeoutSeconds, List<String> args,
|
||||
@Nullable Path stdoutArtifact)
|
||||
throws IOException, InterruptedException {
|
||||
List<String> 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<String> 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) { }
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
@ -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';
|
||||
@ -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';
|
||||
96
mateclaw-server/src/main/resources/skills/officecli/SKILL.md
Normal file
96
mateclaw-server/src/main/resources/skills/officecli/SKILL.md
Normal file
@ -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.
|
||||
@ -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\":\"<x/>\"}]",
|
||||
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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user