@xiongtao.xt/tau-plugins 0.31.1 → 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 +8 -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/types/packages/plugins/src/categories.d.ts +3 -0
  32. package/types/packages/plugins/src/contract.d.ts +0 -2
  33. package/types/packages/plugins/src/index.d.ts +0 -1
  34. package/types/packages/plugins/src/package.d.ts +2 -0
  35. package/types/packages/ui/src/primitives/Tabs.d.ts +1 -3
  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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.32.0
4
+
5
+ - 插件 Skill 改为包内目录:在插件项目根目录放 `skills/<id>/SKILL.md`,可附带参考资料和脚本,构建时打进插件包并写入 manifest 的 `skills` 与文件摘要。名称、描述与调用方式取自 SKILL.md 的 front matter。
6
+ - `definePlugin` 的 `skills` 字段与 `PluginSkill` 类型已移除,已有插件请把正文移到 `skills/<id>/SKILL.md` 后重新构建。
7
+ - 新增独立 Skill 包:manifest 不写 `main`、只声明 `skills`,经插件中心分发,安装后在 Skill 页直接可用。
8
+ - 带脚本的 Skill 与本机后台一样需要插件中心人工审核后才能上架。
9
+ - 插件分类新增 `skill`。
10
+
3
11
  ## 0.31.1
4
12
 
5
13
  - 修复:数据客户端读取 0.31.0 生成的站点清单时报「站点数据缺失」。客户端改为校验 `site.fields`,不再要求已移除的 `site.description`。
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Tau 插件开发包 @tau/plugins
2
2
 
3
- 开发 Tau 插件所需的类型与接口,通过 npm 别名安装为 `@tau/plugins`。根入口的实际实现由 Tau 客户端提供,构建时需设为 external。plugins 包另提供独立站点可用的 publish-data 类型和 publish-client 纯数据客户端,见该包 PUBLISH-DATA.md;不包含站点渲染、路由或预览实现。
3
+ 开发 Tau 插件所需的类型与接口,通过 npm 别名安装为 `@tau/plugins`。根入口的实现由 Tau 在运行时提供,插件构建时需设为 external。
4
4
 
5
5
  从模板开始开发:https://github.com/LastKnightCoder/tau-plugin-template
6
6
 
7
- 编译与转换服务见 [SERVICES.md](SERVICES.md),编辑器扩展、源码预览与插件 Skill 见 [EDITOR.md](EDITOR.md),需要 Tau 0.59.0 及以上。AI 功能声明、模型分配与用量审计见 [AI.md](AI.md)。
7
+ 开发文档见 [docs/README.md](docs/README.md)。
@@ -45,6 +45,7 @@ var PLUGIN_CATEGORIES = [
45
45
  { id: "blog-template", label: "\u535A\u5BA2\u6A21\u677F" },
46
46
  { id: "blog-publish", label: "\u535A\u5BA2\u53D1\u5E03" },
47
47
  { id: "ai", label: "AI" },
48
+ { id: "skill", label: "Skill" },
48
49
  { id: "voice", label: "\u8BED\u97F3" },
49
50
  { id: "video", label: "\u89C6\u9891" },
50
51
  { id: "page", label: "\u9875\u9762" }
@@ -215,6 +216,42 @@ function validateBackendManifest(value) {
215
216
  });
216
217
  }
217
218
 
