基于 QuickJS 的插件系统,兼容 Bob 插件生态。复制模板,改接口地址,打包安装。
一个插件就是一个 zip 包,里面至少包含两个文件:
info.json — 插件的「身份证」,告诉 SpCat 这个插件叫什么、做什么main.js — 插件的代码,实际干活的地方文件结构myplugin/ ├── info.json ← 必填 ├── main.js ← 必填 └── icon.png ← 可选图标(128×128 PNG,显示在设置页和添加服务页)
插件图标有两种设置方式:
在插件根目录放一个 icon.png(建议 128×128 像素),SpCat 会自动识别。
在 info.json 里添加 "icon" 字段:
icon 字段{ "identifier": "com.example.myplugin", "name": "MyPlugin", "icon": "icon.png", ← 插件包内的相对路径 ... }
| icon 值 | 含义 |
|---|---|
"icon.png" | 插件包内相对路径(如 icon.png、icons/app.png) |
"113" | 内置图标 ID(纯数字),SpCat 提供了 50+ 个内置图标可选 |
| 省略 | 自动查找插件目录下的 icon.png |
在 info.json 的 "icon" 字段填入编号即可使用。点击图标可复制编号。
icon.png(推荐 128×128)。在插件文件夹里全选所有文件 → 压缩成 zip → 扩展名改成 .spcatplugin。
info.json 和 main.js,不要多套一层文件夹。错误:myplugin/myplugin/info.json;正确:myplugin/info.json。选中你的插件后,页面上有 调试插件 按钮,点击打开插件调试控制台 —— 这是开发插件最常用的工具:
| 能力 | 说明 |
|---|---|
| 实时日志 | 插件里的 $log.info() / $log.error() 等输出会直接显示在这里,不用去翻系统日志 |
| 发送请求 | 用内置测试图(SpCat Debug Test 123,400×140)跑一次,并显示插件返回的识别结果 JSON |
| 热重载 | 每次点「发送请求」都会重新加载插件文件 —— 改完 main.js 直接回到控制台再点一次即可生效,不用重启 SpCat |
| 打开目录 | 在系统文件管理器里定位插件目录(沙盒和日志都在里面) |
main.js → 点「发送请求」→ 看日志定位问题。只有在排查与 Qt 交互的疑难问题(如插件加载失败)时,才需要看应用日志。下面提供了 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 Key | GLM-4-Flash 免费 | info.json · main.js | ⬇ 下载 |
| 智谱清言OCR | 识别 | API Key | GLM-4V-Flash 免费 | info.json · main.js | ⬇ 下载 |
安装插件后,SpCat 会读取 info.json,在设置面板右侧自动渲染配置页面。你不需要写任何 UI 代码。
下表是 info.json 的顶层字段。其中 identifier、category 虽然不直接渲染 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 决定插件的「身份」,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 函数」。| type | 渲染效果 | 细节 |
|---|---|---|
"text"(默认) | 标签 + 单行输入框 | identifier 含 key / password / token / secret / 密钥 / 密码,或以 id 结尾 → 自动变密码框 + 眼睛按钮 |
"menu" | 标签 + 下拉选择框 | 选项列表从 menuValues 读取 |
"checkbox" | 复选框 | defaultValue 为 "1" / "true" / "YES" 时默认勾选 |
"textfield" | 标签 + 多行文本框 | 固定高度 90px |
| 字段 | 必填 | 说明 |
|---|---|---|
identifier | 必填 | 选项键名,代码里用 $option.xxx 读取 |
title | 推荐 | 显示标签(省略则用 identifier) |
type | 可选 | text / menu / checkbox / textfield,默认 text |
defaultValue | 可选 | 默认值(字符串) |
menuValues | menu 必填 | 下拉选项 [{title, value}, ...] |
textConfig.placeholderText | 可选 | 输入框占位提示文字 |
试试修改左侧的 options,右边会实时渲染出设置面板的效果:
识别插件只需要导出 1 个必须函数。info.json 的写法见第 3 节。
| 函数名 | 是否必须 | 作用 |
|---|---|---|
ocr(query, completion) | 必须 | 接收截图,调用 OCR API,返回识别结果 |
调用 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.pixelWidth | API 返回归一化坐标时,乘以它还原成像素 |
query.pixelHeight | 同上 |
query.options | 和 $option 内容一致,二选一用 |
处理完毕后必须调用 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。如果用大模型做 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、未捕获异常 |
try/catch,catch 里也调 completion({ error: ... })。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" 判断。main.jsasync 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) } }); } }
main.jsasync 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) } }); } }
翻译插件需要导出 2 个必须函数。info.json 的写法见第 3 节。
| 函数名 | 是否必须 | 作用 |
|---|---|---|
supportLanguages() | 必须 | 告诉 SpCat 你的 API 支持哪些目标语言 |
translate(query, completion) | 必须 | 接收原文,调用翻译 API,返回译文 |
return 一个字符串数组"en"、"zh-Hans")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, 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 内容一致,二选一用 |
翻译完毕后必须调用 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、未捕获异常 |
try/catch,catch 里也调 completion({ error: ... })。和识别插件一样,通过 $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) } }); } }
| 方法 | 说明 |
|---|---|
$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 等增量接口),详见下方 |
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
url | string | 必填 | 请求地址 |
method | string | GET | GET / POST / PUT / DELETE |
header | object | — | 请求头。用单数 header(也兼容 headers) |
body | 多种 | — | 请求体,详见下方 |
files | array | — | 文件上传(multipart),有 files 时忽略 body |
timeout | number | 60 | 超时秒数 |
handler | function | — | 回调风格:无论成败都会调用 handler(resp) |
streamHandler | function | — | 流式增量回调:streamHandler({text}) |
| body 类型 | 行为 | Content-Type |
|---|---|---|
字节对象 {__bobBytes: "base64..."} | 解码为原始字节发送 | 需自行通过 header 指定 |
| 普通 JS 对象 | 若 header 含 json → JSON.stringify;否则 → form-urlencoded | json 时自动设;否则自动设 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} });
当 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} / 字符串 }]
用于 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] } }); } });
handler 回调为准。resp 结构{ data: // JSON 响应 → 自动解析为对象;否则 → 字符串 statusCode: // HTTP 状态码(number) response: { statusCode: // 同上(插件通常读这里) header: // 响应头 } }
| 风格 | 写法 | 说明 |
|---|---|---|
| Promise | const resp = await $http.request({...}) | 推荐。网络失败时 reject |
| 回调 | $http.request({..., handler(resp){...}}) | 无论成败都调用 handler |
RemoteHostClosed / Timeout / ConnectionRefused / HostNotFound / Network unreachable。| 对象 | 用途 | 简单示例 |
|---|---|---|
$http | 发网络请求 | await $http.request({url, method, header, body}) |
$option | 读取用户配置 | $option.apiKey(全是字符串) |
$log | 打印日志(输出到系统控制台) | $log.info("hello") / $log.error("err") |
setTimeout | 异步延时(含 setInterval) | setTimeout(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) |
每个插件有独立的沙盒目录($sandbox),位于插件自己的目录下(<插件目录>/sandbox),$file 只能操作沙盒内的文件,防止目录逃逸。
| 方法 | 说明 |
|---|---|
$file.read(path) | 读取文件,返回字节对象 {__bobBytes: "base64..."} |
$file.write(path, data) | 写入文件(自动创建父目录),data 可以是字符串或字节对象 |
$file.exists(path) | 检查文件是否存在,返回 true / false |
$file.remove(path) | 删除文件 |
$sandbox(自动创建父目录),不能用 .. 或绝对路径。在字符串和字节对象之间转换。字节对象格式:{__bobBytes: "base64编码"}。
| 方法 | 说明 |
|---|---|
$data.fromUTF8(str) | 字符串 → 字节对象 |
$data.toUTF8(byteObj) | 字节对象 → 字符串 |
$data.toBase64(byteObj) | 字节对象 → base64 字符串 |
$data.fromBase64(str) | base64 字符串 → 字节对象 |
$file.read() 拿到字节对象,再 $data.toUTF8() 转成字符串,是最常见的组合。在宿主侧(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,图像按比例内接居中,不会拉伸 |
jpegQuality | 92 | 重编码质量。原图本身是 JPEG 时直通,不做二次压缩 |
minPageSide | 400 | 页面短边下限(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" }) }] });
try / catch 包住并 completion({ error: {...} }) 回报。内置 crypto-js 的 MD5 子集,主要用于 Google 翻译等需要签名的接口。
crypto-js MD5const CryptoJS = require("crypto-js"); const hash = CryptoJS.MD5("hello").toString(); // → "5d41402abc4b2a76b9719d911017c592"
$log 用法$log.info("调试信息"); $log.error("错误信息"); $log.debug("调试详情"); $log.warning("警告");
识别结果里的图片常常是远程链接 —— 编辑器里不一定显示,而且会过期、正文不再自包含。内联成 data: URI 即可解决,标签形如:
内联图片标签<img src="data:image/jpeg;base64,..." alt="image" width="300" height="200" origin-size="300,200" />
$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 可直接喂给 $http 的 files[] 或 $pdf。
把正文里的远程图片批量内联// 并发下载,单张失败保留原链接即可(不要因为一张图丢掉整篇结果) const jobs = urls.map(u => $image.toDataUri(u).catch(() => null)); const got = (await Promise.all(jobs)).filter(Boolean);
crop 直接从原截图裁一块,比缩放服务端返回的图更清晰、尺寸也更准。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); // 每次等待都让出主线程 }
while 空转)或超长同步循环会直接冻住软件:界面不刷新、按钮点不动,直到那段 JS 跑完。等待时间、等待网络结果,一律用 await。❌ 这些写法会卡死界面// 这 1.5 秒里界面完全无响应,事件循环一次都不会跑 const t = Date.now(); while (Date.now() - t < 1500) { } // 死等网络结果 —— 回调没有机会执行,必然死锁 while (!done) { }
await。点「停止」会取消所有挂起的请求与定时器。详细的输入输出格式见第 4 节(识别)和第 5 节(翻译)。
| 场景 | 调用方式 |
|---|---|
| 翻译成功 | completion({ result: { toParagraphs: ["译文"] } }) |
| 翻译成功(词典) | completion({ result: { toDict: {"原词": "译词"} } }) |
| 识别成功 | completion({ result: { texts: [{ text: "文字" }] } }) |
| 失败 | completion({ error: { type: "api", message: "..." } }) |
supportLanguages() 返回的语言码遵循 BCP-47 格式。宿主会自动转换界面上的中文语言名和插件码。
翻译模板里 supportLanguages() 的完整写法(按需增删):
supportLanguagesfunction 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 实际支持的语言即可。| 问题 | 原因 | 解决 |
|---|---|---|
| 请求卡死超时 | 没调 completion() | 所有分支(包括 catch)都要调用 |
| GET 参数丢了 | GET 不发 body | 参数拼到 URL 上 |
| $option 读到 undefined | info.json 没声明 / 键名拼错 | 检查 options[].identifier |
| body 被编码成 form | 没设 Content-Type | 加 "Content-Type": "application/json" |
| require 找不到模块 | 用了绝对路径或 .. | 只用包内相对路径 |
| PNG / 透明图转 PDF 失败 | 插件里手写 PDF 只能内嵌 JPEG | 改用 $pdf.imageToPdf(image)(宿主原生,支持 PNG 等) |
| 上传的 PDF 被判「空白页」 | 页面点数太小(仅极小图片会遇到) | 调大 minPageSide,例如 { minPageSide: 600 } |
| 识别结果里的图片偏大 | 服务端按自己的坐标空间返回裁剪图,不等于原图像素 | 按 原图像素 ÷ 返回的页面尺寸 换算尺寸,或直接用 $image 的 crop 从原图裁剪 |
| 点了识别后软件卡死 | 插件在忙等(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 侧完成) |