Serve paseo.sh in Simplified Chinese (#7373)

* feat(website): serve paseo.sh in Simplified Chinese

Every page of the website now has a Simplified Chinese version under
/zh-CN: the interface, docs, blog posts, comparison pages, legal pages,
all changelog releases and the homepage mockup. English URLs are unchanged.

The URL alone decides the language. There is no browser-language redirect
and no stored preference, because either would have to redirect the
unprefixed English URLs that people share and search engines index.
Internal links carry the page's language instead, and hreflang alternates
point Chinese search results at /zh-CN.

Translated headings pin the English anchor with {#id}, so section links
work in both languages. content-translations.test.ts fails when a page
has no translation or a translation's headings drift from the English.
Changelog releases and plugin listings without a translation render
whole in English under a notice, so a release never waits on one.

Plugin manifests and the registry gain optional translations and
OVERVIEW.zh-CN.md fields; older clients and registries ignore them.

* feat(website): replace the native language select with an EN/ZH menu

The header showed an OS-native select whose popup did not match the site.
The trigger now shows the language abbreviated (EN, ZH) and opens a menu in
the app's dropdown style that names each language in itself, with its name
in the page language underneath. Rows are real links to the same page in
that language; arrows, Escape, outside presses and taps behave as a menu
button, and the menu keeps itself on screen when a narrow header pushes the
trigger toward an edge.

Also drops the stray space after the full-width comma in the Chinese
homepage subtitle.

* docs(website): translate main's new docs and 0.12.0 release notes

Main added Keep run workspaces to the schedules docs, rewrote plugin icons
around inline SVG, and promoted 0.12.0 with new entries. Bring the Chinese
pages and changelog in line so no section falls back to English.

* fix(server): leave plugin listing translations to the registry

The daemon rejected manifests whose translations the registry would refuse,
so a plugin main installs could fail to load here. Paseo never reads
translations; the registry validates them at publish. Docs now say which
checks are automated at publish and which are human review.

* docs(plugins): describe the registry's final overview translation discovery

* style(server): format manifest test

* test(app): measure sidebar rows one at a time

After a reload the developer error toast appears while expectVerticalOrder
measures every row at once. Each concurrent boundingBox runs the toast
locator handler, and only one of them resumes, so the poll timed out on
every attempt since #7424. Measuring sequentially keeps one action in flight.

* test(app): return sidebar row measuring to main

#7480 hides the developer toast instead of running a locator handler, so
measuring the rows concurrently no longer races it. Reverts 41ac23016.

* test(app): wait for the compact shell before opening compact Changes

The shell survives the breakpoint, so right after the resize the wide
Explorer could still be visible: the test skipped the Explorer toggle and
the tab then disappeared, or toggled at the wrong time. Wait for the
compact-shell marker, as the composer helpers do since #7424.

* docs(website): translate main's plugin file panel docs

* test(app): edit the code font size after the font family remount

A committed appearance change remounts the settings screen through
AppearanceStyleBoundary, and the new form starts from the stored size.
Filling the size before that remount lost the edit in CI.

* docs(website): translate main's client/ui import docs

Follows #7474: one /client/ui entry, StreamedText instead of useRevealedText,
SettingsGroup dropped from the settings table, and the new
'UI imports in v0.13' migration section.

* test(app): wait for the previous layout's file panel picker to leave

After a resize the old layout's picker animates out while the new one is
already mounted, so the Use button matched two elements and the strict
locator failed. Wait for the shell's layout and a single picker first.
This commit is contained in:
Mohamed Boudra
2026-10-21 13:13:03 +07:00
committed by GitHub
parent c70a6cfb9e
commit bd15e40e94
266 changed files with 22495 additions and 3863 deletions
+253
View File
@@ -0,0 +1,253 @@
---
title: 插件快速上手
description: 构建、安装、分享和更新一个受信任的 Paseo 插件。
nav: 快速上手
---
# 插件快速上手 {#plugin-quickstart}
插件是安装到某个 Paseo daemon 中的 TypeScript 项目。它可以添加[界面和侧边栏项](/docs/plugins/reference#screens-and-sidebar-items)、[工作区面板](/docs/plugins/reference#workspace-panels)、[文件面板](/docs/plugins/reference#file-panels)、[Command Center 项](/docs/plugins/reference#command-center-items)、[斜杠命令](/docs/plugins/reference#slash-commands)、[Composer 标签](/docs/plugins/reference#composer-pills)、[时间线条目](/docs/plugins/reference#timeline-items)、[主题](/docs/plugins/reference#contribute-a-theme)、[附件来源](/docs/plugins/reference#add-a-composer-attachment-source)和 [daemon 端 RPC](/docs/plugins/reference#add-plugin-specific-backend-behavior)。它还可以[把编程 Agent 接入为 Provider](/docs/plugins/providers)。客户端贡献会在连接到该 daemon 的每个 Paseo 客户端上运行,包括移动端。
浏览[已发布的插件](https://paseo.sh/plugins),然后在你的 daemon 上安装一个:
```bash
paseo plugin add owner/slug
```
或者把 `owner/slug` 粘贴到 **设置 → 插件 → 插件来源**,然后选择 **安装插件**。Paseo 会安装注册表中经过审查的制品。想直接从 Git 仓库安装时,请使用 `git:owner/repository` 或完整的 Git URL。
本指南会搭建一个插件、运行它,并为它添加一个工作区面板。
## 创建插件 {#create-a-plugin}
使用 daemon 所在机器上的绝对路径:
```bash
paseo plugin init /absolute/path/to/workspace-plugin
cd /absolute/path/to/workspace-plugin
npm install
```
`init` 会写入一个严格模式的 TypeScript 项目,但不会运行包管理器。`npm install` 只添加用于类型检查和测试的开发依赖;插件 SDK、React、React Native、TanStack Query 和 Zod 由 Paseo 在运行时提供。
生成的脚手架就是一个可用的插件:一个从侧边栏打开的界面,其中有一个按钮,通过 RPC 向 daemon 请求一句问候。
```text
workspace-plugin/
paseo-plugin.json # plugin ID and supported Paseo versions
index.client.tsx # runs in the Paseo app
index.server.ts # runs in a daemon subprocess
client/greeting.tsx # the screen component
client/web.ts # the only file allowed to touch browser APIs
server/greeting.ts # the RPC handler
shared/greeting.ts # the RPC contract, imported by both
package.json
tsconfig.json
```
每个入口默认导出一个函数,该函数注册贡献并返回一个清理函数。`index.client.tsx` 注册界面以及打开它的侧边栏项:
```tsx
import type { PluginClientContext, PluginSidebarItemProps } from "@getpaseo/plugin/client";
import { SidebarRow } from "@getpaseo/plugin/client/ui";
import { GreetingScreen } from "./client/greeting";
function GreetingItem({ currentScreen, openScreen }: PluginSidebarItemProps) {
return (
<SidebarRow
icon="MessageCircle"
active={currentScreen?.screenId === "greeting"}
onPress={() => openScreen({ screenId: "greeting" })}
/>
);
}
export default function contribute(client: PluginClientContext) {
client.addScreen({ id: "greeting", title: "Greeting", Component: GreetingScreen });
client.addSidebarHeaderItem({ id: "greeting", title: "Greeting", Component: GreetingItem });
return () => {};
}
```
`index.server.ts` 为 `shared/greeting.ts` 中的契约注册处理函数:
```ts
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { createGreeting } from "./server/greeting";
import { greetingRpc } from "./shared/greeting";
export default function contribute(server: PluginServerContext) {
server.handle(greetingRpc, createGreeting);
return () => {};
}
```
目录就是边界。`client/` 下的代码只编译进应用的包,`server/` 下的代码只编译进 daemon 的包,`shared/` 则编译进两者。跨越这条边界导入、在根目录添加代码文件,或在客户端代码中导入 `node:` 模块,都会导致编译错误。没有 daemon 端工作的插件可以省略 `index.server.ts`;没有 UI 的插件可以省略 `index.client.tsx`。
客户端代码既在浏览器中运行,也在手机上运行。项目在不包含 DOM 库的情况下进行类型检查,因此在 `client/web.ts` 之外使用 `document` 和 `window` 都会报错;`client/web.ts` 演示了如何用 `Platform.OS` 把浏览器 API 隔离开,并提供原生端的后备实现。编写 UI 之前,请先阅读[跨平台规则](/docs/plugins/reference#cross-platform-rules)。
## 安装并试用 {#install-and-try-it}
插件是受信任的、没有沙盒的代码:服务端代码和准备命令以 daemon 用户在 daemon 所在机器上的权限运行,客户端代码在 Paseo 应用内部运行。安装插件意味着你信任该代码库、它的依赖以及它将来的更新。
在你要安装到的 daemon 上,打开 **设置 → 插件** 下的 **启用插件**。它是该 daemon 上所有插件的全局开关,也对应 daemon 的 `config.json` 中根级的 `pluginsEnabled` 字段;编辑该文件后,用 `paseo reload --json` 应用更改。自动化工具必须先读取当前值,并在获得你的明确许可后才能打开它。
然后进行类型检查并安装:
```bash
npm run typecheck
paseo plugin install /absolute/path/to/workspace-plugin
paseo plugin ls
```
或者从应用中安装该目录:
1. 在目标主机上打开 **设置 → 插件**。
2. 把 `/absolute/path/to/workspace-plugin` 粘贴到 **插件来源**。
3. 选择 **安装插件**。应用会使用 `paseo-plugin.json` 中的插件 ID。
来源字段也接受 Git 仓库、npm 包和插件子目录。可接受的语法和解析规则见[插件来源](/docs/plugins/reference#plugin-sources)。如果表单要求你更新主机,请先更新该主机再安装;已有的管理操作仍然可用。
每个已安装插件的行都会显示它的状态和描述。用它的开关启用或停用它,打开它的三点菜单可以进行设置、查看日志、重新加载和移除。
`paseo plugin ls` 应该把该插件报告为 `running`。打开 Paseo,在侧边栏中选择 **Greeting**,然后按 **Create greeting**。消息会通过 RPC 从 daemon 子进程返回。
如果侧边栏项没有出现,请检查 **启用插件** 是否已打开、插件是否为 `running`,以及客户端正在查看的是否就是你安装到的主机。`paseo plugin logs workspace-plugin` 会显示 daemon 端的输出,包括加载错误。
## 添加工作区面板 {#add-a-workspace-panel}
工作区面板会作为标签页,与 Agent、终端和文件并排打开。创建 `client/overview.tsx`:
```tsx
import { type PluginWorkspacePanelProps, useWorkspace } from "@getpaseo/plugin/client";
import { useMemo } from "react";
import { Text, View } from "react-native";
export function WorkspaceOverview({ theme, layout, workspaceId }: PluginWorkspacePanelProps) {
const workspace = useWorkspace(workspaceId, ({ name, directory }) => ({
name,
directory,
}));
const styles = useMemo(
() => ({
screen: {
flex: 1,
padding: layout.compact ? 16 : 24,
gap: layout.compact ? 8 : 12,
backgroundColor: theme.colors.surface0,
},
title: { color: theme.colors.foreground, fontSize: layout.compact ? 20 : 24 },
label: { color: theme.colors.foregroundMuted },
detail: { color: theme.colors.foreground },
}),
[theme, layout.compact],
);
return (
<View style={styles.screen}>
<Text style={styles.title}>{workspace?.name}</Text>
<Text style={styles.label}>Directory</Text>
<Text style={styles.detail}>{workspace?.directory}</Text>
</View>
);
}
```
`useWorkspace` 从应用的缓存状态中读取面板要渲染的字段,不需要 RPC,并且在无关字段变化时不会重新渲染。每个 `Text` 都从 `theme.colors` 获取颜色,`layout.compact` 决定间距,因此面板在每个 Paseo 主题和手机上都能正常显示。令牌列表见[主题与布局](/docs/plugins/reference#theme-and-layout)。
在 `index.client.tsx` 中加入以下内容,注册该面板以及一个打开它的 Command Center 项:
```tsx
import { WorkspaceOverview } from "./client/overview";
// Inside contribute(client), after the existing registrations:
client.addWorkspacePanel({
id: "overview",
title: "Workspace overview",
icon: "PanelsTopLeft",
context: "workspace",
locations: ["workspace", "explorer"],
Component: WorkspaceOverview,
});
client.addCommandCenterItem({
id: "open-overview",
title: "Open workspace overview",
icon: "PanelsTopLeft",
context: "workspace",
onSelect({ openPanel }) {
openPanel("overview");
},
});
```
`icon` 可以是 [Lucide](https://lucide.dev/icons/) 图标名称、内联 SVG 或组件。参见[图标](/docs/plugins/reference#icons)。
## 编辑并重新加载 {#edit-and-reload}
只有在重新加载插件后,源码改动才会生效:
```bash
npm run typecheck
paseo plugin reload workspace-plugin
```
重新加载会停止旧的插件、运行它的清理函数、编译当前源码,然后重新启动它。重新加载失败后插件会保持失败状态,并在 `paseo plugin ls` 中报告错误;修复源码后再次重新加载即可。
打开一个工作区,在 macOS 上按 **⌘K**,在 Windows 和 Linux 上按 **Ctrl+K**,然后选择 **Open workspace overview**。面板会作为工作区标签页打开。
## 安装已发布的插件 {#install-a-published-plugin}
在 daemon 主机上用 npm 安装一个受信任的 npm 插件:
```bash
paseo plugin install npm:@acme/paseo-review@1.2.0
```
或者把同样的标识符粘贴到 **设置 → 插件 → 插件来源**,然后选择 **安装插件**。安装后的行使用 manifest 中的 ID,并提供与目录插件相同的启用开关以及 Logs、Reload 和 Remove 操作。获取插件时需要 npm;之后的加载和重新加载都使用已安装的文件。
标识符语法见[插件来源](/docs/plugins/reference#plugin-sources)。要分享你自己的插件,请按照[发布插件](/docs/plugins/publishing)操作。
发布在 Git 仓库中的插件通过显式的 Git 简写或 URL 安装:
```bash
paseo plugin add git:owner/repository
paseo plugin add https://gitlab.com/group/repository.git
paseo plugin add git:owner/monorepo:plugins/workspace
paseo plugin add git:owner/repository --ref main
```
当插件位于仓库根目录之下时,追加 `:relative/path`。`--ref` 只在首次安装时选择 Git 内容;省略时安装默认的 HEAD。确切的语法见[来源参考](/docs/plugins/reference#plugin-sources)。
```bash
paseo plugin ls
paseo plugin update workspace-plugin
paseo plugin update --all
```
`ls` 报告来源标识和当前已安装的版本,不会联系远程。`update` 会展示一个拟更新的 npm 或 Git 版本,并请求你批准。npm 提供更新的 latest 发布版本;Git 提供当前默认的 HEAD,与首次安装时的选择器无关。使用 `--check` 只预览,使用 `--yes` 则无需提示直接批准。显式目标和批量更新见 [CLI 参考](/docs/plugins/reference#cli-reference)。
## 读取后端日志 {#read-backend-logs}
daemon 端的处理函数和清理函数可以使用普通的 Node 日志:
```ts
console.log("Refreshing issues");
console.error("Issue refresh failed", error);
```
在 **设置 → 插件** 下打开插件的三点菜单并选择 **Logs**,即可读取最近的输出;也可以使用 CLI:
```bash
paseo plugin logs workspace-plugin
paseo plugin logs workspace-plugin --json
```
日志尾部包含 `[paseo]` 的加载、就绪、停止中和已停止条目,以及编译和加载失败信息,并且在重新加载和崩溃后依然保留。客户端的输出留在应用中。保留策略以及哪些内容不应记录,见[调试后端输出](/docs/plugins/reference#debug-backend-output)。
## 下一步 {#next}
- [发布插件](/docs/plugins/publishing):通过 npm、私有注册表或 Git 分享。
- [构建 Provider 插件](/docs/plugins/providers):直接或通过 ACP 接入 Agent,渲染由 Provider 拥有的时间线条目,测试并发布它。
- [插件参考](/docs/plugins/reference):每种贡献类型及其字段、运行时模块、宿主环境和 CLI。
- [把插件迁移到运行时入口](/docs/plugins/migration):逐步迁移基于单一 `index.ts` 入口编写的插件。
- [TypeScript SDK](/docs/sdk):在客户端和服务端代码中以 `paseo` 提供的工作区、Agent、Provider 和配置 API。
+285
View File
@@ -0,0 +1,285 @@
---
title: 把插件迁移到运行时入口
description: 把混合的插件入口机械地迁移为显式的客户端入口和服务端入口。
nav: 迁移
---
# 把插件迁移到运行时入口 {#migrate-a-plugin-to-runtime-entries}
本指南用于把为 Paseo 0.7 或更早版本编写的插件,更新为 0.8 引入的当前客户端/服务端入口格式。
把本页交给一个编程 Agent,并以插件目录作为它的工作目录。按顺序执行各个步骤。不要保留兼容入口。
## 1. 对现有代码分类 {#1-classify-the-existing-code}
从旧的结构开始:
```text
my-plugin/
paseo-plugin.json
package.json
tsconfig.json
index.ts
greeting.client.tsx
greeting.server.ts
greeting.shared.ts
```
完成后的结构是:
```text
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. 重命名文件和目录 {#2-rename-files-and-directories}
严格按以下规则操作:
1. 用 `index.client.tsx`、`index.server.ts` 或两者替换混合的根入口。
2. 把每个 `name.client.ts` 或 `name.client.tsx` 移动到 `client/name.ts` 或 `client/name.tsx`。
3. 把每个 `name.server.ts` 或 `name.server.tsx` 移动到 `server/name.ts` 或 `server/name.tsx`。
4. 把每个 `name.shared.ts` 或 `name.shared.tsx` 移动到 `shared/name.ts` 或 `shared/name.tsx`。
5. 在对应的运行时目录下保留嵌套的功能目录。
6. 每次移动后更新相对导入路径。
7. 把 `paseo-plugin.json`、`package.json` 和 `tsconfig.json` 保留在根目录。
8. 删除旧的根入口。Paseo 不会加载它。
这些目录就是编译器的边界。`client/` 下的文件只编译进应用的包,`server/` 下的文件只编译进 daemon 的包,`shared/` 则编译进两者。`*.client.tsx` 之类的文件名后缀不再有任何含义,留在插件根目录的代码模块会导致编译错误。
## 3. 迁移每一处注册 {#3-move-every-registration}
把下表作为完整的注册检查清单。
| 旧的注册及位置 | 新的注册及位置 |
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| 旧根入口中的 `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 })`;见[侧边栏项](reference#sidebar-items) |
| 新的侧边栏底部贡献 | `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)`;见[设置界面](reference#settings-screens) |
| 旧根入口中的 `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 标签 {#composer-pills}
更新插件项目的 `@getpaseo/plugin` 依赖,然后运行 `npm run typecheck`。旧的贡献缺少必需的 `button` 字段,`PluginComposerPillProps` 不再导出,并且把新的注册当作函数调用会产生 TypeScript 错误。固定在旧 SDK 上的项目仍然按旧的契约检查;安装或重新加载插件并不会运行 TypeScript 的类型检查器。
把标签的 `Component` 替换为 `button.icon` 和 `button.label`,把 `title` 移入 `button`,把 `onPress` 移入 `button.behavior`。清理方式从调用返回的函数改为调用它的 `.remove()` 方法。
```tsx
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 标签总是显示图标和文字,并且从不显示展开箭头。菜单、弹出框和可见性见[按钮](./reference.md#button-descriptor)。
## 4. 拆分导入 {#4-separate-imports}
把 hook(`usePaseo`、`useRpc`、`useSettings`、`useAgent`、`useWorkspace`)和客户端贡献类型从 `@getpaseo/plugin` 移到 `@getpaseo/plugin/client`。把 `Icon` 移到 `@getpaseo/plugin/client/ui`。从 `@getpaseo/plugin/server` 导入服务端 context 和生命周期契约。共享的辅助函数(`defineRpc`、`defineSettings`、`defineAttachmentSource`)、schema 和普通数据类型保留在根入口。这些规则同样适用于类型导入。完整的契约见[运行时模块](reference#runtime-modules)。
把其余的 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 导入 {#ui-imports-in-v013}
所有 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. 识别只迁移了一半的错误 {#5-recognize-half-migration-errors}
| 编译器或加载错误 | 含义与修复方法 |
| -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `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` {#6-worked-example-plugin-exampleslocal-plugin}
只有入口文件和导入路径发生变化。组件和处理函数的函数体原样移动,无需编辑。
迁移前:
```text
local-plugin/
index.ts
main.client.tsx
increment.server.ts
increment.shared.ts
```
```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 () => {};
}
```
迁移后:
```text
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
```
```tsx
// 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);
}
```
```ts
// 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 () => {};
}
```
被移动文件中的导入路径变化:
```diff
// 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 版本要求 {#7-declare-the-paseo-requirement}
迁移完入口和导入后,在 `paseo-plugin.json` 中添加最低运行时版本:
```json
{
"id": "my-plugin",
"requirements": { "paseo": ">=0.8.0" }
}
```
保留你现有的 ID 和构建命令。缺少 `requirements.paseo` 表示 `<0.8.0`,因此即使文件已经移动,Paseo 0.8 及更高版本也会拒绝该插件。仅仅添加这个字段并不会迁移代码。请把本地的 `@getpaseo/plugin` 开发依赖更新到你的目标版本,并在类型检查之前重新安装依赖。
版本范围和预发布版本的语义见[版本要求](reference#requirements)。
## 8. 验证迁移 {#8-verify-the-migration}
运行:
```bash
npm run typecheck
paseo plugin reload <plugin-id>
paseo plugin ls
```
确认状态为 `running` 且没有错误。试用每一项贡献。对于带 RPC 的插件,调用客户端操作并验证服务端结果。对于只有客户端的插件,确认贡献在没有服务端进程的情况下也能加载。对任何保存下来的注册移除函数调用两次,验证第二次调用不产生任何效果。
+384
View File
@@ -0,0 +1,384 @@
---
title: 构建 Provider 插件
description: 直接把编程 Agent 接入 Paseo,或适配一个 ACP Agent。
nav: Provider 插件
---
# 构建 Provider 插件 {#build-a-provider-plugin}
如果你之前没有构建过 Paseo 插件,请从[插件快速上手](/docs/plugins)开始。
Provider 插件把一个编程 Agent 接入 Paseo,而无需把它加入 Paseo 核心。在 npm 或 Git 仓库中[发布插件](/docs/plugins/publishing),用户就可以用 `paseo plugin install` 安装它。
选择一种实现方式:
| Agent | 方式 |
| -------------------------------------------------- | ---------------------------------------------------------- |
| 已经实现了 ACP | 用 `runAcpProvider()` 注册它,只添加少量针对厂商的转换器。 |
| 提供 TypeScript SDK、JSON-RPC API 或自定义进程协议 | 直接实现 `ProviderRegistration`。 |
示例有:
- [`provider-direct`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/provider-direct):会话、设置、提示词、持久化、子会话,以及一个由 Provider 拥有的时间线渲染器;
- [`provider-acp-transformer`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/provider-acp-transformer):一个 ACP 命令模板,带有一个用 Zod 校验的厂商编辑转换器。加载之前,请把 `example-acp --stdio` 替换为一个已安装的 ACP Agent;
- [`inline-thinking`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/inline-thinking):一个只包含渲染器、不实现 Provider 的插件。
## 注册直接实现的 Provider {#register-a-direct-provider}
添加一个服务端入口:
```ts
// index.server.ts
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { createProvider } from "./server/provider";
export default function contribute(server: PluginServerContext) {
server.registerProvider(createProvider());
return () => {};
}
```
`createProvider()` 返回一个 `ProviderRegistration`:
```ts
import {
negotiateProviderCapabilities,
type ProviderRegistration,
} from "@getpaseo/plugin/server/provider";
const supported = ["prompt.message"] as const;
export function createProvider(): ProviderRegistration {
return {
id: "my-agent",
label: "My agent",
icon: "icon.svg",
async connect(request) {
const capabilities = negotiateProviderCapabilities(request.capabilities, supported);
return createConnection(capabilities);
},
};
}
```
连接有三个操作:
- `send(input)` 接收工作后立即返回。结果以事件的形式发布,而不是返回值。
- `onEvent(listener)` 发布会话状态和完整的时间线快照。
- `close()` 停止所有会话,并释放进程、订阅和待处理的工作。
把原生 SDK、进程和流保留在连接的实现内部。在发布之前,把它的输出转换为 `ProviderEvent` 对象。
## 启动 Provider CLI {#launch-the-provider-cli}
对于进程传输和 CLI 探测,请使用 `@getpaseo/plugin/server` 中的 `spawnProcess` 和 `execCommand`。两者都能处理 Windows 的 `.cmd`/`.bat` 启动器,并为 shell 参数加引号。原生的 Node `spawn` 和 `execFile` 在 Windows 上会拒绝这些启动器。
daemon 在 `request.launch` 中提供解析后的命令、参数前缀和完整的环境变量。请显式传入这些环境变量;这些辅助函数不会应用 daemon 的环境策略。把进程生命周期和协议截止时间保留在你的 Provider 内部。正常关闭时结束 stdin;强制停止时使用 `terminateProcess(child)`。它在 Windows 上会结束整个进程树,包括命令脚本启动器背后的 CLI。为最后等待流关闭的过程设定上限。
```ts
import { spawnProcess, execCommand } from "@getpaseo/plugin/server";
const child = spawnProcess(launch.command, [...launch.args, "serve"], {
env: launch.env,
cwd,
stdio: "pipe",
});
const { stdout } = await execCommand(launch.command, [...launch.args, "--version"], {
env: launch.env,
timeout: 5000,
});
```
## 返回模型、模式和思考选项 {#return-models-modes-and-thinking-options}
Paseo 在创建会话之前会请求目录。返回 Agent 表单需要的选项:
```ts
if (input.type === "catalog") {
emit({
type: "catalog",
requestId: input.requestId,
catalog: {
models: [
{ id: "agent-large", label: "Agent Large" },
{ id: "agent-small", label: "Agent Small" },
],
modes: [
{ id: "build", label: "Build" },
{ id: "plan", label: "Plan" },
],
thinkingOptions: [
{ id: "standard", label: "Standard" },
{ id: "extended", label: "Extended" },
],
defaultModel: "agent-large",
defaultMode: "build",
defaultThinkingOption: "standard",
},
});
}
```
对于 Agent 不提供的类别,返回空数组。选中的 `model`、`mode` 和 `thinkingOption` 会出现在会话配置中。
目录描述的是会话存在之前可用的选项。会话专属的控件之后通过 `session.config` 提供。
## 打开会话 {#open-a-session}
`session.open` 包含完整的启动配置:工作目录、环境变量、系统提示词、MCP 服务器、工具策略、模型、模式、设置、不透明的 Provider 选项以及持久化偏好。
对于持久化的、作用于主机的插件设置,请使用 `server.registerSettings()` 返回的句柄。在目录和会话回调中读取它;当 Provider 在内存中保留配置时,订阅它。`read()` 和 `subscribe()` 的契约见[设置](./reference.md#persisted-values)。
用选中的目录值创建原生会话,然后发布它实际生效的状态:
```ts
const nativeSession = await sdk.createSession({
cwd: input.config.cwd,
model: input.config.model,
mode: input.config.mode,
thinking: input.config.thinkingOption,
systemPrompt: input.config.systemPrompt,
env: input.config.env,
mcpServers: input.config.mcpServers,
});
emit({
type: "session.opened",
requestId: input.requestId,
sessionId: input.sessionId,
capabilities: ["prompt.message"],
restoration: "core",
cwd: input.config.cwd,
});
emit({
type: "session.config",
sessionId: input.sessionId,
config: {
model: nativeSession.model,
mode: nativeSession.mode,
thinkingOption: nativeSession.thinking,
models,
modes,
thinkingOptions,
settings: [],
},
});
emit({ type: "session.ready", requestId: input.requestId, sessionId: input.sessionId });
```
`session.config` 包含已确定的值。请发布原生 Agent 经过规范化后实际选中的值,而不是请求的副本。
## 完成第一段提示词 {#complete-the-first-prompt}
从消息类提示词开始。声明 `prompt.message`,把内容传给原生 Agent,并发布由此产生的生命周期:
```ts
const turnId = crypto.randomUUID();
emit({
type: "timeline.item",
sessionId,
item: {
type: "user_message",
id: `user:${turnId}`,
text,
clientMessageId: prompt.clientMessageId,
},
});
emit({
type: "session.prompt_result",
sessionId,
clientMessageId: prompt.clientMessageId,
result: { type: "turn", turnId },
});
emit({ type: "session.turn", sessionId, turnId, state: "started" });
// Publish complete snapshots as native output changes.
emit({
type: "timeline.item",
sessionId,
item: { type: "assistant_message", id: `assistant:${turnId}`, text: completeText },
});
emit({ type: "session.turn", sessionId, turnId, state: "completed" });
```
对每个 `clientMessageId` 恰好发布一个 `session.prompt_result`。把 `clientMessageId` 复制到用户的时间线条目上,以便 Paseo 替换它乐观显示的消息。每个已开始的轮次都需要一个终止的 `completed`、`failed` 或 `canceled` 事件。
到这一步,Provider 已经可用:用户可以选择模型、打开会话、发送消息并看到回复。
## 添加会话专属的 Composer 控件 {#add-session-specific-composer-controls}
只有当原生 Agent 有用户在打开会话后需要修改的设置时,才添加 `session.configure`。在 `session.config` 中以开关或选择控件的形式发布它们:
```ts
emit({
type: "session.config",
sessionId,
config: {
model: "agent-large",
models: [{ id: "agent-large", label: "Agent Large" }],
modes: [],
thinkingOptions: [],
settings: [
{
type: "toggle",
id: "fast",
label: "Fast mode",
value: nativeSession.fast,
},
{
type: "select",
id: "approval",
label: "Approvals",
value: nativeSession.approval,
options: [
{ label: "Ask", value: "ask" },
{ label: "Auto accept", value: "auto" },
],
},
],
},
});
```
Paseo 会在 Composer 中渲染这些控件。用户的修改会以 `session.configure` 的形式到达;应用它,并在 `request.completed` 之前发布已确定的 `session.config`。Provider 可以在运行时改变可用的控件,包括在模型变化之后。
`settings` 是面向用户的控件。`providerOptions` 是在创建会话时提供的不透明 JSON,用于 Paseo 不会渲染的 Provider 专属配置。Provider 专属的 token 预算应放在这两处之一;它不是一个专门的提示词字段。
## 添加持久化与重放 {#add-persistence-and-replay}
当原生会话可以被重新打开时,声明 `session.persistence`。当 `session.open.persistence` 不存在时,打开一个新的原生会话;存在时,恢复它所指定的原生会话。在 `session.opened` 或 `session.persistence` 中返回一个不透明的持久化值;Paseo 会保存它而不检查其内容。
当 `history` 为 `"replay"` 时,请在 `session.ready` 之前发布原生会话已有的 `timeline.item` 快照。使用 `history: "skip"` 可以在不重放旧记录的情况下打开会话。
Paseo 刷新 Agent 的方式是关闭它当前的 Provider 会话,然后用当前的配置和持久化值重新打开。请在 `session.open` 期间重新读取凭据、环境变量、全局配置和 MCP 服务器。没有单独的重新加载操作。
当 Paseo 可以从持久化值重新打开会话时,使用 `restoration: "core"`。由 Provider 拥有的子会话使用 `restoration: "parent"`;把它作为另一个带有 `parentSessionId` 的 `session.opened` 事件发出,并在恢复父会话时重新创建它。
## 添加命令和引导 {#add-commands-and-steering}
所有用户输入都通过 `session.prompt` 到达:
- `input.type: "message"` 携带文本、图片和附件;
- `input.type: "command"` 携带所选的命令及其参数;
- `delivery: "steer"` 要求你把输入加入当前正在进行的轮次;
- `delivery: "auto"` 允许你开始一个轮次,或者在没有轮次的情况下完成一条命令。
发布 `session.commands`,向 Composer 添加结构化命令。命令可以开始一个普通轮次,也可以作为副作用完成,并返回一个 `completed` 的提示词结果。
只声明你实现了的能力。如果不支持引导,请省略 `prompt.steer`;Paseo 可以改为替换当前轮次。
## 发布时间线条目 {#publish-timeline-items}
通过 `timeline.item` 发布完整的快照。更新流式文本、运行中的工具或待办列表时,复用条目的 `id`。Paseo 会推导出实时增量,并保持正常的时间线。
内置的条目类型涵盖用户和助手消息、推理、工具、待办事项、错误、通知和上下文压缩。当你的 Provider 有这些类型无法表达的展示方式时,使用插件条目:
```ts
emit({
type: "timeline.item",
sessionId,
item: {
type: "plugin",
id: "review-42",
pluginId: "my-provider-plugin",
kind: "review-verdict",
version: 1,
data: { verdict: "ship", summary: "All checks passed" },
},
});
```
在 `index.client.tsx` 中单独注册渲染器:
```tsx
import type { PluginClientContext } from "@getpaseo/plugin/client";
import { z } from "zod";
import { ReviewVerdict } from "./client/review-verdict";
const reviewVerdictSchema = z.object({
verdict: z.enum(["ship", "hold"]),
summary: z.string(),
});
export default function contribute(client: PluginClientContext) {
client.addTimelineRenderer({
kind: "review-verdict",
version: 1,
schema: reviewVerdictSchema,
Component: ReviewVerdict,
});
return () => {};
}
```
同一个渲染器可以显示由 Provider、客户端时间线转换器或 daemon 时间线追加发出的条目。渲染器并不需要 Provider 实现。
## 适配 ACP Agent {#adapt-an-acp-agent}
当 Agent 已经支持 ACP 时,使用 ACP 适配层:
```ts
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { runAcpProvider } from "@getpaseo/plugin/server/acp";
export default function contribute(server: PluginServerContext) {
server.registerProvider(
runAcpProvider({
id: "vendor-agent",
label: "Vendor agent",
icon: "icon.svg",
command: ["vendor-agent", "acp"],
}),
);
return () => {};
}
```
适配层负责 ACP 进程、能力映射、会话生命周期、提示词、权限和时间线转换。不要把这些机制复制到插件中。
只在 ACP 无法描述厂商差异时使用 `transformers`。用 Zod 校验厂商负载,并让格式错误或无关的值保持不变:
```ts
import type { AcpTransformer } from "@getpaseo/plugin/server/acp";
import { z } from "zod";
const editSchema = z.object({
path: z.string(),
before: z.string(),
after: z.string(),
});
export const vendorEdits: AcpTransformer = {
toolCall(toolCall) {
if (toolCall.name !== "vendor_file_edit") return toolCall;
const input = editSchema.safeParse(toolCall.input);
if (!input.success) return toolCall;
return {
...toolCall,
kind: "edit",
input: {
filePath: input.data.path,
oldString: input.data.before,
newString: input.data.after,
},
};
},
};
```
## 测试与发布 {#test-and-publish}
用真实的 Agent 测试 Provider,而不只是模拟的数据帧:
1. 以目录来源安装插件;
2. 确认它的 SVG、模型、模式和设置都会显示;
3. 创建会话并完成一段真实的提示词;
4. 在支持的情况下,试用权限、引导、取消、持久化和重放;
5. 在桌面和移动端宽度的布局中渲染每一种自定义时间线条目;
6. 在会话处于活动状态时重新加载和移除插件,并确认会话随之终止。
按照[发布插件](/docs/plugins/publishing)通过 npm 或 Git 分发 Provider。把 Agent 兼容性和版本发布保留在插件项目中。
确切的运行时和 SVG 规则见[插件参考](/docs/plugins/reference#providers)。
+31
View File
@@ -49,6 +49,37 @@ overview, including development or setup examples. Link to your README for those
Also supply a one-sentence description: use `package.json`'s `description` for npm or the manifest's
`description` for Git. The reviewer can fill in a missing description in the registry.
### Other languages
Listings show in English and Simplified Chinese. English is required and translations are optional.
Add a translated `name` and `description` to `paseo-plugin.json`, keyed by language tag:
```json
{
"id": "review-tools",
"name": "Review tools",
"description": "Reviews changes before merge",
"translations": {
"zh-CN": { "name": "审查工具", "description": "在合并前审查改动" }
}
}
```
Each field is optional, and a missing one shows the English text. The registry checks
`translations` when it publishes the plugin: keys are canonical language tags other than `en`, and
each value sets only `name` or `description`, as non-empty strings. Paseo itself does not read
`translations`, so a mistake there fails publication, not installation.
To translate the overview, publish a complete copy named for the language next to `OVERVIEW.md`,
such as `OVERVIEW.zh-CN.md`. The scaffold's `files` list already includes `OVERVIEW.*.md`. The
registry reads `OVERVIEW.<tag>.md` as a translation when the tag is a canonical language tag with a
two- or three-letter language code, other than `en`. It ignores other names, such as
`OVERVIEW.backup.md` or `OVERVIEW.zh-cn.md`. A three-letter name such as `OVERVIEW.old.md` counts as
a language, so do not use one for a non-translation. Readers in that language see the
translation; others see the English overview with a notice. The overview rules above apply to
translations, so update them when `OVERVIEW.md` changes. Reviewers can add or correct translations
in the registry.
## Icons and screenshots
Themes, panels and other visual plugin interfaces need at least one image showing the current
+232
View File
@@ -0,0 +1,232 @@
---
title: 发布插件
description: 准备、发布 Paseo 插件,并把它提交到经过审查的注册表。
nav: 发布
---
# 发布插件 {#publish-a-plugin}
从一个可用的[插件项目](/docs/plugins)开始。准备好它的展示页,把它发布到 npm 或推送到 GitHub,然后提交到注册表。获得批准后,用户可以用 `paseo plugin add owner/slug` 安装经过审查的版本。
1. 准备你的[展示页](#your-listing-page);对于可视化插件,还要准备一张[界面图片](#icons-and-screenshots)。
2. [发布到 npm](#publish-on-npm)或[通过 Git 分享](#share-through-github-or-git)。
3. [提交到插件注册表](#plugin-registry)。
对于私有插件,请使用[直接从 Git 安装](#share-through-github-or-git)或[私有 npm 包](#private-npm-packages)。
## 你的展示页 {#your-listing-page}
你可以不带 `OVERVIEW.md` 提交;审查者会根据你的 README 和经过验证的行为创建展示页。要自己维护这个页面,请把 `OVERVIEW.md` 放在 `paseo-plugin.json` 旁边,并把它包含在发布的包中。脚手架已经在 `files` 中包含了它。你随包发布的概览会替换审查者编写的页面。
按以下顺序说明这些要点,只在有帮助时使用标题:
1. 插件做什么。
2. 它如何工作(当这一点不明显时)。
3. 设置:设置项、账号、令牌、外部工具、支持的操作系统和 Paseo 版本。
4. 有用的能力、设置项、数据访问、权限和限制。
说明数据会流向哪里,包括发送给 Agent Provider 的提示词和工具结果。本地存储并不意味着数据留在主机上。如果插件在运行时下载或安装软件,请说明它安装什么以及来源,并在下载之前要求用户明确确认。这同样适用于激活和连接测试;用户拒绝时,软件必须保持未安装状态。
使用平实、客观的语言,标题只把首字母大写。省略营销内容、无依据的说法、徽章、更新日志、贡献和许可证章节,以及无助于他人选择插件的实现细节。展示页已经提供了插件的安装说明。
发布校验器会拒绝概览中任何位置出现的 `paseo plugin add`、`npm install` 和 `npm i`,包括开发或设置示例。请为这些命令链接到你的 README。
还要提供一句话的描述:npm 插件使用 `package.json` 的 `description`,Git 插件使用 manifest 的 `description`。缺少描述时,审查者可以在注册表中补上。
### 其他语言 {#other-languages}
展示页以英文和简体中文显示。英文是必需的,翻译是可选的。在 `paseo-plugin.json` 中添加翻译后的 `name` 和 `description`,以语言标签为键:
```json
{
"id": "review-tools",
"name": "Review tools",
"description": "Reviews changes before merge",
"translations": {
"zh-CN": { "name": "审查工具", "description": "在合并前审查改动" }
}
}
```
每个字段都是可选的,缺少的字段会显示英文。注册表在发布插件时检查 `translations`:键必须是除 `en` 之外的标准语言标签,每个值只能设置 `name` 或 `description`,且必须是非空字符串。Paseo 本身不读取 `translations`,因此这里的错误会导致发布失败,而不会影响安装。
要翻译概览,请在 `OVERVIEW.md` 旁边发布一份以语言命名的完整副本,例如 `OVERVIEW.zh-CN.md`。脚手架的 `files` 列表已经包含 `OVERVIEW.*.md`。当标签是语言代码为两个或三个字母、且不是 `en` 的标准语言标签时,注册表会把 `OVERVIEW.<标签>.md` 当作翻译,并忽略 `OVERVIEW.backup.md`、`OVERVIEW.zh-cn.md` 这类其他名称。`OVERVIEW.old.md` 这样的三字母名称会被当作一种语言,因此不要用它命名非翻译文件。使用该语言的读者会看到翻译;其他读者会看到英文概览和一条提示。上面的概览规则同样适用于翻译,因此 `OVERVIEW.md` 变更时请同步更新翻译。审查者可以在注册表中补充或修正翻译。
## 图标和截图 {#icons-and-screenshots}
主题、面板以及其他带可视界面的插件,需要至少一张展示当前界面的图片。请使用示例数据并隐藏隐私信息。只有图标不满足这项要求;没有自己可视界面的插件不需要截图。
发布之前,在 `paseo-plugin.json` 中声明本地图片:
```json
{
"id": "review-tools",
"name": "Review tools",
"description": "Reviews changes before merge",
"icon": "assets/icon.png",
"media": ["assets/screenshot.png"],
"requirements": { "paseo": ">=0.11.0" }
}
```
创建所引用的文件;对于 npm,把 `assets/` 加入 `package.json` 的 `files` 列表。路径相对于 manifest,使用正斜杠,并且必须位于插件目录之内。这些元数据字段需要 Paseo 0.11.0 或更高版本。
| 字段 | 展示行为 |
| ------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `name` | 可选的显示名称;默认是把插件 ID 转换成易读形式。 |
| `icon` | 可选的、相对于包的 PNG 路径。Provider 注册图标有单独的 [SVG 约定](/docs/plugins/reference#providers)。 |
| `media` | 相对于包的路径或公开的 HTTPS URL。图片:PNG、JPG、JPEG、WebP 或 GIF。视频:MP4 或 WebM。第一张图片是卡片缩略图;视频作为补充。 |
公开的媒体 URL 需要带有受支持的文件扩展名(不区分大小写),并且必须以对应的图片或视频内容类型返回 HTTP 200。SVG 和没有扩展名的附件 URL 不能作为注册表媒体。
除了声明 `media`,你也可以在提交或审查 PR 中提供合适的公开图片 URL。审查者可以把它们添加到展示页,无需发布新的插件版本。
## 发布到 npm {#publish-on-npm}
### 1. 设置包的标识 {#1-set-package-identity}
使用一个你有发布权限的 npm scope,并通过 `npm login` 登录。在插件目录中,把下面的 `@acme` 和仓库替换为你自己的值:
```bash
npm pkg set name=@acme/paseo-review version=1.0.0
npm pkg delete private
npm pkg set repository.type=git repository.url=git+https://github.com/acme/paseo-review.git
```
当前的注册表工具要求通过 `package.json` 或 npm provenance 提供一个 GitHub 仓库,用来确定 npm 展示页的归属。请把 `repository` 设置为你公开的源码仓库,以便固定版本时可以确定展示页的所有者。provenance 本身是可选的;如果存在,它的仓库必须与包的仓库一致。审查制品时不需要匹配的源码检出。
### 2. 锁定安装的依赖 {#2-lock-installed-dependencies}
把 Paseo 宿主提供的库和仅用于类型的依赖放在 `devDependencies` 中。把其他运行时库加入 `dependencies`。提交到注册表时,每棵安装的依赖树都需要一个锁文件,包含确切的版本、解析来源和完整性校验,传递依赖也不例外。
对于带有运行时依赖的 npm 包,请在不运行生命周期脚本的情况下更新它的锁文件,然后创建可发布的锁文件:
```bash
npm install --package-lock-only --ignore-scripts
npm shrinkwrap
```
`npm shrinkwrap` 会把 `package-lock.json` 转换为 `npm-shrinkwrap.json`。npm 会在包的根目录发布 shrinkwrap 文件,但不会发布 `package-lock.json`。只存在于仓库中的锁文件或确切的直接依赖版本,并不能锁定用户安装的依赖树。
只使用宿主提供的库和开发依赖的插件不需要运行时锁文件,除非某个准备命令会安装这些依赖。你也可以把库打包进插件,并移除运行时依赖声明。如果你的插件会安装一棵单独的依赖树,请参阅[准备与额外设置](#plugins-with-a-build-step)。
### 3. 检查包并发布 {#3-check-the-package-and-publish}
在检查包之前生成所有需要的文件;dry run 会跳过打包脚本。
```bash
npm run typecheck
npm pack --dry-run --ignore-scripts
```
检查文件列表是否包含 `paseo-plugin.json`、至少一个运行时入口、每个被导入的本地模块、你声明的所有概览和资源文件,以及需要时的 shrinkwrap 文件。只在仓库中存在的导入别名和包外的文件不会到达用户手中。脚手架已经包含了入口以及 `client/`、`server/` 和 `shared/`;请把其他资源目录加入 `files`。
确认所有声明的准备命令在你支持的 Node/npm 版本下都能成功;不同步或不兼容的锁文件仍然会阻止安装。
发布检查过的版本:
```bash
npm publish --access public
```
在测试用的 daemon 上,安装这个确切的已发布版本,并试用它的各项贡献:
```bash
paseo plugin install npm:@acme/paseo-review@1.0.0
paseo plugin ls
```
确认状态为 `running` 且没有错误,然后[提交这个包](#plugin-registry)。
## 通过 GitHub 或 Git 分享 {#share-through-github-or-git}
对于公开注册表,请把插件推送到一个公开的 GitHub 仓库。包含它的 manifest、运行时入口、被导入的文件和展示页资源。提交时 Git 标签和提交是可选的;提供一个即可选择某个修订,否则审查者会解析 `main`。
如果准备过程会安装运行时 npm 依赖,请提交一个同步的 `package-lock.json`,并把以下命令加入 `paseo-plugin.json`:
```json
{
"build": [["npm", "ci", "--omit=dev", "--ignore-scripts"]]
}
```
`npm ci` 使用已提交的锁文件,如果它与 `package.json` 不一致就会失败。`--omit=dev` 排除开发工具,`--ignore-scripts` 禁止依赖的生命周期脚本。daemon 的命令路径中需要有 npm。只使用宿主提供的库的插件不需要准备命令。生成文件或显式脚本见[准备](#plugins-with-a-build-step)。
用户可以不经注册表展示页,直接从 Git 安装:
```bash
paseo plugin add git:acme/paseo-review
paseo plugin add git:acme/monorepo:plugins/review
```
使用 `--ref` 为首次安装选择分支、标签或提交。只要 daemon 有访问权限,直接安装也支持私有仓库和其他 Git 托管平台;见[插件来源](/docs/plugins/reference#plugin-sources)。注册表提交接受公开的 GitHub 来源和公开的 npm 包。
## 插件注册表 {#plugin-registry}
打开[提交表单](https://github.com/getpaseo/plugins/issues/new?template=submit-plugin.yml)。请使用该表单,这样 issue 才会获得启动收录流程的提交标签。
1. 用插件的名称作为标题。
2. 在 **Source** 中填写公开的 npm 包名或页面,或 GitHub 仓库/文件夹的 URL。如果你希望审查某个特定版本,请附上版本号或 Git 引用。npm 默认使用 `latest`。注册表 ID 和本地目录不能作为提交来源。
3. 选择合适的分类。把所有界面图片的 URL 写在 issue 描述中。
机器人会打开一个审查 PR。审查者在不运行插件代码的情况下检查制品和依赖,完善展示页,并记录被批准的确切版本或提交。发布需要符合[审查政策](https://github.com/getpaseo/plugins/blob/main/REVIEW.md),包括锁定的安装输入和恰当的数据访问,即使自动校验已经通过。
如果被要求修改,请在审查 PR 中回复修正后的版本或修订,或所要求的图片 URL。审查者会更新固定的版本和展示页。收录服务的错误会自动重试;缺少图片或概览不会妨碍打开审查 PR。
获得批准并发布后,用户可以这样安装你的插件:
```bash
paseo plugin add owner/slug
```
npm 发布会自动收到更新 PR。Git 更新需要手动提交所需的修订。已有的注册表安装使用被批准的固定版本;直接通过 npm/Git 安装的插件遵循[来源参考](/docs/plugins/reference#plugin-sources)中各自的更新规则。
## 带构建步骤的插件 {#plugins-with-a-build-step}
Paseo 会编译 TypeScript,因此普通插件不需要单独的发布构建。请把生成的模块放在它们的运行时目录中,并把宿主提供的模块保留为外部依赖。
Paseo 在获取包时会禁止 npm 生命周期脚本。manifest 的 `build` 中显式声明的命令仍然会运行,包括它们调用的脚本。请说明这些命令的用途,并让它们的输入保持锁定且可检查。当 npm 已经安装了生产依赖时,请从 npm manifest 中移除只用于 Git 的依赖安装命令。
之后安装另一棵依赖树的设置步骤需要它自己的完整锁文件,即使你的插件已经有一个开发用的锁文件。对于 npm 设置,请把运行时 manifest 和锁文件一起发布,并使用 `npm ci`。对于 Python 设置,请发布一个 requirements 文件,其中包含所有传递依赖包的确切版本和 SHA-256 哈希,然后这样安装:
```bash
python -m pip install --require-hashes -r requirements-documents.txt
```
该命令会拒绝未固定版本或没有哈希的依赖。运行时安装还需要满足上文所述的[确认和披露](#your-listing-page)要求。
## 私有 npm 包 {#private-npm-packages}
:::example[用 GitHub Packages 发布私有包 {#publish-a-private-package-with-github-packages}]
你可以把公司内部的插件发布到 GitHub Packages。按照上面的 npm 步骤操作,使用你组织的 scope,并把发布命令替换为:
```bash
npm publish --registry=https://npm.pkg.github.com
```
发布之前,请先[配置 GitHub 身份验证和包访问权限](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry)。
要安装插件,请**在 daemon 主机上,以运行 Paseo 的用户身份**配置 npm:
1. 把你组织的注册表加入 `~/.npmrc`:
```ini
@acme:registry=https://npm.pkg.github.com
```
2. 用你的 GitHub 用户名登录,并以一个 personal access token (classic) 作为密码。该令牌需要 `read:packages` 以及对该包的访问权限。
```bash
npm login --scope=@acme --auth-type=legacy --registry=https://npm.pkg.github.com
```
3. 安装插件:
```bash
paseo plugin install npm:@acme/paseo-review
```
Paseo 在安装和更新时使用主机的 npm 注册表设置和凭据。在应用中,只需输入来源标识符。
:::
+10 -9
View File
@@ -53,15 +53,16 @@ The required root manifest is `paseo-plugin.json`:
}
```
| Field | Required | Behavior |
| -------------- | -------- | ----------------------------------------------------------------------------------------- |
| `id` | Yes | Default installation ID. |
| `name` | No | Non-empty display name for the registry and website; independent of the installation ID. |
| `icon` | No | Relative path to a PNG inside the plugin package. |
| `media` | No | Array of image or video paths inside the package, or HTTPS URLs. An empty array is valid. |
| `description` | No | Non-empty summary shown below the plugin ID in **Settings → Plugins**. |
| `requirements` | No | Supported Paseo versions, described below. |
| `build` | No | Preparation commands, described in the CLI reference. |
| Field | Required | Behavior |
| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | Yes | Default installation ID. |
| `name` | No | Non-empty display name for the registry and website; independent of the installation ID. |
| `icon` | No | Relative path to a PNG inside the plugin package. |
| `media` | No | Array of image or video paths inside the package, or HTTPS URLs. An empty array is valid. |
| `description` | No | Non-empty summary shown below the plugin ID in **Settings → Plugins**. |
| `translations` | No | Listing text in other languages, read by the registry and ignored by Paseo. See [other languages](/docs/plugins/publishing#other-languages). |
| `requirements` | No | Supported Paseo versions, described below. |
| `build` | No | Preparation commands, described in the CLI reference. |
Paths are relative to `paseo-plugin.json`, use forward slashes, and cannot contain `..`
segments. Registry listings for visual plugins require an interface image even though `media` is
File diff suppressed because it is too large Load Diff