@xiongtao.xt/tau-plugins 0.31.0 → 0.32.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/client/{chunk-ECEBBIHP.js → chunk-SVY5LBAT.js} +44 -0
  4. package/docs/README.md +43 -0
  5. package/docs/ai.md +169 -0
  6. package/docs/backend.md +168 -0
  7. package/docs/editor.md +457 -0
  8. package/docs/excerpts.md +126 -0
  9. package/docs/flashcards.md +127 -0
  10. package/docs/getting-started.md +134 -0
  11. package/docs/mcp.md +173 -0
  12. package/docs/plugin-basics.md +209 -0
  13. package/docs/providers/README.md +108 -0
  14. package/docs/providers/images.md +65 -0
  15. package/docs/providers/publish.md +144 -0
  16. package/docs/providers/sync.md +75 -0
  17. package/docs/providers/tts.md +62 -0
  18. package/docs/providers/upload.md +77 -0
  19. package/docs/providers/video.md +118 -0
  20. package/docs/release.md +130 -0
  21. package/docs/services.md +159 -0
  22. package/docs/site-data.md +90 -0
  23. package/docs/skills.md +129 -0
  24. package/docs/storage-and-network.md +62 -0
  25. package/docs/ui.md +335 -0
  26. package/docs/user-data.md +389 -0
  27. package/docs/whiteboard.md +290 -0
  28. package/package.js +1 -1
  29. package/package.json +1 -1
  30. package/publish-artifacts.js +1 -1
  31. package/publish-client.js +3 -1
  32. package/types/packages/plugins/src/categories.d.ts +3 -0
  33. package/types/packages/plugins/src/contract.d.ts +0 -2
  34. package/types/packages/plugins/src/index.d.ts +0 -1
  35. package/types/packages/plugins/src/package.d.ts +2 -0
  36. package/AI.md +0 -116
  37. package/API.md +0 -881
  38. package/BACKEND.md +0 -68
  39. package/DEVELOPMENT.md +0 -173
  40. package/EDITOR.md +0 -192
  41. package/FLASHCARDS.md +0 -92
  42. package/IMAGES.md +0 -16
  43. package/MCP.md +0 -118
  44. package/NOTIFICATIONS.md +0 -48
  45. package/PUBLISH-DATA.md +0 -48
  46. package/PUBLISH.md +0 -50
  47. package/SERVICES.md +0 -119
  48. package/SYNC.md +0 -37
  49. package/TTS.md +0 -20
  50. package/UPLOAD.md +0 -35
  51. package/VIDEO.md +0 -65
  52. package/WHITEBOARD.md +0 -115
  53. package/types/packages/plugins/src/skills.d.ts +0 -8
