-
Notifications
You must be signed in to change notification settings - Fork 262
feat: 新增 Provider 抽象系统支持插件提供翻译和 OCR 能力 #560
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Particaly
wants to merge
2
commits into
ZToolsCenter:main
Choose a base branch
from
Particaly:feature/third-part-function-provider
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Provider 示例插件 | ||
|
|
||
| 这是一个最小可参考的 provider 插件骨架,演示如何通过 `providers` 声明 + `ztools.registerProvider` 接入「翻译」与「OCR」能力。 | ||
|
|
||
| > 此目录为文档示例,handler 使用 mock 实现,不可作为正式插件直接安装运行。真实接入请参考 `docs/provider-development-guide.md` 替换 handler 逻辑。 | ||
|
|
||
| ## 文件说明 | ||
|
|
||
| - `plugin.json` —— 声明了 `providers.translation` 与 `providers.ocr`。 | ||
| - `preload.js` —— 调用 `ztools.registerProvider` 注册两个 provider 的 mock 实现。 | ||
|
|
||
| ## 接入后会怎样 | ||
|
|
||
| 安装声明了 `providers` 的插件后: | ||
|
|
||
| 1. 「设置 → 提供商」的「翻译」「OCR」tab 会自动列出该 provider。 | ||
| 2. 用户可启用 / 设为默认。 | ||
| 3. 消费方(如超级面板选中翻译)调用 `providerManager.invoke(type, input)` 时会路由到默认 provider。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "name": "provider-example", | ||
| "title": "Provider 示例", | ||
| "description": "演示如何通过 providers + registerProvider 提供翻译与 OCR 能力(仅示例,handler 为 mock 实现)", | ||
| "version": "1.0.0", | ||
| "main": "index.html", | ||
| "preload": "preload.js", | ||
| "author": "ZTools", | ||
| "logo": "logo.png", | ||
| "features": [ | ||
| { | ||
| "code": "demo", | ||
| "explain": "Provider 示例(无实际功能,仅用于展示 providers 声明)", | ||
| "cmds": ["provider 示例"] | ||
| } | ||
| ], | ||
| "providers": { | ||
| "translation": { | ||
| "type": "translation", | ||
| "label": "示例翻译", | ||
| "description": "Mock 翻译 provider,仅用于演示接入流程" | ||
| }, | ||
| "ocr": { | ||
| "type": "ocr", | ||
| "label": "示例 OCR", | ||
| "description": "Mock OCR provider,仅用于演示接入流程" | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /** | ||
| * Provider 示例插件 preload | ||
| * | ||
| * 演示如何按契约注册 translation / ocr 两个 provider。 | ||
| * 这里用 mock 实现演示接入流程;真实插件请替换为对自身服务的调用。 | ||
| */ | ||
|
|
||
| // 翻译 provider:入参 { text, from?, to? },返回 { text, detectedFrom? } | ||
| ztools.registerProvider('translation', async (input) => { | ||
| const { text } = input | ||
| // === mock:真实场景替换为你的翻译 API 调用 === | ||
| return { | ||
| text: `[示例翻译] ${text}`, | ||
| detectedFrom: 'auto' | ||
| } | ||
| }) | ||
|
|
||
| // OCR provider:入参 { image, lang? },返回 { text, blocks?, confidence? } | ||
| ztools.registerProvider('ocr', async (input) => { | ||
| const { image } = input | ||
| // === mock:真实场景替换为你的 OCR API 调用(image 可为路径/dataURI/URL) === | ||
| console.log('[provider-example] ocr called with image:', image) | ||
| return { | ||
| text: '[示例 OCR 结果] 此处为识别到的文本', | ||
| blocks: ['[示例 OCR 结果] 此处为识别到的文本'], | ||
| confidence: 0.99 | ||
| } | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| # Provider(提供商)开发指南 | ||
|
|
||
| ZTools 把「翻译」「OCR」等能力抽象为 **Provider(提供商)**。主程序不再硬编码这些能力的实现,而是由插件按统一契约提供,主程序负责聚合、展示与调用。 | ||
|
|
||
| > AI 模型不纳入 provider 抽象,仍走独立的 AI 模型配置。 | ||
|
|
||
| --- | ||
|
|
||
| ## 支持的 Provider 类型 | ||
|
|
||
| | type | 说明 | 入参 | 返回 | | ||
| | ------------- | ------------ | ---------------------- | -------------------------------- | | ||
| | `translation` | 文本翻译 | `{ text, from?, to? }` | `{ text, detectedFrom? }` | | ||
| | `ocr` | 图片文字识别 | `{ image, lang? }` | `{ text, blocks?, confidence? }` | | ||
|
|
||
| - `translation.image` / `ocr.image` 可为:本地路径 / `data:` URI / `http(s)` URL(具体支持取决于实现)。 | ||
| - 完整契约定义见主程序源码 `src/shared/providerShared.ts`。 | ||
|
|
||
| --- | ||
|
|
||
| ## 第一步:在 plugin.json 声明 providers | ||
|
|
||
| 在插件的 `plugin.json` 中新增 `providers` 字段,声明本插件提供哪些 type: | ||
|
|
||
| ```json | ||
| { | ||
| "name": "my-cloud-ocr", | ||
| "title": "云 OCR", | ||
| "providers": { | ||
| "ocr": { | ||
| "type": "ocr", | ||
| "label": "云 OCR", | ||
| "description": "基于云端 API 的图片文字识别" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| - 一个插件可同时声明多个 type(如同时提供 `translation` 和 `ocr`)。 | ||
| - `label` / `description` 会展示在「设置 → 提供商」对应 tab 中。 | ||
|
|
||
| 声明后,安装该插件即在「设置 → 提供商 → OCR/翻译」tab 列出该 provider,用户可启用并设为默认。 | ||
|
|
||
| --- | ||
|
|
||
| ## 第二步:在 preload 注册 handler | ||
|
|
||
| 在插件 preload 脚本中调用 `ztools.registerProvider(type, handler)`,handler 签名必须匹配该 type 的契约: | ||
|
|
||
| ```js | ||
| // 插件 preload | ||
| ztools.registerProvider('ocr', async (input) => { | ||
| const { image, lang } = input | ||
| // 调用你的 OCR 服务 | ||
| const res = await fetch('https://your-ocr-api/recognize', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ image, lang }) | ||
| }) | ||
| const data = await res.json() | ||
| return { | ||
| text: data.text, | ||
| blocks: data.blocks, | ||
| confidence: data.confidence | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| 注意事项: | ||
|
|
||
| - `registerProvider` 必须在 plugin.json 声明过对应 type 之后调用,否则注册会被拒绝。 | ||
| - handler 是 `async` 函数,返回值需符合契约;入参缺失字段请自行兜底。 | ||
| - 一个插件对同一 type 只能注册一次。 | ||
|
|
||
| --- | ||
|
|
||
| ## 翻译 provider 示例 | ||
|
|
||
| ```js | ||
| ztools.registerProvider('translation', async (input) => { | ||
| const { text, from, to } = input | ||
| const res = await fetch('https://your-translate-api/translate', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ text, from: from || 'auto', to: to || 'zh' }) | ||
| }) | ||
| const data = await res.json() | ||
| return { | ||
| text: data.translated, | ||
| detectedFrom: data.detectedSource | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 内置 provider | ||
|
|
||
| 主程序内置了 **Bergamot 离线翻译引擎**(type `translation`,id `builtin-bergamot`),仅支持英译中。它与插件 provider 并列展示、可设为默认,用户可随时切换。 | ||
|
|
||
| OCR 暂无内置实现,完全由插件提供。 | ||
|
|
||
| --- | ||
|
|
||
| ## 作为消费方调用 provider 能力 | ||
|
|
||
| 任何插件(不仅是 provider 的提供方)都可以**主动发起一次翻译 / OCR**,复用用户已启用的 provider。这是 Provider 抽象的核心价值之一:能力可被跨插件复用。 | ||
|
|
||
| ### 通用入口:`ztools.providers` | ||
|
|
||
| ```js | ||
| // 查询某个 type 下的全部渠道,每个渠道带 isDefault 标记 | ||
| const list = await ztools.providers.getProviders('translation') | ||
| // → [{ id, type, label, description, source, isDefault }, ...] | ||
|
|
||
| // 单独查询默认渠道(无可用时返回 null) | ||
| const def = await ztools.providers.getDefaultProvider('translation') | ||
| // → { id, type, label, ..., isDefault: true } | null | ||
|
|
||
| // 统一调用入口:providerId 可选,缺省走该 type 的默认渠道 | ||
| const out = await ztools.providers.invokeProvider('translation', { text: 'hello', to: 'zh' }) | ||
| // → { text: '你好', detectedFrom?: 'en' } | ||
| ``` | ||
|
|
||
| `invokeProvider` 失败会抛错(如没有可用 provider、provider 加载超时等),调用方需 `try/catch`。 | ||
|
|
||
| ### 便捷封装:`ztools.translate` / `ztools.ocr` | ||
|
|
||
| 为最常用的两种调用提供语法糖,签名更贴近直觉: | ||
|
|
||
| ```js | ||
| // 翻译:options: { from?, to?, providerId? } | ||
| const result = await ztools.translate('hello', { from: 'en', to: 'zh' }) | ||
| console.log(result.text) // 翻译结果 | ||
| console.log(result.detectedFrom) // 实际识别到的源语言(可选) | ||
|
|
||
| // OCR:options: { lang?, providerId? },image 为本地路径 / data URI / URL | ||
| const ocrResult = await ztools.ocr('/path/to/image.png', { lang: 'eng' }) | ||
| console.log(ocrResult.text) | ||
| ``` | ||
|
|
||
| - `providerId` 缺省时使用用户在该 type 下设置的默认渠道;显式传入则调用指定渠道。 | ||
| - `image` / `text` 的具体能力边界取决于所选 provider 的实现。 | ||
|
|
||
| ### 默认渠道选择规则 | ||
|
|
||
| 调用时若未显式指定 `providerId`,主进程按以下优先级选择: | ||
|
|
||
| 1. 用户在「设置 → 提供商」设为默认的 provider; | ||
| 2. 该 type 下第一个启用的 provider; | ||
| 3. 该 type 下第一个可用 provider(兜底)。 | ||
|
|
||
| 均无可用项时抛出「没有可用的 xxx 提供商」错误。 | ||
|
|
||
| --- | ||
|
|
||
| ## 调用链路 | ||
|
|
||
| 1. 用户在「设置 → 提供商」启用 / 设为默认某个 provider。 | ||
| 2. 消费方调用 `providerManager.invoke(type, input)`: | ||
| - 主程序内(如超级面板选中翻译)直接调用主进程方法; | ||
| - 插件通过 `ztools.providers.invokeProvider` / `ztools.translate` / `ztools.ocr` 发起,经统一分发器转发到同一个 `providerManager.invoke`。 | ||
| 3. 主进程按默认 / 启用项选择 provider: | ||
| - 内置 provider:直接在主进程调用本地实现。 | ||
| - 插件 provider:按需预加载插件,等待 `registerProvider` 完成后回调 handler。 | ||
| 4. handler 返回结果按契约透传给消费方。 | ||
|
|
||
| --- | ||
|
|
||
| ## 调试 | ||
|
|
||
| - provider 注册失败会在插件控制台抛错(如未声明该 type)。 | ||
| - 「设置 → 提供商」tab 可确认你的 provider 是否被识别、是否启用 / 默认。 | ||
| - 翻译可在超级面板选中文本时触发验证;插件内可直接 `await ztools.translate('hello')` / `await ztools.ocr(imagePath)` 验证。 | ||
|
|
||
| ## 完整 plugin.json 示例 | ||
|
|
||
| ```json | ||
| { | ||
| "name": "cloud-providers", | ||
| "title": "云翻译与 OCR", | ||
| "description": "提供云端翻译与 OCR 能力", | ||
| "version": "1.0.0", | ||
| "main": "index.html", | ||
| "preload": "preload.js", | ||
| "features": [ | ||
| { | ||
| "code": "translate", | ||
| "explain": "翻译", | ||
| "cmds": ["翻译"] | ||
| } | ||
| ], | ||
| "providers": { | ||
| "translation": { | ||
| "type": "translation", | ||
| "label": "云翻译", | ||
| "description": "多语言云翻译" | ||
| }, | ||
| "ocr": { | ||
| "type": "ocr", | ||
| "label": "云 OCR", | ||
| "description": "高精度图片文字识别" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| <script setup lang="ts"> | ||
| import { computed, ref, watch } from 'vue' | ||
| import ProgressCircleButton from '@/components/common/ProgressCircleButton/ProgressCircleButton.vue' | ||
| import type { PluginDownloadState, PluginItem } from './types' | ||
|
|
||
|
|
@@ -15,6 +16,48 @@ const emit = defineEmits<{ | |
| (e: 'upgrade'): void | ||
| }>() | ||
|
|
||
| // 本插件提供的 provider 能力标签(翻译 / OCR)。 | ||
| // 仅对已安装插件展示,从主进程聚合后的 provider 列表中按 pluginName 过滤。 | ||
| const providerTypes = ref<Array<'translation' | 'ocr'>>([]) | ||
|
|
||
| async function loadProviderTypes(): Promise<void> { | ||
| if (!props.plugin.installed || !props.plugin.name) { | ||
| providerTypes.value = [] | ||
| return | ||
| } | ||
| try { | ||
| const res = await window.ztools.internal.providers.getAll() | ||
| if (res.success && Array.isArray(res.data)) { | ||
| const types = new Set<'translation' | 'ocr'>() | ||
| for (const entry of res.data) { | ||
| if (entry.source === 'plugin' && entry.pluginName === props.plugin.name && entry.type) { | ||
| types.add(entry.type) | ||
| } | ||
| } | ||
| providerTypes.value = Array.from(types) | ||
| } else { | ||
| providerTypes.value = [] | ||
| } | ||
| } catch { | ||
| providerTypes.value = [] | ||
| } | ||
|
Comment on lines
+41
to
+43
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| watch( | ||
| () => [props.plugin.name, props.plugin.installed], | ||
| () => { | ||
| loadProviderTypes() | ||
| }, | ||
| { immediate: true } | ||
| ) | ||
|
|
||
| const providerLabels = computed(() => | ||
| providerTypes.value.map((type) => ({ | ||
| type, | ||
| label: type === 'translation' ? '翻译提供商' : 'OCR 提供商' | ||
| })) | ||
| ) | ||
|
|
||
| function formatSize(bytes?: number): string { | ||
| if (!bytes || bytes <= 0) return '' | ||
| const mb = bytes / (1024 * 1024) | ||
|
|
@@ -49,6 +92,14 @@ function openHomepage(): void { | |
| <div class="detail-title"> | ||
| <span class="detail-name">{{ plugin.title || plugin.name }}</span> | ||
| <slot name="title-badge" /> | ||
| <span | ||
| v-for="p in providerLabels" | ||
| :key="p.type" | ||
| class="provider-badge" | ||
| :title="`本插件提供 ${p.label}(可在「设置 → 提供商」中启用)`" | ||
| > | ||
| {{ p.label }} | ||
| </span> | ||
| </div> | ||
| <div class="detail-desc">{{ plugin.description || '暂无描述' }}</div> | ||
| </div> | ||
|
|
@@ -368,6 +419,17 @@ function openHomepage(): void { | |
| flex-wrap: wrap; | ||
| } | ||
|
|
||
| .provider-badge { | ||
| display: inline-block; | ||
| font-size: 11px; | ||
| font-weight: 500; | ||
| color: #0891b2; | ||
| background: rgba(8, 145, 178, 0.12); | ||
| padding: 2px 8px; | ||
| border-radius: 4px; | ||
| line-height: 1.4; | ||
| } | ||
|
|
||
| .detail-name { | ||
| font-size: 18px; | ||
| font-weight: 700; | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
文档此处似乎有一个小错误。根据表格中的定义,
translation提供商的输入是文本,不包含image属性。image属性是ocr提供商的输入。建议修正此处的描述以避免开发者混淆。