@xiongtao.xt/tau-plugins 0.31.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +2 -2
- package/client/{chunk-ECEBBIHP.js → chunk-SVY5LBAT.js} +44 -0
- package/docs/README.md +43 -0
- package/docs/ai.md +169 -0
- package/docs/backend.md +168 -0
- package/docs/editor.md +590 -0
- package/docs/excerpts.md +126 -0
- package/docs/flashcards.md +127 -0
- package/docs/getting-started.md +134 -0
- package/docs/mcp.md +173 -0
- package/docs/plugin-basics.md +209 -0
- package/docs/providers/README.md +108 -0
- package/docs/providers/images.md +65 -0
- package/docs/providers/publish.md +144 -0
- package/docs/providers/sync.md +75 -0
- package/docs/providers/tts.md +62 -0
- package/docs/providers/upload.md +77 -0
- package/docs/providers/video.md +118 -0
- package/docs/release.md +130 -0
- package/docs/services.md +159 -0
- package/docs/site-data.md +90 -0
- package/docs/skills.md +129 -0
- package/docs/storage-and-network.md +62 -0
- package/docs/ui.md +336 -0
- package/docs/user-data.md +389 -0
- package/docs/whiteboard.md +290 -0
- package/package.js +1 -1
- package/package.json +1 -1
- package/publish-artifacts.js +38 -1
- package/types/packages/db-schema/content-resources.d.ts +76 -0
- package/types/packages/editor/contracts.d.ts +1 -1
- package/types/packages/editor/source-contracts.d.ts +41 -1
- package/types/packages/editor/src/core/api/extension.d.ts +15 -0
- package/types/packages/editor/src/core/api/index.d.ts +5 -1
- package/types/packages/editor/src/core/api/resources.d.ts +6 -0
- package/types/packages/editor/src/core/components/ElementFrame/index.d.ts +25 -0
- package/types/packages/editor/src/core/utils/blockKeyTarget.d.ts +9 -0
- package/types/packages/plugins/src/categories.d.ts +3 -0
- package/types/packages/plugins/src/contract.d.ts +0 -2
- package/types/packages/plugins/src/editor.d.ts +1 -0
- package/types/packages/plugins/src/element.d.ts +11 -0
- package/types/packages/plugins/src/index.d.ts +4 -1
- package/types/packages/plugins/src/media.d.ts +69 -0
- package/types/packages/plugins/src/package.d.ts +2 -0
- package/types/packages/ui/src/index.d.ts +1 -0
- package/types/packages/ui/src/primitives/Sortable.d.ts +32 -0
- package/types/packages/ui/src/primitives/Tabs.d.ts +1 -3
- package/AI.md +0 -116
- package/API.md +0 -881
- package/BACKEND.md +0 -68
- package/DEVELOPMENT.md +0 -173
- package/EDITOR.md +0 -192
- package/FLASHCARDS.md +0 -92
- package/IMAGES.md +0 -16
- package/MCP.md +0 -118
- package/NOTIFICATIONS.md +0 -48
- package/PUBLISH-DATA.md +0 -48
- package/PUBLISH.md +0 -50
- package/SERVICES.md +0 -119
- package/SYNC.md +0 -37
- package/TTS.md +0 -20
- package/UPLOAD.md +0 -35
- package/VIDEO.md +0 -65
- package/WHITEBOARD.md +0 -115
- package/types/packages/plugins/src/skills.d.ts +0 -8
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# 插件的组成
|
|
2
|
+
|
|
3
|
+
一个 Tau 插件由以下几部分组成:
|
|
4
|
+
|
|
5
|
+
- **manifest.json**:插件是谁、需要什么。Tau 不运行代码就能读到它。
|
|
6
|
+
- **入口模块**(构建后是 `main.js`):默认导出一个插件定义,包含静态声明和运行时逻辑。
|
|
7
|
+
- **样式**(构建后是 `main.css`,可选)。
|
|
8
|
+
- **Skill 目录**(`skills/<id>/`,可选):给 AI 看的使用说明,见 [Skill](skills.md)。
|
|
9
|
+
|
|
10
|
+
这一篇依次讲 manifest 和入口,以及贯穿所有能力的几条规则:生命周期、`context`、ID、取消与清理。
|
|
11
|
+
|
|
12
|
+
## manifest
|
|
13
|
+
|
|
14
|
+
模板的 `manifest.json`:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"formatVersion": 1,
|
|
19
|
+
"id": "example.external-counter",
|
|
20
|
+
"name": "计数器示例",
|
|
21
|
+
"description": "在侧边栏记录并保存计数。",
|
|
22
|
+
"categories": ["page"],
|
|
23
|
+
"keywords": ["计数", "打卡"],
|
|
24
|
+
"version": "1.0.0",
|
|
25
|
+
"minAppVersion": "0.59.0",
|
|
26
|
+
"apiVersion": 1,
|
|
27
|
+
"platform": "desktop",
|
|
28
|
+
"format": "cjs",
|
|
29
|
+
"main": "main.js"
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 需要你填写的字段
|
|
34
|
+
|
|
35
|
+
| 字段 | 说明 |
|
|
36
|
+
| --------------- | --------------------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `id` | 插件的唯一标识,由小写字母、数字组成,用点或连字符分段,例如 `yourname.counter`。发布后不要再改 |
|
|
38
|
+
| `name` | 插件名称,显示在插件页和插件中心 |
|
|
39
|
+
| `description` | 一句话简介,1 至 160 字 |
|
|
40
|
+
| `version` | 插件版本,格式为 `主版本.次版本.修订号`,可带预发布后缀,如 `1.2.0-beta.1` |
|
|
41
|
+
| `minAppVersion` | 需要的最低 Tau 版本,如 `0.59.0`。写完整版本号,不写 `>=`、`^` 之类的范围。客户端版本低于它时不会加载插件 |
|
|
42
|
+
| `categories` | 插件中心的分类,可多选,也可以不写,见下表 |
|
|
43
|
+
| `keywords` | 插件中心的搜索词,最多 5 个,每个不超过 20 字。例如 S3 插件可以写 `MinIO`、`R2` |
|
|
44
|
+
| `permissions` | 插件可能申请的用户数据权限,见[用户数据 · 权限](user-data.md#权限)和 [AI](ai.md) |
|
|
45
|
+
| `editor` | 声明了编辑器扩展时写 `true`,见[编辑器扩展](editor.md) |
|
|
46
|
+
| `whiteboard` | 声明了白板扩展时写 `true`,见[白板扩展](whiteboard.md) |
|
|
47
|
+
| `backend` | 需要本机后台时填写,同时把 `formatVersion` 改为 `2`,见[本机后台](backend.md) |
|
|
48
|
+
|
|
49
|
+
| 分类 | 适合的插件 |
|
|
50
|
+
| --------------- | ----------------------------- |
|
|
51
|
+
| `page` | 提供页面和侧边栏入口 |
|
|
52
|
+
| `editor` | 在文档中添加块 |
|
|
53
|
+
| `whiteboard` | 在白板中添加元素或工具 |
|
|
54
|
+
| `ai` | MCP 工具、AI 功能、图片生成 |
|
|
55
|
+
| `skill` | Skill 包,或附带 Skill 的插件 |
|
|
56
|
+
| `sync` | 同步存储 |
|
|
57
|
+
| `upload` | 图片与附件上传 |
|
|
58
|
+
| `voice` | 语音合成 |
|
|
59
|
+
| `video` | 在线视频来源 |
|
|
60
|
+
| `blog-publish` | 博客发布目标 |
|
|
61
|
+
| `blog-template` | 博客模板 |
|
|
62
|
+
|
|
63
|
+
分类只影响插件中心的展示,不授予任何能力。只提供命令或普通设置的插件可以不写分类。
|
|
64
|
+
|
|
65
|
+
### 保持模板值的字段
|
|
66
|
+
|
|
67
|
+
`formatVersion`、`apiVersion`、`platform`、`format`、`main` 保持模板里的值。`styles`、`skills` 和 `integrity`(文件摘要)由 `pnpm build` 自动写入构建产物,不要手写,也不要直接修改 ZIP 里的文件。
|
|
68
|
+
|
|
69
|
+
## 入口与插件定义
|
|
70
|
+
|
|
71
|
+
入口模块默认导出 `definePlugin(...)` 的结果。最小的插件长这样:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { definePlugin } from "@tau/plugins";
|
|
75
|
+
|
|
76
|
+
declare const __PLUGIN_META__: { id: string; name: string; version: string };
|
|
77
|
+
|
|
78
|
+
export default definePlugin({
|
|
79
|
+
...__PLUGIN_META__,
|
|
80
|
+
create: () => ({
|
|
81
|
+
activate(context) {
|
|
82
|
+
context.commands.register({
|
|
83
|
+
id: "hello",
|
|
84
|
+
title: "打个招呼",
|
|
85
|
+
run: () => {
|
|
86
|
+
context.notifications.send({ title: "你好" });
|
|
87
|
+
return null;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`__PLUGIN_META__` 由构建脚本从 manifest 注入,包含 `id`、`name`、`version`,这样插件定义和 manifest 始终一致。
|
|
96
|
+
|
|
97
|
+
插件定义里的字段分两类:
|
|
98
|
+
|
|
99
|
+
| 字段 | 类型 | 说明 |
|
|
100
|
+
| ---------------------- | ---------- | --------------------------------------------------------------------- |
|
|
101
|
+
| `create` | 运行时逻辑 | 每次启用时调用,返回本次运行的实例。只有静态声明的插件可以省略 |
|
|
102
|
+
| `editorExtensions` | 静态声明 | 编辑器块,见[编辑器扩展](editor.md) |
|
|
103
|
+
| `editorCodecs` | 静态声明 | 编辑器块的 Markdown 转换,见[编辑器扩展](editor.md#markdown-导入导出) |
|
|
104
|
+
| `whiteboardExtensions` | 静态声明 | 白板元素与工具,见[白板扩展](whiteboard.md) |
|
|
105
|
+
| `ai` | 静态声明 | 插件自定义的 AI 功能,见 [AI](ai.md#自定义-ai-功能) |
|
|
106
|
+
|
|
107
|
+
**静态声明**直接写在插件定义上,Tau 读取插件包时就能拿到,不依赖 `activate`。例如编辑器块不仅在桌面端显示,也会出现在用户发布的博客里,那里根本不会运行 `activate`。**运行时逻辑**则写在 `activate` 里,通过 `context` 注册。
|
|
108
|
+
|
|
109
|
+
## 生命周期
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
create: () => ({
|
|
113
|
+
async activate(context) {
|
|
114
|
+
// 注册功能,读取必要的配置
|
|
115
|
+
},
|
|
116
|
+
async deactivate() {
|
|
117
|
+
// 可选:额外清理
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- **启用**:Tau 调用 `create()` 得到一个新实例,再调用 `activate(context)`。`activate` 完成后插件才算启用成功;它抛出异常则启用失败,已注册的内容全部撤销。
|
|
123
|
+
- **停用**:用户停用插件、切换工作区,或者 Tau 退出时,本次运行结束。通过 `context` 注册的命令、页面、设置等全部自动撤销,`context.signal` 被中止,然后调用 `deactivate`(如果有)。
|
|
124
|
+
- **再次启用**:重新调用 `create()` 和 `activate()`,拿到一个全新的 `context`。
|
|
125
|
+
|
|
126
|
+
据此有几条规则:
|
|
127
|
+
|
|
128
|
+
- 运行状态放在 `create` 返回的闭包或 React 组件里,不放在模块顶层。模块只会加载一次,顶层变量会跨越多次启用残留下来。
|
|
129
|
+
- 需要跨启用、跨重启保留的数据,存进 [`context.storage`](storage-and-network.md)。
|
|
130
|
+
- `activate` 应尽快返回。不要在里面等待用户操作或永不结束的任务;长时间的工作在后台启动,并响应 `context.signal`。
|
|
131
|
+
- 不要依赖 `deactivate` 保存数据,它不保证在所有情况下都有机会执行完。数据应该在用户操作时就保存。
|
|
132
|
+
- 不要把 `context` 保存下来留到下次启用使用。旧的 `context` 失效后,调用它的方法会报错。
|
|
133
|
+
|
|
134
|
+
## context
|
|
135
|
+
|
|
136
|
+
`activate` 收到的 `context` 是插件使用 Tau 能力的唯一入口。页面、设置组件也会通过 props 拿到同一个 `context`。
|
|
137
|
+
|
|
138
|
+
| 属性 | 用途 | 文档 |
|
|
139
|
+
| ----------------------------------------------------- | -------------------------------------------- | --------------------------------------------- |
|
|
140
|
+
| `pluginId`、`workspaceId` | 当前插件 ID 与工作区 ID(不要解析工作区 ID) | |
|
|
141
|
+
| `signal` | 本次运行结束时中止 | [取消与清理](#取消与清理) |
|
|
142
|
+
| `register` | 托管需要清理的资源 | [取消与清理](#取消与清理) |
|
|
143
|
+
| `commands` | 注册命令 | [界面](ui.md#命令) |
|
|
144
|
+
| `pages`、`sidebar` | 注册页面与侧边栏入口 | [界面](ui.md#页面) |
|
|
145
|
+
| `settings` | 注册设置区域 | [界面](ui.md#设置) |
|
|
146
|
+
| `notifications` | 发送通知 | [界面](ui.md#通知) |
|
|
147
|
+
| `storage` | 插件私有存储 | [存储与网络](storage-and-network.md) |
|
|
148
|
+
| `fetch` | 网络请求 | [存储与网络](storage-and-network.md#网络请求) |
|
|
149
|
+
| `permissions` | 查询与申请权限 | [用户数据](user-data.md#权限) |
|
|
150
|
+
| `data`、`content`、`events` | 读写用户数据与正文、订阅变化 | [用户数据](user-data.md) |
|
|
151
|
+
| `app` | 选择与打开已有内容、导出与演示 | [用户数据](user-data.md#选择并打开已有内容) |
|
|
152
|
+
| `entities` | 插件自己的内容 | [用户数据](user-data.md#插件内容) |
|
|
153
|
+
| `excerpts`、`materials` | 摘录与材料动作 | [摘录](excerpts.md) |
|
|
154
|
+
| `ai` | 调用大模型 | [AI](ai.md) |
|
|
155
|
+
| `mcp`、`chat` | 给 AI 提供工具、自定义工具结果的展示 | [MCP 工具](mcp.md) |
|
|
156
|
+
| `flashcards` | 闪卡 | [闪卡](flashcards.md) |
|
|
157
|
+
| `services` | 编译与转换服务 | [编译与转换服务](services.md) |
|
|
158
|
+
| `backend` | 调用本机后台 | [本机后台](backend.md) |
|
|
159
|
+
| `sync`、`upload`、`tts`、`images`、`video`、`publish` | 注册服务提供方 | [服务提供方](providers/README.md) |
|
|
160
|
+
|
|
161
|
+
## ID 规则
|
|
162
|
+
|
|
163
|
+
命令、页面、设置、侧边栏条目、MCP 等注册时填写的 `id` 都是**插件内的局部 ID**:Tau 会自动加上插件命名空间,你不需要也不应该自己拼接插件 ID。不同插件可以使用相同的局部 ID,互不影响。
|
|
164
|
+
|
|
165
|
+
局部 ID 和存储的 key 都是 1 至 128 个字符,以字母或数字开头,后面可以是字母、数字、点、横线和下划线。同一类注册里重复的 ID 会报错。
|
|
166
|
+
|
|
167
|
+
## 取消与清理
|
|
168
|
+
|
|
169
|
+
**通过 `context` 注册的东西都会自动清理**,包括命令、页面、设置、侧边栏、MCP、通知、事件订阅等。注册方法返回的 `Disposable` 可以用来提前撤销:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const command = context.commands.register({ id: "temp", title: "临时命令", run: () => null });
|
|
173
|
+
// 不再需要时
|
|
174
|
+
await command.dispose();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**你自己创建的资源**,如计时器、DOM 事件监听、第三方库的连接,交给 `context.register` 托管,停用时按注册顺序的逆序清理:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const timer = setInterval(refresh, 60_000);
|
|
181
|
+
context.register({ dispose: () => clearInterval(timer) });
|
|
182
|
+
|
|
183
|
+
const onOnline = () => refresh();
|
|
184
|
+
window.addEventListener("online", onOnline);
|
|
185
|
+
context.register({ dispose: () => window.removeEventListener("online", onOnline) });
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
React 组件内的订阅照常在 `useEffect` 的清理函数里处理。
|
|
189
|
+
|
|
190
|
+
**异步任务**要响应 `context.signal`。网络请求用 [`context.fetch`](storage-and-network.md#网络请求),它会随插件停用自动取消。异步结果写回界面前检查 `context.signal.aborted`,避免切换工作区后旧请求的结果出现在新界面上:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const result = await loadSomething();
|
|
194
|
+
if (context.signal.aborted) return;
|
|
195
|
+
setState(result);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 错误处理
|
|
199
|
+
|
|
200
|
+
- 预期内的失败(网络错误、配置缺失、用户拒绝授权)要在界面上给出可读的提示,并提供重试或修复的入口。
|
|
201
|
+
- 命令的 `run` 抛出异常表示执行失败。需要告诉用户失败原因时,自己发送[通知](ui.md#通知)或在页面上显示。
|
|
202
|
+
- 读取存储、网络响应、用户配置等外部数据时,先校验类型再使用,不要假定数据一定符合预期。
|
|
203
|
+
- 不要在遇到额度不足、拒绝授权这类错误后自动循环重试。
|
|
204
|
+
|
|
205
|
+
## 运行环境
|
|
206
|
+
|
|
207
|
+
插件代码与 Tau 运行在同一个浏览器环境中,可以使用标准的 Web API 和浏览器兼容的 npm 包,但不能导入 Node.js 内置模块(`fs`、`path`、`child_process` 等)。需要 Node.js 能力时,使用[本机后台](backend.md)。
|
|
208
|
+
|
|
209
|
+
插件中心会审核提交的版本,但插件与 Tau 之间没有安全沙箱。只申请功能确实需要的权限,不要把密钥、令牌或你机器上的路径写进源码和插件包。
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# 服务提供方
|
|
2
|
+
|
|
3
|
+
Tau 的一些功能允许用户选择背后的服务:同步到哪个存储、图片上传到哪个图床、用哪家的语音合成等等。插件可以注册为这些功能的**提供方**,把一个新的服务接入进来。
|
|
4
|
+
|
|
5
|
+
| 提供方 | 接入后用户可以 | 配置界面出现在 |
|
|
6
|
+
| ---------------------- | ---------------------------------------- | ---------------------------- |
|
|
7
|
+
| [同步存储](sync.md) | 把笔记库同步到你接入的存储服务 | 设置 · 同步 |
|
|
8
|
+
| [资源上传](upload.md) | 把编辑器里的图片、附件上传到你接入的存储 | 设置 · 资源存储 |
|
|
9
|
+
| [语音合成](tts.md) | 用你接入的服务朗读、生成播客、复刻音色 | 设置 · 语音 |
|
|
10
|
+
| [图片生成](images.md) | 用你接入的模型生成和编辑图片 | 设置 · 图片生成 |
|
|
11
|
+
| [视频来源](video.md) | 从你接入的视频网站或服务添加视频 | 设置 · 账号与连接 · 在线视频 |
|
|
12
|
+
| [博客发布](publish.md) | 把博客发布到你接入的目标 | 发布 · 站点设置 · 发布目标 |
|
|
13
|
+
|
|
14
|
+
## 共同的模式
|
|
15
|
+
|
|
16
|
+
所有提供方都遵循同一套写法,理解了一个,其他的就很容易上手。
|
|
17
|
+
|
|
18
|
+
### 1. 注册提供方
|
|
19
|
+
|
|
20
|
+
在 `activate` 中注册,`id` 固定为 `context.pluginId`。每个插件只能注册一个同类提供方。
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
context.upload.register({
|
|
24
|
+
id: context.pluginId,
|
|
25
|
+
name: "我的图床",
|
|
26
|
+
async upload(file, config, signal) {
|
|
27
|
+
// ...
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
多数提供方用 `create(config)` 根据用户配置创建实例:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
context.sync.register({
|
|
36
|
+
id: context.pluginId,
|
|
37
|
+
name: "我的存储",
|
|
38
|
+
create: config => createStorage(context, parseConfig(config)),
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`create` 是同步的,只根据配置创建对象,**不发起网络请求**。真正的连接在实例的方法被调用时才发生。Tau 在每次任务开始时调用 `create`,同一次任务中配置保持不变。
|
|
43
|
+
|
|
44
|
+
### 2. 注册配置表单
|
|
45
|
+
|
|
46
|
+
用户需要填写账号、地址等配置。用 `context.settings.register` 注册一个带 `target` 的设置区域,它会出现在对应功能的设置里,而不是插件详情页:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import type { PluginSettingsProps } from "@tau/plugins";
|
|
50
|
+
|
|
51
|
+
function UploadSettings({ uploadConfig }: PluginSettingsProps) {
|
|
52
|
+
if (!uploadConfig) return null;
|
|
53
|
+
const initial = parseConfig(uploadConfig.value);
|
|
54
|
+
// 表单...
|
|
55
|
+
async function save(next: MyConfig) {
|
|
56
|
+
await uploadConfig.save(next);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
context.settings.register({
|
|
61
|
+
id: "upload",
|
|
62
|
+
title: "我的图床",
|
|
63
|
+
target: "upload",
|
|
64
|
+
component: UploadSettings,
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| `target` | 组件收到的配置属性 | 保存方法 |
|
|
69
|
+
| --------- | ------------------ | ---------------------- |
|
|
70
|
+
| `sync` | `syncConfig` | `save(value)` |
|
|
71
|
+
| `upload` | `uploadConfig` | `save(value)` |
|
|
72
|
+
| `tts` | `ttsConfig` | `save(value, models)` |
|
|
73
|
+
| `images` | `imageConfig` | `save(value, modelId)` |
|
|
74
|
+
| `video` | `videoConfig` | `save(value)` |
|
|
75
|
+
| `publish` | `publishConfig` | `save(value)` |
|
|
76
|
+
|
|
77
|
+
- 配置属性是可选的,使用前先检查是否存在。
|
|
78
|
+
- `value` 是之前保存的 JSON 值,用户还没保存过时可能为空。读取后自己校验结构,缺失的字段用默认值。
|
|
79
|
+
- `save` 失败时保留用户的输入并显示错误;成功后再提示已保存。
|
|
80
|
+
- 配置由 Tau 保存,**不要再存一份到 `context.storage`**:`storage` 会随笔记库同步,不适合放凭据。
|
|
81
|
+
- `create(config)` 收到的就是这里保存的值。
|
|
82
|
+
|
|
83
|
+
注册后,插件详情页会提供跳转到这个配置表单的入口,`register` 返回的句柄的 `open()` 也会跳转过去。
|
|
84
|
+
|
|
85
|
+
### 3. 网络请求与取消
|
|
86
|
+
|
|
87
|
+
- 请求统一用 [`context.fetch`](../storage-and-network.md#网络请求)。
|
|
88
|
+
- 提供方的方法会收到 `signal`,把它传给 `context.fetch`。重试前的等待、轮询也要响应取消。
|
|
89
|
+
- 自己设置超时时,用 `AbortSignal.any([signal, AbortSignal.timeout(ms)])` 组合,不要丢掉 Tau 传入的 `signal`。
|
|
90
|
+
- 取消后不要把 `AbortError` 当成可重试的错误。
|
|
91
|
+
|
|
92
|
+
### 4. 错误处理
|
|
93
|
+
|
|
94
|
+
- 失败时抛出带可读信息的错误,不要吞掉异常后返回"成功"或空结果,否则 Tau 会误以为操作完成了。
|
|
95
|
+
- 错误信息里不要包含 Token、密码等凭据。
|
|
96
|
+
- 插件停用时,进行中的请求会被取消,迟到的结果会被丢弃。
|
|
97
|
+
|
|
98
|
+
### 5. 运行环境
|
|
99
|
+
|
|
100
|
+
提供方运行在浏览器环境中,可以使用浏览器兼容的 npm 包,但不能使用 Node.js 模块(`fs`、`http`、`Buffer` 等)。很多服务商的 SDK 允许替换 HTTP 实现,把它接到 `context.fetch` 上即可。
|
|
101
|
+
|
|
102
|
+
### 6. 在 manifest 中选择分类
|
|
103
|
+
|
|
104
|
+
在 `categories` 中写上对应的分类(`sync`、`upload`、`voice`、`ai`、`video`、`blog-publish`),用户在插件中心按分类筛选时就能找到你的插件。分类只影响展示,注册提供方的是代码。
|
|
105
|
+
|
|
106
|
+
### 7. 写好 README
|
|
107
|
+
|
|
108
|
+
提供方插件通常需要用户在服务商那里做一些准备,比如创建存储桶、开放公开读取、生成 Token。把这些步骤和常见错误写进插件的 `README.md`。
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 图片生成
|
|
2
|
+
|
|
3
|
+
图片提供方让用户用你接入的模型生成图片,或基于参考图编辑图片。
|
|
4
|
+
|
|
5
|
+
先读[服务提供方](README.md)了解共同的写法。
|
|
6
|
+
|
|
7
|
+
## 注册
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
context.images.register({
|
|
11
|
+
id: context.pluginId,
|
|
12
|
+
name: "我的图片服务",
|
|
13
|
+
create: (config, modelId) => ({
|
|
14
|
+
async generate({ prompt, options }, signal) {
|
|
15
|
+
// 调用服务商接口,返回图片
|
|
16
|
+
return [{ name: "image.png", mimeType: "image/png", content: bytes }];
|
|
17
|
+
},
|
|
18
|
+
async edit({ prompt, options, images }, signal) {
|
|
19
|
+
// 基于参考图编辑
|
|
20
|
+
return [{ name: "edited.png", mimeType: "image/png", content: bytes }];
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`create(config, modelId)` 用一个账号的配置和模型创建实例,不发起网络请求。和语音一样,用户可以添加多个账号。
|
|
27
|
+
|
|
28
|
+
实例至少实现一个方法,没实现的方法在 Tau 中不会显示对应入口:
|
|
29
|
+
|
|
30
|
+
| 方法 | 说明 |
|
|
31
|
+
| ------------------------------------------- | ------------------------ |
|
|
32
|
+
| `generate({ prompt, options }, signal)` | 根据文字生成图片 |
|
|
33
|
+
| `edit({ prompt, options, images }, signal)` | 基于一张或多张参考图编辑 |
|
|
34
|
+
|
|
35
|
+
输入和输出的图片都是 `{ name, mimeType, content: Uint8Array }`,输出可以额外附带 `revisedPrompt`(服务商改写后的提示词)。
|
|
36
|
+
|
|
37
|
+
## 参数
|
|
38
|
+
|
|
39
|
+
`options` 包含 `size`、`quality`、`outputFormat`(`png`、`jpeg`、`webp`)、`background`、`count`,都是可选的。
|
|
40
|
+
|
|
41
|
+
- 只把用户显式设置的参数传给服务商。
|
|
42
|
+
- 服务商不支持某个参数时,抛出错误说明,不要悄悄忽略;也不要通过多次调用来凑够 `count`。
|
|
43
|
+
|
|
44
|
+
## 限制
|
|
45
|
+
|
|
46
|
+
- 支持 PNG、JPEG、WebP。
|
|
47
|
+
- 单次最多 16 张;输入、输出的总大小各不超过 128 MiB。
|
|
48
|
+
- 服务商返回的是临时下载地址时,由插件下载成字节再返回。下载其他域名的文件时,不要带上服务商的 API 凭据。
|
|
49
|
+
|
|
50
|
+
## 配置表单
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
context.settings.register({
|
|
54
|
+
id: "images",
|
|
55
|
+
title: "我的图片服务",
|
|
56
|
+
target: "images",
|
|
57
|
+
component: ImageSettings,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
组件收到 `imageConfig`:`instanceId`(正在编辑的账号)、`value`、`modelId`,以及 `save(value, modelId)`。模型 ID 由你的表单让用户填写。只编辑当前这一个账号,不要把它当成全局的当前账号。
|
|
62
|
+
|
|
63
|
+
## 职责划分
|
|
64
|
+
|
|
65
|
+
协议、鉴权、超时、重试和轮询由插件负责;文件解析、上传到用户的资源存储、插入正文由 Tau 负责。插件停用或用户取消时,进行中的任务会被取消,迟到的结果被丢弃;但远端已经开始的任务和产生的费用无法撤回。
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# 博客发布
|
|
2
|
+
|
|
3
|
+
发布提供方让用户把 Tau 生成的博客站点发布到你接入的目标,比如对象存储、静态托管服务或 Git 仓库。Tau 负责生成站点文件、计算增量、重试和清理旧文件;插件只负责把文件送到远端。
|
|
4
|
+
|
|
5
|
+
先读[服务提供方](README.md)了解共同的写法。开发博客模板或独立站点请看[站点数据](../site-data.md)。
|
|
6
|
+
|
|
7
|
+
## 两种发布方式
|
|
8
|
+
|
|
9
|
+
| 方式 | `mode` | 适合 | 需要实现 |
|
|
10
|
+
| -------- | ------------ | -------------------------------------- | -------------------------- |
|
|
11
|
+
| 逐个文件 | `"files"` | 对象存储、支持单文件上传的托管服务 | `uploadFile`、`deleteFile` |
|
|
12
|
+
| 整体快照 | `"snapshot"` | Git 仓库等需要一次性提交全部变化的目标 | `publish` |
|
|
13
|
+
|
|
14
|
+
## 注册
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import type { PublishInstance } from "@tau/plugins";
|
|
18
|
+
|
|
19
|
+
context.publish.register({
|
|
20
|
+
id: context.pluginId,
|
|
21
|
+
name: "我的存储",
|
|
22
|
+
create: (config): PublishInstance => {
|
|
23
|
+
const settings = parseConfig(config);
|
|
24
|
+
return {
|
|
25
|
+
mode: "files",
|
|
26
|
+
concurrency: 8,
|
|
27
|
+
async describe(signal) {
|
|
28
|
+
return {
|
|
29
|
+
identity: `my-storage:${settings.bucket}`,
|
|
30
|
+
basePath: "/",
|
|
31
|
+
publicUrl: settings.publicUrl,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
async checkConnection(signal) {
|
|
35
|
+
const response = await context.fetch(settings.endpoint, { method: "HEAD", signal });
|
|
36
|
+
return response.ok;
|
|
37
|
+
},
|
|
38
|
+
async uploadFile(path, content, signal) {
|
|
39
|
+
const response = await context.fetch(`${settings.endpoint}/${path}`, {
|
|
40
|
+
method: "PUT",
|
|
41
|
+
body: content,
|
|
42
|
+
signal,
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) throw new Error(`上传 ${path} 失败:${response.status}`);
|
|
45
|
+
},
|
|
46
|
+
async deleteFile(path, signal) {
|
|
47
|
+
const response = await context.fetch(`${settings.endpoint}/${path}`, {
|
|
48
|
+
method: "DELETE",
|
|
49
|
+
signal,
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok && response.status !== 404)
|
|
52
|
+
throw new Error(`删除 ${path} 失败:${response.status}`);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 两种方式都要实现的方法
|
|
60
|
+
|
|
61
|
+
| 方法 | 说明 |
|
|
62
|
+
| ------------------------- | ------------------------------------------------------------------------------ |
|
|
63
|
+
| `describe(signal)` | 返回发布目标的信息,见下表 |
|
|
64
|
+
| `checkConnection(signal)` | 检查目标能否访问,返回 `true` 或 `false`;网络、认证错误可以直接抛出可读的错误 |
|
|
65
|
+
| `dispose()` | 可选。发布结束时释放连接 |
|
|
66
|
+
|
|
67
|
+
| `describe` 返回的字段 | 说明 |
|
|
68
|
+
| --------------------- | --------------------------------------------------------------------------------- |
|
|
69
|
+
| `identity` | 发布目标的稳定标识,如存储桶名加路径。不含凭据,也不随插件版本、模板或 Token 变化 |
|
|
70
|
+
| `basePath` | 站点的根路径,以 `/` 开头和结尾,根目录就是 `/` |
|
|
71
|
+
| `publicUrl` | 可选。站点的公开访问地址 |
|
|
72
|
+
|
|
73
|
+
### 逐个文件
|
|
74
|
+
|
|
75
|
+
| 成员 | 说明 |
|
|
76
|
+
| ----------------------------------- | ------------------------------------------------ |
|
|
77
|
+
| `uploadFile(path, content, signal)` | 上传一个文件,`content` 是 `Uint8Array` |
|
|
78
|
+
| `deleteFile(path, signal)` | 删除一个文件。文件已经不存在时视为成功,便于重试 |
|
|
79
|
+
| `concurrency` | 可选。并发上限,1 至 32 |
|
|
80
|
+
|
|
81
|
+
`path` 是站点内的相对路径。拼接远端地址时保留路径结构并正确编码,不要拼成 Shell 命令。
|
|
82
|
+
|
|
83
|
+
### 整体快照
|
|
84
|
+
|
|
85
|
+
| 成员 | 说明 |
|
|
86
|
+
| --------------------------- | ---------------------------------------------- |
|
|
87
|
+
| `publish(snapshot, signal)` | 提交本轮全部变化,远端确认整体提交成功后才返回 |
|
|
88
|
+
|
|
89
|
+
`snapshot` 包含本轮的临时 `id`、待上传的文件(`path` 与 `hash`)和待删除的路径。失败时抛出异常,不要返回部分成功。
|
|
90
|
+
|
|
91
|
+
## Git 仓库
|
|
92
|
+
|
|
93
|
+
发布到 Git 仓库时,不需要自己实现 Git 协议,使用 Tau 提供的服务:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
async publish(snapshot, signal) {
|
|
97
|
+
await context.services.git.publishSnapshot(
|
|
98
|
+
{
|
|
99
|
+
publicationId: snapshot.id,
|
|
100
|
+
remote: "https://github.com/yourname/blog.git",
|
|
101
|
+
branch: "main",
|
|
102
|
+
author: { name: "Your Name", email: "you@example.com" },
|
|
103
|
+
authentication: { username: "x-access-token", password: settings.token },
|
|
104
|
+
cacheId: settings.cacheId,
|
|
105
|
+
},
|
|
106
|
+
{ signal }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- `remote` 只接受不含凭据的 HTTPS 地址,凭据通过 `authentication` 传入。
|
|
112
|
+
- `publicationId` 只在本轮发布中有效。
|
|
113
|
+
- 需要本地缓存目录加快发布时,调用 `context.services.git.selectCacheDirectory({})`,由用户选择目录,返回 `{ id, label }`;用户取消时返回 `null`。只保存 `id` 和用于显示的 `label`,作为 `cacheId` 传入。
|
|
114
|
+
|
|
115
|
+
## 配置表单
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
context.settings.register({
|
|
119
|
+
id: "publish",
|
|
120
|
+
title: "我的存储",
|
|
121
|
+
target: "publish",
|
|
122
|
+
component: PublishSettings,
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
组件收到 `publishConfig`:`value` 和 `save(value)`。表单显示在「站点设置 · 发布目标」的卡片里,位于 Tau 的连接状态和「服务」行之后。为了和周围的界面一致:
|
|
127
|
+
|
|
128
|
+
- **按行排列**:每行左侧是 64px 宽的标签,右侧是控件;行高至少 46px,内边距 `8px 12px`,行与行之间用 `--tau-line-2` 的顶部边线分隔。标签字号用 `--tau-text-meta`,颜色用 `--tau-ink-2`。
|
|
129
|
+
- **不要再包一层卡片或标题**。
|
|
130
|
+
- **自动保存**:不提供「保存」和「测试连接」按钮,测试连接与完成由 Tau 提供。用户停止输入约 600ms 或输入框失焦时调用 `save`,组件卸载前保存未提交的修改。Tau 回写的配置与你最近一次保存的值相同时,不要覆盖用户正在输入的内容。
|
|
131
|
+
- 辅助操作(如「读取仓库列表」)放在对应输入框的尾部;不常用的选项收进「高级选项」折叠区。
|
|
132
|
+
- `save` 失败时,在表单末行显示错误,保留用户输入。
|
|
133
|
+
|
|
134
|
+
## 注意事项
|
|
135
|
+
|
|
136
|
+
- 用户离开发布页后,发布会在后台继续;插件停用、切换工作区或发起发布的窗口关闭时,本轮发布会中止。
|
|
137
|
+
- 每个请求都传入 `signal`,检查 `response.ok`。不要吞掉异常后返回成功,否则 Tau 会误以为文件已经发布。
|
|
138
|
+
- Token 不要出现在 `identity`、公开地址、错误信息、README 和日志里。
|
|
139
|
+
- `create` 同步返回实例,连接和上传放在各个异步方法里。
|
|
140
|
+
- 新安装的发布插件默认停用。插件缺失或未启用时,Tau 会保留用户的配置和选择,不会自动切换到其他目标。
|
|
141
|
+
|
|
142
|
+
## 测试清单
|
|
143
|
+
|
|
144
|
+
用真实的远端验证成功之后,再声称支持该目标。至少覆盖:配置保存失败、连接失败、重复发布、远端文件已存在或已删除、用户取消、插件停用、切换工作区,以及快照提交中途失败后的重试。
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# 同步存储
|
|
2
|
+
|
|
3
|
+
同步存储提供方让用户把笔记库同步到你接入的存储服务(对象存储、网盘等)。你只需要实现"读写远端文件"的几个基本操作;同步什么、怎么命名、版本与冲突、断点恢复都由 Tau 负责。
|
|
4
|
+
|
|
5
|
+
先读[服务提供方](README.md)了解共同的写法。
|
|
6
|
+
|
|
7
|
+
## 注册
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import type { SyncProvider } from "@tau/plugins/sync";
|
|
11
|
+
|
|
12
|
+
context.sync.register({
|
|
13
|
+
id: context.pluginId,
|
|
14
|
+
name: "我的存储",
|
|
15
|
+
create: config => createStorage(context, parseConfig(config)),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function createStorage(context: PluginContext, config: MyConfig): SyncProvider {
|
|
19
|
+
return {
|
|
20
|
+
async uploadFile(name, bytes) {
|
|
21
|
+
/* ... */ return true;
|
|
22
|
+
},
|
|
23
|
+
async downloadFile(name) {
|
|
24
|
+
/* ... */ return new Uint8Array();
|
|
25
|
+
},
|
|
26
|
+
async fileExists(name) {
|
|
27
|
+
/* ... */ return false;
|
|
28
|
+
},
|
|
29
|
+
async listFiles(prefix) {
|
|
30
|
+
/* ... */ return [];
|
|
31
|
+
},
|
|
32
|
+
async createFileIfAbsent(name, bytes) {
|
|
33
|
+
/* ... */ return true;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
类型 `SyncProvider`、`SyncProviderDefinition`、`RemoteFileInfo` 从 `@tau/plugins/sync` 导入。
|
|
40
|
+
|
|
41
|
+
## 需要实现的方法
|
|
42
|
+
|
|
43
|
+
| 方法 | 返回 | 说明 |
|
|
44
|
+
| --------------------------------- | --------------------------- | ------------------------------------------------------------ |
|
|
45
|
+
| `uploadFile(name, bytes)` | `Promise<boolean>` | 上传对象,已存在时覆盖 |
|
|
46
|
+
| `downloadFile(name)` | `Promise<Uint8Array>` | 下载对象 |
|
|
47
|
+
| `fileExists(name)` | `Promise<boolean>` | 对象是否存在 |
|
|
48
|
+
| `listFiles(prefix?)` | `Promise<RemoteFileInfo[]>` | 列出前缀下的全部对象,**分页由你在内部处理完**,返回完整列表 |
|
|
49
|
+
| `createFileIfAbsent(name, bytes)` | `Promise<boolean>` | 只在对象不存在时创建,已存在时返回 `false` |
|
|
50
|
+
|
|
51
|
+
`RemoteFileInfo` 包含 `name`、`path`(相对路径)、`size`,以及可选的 `lastModified`、`hash`。
|
|
52
|
+
|
|
53
|
+
### createFileIfAbsent 必须是原子的
|
|
54
|
+
|
|
55
|
+
Tau 用它来保证多台设备同时同步时不会互相覆盖。它必须使用存储服务本身的条件写入能力(如 S3 的 `If-None-Match: *`),**不能用"先查是否存在、再上传"来模拟**,那样在并发时会出错。
|
|
56
|
+
|
|
57
|
+
请求失败、无法确定结果时直接抛出错误,Tau 会按自己的规则核实。
|
|
58
|
+
|
|
59
|
+
## 配置表单
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
context.settings.register({
|
|
63
|
+
id: "sync",
|
|
64
|
+
title: "我的存储",
|
|
65
|
+
target: "sync",
|
|
66
|
+
component: SyncSettings,
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
组件从 `syncConfig.value` 读取配置,用 `await syncConfig.save(config)` 保存。字段和校验由你决定。
|
|
71
|
+
|
|
72
|
+
- 每次同步任务开始时,Tau 用当时的配置调用 `create`,任务过程中配置不变。
|
|
73
|
+
- 关闭设置页不影响进行中的同步。
|
|
74
|
+
- 同步进行中,插件不能被停用或卸载。
|
|
75
|
+
- 停用插件后,配置会保留。
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 语音合成
|
|
2
|
+
|
|
3
|
+
语音提供方让用户用你接入的服务朗读文字、生成播客,以及复刻音色。
|
|
4
|
+
|
|
5
|
+
先读[服务提供方](README.md)了解共同的写法。
|
|
6
|
+
|
|
7
|
+
## 注册
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
context.tts.register({
|
|
11
|
+
id: context.pluginId,
|
|
12
|
+
name: "我的语音服务",
|
|
13
|
+
create: (config, modelId) => createVoiceService(context, parseConfig(config), modelId),
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`create(config, modelId)` 用用户的一个账号配置和所选模型创建实例,不发起网络请求。
|
|
18
|
+
|
|
19
|
+
**用户可以为同一个插件添加多个账号**,每个账号是一份独立的配置。所以不要把"当前的 API Key"存在模块级变量里,一切都从 `create` 收到的 `config` 读取。
|
|
20
|
+
|
|
21
|
+
## 实例
|
|
22
|
+
|
|
23
|
+
| 成员 | 说明 |
|
|
24
|
+
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `getVoices(signal)` | 返回音色列表:`{ id, name, type, description? }`,`type` 为 `system`(预置)、`cloned`(复刻)或 `custom` |
|
|
26
|
+
| `synthesize({ text, voiceId, options }, signal)` | 合成语音,返回 `{ audio: Uint8Array, mime }`。`options` 包含可选的 `speed`、`volume`、`pitch` |
|
|
27
|
+
| `cloneVoice({ file, requestedId?, name? }, signal)` | 可选。用一个参考音频文件复刻音色,返回新建的音色 |
|
|
28
|
+
| `textInstructions` | 可选。当前模型支持的文本标记规则(如停顿、语气标签),Tau 生成播客脚本时会参考 |
|
|
29
|
+
|
|
30
|
+
- 网络请求使用 `context.fetch`,并传入方法收到的 `signal`。
|
|
31
|
+
- `cloneVoice` 的 `file` 是 `{ name, mimeType, content }`,每次一个文件。返回实际创建的音色,不要返回上传文件的 ID。
|
|
32
|
+
|
|
33
|
+
## 配置表单
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
context.settings.register({
|
|
37
|
+
id: "tts",
|
|
38
|
+
title: "我的语音服务",
|
|
39
|
+
target: "tts",
|
|
40
|
+
component: TTSSettings,
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
组件收到 `ttsConfig`:
|
|
45
|
+
|
|
46
|
+
| 字段 | 说明 |
|
|
47
|
+
| --------------------- | -------------------------------------- |
|
|
48
|
+
| `instanceId` | 正在编辑的账号 ID |
|
|
49
|
+
| `value` | 这个账号的配置 |
|
|
50
|
+
| `models` | 这个账号的模型列表,`{ id, label? }[]` |
|
|
51
|
+
| `save(value, models)` | 保存这个账号的配置和模型列表 |
|
|
52
|
+
|
|
53
|
+
- 表单只编辑当前这一个账号,不要影响同插件的其他账号,也不要把它设为默认。
|
|
54
|
+
- 模型列表由你的表单提供,可以允许用户填写自定义模型。
|
|
55
|
+
|
|
56
|
+
## 上传参考音频
|
|
57
|
+
|
|
58
|
+
服务需要参考音频的公开地址时,用 `context.upload.uploadRemote(file, { signal })` 上传,它使用用户选择的上传配置,见[资源上传](upload.md#在其他功能中使用上传)。不要自己选择用户的上传插件或读取上传凭据。服务商自带文件上传接口的,直接用服务商的接口即可。
|
|
59
|
+
|
|
60
|
+
## 职责划分
|
|
61
|
+
|
|
62
|
+
音频的播放、拼接和保存由 Tau 负责,插件只负责调用服务商接口,不直接写笔记或文件。
|