@@ -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 负责,插件只负责调用服务商接口,不直接写笔记或文件。
@@ -0,0 +1,77 @@
1
+ # 资源上传
2
+
3
+ 资源上传提供方让用户把编辑器里的图片、附件上传到你接入的存储(图床、对象存储、Git 仓库等),正文中直接引用返回的地址。
4
+
5
+ 先读[服务提供方](README.md)了解共同的写法。资源上传与[同步存储](sync.md)是两套独立的接口。
6
+
7
+ ## 注册
8
+
9
+ ```ts
10
+ context.upload.register({
11
+ id: context.pluginId,
12
+ name: "我的图床",
13
+ async upload(file, config, signal) {
14
+ const settings = parseConfig(config);
15
+ const response = await context.fetch(`${settings.endpoint}/${encodeURIComponent(file.name)}`, {
16
+ method: "PUT",
17
+ headers: { "Content-Type": file.mimeType, Authorization: `Bearer ${settings.token}` },
18
+ body: file.content,
19
+ signal,
20
+ });
21
+ if (!response.ok) throw new Error(`上传失败:${response.status}`);
22
+ return `${settings.publicBaseUrl}/${encodeURIComponent(file.name)}`;
23
+ },
24
+ });
25
+ ```
26
+
27
+ `upload(file, config, signal)`:
28
+
29
+ | 参数 | 说明 |
30
+ | -------- | ---------------------------------------------------------------------------------- |
31
+ | `file` | `{ name, mimeType, content }`。`name` 已由 Tau 唯一命名,`content` 是 `Uint8Array` |
32
+ | `config` | 用户保存的配置 |
33
+ | `signal` | 取消信号,传给 `context.fetch` |
34
+
35
+ 返回资源的 HTTP(S) 地址。
36
+
37
+ ## 返回的地址
38
+
39
+ - 地址会写进正文,必须**长期有效**。不要返回有过期时间的签名地址。
40
+ - 上传成功不等于任何人都能访问:存储桶或仓库的公开读取需要用户自己设置。不要自动修改用户的存储权限,在 README 里说明怎么配置。
41
+ - 需要自定义公开域名时,在配置表单中让用户填写,按对象路径拼出完整地址。
42
+
43
+ ## 配置表单
44
+
45
+ ```ts
46
+ context.settings.register({
47
+ id: "upload",
48
+ title: "我的图床",
49
+ target: "upload",
50
+ component: UploadSettings,
51
+ });
52
+ ```
53
+
54
+ 组件从 `uploadConfig.value` 读取配置,用户保存时调用 `await uploadConfig.save(config)`。保存后,Tau 会把你的插件选为当前的上传目标。
55
+
56
+ ## 职责划分
57
+
58
+ Tau 负责图片压缩、文件命名、本地保存和多窗口之间的转发;插件只负责把文件传到远端并返回地址。插件拿不到用户的文件路径,也不修改笔记内容。
59
+
60
+ ## 在其他功能中使用上传
61
+
62
+ 插件自己需要上传文件时(例如上传参考音频),调用 `context.upload.uploadRemote`,它使用用户当前选择的远端上传配置:
63
+
64
+ ```ts
65
+ const url = await context.upload.uploadRemote(
66
+ { name: "sample.mp3", mimeType: "audio/mpeg", content: bytes },
67
+ { signal }
68
+ );
69
+ ```
70
+
71
+ 用户选择的是本地存储时,`uploadRemote` 会明确报错,不会自动换成其他存储。取消只是停止等待,不会撤回已经上传的文件。
72
+
73
+ ## 测试清单
74
+
75
+ - 用户选择本地存储时,插件不会被调用。
76
+ - 上传成功、失败(认证错误、网络错误)都有正确的结果和提示。
77
+ - 保存配置、停用插件、在独立窗口中上传、重启后再上传。
@@ -0,0 +1,118 @@
1
+ # 视频来源
2
+
3
+ 视频提供方让用户从你接入的视频网站或自建服务(如 NAS)添加视频。启用后,编辑器、白板、项目视频笔记和学习资料的「添加视频」中会出现以你的提供方命名的入口。
4
+
5
+ 插件只需要懂得视频来源本身:识别链接、查询标题和清晰度、给出真实的视频流地址。选择弹窗、下载进度、音视频合并、缓存和播放都由 Tau 完成,插件不传输视频数据。
6
+
7
+ 先读[服务提供方](README.md)了解共同的写法。
8
+
9
+ ## 流程
10
+
11
+ 1. 用户在你的入口粘贴一个链接。
12
+ 2. Tau 调用 `matches(url)` 确认链接属于你的插件,再调用 `resolve(url)` 获取标题和可选清晰度。
13
+ 3. 用户选择清晰度,Tau 把这个清晰度的 `ref` 保存到文档中。
14
+ 4. 需要下载时,Tau 调用 `getPlayback(ref)` 获取当前可用的流地址,下载并缓存。之后离线也能播放。
15
+
16
+ ## 注册
17
+
18
+ ```ts
19
+ import type { VideoInstance } from "@tau/plugins";
20
+
21
+ context.video.register({
22
+ id: context.pluginId,
23
+ name: "我的视频站",
24
+ create: (config): VideoInstance => ({
25
+ matches: url => /^https:\/\/video\.example\.com\/v\//.test(url),
26
+ async resolve(url, signal) {
27
+ const info = await fetchVideoInfo(context, url, signal);
28
+ return {
29
+ title: info.title,
30
+ cover: info.cover,
31
+ variants: info.qualities.map(quality => ({
32
+ label: quality.label,
33
+ ref: { videoId: info.id, quality: quality.id },
34
+ })),
35
+ };
36
+ },
37
+ cacheKey: ref => `${ref.videoId}:${ref.quality}`,
38
+ async getPlayback(ref, signal) {
39
+ const stream = await fetchStreamUrl(context, ref, signal);
40
+ return {
41
+ kind: "file",
42
+ stream: { url: stream.url, headers: { Referer: "https://video.example.com" } },
43
+ };
44
+ },
45
+ }),
46
+ });
47
+ ```
48
+
49
+ | 方法 | 说明 |
50
+ | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `matches(url)` | 同步判断链接是否属于你的来源,**不访问网络** |
52
+ | `resolve(url, signal)` | 返回 `{ title, cover?, variants, defaultVariant? }`。`variants` 是可选清晰度,至少一项,每项 `{ label, hint?, ref }`;`defaultVariant` 是默认项的下标,从 0 开始 |
53
+ | `cacheKey(ref)` | 返回稳定的缓存标识,应包含清晰度。已缓存的视频不会再调用 `getPlayback` |
54
+ | `getPlayback(ref, signal)` | 每次下载前调用,返回当前可用的播放源,可以是会过期的地址 |
55
+
56
+ 链接属于其他已启用插件时,Tau 会提示用户改用对应的入口。
57
+
58
+ ## ref:可以长期保存的视频引用
59
+
60
+ 用户选中的 `ref` 会原样写进文档和数据库,以后只能靠它找回视频。所以:
61
+
62
+ - `ref` 必须是可以长期保存的 JSON,例如视频 ID 加清晰度。
63
+ - 不要放会过期的地址、Cookie 或 Token。
64
+ - 不要使用 `type`、`title`、`thumbnail` 这三个键,它们由 Tau 占用。
65
+ - `cacheKey` 也不要用会变的签名地址:账号刷新或地址过期,不应该改变同一个视频、同一清晰度的缓存标识。
66
+
67
+ ## 播放源
68
+
69
+ `getPlayback` 返回三种之一:
70
+
71
+ | `kind` | 结构 | 说明 |
72
+ | --------- | --------------------------------------------- | ------------------------------------------ |
73
+ | `file` | `{ stream }` | 音视频合一的单个文件 |
74
+ | `split` | `{ video, audio }` | 音视频分离的两路流,Tau 分别下载后无损合并 |
75
+ | `extract` | `{ url, videoFormat?, audioFormat?, proxy? }` | 交给 Tau 内置的视频解析器下载,见下文 |
76
+
77
+ `stream` 是 `{ url, headers? }`。需要 Referer、Cookie 等请求头时放在 `headers` 里,Tau 只在下载时使用,不会写入文档或缓存索引。
78
+
79
+ ## 使用内置的视频解析器
80
+
81
+ 对于内置解析器支持的网站,可以先用 `context.services.videoExtractor.probe` 查询可下载的格式,再返回 `kind: "extract"`:
82
+
83
+ ```ts
84
+ const probe = await context.services.videoExtractor.probe(url, { signal });
85
+ // probe:{ title, cover?, duration?, formats }
86
+ // formats 每项:{ id, videoOnly, audioOnly, ext, width, height, fps, bitrate, size, note }
87
+ ```
88
+
89
+ 根据 `formats` 组织清晰度,`getPlayback` 时返回 `{ kind: "extract", url, videoFormat, audioFormat }`。插件不能自己启动外部程序下载视频。
90
+
91
+ ## 配置表单
92
+
93
+ 账号、Cookie、代理等通过配置表单让用户填写:
94
+
95
+ ```ts
96
+ context.settings.register({
97
+ id: "video",
98
+ title: "我的视频站",
99
+ target: "video",
100
+ component: VideoSettings,
101
+ });
102
+ ```
103
+
104
+ 组件收到 `videoConfig`:`value` 和 `save(value)`,每个插件一份配置。保存后,下一次 `create` 就会读到新的值。
105
+
106
+ ## 插件不可用时
107
+
108
+ - 插件未启用时,已保存的视频显示需要启用插件的提示,数据保留。
109
+ - 移动端和发布的站点不运行插件,这些视频在那里只显示说明。
110
+
111
+ ## 测试清单
112
+
113
+ - 不属于你的链接、没有可用格式的视频。
114
+ - 地址过期后重新获取播放源。
115
+ - 音视频分离的视频。
116
+ - 已缓存的视频离线播放。
117
+ - 更新账号配置、取消下载、停用插件。
118
+ - 网站限制或认证失败时,返回可读的错误,不要返回无法播放的结果。