SpCat 插件开发指南

基于 QuickJS 的插件系统,兼容 Bob 插件生态。复制模板,改接口地址,打包安装。

1. 快速开始

⚠️ 版本要求插件功能需要 SpCat 0.5.0 及以上版本。请在设置页确认版本号,低于此版本不支持插件。

一个插件就是一个 zip 包,里面至少包含两个文件:

  • info.json — 插件的「身份证」,告诉 SpCat 这个插件叫什么、做什么
  • main.js — 插件的代码,实际干活的地方
文件结构
myplugin/ ├── info.json ← 必填 ├── main.js ← 必填 └── icon.png ← 可选图标(128×128 PNG,显示在设置页和添加服务页)

图标 icon

插件图标有两种设置方式:

方式一:直接放 icon.png(推荐)

在插件根目录放一个 icon.png(建议 128×128 像素),SpCat 会自动识别。

方式二:在 info.json 里指定

info.json 里添加 "icon" 字段:

icon 字段
{ "identifier": "com.example.myplugin", "name": "MyPlugin", "icon": "icon.png", ← 插件包内的相对路径 ... }
icon 值含义
"icon.png"插件包内相对路径(如 icon.pngicons/app.png
"113"内置图标 ID(纯数字),SpCat 提供了 50+ 个内置图标可选
省略自动查找插件目录下的 icon.png

内置图标一览

info.json"icon" 字段填入编号即可使用。点击图标可复制编号。

说明如果找不到图标,会使用默认的蓝色插件图标。你也可以直接在插件目录放 icon.png(推荐 128×128)。
📎 图标来源内置图标来自 Bob 插件图标,与 Bob 插件生态保持兼容。

打包

在插件文件夹里全选所有文件 → 压缩成 zip → 扩展名改成 .spcatplugin

⚠️ 注意zip 里面直接就是 info.jsonmain.js,不要多套一层文件夹。错误:myplugin/myplugin/info.json;正确:myplugin/info.json

安装

  1. 打开 设置 → 翻译(翻译插件)或 设置 → 识别(识别插件)
  2. 点击 添加服务 → 安装插件…,选择你打包好的文件
  3. 安装完成,服务列表里就能看到了

调试

选中你的插件后,页面上有 调试插件 按钮,点击打开插件调试控制台 —— 这是开发插件最常用的工具:

能力说明
实时日志插件里的 $log.info() / $log.error() 等输出会直接显示在这里,不用去翻系统日志
发送请求用内置测试图(SpCat Debug Test 123,400×140)跑一次,并显示插件返回的识别结果 JSON
热重载每次点「发送请求」都会重新加载插件文件 —— 改完 main.js 直接回到控制台再点一次即可生效,不用重启 SpCat
打开目录在系统文件管理器里定位插件目录(沙盒和日志都在里面)
💡 推荐流程main.js → 点「发送请求」→ 看日志定位问题。只有在排查与 Qt 交互的疑难问题(如插件加载失败)时,才需要看应用日志。

2. 测试插件下载 真实可用

下面提供了 6 个完整可运行的测试插件,覆盖翻译和识别两大类。直接下载安装即可验证插件系统。点击文件名可查看源码。

插件类型API Key免费额度文件
MyMemory翻译 不需要每天 1000 词 info.json · main.js ⬇ 下载
OCRSpace识别 不需要公共测试 Key info.json · main.js ⬇ 下载
百度翻译翻译 App ID + Secret Key每月 100 万字符 info.json · main.js ⬇ 下载
百度OCR识别 API Key + Secret Key每月 1000 次 info.json · main.js ⬇ 下载
智谱清言翻译翻译 API KeyGLM-4-Flash 免费 info.json · main.js ⬇ 下载
智谱清言OCR识别 API KeyGLM-4V-Flash 免费 info.json · main.js ⬇ 下载
💡 提示MyMemory 和 OCRSpace 无需 API Key,安装即可用,适合快速验证插件系统是否正常工作。

3. info.json 与 UI

安装插件后,SpCat 会读取 info.json,在设置面板右侧自动渲染配置页面。你不需要写任何 UI 代码。

对应关系一览

下表是 info.json顶层字段。其中 identifiercategory 虽然不直接渲染 UI,但决定插件能否被正确识别、归类到对应页面,务必写对。完整的 info.json 内容还会以 $info 注入脚本,插件运行时可读取任意顶层字段(如 $info.identifier$info.version)。

字段必填作用 / UI 效果
identifier必填插件唯一标识(如 com.example.myocr)。缺失时安装与加载都会直接失败;安装后作为插件目录名与模板 id(plugin_<identifier>)。
category必填插件类型:识别插件写 "ocr",翻译插件写 "translate"。决定插件出现在哪个设置页、加载时校验哪组导出函数,详见下方「category:识别插件还是翻译插件」。
name推荐页面顶部标题 + 左侧列表项名称(省略则显示 identifier)
version可选插件版本号(如 "1.0.0"),随 $info 注入脚本;暂无独立 UI 展示,属于打包规范的一部分。
summary(或 desc可选标题下方的描述文字 + 左侧列表项名称后缀(如「翻译插件(描述)」)。Bob 兼容:desc 优先、summary 兜底,两者都可不填。
icon可选左侧列表项图标(内置图标 ID 或包内相对路径,详见第 1 节)
homepage可选底部「插件主页」按钮(字段为空则不显示)
options可选配置表单(每个选项渲染一个控件,字段说明见下表)

category:识别插件还是翻译插件

category 决定插件的「身份」,SpCat 据此把插件归入对应页面,并在加载时校验 main.js 的导出:

category 值插件类型出现在引擎校验 main.js 必须导出
"ocr"识别插件设置 → 识别ocr(query, completion)
"translate"翻译插件设置 → 翻译translate(query, completion) + supportLanguages()
注意设置页列表按 category 精确匹配:识别页只收 "ocr",翻译页只收 "translate"。引擎加载时同样按 category 分流:等于 "ocr" 时要求导出 ocr();其余值(含留空,兼容 Bob 老翻译插件)一律按翻译插件要求导出 translate() + supportLanguages()。所以新建插件务必显式写对:识别写 "ocr"、翻译写 "translate";写错或留空可能导致安装后在设置页看不到它,或加载时报「未导出 xxx 函数」。

options 每一项的 type → 控件映射

type渲染效果细节
"text"(默认)标签 + 单行输入框identifier 含 key / password / token / secret / 密钥 / 密码,或以 id 结尾 → 自动变密码框 + 眼睛按钮
"menu"标签 + 下拉选择框选项列表从 menuValues 读取
"checkbox"复选框defaultValue 为 "1" / "true" / "YES" 时默认勾选
"textfield"标签 + 多行文本框固定高度 90px

options 支持的字段

字段必填说明
identifier必填选项键名,代码里用 $option.xxx 读取
title推荐显示标签(省略则用 identifier)
type可选text / menu / checkbox / textfield,默认 text
defaultValue可选默认值(字符串)
menuValuesmenu 必填下拉选项 [{title, value}, ...]
textConfig.placeholderText可选输入框占位提示文字

动态预览:左边改 JSON,右边实时看 UI

试试修改左侧的 options,右边会实时渲染出设置面板的效果:

4. 识别插件模板 直接复制用

识别插件只需要导出 1 个必须函数。info.json 的写法见第 3 节。

必须导出的函数

函数名是否必须作用
ocr(query, completion)必须接收截图,调用 OCR API,返回识别结果

ocr() 的 query 参数

调用 ocr(query, completion) 时,query 里包含截图信息。你需要从中取出图片数据发给你的 OCR API。

query 结构
query = { "image": { // 图片数据 "base64": "iVBORw0KGgo...", // JPEG base64(无 data:image 前缀) "toBase64": function() {...} // 同上,调用返回 base64 字符串 }, "pixelWidth": 1920, // 图片原始宽度(像素) "pixelHeight": 1080, // 图片原始高度(像素) "options": { // 用户配置(和 $option 一样) "apiKey": "sk-xxx", "apiUrl": "https://..." } }
字段怎么用
query.image.base64放到 HTTP body 里发给 API(最常用)
query.image.toBase64()同上,调用后返回 base64 字符串
query.image(直接传)$http.files 上传时,data: query.image 直接传字节对象
query.pixelWidthAPI 返回归一化坐标时,乘以它还原成像素
query.pixelHeight同上
query.options$option 内容一致,二选一用

ocr() 的 completion 回调

处理完毕后必须调用 completion() 把结果交给宿主。有两种调用方式:

✅ 识别成功

成功格式
completion({ "result": { "from": "zh", // 可选:识别出的语言码(如 API 有返回的话) "texts": [ { "text": "识别出的文字", // 必填:这一块的文字内容 "boundingBox": { // 可选:文字在图片中的位置(归一化 0~1) "points": [ { "x": 0.1, "y": 0.2 }, // 左上 { "x": 0.9, "y": 0.2 }, // 右上 { "x": 0.9, "y": 0.8 }, // 右下 { "x": 0.1, "y": 0.8 } // 左下 ] } }, { "text": "第二块文字" // 多块文字按顺序返回 } ] } });
说明texts 数组按从上到下、从左到右的顺序排列。每项的 boundingBox 是可选的(坐标值 0~1 归一化,宿主会乘以 pixelWidth/pixelHeight 还原成像素)。如果 API 不返回位置信息,省略 boundingBox 即可。
⚠️ 关于自动排版宿主的智能段落排版功能完全依赖 boundingBox 坐标数据来判断换行、缩进和段落分隔。如果插件返回的每条文字都带有 boundingBox,宿主会自动进行段落合并与排版;如果没有任何 boundingBox,宿主只能把所有文字用换行符简单拼接,不会触发自动排版。因此,若你的 OCR API 能返回文字位置信息(如百度 general/accurate 接口),请务必带上 boundingBox

📝 大模型识别:输出 Markdown

如果用大模型做 OCR(如 GPT-4o、Claude),API 返回的可能是 Markdown 格式。直接把 Markdown 内容放在 text 字段即可,不需要特殊处理:

Markdown 输出示例
async function ocr(query, completion) { const resp = await $http.request({ url: "https://api.openai.com/v1/chat/completions", method: "POST", header: { "Content-Type": "application/json", "Authorization": "Bearer " + $option.apiKey }, body: { model: "gpt-4o", messages: [{ role: "user", content: [ { type: "text", text: "识别图片中的文字,输出 Markdown 格式" }, { type: "image_url", image_url: { url: "data:image/jpeg;base64," + query.image.base64 }} ] }] } }); const mdText = resp.data.choices[0].message.content; // 直接把整个 Markdown 放进一个 text 字段 completion({ result: { texts: [{ text: mdText }] }}); }
💡 说明
  • text 字段可以是任意字符串,包括 Markdown、LaTeX、代码块等
  • 多个 texts 项之间用换行符连接,如果 Markdown 是完整的一段,放一个 { text: "..." } 就够了
  • 大模型识别通常不需要 boundingBox(模型不返回位置信息),省略即可

❌ 识别失败

失败格式
completion({ "error": { "type": "api", // 错误类型(见下表) "message": "请求失败", // 必填:用户可见的错误描述 "addition": "详细信息" // 可选:附加说明(显示在括号里) } });
error.type含义典型场景
"param"参数问题没填 API Key、配置缺失
"auth"鉴权失败API Key 无效、过期
"api"接口报错HTTP 4xx/5xx、API 返回错误
"network"网络问题超时、DNS 解析失败、连接断开
"plugin"脚本异常代码 bug、未捕获异常
⚠️ 关键所有分支(成功、失败、catch)都必须调用 completion()!不调用会导致界面卡死超时。推荐在最外层 try/catch,catch 里也调 completion({ error: ... })

$option 读取配置

info.json 里 options 声明的每一项,通过 $option.键名 读取:

$option 对应关系
// info.json 里声明: // "identifier": "apiKey" → $option.apiKey // "identifier": "apiUrl" → $option.apiUrl // "identifier": "mode" → $option.mode const apiKey = $option.apiKey || ""; // 字符串,需要数字用 Number() 转换 const mode = $option.mode || "fast";
⚠️ 注意所有值都是字符串。数字用 Number($option.xxx) 转换,布尔用 $option.xxx === "true" 判断。

完整示例

方式一:JSON API(图片以 base64 发送)

main.js
async function ocr(query, completion) { const apiKey = $option.apiKey || ""; try { const resp = await $http.request({ url: "https://api.example.com/v1/ocr", method: "POST", header: { "Content-Type": "application/json" }, body: { image: query.image.base64, apiKey: apiKey }, timeout: 30 }); const data = resp.data; if (resp.response.statusCode !== 200 || !data || !data.text) { completion({ error: { type: "api", message: "HTTP " + resp.response.statusCode } }); return; } // 成功:把 API 返回的文字放进 texts 数组 completion({ result: { texts: [{ text: data.text }] } }); } catch (e) { completion({ error: { type: "network", message: String(e) } }); } }

方式二:文件上传(multipart,适合图片类 API)

main.js
async function ocr(query, completion) { try { // query.image 直接作为字节对象传给 files.data const resp = await $http.request({ url: "https://api.example.com/v1/ocr?token=xxx", // 参数拼URL method: "POST", files: [{ name: "file", // 表单字段名 filename: "image.jpg", // 文件名 "content-type": "image/jpeg", // MIME类型 data: query.image // 字节对象,直接传 }], timeout: 30 }); const data = resp.data; completion({ result: { texts: [{ text: data.text }] } }); } catch (e) { completion({ error: { type: "network", message: String(e) } }); } }

5. 翻译插件模板 直接复制用

翻译插件需要导出 2 个必须函数。info.json 的写法见第 3 节。

必须导出的函数

函数名是否必须作用
supportLanguages()必须告诉 SpCat 你的 API 支持哪些目标语言
translate(query, completion)必须接收原文,调用翻译 API,返回译文

supportLanguages 函数详解

  • 同步函数,直接 return 一个字符串数组
  • 数组里的每个值是 BCP-47 语言码(如 "en""zh-Hans"
  • 完整语言码表见第 8 节
  • 只填你 API 实际支持的语言即可
supportLanguages 示例
function supportLanguages() { return [ "zh-Hans", // 中文简体 "zh-Hant", // 中文繁体 "en", // 英语 "ja", // 日语 "ko", // 韩语 "fr", // 法语 "de", // 德语 "es", // 西班牙语 "it", // 意大利语 "pt", // 葡萄牙语 "ru", // 俄语 "ar", // 阿拉伯语 "vi", // 越南语 "th", // 泰语 "hi", // 印地语 "ms", // 马来语 "id", // 印尼语 "tr", // 土耳其语 "pl", // 波兰语 "nl", // 荷兰语 "uk", // 乌克兰语 "cs", // 捷克语 "sv", // 瑞典语 "el", // 希腊语 "fi", // 芬兰语 "he", // 希伯来语 "hu", // 匈牙利语 "nb", // 挪威语 "sk", // 斯洛伐克语 "sl", // 斯洛文尼亚语 "da" // 丹麦语 ]; }

translate() 的 query 参数

调用 translate(query, completion) 时,query 里包含翻译请求。你需要从中取出原文和语言信息发给你的翻译 API。

query 结构
query = { "text": "Hello, world!", // 待翻译的原文 "from": "en", // 源语言码(如 "en"、"auto") "to": "zh-Hans", // 目标语言码(如 "zh-Hans") "options": { // 用户配置(和 $option 一样) "apiKey": "sk-xxx", "mode": "fast" } }
字段怎么用
query.text发给翻译 API 的输入原文
query.from源语言码。为 "auto" 时由 API 检测语言
query.to目标语言码
query.options$option 内容一致,二选一用

translate() 的 completion 回调

翻译完毕后必须调用 completion() 把结果交给宿主。有三种调用方式:

✅ 段落翻译(最常用)

按行返回译文,数组第 i 项对应原文第 i 行:

段落翻译格式
completion({ "result": { "toParagraphs": [ "你好,世界!" // 如果原文只有一行,数组就一个元素 ] } });

多行原文对应多行译文:

多行示例
// 原文: "Hello\nWorld" completion({ "result": { "toParagraphs": ["你好", "世界"] } });

✅ 词典翻译

返回「原文 → 译文」的键值对,适合词典类 API:

词典格式
completion({ "result": { "toDict": { "apple": "苹果", "banana": "香蕉", "bank": "银行;河岸" // 值可以带多义 } } });

❌ 翻译失败

失败格式
completion({ "error": { "type": "auth", // 错误类型(见下表) "message": "API Key 无效", // 必填:用户可见的错误描述 "addition": "401Unauthorized"// 可选:附加说明(显示在括号里) } });
error.type含义典型场景
"param"参数问题没填 API Key、配置缺失
"auth"鉴权失败API Key 无效、过期、余额不足
"api"接口报错HTTP 4xx/5xx、API 返回错误
"network"网络问题超时、DNS 解析失败、连接断开
"plugin"脚本异常代码 bug、未捕获异常
⚠️ 关键所有分支(成功、失败、catch)都必须调用 completion()!不调用会导致界面卡死超时。推荐在最外层 try/catch,catch 里也调 completion({ error: ... })

$option 读取配置

和识别插件一样,通过 $option.键名 读取用户在设置面板填的值:

$option 对应关系
// info.json 里声明: // "identifier": "apiKey" → $option.apiKey // "identifier": "apiUrl" → $option.apiUrl // "identifier": "mode" → $option.mode const apiKey = $option.apiKey || ""; // 字符串,需要数字用 Number() 转换 const apiUrl = $option.apiUrl || "https://api.example.com"; const mode = $option.mode || "fast";
⚠️ 注意所有值都是字符串。数字用 Number($option.xxx) 转换,布尔用 $option.xxx === "true" 判断。

完整示例

main.js
// 【函数1】告诉 SpCat 支持哪些语言 function supportLanguages() { return ["zh-Hans", "zh-Hant", "en", "ja", "ko", "fr", "de", "es"]; } // 【函数2】执行翻译 async function translate(query, completion) { const apiKey = $option.apiKey || ""; if (!apiKey) { completion({ error: { type: "param", message: "请在设置中填写 API Key" } }); return; } try { const resp = await $http.request({ url: "https://api.example.com/v1/translate", method: "POST", header: { "Content-Type": "application/json", "Authorization": "Bearer " + apiKey }, body: { text: query.text, from: query.from, to: query.to }, timeout: 20 }); const data = resp.data; if (resp.response.statusCode !== 200 || !data || !data.translation) { completion({ error: { type: "api", message: "HTTP " + resp.response.statusCode } }); return; } // 成功:返回段落数组 completion({ result: { toParagraphs: [data.translation] } }); } catch (e) { completion({ error: { type: "network", message: String(e) } }); } }

6. $http 网络请求

6 种请求方法

方法说明
$http.request(config)通用请求,method 由 config.method 指定
$http.get(config)等价 request,method 强制 GET
$http.post(config)等价 request,method 强制 POST
$http.put(config)等价 request,method 强制 PUT
$http.delete(config)等价 request,method 强制 DELETE
$http.streamRequest(config)流式请求(SSE 等增量接口),详见下方

请求配置字段

字段类型默认说明
urlstring必填请求地址
methodstringGETGET / POST / PUT / DELETE
headerobject请求头。用单数 header(也兼容 headers
body多种请求体,详见下方
filesarray文件上传(multipart),有 files 时忽略 body
timeoutnumber60超时秒数
handlerfunction回调风格:无论成败都会调用 handler(resp)
streamHandlerfunction流式增量回调:streamHandler({text})

body 三种形态

body 类型行为Content-Type
字节对象 {__bobBytes: "base64..."}解码为原始字节发送需自行通过 header 指定
普通 JS 对象若 header 含 json → JSON.stringify;否则 → form-urlencodedjson 时自动设;否则自动设 application/x-www-form-urlencoded
字符串原样发送需自行通过 header 指定
body 示例
// ① JSON 对象 — header 含 "application/json" → 自动 JSON.stringify $http.request({ url: "https://api.example.com/v1/translate", method: "POST", header: { "Content-Type": "application/json" }, body: { text: "Hello", from: "en", to: "zh-Hans" } }); // ② form-urlencoded — 不设 json header → 自动编码为 form $http.request({ url: "https://translate.googleapis.com/translate_a/single", method: "POST", body: { q: "Hello", sl: "en", tl: "zh-Hans" } }); // ③ 字节对象 — 原始字节发送 $http.request({ url: "https://api.example.com/v1/ocr", method: "POST", header: { "Content-Type": "application/octet-stream" }, body: query.image // 字节对象 {__bobBytes} });
⚠️ GET 的 body 会被丢弃GET 请求不能带 body,参数拼到 URL 查询字符串上。

files 文件上传(multipart)

config.files 非空时,自动组装 multipart/form-data,跳过 body 处理。

files 结构
files: [{ name: "file", ← 表单字段名(默认 "file") filename: "image.jpg", ← 文件名(可选) "content-type": "image/jpeg", ← MIME(默认 application/octet-stream) data: byteObj ← 字节对象 / {base64} / 字符串 }]

流式请求(streamRequest)

用于 SSE 等流式接口。增量数据通过 streamHandler({text}) 逐块推送,完成时调用 handler(resp) 收尾。

流式请求示例
let fullText = ""; $http.streamRequest({ url: "https://api.example.com/v1/translate-stream", method: "POST", header: { "Content-Type": "application/json" }, body: { text: query.text, from: query.from, to: query.to }, // 每收到一块数据就调用一次 streamHandler(resp) { fullText += resp.text; }, // 流结束后调用,提交最终结果 handler(resp) { completion({ result: { toParagraphs: [fullText] } }); } });
说明流式请求的 Promise 不会 resolve,请以 handler 回调为准。

响应对象

resp 结构
{ data: // JSON 响应 → 自动解析为对象;否则 → 字符串 statusCode: // HTTP 状态码(number) response: { statusCode: // 同上(插件通常读这里) header: // 响应头 } }

两种调用风格

风格写法说明
Promiseconst resp = await $http.request({...})推荐。网络失败时 reject
回调$http.request({..., handler(resp){...}})无论成败都调用 handler
💡 自动重试以下错误会自动重试(1 秒后):RemoteHostClosed / Timeout / ConnectionRefused / HostNotFound / Network unreachable

7. 可用能力速查

对象用途简单示例
$http发网络请求await $http.request({url, method, header, body})
$option读取用户配置$option.apiKey(全是字符串)
$log打印日志(输出到系统控制台)$log.info("hello") / $log.error("err")
setTimeout异步延时(含 setIntervalsetTimeout(fn, 1500)
$file沙盒文件读写$file.write("a.txt", "内容")
$data数据转换$data.toUTF8(byteObj)
$pdf图片转 PDF(宿主原生)$pdf.imageToPdf(query.image)
$image图片转内联 data URI(宿主原生)await $image.toDataUri(url)
$sandbox沙盒目录路径(就在插件自己的目录下)"<插件目录>/sandbox"
require引入模块require("crypto-js")(内置 MD5)

$file 沙盒文件读写

每个插件有独立的沙盒目录($sandbox),位于插件自己的目录下(<插件目录>/sandbox),$file 只能操作沙盒内的文件,防止目录逃逸。

说明沙盒路径由插件目录推导,因此 macOS / Windows / Linux 三个平台位置一致。从旧版本升级时会自动把原沙盒(含凭证)搬迁过来,无需重新登录。
方法说明
$file.read(path)读取文件,返回字节对象 {__bobBytes: "base64..."}
$file.write(path, data)写入文件(自动创建父目录),data 可以是字符串或字节对象
$file.exists(path)检查文件是否存在,返回 true / false
$file.remove(path)删除文件
⚠️ 注意路径都相对于 $sandbox(自动创建父目录),不能用 .. 或绝对路径。

$data 数据转换

在字符串和字节对象之间转换。字节对象格式:{__bobBytes: "base64编码"}

方法说明
$data.fromUTF8(str)字符串 → 字节对象
$data.toUTF8(byteObj)字节对象 → 字符串
$data.toBase64(byteObj)字节对象 → base64 字符串
$data.fromBase64(str)base64 字符串 → 字节对象
说明$file.read() 拿到字节对象,再 $data.toUTF8() 转成字符串,是最常见的组合。

$pdf 图片转 PDF

在宿主侧(Qt)把图片生成 PDF,插件不必自己拼 PDF 结构。支持 JPEG / PNG / WebP / BMP / TIFF / GIF,自动按 EXIF 方向转正;传数组即多页(每张一页)。

调用说明
$pdf.imageToPdf(image, options?)image 为字节对象 / {base64} / base64 字符串,或它们的数组(多页)
options默认说明
format"text""text" 全 ASCII 文本,可直接当 $http 字符串 body;"base64""bytes" 字节对象(配 files[] 上传)
pageWidth / pageHeight图片像素页面尺寸(pt)。默认 1 像素 = 1pt,图像按比例内接居中,不会拉伸
jpegQuality92重编码质量。原图本身是 JPEG 时直通,不做二次压缩
minPageSide400页面短边下限(pt),仅在图片尺寸未知时兜底;0 = 不放大
图片 → PDF → 上传
// 生成(默认返回全 ASCII 文本,可直接当 $http 字符串 body) const pdf = $pdf.imageToPdf(query.image); // 当文件上传:bytes 形态配 files[] 最省事 await $http.post({ url: "https://your.api/upload", files: [{ name: "file", filename: "doc.pdf", "content-type": "application/pdf", data: $pdf.imageToPdf(query.image, { format: "bytes" }) }] });
⚠️ 出错会抛异常图片解码失败时 throw,请用 try / catch 包住并 completion({ error: {...} }) 回报。

require 内置模块 — crypto-js

内置 crypto-js 的 MD5 子集,主要用于 Google 翻译等需要签名的接口。

crypto-js MD5
const CryptoJS = require("crypto-js"); const hash = CryptoJS.MD5("hello").toString(); // → "5d41402abc4b2a76b9719d911017c592"
说明只提供 MD5 子集(纯 JS、UTF-8 输入),不提供完整 crypto-js。

$log 日志输出

$log 用法
$log.info("调试信息"); $log.error("错误信息"); $log.debug("调试详情"); $log.warning("警告");
💡 看日志在设置页选中插件后点 调试插件,日志会实时显示在插件调试控制台里(见第 1 节「调试」),这是最方便的方式。日志同时也会写入应用日志(macOS 可用 Console.app 按进程名过滤查看)。

$image 图片转内联 data URI

识别结果里的图片常常是远程链接 —— 编辑器里不一定显示,而且会过期、正文不再自包含。内联成 data: URI 即可解决,标签形如:

内联图片标签
<img src="data:image/jpeg;base64,..." alt="image" width="300" height="200" origin-size="300,200" />
⚠️ 别用 $http 下图片$http 的非 JSON 响应以 JS 字符串回传,而 JPEG/PNG 不是合法 UTF-8,字节会被替换成 U+FFFD(一张 13KB 的图只能还原出 12 字节)。下载必须交给 $image,由 Qt 侧完成下载 / 嗅探 / base64。
调用说明
await $image.toDataUri(url)下载 http(s) 图片 → 返回结果对象
$image.toDataUri(bytes, opts?)本地字节 / base64 / data: URI → 同步返回;opts.crop = {x,y,w,h} 可先裁剪(源图像素坐标,越界自动钳制)

返回 { dataUri, mimeType, width, height, size, bytes }mimeType 按文件头嗅探;bytes 可直接喂给 $httpfiles[]$pdf

把正文里的远程图片批量内联
// 并发下载,单张失败保留原链接即可(不要因为一张图丢掉整篇结果) const jobs = urls.map(u => $image.toDataUri(u).catch(() => null)); const got = (await Promise.all(jobs)).filter(Boolean);
说明从原图按真实坐标裁剪时,用 crop 直接从原截图裁一块,比缩放服务端返回的图更清晰、尺寸也更准。

setTimeout 定时器

QuickJS 标准库不含定时器,由宿主提供。配合 $http(同样是异步的)即可实现轮询、超时重试、限流等。

API说明
setTimeout(fn, ms)延时执行一次,返回句柄 id
setInterval(fn, ms)每隔 ms 执行一次,返回句柄 id
clearTimeout(id) / clearInterval(id)取消定时器
轮询任务状态
const waitMs = ms => new Promise(r => setTimeout(r, ms)); while (true) { const resp = await $http.post({ url: statusUrl, body: { id } }); if (resp.data.status === 2) break; await waitMs(1500); // 每次等待都让出主线程 }

异步执行与主线程

⚠️ 插件跑在主线程上插件脚本与界面共用同一个 Qt 事件循环。忙等while 空转)或超长同步循环会直接冻住软件:界面不刷新、按钮点不动,直到那段 JS 跑完。等待时间、等待网络结果,一律用 await
❌ 这些写法会卡死界面
// 这 1.5 秒里界面完全无响应,事件循环一次都不会跑 const t = Date.now(); while (Date.now() - t < 1500) { } // 死等网络结果 —— 回调没有机会执行,必然死锁 while (!done) { }
💡 超时保护为了不让写错的插件永久卡死软件,宿主对单次同步执行设了 5 秒上限,超过就中断并回报错误。上限按每次同步执行计算,不是插件总运行时长 —— 一次识别里轮询几分钟都正常,只要中间有 await。点「停止」会取消所有挂起的请求与定时器。

completion 回调格式速查

详细的输入输出格式见第 4 节(识别)和第 5 节(翻译)。

场景调用方式
翻译成功completion({ result: { toParagraphs: ["译文"] } })
翻译成功(词典)completion({ result: { toDict: {"原词": "译词"} } })
识别成功completion({ result: { texts: [{ text: "文字" }] } })
失败completion({ error: { type: "api", message: "..." } })

8. 语言码映射

supportLanguages() 返回的语言码遵循 BCP-47 格式。宿主会自动转换界面上的中文语言名和插件码。

翻译模板里 supportLanguages() 的完整写法(按需增删):

supportLanguages
function supportLanguages() { return [ "zh-Hans", // 中文简体 "zh-Hant", // 中文繁体 "en", // 英语 "ja", // 日语 "ko", // 韩语 "fr", // 法语 "de", // 德语 "es", // 西班牙语 "it", // 意大利语 "pt", // 葡萄牙语 "ru", // 俄语 "ar", // 阿拉伯语 "vi", // 越南语 "th", // 泰语 "hi", // 印地语 "ms", // 马来语 "id", // 印尼语 "tr", // 土耳其语 "pl", // 波兰语 "nl", // 荷兰语 "uk", // 乌克兰语 "cs", // 捷克语 "sv", // 瑞典语 "el", // 希腊语 "fi", // 芬兰语 "he", // 希伯来语 "hu", // 匈牙利语 "nb", // 挪威语 "sk", // 斯洛伐克语 "sl", // 斯洛文尼亚语 "da" // 丹麦语 ]; }

完整语言码对照表

BCP-47 码中文名BCP-47 码中文名BCP-47 码中文名
auto自动选择zh-Hans中文简体zh-Hant中文繁体
en英语ja日语ko韩语
fr法语de德语es西班牙语
it意大利语pt葡萄牙语ru俄语
ar阿拉伯语vi越南语th泰语
hi印地语ms马来语id印尼语
tr土耳其语pl波兰语nl荷兰语
uk乌克兰语cs捷克语sv瑞典语
el希腊语fi芬兰语he希伯来语
hu匈牙利语nb挪威语sk斯洛伐克语
sl斯洛文尼亚语da丹麦语未收录的码原样透传
💡 提示supportLanguages() 返回你插件目标语言的码列表。源语言通常额外支持 "auto"(宿主会处理自动检测)。只填你 API 实际支持的语言即可。

9. 常见问题

问题原因解决
请求卡死超时没调 completion()所有分支(包括 catch)都要调用
GET 参数丢了GET 不发 body参数拼到 URL 上
$option 读到 undefinedinfo.json 没声明 / 键名拼错检查 options[].identifier
body 被编码成 form没设 Content-Type"Content-Type": "application/json"
require 找不到模块用了绝对路径或 ..只用包内相对路径
PNG / 透明图转 PDF 失败插件里手写 PDF 只能内嵌 JPEG改用 $pdf.imageToPdf(image)(宿主原生,支持 PNG 等)
上传的 PDF 被判「空白页」页面点数太小(仅极小图片会遇到)调大 minPageSide,例如 { minPageSide: 600 }
识别结果里的图片偏大服务端按自己的坐标空间返回裁剪图,不等于原图像素原图像素 ÷ 返回的页面尺寸 换算尺寸,或直接用 $imagecrop 从原图裁剪
点了识别后软件卡死插件在忙等(while 等时间/等结果),JS 跑在主线程会冻住界面把等待写成 await new Promise(r => setTimeout(r, ms))$http 本身是异步的,直接 await 即可
报「同步执行超过 5 秒已被中断」某段 JS 连续同步跑了 5 秒还没让出主线程拆分长循环、把耗时计算改成异步或交给 $pdf 等宿主原生能力
识别结果里的图片显示不出来图片是远程链接,编辑器不一定加载(且会过期)$image.toDataUri(url) 内联成 data: URI
用 $http 下图片,图片坏了二进制被当 UTF-8 字符串回传,字节被替换成 U+FFFD改用 $image.toDataUri(下载在 Qt 侧完成)
SpCat 插件开发指南 · 兼容 Bob 插件生态