Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/examples/provider-example/README.md
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。
29 changes: 29 additions & 0 deletions docs/examples/provider-example/plugin.json
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,仅用于演示接入流程"
}
}
}
28 changes: 28 additions & 0 deletions docs/examples/provider-example/preload.js
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
}
})
206 changes: 206 additions & 0 deletions docs/provider-development-guide.md
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(具体支持取决于实现)。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

文档此处似乎有一个小错误。根据表格中的定义,translation 提供商的输入是文本,不包含 image 属性。image 属性是 ocr 提供商的输入。建议修正此处的描述以避免开发者混淆。

Suggested change
- `translation.image` / `ocr.image` 可为:本地路径 / `data:` URI / `http(s)` URL(具体支持取决于实现)。
- `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": "高精度图片文字识别"
}
}
}
```
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'

Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

catch 块中静默地处理错误可能会导致问题难以追踪。建议使用 console.error 记录错误信息,这样在出现问题时可以更方便地进行调试。

  } catch (err) {
    console.error(`加载插件 [${props.plugin.name}] 的 provider 类型失败:`, err)
    providerTypes.value = []
  }

}

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)
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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;
Expand Down
Loading