@vetta-org/plugin-sdk 0.2.0 → 0.3.1
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/dist/agent.d.ts +0 -12
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/ai.d.ts +13 -0
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js.map +1 -1
- package/dist/app-actions.d.ts +0 -1
- package/dist/app-actions.d.ts.map +1 -1
- package/dist/app-actions.js.map +1 -1
- package/dist/cli-provider.d.ts +18 -0
- package/dist/cli-provider.d.ts.map +1 -0
- package/dist/cli-provider.js +2 -0
- package/dist/cli-provider.js.map +1 -0
- package/dist/context.d.ts +13 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest-schema.d.ts +454 -51
- package/dist/manifest-schema.d.ts.map +1 -1
- package/dist/manifest-schema.js +139 -33
- package/dist/manifest-schema.js.map +1 -1
- package/dist/manifest.d.ts +2 -2
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +164 -38
- package/dist/manifest.js.map +1 -1
- package/dist/models.d.ts +35 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +2 -0
- package/dist/models.js.map +1 -0
- package/dist/ocr.d.ts +78 -0
- package/dist/ocr.d.ts.map +1 -0
- package/dist/ocr.js +2 -0
- package/dist/ocr.js.map +1 -0
- package/dist/official.d.ts +1 -0
- package/dist/official.d.ts.map +1 -1
- package/dist/official.js.map +1 -1
- package/dist/permissions.d.ts +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +8 -0
- package/dist/permissions.js.map +1 -1
- package/dist/secrets.d.ts +27 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +2 -0
- package/dist/secrets.js.map +1 -0
- package/dist/service-provider.d.ts +62 -0
- package/dist/service-provider.d.ts.map +1 -0
- package/dist/service-provider.js +2 -0
- package/dist/service-provider.js.map +1 -0
- package/dist/storage.d.ts +39 -6
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +8 -1
- package/dist/storage.js.map +1 -1
- package/dist/ui.d.ts +118 -7
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js.map +1 -1
- package/docs/.source.json +22 -0
- package/docs/README.md +104 -0
- package/docs/ability-details.md +424 -0
- package/docs/ai.md +121 -0
- package/docs/app-actions.md +111 -0
- package/docs/browser.md +80 -0
- package/docs/conversation-and-agent.md +619 -0
- package/docs/file-explorer.md +118 -0
- package/docs/getting-started.md +295 -0
- package/docs/guiding-the-agent.md +183 -0
- package/docs/manifest.md +296 -0
- package/docs/mcp.md +152 -0
- package/docs/media.md +141 -0
- package/docs/message-cards.md +188 -0
- package/docs/permissions.md +145 -0
- package/docs/styling-and-pitfalls.md +254 -0
- package/docs/system-plugins.md +58 -0
- package/docs/ui-slots.md +628 -0
- package/package.json +7 -3
- package/dist/settings.d.ts +0 -13
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js +0 -2
- package/dist/settings.js.map +0 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# 消息卡片系统(ADR-0030)
|
|
2
|
+
|
|
3
|
+
消息列表里每条 assistant 消息**下方**可以渲染一组「卡片」。本系统把卡片做成**声明式描述符 + 按 `type` 的动态渲染器注册表**:
|
|
4
|
+
|
|
5
|
+
- **谁产数据**:工具在其结果的 out-of-band `details.cards` 上声明卡片描述符(模型永不可见)。
|
|
6
|
+
- **谁画**:插件经 `ctx.ui.registerCardRenderer({ type, component, ... })` 注册渲染器,按描述符的 `type` 匹配。
|
|
7
|
+
- **谁编排**:宿主(card host)持有每条消息真实的卡片列表,按 `type` 查渲染器、按 `key` 跨轮去重,并在 ≥2 张时套上[收纳 UI](#收纳-ui)。
|
|
8
|
+
|
|
9
|
+
> 这是相对旧模型(「每条消息 mount 全部 slot、各自 `return null` 自隐」)的反转:旧模型宿主不知道某条消息到底渲染了哪几张卡片,tab 收纳与标签都算不出来;现在宿主**声明式地**知道卡片列表。
|
|
10
|
+
|
|
11
|
+
## 数据流
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
工具执行
|
|
15
|
+
└─ 结果带 details.cards: CardDescriptor[] (模型不可见的 out-of-band 通道)
|
|
16
|
+
│
|
|
17
|
+
▼
|
|
18
|
+
宿主 card host(每条消息)
|
|
19
|
+
├─ 收集本条消息所有 tool_call block 的 details.cards(settled 卡)
|
|
20
|
+
├─ 对 pending 的 tool_call,调每个渲染器的 pendingFor() 合成 pending 卡(骨架)
|
|
21
|
+
├─ 按 descriptor.key 跨轮去重(同 key 只在最新一条消息渲染)
|
|
22
|
+
├─ 按 descriptor.type 查 registerCardRenderer 注册的渲染器
|
|
23
|
+
└─ 0 张→不渲染;1 张→裸渲染;≥2 张→收纳 UI(tab/列表)
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
你的渲染器组件 <Component descriptor pending message />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 描述符 CardDescriptor
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
interface CardDescriptor {
|
|
33
|
+
type: string; // 选渲染器(全局唯一、插件自拥,约定以插件 id 前缀,如 "image-gen:preview")
|
|
34
|
+
key?: string; // 跨轮去重:同 key = 同一逻辑卡片,只在最新锚点渲染
|
|
35
|
+
payload?: unknown; // 稳定引用(如 image id / rootId),不是内容快照——渲染器据此解析实时状态
|
|
36
|
+
title?: string; // tab 标签(覆盖注册时的默认 title)
|
|
37
|
+
icon?: string; // icon symbol 字符串(跨 agent→宿主边界序列化,故不是 React 节点)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **`payload` 存引用而非快照**:例如只存 image id,渲染器再据此异步取最新 lineage——这样卡片内容能跟随后续编辑变化。
|
|
42
|
+
- **`key` 驱动跨轮去重**:见 [跨轮去重](#跨轮去重)。
|
|
43
|
+
- **序列化约束**:描述符从工具结果跨进程而来,`title` 是字符串、`icon` 是 symbol 串;**注册时**的默认 `icon` 才可以是 React 节点(它活在插件 bundle 内)。
|
|
44
|
+
|
|
45
|
+
## 注册渲染器 registerCardRenderer
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
interface PluginPendingToolCall {
|
|
49
|
+
toolName: string;
|
|
50
|
+
args: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface PluginCardProps {
|
|
54
|
+
descriptor: CardDescriptor; // 这张卡的数据
|
|
55
|
+
pending: boolean; // true=从在途工具合成的骨架卡(画 skeleton)
|
|
56
|
+
message: ConversationMessage;// 锚定的消息 { id, role, text, timestamp? }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface PluginCardRendererContribution {
|
|
60
|
+
type: string; // 与描述符 type 完全一致
|
|
61
|
+
component: ComponentType<PluginCardProps>;
|
|
62
|
+
title?: string; // 默认 tab 标签
|
|
63
|
+
icon?: ReactNode; // 默认 tab 图标(React 节点)
|
|
64
|
+
pendingFor?: (toolCall: PluginPendingToolCall) => CardDescriptor | null;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- 权限:`ui.slot.message`(缺权限**抛错**)。
|
|
69
|
+
- `type` **不被宿主命名空间化**(与 slot id 不同)——它是插件自拥、全局唯一的 key,描述符与注册必须用**完全相同**的字符串。约定以插件 id 前缀。
|
|
70
|
+
- 一个插件可注册多个渲染器(多种卡片 type)。
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
ctx.ui.registerCardRenderer({
|
|
74
|
+
type: "image-gen:preview",
|
|
75
|
+
component: ImagePreviewCard, // (props: PluginCardProps) => JSX | null
|
|
76
|
+
title: "图像",
|
|
77
|
+
icon: <IconImage className="h-3.5 w-3.5" />,
|
|
78
|
+
pendingFor: pendingPreviewCard, // 见下
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 生成中骨架 pendingFor
|
|
83
|
+
|
|
84
|
+
`details.cards` 只在工具**完成后**才有。为了让「生成中」也占一张卡片/一个 tab,宿主对每个 **pending 的** tool_call 调用每个渲染器的 `pendingFor(toolCall)`:
|
|
85
|
+
|
|
86
|
+
- 返回一个**预备描述符** → 宿主用同一渲染器渲染它(`pending=true`),你画 skeleton。
|
|
87
|
+
- 返回 `null` → 该渲染器不处理这个工具。
|
|
88
|
+
- `pendingFor` 在 render 期间被调用,必须**纯/廉价**(可读插件内存缓存,别做异步)。
|
|
89
|
+
|
|
90
|
+
工具完成后,pending 卡随 block 状态翻为 success 而消失,`details.cards` 的 settled 卡同时接管——**同一 block 上 pending 与 settled 时间互斥**,不会双份。
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
function pendingPreviewCard(toolCall: PluginPendingToolCall): CardDescriptor | null {
|
|
94
|
+
if (toolCall.toolName !== "generate_image" && toolCall.toolName !== "edit_image") return null;
|
|
95
|
+
if (toolCall.toolName === "edit_image") {
|
|
96
|
+
const sourceId = typeof toolCall.args.sourceImageId === "string" ? toolCall.args.sourceImageId : undefined;
|
|
97
|
+
// 同步从插件缓存解析 lineage rootId 作 key,使「上一轮那张卡」在编辑期间隐藏
|
|
98
|
+
const key = sourceId ? cachedRootId(sourceId) : undefined;
|
|
99
|
+
return { type: "image-gen:preview", ...(key ? { key } : {}), payload: { editingImageId: sourceId } };
|
|
100
|
+
}
|
|
101
|
+
return { type: "image-gen:preview", payload: {} }; // 全新生成:独立骨架,无 key
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 跨轮去重
|
|
106
|
+
|
|
107
|
+
`key` 标识「同一张逻辑卡片」。同 `key` 跨轮(跨消息)出现时,宿主**只在其最新锚点**(最后产生该 key 的消息)渲染,旧锚点自动隐藏。
|
|
108
|
+
|
|
109
|
+
典型:图片 A(第 1 轮)→ 编辑成 A′(第 2 轮)。两轮的卡片都 `key = 该 lineage 的 rootId`,于是第 1 轮卡片隐藏、第 2 轮卡片显示完整版本。无 `key` 的卡片不去重、各显各的。
|
|
110
|
+
|
|
111
|
+
> 编辑在途时要让「上一轮卡」立刻隐藏,pending 卡必须带**与 settled 卡相同的 key**(rootId)。`pendingFor` 只能拿到工具 args,需自行从插件缓存把 `sourceImageId → rootId` 同步解析出来。
|
|
112
|
+
|
|
113
|
+
## 收纳 UI
|
|
114
|
+
|
|
115
|
+
宿主按本条消息的可见卡片数自动决定形态:
|
|
116
|
+
|
|
117
|
+
- **0 张**:不渲染。
|
|
118
|
+
- **1 张**:裸渲染该卡片,无任何操作区。
|
|
119
|
+
- **≥2 张**:卡片区上方出现**操作区**——左侧 tab 切换卡片、右侧「列表 / 收纳」两个图标切布局:
|
|
120
|
+
- **收纳(默认)**:tab 切换,一次只显示一张。
|
|
121
|
+
- **列表**:所有卡片纵向平铺。
|
|
122
|
+
- 布局是**临时态、不持久化**:列表是临时形态,卸载 / 切会话即回落收纳。
|
|
123
|
+
|
|
124
|
+
tab 顺序按卡片在消息里出现的顺序,默认激活第一个;tab 标签取 `descriptor.title` → 注册默认 `title` → 回退插件名。
|
|
125
|
+
|
|
126
|
+
## 渲染器组件写法
|
|
127
|
+
|
|
128
|
+
组件是 `descriptor` 的纯函数——**不要**自己探测「是否在生成中」,由 `pending` 决定画 skeleton 还是内容:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import type { PluginCardProps } from "@vetta-org/plugin-sdk";
|
|
132
|
+
|
|
133
|
+
function ImagePreviewCard({ descriptor, pending }: PluginCardProps) {
|
|
134
|
+
const payload = (descriptor.payload ?? {}) as { images?: ImageRef[]; editingImageId?: string };
|
|
135
|
+
if (pending && payload.editingImageId) return <Swiper sourceId={payload.editingImageId} leadingSkeleton />;
|
|
136
|
+
if (pending) return <GenerationSkeleton />;
|
|
137
|
+
const images = payload.images ?? [];
|
|
138
|
+
if (images.length === 0) return null;
|
|
139
|
+
return <Swiper images={images} />;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 第三方插件如何拿到卡片数据
|
|
144
|
+
|
|
145
|
+
卡片的 **settled 数据源是工具结果的 `details.cards`**。三条路径:
|
|
146
|
+
|
|
147
|
+
1. **插件自注册工具返回 `cards`(推荐给插件作者)**:`ctx.agent.registerTool` 的 handler 在返回值里带一个 `cards: CardDescriptor[]` 字段,宿主自动把它**提升**到 `details.cards`,并从模型可见的结果文本里**剔除**(不污染 LLM 上下文)。这样插件**用自己的工具**就能产出消息下方卡片。
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
ctx.agent.registerTool({
|
|
151
|
+
id: "show-regions",
|
|
152
|
+
description: "...",
|
|
153
|
+
parameters: { /* JSON schema */ },
|
|
154
|
+
handler: async (input) => ({
|
|
155
|
+
count: matches.length,
|
|
156
|
+
results: matches.map(summarize), // 模型可见(结果摘要)
|
|
157
|
+
cards: [{ type: "my-plugin:regions", payload: { ids } }], // 被提升到 details.cards,模型不可见
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`demo-map` 即用此法:`demo_map_focus` / `demo_map_present_regions` 返回 `cards`,渲染在当前 turn 消息下方。
|
|
163
|
+
|
|
164
|
+
2. **协同设计的内置工具**:coding-agent 内置工具的 `execute` 直接在 `details.cards` 放描述符(image-gen 的 `generate_image` / `edit_image` 即如此)。内置工具与插件渲染器**成对维护**(「内置 tool 出能力 + plugin 出界面」)。
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// coding-agent 内置工具 execute 返回
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text", text: "已生成图像" }],
|
|
170
|
+
details: { cards: [{ type: "image-gen:preview", key: rootId, payload: { images } }] },
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
3. **`pendingFor` 合成的骨架卡**:对**任意**在途 tool_call,你的渲染器都能合成 pending 卡(它只看 `toolName` / `args`),为执行过程提供骨架反馈。
|
|
175
|
+
|
|
176
|
+
> 三条路径产出的描述符进入**同一个** `details.cards` 通道,由 host 按 `type` 查渲染器、按 `key` 去重、渲染在消息下方(≥2 张走收纳 UI)。`details.cards` 始终模型不可见。
|
|
177
|
+
|
|
178
|
+
## 与 registerToolCallSlot
|
|
179
|
+
|
|
180
|
+
- **消息卡片**(本节):挂在**消息下方**,数据来自 `details.cards` / `cards` 提升。
|
|
181
|
+
- **Tool-call 槽**([ui-slots.md](./ui-slots.md#工具行内渲染-registertoolcallslot)):按 `toolName` **替换工具调用行内**默认 UI。
|
|
182
|
+
- 插件工具**可以**产卡片(返回 `cards`);需要行内自定义 UI 时再用 `registerToolCallSlot`。两者可并用。
|
|
183
|
+
|
|
184
|
+
## 完整参考实现
|
|
185
|
+
|
|
186
|
+
- `packages/plugins/presets/demo-map`:插件自注册工具返回 `cards` + `registerCardRenderer` 渲染——**插件全自包含**的端到端范例。
|
|
187
|
+
- `packages/plugins/presets/image-gen`:`registerCardRenderer` + `pendingFor`,配合 coding-agent 内置 `generate_image` / `edit_image` 在 `details.cards` 产出描述符。
|
|
188
|
+
- `packages/plugins/presets/git`:`registerTurnCard`(本轮变更,非 tool 绑定)——见 [ui-slots Turn 卡](./ui-slots.md#本轮-turn-卡-registerturncard)。
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# 权限
|
|
2
|
+
|
|
3
|
+
插件必须在 `plugin.json` 的 `permissions` 数组里声明它要用的宿主 API。宿主在插件管理页**单独授权**,并在运行时校验。
|
|
4
|
+
|
|
5
|
+
> 权限是能力声明、用户知情同意和宿主 API 门控规范,**不是安全沙箱**。插件与宿主共享 renderer realm;请只安装和启用可信插件。
|
|
6
|
+
|
|
7
|
+
## 声明 → 授权 → 校验
|
|
8
|
+
|
|
9
|
+
1. **声明**:`plugin.json` 列出权限。未声明的权限永远拿不到。
|
|
10
|
+
2. **授权**:用户在设置 → 插件页为该插件勾选授权(系统插件自动全量授予、不可撤,见 [system-plugins.md](./system-plugins.md))。Agent 经 `install-from-path` 安装时,确认后可**按声明一次授予**(见 [getting-started.md](./getting-started.md#安装))。
|
|
11
|
+
3. **校验**:调用受门控的 API 时运行时检查;**未授权**会按下表两种方式之一处理。
|
|
12
|
+
|
|
13
|
+
更新插件(包括 GitHub 能力市场)保留用户已授予且新版本仍声明的权限,不自动授予新增权限,也不恢复用户撤销的权限。
|
|
14
|
+
若旧版市场更新已清空授权,详情页会提示配置面板未显示,可点击「检查插件权限」,在权限页开启「显示详情配置面板」以及所需的其它权限;无需卸载插件或重新授权服务商账号。保存权限后宿主自动重新加载插件。
|
|
15
|
+
宿主无法从空授权记录推断用户原来的选择,因此不会自动全量补授。
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// 运行时也可自查
|
|
19
|
+
ctx.permissions.has("fs.read"); // boolean
|
|
20
|
+
ctx.permissions.require("fs.read"); // 缺则抛 Plugin permission denied: fs.read
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 缺权限的两种行为
|
|
24
|
+
|
|
25
|
+
不同注册点对「声明了但未授权」的处理不同:
|
|
26
|
+
|
|
27
|
+
- **抛错(require)**:`registerInputAction`、`registerCardRenderer`、`registerToolCallSlot`、`registerTurnCard`、`registerShortcutScope`、`openActivityTab`、`setActivityTabVisible`、`setPromptAttachment`、`fileExplorer.*`、`agent.registerContinuationProvider`、`agent.registerSystemPromptProvider`、`conversation.*`、`fs.*`、`network.*`、`storage.*`、`media.*`、`ai.*`、`command.run`。缺权限直接抛 `Plugin permission denied: <permission>`,中断该次调用。
|
|
28
|
+
- **跳过 + 警告(warn+noop)**:`registerGlobalSlot`、`registerFilePreview`、`registerActivityTab`、`registerNewSessionContext`、`agent.registerTool`、`agent.registerHook`、`appActions.register`。缺权限时静默跳过该贡献并打 `console.warn`,**不影响**插件其它已授权能力。
|
|
29
|
+
|
|
30
|
+
> 设计上一个缺失权限不应拖垮插件的其它能力——`activate()` 里建议把可选能力的注册各自独立,避免一处 throw 掉整段。
|
|
31
|
+
|
|
32
|
+
## 已实现权限(有对应 API / 清单面)
|
|
33
|
+
|
|
34
|
+
| 权限 | 门控 | 文档 |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `ui.slot.global` | `ctx.ui.registerGlobalSlot()` | [ui-slots](./ui-slots.md#全局浮层-registerglobalslot) |
|
|
37
|
+
| `ui.slot.workspace-view` | `ctx.ui.registerWorkspaceView()` / `openWorkspaceView()` / `setWorkspaceViewBadge()` | [ui-slots](./ui-slots.md#工作区视图-registerworkspaceview) |
|
|
38
|
+
| `ui.slot.file-preview` | `ctx.ui.registerFilePreview()` | [ui-slots](./ui-slots.md#文件预览-registerfilepreview) |
|
|
39
|
+
| `ui.slot.activity-tab` | `registerActivityTab` / `openActivityTab` / `setActivityTabVisible` | [ui-slots](./ui-slots.md#活动面板-tab-registeractivitytab) |
|
|
40
|
+
| `ui.slot.input-action` | `registerInputAction` / `setPromptAttachment` | [ui-slots](./ui-slots.md#输入栏动作-registerinputaction) |
|
|
41
|
+
| `ui.slot.new-session-context` | `ctx.ui.registerNewSessionContext()` | [ui-slots](./ui-slots.md#新会话上下文区-registernewsessioncontext) |
|
|
42
|
+
| `conversation.draft.read` | 新会话上下文区的 `context.draft`(未授予恒为空串) | 同上 |
|
|
43
|
+
| `ui.slot.message` | `ctx.ui.registerCardRenderer()` | [message-cards](./message-cards.md) |
|
|
44
|
+
| `ui.slot.tool-call` | `ctx.ui.registerToolCallSlot()` | [ui-slots](./ui-slots.md#工具行内渲染-registertoolcallslot) |
|
|
45
|
+
| `ui.slot.turn-card` | `ctx.ui.registerTurnCard()` | [ui-slots](./ui-slots.md#本轮-turn-卡-registerturncard) |
|
|
46
|
+
| `ui.slot.ability-detail` | `ctx.ui.registerAbilityDetailSlot()` | [下方](#尚无专章的能力) |
|
|
47
|
+
| `ui.shortcuts.register` | `ctx.ui.registerShortcutScope()` / `usePluginShortcutScope` | [ui-slots](./ui-slots.md#键盘快捷键-registershortcutscope) |
|
|
48
|
+
| `ui.file-explorer.decorations` | `ctx.fileExplorer.registerDecorationProvider()` | [file-explorer](./file-explorer.md#文件装饰) |
|
|
49
|
+
| `ui.file-explorer.context-menu` | `ctx.fileExplorer.registerContextMenuAction()` | [file-explorer](./file-explorer.md#右键菜单) |
|
|
50
|
+
| `ui.file-explorer.toolbar` | `ctx.fileExplorer.registerToolbarAction()` | [file-explorer](./file-explorer.md#工具栏动作) |
|
|
51
|
+
| `workspace.read` | 文件列表查询、定位、刷新与事件 | [file-explorer](./file-explorer.md#工作区选择与定位) |
|
|
52
|
+
| `agent.session.read` | `ctx.conversation.on()` + 对话 hook | [conversation-and-agent](./conversation-and-agent.md#对话读状态) |
|
|
53
|
+
| `agent.session.write` | `sendPrompt` / `insertText` / `abort` | [conversation-and-agent](./conversation-and-agent.md#对话驾驶) |
|
|
54
|
+
| `agent.command.run` | `ctx.command.run` + 清单 `commands` | [conversation-and-agent](./conversation-and-agent.md#命令执行-command) |
|
|
55
|
+
| `agent.command.spawn` | `ctx.command.spawn`(长驻进程)+ 清单 `commands` | [conversation-and-agent](./conversation-and-agent.md#长驻进程-commandspawn) |
|
|
56
|
+
| `capture.offscreen` | `ctx.capture.offscreen`(主进程离屏窗口截图) | [conversation-and-agent](./conversation-and-agent.md#离屏截图-captureoffscreen) |
|
|
57
|
+
| `agent.skills.control` | 清单 `agent.skillPaths` | [manifest](./manifest.md#agent-agent-侧贡献) |
|
|
58
|
+
| `agent.mcp.control` | 清单 `agent.mcpServers`(三源聚合之插件源) | [mcp](./mcp.md) |
|
|
59
|
+
| `agent.tools.register` | `ctx.agent.registerTool()`(注册 shell) | [conversation-and-agent](./conversation-and-agent.md#注册-agent-工具) |
|
|
60
|
+
| `agent.toolHandler.execute` | 工具 handler 被 agent 调用时执行 | 同上 |
|
|
61
|
+
| `agent.hooks.register` | `ctx.agent.registerHook()`(注册 Coding Agent 生命周期 Hook) | [conversation-and-agent](./conversation-and-agent.md#注册-coding-agent-hook) |
|
|
62
|
+
| `agent.hookHandler.execute` | Coding Agent 到达匹配事件时调用插件 Hook handler | 同上 |
|
|
63
|
+
| `agent.tools.control` | 清单 `agent.toolPolicy`;动态 `setToolEnabled` / `actions.tools.*` | [manifest](./manifest.md#agent-agent-侧贡献) / [conversation-and-agent](./conversation-and-agent.md#注册动态系统提示词-provider) |
|
|
64
|
+
| `agent.systemPrompt.write` | `registerSystemPromptProvider`;仅本插件 block | [conversation-and-agent](./conversation-and-agent.md#注册动态系统提示词-provider) |
|
|
65
|
+
| `agent.systemPrompt.fullControl` | 动态 provider 操作非本插件 block | 同上 |
|
|
66
|
+
| `agent.continuation.register` | `registerContinuationProvider` | [conversation-and-agent](./conversation-and-agent.md#注册-agent-自动续跑策略) |
|
|
67
|
+
| `app.actions.register` | `ctx.appActions.register()`(注册声明) | [app-actions](./app-actions.md) |
|
|
68
|
+
| `app.actionHandler.execute` | Action handler 被本地 Action RPC 调用时执行 | 同上 |
|
|
69
|
+
| `fs.read` | `readDir` / `readFile` / `stat` / `listFilesRecursive` | [conversation-and-agent](./conversation-and-agent.md#文件-api) |
|
|
70
|
+
| `fs.write` | `writeFile` / `rename` / `delete` / `move` / `createDirectory` | 同上 |
|
|
71
|
+
| `network.fetch` | `ctx.network.request` | [conversation-and-agent](./conversation-and-agent.md#网络-api) |
|
|
72
|
+
| `browser.read` | 创建/查询/关闭 session,导航、快照、文本与截图 | [browser](./browser.md) |
|
|
73
|
+
| `browser.open` | `ctx.browser.open`;仅打开宿主内置 Browser Panel,不读取或操作页面 | [browser](./browser.md) |
|
|
74
|
+
| `browser.interact` | `ctx.browser.act`;必须同时声明 `browser.read` | [browser](./browser.md) |
|
|
75
|
+
| `browser.profile.persist` | 创建宿主管理的持久 profile | [browser](./browser.md#多账号-profile) |
|
|
76
|
+
| `browser.attach` | 附着用户自行开启调试的 Chrome | [browser](./browser.md) |
|
|
77
|
+
| `browser.runtime.manage` | 安装/修复浏览器运行时 | [browser](./browser.md) |
|
|
78
|
+
| `storage.read` | `ctx.storage.list/readFile/readSnapshot/readBlob/getBlobRef` | [conversation-and-agent](./conversation-and-agent.md#插件私有存储-api) |
|
|
79
|
+
| `storage.write` | `ctx.storage.writeFile/commit/putBlob/putBlobFromFile` | 同上 |
|
|
80
|
+
| `secrets.read` | `ctx.secrets.get/has/keys` | [conversation-and-agent](./conversation-and-agent.md#密钥-api) |
|
|
81
|
+
| `secrets.write` | `ctx.secrets.set/delete` | 同上 |
|
|
82
|
+
| `media.generate` | `ctx.media.listProviders/createJob/getJob/cancelJob` | [media](./media.md) |
|
|
83
|
+
| `media.provider.register` | `ctx.media.registerProvider`(注册媒体 Provider) | [media](./media.md#注册-provider) |
|
|
84
|
+
| `ai.models.list` | `ctx.ai.listModels()` | [ai](./ai.md) |
|
|
85
|
+
| `ai.complete` | `ctx.ai.complete()` / `ctx.ai.stream()` / `ctx.ai.chat()` | [ai](./ai.md) |
|
|
86
|
+
| `models.manage` | `ctx.models.replaceOwnedProviders()` / `listOwnedProviders()` | [下方](#尚无专章的能力) |
|
|
87
|
+
| `ai.ocr.recognize` | `ctx.ocr.recognize()` / `listProviders()` / `onProvidersChanged()` | [下方](#尚无专章的能力) |
|
|
88
|
+
| `ai.ocr.provider.register` | `ctx.ocr.registerProvider()`(注册 OCR Provider) | [下方](#尚无专章的能力) |
|
|
89
|
+
| `shell.openExternal` | `ctx.ui.openExternal()`(交给系统默认浏览器) | [下方](#尚无专章的能力) |
|
|
90
|
+
|
|
91
|
+
> 清单 `agent.agents` / `agent.teams`(插件贡献的智能体与团队)**不需要权限**——那是声明面而非运行时 API,见 [manifest](./manifest.md#贡献智能体与团队)。
|
|
92
|
+
>
|
|
93
|
+
> `ctx.i18n` / **`ctx.ui.notify`** **不需要权限**——分别读本插件 catalog、以及向宿主右下角推送 Toast(含错误堆栈复制)。错误上报规范见 [ui-slots → notify](./ui-slots.md#全局通知-notify)。
|
|
94
|
+
|
|
95
|
+
## 尚无专章的能力
|
|
96
|
+
|
|
97
|
+
以下 API 已经实装并受权限门控,但还没有独立章节。这里给出用它们之前必须知道的边界;形状以
|
|
98
|
+
`@vetta-org/plugin-sdk` 的类型为准。
|
|
99
|
+
|
|
100
|
+
### ctx.ui.registerAbilityDetailSlot(`ui.slot.ability-detail`)
|
|
101
|
+
|
|
102
|
+
往**某个能力的详情页**里挂一块插件自绘的 UI,按 `abilityId` 定位目标能力。典型用途是承接上游
|
|
103
|
+
原生的配置流程(登录、装 CLI、填端点),而不是把这些塞进 `ability.json` 的静态区块。
|
|
104
|
+
|
|
105
|
+
- 缺权限 **warn+noop**;`abilityId` 必填,空串直接抛错。
|
|
106
|
+
- 与 [ability-details.md](./ability-details.md) 的分工:那是**声明式**的静态详情页(showcase、
|
|
107
|
+
功能网格、Markdown),这里是**运行时**的交互区。
|
|
108
|
+
|
|
109
|
+
### ctx.models(`models.manage`)
|
|
110
|
+
|
|
111
|
+
维护**以本插件 id 命名**的模型 Provider,拿不到也改不了别人的。
|
|
112
|
+
|
|
113
|
+
- `replaceOwnedProviders(providers)` 是**原子快照**:省略即删除。所以每次写入都得重建完整真相。
|
|
114
|
+
- 正因如此,写之前先 `listOwnedProviders()` 读回宿主当前持有的状态并做对账——否则上游一时没返回的
|
|
115
|
+
模型会被当成用户丢失的模型抹掉。读回的 `apiKey` 是掩码,下次写入要带上真凭据。
|
|
116
|
+
|
|
117
|
+
### ctx.ocr(`ai.ocr.recognize` / `ai.ocr.provider.register`)
|
|
118
|
+
|
|
119
|
+
两个方向,权限分开:**消费**识别能力用 `ai.ocr.recognize`(`recognize()` / `listProviders()` /
|
|
120
|
+
`onProvidersChanged()`),**提供**识别能力用 `ai.ocr.provider.register`(`registerProvider()`)。
|
|
121
|
+
|
|
122
|
+
- 消费方提交的是批量图片引用;取消走 `options.signal`。
|
|
123
|
+
- Provider 通过受控的输入 URL / 上传接口适配本地或远程服务,权限、取消、进度、能力协商与结果校验
|
|
124
|
+
统一由宿主处理(ADR-0108)。
|
|
125
|
+
|
|
126
|
+
### ctx.ui.openExternal(`shell.openExternal`)
|
|
127
|
+
|
|
128
|
+
把 URL 交给**系统默认浏览器**,不是宿主内置的 Browser Panel(那是 `ctx.browser.open`,见
|
|
129
|
+
[browser.md](./browser.md))。**只接受 `http:` / `https:`**,其它协议一律拒绝——这条限制是刻意的,
|
|
130
|
+
避免插件借它拉起任意 scheme(含 `file://`)。
|
|
131
|
+
|
|
132
|
+
## 占位符权限(已声明、暂无对应 API)
|
|
133
|
+
|
|
134
|
+
`PluginPermission` 联合里还包含以下值,目前是**声明了但还没对应能力 API** 的占位符,现在声明它们不会解锁任何功能:
|
|
135
|
+
|
|
136
|
+
`agent.systemPrompt.read`、`agent.state.read`、`agent.state.write`、`agent.runtime.configure`、`settings.read`、`settings.write`。
|
|
137
|
+
|
|
138
|
+
## 最小授权原则
|
|
139
|
+
|
|
140
|
+
只声明真正用到的权限。`permissions` 越小,用户授权越省心、审核越快。
|
|
141
|
+
|
|
142
|
+
使用 `@vetta-org/plugin-vite` 构建或执行 `vetta-plugin pack` 时,构建器会检查最终 JavaScript
|
|
143
|
+
产物和 `plugin.json` 的能力声明;发现 `registerSystemPromptProvider`、`setToolEnabled`、
|
|
144
|
+
`registerTool`、`registerHook` 等能力缺少对应权限时会直接终止构建。运行时权限校验仍然保留,
|
|
145
|
+
用于验证用户是否实际授权并约束通过宿主公开 API 发起的调用。
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# 样式与陷阱
|
|
2
|
+
|
|
3
|
+
把这页当 checklist:以下几条都踩过坑,足以让插件「加载失败」「元素不可见」「改了不生效」「污染宿主 UI」。
|
|
4
|
+
|
|
5
|
+
## 样式隔离:正常写 Tailwind 或 CSS
|
|
6
|
+
|
|
7
|
+
使用 `@vetta-org/plugin-vite` 构建时,插件 CSS 会自动包进以
|
|
8
|
+
`[data-vetta-plugin-root="<id>"]` 为根的原生 `@scope`,`:root` / `:host` 会自动映射为
|
|
9
|
+
`:scope`。
|
|
10
|
+
插件作者不需要手写插件 id 前缀或 cascade layer,可以正常使用 Tailwind,也可以编写业务 CSS。
|
|
11
|
+
|
|
12
|
+
宿主加载插件 CSS 时还会统一放入低优先级 `vetta-plugins` layer,兼容旧版构建产物,
|
|
13
|
+
避免插件样式覆盖 Desktop。插件仍与宿主共享同一 document,不要依赖修改 `body`、`html`
|
|
14
|
+
或宿主私有 class;这些选择器在新版构建中不会匹配到插件根以外的元素。
|
|
15
|
+
|
|
16
|
+
| 允许 | 禁止 |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| JSX:`className="flex h-8 gap-2 rounded-lg bg-background text-foreground"` | 依赖修改 Desktop 私有 class |
|
|
19
|
+
| `.panel button { padding: 8px }` 等插件业务样式 | 用 `body` / `html` 定制 App 外壳 |
|
|
20
|
+
| 在 `:root` 定义插件内部变量(构建后映射到插件根) | `createPortal(..., document.body)` 逃出插件根 |
|
|
21
|
+
|
|
22
|
+
主题色优先用 **宿主 CSS 变量**对应的 Tailwind 语义类(或 `var(--foreground)` 仅出现在不可避免的例外里):`--foreground` / `--background` / `--border` / `--primary` / `--muted` 等。
|
|
23
|
+
|
|
24
|
+
### Tailwind 入口
|
|
25
|
+
|
|
26
|
+
需要 Tailwind 时,`src/style.css` 可以直接导入完整 Tailwind;preflight 和 utilities 都会被自动限制在插件根:
|
|
27
|
+
|
|
28
|
+
```css
|
|
29
|
+
@import "tailwindcss";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Vite 配 `@tailwindcss/vite`;入口 `import "./style.css"` 一次即可。
|
|
33
|
+
- `vettaPluginFederation` 会在 Tailwind 编译前自动注入
|
|
34
|
+
`@vetta-org/plugin-sdk/tailwind-theme.css` 的纯 Token 契约;插件不需要导入
|
|
35
|
+
Desktop CSS、SDK 主题 CSS,也不需要重复声明 `@theme`。
|
|
36
|
+
- `plugin.json`:`"styles": ["dist/style.css"]`。
|
|
37
|
+
- 如不需要 preflight,也可以继续只导入 `theme.css` + `utilities.css` 以减小产物。
|
|
38
|
+
|
|
39
|
+
`text-foreground`、`text-card-foreground`、`text-muted-foreground/60`、
|
|
40
|
+
`bg-card`、`border-border` 等语义工具类会引用宿主运行时 CSS 变量并随主题切换;
|
|
41
|
+
插件制品不包含 Desktop 的实际颜色值或组件样式。
|
|
42
|
+
|
|
43
|
+
### JSX 示例
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// ✅ 仅 Tailwind className
|
|
47
|
+
function Panel() {
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex h-full flex-col gap-2 p-3 text-sm text-foreground">
|
|
50
|
+
<button type="button" className="rounded-md border border-border bg-accent px-2 py-1">
|
|
51
|
+
刷新
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ✅ 也可以 import 插件自己的业务 CSS,构建工具会自动加作用域
|
|
58
|
+
// import "./panel-styles.css";
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Agent 写插件时可以按普通 React 项目使用 `className` 或 CSS;不要为了隔离手写插件 id 前缀。
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 面板类 slot 布局边界(禁止 viewport 级浮层)
|
|
66
|
+
|
|
67
|
+
插件 UI 与宿主**共享同一 document / 同一 React 树**(无 iframe / Shadow 沙箱)。面板类扩展点渲染在活动面板的有限矩形内,**不得**做成贴浏览器/App 视口的全局浮层。
|
|
68
|
+
|
|
69
|
+
### 哪些算「面板类」
|
|
70
|
+
|
|
71
|
+
| 扩展点 | 布局预期 |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| **`registerFilePreview`** | 内容铺满预览区;所有 UI 留在预览壳内 |
|
|
74
|
+
| **`registerActivityTab`** | 内容铺满 Tab 面板;所有 UI 留在面板内 |
|
|
75
|
+
| **`registerInputAction` 面板内容**(若有) | 同面板约束 |
|
|
76
|
+
| **`registerGlobalSlot`** | **例外**:这是全局浮层扩展点,可相对视口布局 |
|
|
77
|
+
| **`ctx.ui.notify`** | **例外**:全局 Toast,由宿主渲染,不要自己造右上角固定条 |
|
|
78
|
+
|
|
79
|
+
### 硬规则(agent / 作者)
|
|
80
|
+
|
|
81
|
+
1. **禁止**在面板类 slot 内用 Tailwind `fixed` / `sticky` 去贴窗口(如 `fixed right-4 top-4`、`fixed inset-0` 当全屏遮罩)。
|
|
82
|
+
2. **禁止**超高 z-index 抢宿主 chrome(如 `z-[2147483647]`)。面板内层级用普通 `z-10` / `z-20` 即可。
|
|
83
|
+
3. 面板内需要「浮在内容上」的工具条 / 调试按钮:根节点 `relative`,子节点用 **`absolute`**(相对面板,不是视口)。
|
|
84
|
+
4. 需要真正的**全局** UI(设置引导、跨页面悬浮球)→ 用 **`registerGlobalSlot`**(权限 `ui.slot.global`),不要塞进 file-preview / activity-tab。
|
|
85
|
+
5. 需要错误/提示 → **`ctx.ui.notify`**,不要自己 `fixed` 一个 Toast。
|
|
86
|
+
6. **不要** `createPortal(..., document.body)` 把节点挂到 body 逃出面板;弹层留在组件树内,或 portal 到本面板根节点(若必须 portal)。
|
|
87
|
+
|
|
88
|
+
### 正反例
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
// ❌ 相对视口:会跑到 App 窗口角落 / 盖住宿主 chrome
|
|
92
|
+
<button type="button" className="fixed right-4 top-4 z-[2147483647] ...">
|
|
93
|
+
测试
|
|
94
|
+
</button>
|
|
95
|
+
|
|
96
|
+
// ✅ 相对预览/面板根:工具条贴在内容区右上角
|
|
97
|
+
function Preview() {
|
|
98
|
+
return (
|
|
99
|
+
<div className="relative flex h-full min-h-0 flex-col overflow-hidden">
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className="absolute right-3 top-3 z-10 rounded-md border border-border bg-background px-2 py-1 text-xs"
|
|
103
|
+
>
|
|
104
|
+
测试
|
|
105
|
+
</button>
|
|
106
|
+
{/* 预览主体 */}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 宿主侧兜底(file-preview)
|
|
113
|
+
|
|
114
|
+
`registerFilePreview` 的挂载壳会建立 **fixed containing block**(`transform`)并 `overflow: hidden` + stacking `isolate`:即便误写了 `fixed`,也会被收进预览矩形,而不是贴 App 视口。
|
|
115
|
+
**这是兜底,不是许可证**——实现仍须按上面规则写 `absolute` / 走 global / notify。activity-tab 等其它面板目前主要靠约定;不要依赖「写 fixed 宿主会修好」。
|
|
116
|
+
|
|
117
|
+
详见 [ui-slots.md → 文件预览](./ui-slots.md#文件预览-registerfilepreview)。
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Module Federation 顶层 JSX 陷阱
|
|
122
|
+
|
|
123
|
+
**模块顶层不要使用共享依赖(含 JSX)。**
|
|
124
|
+
|
|
125
|
+
MF 的共享模块(`react` / `react/jsx-runtime`)是**异步填充**的——bootstrap 完成前为 `undefined`。模块顶层的 JSX 字面量会在模块求值时立即执行,此时 `jsx` 运行时还没就位:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
// ❌ 顶层 JSX:模块求值即抛 TypeError: ... is not a function,整个插件加载失败
|
|
129
|
+
const ICON = <svg viewBox="0 0 24 24" />;
|
|
130
|
+
|
|
131
|
+
// ✅ 放进组件函数体或 activate() 内(求值被推迟到运行时,依赖已就位)
|
|
132
|
+
function Icon() {
|
|
133
|
+
return <svg viewBox="0 0 24 24" />;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
同理,别在模块顶层直接调用任何共享依赖的运行时值。图标、常量化的 JSX、`React.createContext(...)` 等都放进函数体内或 `activate()`。
|
|
138
|
+
|
|
139
|
+
## React 是宿主单例
|
|
140
|
+
|
|
141
|
+
`react` / `react-dom` 运行时由**宿主作为共享单例**提供,不打进你的 bundle(`vettaPluginFederation` 已设 `singleton + import:false`)。因此:
|
|
142
|
+
|
|
143
|
+
- 你的插件与宿主用**同一个 React**,hook、context、状态都跨得过去(这正是 `useActiveConversation` 等能工作的前提)。
|
|
144
|
+
- `package.json` 里的 `react` 只用于类型与本地构建,别试图 bundle 一份自己的 React。
|
|
145
|
+
- `@vetta-org/plugin-sdk` 同样 external,运行时由宿主提供。
|
|
146
|
+
|
|
147
|
+
## 可选:`@vetta/ui` 宿主 primitives
|
|
148
|
+
|
|
149
|
+
插件**可以**直接使用宿主的设计系统 primitives,与 App chrome 对齐:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { Button, Switch, Slider, Dialog, DialogContent, cn } from "@vetta/ui";
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
约定:
|
|
156
|
+
|
|
157
|
+
| 项 | 说明 |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| 运行时 | 由宿主单例提供(MF share + `vetta-host://ui`),**不要**打进插件 bundle |
|
|
160
|
+
| 构建 | `vettaPluginFederation` 已把 `@vetta/ui` 设为 `shared.singleton + import:false`,并 rollup external |
|
|
161
|
+
| `package.json` | 仅作类型 / 本地 tsc:`devDependencies` 里 `@vetta/ui`(仓库内 `workspace:*`,仓库外按发布版本) |
|
|
162
|
+
| 样式 | 组件 class 走宿主全局 token / Tailwind;插件 scoped CSS **管不到** Dialog 等 portal 到 `document.body` 的浮层(浮层依赖宿主已加载的全局样式,这是预期行为) |
|
|
163
|
+
| 宿主版本 | 需要宿主提供 `vetta-host://ui` shim:desktop **>= 0.5.31**,且 `@vetta-org/plugin-vite` **>= 0.0.5**。旧宿主上 import `@vetta/ui` 会在加载插件时解析失败(模块找不到,整个插件不激活)——若你的插件要兼容更早的 App,就别用这条通道,自写 JSX + 语义 class |
|
|
164
|
+
| 稳定性 | **半稳定、可选**。宿主会尽量不无故破坏,但不对跨 App 大版本做 semver 承诺;props / 导出变更时官方插件随 monorepo 同改 |
|
|
165
|
+
| 不在此列 | `@vetta/theme-ui/plugin-ui` 是独立的按需共享合同,见下节;不要从其它 `@vetta/theme-ui/*` 入口导入宿主业务 View |
|
|
166
|
+
|
|
167
|
+
默认路径仍是:自写 JSX + 语义 class(`bg-background` / `text-foreground`…)。`@vetta/ui` 适合按钮、开关、对话框等控件统一,不是强制。
|
|
168
|
+
|
|
169
|
+
顶层不要对 `@vetta/ui` 做立即求值(与 React 相同,见上文「MF 顶层 JSX 陷阱」)——在组件函数内使用即可。
|
|
170
|
+
|
|
171
|
+
## 按需:`@vetta/theme-ui/plugin-ui` 宿主成品 UI
|
|
172
|
+
|
|
173
|
+
少量经过明确审核的宿主成品组件会从窄入口 `@vetta/theme-ui/plugin-ui` 开放。
|
|
174
|
+
它不是默认共享依赖;只有实际使用这些组件的插件才应开启:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
vettaPluginFederation({
|
|
178
|
+
name: "my_plugin",
|
|
179
|
+
hostThemeUi: true,
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
同时在 `devDependencies` 声明基础包 `@vetta/theme-ui`(仓库内使用
|
|
184
|
+
`workspace:*`)。`hostThemeUi` 只让运行时从宿主共享域取组件,不会把 Theme UI
|
|
185
|
+
打进插件 bundle。不要为了消除构建警告给未使用该入口的插件增加依赖;也不要把
|
|
186
|
+
`@vetta/theme-ui` 的其它业务入口当作插件公共 API。
|
|
187
|
+
|
|
188
|
+
## 缓存刷新
|
|
189
|
+
|
|
190
|
+
插件 bundle 经 `vetta-plugin://` 协议加载,**Chromium 会缓存 `remoteEntry.js`**——你改了代码重装,**重启 App 也未必清掉旧缓存**,表现为「改动不生效」。
|
|
191
|
+
|
|
192
|
+
可靠的强刷办法:**把 `plugin.json` 的 `version` 往上 bump**,宿主当作新版本重新拉取。配合在设置页 `reload(id)`(或重开 App)。调试期每次有效改动都建议 bump 一下 patch 版本。
|
|
193
|
+
|
|
194
|
+
**开发期更省事的做法:插件工作台面板的「热更新」开关**。对已安装过一次的插件打开后,宿主把该插件 dev 链接到工程目录并常驻 `vetta-plugin dev`:React / CSS 走 HMR,清单与 agent 等资源定向重载,无需 bump / 重打 zip / 手动 reload。关闭开关(或重启 App)后回落安装目录。dev 会话内新增的普通权限仅在内存中放行;要在关闭热更新或重启后保留,仍需重新应用插件。
|
|
195
|
+
|
|
196
|
+
## 清理副作用
|
|
197
|
+
|
|
198
|
+
`ctx.ui.register*` 返回的 `Disposable` 由宿主在卸载时统一处置,**无需**手动 dispose。自己创建的副作用(`setInterval`、`window.addEventListener`、订阅、业务运行时等)应由 `activate()` 返回 cleanup 函数或 `Disposable` 清理。
|
|
199
|
+
|
|
200
|
+
宿主热更新采用 last-known-good 替换:新 activation 准备完成后才发布,并在随后释放旧 activation。返回值 cleanup 与具体 activation 一一绑定,因此不要用模块级可变单例在新旧实例之间传递运行时所有权。模块级 `deactivate()` 仅用于兼容无重叠所有权的旧插件。
|
|
201
|
+
|
|
202
|
+
**这条最容易在 UI 组件上翻车**:组件在**渲染期**回头去读模块级运行时,就会读到别的 activation 的那一份,或读到刚被旧实例 `deactivate()` 清空的 `null`——重载后一进页面就白屏报错。运行态要在 `activate()` 里建好并由该次注册的组件**闭包持有**:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
// ❌ 渲染期读模块级单例:旧实例的 deactivate() 会把它清成 null
|
|
206
|
+
let store: Store | null = null;
|
|
207
|
+
export default definePlugin({
|
|
208
|
+
activate(ctx) {
|
|
209
|
+
store = new Store(ctx);
|
|
210
|
+
ctx.ui.registerWorkspaceView({ id: "board", label: "…", component: BoardView });
|
|
211
|
+
},
|
|
212
|
+
deactivate() { store = null; }, // 这一句会打死「新」实例刚注册的视图
|
|
213
|
+
});
|
|
214
|
+
function BoardView() { return <Board store={getStore()} />; }
|
|
215
|
+
|
|
216
|
+
// ✅ 闭包持有本次 activation 的运行态;层级深就用 context 往下传
|
|
217
|
+
export default definePlugin({
|
|
218
|
+
activate(ctx) {
|
|
219
|
+
const store = new Store(ctx);
|
|
220
|
+
ctx.ui.registerWorkspaceView({
|
|
221
|
+
id: "board",
|
|
222
|
+
label: "…",
|
|
223
|
+
component: () => <RuntimeProvider value={store}><BoardView /></RuntimeProvider>,
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
没有需要自己释放的资源(订阅、定时器、长驻进程)时,**不要为了对称写一个 `deactivate()`**——注册的贡献本来就由宿主统一处置,多写反而制造上面这条竞态。
|
|
230
|
+
|
|
231
|
+
## 权限缺失的两种后果
|
|
232
|
+
|
|
233
|
+
复习 [permissions.md](./permissions.md):部分注册点缺权限**抛错**(中断该次调用)、部分**跳过+警告**(不影响其它能力)。把可选能力的注册各自独立,避免一处 throw 掉整段 `activate()`。
|
|
234
|
+
|
|
235
|
+
## 错误必须上报用户(notify)
|
|
236
|
+
|
|
237
|
+
预览解析、工具执行、读盘、调用外部库等**可能失败**的路径:
|
|
238
|
+
|
|
239
|
+
- **禁止**只 `catch` 成一句「失败了」写在组件里、不把原始 `error` 交给宿主。
|
|
240
|
+
- **必须**调用 `ctx.ui.notify({ message: 用户可读摘要, error })`(无权限)。宿主右下角 Toast 提供 **「复制堆栈」**,便于用户粘贴给 agent / 反馈。
|
|
241
|
+
- 组件内仍可保留简短失败 UI;notify 与内联文案互补,不是二选一。
|
|
242
|
+
- 在 `activate` 里把 `notify` 赋给模块变量,供组件闭包使用(组件 props 不含 `ctx`)。
|
|
243
|
+
|
|
244
|
+
完整约定与示例见 [ui-slots.md → 全局通知 notify](./ui-slots.md#全局通知-notify)。
|
|
245
|
+
|
|
246
|
+
## 文件预览必须考虑大文件
|
|
247
|
+
|
|
248
|
+
做 `registerFilePreview` 时**禁止**「小样例能开就交差」:
|
|
249
|
+
|
|
250
|
+
- **优先 `file.getUrl()`**(Range 流式),需要整包时用 `fetch(url).arrayBuffer()`;不要默认 `readBytes()`。
|
|
251
|
+
- `readBytes` / `readText` 经 IPC **约 10MB 硬上限**,再大直接抛错——只靠它们的预览在真实 pptx/pdf/音视频上会大面积失败。
|
|
252
|
+
- 读 `file.size`、做 loading/取消、超大时降级或明确提示;失败走 `notify({ message, error })`。
|
|
253
|
+
|
|
254
|
+
细则、选型表与正反例见 [ui-slots.md → 文件预览 / 大文件](./ui-slots.md#文件预览-registerfilepreview)。
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# 系统插件(presets)
|
|
2
|
+
|
|
3
|
+
除用户自行安装的插件外,还有**系统插件**——随 App 一起发布、用户**不可删除/修改**(ADR-0024)。它与普通插件用**完全相同**的 SDK 与清单,区别只在分发方式与运行时语义。
|
|
4
|
+
|
|
5
|
+
## 源码位置
|
|
6
|
+
|
|
7
|
+
源码放在 monorepo 的 `packages/plugins/presets/<id>/`,结构与普通插件包一致:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
packages/plugins/presets/
|
|
11
|
+
svg-viewer/
|
|
12
|
+
plugin.json
|
|
13
|
+
vite.config.ts
|
|
14
|
+
src/index.tsx
|
|
15
|
+
image-gen/
|
|
16
|
+
plugin.json
|
|
17
|
+
vite.config.ts
|
|
18
|
+
src/index.tsx
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
放进该目录会让它成为可选的系统插件 preset;还必须把 id 加入
|
|
22
|
+
`packages/plugins/tenants.json` 中需要它的 profile/租户清单,才会进入对应开发或打包流程。
|
|
23
|
+
|
|
24
|
+
## 构建与集成
|
|
25
|
+
|
|
26
|
+
- **构建制品**:`bun run build:presets` 先构建根 workspace 中的插件 SDK / 构建包,再逐个产出 `release/<id>-<version>.zip`。`dev` / `start` / 打包流程都会先跑它。
|
|
27
|
+
- **依赖管理**:presets 与其它 monorepo 包统一属于根 workspace、共用根 `bun.lock`;`@vetta-org/plugin-sdk`、`@vetta-org/plugin-vite` 等本地包经 `workspace:*` 直链仓库源码。
|
|
28
|
+
- **校验**:Desktop 按 preset 的 `plugin.json` 精确定位 zip,拒绝路径穿越、id/version 不一致、入口或样式缺失的归档。
|
|
29
|
+
- **dev**:zip 解压到 `apps/desktop/.artifacts/system-plugins/<id>/`,主进程只读该 staging,不直接读 preset 源码或 `dist/`。
|
|
30
|
+
- **打包**:`prepare-pack.js` 从 zip 解压到打包 staging 的 `system-plugins/<id>/`,再随 `extraResources` 进入 `Resources/system-plugins/<id>/`。
|
|
31
|
+
|
|
32
|
+
## 环境与租户打包(tenants.json)
|
|
33
|
+
|
|
34
|
+
`packages/plugins/tenants.json` 先按开发/生产 profile,再按业务租户定义
|
|
35
|
+
**preset id 完整列表**(非增量)。环境变量 **`VETTA_TENANT`** 选择租户
|
|
36
|
+
(缺省取 `default` 指向的租户名)。
|
|
37
|
+
|
|
38
|
+
- `build:presets:dev` 使用 `development` profile;`prepare:desktop-pack` 及所有 `pack` /
|
|
39
|
+
`dist` 入口使用 `production` profile。
|
|
40
|
+
- `build:presets` / `prepare-pack` 只构建并打入当前 profile + 租户列表中的插件。
|
|
41
|
+
- 新增 preset 后,需要它的 profile/租户组合都要在各自数组里补上 id。
|
|
42
|
+
|
|
43
|
+
详见 `packages/plugins/AGENTS.md`。
|
|
44
|
+
|
|
45
|
+
## 运行时语义
|
|
46
|
+
|
|
47
|
+
- `source: "system"`,`listPlugins()` 运行时发现并与用户插件合并;每条含 **`rootPath`**(staging / Resources 下的插件根)。
|
|
48
|
+
- **不落用户态目录**:不进 `~/.vetta/plugins`、不写 `plugins-manifest.json`;每次启动从只读 staging 重新发现。
|
|
49
|
+
- **id 冲突**:系统插件优先、id 保留——用户安装同 id 被拒,已存在的同 id 用户插件被遮蔽。
|
|
50
|
+
- **权限**:`plugin.json` 声明的权限**自动全量授予**,用户不可撤。
|
|
51
|
+
- **停用**:默认启用,用户可在设置里关闭(偏好存 `~/.vetta/system-plugin-prefs.json`),但**不可卸载、不可改文件/权限**。
|
|
52
|
+
- **更新**:版本随 App,不走用户插件的 pending/reload 更新流。
|
|
53
|
+
- **硬隔离**:若声明 `contributionMode.hardIsolation`,agent 贡献仍受 mode gate(如插件工作台),与用户授权无关。
|
|
54
|
+
|
|
55
|
+
## 何时做成系统插件
|
|
56
|
+
|
|
57
|
+
- 想随 App 默认提供、对所有用户开箱即用、且权限敏感(自动全量授予)→ 系统插件。
|
|
58
|
+
- 想由用户自行选择安装、独立于 App 发版迭代 → 普通插件([getting-started.md](./getting-started.md) 的 zip / `install-from-path`)。
|