{{ imageName }}
+ +diff --git a/plugins/ocr-new/.gitignore b/plugins/ocr-new/.gitignore new file mode 100644 index 00000000..11a2d256 --- /dev/null +++ b/plugins/ocr-new/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist.zip +dist-ssr +*.local +public/local-ocr-runtime/win32/ +local-ocr-runtime/win32/ +.runtime-build/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/plugins/ocr-new/README.md b/plugins/ocr-new/README.md new file mode 100644 index 00000000..b3e2d44e --- /dev/null +++ b/plugins/ocr-new/README.md @@ -0,0 +1,146 @@ +# OCR 翻译 + +识别图片中的文字,并把识别结果跳转到 ZTools 中的翻译指令。 + +这是一个使用 **Vue 3 + Vite + TypeScript** 构建的 ZTools 插件。 + +## 功能特性 + +### OCR 翻译 + +- 触发指令:`OCR` / `文字识别` / `图片翻译` +- 触发指令:`OCR 文字识别+复制` / `OCR 文字识别+翻译` +- 触发指令:`截图文字识别` / `截图文字识别+复制` / `截图文字识别+翻译` +- 匹配指令:图片 → `OCR 文字识别` / `文字识别+复制` / `文字识别+翻译`,图片文件 → `识别图片文字` +- 可在 `OCR 设置` 中固定单个引擎,也可以选择“按优先级”并配置 1-4 顺位 +- 固定单个云端引擎时,失败会直接报错;按优先级模式会按顺位尝试下一个引擎 +- 按优先级模式会跳过未配置密钥的百度、腾讯和 OpenAI-compatible,避免无意义等待 +- 云端请求支持 5-180 秒超时设置,避免长时间卡在某个供应商 +- 如果接口返回 `unknown variant image_url`,说明当前 endpoint 或模型不支持图片输入,需要换视觉模型 +- 可在 `OCR 设置` 中切换为 OCR.Space;留空 API Key 会使用 OCR.Space 测试 key +- 可在 `OCR 设置` 中配置百度 OCR、腾讯云 OCR 密钥,适配常见国内免费试用额度 +- 可在 `OCR 设置` 中配置 OpenAI-compatible 视觉接口,适配云端供应商或自建 DeepSeek-OCR/vLLM 服务 +- 可在设置页一键安装/启动本地 RapidOCR 服务,并选择 `本地 RapidOCR/PaddleOCR` +- 本地 OCR 默认识别中文和英文,图片会先进行放大、灰度和对比度增强后再识别 +- 默认开启文本清理,会修正 `ZTools Al`、`OpenAl`、行首 `。` 误作项目符号等常见误识别;可在设置中关闭 +- 普通 `OCR` 入口只展示识别结果,只有 `+复制`、`+翻译` 命令或按钮会继续执行复制/跳转 +- 翻译流程会复制识别结果,并通过 `window.ztools.redirect()` 跳转到翻译指令 +- 翻译目标默认是 `翻译`,旧的 `翻译文本` 设置会自动迁移为 `翻译` +- 翻译目标也可以改成 `插件标题/指令名`,例如 `翻译插件/翻译` +- OCR 引擎使用 `tesseract.js`,首次识别会加载 OCR 资源和语言数据 + +## 项目结构 + +``` +. +├── public/ +│ ├── logo.png # 插件图标 +│ ├── local-ocr-server/ # 打包进插件的一键安装服务脚本 +│ ├── plugin.json # 插件配置文件 +│ └── preload/ # Preload 脚本目录 +│ ├── package.json # Preload 依赖配置 +│ └── services.js # Node.js 能力扩展 +├── src/ +│ ├── main.ts # 入口文件 +│ ├── main.css # 全局样式 +│ ├── App.vue # 根组件 +│ ├── env.d.ts # 类型声明 +│ └── Ocr/ # OCR 功能组件 +│ └── index.vue +├── index.html # HTML 模板 +├── vite.config.js # Vite 配置 +├── tsconfig.json # TypeScript 配置 +├── package.json # 项目依赖 +└── README.md # 项目文档 +``` + +## 快速开始 + +### 安装依赖 + +```bash +npm install +``` + +### 开发模式 + +```bash +npm run dev +``` + +开发服务器将在 `http://localhost:5173` 启动。ZTools 会自动加载开发版本。 + +### 构建生产版本 + +```bash +npm run build +``` + +构建产物将输出到 `dist/` 目录。 + +## 工作方式 + +图片通过 ZTools 的 `img` / `files` 匹配指令进入插件,文件路径会在 `public/preload/services.js` 中读取为 Data URL,再交给渲染进程中的 `tesseract.js` 识别。插件也支持 `screenCapture()` 截图和 Electron 剪贴板图片读取。 + +识别完成后,插件会调用: + +```ts +window.ztools.copyText(text) +window.ztools.redirect(target, text) +``` + +`target` 可以是单个指令名,例如 `翻译`;也可以是 `插件标题/指令名`,用于精准跳转到指定插件。 + +## 云端 OCR 配置 + +- ZTools AI:在 ZTools 的 AI 模型设置中添加支持图片的模型,然后在插件设置页选择 `ZTools AI`。 +- OCR.Space:到 `ocr.space/ocrapi` 注册免费 API Key;插件设置页里提供了可点击链接,留空会使用测试 Key,仅适合临时验证。 +- 百度 OCR:在百度智能云开通 OCR,填写 API Key 和 Secret Key。 +- 腾讯云 OCR:在腾讯云开通 OCR,填写 SecretId、SecretKey 和地域。 +- OpenAI-compatible:选择支持视觉输入的供应商,填 `Base URL`、模型名和 API Key。 +- DeepSeek-OCR:这是 MIT 开源模型,不是官方免费云 API;自建 vLLM/OpenAI-compatible 服务后,把 endpoint 设为类似 `http://127.0.0.1:8000/v1`,模型名设为 `deepseek-ai/DeepSeek-OCR`。 + +## 本地 RapidOCR/PaddleOCR + +项目内提供了一个轻量本地服务,源码路径为 [local-ocr-server](local-ocr-server)。当前 Windows 构建已把 RapidOCR 运行时打进插件,产物位于 `local-ocr-runtime/win32/`,会随 `dist/` 一起复制到 ZTools 插件目录。安装后可在设置页直接点击 `启动服务` 和 `停止服务`。 + +本地服务默认常驻后台,不会随插件窗口关闭自动退出;这是为了避免每次识别都重新加载模型。也可以在设置页把 `服务策略` 改为 `退出插件时自动停止`。需要手动关闭时,点击 `停止服务`。 + +如果插件已经包含内置 RapidOCR 运行时,用户电脑不需要 Python;设置页会显示 `运行时已内置`。没有内置运行时时,设置页会提示先安装 Python 3,并提供 `一键安装` 作为开发/备用路径。没有 Python 时仍可使用 OCR.Space、百度 OCR、腾讯云 OCR、ZTools AI 或 OpenAI-compatible 视觉接口。 + +```powershell +cd local-ocr-server +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +python server.py +``` + +默认 Endpoint 是 `http://127.0.0.1:8765/ocr`。 + +## 构建与发布 + +### 1. 构建插件 + +```bash +npm run build +``` + +Windows 下如需同时打包内置 RapidOCR 运行时: + +```powershell +npm run build:with-runtime +``` + +当前 Windows runtime 约 265 MB,包含 `rapidocr-server.exe`、ONNX Runtime 和 PP-OCRv4 检测/分类/识别模型。首次启动不再需要下载模型。 + +### 2. 测试构建产物 + +将 `dist/` 目录中的所有文件复制到 ZTools 插件目录进行测试。 + +## 相关资源 + +- [ZTools 官方仓库](https://github.com/ZToolsCenter/ZTools) +- [ZTools API 类型包](https://www.npmjs.com/package/@ztools-center/ztools-api-types) +- [Vue 3 文档](https://vuejs.org/) +- [Vite 文档](https://vitejs.dev/) diff --git a/plugins/ocr-new/index.html b/plugins/ocr-new/index.html new file mode 100644 index 00000000..eaa17316 --- /dev/null +++ b/plugins/ocr-new/index.html @@ -0,0 +1,11 @@ + + +
+ + + + + + + + diff --git a/plugins/ocr-new/local-ocr-server/README.md b/plugins/ocr-new/local-ocr-server/README.md new file mode 100644 index 00000000..5566c545 --- /dev/null +++ b/plugins/ocr-new/local-ocr-server/README.md @@ -0,0 +1,43 @@ +# 本地 OCR 服务 + +这个服务给插件的 `本地 RapidOCR/PaddleOCR` 引擎使用,默认监听 `http://127.0.0.1:8765/ocr`。 + +## 安装 + +```powershell +cd local-ocr-server +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +``` + +## 启动 + +```powershell +python server.py +``` + +启动后在插件设置里选择 `本地 RapidOCR/PaddleOCR`,Endpoint 保持 `http://127.0.0.1:8765/ocr`。插件内一键启动的服务默认常驻后台,也可在设置页改为退出插件时自动停止。 + +如果插件包含 `local-ocr-runtime` 内置运行时,则不需要 Python,设置页会显示运行时已内置。没有内置运行时时,插件设置页会提示先安装 Python;不想安装 Python 时,可改用 OCR.Space、百度、腾讯等云端 OCR。 + +## 接口 + +```http +POST /ocr +Content-Type: application/json + +{ + "image": "data:image/png;base64,...", + "language": "chi_sim+eng" +} +``` + +返回: + +```json +{ + "ok": true, + "text": "识别结果" +} +``` \ No newline at end of file diff --git a/plugins/ocr-new/local-ocr-server/requirements.txt b/plugins/ocr-new/local-ocr-server/requirements.txt new file mode 100644 index 00000000..147dba8c --- /dev/null +++ b/plugins/ocr-new/local-ocr-server/requirements.txt @@ -0,0 +1,2 @@ +rapidocr>=3.8.1 +onnxruntime>=1.18.0 \ No newline at end of file diff --git a/plugins/ocr-new/local-ocr-server/server.py b/plugins/ocr-new/local-ocr-server/server.py new file mode 100644 index 00000000..d3a6da58 --- /dev/null +++ b/plugins/ocr-new/local-ocr-server/server.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import base64 +import json +import re +import tempfile +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any + + +HOST = "127.0.0.1" +PORT = 8765 + + +def load_engine() -> Any: + try: + import onnxruntime # noqa: F401 + from rapidocr_onnxruntime import RapidOCR + except ImportError: + import onnxruntime # noqa: F401 + from rapidocr import RapidOCR + + return RapidOCR() + + +ENGINE = load_engine() + + +def decode_image(data_url: str) -> tuple[bytes, str]: + match = re.match(r"^data:image/([a-zA-Z0-9.+-]+);base64,(.*)$", data_url, re.DOTALL) + if match: + suffix = match.group(1).lower().replace("jpeg", "jpg") + payload = match.group(2) + else: + suffix = "png" + payload = data_url + + return base64.b64decode(payload), suffix + + +def extract_text(result: Any) -> str: + if hasattr(result, "txts"): + return "\n".join(str(text) for text in result.txts if str(text).strip()) + + data = result[0] if isinstance(result, tuple) else result + lines: list[str] = [] + + if isinstance(data, list): + for item in data: + if isinstance(item, (list, tuple)) and len(item) >= 2: + lines.append(str(item[1])) + elif isinstance(item, dict): + text = item.get("text") or item.get("rec_text") or item.get("txt") + if text: + lines.append(str(text)) + + return "\n".join(line.strip() for line in lines if line.strip()) + + +def recognize(data_url: str) -> str: + image_bytes, suffix = decode_image(data_url) + with tempfile.NamedTemporaryFile(suffix=f".{suffix}", delete=False) as image_file: + image_file.write(image_bytes) + image_path = Path(image_file.name) + + try: + return extract_text(ENGINE(str(image_path))) + finally: + image_path.unlink(missing_ok=True) + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self) -> None: + if self.path == "/health": + self.write_json({"ok": True, "engine": "rapidocr"}) + return + self.write_json({"ok": False, "error": "Not found"}, 404) + + def do_POST(self) -> None: + if self.path != "/ocr": + self.write_json({"ok": False, "error": "Not found"}, 404) + return + + try: + content_length = int(self.headers.get("Content-Length", "0")) + body = self.rfile.read(content_length).decode("utf-8") + payload = json.loads(body or "{}") + image = payload.get("image") + if not image: + raise ValueError("missing image") + + self.write_json({"ok": True, "text": recognize(str(image))}) + except Exception as error: # noqa: BLE001 + self.write_json({"ok": False, "error": str(error)}, 500) + + def write_json(self, payload: dict[str, Any], status: int = 200) -> None: + data = json.dumps(payload, ensure_ascii=False).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def log_message(self, format: str, *args: Any) -> None: + print(f"[{self.log_date_time_string()}] {format % args}") + + +def main() -> None: + server = ThreadingHTTPServer((HOST, PORT), Handler) + print(f"Local OCR server listening on http://{HOST}:{PORT}/ocr") + server.serve_forever() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/plugins/ocr-new/package-lock.json b/plugins/ocr-new/package-lock.json new file mode 100644 index 00000000..6a781b42 --- /dev/null +++ b/plugins/ocr-new/package-lock.json @@ -0,0 +1,1623 @@ +{ + "name": "ocr-new", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ocr-new", + "version": "1.0.0", + "dependencies": { + "tesseract.js": "^7.0.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "@ztools-center/ztools-api-types": "^1.0.1", + "typescript": "^5.3.0", + "vite": "^6.0.11", + "vue-tsc": "^2.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "license": "MIT" + }, + "node_modules/@ztools-center/ztools-api-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@ztools-center/ztools-api-types/-/ztools-api-types-1.0.3.tgz", + "integrity": "sha512-dv1eOAIasAupqKaQL/gESk1i2+RebdM/1gvZhrvH2D/bo3enCUsAGJ8nrHnlCLBSOGB81eC/SU0IH8BNsUlmvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tesseract.js": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-7.0.0.tgz", + "integrity": "sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bmp-js": "^0.1.0", + "idb-keyval": "^6.2.0", + "is-url": "^1.2.4", + "node-fetch": "^2.6.9", + "opencollective-postinstall": "^2.0.3", + "regenerator-runtime": "^0.13.3", + "tesseract.js-core": "^7.0.0", + "wasm-feature-detect": "^1.8.0", + "zlibjs": "^0.3.1" + } + }, + "node_modules/tesseract.js-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-7.0.0.tgz", + "integrity": "sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw==", + "license": "Apache-2.0" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/zlibjs": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", + "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", + "license": "MIT", + "engines": { + "node": "*" + } + } + } +} diff --git a/plugins/ocr-new/package.json b/plugins/ocr-new/package.json new file mode 100644 index 00000000..4234f349 --- /dev/null +++ b/plugins/ocr-new/package.json @@ -0,0 +1,23 @@ +{ + "name": "ocr-new", + "version": "1.0.0", + "description": "", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "build:runtime:win": "powershell -ExecutionPolicy Bypass -File scripts/build-local-ocr-runtime.ps1", + "build:with-runtime": "npm run build:runtime:win && npm run build" + }, + "dependencies": { + "tesseract.js": "^7.0.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "@ztools-center/ztools-api-types": "^1.0.1", + "typescript": "^5.3.0", + "vite": "^6.0.11", + "vue-tsc": "^2.0.0" + } +} diff --git a/plugins/ocr-new/public/local-ocr-runtime/README.md b/plugins/ocr-new/public/local-ocr-runtime/README.md new file mode 100644 index 00000000..cfc6917b --- /dev/null +++ b/plugins/ocr-new/public/local-ocr-runtime/README.md @@ -0,0 +1,5 @@ +# Local OCR Runtime + +This folder is copied into the ZTools plugin build. On Windows, `scripts/build-local-ocr-runtime.ps1` creates `win32/rapidocr-server/rapidocr-server.exe` here. + +When the executable exists, the plugin starts it directly and does not require the end user to install Python. \ No newline at end of file diff --git a/plugins/ocr-new/public/local-ocr-runtime/runtime-info.json b/plugins/ocr-new/public/local-ocr-runtime/runtime-info.json new file mode 100644 index 00000000..3883d245 --- /dev/null +++ b/plugins/ocr-new/public/local-ocr-runtime/runtime-info.json @@ -0,0 +1,6 @@ +{ + "platform": "win32", + "kind": "pyinstaller-onedir", + "entry": "win32/rapidocr-server/rapidocr-server.exe", + "builtAt": "2026-05-16T04:21:02" +} diff --git a/plugins/ocr-new/public/local-ocr-server/README.md b/plugins/ocr-new/public/local-ocr-server/README.md new file mode 100644 index 00000000..4c5f7eae --- /dev/null +++ b/plugins/ocr-new/public/local-ocr-server/README.md @@ -0,0 +1,13 @@ +# 本地 OCR 服务 + +这个服务给插件的 `本地 RapidOCR/PaddleOCR` 引擎使用,默认监听 `http://127.0.0.1:8765/ocr`。 + +安装到 ZTools 后,可在插件设置页点击 `一键安装`、`启动服务` 和 `停止服务`,也可以选择退出插件时自动停止。 + +如果插件包含 `local-ocr-runtime` 内置运行时,则不需要 Python。没有内置运行时时,先安装 Python,或改用 OCR.Space、百度、腾讯等云端 OCR。 + +手动启动: + +```powershell +python server.py +``` \ No newline at end of file diff --git a/plugins/ocr-new/public/local-ocr-server/requirements.txt b/plugins/ocr-new/public/local-ocr-server/requirements.txt new file mode 100644 index 00000000..147dba8c --- /dev/null +++ b/plugins/ocr-new/public/local-ocr-server/requirements.txt @@ -0,0 +1,2 @@ +rapidocr>=3.8.1 +onnxruntime>=1.18.0 \ No newline at end of file diff --git a/plugins/ocr-new/public/local-ocr-server/server.py b/plugins/ocr-new/public/local-ocr-server/server.py new file mode 100644 index 00000000..d3a6da58 --- /dev/null +++ b/plugins/ocr-new/public/local-ocr-server/server.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import base64 +import json +import re +import tempfile +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any + + +HOST = "127.0.0.1" +PORT = 8765 + + +def load_engine() -> Any: + try: + import onnxruntime # noqa: F401 + from rapidocr_onnxruntime import RapidOCR + except ImportError: + import onnxruntime # noqa: F401 + from rapidocr import RapidOCR + + return RapidOCR() + + +ENGINE = load_engine() + + +def decode_image(data_url: str) -> tuple[bytes, str]: + match = re.match(r"^data:image/([a-zA-Z0-9.+-]+);base64,(.*)$", data_url, re.DOTALL) + if match: + suffix = match.group(1).lower().replace("jpeg", "jpg") + payload = match.group(2) + else: + suffix = "png" + payload = data_url + + return base64.b64decode(payload), suffix + + +def extract_text(result: Any) -> str: + if hasattr(result, "txts"): + return "\n".join(str(text) for text in result.txts if str(text).strip()) + + data = result[0] if isinstance(result, tuple) else result + lines: list[str] = [] + + if isinstance(data, list): + for item in data: + if isinstance(item, (list, tuple)) and len(item) >= 2: + lines.append(str(item[1])) + elif isinstance(item, dict): + text = item.get("text") or item.get("rec_text") or item.get("txt") + if text: + lines.append(str(text)) + + return "\n".join(line.strip() for line in lines if line.strip()) + + +def recognize(data_url: str) -> str: + image_bytes, suffix = decode_image(data_url) + with tempfile.NamedTemporaryFile(suffix=f".{suffix}", delete=False) as image_file: + image_file.write(image_bytes) + image_path = Path(image_file.name) + + try: + return extract_text(ENGINE(str(image_path))) + finally: + image_path.unlink(missing_ok=True) + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self) -> None: + if self.path == "/health": + self.write_json({"ok": True, "engine": "rapidocr"}) + return + self.write_json({"ok": False, "error": "Not found"}, 404) + + def do_POST(self) -> None: + if self.path != "/ocr": + self.write_json({"ok": False, "error": "Not found"}, 404) + return + + try: + content_length = int(self.headers.get("Content-Length", "0")) + body = self.rfile.read(content_length).decode("utf-8") + payload = json.loads(body or "{}") + image = payload.get("image") + if not image: + raise ValueError("missing image") + + self.write_json({"ok": True, "text": recognize(str(image))}) + except Exception as error: # noqa: BLE001 + self.write_json({"ok": False, "error": str(error)}, 500) + + def write_json(self, payload: dict[str, Any], status: int = 200) -> None: + data = json.dumps(payload, ensure_ascii=False).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def log_message(self, format: str, *args: Any) -> None: + print(f"[{self.log_date_time_string()}] {format % args}") + + +def main() -> None: + server = ThreadingHTTPServer((HOST, PORT), Handler) + print(f"Local OCR server listening on http://{HOST}:{PORT}/ocr") + server.serve_forever() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/plugins/ocr-new/public/logo.png b/plugins/ocr-new/public/logo.png new file mode 100644 index 00000000..46e3edd9 Binary files /dev/null and b/plugins/ocr-new/public/logo.png differ diff --git a/plugins/ocr-new/public/logo.svg b/plugins/ocr-new/public/logo.svg new file mode 100644 index 00000000..cb1dbb51 --- /dev/null +++ b/plugins/ocr-new/public/logo.svg @@ -0,0 +1,42 @@ + diff --git a/plugins/ocr-new/public/plugin.json b/plugins/ocr-new/public/plugin.json new file mode 100644 index 00000000..8a0935d2 --- /dev/null +++ b/plugins/ocr-new/public/plugin.json @@ -0,0 +1,120 @@ +{ + "name": "ocr-new", + "title": "OCR 翻译", + "description": "识别图片文字并跳转到翻译插件", + "author": "FAR", + "version": "1.0.0", + "main": "index.html", + "preload": "preload/services.js", + "logo": "logo.png", + "pluginSetting": { + "single": false + }, + "development": { + "main": "http://localhost:5173" + }, + "features": [ + { + "code": "ocr", + "explain": "图片文字识别", + "icon": "logo.png", + "cmds": [ + "OCR", + "OCR 文字识别", + "文字识别", + "图片翻译", + { + "type": "files", + "fileType": "file", + "maxLength": 1, + "extensions": [ + "png", + "jpg", + "jpeg", + "webp", + "bmp", + "gif", + "tif", + "tiff", + "avif" + ], + "label": "识别图片文字" + }, + { + "type": "img", + "label": "OCR 文字识别" + } + ] + }, + { + "code": "ocr-settings", + "explain": "OCR 插件设置", + "icon": "logo.png", + "cmds": [ + "OCR 设置", + "OCR 插件设置" + ] + }, + { + "code": "ocr-copy", + "mainHide": true, + "explain": "OCR 文字识别后自动复制识别内容", + "icon": "logo.png", + "cmds": [ + "OCR 文字识别+复制", + { + "type": "img", + "label": "文字识别+复制" + } + ] + }, + { + "code": "ocr-translate", + "mainHide": true, + "explain": "OCR 文字识别后自动前往翻译识别内容", + "icon": "logo.png", + "cmds": [ + "OCR 文字识别+翻译", + { + "type": "img", + "label": "文字识别+翻译" + } + ] + }, + { + "code": "ocr-screenshot", + "mainHide": true, + "explain": "截图 OCR 文字识别", + "icon": "logo.png", + "cmds": [ + "截图文字识别", + "截图 OCR 识别" + ] + }, + { + "code": "ocr-screenshot-copy", + "mainHide": true, + "explain": "截图 OCR 文字识别后自动复制识别内容", + "icon": "logo.png", + "cmds": [ + "截图文字识别+复制", + "截图 OCR 复制" + ] + }, + { + "code": "ocr-screenshot-translate", + "mainHide": true, + "explain": "截图 OCR 文字识别后自动前往翻译识别内容", + "icon": "logo.png", + "cmds": [ + "截图文字识别+翻译", + "截图 OCR 翻译" + ] + } + ], + "platform": [ + "darwin", + "win32", + "linux" + ] +} \ No newline at end of file diff --git a/plugins/ocr-new/public/preload/package.json b/plugins/ocr-new/public/preload/package.json new file mode 100644 index 00000000..5bbefffb --- /dev/null +++ b/plugins/ocr-new/public/preload/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/plugins/ocr-new/public/preload/services.js b/plugins/ocr-new/public/preload/services.js new file mode 100644 index 00000000..2490192e --- /dev/null +++ b/plugins/ocr-new/public/preload/services.js @@ -0,0 +1,350 @@ +const fs = require('node:fs') +const path = require('node:path') +const crypto = require('node:crypto') +const { spawn } = require('node:child_process') +const { clipboard } = require('electron') + +const imageMimeTypes = { + avif: 'image/avif', + bmp: 'image/bmp', + gif: 'image/gif', + jpeg: 'image/jpeg', + jpg: 'image/jpeg', + png: 'image/png', + tif: 'image/tiff', + tiff: 'image/tiff', + webp: 'image/webp' +} + +const ocrSpaceLanguages = { + 'chi_sim+eng': 'chs', + chi_sim: 'chs', + eng: 'eng' +} + +const normalizeOpenAiEndpoint = (endpoint) => { + const trimmed = String(endpoint || '').trim().replace(/\/+$/, '') + if (!trimmed) throw new Error('未配置 OpenAI-compatible Endpoint') + if (trimmed.endsWith('/chat/completions')) return trimmed + if (trimmed.endsWith('/v1')) return `${trimmed}/chat/completions` + return `${trimmed}/v1/chat/completions` +} + +const normalizeLocalOcrEndpoint = (endpoint) => { + const trimmed = String(endpoint || '').trim().replace(/\/+$/, '') + if (!trimmed) return 'http://127.0.0.1:8765/ocr' + return trimmed +} + +const normalizeTimeoutMs = (timeoutMs) => { + const value = Number(timeoutMs) + if (!Number.isFinite(value)) return 30000 + return Math.max(5000, Math.min(180000, value)) +} + +const requestWithTimeout = async (url, options, timeoutMs) => { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), normalizeTimeoutMs(timeoutMs)) + try { + return await fetch(url, { + ...options, + signal: controller.signal + }) + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`请求超时(${Math.round(normalizeTimeoutMs(timeoutMs) / 1000)} 秒)`) + } + throw error + } finally { + clearTimeout(timer) + } +} + +const normalizeVisionError = (message) => { + const text = String(message || '') + if (text.includes('unknown variant `image_url`') || text.includes('expected `text`')) { + return '当前接口或模型不支持图片输入(image_url),请换支持视觉的模型' + } + return text +} + +const stripImageBase64 = (base64Url) => String(base64Url || '').replace(/^data:image\/[a-zA-Z0-9.+-]+;base64,/, '') + +const sha256 = (content, encoding = 'hex') => crypto.createHash('sha256').update(content).digest(encoding) + +const hmac = (key, content, encoding) => crypto.createHmac('sha256', key).update(content).digest(encoding) + +const getServerDir = () => path.resolve(__dirname, '..', 'local-ocr-server') + +const getPidFile = () => path.join(getServerDir(), '.server.pid') + +const getBundledRuntime = () => { + const executableName = process.platform === 'win32' ? 'rapidocr-server.exe' : 'rapidocr-server' + const runtimeRoot = path.resolve(__dirname, '..', 'local-ocr-runtime', process.platform) + const candidates = [ + path.join(runtimeRoot, executableName), + path.join(runtimeRoot, 'rapidocr-server', executableName) + ] + const executablePath = candidates.find((candidate) => fs.existsSync(candidate)) + if (!executablePath) return null + return { + executablePath, + cwd: path.dirname(executablePath) + } +} + +const startDetached = (command, args, cwd) => { + const child = spawn(command, args, { + cwd, + detached: true, + stdio: 'ignore', + windowsHide: true + }) + child.unref() + return child.pid +} + +// 通过 window 对象向渲染进程注入 nodejs 能力 +window.services = { + // 读文件 + readFile(file) { + return fs.readFileSync(file, { encoding: 'utf-8' }) + }, + // 图片读取为 Data URL,供 OCR 渲染进程使用 + readImageAsDataUrl(file) { + const ext = path.extname(file).slice(1).toLowerCase() + const mimeType = imageMimeTypes[ext] || 'application/octet-stream' + const base64 = fs.readFileSync(file).toString('base64') + return { + dataUrl: `data:${mimeType};base64,${base64}`, + name: path.basename(file), + path: file + } + }, + // 读取系统剪贴板中的图片 + getCopyedImage() { + const image = clipboard.readImage() + return !image || image.isEmpty() ? null : image.toDataURL() + }, + async ocrSpaceRecognize(options) { + const body = new URLSearchParams() + body.set('base64Image', options.base64Url) + body.set('language', ocrSpaceLanguages[options.language] || 'chs') + body.set('isOverlayRequired', 'false') + body.set('scale', 'true') + body.set('OCREngine', '2') + + const response = await requestWithTimeout('https://api.ocr.space/parse/image', { + method: 'POST', + headers: { + apikey: options.apiKey || 'helloworld', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body + }, options.timeoutMs) + if (!response.ok) { + throw new Error(`OCR.Space 请求失败:${response.status}`) + } + + const result = await response.json() + if (result.IsErroredOnProcessing) { + const message = Array.isArray(result.ErrorMessage) ? result.ErrorMessage.join('\n') : result.ErrorMessage + throw new Error(message || 'OCR.Space 识别失败') + } + + return (result.ParsedResults || []) + .map((item) => item.ParsedText || '') + .filter(Boolean) + .join('\n') + }, + async openAiVisionRecognize(options) { + const endpoint = normalizeOpenAiEndpoint(options.endpoint) + const headers = { + 'Content-Type': 'application/json' + } + if (options.apiKey) { + headers.Authorization = `Bearer ${options.apiKey}` + } + + const response = await requestWithTimeout(endpoint, { + method: 'POST', + headers, + body: JSON.stringify({ + model: options.model, + temperature: 0, + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: options.prompt + }, + { + type: 'image_url', + image_url: { + url: options.base64Url, + detail: 'high' + } + } + ] + } + ] + }) + }, options.timeoutMs) + + const result = await response.json().catch(() => ({})) + if (!response.ok) { + const message = result?.error?.message || result?.message || `OpenAI-compatible 请求失败:${response.status}` + throw new Error(normalizeVisionError(message)) + } + + return result?.choices?.[0]?.message?.content || result?.output_text || '' + }, + async baiduOcrRecognize(options) { + if (!options.apiKey || !options.secretKey) throw new Error('未配置百度 OCR API Key / Secret Key') + const tokenResponse = await requestWithTimeout( + `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${encodeURIComponent(options.apiKey)}&client_secret=${encodeURIComponent(options.secretKey)}`, + { method: 'POST' }, + options.timeoutMs + ) + const tokenResult = await tokenResponse.json().catch(() => ({})) + if (!tokenResponse.ok || !tokenResult.access_token) { + throw new Error(tokenResult.error_description || tokenResult.error || '百度 OCR access_token 获取失败') + } + + const body = new URLSearchParams() + body.set('image', stripImageBase64(options.base64Url)) + body.set('language_type', options.language === 'eng' ? 'ENG' : 'CHN_ENG') + body.set('detect_direction', 'true') + body.set('paragraph', 'true') + + const response = await requestWithTimeout( + `https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token=${encodeURIComponent(tokenResult.access_token)}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body + }, + options.timeoutMs + ) + const result = await response.json().catch(() => ({})) + if (!response.ok || result.error_code) { + throw new Error(result.error_msg || `百度 OCR 请求失败:${response.status}`) + } + return (result.words_result || []).map((item) => item.words || '').filter(Boolean).join('\n') + }, + async tencentOcrRecognize(options) { + if (!options.secretId || !options.secretKey) throw new Error('未配置腾讯云 SecretId / SecretKey') + const host = 'ocr.tencentcloudapi.com' + const service = 'ocr' + const action = 'GeneralBasicOCR' + const version = '2018-11-19' + const region = options.region || 'ap-guangzhou' + const timestamp = Math.floor(Date.now() / 1000) + const date = new Date(timestamp * 1000).toISOString().slice(0, 10) + const payload = JSON.stringify({ ImageBase64: stripImageBase64(options.base64Url) }) + const hashedPayload = sha256(payload) + const canonicalHeaders = `content-type:application/json; charset=utf-8\nhost:${host}\nx-tc-action:${action.toLowerCase()}\n` + const signedHeaders = 'content-type;host;x-tc-action' + const canonicalRequest = `POST\n/\n\n${canonicalHeaders}\n${signedHeaders}\n${hashedPayload}` + const credentialScope = `${date}/${service}/tc3_request` + const stringToSign = `TC3-HMAC-SHA256\n${timestamp}\n${credentialScope}\n${sha256(canonicalRequest)}` + const secretDate = hmac(`TC3${options.secretKey}`, date) + const secretService = hmac(secretDate, service) + const secretSigning = hmac(secretService, 'tc3_request') + const signature = hmac(secretSigning, stringToSign, 'hex') + const authorization = `TC3-HMAC-SHA256 Credential=${options.secretId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}` + + const response = await requestWithTimeout(`https://${host}`, { + method: 'POST', + headers: { + Authorization: authorization, + 'Content-Type': 'application/json; charset=utf-8', + Host: host, + 'X-TC-Action': action, + 'X-TC-Timestamp': String(timestamp), + 'X-TC-Version': version, + 'X-TC-Region': region + }, + body: payload + }, options.timeoutMs) + + const result = await response.json().catch(() => ({})) + const payloadResult = result.Response || {} + if (!response.ok || payloadResult.Error) { + throw new Error(payloadResult.Error?.Message || `腾讯云 OCR 请求失败:${response.status}`) + } + return (payloadResult.TextDetections || []).map((item) => item.DetectedText || '').filter(Boolean).join('\n') + }, + async localHttpRecognize(options) { + const response = await requestWithTimeout(normalizeLocalOcrEndpoint(options.endpoint), { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + image: options.base64Url, + language: options.language + }) + }, options.timeoutMs) + + const result = await response.json().catch(() => ({})) + if (!response.ok || result?.ok === false) { + throw new Error(result?.error || `本地 OCR 服务请求失败:${response.status}`) + } + + return result?.text || '' + }, + async getLocalOcrStatus(options = {}) { + const bundledRuntime = getBundledRuntime() + const runtimeBundled = !!bundledRuntime + try { + const endpoint = normalizeLocalOcrEndpoint(options.endpoint) + const healthUrl = endpoint.endsWith('/ocr') ? endpoint.slice(0, -4) + '/health' : endpoint.replace(/\/+$/, '') + '/health' + const response = await requestWithTimeout(healthUrl, { method: 'GET' }, 2000) + const result = await response.json().catch(() => ({})) + return { running: !!result.ok, runtimeBundled } + } catch (_error) { + return { running: false, runtimeBundled } + } + }, + async startLocalOcr() { + const bundledRuntime = getBundledRuntime() + if (!bundledRuntime) throw new Error(`当前平台 (${process.platform}) 未集成 RapidOCR 运行时`) + const pid = startDetached(bundledRuntime.executablePath, [], bundledRuntime.cwd) + if (pid) fs.writeFileSync(getPidFile(), String(pid), { encoding: 'utf-8' }) + return { ok: true } + }, + async stopLocalOcr() { + const pidFile = getPidFile() + if (!fs.existsSync(pidFile)) return { ok: true, stopped: false } + const pid = Number(fs.readFileSync(pidFile, { encoding: 'utf-8' })) + if (Number.isFinite(pid) && pid > 0) { + try { + process.kill(pid) + } catch (_error) { + // 进程可能已经退出,只清理 pid 文件 + } + } + fs.rmSync(pidFile, { force: true }) + return { ok: true, stopped: true } + }, + // 文本写入到下载目录 + writeTextFile(text) { + const filePath = path.join(window.ztools.getPath('downloads'), Date.now().toString() + '.txt') + fs.writeFileSync(filePath, text, { encoding: 'utf-8' }) + return filePath + }, + // 图片写入到下载目录 + writeImageFile(base64Url) { + const matchs = /^data:image\/([a-z]{1,20});base64,/i.exec(base64Url) + if (!matchs) return + const filePath = path.join( + window.ztools.getPath('downloads'), + Date.now().toString() + '.' + matchs[1] + ) + fs.writeFileSync(filePath, base64Url.substring(matchs[0].length), { encoding: 'base64' }) + return filePath + } +} diff --git a/plugins/ocr-new/scripts/build-local-ocr-runtime.ps1 b/plugins/ocr-new/scripts/build-local-ocr-runtime.ps1 new file mode 100644 index 00000000..7e6f8d54 --- /dev/null +++ b/plugins/ocr-new/scripts/build-local-ocr-runtime.ps1 @@ -0,0 +1,88 @@ +$ErrorActionPreference = "Stop" + +$ProjectRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$BuildRoot = Join-Path $ProjectRoot ".runtime-build" +$VenvDir = Join-Path $BuildRoot ".venv" +$VenvPython = Join-Path $VenvDir "Scripts\python.exe" +$ServerScript = Join-Path $ProjectRoot "local-ocr-server\server.py" +$Requirements = Join-Path $ProjectRoot "local-ocr-server\requirements.txt" +$RuntimeRoot = Join-Path $ProjectRoot "public\local-ocr-runtime\win32" +$WorkPath = Join-Path $BuildRoot "pyinstaller-work" +$SpecPath = Join-Path $BuildRoot "pyinstaller-spec" + +function Invoke-FirstPython { + param([string[]]$Arguments) + + $candidates = @( + @{ Command = "py"; Args = @("-3") }, + @{ Command = "python"; Args = @() }, + @{ Command = "python3"; Args = @() } + ) + + foreach ($candidate in $candidates) { + try { + & $candidate.Command @($candidate.Args + $Arguments) + if ($LASTEXITCODE -eq 0) { return } + } catch { + } + } + + throw "Python 3 was not found. Install Python 3 to build the bundled runtime." +} + +function Test-PythonModule { + param([string]$ModuleName) + + & $VenvPython -c "import importlib.util; raise SystemExit(0 if importlib.util.find_spec('$ModuleName') else 1)" *> $null + return $LASTEXITCODE -eq 0 +} + +New-Item -ItemType Directory -Force -Path $BuildRoot | Out-Null +New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null + +if (!(Test-Path $VenvPython)) { + Invoke-FirstPython -Arguments @("-m", "venv", $VenvDir) +} + +& $VenvPython -m pip install --upgrade pip +& $VenvPython -m pip install -r $Requirements pyinstaller + +$collectArgs = @() +foreach ($module in @("rapidocr", "rapidocr_onnxruntime", "onnxruntime", "cv2", "numpy", "PIL", "pyclipper", "shapely")) { + if (Test-PythonModule $module) { + $collectArgs += @("--collect-all", $module) + } +} + +$existingRuntime = Join-Path $RuntimeRoot "rapidocr-server" +if (Test-Path $existingRuntime) { + Remove-Item -Recurse -Force $existingRuntime +} + +& $VenvPython -m PyInstaller ` + --noconfirm ` + --clean ` + --onedir ` + --name rapidocr-server ` + --distpath $RuntimeRoot ` + --workpath $WorkPath ` + --specpath $SpecPath ` + --hidden-import rapidocr_onnxruntime ` + --hidden-import rapidocr ` + --hidden-import onnxruntime ` + --hidden-import onnxruntime.capi._pybind_state ` + --hidden-import onnxruntime.capi.onnxruntime_pybind11_state ` + --collect-submodules onnxruntime ` + --collect-binaries onnxruntime ` + @collectArgs ` + $ServerScript + +$info = [ordered]@{ + platform = "win32" + kind = "pyinstaller-onedir" + entry = "win32/rapidocr-server/rapidocr-server.exe" + builtAt = (Get-Date).ToString("s") +} +$info | ConvertTo-Json | Set-Content -Encoding UTF8 (Join-Path (Split-Path $RuntimeRoot -Parent) "runtime-info.json") + +Write-Host "Bundled RapidOCR runtime built at: $RuntimeRoot" \ No newline at end of file diff --git a/plugins/ocr-new/scripts/build-local-ocr-runtime.sh b/plugins/ocr-new/scripts/build-local-ocr-runtime.sh new file mode 100644 index 00000000..7eb3b4ee --- /dev/null +++ b/plugins/ocr-new/scripts/build-local-ocr-runtime.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +PLATFORM="$(uname -s | tr '[:upper:]' '[:lower:]')" +RUNTIME_DIR="$PROJECT_DIR/public/local-ocr-runtime/$PLATFORM/rapidocr-server" +SERVER_SRC="$PROJECT_DIR/local-ocr-server" +BUILD_DIR="$PROJECT_DIR/.runtime-build" + +echo "=== 构建 RapidOCR 运行时:$PLATFORM ===" + +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR" + +echo "[1/4] 创建 Python 虚拟环境 ..." +python3 -m venv "$BUILD_DIR/.venv" +source "$BUILD_DIR/.venv/bin/activate" + +echo "[2/4] 安装依赖 ..." +pip install --upgrade pip +pip install rapidocr>=3.8.1 onnxruntime>=1.18.0 pyinstaller + +echo "[3/4] PyInstaller 打包 ..." +pyinstaller \ + --onedir \ + --name rapidocr-server \ + --add-data "$(python -c "import rapidocr; print(rapidocr.__path__[0])")"/models:rapidocr/models \ + --hidden-import rapidocr_onnxruntime \ + --hidden-import rapidocr \ + --hidden-import onnxruntime \ + --hidden-import onnxruntime.capi \ + --hidden-import cv2 \ + --hidden-import numpy \ + --hidden-import PIL \ + --hidden-import PIL.Image \ + --hidden-import pyclipper \ + --hidden-import shapely \ + --noconsole \ + "$SERVER_SRC/server.py" + +deactivate + +echo "[4/4] 复制产物到 $RUNTIME_DIR ..." +rm -rf "$RUNTIME_DIR" +mkdir -p "$RUNTIME_DIR" +cp -r "$BUILD_DIR/dist/rapidocr-server/"* "$RUNTIME_DIR/" + +echo "" +echo "=== 构建完成 ===" +echo "产物路径: $RUNTIME_DIR" +echo "平台: $PLATFORM" +echo "" +echo "提示:将此目录随插件一起分发,ZTools 会自动加载当前平台的运行时。" diff --git a/plugins/ocr-new/src/App.vue b/plugins/ocr-new/src/App.vue new file mode 100644 index 00000000..96099576 --- /dev/null +++ b/plugins/ocr-new/src/App.vue @@ -0,0 +1,23 @@ + + + +{{ imageName }}
+ ++ ZTools AI:在 ZTools AI 模型里添加支持图片的模型,然后选择“ZTools AI”。 +
++ OCR.Space:访问 + + ocr.space/ocrapi + + 注册免费 Key;留空会使用测试 Key,适合临时验证。 +
++ 百度 / 腾讯:开通对应云 OCR 服务后填入密钥,通常有新用户免费额度或试用额度。 +
++ 云端视觉模型:选择支持 OpenAI-compatible 的供应商,填 Base URL、模型名和 API Key。 +
++ DeepSeek-OCR:它是开源模型,不是官方免费 API;自建 vLLM 服务后填 + http://127.0.0.1:8000/v1 和模型名 + deepseek-ai/DeepSeek-OCR。 +
++ 本地 RapidOCR:插件已内置 RapidOCR 运行时,无需额外安装,可直接启动本地 OCR 服务。 + 不想启动本地服务时,可以使用 OCR.Space / 百度 / 腾讯等云端 OCR。 +
+