#!/usr/bin/env node /** * 腾讯文档 MCP 本地图片 OCR 辅助脚本(跨平台) * * 功能: * 自动将本地图片 base64 编码后调用 ocr.* 工具,支持三种操作: * - extract: 识别图片中的文字 * - toword: 将图片转为 Word 文档 * - toexcel: 将图片转为 Excel 文档 * * 用法: * node ocr.js extract [--accurate|--efficient] [--positions] * node ocr.js toword [ ...] [--title "标题"] * node ocr.js toexcel [ ...] [--title "标题"] * * 依赖: * - Node.js (>= 14) * - mcporter(已配置 tencent-docs 服务) */ "use strict"; const { execFileSync } = require("child_process"); const fs = require("fs"); const path = require("path"); // ── 常量 ────────────────────────────────────────────────────────────────── const MCP_SERVICE = "tencent-docs"; const SUPPORTED_EXTS = new Set(["png", "jpg", "jpeg", "bmp", "webp"]); const MAX_SINGLE_SIZE = 10 * 1024 * 1024; const MAX_TOTAL_SIZE = 50 * 1024 * 1024; const MAX_IMAGE_COUNT = 9; // ── 工具函数 ────────────────────────────────────────────────────────────── function die(msg) { console.error("ERROR: " + msg); process.exit(1); } function validateImage(filePath) { if (!fs.existsSync(filePath)) die("文件不存在: " + filePath); var stat = fs.statSync(filePath); if (!stat.isFile()) die("不是文件: " + filePath); var ext = path.extname(filePath).slice(1).toLowerCase(); if (!SUPPORTED_EXTS.has(ext)) { die("不支持的格式 ." + ext + ",支持: " + Array.from(SUPPORTED_EXTS).join(", ")); } if (stat.size === 0) die("文件为空: " + filePath); if (stat.size > MAX_SINGLE_SIZE) die("文件超过 10MB: " + filePath); return stat.size; } function encodeBase64(filePath) { return fs.readFileSync(filePath).toString("base64"); } // ── mcporter 调用封装 ──────────────────────────────────────────────────── // 使用 execFileSync 直接传参数数组,绕过 shell,兼容 Windows 且无命令行长度限制 function mcpCall(tool, argsObj) { var argsJson = JSON.stringify(argsObj); try { var stdout = execFileSync( "mcporter", ["call", MCP_SERVICE, tool, "--args", argsJson], { encoding: "utf-8", timeout: 120000 } ); return stdout.trim(); } catch (err) { var msg = err.stderr || err.stdout || err.message || "unknown error"; throw new Error("mcporter call " + tool + " failed: " + msg); } } // ── 参数解析 ────────────────────────────────────────────────────────────── function parseArgs() { var args = process.argv.slice(2); if (args.length === 0) { console.log("用法:"); console.log(" node ocr.js extract [--accurate|--efficient] [--positions]"); console.log(' node ocr.js toword [ ...] [--title "标题"]'); console.log(' node ocr.js toexcel [ ...] [--title "标题"]'); process.exit(1); } var action = args[0]; if (["extract", "toword", "toexcel"].indexOf(action) === -1) { die("未知操作 '" + action + "',支持: extract, toword, toexcel"); } return { action: action, rest: args.slice(1) }; } // ── extract ────────────────────────────────────────────────────────────── function handleExtract(args) { var image = ""; var extractType = "basic"; var withPositions = false; for (var i = 0; i < args.length; i++) { switch (args[i]) { case "--accurate": extractType = "accurate"; break; case "--efficient": extractType = "efficient"; break; case "--positions": withPositions = true; break; default: if (args[i].charAt(0) === "-") die("未知选项 '" + args[i] + "'"); if (image) die("extract 只支持单张图片"); image = args[i]; break; } } if (!image) die("未指定图片路径"); validateImage(image); console.log("⏳ 正在识别 " + path.basename(image) + " ..."); var result = mcpCall("ocr.extract", { image_base64: encodeBase64(image), extract_type: extractType, with_positions: withPositions, }); console.log(result); } // ── toword / toexcel ───────────────────────────────────────────────────── function handleConvert(action, args) { var images = []; var title = ""; for (var i = 0; i < args.length; i++) { if (args[i] === "--title") { if (i + 1 >= args.length) die("--title 需要值"); title = args[++i]; } else if (args[i].charAt(0) === "-") { die("未知选项 '" + args[i] + "'"); } else { images.push(args[i]); } } if (images.length === 0) die("未指定图片路径"); if (images.length > MAX_IMAGE_COUNT) die("图片数量超过 " + MAX_IMAGE_COUNT + " 张限制"); var totalSize = 0; for (var j = 0; j < images.length; j++) { totalSize += validateImage(images[j]); } if (totalSize > MAX_TOTAL_SIZE) die("图片总大小超过 50MB"); console.log("⏳ 正在处理 " + images.length + " 张图片 ..."); var callArgs = { images: images.map(function (img) { return { image_base64: encodeBase64(img) }; }), }; if (title) callArgs.title = title; var result = mcpCall("ocr." + action, callArgs); console.log(result); } // ── 主流程 ──────────────────────────────────────────────────────────────── function main() { var parsed = parseArgs(); if (parsed.action === "extract") { handleExtract(parsed.rest); } else { handleConvert(parsed.action, parsed.rest); } } main();