219
+ // packages/plugins/src/skills.ts
220
+ var SKILL_ROOT = "skills";
221
+ var SKILL_README = "SKILL.md";
222
+ var SKILL_LIMITS = { skills: 32 };
223
+ var skillDirectory = (id) => `${SKILL_ROOT}/${id}`;
224
+ function readSkillsField(value) {
225
+ if (value === void 0) return void 0;
226
+ if (!Array.isArray(value) || value.length > SKILL_LIMITS.skills)
227
+ throw new Error(`skills \u5FC5\u987B\u662F\u6700\u591A ${SKILL_LIMITS.skills} \u9879\u7684\u6570\u7EC4`);
228
+ const ids = /* @__PURE__ */ new Set();
229
+ for (const id of value) {
230
+ if (typeof id !== "string") throw new Error("Skill ID \u65E0\u6548");
231
+ assertLocalId(id);
232
+ if (ids.has(id)) throw new Error(`Skill ID \u91CD\u590D\uFF1A${id}`);
233
+ ids.add(id);
234
+ }
235
+ return ids.size ? Object.freeze([...ids]) : void 0;
236
+ }
237
+ function readSkillFiles(skills, integrity) {
238
+ const files = {};
239
+ const declared = new Set(skills ?? []);
240
+ for (const [name, hash] of Object.entries(integrity)) {
241
+ if (!name.startsWith(SKILL_ROOT + "/")) continue;
242
+ const [, id, ...rest] = name.split("/");
243
+ if (!declared.has(id) || !rest.length || rest.some((part) => !part || part === "." || part === ".."))
244
+ throw new Error(`Skill \u6587\u4EF6\u4E0D\u5C5E\u4E8E\u5DF2\u58F0\u660E\u7684 Skill\uFF1A${name}`);
245
+ if (typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash))
246
+ throw new Error("Skill \u6587\u4EF6\u7F3A\u5C11\u6709\u6548\u7684\u5B8C\u6574\u6027\u6458\u8981");
247
+ files[name] = hash;
248
+ }
249
+ for (const id of declared)
250
+ if (!files[`${skillDirectory(id)}/${SKILL_README}`])
251
+ throw new Error(`Skill ${id} \u7F3A\u5C11 ${SKILL_README}`);
252
+ return files;
253
+ }
254
+
218
255
  // packages/plugins/src/package.ts
