把插件迁移到运行时入口
本指南用于把为 Paseo 0.7 或更早版本编写的插件,更新为 0.8 引入的当前客户端/服务端入口格式。
把本页交给一个编程 Agent,并以插件目录作为它的工作目录。按顺序执行各个步骤。不要保留兼容入口。
1. 对现有代码分类
从旧的结构开始:
my-plugin/
paseo-plugin.json
package.json
tsconfig.json
index.ts
greeting.client.tsx
greeting.server.ts
greeting.shared.ts
完成后的结构是:
my-plugin/
paseo-plugin.json
package.json
tsconfig.json
index.client.tsx
index.server.ts
client/greeting.tsx
server/greeting.ts
shared/greeting.ts
只创建插件需要的入口,至少需要一个。组件和客户端回调需要客户端入口。RPC 处理函数和 Node API 需要服务端入口。
2. 重命名文件和目录
严格按以下规则操作:
- 用
index.client.tsx、index.server.ts或两者替换混合的根入口。 - 把每个
name.client.ts或name.client.tsx移动到client/name.ts或client/name.tsx。 - 把每个
name.server.ts或name.server.tsx移动到server/name.ts或server/name.tsx。 - 把每个
name.shared.ts或name.shared.tsx移动到shared/name.ts或shared/name.tsx。 - 在对应的运行时目录下保留嵌套的功能目录。
- 每次移动后更新相对导入路径。
- 把
paseo-plugin.json、package.json和tsconfig.json保留在根目录。 - 删除旧的根入口。Paseo 不会加载它。
这些目录就是编译器的边界。client/ 下的文件只编译进应用的包,server/ 下的文件只编译进 daemon 的包,shared/ 则编译进两者。*.client.tsx 之类的文件名后缀不再有任何含义,留在插件根目录的代码模块会导致编译错误。
3. 迁移每一处注册
把下表作为完整的注册检查清单。
| 旧的注册及位置 | 新的注册及位置 |
|---|---|
旧根入口中的 plugin.handle(contract, handler) | index.server.ts 中的 server.handle(contract, handler) |
旧根入口中的 plugin.addSurface(id, Component) | index.client.tsx 中的 client.addScreen({ id, title, Component }) |
旧根入口中的 plugin.addSidebarItem({ id, title, icon, surface }) | index.client.tsx 中的 client.addSidebarHeaderItem({ id, title, Component });见侧边栏项 |
| 新的侧边栏底部贡献 | index.client.tsx 中的 client.addSidebarFooterItem({ id, title, Component }) |
| 新的文件面板贡献 | index.client.tsx 中的 client.addFilePanel(panel) |
旧根入口中的 plugin.addWorkspacePanel(panel) | index.client.tsx 中的 client.addWorkspacePanel(panel) |
旧根入口中的 plugin.addCommandCenterItem(item) | index.client.tsx 中的 client.addCommandCenterItem(item) |
旧根入口中的 plugin.addClientSlashCommand(command) | index.client.tsx 中的 client.addSlashCommand(command) |
旧根入口中的 plugin.addClientSide(fn) | 删除这层包装,把 fn 的函数体移入默认的客户端入口函数 |
旧客户端回调中的 client.addComposerPill(pill) | index.client.tsx 或被导入的 client/ 函数中的 client.addComposerPill(pill) |
| 新的标题栏贡献 | client.addHeaderButton({ id, workspaceId, button }) |
旧根入口中的 plugin.addAttachmentSource(source) | index.client.tsx 中的 client.addAttachmentSource(source) |
| 新的设置界面贡献 | index.client.tsx 中的 client.addSettingsScreen(screen);见设置界面 |
旧根入口中的 plugin.addTheme(theme) | index.client.tsx 中的 client.addTheme(theme) |
旧根入口中的 plugin.addTimelineTransformer(transformer) | index.client.tsx 中的 client.addTimelineTransformer(transformer) |
旧根入口中的 plugin.addTimelineRenderer(renderer) | index.client.tsx 中的 client.addTimelineRenderer(renderer) |
shared 文件中的 import { defineRpc, defineAttachmentSource } from "@getpaseo/plugin/server" | import { defineRpc, defineAttachmentSource } from "@getpaseo/plugin" |
处理函数参数类型 ZodOutput<typeof contract.input> | 来自 @getpaseo/plugin 的 RpcInput<typeof contract>;返回类型使用 RpcOutput |
用原 addSidebarItem 项的 id 注册替代它的界面,这样已保存的链接仍然有效。/plugin/<plugin>/sidebar/<id> 链接以及更早的 /plugin/<plugin>/<id> 链接,会打开具有该 id 的 addSidebarItem,否则打开具有该 id 的界面。
从 @getpaseo/plugin/client 导入 PluginClientContext,从 @getpaseo/plugin/server 导入 PluginServerContext。删除对旧 context 类型的导入。客户端注册返回幂等的移除函数,但标题栏按钮和 Composer 标签除外,它们返回 { update, remove } 句柄。保留插件在销毁之前调用的所有移除函数;入口的清理函数运行后,Paseo 会移除尚未移除的注册。
Composer 标签
更新插件项目的 @getpaseo/plugin 依赖,然后运行 npm run typecheck。旧的贡献缺少必需的 button 字段,PluginComposerPillProps 不再导出,并且把新的注册当作函数调用会产生 TypeScript 错误。固定在旧 SDK 上的项目仍然按旧的契约检查;安装或重新加载插件并不会运行 TypeScript 的类型检查器。
把标签的 Component 替换为 button.icon 和 button.label,把 title 移入 button,把 onPress 移入 button.behavior。清理方式从调用返回的函数改为调用它的 .remove() 方法。
const pill = client.addComposerPill({
id: "review",
workspaceId,
agentId,
button: {
title: "Open review",
icon: "Scan",
label: "Review",
behavior: { kind: "action", onPress: openReview },
},
});
pill.update({ label: "Review · 3", visible: true });
// Client entry cleanup:
pill.remove();
把原组件中的动态文本改为由你的模型或 SDK 订阅发出更新。自定义图标组件仍然可以使用 hook。Composer 标签总是显示图标和文字,并且从不显示展开箭头。菜单、弹出框和可见性见按钮。
4. 拆分导入
把 hook(usePaseo、useRpc、useSettings、useAgent、useWorkspace)和客户端贡献类型从 @getpaseo/plugin 移到 @getpaseo/plugin/client。把 Icon 移到 @getpaseo/plugin/client/ui。从 @getpaseo/plugin/server 导入服务端 context 和生命周期契约。共享的辅助函数(defineRpc、defineSettings、defineAttachmentSource)、schema 和普通数据类型保留在根入口。这些规则同样适用于类型导入。完整的契约见运行时模块。
把其余的 SDK 子路径移到它们所属的运行时下:
| 旧入口 | 当前入口 |
|---|---|
@getpaseo/plugin/react-native | @getpaseo/plugin/client/ui |
@getpaseo/plugin/client/react-native | @getpaseo/plugin/client/ui |
@getpaseo/plugin/ui | @getpaseo/plugin/client/ui |
@getpaseo/plugin/provider | @getpaseo/plugin/server/provider |
@getpaseo/plugin/acp | @getpaseo/plugin/server/acp |
0.8 之前的入口和旧的 @paseo/plugin scope 已被移除。/client/host 是 Paseo 应用集成的私有模块,插件作者从不导入它。
客户端入口只导入 client/、shared/ 和可在客户端安全使用的包。服务端入口只导入 server/、shared/ 和可在服务端安全使用的包。在客户端入口或从它可达的任何代码中导入 node: 模块都会导致编译错误。不要仅仅为了接线注册而把组件导入服务端入口;那项注册属于客户端入口。
v0.13 中的 UI 导入
所有 Paseo UI 组件和 UI hook 都从 /client/ui 导入。之前的入口作为未写入文档的兼容别名保留到 2027-04-20。
对于逐步显示的时间线文本,把 useRevealedText(text, phase) 及其外层的 Text 换成 /client/ui 中的 <StreamedText text={text} phase={phase} style={style} />。插件设置的标题使用 SettingsSection;SettingsGroup 已弃用。
5. 识别只迁移了一半的错误
| 编译器或加载错误 | 含义与修复方法 |
|---|---|
This plugin has no requirements.paseo | 完成迁移,并按第 7 步声明版本范围。 |
This plugin was made for an older version of Paseo | 目录中仍然只有旧的根入口。创建一个运行时入口,迁移注册,然后删除旧文件。 |
Plugin entry points are missing: expected index.client.ts or index.client.tsx and/or index.server.ts or index.server.tsx | 不存在受支持的入口。至少添加一个文件名完全匹配的入口。 |
server-only module cannot be imported into the plugin client bundle: <file> | 某个客户端导入触及了 server/。把这个调用移到 RPC 之后,并从 shared/ 导入它的契约。 |
client-only module cannot be imported into the plugin server bundle: <file> | 某个服务端导入触及了 client/。把那项注册及其导入移到客户端入口。 |
Plugin modules belong in client/, server/, or shared/: <file> | 某个代码模块仍在插件根目录。把它移到对应的目录并修复它的导入。 |
Node module cannot be imported into the plugin client bundle: node:<name> imported by <file> | 客户端代码导入了 Node API。把该操作移到 server/,在 shared/ 中暴露一个 RPC,并从客户端调用它。 |
TypeScript 报告 PluginContext、addClientSide 或 addClientSlashCommand 不存在 | 按照上面的表格替换旧的 context 类型和注册。 |
6. 完整示例:plugin-examples/local-plugin
只有入口文件和导入路径发生变化。组件和处理函数的函数体原样移动,无需编辑。
迁移前:
local-plugin/
index.ts
main.client.tsx
increment.server.ts
increment.shared.ts
// index.ts
import type { PluginContext } from "@getpaseo/plugin";
import { contributeClient, ExamplePanel } from "./main.client";
import { increment } from "./increment.server";
import { incrementRpc } from "./increment.shared";
export default function contribute(plugin: PluginContext) {
plugin.handle(incrementRpc, increment);
plugin.addWorkspacePanel({
id: "counter",
title: "Plugin counter",
icon: "Blocks",
context: "workspace",
locations: ["workspace", "explorer"],
Component: ExamplePanel,
});
plugin.addCommandCenterItem({
id: "open-counter",
title: "Open plugin counter",
icon: "Blocks",
context: "workspace",
onSelect({ openPanel }) {
openPanel("counter");
},
});
plugin.addClientSide(contributeClient);
return () => {};
}
迁移后:
local-plugin/
index.client.tsx
index.server.ts
client/main.tsx # was main.client.tsx
server/increment.ts # was increment.server.ts
shared/increment.ts # was increment.shared.ts
// index.client.tsx
import type { PluginClientContext } from "@getpaseo/plugin/client";
import { contributeClient, ExamplePanel } from "./client/main";
export default function contribute(client: PluginClientContext) {
client.addWorkspacePanel({
id: "counter",
title: "Plugin counter",
icon: "Blocks",
context: "workspace",
locations: ["workspace", "explorer"],
Component: ExamplePanel,
});
client.addCommandCenterItem({
id: "open-counter",
title: "Open plugin counter",
icon: "Blocks",
context: "workspace",
onSelect({ openPanel }) {
openPanel("counter");
},
});
return contributeClient(client);
}
// index.server.ts
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { increment } from "./server/increment";
import { incrementRpc } from "./shared/increment";
export default function contribute(server: PluginServerContext) {
server.handle(incrementRpc, increment);
return () => {};
}
被移动文件中的导入路径变化:
// client/main.tsx
-import { incrementRpc } from "./increment.shared";
+import { incrementRpc } from "../shared/increment";
// server/increment.ts
-import { incrementRpc } from "./increment.shared";
+import { incrementRpc } from "../shared/increment";
contributeClient 原本就接收 PluginClientContext 并返回清理函数,因此客户端入口直接调用它并返回它的清理函数。如果插件的 addClientSide 回调还注册了标签或订阅,请保留那些代码;只去掉外层包装。
7. 声明 Paseo 版本要求
迁移完入口和导入后,在 paseo-plugin.json 中添加最低运行时版本:
{
"id": "my-plugin",
"requirements": { "paseo": ">=0.8.0" }
}
保留你现有的 ID 和构建命令。缺少 requirements.paseo 表示 <0.8.0,因此即使文件已经移动,Paseo 0.8 及更高版本也会拒绝该插件。仅仅添加这个字段并不会迁移代码。请把本地的 @getpaseo/plugin 开发依赖更新到你的目标版本,并在类型检查之前重新安装依赖。
版本范围和预发布版本的语义见版本要求。
8. 验证迁移
运行:
npm run typecheck
paseo plugin reload <plugin-id>
paseo plugin ls
确认状态为 running 且没有错误。试用每一项贡献。对于带 RPC 的插件,调用客户端操作并验证服务端结果。对于只有客户端的插件,确认贡献在没有服务端进程的情况下也能加载。对任何保存下来的注册移除函数调用两次,验证第二次调用不产生任何效果。
赞助商
Cloud coding agents
- 成为赞助商