@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +20 -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 +590 -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 +336 -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 +38 -1
  31. package/types/packages/db-schema/content-resources.d.ts +76 -0
  32. package/types/packages/editor/contracts.d.ts +1 -1
  33. package/types/packages/editor/source-contracts.d.ts +41 -1
  34. package/types/packages/editor/src/core/api/extension.d.ts +15 -0
  35. package/types/packages/editor/src/core/api/index.d.ts +5 -1
  36. package/types/packages/editor/src/core/api/resources.d.ts +6 -0
  37. package/types/packages/editor/src/core/components/ElementFrame/index.d.ts +25 -0
  38. package/types/packages/editor/src/core/utils/blockKeyTarget.d.ts +9 -0
  39. package/types/packages/plugins/src/categories.d.ts +3 -0
  40. package/types/packages/plugins/src/contract.d.ts +0 -2
  41. package/types/packages/plugins/src/editor.d.ts +1 -0
  42. package/types/packages/plugins/src/element.d.ts +11 -0
  43. package/types/packages/plugins/src/index.d.ts +4 -1
  44. package/types/packages/plugins/src/media.d.ts +69 -0
  45. package/types/packages/plugins/src/package.d.ts +2 -0
  46. package/types/packages/ui/src/index.d.ts +1 -0
  47. package/types/packages/ui/src/primitives/Sortable.d.ts +32 -0
  48. package/types/packages/ui/src/primitives/Tabs.d.ts +1 -3
  49. package/AI.md +0 -116
  50. package/API.md +0 -881
  51. package/BACKEND.md +0 -68
  52. package/DEVELOPMENT.md +0 -173
  53. package/EDITOR.md +0 -192
  54. package/FLASHCARDS.md +0 -92
  55. package/IMAGES.md +0 -16
  56. package/MCP.md +0 -118
  57. package/NOTIFICATIONS.md +0 -48
  58. package/PUBLISH-DATA.md +0 -48
  59. package/PUBLISH.md +0 -50
  60. package/SERVICES.md +0 -119
  61. package/SYNC.md +0 -37
  62. package/TTS.md +0 -20
  63. package/UPLOAD.md +0 -35
  64. package/VIDEO.md +0 -65
  65. package/WHITEBOARD.md +0 -115
  66. package/types/packages/plugins/src/skills.d.ts +0 -8