219
256
  var PLUGIN_SHARED_MODULES = [
220
257
  "@tau/plugins",
@@ -266,6 +303,12 @@ function validatePluginManifest(value, options = {}) {
266
303
  throw new Error("\u63D2\u4EF6\u4EA7\u7269\u7F3A\u5C11\u6709\u6548\u7684\u5B8C\u6574\u6027\u6458\u8981");
267
304
  integrity[name] = hash;
268
305
  }
306
+ const skills = readSkillsField(item.skills);
307
+ if (skills)
308
+ Object.assign(
309
+ integrity,
310
+ readSkillFiles(skills, item.integrity ?? {})
311
+ );
269
312
  return Object.freeze({
270
313
  formatVersion: item.formatVersion,
271
314
  id: item.id,
@@ -281,6 +324,7 @@ function validatePluginManifest(value, options = {}) {
281
324
  ...item.whiteboard === true ? { whiteboard: true } : {},
282
325
  ...catalog,
283
326
  ...permissions,
327
+ ...skills ? { skills } : {},
284
328
  integrity: Object.freeze(integrity),
285
329
  ...styles === void 0 ? {} : { styles }
286
330
  });
package/docs/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Tau 插件开发文档
2
+
3
+ Tau 插件用 TypeScript 和 React 编写,打包成一个 ZIP,安装后在 Tau 桌面端运行。插件可以给 Tau 加页面和侧边栏入口,在文档里放自定义块,给 AI 提供工具,接入第三方存储、语音、视频等服务,或者读写用户的卡片和待办。
4
+
5
+ 这份文档按"从零写出一个插件"的顺序组织。第一次接触时按下面的路线读,之后按需查阅。
6
+
7
+ ## 阅读路线
8
+
9
+ 1. [快速开始](getting-started.md):装好环境,跑通模板自带的计数器,再把它改成自己的插件。
10
+ 2. [插件的组成](plugin-basics.md):manifest、入口、生命周期和 `context`,所有能力都建立在这些概念上。
11
+ 3. 按你要做的功能,读下表中对应的文档。
12
+ 4. [发布与更新](release.md):发布到插件中心,以及之后怎么发新版本。
13
+
14
+ ## 我想……
15
+
16
+ | 想做的事 | 读这篇 |
17
+ | ---------------------------------------------------------- | -------------------------------------------- |
18
+ | 加一个页面、侧边栏入口、命令、设置项,或发送通知 | [界面](ui.md) |
19
+ | 保存插件自己的数据、请求网络 | [存储与网络](storage-and-network.md) |
20
+ | 读取或修改用户的卡片、文章、项目文档、待办、日程 | [用户数据](user-data.md) |
21
+ | 创建插件自己的内容(如周记),带一份可编辑的正文 | [用户数据 · 插件内容](user-data.md#插件内容) |
22
+ | 把外部的划线、批注导入 Tau | [摘录](excerpts.md) |
23
+ | 在文档里添加自定义块 | [编辑器扩展](editor.md) |
24
+ | 在白板上添加自定义元素和工具 | [白板扩展](whiteboard.md) |
25
+ | 调用用户配置的大模型 | [AI](ai.md) |
26
+ | 给 Tau 的 AI 助手提供工具 | [MCP 工具](mcp.md) |
27
+ | 给 AI 提供使用说明,或单独发布一套 Skill | [Skill](skills.md) |
28
+ | 创建闪卡、打开制卡窗口 | [闪卡](flashcards.md) |
29
+ | 编译 Tailwind、转换 Markdown、处理音视频 | [编译与转换服务](services.md) |
30
+ | 运行 Node.js 代码、调用本机程序 | [本机后台](backend.md) |
31
+ | 接入同步存储、图床、语音、图片生成、视频网站、博客发布目标 | [服务提供方](providers/README.md) |
32
+ | 开发独立博客站点,读取 Tau 发布的数据 | [站点数据](site-data.md) |
33
+
34
+ ## 约定
35
+
36
+ - 插件接口从 `@tau/plugins` 导入,界面组件从 `@tau/ui` 导入。它们的实现由 Tau 在运行时提供,模板的构建配置已经处理好,不需要你打包。
37
+ - 文中的 `context` 指 `activate(context)` 收到的插件上下文;代码片段如果没有写出外层结构,默认放在 `activate` 里。
38
+ - 所有接口都有完整的 TypeScript 类型。文档讲用法和规则,参数的精确类型以编辑器里的类型提示为准。
39
+ - 插件需要 Tau 0.59.0 或更高版本。
40
+
41
+ ## 给 AI 编程助手
42
+
43
+ 模板根目录的 `AGENTS.md` 是给 AI 编程助手看的项目指引。用 AI 写插件时,让它先读 `AGENTS.md` 和本目录下与任务相关的文档。
package/docs/ai.md ADDED
@@ -0,0 +1,169 @@
1
+ # AI
2
+
3
+ 插件可以调用用户在 Tau 中配置好的大模型。插件拿不到用户的 API Key 和服务地址:请求由 Tau 代为发出,用量记在插件名下,受用户设置的额度约束。
4
+
5
+ 有两种用法:
6
+
7
+ | 用法 | 适合 | 用户需要做什么 |
8
+ | --------------------------------------------------- | ----------------------------------------------------- | -------------------------------- |
9
+ | [使用 Tau 已有的模型分配](#使用-tau-已有的模型分配) | 功能和 Tau 某项内置 AI 功能相近,如生成标题、理解图片 | 首次使用时授权 |
10
+ | [自定义 AI 功能](#自定义-ai-功能) | 插件特有的功能,需要用户单独选择模型 | 在插件设置里为功能选择模型并开启 |
11
+
12
+ 大多数插件用第一种就够了,不需要用户额外配置。
13
+
14
+ ## 使用 Tau 已有的模型分配
15
+
16
+ Tau 的每项内置 AI 功能(对话、标题总结、闪卡制作等)都由用户分配了模型。插件可以申请沿用其中某一项的模型。
17
+
18
+ ### 1. 在 manifest 中声明
19
+
20
+ ```json
21
+ { "permissions": ["ai:titleSummary"] }
22
+ ```
23
+
24
+ ### 2. 申请并调用
25
+
26
+ ```ts
27
+ const model = await context.ai.requestCapability("titleSummary");
28
+ const result = await model.chat({
29
+ messages: [{ role: "user", content: "为这段文字拟一个简短的标题:……" }],
30
+ });
31
+ console.log(result.text);
32
+ ```
33
+
34
+ 第一次申请时,Tau 会询问用户是否允许。每项能力分别授权,用户可以在插件详情页的「权限」中撤销。
35
+
36
+ 可以申请的能力(也可以从 `PLUGIN_AI_CAPABILITIES` 导入):
37
+
38
+ | 能力名 | 沿用的模型分配 |
39
+ | ------------------------------ | -------------- |
40
+ | `chat` | 对话功能 |
41
+ | `titleSummary` | 标题总结 |
42
+ | `webClip` | 网页剪藏 |
43
+ | `podcastGeneration` | 播客生成 |
44
+ | `flashcardGeneration` | 闪卡制作 |
45
+ | `aiJudgement` | 答案评判 |
46
+ | `transcriptionOptimization` | 转录断句优化 |
47
+ | `videoSummary` | 视频总结 |
48
+ | `videoEbookRewrite` | 视频电子书重写 |
49
+ | `vibeLearningImageDescription` | 图片理解 |
50
+ | `contentMetadata` | 笔记元信息 |
51
+ | `contentInsights` | AI 洞见 |
52
+
53
+ 这里只是借用模型,不会执行 Tau 对应功能的业务流程,也不会自动读取用户数据。提示词和输入由你的插件提供。用户没有为这项能力分配模型时,调用会明确失败,不会自动换成其他模型。
54
+
55
+ ## 自定义 AI 功能
56
+
57
+ 功能和 Tau 已有的能力都不相近时,声明插件自己的 AI 功能。用户在插件详情页的「设置 → AI 配置」中为它选择模型并开启。
58
+
59
+ ```ts
60
+ import { definePlugin } from "@tau/plugins";
61
+
62
+ declare const __PLUGIN_META__: { id: string; name: string; version: string };
63
+
64
+ export default definePlugin({
65
+ ...__PLUGIN_META__,
66
+ ai: {
67
+ features: [{ id: "summary", name: "文章摘要", description: "概括用户选择的文章" }],
68
+ },
69
+ create: () => ({
70
+ activate(context) {
71
+ context.commands.register({
72
+ id: "summary",
73
+ title: "生成摘要",
74
+ async run(input) {
75
+ const status = await context.ai.getStatus("summary");
76
+ if (!status.configured || !status.enabled) {
77
+ context.ai.openSettings("summary");
78
+ return null;
79
+ }
80
+ const result = await context.ai.chat({
81
+ feature: "summary",
82
+ messages: [
83
+ { role: "system", content: "概括主要内容,不添加原文以外的信息。" },
84
+ { role: "user", content: typeof input === "string" ? input : "请先选择正文。" },
85
+ ],
86
+ maxOutputTokens: 1024,
87
+ });
88
+ return result.text;
89
+ },
90
+ });
91
+ },
92
+ }),
93
+ });
94
+ ```
95
+
96
+ `ai.features` 写在插件定义上(静态声明),每项包含 `id`、`name` 和可选的 `description`,最多 32 项。
97
+
98
+ | 接口 | 说明 |
99
+ | ------------------------ | -------------------------------------------------------- |
100
+ | `getStatus(feature)` | 返回 `{ configured, enabled }`:是否已选模型、是否已开启 |
101
+ | `openSettings(feature?)` | 打开插件的 AI 设置,让用户选择模型或开启功能 |
102
+ | `chat(request)` | 一次性返回完整结果 |
103
+ | `stream(request)` | 流式返回结果 |
104
+
105
+ 调用前先用 `getStatus` 检查,没配置好时用 `openSettings` 引导用户,而不是直接报错。
106
+
107
+ ## 发送请求
108
+
109
+ 两种用法的 `chat` 和 `stream` 参数相同,只是自定义功能需要多传一个 `feature`:
110
+
111
+ | 字段 | 说明 |
112
+ | ----------------- | --------------------------------------------------- |
113
+ | `feature` | 只用于自定义功能:插件声明的功能 ID |
114
+ | `messages` | 消息数组,`role` 为 `system`、`user` 或 `assistant` |
115
+ | `maxOutputTokens` | 可选。最大输出 Token |
116
+ | `signal` | 可选。取消信号 |
117
+
118
+ `chat` 返回 `{ text, usage }`,`usage` 包含输入、输出和总 Token 数,模型服务没有报告用量时为 `null`。
119
+
120
+ ### 流式输出
121
+
122
+ ```ts
123
+ const controller = new AbortController();
124
+ for await (const event of model.stream({
125
+ messages: [{ role: "user", content: article }],
126
+ signal: controller.signal,
127
+ })) {
128
+ if (event.type === "text") appendText(event.delta);
129
+ else showUsage(event.result.usage);
130
+ }
131
+ // 用户点击「停止」时:controller.abort()
132
+ ```
133
+
134
+ 依次产生 `{ type: "text", delta }`,最后产生 `{ type: "done", result }`。
135
+
136
+ ### 多轮对话与图片
137
+
138
+ - 多轮对话需要你自己维护历史,把之前的消息一起放进 `messages`。多条 `system` 消息会合并成一个系统提示。
139
+ - `user` 消息可以附带图片:`images: [{ data, mimeType }]`。`data` 是不带 `data:` 前缀的 Base64;支持 PNG、JPEG、WebP、GIF,每条消息最多 10 张,整次请求的图片总共不超过 20 MiB。模型本身需要支持图片输入。
140
+ - 接口只提供文本生成,不支持工具调用。需要让 Tau 的 AI 助手调用你的功能,使用 [MCP 工具](mcp.md)。
141
+
142
+ ### 取消
143
+
144
+ 传入 `signal` 并调用 `abort()`,或在 `for await` 中提前 `break`,都会取消请求。插件停用、用户撤销授权时,进行中的请求也会被取消。取消无法退回已经产生的模型费用。
145
+
146
+ ## 用量与额度
147
+
148
+ 插件的所有模型调用都记录在 Tau 的「大语言模型 → 用量统计」中,插件设置里的「查看用量」会跳转并筛选出本插件。记录包含时间、模型、状态、Token、估算费用和耗时,不保存提示词和回复内容。
149
+
150
+ 用户可以为插件设置每日请求次数、每日 Token 和并发数上限。达到并发上限时,新请求直接失败,不会排队。
151
+
152
+ ## 错误
153
+
154
+ 错误消息以稳定的代码开头,可以据此给用户合适的提示:
155
+
156
+ | 代码 | 含义 | 建议 |
157
+ | ----------------------- | ---------------------- | -------------------- |
158
+ | `AI_NOT_CONFIGURED` | 没有可用的模型 | 引导用户去设置 |
159
+ | `AI_DISABLED` | 未授权,或插件已停用 | 引导用户授权 |
160
+ | `AI_UNDECLARED_FEATURE` | 功能没有声明 | 检查 `ai.features` |
161
+ | `AI_QUOTA_EXCEEDED` | 今日额度用完 | 提示用户,不要重试 |
162
+ | `AI_CONCURRENCY_LIMIT` | 同时进行的请求太多 | 等当前请求完成后再发 |
163
+ | `AI_CONFIG_CHANGED` | 模型分配在请求期间变了 | 可以重试一次 |
164
+ | `AI_ABORTED` | 已取消 | 不需要提示 |
165
+ | `AI_REQUEST_FAILED` | 模型服务请求失败 | 显示错误信息 |
166
+
167
+ 通过 `signal` 取消时,也可能抛出标准的 `AbortError`。
168
+
169
+ 遇到额度不足、拒绝授权这类错误后,不要自动循环重试。不要在模块顶层调用 AI;在 `activate` 中也不要等待 AI 请求完成,以免拖慢插件启用。
@@ -0,0 +1,168 @@
1
+ # 本机后台
2
+
3
+ 插件的界面代码运行在浏览器环境中,不能使用 Node.js。需要读写本机文件、连接本机设备、调用命令行程序时,给插件加一个**本机后台**:一段在独立 Node.js 进程中运行的代码,界面通过消息调用它。
4
+
5
+ 后台是一个比较重的能力:用户首次运行前需要授权,插件中心也会对带后台的每个版本重点审核。只在浏览器能力确实不够时使用。
6
+
7
+ ## 准备
8
+
9
+ ### 1. 修改 manifest
10
+
11
+ 把 `formatVersion` 改为 `2`,并加上 `backend`:
12
+
13
+ ```json
14
+ {
15
+ "formatVersion": 2,
16
+ "minAppVersion": "0.59.0",
17
+ "main": "main.js",
18
+ "backend": {
19
+ "entry": "backend.js",
20
+ "runtime": "node",
21
+ "apiVersion": 1,
22
+ "scope": "application",
23
+ "activation": "on-demand",
24
+ "lifetime": "idle",
25
+ "reason": "读取本机设备状态并显示在插件页面中",
26
+ "systemAccess": ["network"]
27
+ }
28
+ }
29
+ ```
30
+
31
+ 带后台的插件必须声明 `minAppVersion`。这里只列出相关字段,其余字段与普通插件相同。
32
+
33
+ | 字段 | 说明 |
34
+ | ----------------------- | -------------------------------------------------------------------------------------------------------- |
35
+ | `entry` | 保持 `backend.js`,模板的构建脚本会从 `src/backend.ts` 生成它 |
36
+ | `runtime`、`apiVersion` | 保持 `node` 和 `1` |
37
+ | `scope` | `application`:整个应用一个后台;`workspace`:每个工作区一个后台 |
38
+ | `activation` | 什么时候启动,见下表 |
39
+ | `lifetime` | `idle`:空闲一段时间后停止,有调用时再启动;`persistent`:一直运行到作用域结束,意外退出时自动重启 |
40
+ | `reason` | 向用户说明后台用来做什么,1 至 500 字。用户授权时会看到它 |
41
+ | `systemAccess` | 后台会用到的系统能力:`filesystem`(文件)、`network`(网络)、`process`(启动程序),用于告知用户和审核 |
42
+ | `tools` | 需要启动外部程序时声明,见[调用外部程序](#调用外部程序) |
43
+
44
+ | `activation` | 启动时机 | 可搭配的 `scope` |
45
+ | ----------------- | ---------------- | ---------------- |
46
+ | `on-demand` | 界面第一次调用时 | 两者均可 |
47
+ | `app-ready` | Tau 启动后 | `application` |
48
+ | `workspace-ready` | 工作区打开后 | `workspace` |
49
+
50
+ ### 2. 安装 Node.js 类型
51
+
52
+ ```sh
53
+ pnpm add -D @types/node@24.0.0 --save-exact
54
+ ```
55
+
56
+ ### 3. 创建 `src/backend.ts`
57
+
58
+ 见下一节。构建脚本检测到 `backend` 声明后,会把 `src/backend.ts` 单独打包成 `backend.js`,和界面代码一起放进同一个 ZIP。
59
+
60
+ ## 定义后台方法
61
+
62
+ ```ts
63
+ // src/backend.ts
64
+ import { defineBackend } from "@tau/plugins/backend";
65
+ import { platform } from "node:os";
66
+
67
+ export default defineBackend({
68
+ create() {
69
+ return {
70
+ activate(ctx) {
71
+ ctx.rpc.register(
72
+ "system.platform",
73
+ {
74
+ input: {
75
+ parse(value: unknown) {
76
+ if (value !== null) throw new Error("此方法不接受参数");
77
+ return null;
78
+ },
79
+ },
80
+ output: {
81
+ parse(value: unknown) {
82
+ if (typeof value !== "string") throw new Error("平台结果无效");
83
+ return value;
84
+ },
85
+ },
86
+ },
87
+ () => platform()
88
+ );
89
+ },
90
+ };
91
+ },
92
+ });
93
+ ```
94
+
95
+ `ctx.rpc.register(method, schemas, handler)`:
96
+
97
+ - `schemas.input`、`schemas.output` 是带 `parse` 方法的校验器,必须提供。输入不合法时抛错,错误会返回给调用方。也可以直接使用 zod 等提供 `parse` 方法的校验库。
98
+ - `handler(input, { signal })` 处理请求,`signal` 在调用方取消时中止。
99
+ - 请求和结果必须是 JSON,单条消息不超过 1 MiB。
100
+
101
+ 后台代码可以使用 Node.js 的内置模块,第三方 npm 包会被打进 `backend.js`。只有 `@tau/plugins/backend` 由 Tau 提供。
102
+
103
+ ## 在界面中调用
104
+
105
+ ```ts
106
+ const os = await context.backend.call("system.platform", null, { signal });
107
+ ```
108
+
109
+ | 方法 | 说明 |
110
+ | ---------------------------------- | ----------------------------------------------------------- |
111
+ | `call(method, input, { signal? })` | 调用后台方法,返回结果 |
112
+ | `subscribe(event, listener)` | 订阅后台推送的事件,返回 `Disposable` |
113
+ | `getState()` | 查询后台状态:`stopped`、`starting`、`running`、`failed` 等 |
114
+
115
+ 后台用 `ctx.rpc.emit(event, value)` 推送事件,界面用 `subscribe` 接收。插件停用或切换工作区后,旧的连接失效。
116
+
117
+ 不要在界面代码里 `import` 后台文件,两者只通过消息通信。
118
+
119
+ ## 调用外部程序
120
+
121
+ 先在 manifest 的 `backend` 中声明 `process` 能力和要用到的程序:
122
+
123
+ ```json
124
+ {
125
+ "backend": {
126
+ "systemAccess": ["process"],
127
+ "tools": [{ "id": "git", "command": "git", "args": ["--no-pager"] }]
128
+ }
129
+ }
130
+ ```
131
+
132
+ | 工具字段 | 说明 |
133
+ | --------- | -------------------------------------- |
134
+ | `id` | 工具 ID,以小写字母开头 |
135
+ | `command` | 程序名或绝对路径 |
136
+ | `args` | 可选。固定的前置参数,每次调用都会带上 |
137
+
138
+ 最多声明 32 个工具。然后在后台中调用:
139
+
140
+ ```ts
141
+ const result = await ctx.process.run("git", ["status", "--short"], { signal: request.signal });
142
+ // result.exitCode、result.stdout、result.stderr
143
+ ```
144
+
145
+ - 参数按数组原样传递,不经过 Shell,不需要也不能使用引号、管道等 Shell 语法。
146
+ - 单次执行最长 30 秒,stdout 与 stderr 合计最多 512 KiB。
147
+ - 在 Windows 上,`command` 必须能解析到唯一的 `.exe` 文件。
148
+ - 需要取消时,把 RPC 请求的 `signal` 传给 `run`。
149
+
150
+ ## 生命周期与清理
151
+
152
+ - 不要在模块顶层启动任务,在 `activate` 中开始。
153
+ - 计时器、连接等资源交给 `ctx.register` 托管,后台停止时会自动清理。
154
+ - 长时间运行的任务要响应 `ctx.signal`。
155
+ - `ctx.workspaceId` 在 `workspace` 作用域下是当前工作区,在 `application` 作用域下为 `null`。
156
+
157
+ ## 授权与审核
158
+
159
+ - 用户首次运行插件的后台前,Tau 会请求本机执行授权。安装插件不等于授权,`app-ready` 等自动启动在没有授权时会等待用户处理。
160
+ - 插件更新后,用户需要重新确认授权。
161
+ - 插件中心会人工审核每个带后台的版本。
162
+ - `systemAccess` 用于告知用户和审核,不是操作系统级的权限限制,后台进程实际上可以访问本机的文件和网络。请如实声明。
163
+
164
+ ## 发布
165
+
166
+ 后台和界面在同一个 ZIP 中,作为一个整体更新,不能只替换 `backend.js`。构建、安装和发布流程与普通插件相同,见[快速开始](getting-started.md)和[发布与更新](release.md)。
167
+
168
+ 插件的管理页面会显示后台的状态、错误、启动耗时和资源占用,便于用户和你排查问题。后台的 stdout、stderr 不会写入 Tau 的日志,需要诊断信息时通过 RPC 返回给界面。