package/docs/skills.md ADDED
@@ -0,0 +1,129 @@
1
+ # Skill
2
+
3
+ Skill 是给 Tau 的 AI 助手看的使用说明:什么时候该做什么、按什么步骤做,必要时还可以附带参考资料和脚本。Tau 的 Skill 使用通用的 Agent Skills 目录格式,和 Claude Code、Codex 等工具里的 Skill 是同一种东西。
4
+
5
+ 你可以用两种方式分发 Skill:
6
+
7
+ | 方式 | 适合 | 用户看到的 |
8
+ | ----------------------------------- | ------------------------------------------------ | -------------------------------- |
9
+ | [随插件附带](#随插件附带) | 插件提供了块、工具或页面,需要教 AI 怎么配合使用 | Skill 随插件启用出现,停用后消失 |
10
+ | [独立的 Skill 包](#独立的-skill-包) | 一套写作规范、工作流程等,不需要任何插件代码 | 从插件中心安装后,Skill 直接可用 |
11
+
12
+ 两种方式的 Skill 目录写法完全相同。
13
+
14
+ ## 一个 Skill 目录
15
+
16
+ ```text
17
+ skills/
18
+ chart-authoring/
19
+ SKILL.md # 必需:说明正文
20
+ references/ # 可选:参考资料,AI 需要时再读
21
+ chart-types.md
22
+ scripts/ # 可选:AI 可以执行的脚本
23
+ validate.py
24
+ ```
25
+
26
+ - `skills/` 下的每个子目录是一个 Skill,目录名就是 Skill 的 ID,只能使用字母、数字、点、横线和下划线。
27
+ - 一个包最多 32 个 Skill。
28
+ - 目录里的文件会原样打进插件包。以 `.` 开头的文件、`node_modules` 和 `__pycache__` 会被忽略;不能包含符号链接。
29
+
30
+ ### SKILL.md
31
+
32
+ ```markdown
33
+ ---
34
+ name: 图表写作
35
+ description: 用户要在文档里插入或修改图表块时加载,说明图表块的数据格式和常见写法
36
+ ---
37
+
38
+ # 图表写作
39
+
40
+ ## 什么时候用
41
+
42
+ ……
43
+
44
+ ## 步骤
45
+
46
+ 1. ……
47
+ ```
48
+
49
+ 开头的 front matter 决定 Skill 在 Tau 中如何显示和调用:
50
+
51
+ | 字段 | 说明 |
52
+ | -------------------------- | -------------------------------------------------------------- |
53
+ | `name` | 可选。Skill 页显示的名称,省略时使用目录名 |
54
+ | `description` | 必需。AI 根据它判断什么时候加载这份 Skill |
55
+ | `disable-model-invocation` | 可选。为 `true` 时 AI 不会自动加载,只在用户用 `$` 点名时使用 |
56
+ | `allowed-tools` | 可选。与 Claude Code 的同名字段一致,列出 Skill 建议使用的工具 |
57
+
58
+ 缺少 `description` 时构建会失败,插件中心也会拒绝提交。
59
+
60
+ ### 附带文件与脚本
61
+
62
+ - **参考资料**:AI 加载 Skill 后,可以按需读取目录里的其他文件。在 SKILL.md 里写清楚什么情况下该读哪个文件,比把所有内容塞进 SKILL.md 更省上下文。
63
+ - **脚本**:AI 可以执行目录里的脚本,工作目录就是这个 Skill 目录。按扩展名选择解释器:`.py` 用 python3,`.js`、`.mjs`、`.cjs` 用 Node.js,`.sh` 用 bash,`.ts` 用 tsx;其他文件需要有可执行权限。
64
+ - 每次执行脚本前,Tau 都会请用户确认。单次执行最长 120 秒,输出最多保留 64 KiB。
65
+ - 脚本以用户身份运行,没有沙箱。Tau 不会替用户安装 Python 等运行环境,需要时在 README 里写清楚。
66
+ - **带脚本的版本需要插件中心人工审核**(包括 `scripts/` 下的任何文件,以及目录里的 `.js`、`.mjs`、`.cjs`、`.ts`、`.py`、`.sh` 文件),不能通过自动发布直接上架。只含文档的 Skill 不受此限制。
67
+
68
+ ## 随插件附带
69
+
70
+ 在插件项目根目录建一个 `skills/` 目录,放进你的 Skill,然后照常 `pnpm build`:
71
+
72
+ ```text
73
+ my-plugin/
74
+ manifest.json
75
+ src/index.tsx
76
+ skills/
77
+ chart-authoring/
78
+ SKILL.md
79
+ ```
80
+
81
+ 构建脚本会把 `skills/` 下的每个目录打进插件包,并在打包后的 manifest 中自动写入 `skills` 字段和文件摘要,你不需要改 manifest 或插件代码。
82
+
83
+ - 插件启用后,Skill 出现在 Tau 的 Skill 页,标注「插件 · 插件名」;插件停用后自动消失。
84
+ - 用户可以在 Skill 页调整它的调用方式(自动、仅手动、停用),或者复制一份到自己的 Skill 目录修改。
85
+ - AI 引用它时使用 `插件ID:目录名`,例如 `yourname.chart:chart-authoring`。
86
+ - 你的插件提供了 [MCP 工具](mcp.md)时,可以在 SKILL.md 里说明什么时候调用哪个工具、参数怎么填。
87
+
88
+ ## 独立的 Skill 包
89
+
90
+ 不需要任何插件代码时,把 Skill 单独做成一个包。用插件模板创建项目,删掉 `src/` 目录,把 `manifest.json` 改成:
91
+
92
+ ```json
93
+ {
94
+ "formatVersion": 1,
95
+ "id": "yourname.writing-style",
96
+ "name": "写作风格",
97
+ "description": "让 AI 按统一的风格写文章和卡片。",
98
+ "categories": ["skill"],
99
+ "keywords": ["写作", "风格"],
100
+ "version": "1.0.0",
101
+ "minAppVersion": "0.59.0",
102
+ "apiVersion": 1,
103
+ "platform": "desktop"
104
+ }
105
+ ```
106
+
107
+ 和代码插件相比,Skill 包**没有** `main`、`format`,也不能声明 `permissions`、`backend`、`editor`、`whiteboard`。构建脚本看到 manifest 里没有 `main`,就会跳过代码打包,只打包 `skills/` 目录。
108
+
109
+ ```text
110
+ writing-style/
111
+ manifest.json
112
+ README.md
113
+ skills/
114
+ writing-style/
115
+ SKILL.md
116
+ ```
117
+
118
+ - `pnpm build` 和 `pnpm install:local` 的用法与代码插件相同。安装后在插件页点「重新扫描」,Skill 就会出现在 Skill 页,标注「Skill 包 · 包名」。
119
+ - Skill 包不参与插件启停,用户在 Skill 页逐个调整调用方式或停用。
120
+ - 发布流程与代码插件完全相同,见[发布与更新](release.md)。分类建议填 `skill`。
121
+ - README 照样是给用户看的:这套 Skill 能让 AI 做什么,用户怎么点名使用。
122
+
123
+ ## 写好一份 Skill
124
+
125
+ - **`description` 决定 AI 会不会在合适的时候用它**。写"在什么情况下加载",而不是"这份文档是什么"。例如"用户要求生成周报时加载",而不是"周报指南"。
126
+ - **正文写给 AI 看**:什么时候用、具体步骤、参数含义、正确与错误的例子。
127
+ - **只写与这件事相关的内容**。通用的写作建议对 AI 没有帮助,反而占用上下文。
128
+ - **大段参考资料放进单独的文件**,在 SKILL.md 中说明何时读取。
129
+ - **脚本要能独立运行**:参数通过命令行传入,结果打印到标准输出,失败时以非 0 退出码结束,并输出可读的错误信息。
@@ -0,0 +1,62 @@
1
+ # 存储与网络
2
+
3
+ ## 私有存储
4
+
5
+ `context.storage` 是插件自己的键值存储,适合保存设置、进度、缓存等小块数据。
6
+
7
+ ```ts
8
+ const saved = await context.storage.get("count");
9
+ const count = typeof saved === "number" ? saved : 0;
10
+ await context.storage.set("count", count + 1);
11
+ ```
12
+
13
+ | 方法 | 说明 |
14
+ | ----------------- | ------------------------------ |
15
+ | `get(key)` | 读取,不存在时返回 `undefined` |
16
+ | `set(key, value)` | 保存或覆盖 |
17
+ | `delete(key)` | 删除 |
18
+
19
+ ### 数据保存在哪里
20
+
21
+ - 按插件和工作区隔离:同一个插件在不同工作区读到的是各自的数据,也读不到其他插件的数据。
22
+ - 保存在工作区的笔记库里,随云同步。用户换一台设备,也能读到同样的数据。
23
+ - 停用、重启、更新插件都不会删除数据。用户卸载插件时可以选择是否一并删除。
24
+ - 数据跟随插件 ID。改了 ID,就成了另一个插件,读不到原来的数据。
25
+
26
+ ### 能存什么
27
+
28
+ 值必须是 JSON:`null`、布尔值、字符串、有限数字,以及由它们组成的数组和普通对象。不能存 `undefined`、函数、`Date`、`Map`、`NaN`、`Infinity` 或循环引用;日期先转成字符串或时间戳。
29
+
30
+ 单个值不超过 1 MB。更大的内容(比如一篇长文)放进[插件内容](user-data.md#插件内容)的正文里。
31
+
32
+ ### 使用要点
33
+
34
+ - **读取后校验**。`get` 返回的是 `JsonValue`,不带类型保证。数据可能来自旧版本的插件,也可能在其他设备上被修改过,使用前先检查结构,不符合时用默认值或提示错误。
35
+ - **改了要保存**。修改 `get` 返回的对象不会自动写回,必须再调用 `set`。
36
+ - **读写不是原子的**。"先 `get` 再 `set`"之间如果有另一次写入,后写的会覆盖先写的。保存期间禁用重复提交,模板的计数器按钮就是这样做的。
37
+ - **兼顾新旧版本**。用户可以把插件恢复到上一版,但数据不会跟着回退。改变数据结构时,让新版本能读懂旧数据,也尽量让旧版本读到新数据时不出错(例如只新增字段,不改已有字段的含义)。
38
+ - **不要存凭据**。账号、Token 这类配置会随笔记库同步,不应放在这里。服务提供方的账号配置使用专门的配置保存接口,见[服务提供方](providers/README.md)。
39
+
40
+ ## 网络请求
41
+
42
+ 用 `context.fetch` 发起网络请求。它的参数和返回值与标准的 `fetch` 相同:
43
+
44
+ ```ts
45
+ const response = await context.fetch("https://api.example.com/status", {
46
+ headers: { Authorization: `Bearer ${token}` },
47
+ });
48
+ if (!response.ok) throw new Error(`请求失败:${response.status}`);
49
+ const data: unknown = await response.json();
50
+ ```
51
+
52
+ 与直接调用浏览器的 `fetch` 相比:
53
+
54
+ - 请求由 Tau 代为发出,不受浏览器跨域(CORS)限制,也不会携带浏览器的 Cookie。需要认证时,自己在请求头里提供。
55
+ - 插件停用时,进行中的请求会被自动取消。你也可以照常传入自己的 `signal`,两者任一中止都会取消请求。
56
+
57
+ 注意:
58
+
59
+ - 和标准 `fetch` 一样,HTTP 4xx、5xx 不会抛出异常,要检查 `response.ok`。
60
+ - 响应数据先校验再使用,尤其是写入存储之前。
61
+ - 不要把私人的 API Key 写进源码。需要用户自己的凭据时,让用户在设置中填写。
62
+ - 能读到的用户数据都可以通过网络发送出去。只在功能确实需要时这样做,并在 README 里向用户说明。
package/docs/ui.md ADDED
@@ -0,0 +1,336 @@
1
+ # 界面
2
+
3
+ 插件可以通过四种方式出现在 Tau 的界面里:
4
+
5
+ - **命令**:出现在 Ctrl+K 命令面板中,用户搜索后执行。
6
+ - **页面**:一个完整的 React 页面,和 Tau 自己的页面一样参与前进、后退。
7
+ - **侧边栏入口**:在侧边栏放一个条目,点击打开某个页面。
8
+ - **设置**:插件详情页「设置」中的一个区域。
9
+
10
+ 另外,插件可以随时发送**通知**。
11
+
12
+ 这些都在 `activate(context)` 里注册,插件停用时自动撤销。
13
+
14
+ ## 命令
15
+
16
+ ```ts
17
+ context.commands.register({
18
+ id: "reset-count",
19
+ title: "重置计数",
20
+ run: async () => {
21
+ await context.storage.set("count", 0);
22
+ return null;
23
+ },
24
+ });
25
+ ```
26
+
27
+ | 字段 | 说明 |
28
+ | ------- | ------------------------------------------------------------------- |
29
+ | `id` | 局部 ID,见 [ID 规则](plugin-basics.md#id-规则) |
30
+ | `title` | 命令标题,用户在命令面板里按它搜索 |
31
+ | `run` | 执行函数,可以是异步的。返回值必须是 JSON 值,没有结果时返回 `null` |
32
+
33
+ - 注册时不会执行命令,用户选中它时才调用 `run`。
34
+ - `run` 的参数 `input` 不保证有内容,命令面板执行时不会传自定义参数。需要用户输入时,打开一个页面或设置让用户填写。
35
+ - `run` 抛出异常表示执行失败。耗时较长的命令应该在界面上给出进度或结果反馈。
36
+
37
+ ## 页面
38
+
39
+ ```tsx
40
+ import type { PluginPageProps } from "@tau/plugins";
41
+
42
+ function StatsPage({ context }: PluginPageProps) {
43
+ return <h1>统计</h1>;
44
+ }
45
+
46
+ // activate 内
47
+ const stats = context.pages.register({ id: "stats", title: "统计", component: StatsPage });
48
+ await stats.open();
49
+ ```
50
+
51
+ | 字段 | 说明 |
52
+ | ----------- | -------------------------------------------------------------------------- |
53
+ | `id` | 局部 ID |
54
+ | `title` | 页面标题 |
55
+ | `component` | React 组件,收到 `context`、`params`、`pageContext` 三个属性 |
56
+ | `match` | 可选。默认 `"single"`,页面只有一个实例;`"params"` 表示按参数区分多个实例 |
57
+ | `getId` | 可选。`match: "params"` 时,用它从参数算出实例身份;默认按全部参数区分 |
58
+
59
+ `register` 返回页面句柄,提供 `open(params?)` 和 `dispose()`。用 `open` 打开页面,不要自己操作浏览器地址或历史记录。
60
+
61
+ ### 带参数的页面
62
+
63
+ 同一个页面需要展示不同对象(例如不同的书)时,使用 `match: "params"`:
64
+
65
+ ```tsx
66
+ function BookPage({ params }: PluginPageProps) {
67
+ return <h1>书籍:{String(params.bookId)}</h1>;
68
+ }
69
+
70
+ const book = context.pages.register({
71
+ id: "book",
72
+ title: "书籍",
73
+ component: BookPage,
74
+ match: "params",
75
+ getId: params => String(params.bookId),
76
+ });
77
+
78
+ await book.open({ bookId: "42" });
79
+ ```
80
+
81
+ - 参数是一个扁平对象,值只能是字符串、有限数字或布尔值,不支持数组、嵌套对象和 `null`。
82
+ - 已经打开的同一实例会被复用,不会重复打开。
83
+ - 有了 `getId`,`{ bookId: "42", tab: "notes" }` 和 `{ bookId: "42" }` 会打开同一个实例;没有 `getId` 时它们是两个实例。
84
+
85
+ ### 向 AI 提供页面上下文
86
+
87
+ 用户在你的页面上和 AI 对话时,AI 可以通过 Tau 内置的「获取当前上下文」工具了解用户正在看什么。用 `pageContext.register` 告诉 AI 当前页面的状态:
88
+
89
+ ```tsx
90
+ import { useEffect, useState } from "react";
91
+ import type { PluginPageProps } from "@tau/plugins";
92
+
93
+ function BookPage({ params, pageContext }: PluginPageProps) {
94
+ const [chapter, setChapter] = useState(1);
95
+
96
+ useEffect(() => {
97
+ const registration = pageContext.register(() => ({
98
+ bookId: String(params.bookId),
99
+ chapter,
100
+ }));
101
+ return () => {
102
+ registration.dispose();
103
+ };
104
+ }, [pageContext, params.bookId, chapter]);
105
+
106
+ // ...
107
+ }
108
+ ```
109
+
110
+ - 提供函数返回任意 JSON 值,内容和形状完全由你决定。它应该只读取现有状态,不发起请求,也不修改数据。
111
+ - 状态变化时像示例一样重新注册,避免提供函数读到旧值。
112
+ - 每个页面实例同一时间只有一个提供函数。页面隐藏、卸载或插件停用时会自动清理。
113
+ - 只有当前显示的页面实例会被读取。
114
+
115
+ 页面上下文只是让 AI 知道用户在看什么。需要让 AI 能对页面执行操作,另外用 [MCP 工具](mcp.md)提供。
116
+
117
+ ## 侧边栏
118
+
119
+ 侧边栏条目关联一个已注册的页面,点击时打开它,并在该页面显示时高亮。
120
+
121
+ ```ts
122
+ import { SIDEBAR_GROUPS } from "@tau/plugins";
123
+
124
+ const page = context.pages.register({ id: "counter", title: "计数", component: CounterPage });
125
+ context.sidebar.registerItem({
126
+ id: "counter",
127
+ title: "计数",
128
+ icon: CounterIcon,
129
+ group: SIDEBAR_GROUPS.daily,
130
+ page,
131
+ });
132
+ ```
133
+
134
+ | 字段 | 说明 |
135
+ | -------- | -------------------------------------- |
136
+ | `id` | 局部 ID |
137
+ | `title` | 入口名称 |
138
+ | `page` | 本次启用中注册的页面句柄 |
139
+ | `params` | 可选。点击时传给页面的参数 |
140
+ | `icon` | 可选。图标组件,省略时使用默认插件图标 |
141
+ | `group` | 可选。所在分组,省略时放进「插件」分组 |
142
+
143
+ ### 分组
144
+
145
+ 放进 Tau 已有的分组时,使用 `SIDEBAR_GROUPS` 常量:
146
+
147
+ | 常量 | 分组 |
148
+ | -------------------------- | ---- |
149
+ | `SIDEBAR_GROUPS.daily` | 日常 |
150
+ | `SIDEBAR_GROUPS.knowledge` | 知识 |
151
+ | `SIDEBAR_GROUPS.learning` | 学习 |
152
+ | `SIDEBAR_GROUPS.ai` | AI |
153
+ | `SIDEBAR_GROUPS.sharing` | 分享 |
154
+ | `SIDEBAR_GROUPS.plugins` | 插件 |
155
+
156
+ 也可以创建自己的分组,它会显示在 Tau 的分组之后:
157
+
158
+ ```ts
159
+ const group = context.sidebar.registerGroup({ id: "habits", title: "习惯" });
160
+ context.sidebar.registerItem({ id: "water", title: "喝水", group: group.id, page });
161
+ ```
162
+
163
+ 引用自建分组时使用返回的 `group.id`,不要直接写 `"habits"`。没有条目的分组不显示;分组被撤销后,其中的条目移到「插件」分组。
164
+
165
+ 同一个带参数的页面可以注册多个入口,分别传不同的 `params`。
166
+
167
+ ### 图标
168
+
169
+ `icon` 接收组件本身(`icon: CounterIcon`),不是 `<CounterIcon />`,也不是图片地址。图标是一个线性单色的 SVG 组件:
170
+
171
+ ```tsx
172
+ import type { PluginIconProps } from "@tau/plugins";
173
+
174
+ export function WaterDropIcon({ size = 24, strokeWidth = 2, ...props }: PluginIconProps) {
175
+ return (
176
+ <svg
177
+ width={size}
178
+ height={size}
179
+ viewBox="0 0 24 24"
180
+ fill="none"
181
+ stroke="currentColor"
182
+ strokeWidth={strokeWidth}
183
+ strokeLinecap="round"
184
+ strokeLinejoin="round"
185
+ {...props}
186
+ >
187
+ <path d="M12 3C9 7 5 11 5 15a7 7 0 0 0 14 0c0-4-4-8-7-12Z" />
188
+ </svg>
189
+ );
190
+ }
191
+ ```
192
+
193
+ - Tau 会传入 `size`、`strokeWidth`、`className` 和 `aria-hidden`,图标要把它们透传给 `<svg>`。
194
+ - 颜色用 `currentColor`,这样会跟随明暗主题和选中状态。不要写死宽高和颜色。
195
+ - 也可以直接使用 [lucide-react](https://lucide.dev) 等图标库的组件(先 `pnpm add lucide-react`)。
196
+
197
+ ## 设置
198
+
199
+ ```tsx
200
+ import type { PluginSettingsProps } from "@tau/plugins";
201
+
202
+ function Preferences({ context }: PluginSettingsProps) {
203
+ // 用 context.storage 读写设置值
204
+ }
205
+
206
+ const preferences = context.settings.register({
207
+ id: "preferences",
208
+ title: "计数设置",
209
+ description: "调整计数的步长",
210
+ component: Preferences,
211
+ });
212
+ ```
213
+
214
+ | 字段 | 说明 |
215
+ | ------------- | --------------------------------------------------------------- |
216
+ | `id` | 局部 ID |
217
+ | `title` | 设置区域的标题 |
218
+ | `description` | 可选。说明文字 |
219
+ | `component` | React 组件,收到 `context` |
220
+ | `target` | 服务提供方的配置表单才需要,见[服务提供方](providers/README.md) |
221
+
222
+ 设置区域显示在插件详情页的「设置」里。`register` 返回的句柄提供 `open()`,可以从命令或页面里跳转过去:
223
+
224
+ ```ts
225
+ context.commands.register({
226
+ id: "open-preferences",
227
+ title: "打开计数设置",
228
+ run: async () => {
229
+ await preferences.open();
230
+ return null;
231
+ },
232
+ });
233
+ ```
234
+
235
+ 设置值不会自动保存,组件需要自己用 [`context.storage`](storage-and-network.md) 读写。页面和设置同时显示同一份数据时,保存后要刷新另一处的状态,`storage` 本身不会通知变化。模板用 React Query 管理读取、保存和失败状态,可以照着写。
236
+
237
+ ## 通知
238
+
239
+ ```ts
240
+ context.notifications.send({
241
+ title: "导入完成",
242
+ body: "新增了 12 条摘录。",
243
+ tone: "ok",
244
+ });
245
+ ```
246
+
247
+ | 字段 | 说明 |
248
+ | --------- | ---------------------------------------------- |
249
+ | `title` | 标题,必填,最多 200 字 |
250
+ | `body` | 可选。正文,最多 5000 字 |
251
+ | `tone` | 可选。`neutral`(默认)、`ok`、`danger` |
252
+ | `mode` | 可选。通知出现的位置,见下表 |
253
+ | `sticky` | 可选。为 `true` 时一直显示,直到用户处理或关闭 |
254
+ | `actions` | 可选。最多两个按钮,其中最多一个主按钮 |
255
+
256
+ | `mode` | 出现的位置 |
257
+ | -------------- | ------------------------------------------------------ |
258
+ | `auto`(默认) | Tau 窗口在前台时显示在窗口内,否则显示在独立的通知窗口 |
259
+ | `in-app` | 总是显示在窗口内 |
260
+ | `system` | 总是显示在独立的通知窗口 |
261
+
262
+ 通知的来源固定显示为你的插件名称。
263
+
264
+ ### 带按钮的通知
265
+
266
+ ```ts
267
+ context.notifications.send({
268
+ title: "扫描完成",
269
+ body: "发现 3 处断开的链接。",
270
+ sticky: true,
271
+ actions: [
272
+ {
273
+ label: "查看",
274
+ primary: true,
275
+ onClick: async () => {
276
+ await reportPage.open();
277
+ },
278
+ },
279
+ ],
280
+ });
281
+ ```
282
+
283
+ | 按钮字段 | 说明 |
284
+ | -------------- | --------------------------------------------------------- |
285
+ | `label` | 按钮文字,最多 100 字 |
286
+ | `onClick` | 点击时执行,可以是异步的;执行期间按钮显示加载状态 |
287
+ | `primary` | 可选。是否为主按钮 |
288
+ | `resolvedText` | 可选。`onClick` 成功后在通知里显示的结果文字,最多 500 字 |
289
+
290
+ `onClick` 失败时,错误显示在通知里。插件停用后,旧通知上的按钮不再可用。
291
+
292
+ ### 撤销通知
293
+
294
+ `send` 返回一个 `Disposable`,调用 `dispose()` 撤销这一条通知。插件停用时,它发出的通知会自动撤销。
295
+
296
+ 发送通知不需要权限,也不需要用户操作触发,适合在异步任务完成时提醒用户。返回句柄只表示通知已经发出,不代表用户已经看到。
297
+
298
+ ## 界面组件与样式
299
+
300
+ 插件界面用 React 编写,可以使用 `@tau/ui` 提供的组件,它们会自动适配 Tau 的主题:
301
+
302
+ ```tsx
303
+ import { useState } from "react";
304
+ import { Button, Dialog, DialogBody, Panel } from "@tau/ui";
305
+
306
+ export function Help() {
307
+ const [open, setOpen] = useState(false);
308
+ return (
309
+ <Panel>
310
+ <Button variant="ghost" onClick={() => setOpen(true)}>
311
+ 查看说明
312
+ </Button>
313
+ <Dialog open={open} onOpenChange={setOpen} title="使用说明">
314
+ <DialogBody>数据保存在当前工作区。</DialogBody>
315
+ </Dialog>
316
+ </Panel>
317
+ );
318
+ }
319
+ ```
320
+
321
+ | 组件 | 常用属性 |
322
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
323
+ | `Button` | `onClick`、`disabled`、`loading`、`loadingText`;`variant` 可选 `primary`、`secondary`、`soft`、`ghost`、`danger`、`danger-soft` |
324
+ | `Panel` | 内容容器 |
325
+ | `Dialog` | `open`、`onOpenChange`、`title`,`footer` 放操作按钮 |
326
+ | `DialogBody` | 弹窗正文 |
327
+ | `Sortable` | 拖动排序:`items`、`getKey`、`onMove(from, to)`、`renderItem(item, state)`,把 `state.handleProps` 展开到拖动手柄上;手柄获得焦点时方向键前后移动 |
328
+
329
+ 完整的组件列表和属性见 `@tau/ui` 的类型提示。
330
+
331
+ 自己写的样式放在 CSS 文件里,在入口中 `import`:
332
+
333
+ - 选择器加上插件专属前缀(如 `.yourname-counter`),不要修改 `body` 或 Tau 自身元素的样式。
334
+ - 颜色、圆角等使用 Tau 的主题变量(如 `var(--tau-accent)`),这样会跟随用户的主题切换。
335
+ - SVG 图片可以直接在 CSS 里用 `url()` 引用,构建时会内联进样式,不需要额外复制文件。
336
+ - 如果你用 Tailwind 构建插件样式,可以导入 `@tau/plugins/tailwind.css`,它把 Tau 的主题变量映射成 Tailwind 语义类(如 `bg-surface`、`text-ink-1`)。Tau 不会替插件生成 Tailwind 样式,需要你在构建时自己编译。