Files
proxycast/docs/plugin-ui-design.md
T
coso 303db47193 feat: 插件系统完整实现 v0.22.0
- 实现插件安装器 (URL/本地/GitHub 安装)
- 实现插件 UI 渲染系统
- 支持 builtin 类型插件 (内置 React 组件)
- 支持 binary 类型插件 (外部二进制)
- MachineIdTool 迁移为插件
- 工具箱页面动态加载插件工具
- 推荐插件一键安装功能
- 插件卸载功能
- Release workflow 自动打包插件
2025-12-30 00:47:26 +08:00

463 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ProxyCast Plugin UI 系统设计
## 概述
借鉴 A2UI 的设计理念,为 ProxyCast 设计一套声明式的插件 UI 系统。核心思想是:
- **安全如数据,表达如代码**:插件只能声明 UI 结构,不能执行任意代码
- **声明式 JSON 格式**:插件通过 JSON 描述 UI 意图,宿主应用负责渲染
- **组件目录(Catalog)机制**:预定义可用组件集,插件只能使用目录中的组件
- **数据绑定分离**:UI 结构与数据模型分离,支持增量更新
## 架构设计
```
┌─────────────────────────────────────────────────────────────────┐
│ ProxyCast Host │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Plugin UI Renderer │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ Component │ │ Data │ │ Event │ │ │
│ │ │ Registry │ │ Store │ │ Handler │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ JSON Messages │
│ ┌───────────────────────────┼─────────────────────────────┐ │
│ │ Plugin Bridge │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ Tauri │ │ Schema │ │ Message │ │ │
│ │ │ IPC │ │ Validator │ │ Router │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
▲
│
┌───────────────┴───────────────┐
│ Plugin (Rust) │
│ ┌─────────────────────────┐ │
│ │ UI Declaration API │ │
│ │ - surface_update() │ │
│ │ - data_update() │ │
│ │ - begin_rendering() │ │
│ └─────────────────────────┘ │
└───────────────────────────────┘
```
## 核心概念
### 1. Surface(渲染表面)
每个插件可以拥有一个或多个 Surface,代表独立的 UI 区域:
```typescript
interface Surface {
surfaceId: string; // 唯一标识
pluginId: string; // 所属插件
rootComponentId: string; // 根组件 ID
components: Map<string, Component>; // 组件缓冲区
dataModel: Record<string, any>; // 数据模型
styles?: SurfaceStyles; // 样式配置
}
```
### 2. Component Catalog(组件目录)
预定义的安全组件集,插件只能使用这些组件:
```typescript
// 标准组件目录
const StandardCatalog = {
// 布局组件
Row: { children: 'ComponentRef[]', gap?: 'number', align?: 'Alignment' },
Column: { children: 'ComponentRef[]', gap?: 'number', align?: 'Alignment' },
Card: { child: 'ComponentRef', title?: 'BoundValue<string>' },
Tabs: { items: 'TabItem[]' },
// 展示组件
Text: { text: 'BoundValue<string>', variant?: 'TextVariant' },
Icon: { name: 'IconName', size?: 'number', color?: 'string' },
Badge: { text: 'BoundValue<string>', variant?: 'BadgeVariant' },
Progress: { value: 'BoundValue<number>', max?: 'number' },
// 输入组件
Button: { child: 'ComponentRef', action: 'Action', variant?: 'ButtonVariant' },
TextField: { label: 'BoundValue<string>', value: 'BoundValue<string>' },
Switch: { label: 'BoundValue<string>', checked: 'BoundValue<boolean>' },
Select: { options: 'SelectOption[]', value: 'BoundValue<string>' },
// 数据展示
Table: { columns: 'TableColumn[]', data: 'BoundValue<any[]>' },
List: { children: 'ChildrenDef', direction?: 'Direction' },
KeyValue: { items: 'KeyValueItem[]' },
// 反馈组件
Alert: { message: 'BoundValue<string>', type: 'AlertType' },
Spinner: { size?: 'number' },
Empty: { description?: 'BoundValue<string>' },
};
```
### 3. 消息协议
#### Server → Client 消息
```typescript
// 组件更新
interface SurfaceUpdate {
surfaceId: string;
components: ComponentDef[];
}
// 数据更新
interface DataModelUpdate {
surfaceId: string;
path?: string; // JSONPath,如 '/credentials/0/status'
contents: DataEntry[];
}
// 开始渲染
interface BeginRendering {
surfaceId: string;
root: string; // 根组件 ID
catalogId?: string;
styles?: SurfaceStyles;
}
// 删除 Surface
interface DeleteSurface {
surfaceId: string;
}
```
#### Client → Server 消息
```typescript
// 用户操作
interface UserAction {
name: string; // 操作名称
surfaceId: string;
sourceComponentId: string;
context: Record<string, any>; // 解析后的上下文数据
timestamp: string;
}
```
### 4. 数据绑定
支持字面值和路径绑定:
```typescript
type BoundValue<T> =
| { literal: T } // 字面值
| { path: string } // 数据路径
| { literal: T; path: string }; // 初始化 + 绑定
// 示例
const textComponent = {
id: 'status-text',
component: {
Text: {
text: { path: '/credential/status' }, // 绑定到数据模型
variant: 'body'
}
}
};
```
## 实现方案
### 前端:React Renderer
```
src/lib/plugin-ui/
├── index.ts # 导出入口
├── types.ts # 类型定义
├── PluginUIRenderer.tsx # 主渲染器组件
├── PluginSurface.tsx # Surface 容器
├── ComponentRegistry.ts # 组件注册表
├── DataStore.ts # 数据存储
├── MessageHandler.ts # 消息处理
└── components/ # 标准组件实现
├── layout/
│ ├── Row.tsx
│ ├── Column.tsx
│ ├── Card.tsx
│ └── Tabs.tsx
├── display/
│ ├── Text.tsx
│ ├── Icon.tsx
│ ├── Badge.tsx
│ └── Progress.tsx
├── input/
│ ├── Button.tsx
│ ├── TextField.tsx
│ ├── Switch.tsx
│ └── Select.tsx
└── data/
├── Table.tsx
├── List.tsx
└── KeyValue.tsx
```
### 后端:Rust Plugin API
```rust
// src-tauri/src/plugins/ui_api.rs
/// 插件 UI 声明 API
pub trait PluginUI {
/// 获取插件的 Surface 定义
fn get_surfaces(&self) -> Vec<SurfaceDefinition>;
/// 处理用户操作
fn handle_action(&mut self, action: UserAction) -> Result<Vec<UIMessage>>;
}
/// UI 消息类型
pub enum UIMessage {
SurfaceUpdate(SurfaceUpdate),
DataModelUpdate(DataModelUpdate),
BeginRendering(BeginRendering),
DeleteSurface(DeleteSurface),
}
/// Surface 定义
pub struct SurfaceDefinition {
pub surface_id: String,
pub initial_components: Vec<ComponentDef>,
pub initial_data: serde_json::Value,
pub root_id: String,
}
```
## 使用示例
### 插件端(Rust)
```rust
impl PluginUI for CredentialMonitorPlugin {
fn get_surfaces(&self) -> Vec<SurfaceDefinition> {
vec![SurfaceDefinition {
surface_id: "credential-monitor".into(),
root_id: "root".into(),
initial_components: vec![
component!("root", Column {
children: explicit_list!["header", "credential-list"],
gap: 16
}),
component!("header", Row {
children: explicit_list!["title", "refresh-btn"],
align: "spaceBetween"
}),
component!("title", Text {
text: literal!("凭证监控"),
variant: "h3"
}),
component!("refresh-btn", Button {
child: "refresh-icon",
action: action!("refresh")
}),
component!("refresh-icon", Icon { name: "refresh" }),
component!("credential-list", List {
children: template!("credential-item", "/credentials"),
direction: "vertical"
}),
// 模板组件
component!("credential-item", Card {
child: "item-content"
}),
component!("item-content", Row {
children: explicit_list!["item-name", "item-status"]
}),
component!("item-name", Text {
text: path!("name") // 相对路径,从列表项数据解析
}),
component!("item-status", Badge {
text: path!("status"),
variant: path!("statusVariant")
}),
],
initial_data: json!({
"credentials": []
}),
}]
}
fn handle_action(&mut self, action: UserAction) -> Result<Vec<UIMessage>> {
match action.name.as_str() {
"refresh" => {
let credentials = self.fetch_credentials()?;
Ok(vec![UIMessage::DataModelUpdate(DataModelUpdate {
surface_id: "credential-monitor".into(),
path: Some("/credentials".into()),
contents: credentials.into_data_entries(),
})])
}
_ => Ok(vec![])
}
}
}
```
### 宿主端(React)
```tsx
// 在插件详情页使用
function PluginDetailPage({ pluginId }: { pluginId: string }) {
return (
<div className="plugin-detail">
<PluginInfo pluginId={pluginId} />
{/* 插件 UI 渲染区域 */}
<PluginUIRenderer
pluginId={pluginId}
onAction={(action) => invoke('plugin_handle_action', { pluginId, action })}
/>
</div>
);
}
```
## 安全考虑
1. **组件白名单**:只允许使用预定义的组件类型
2. **Schema 验证**:所有消息必须通过 JSON Schema 验证
3. **沙箱隔离**:每个插件的 Surface 相互隔离
4. **Action 审计**:记录所有用户操作,支持权限控制
5. **资源限制**:限制组件数量、数据大小等
## 扩展机制
### 自定义组件注册
允许宿主应用注册额外的组件:
```typescript
// 注册自定义组件
componentRegistry.register('CredentialCard', CredentialCardComponent, {
schema: {
credential: { type: 'object', required: true },
onRefresh: { type: 'action' }
}
});
```
### 主题支持
通过 Surface styles 支持主题定制:
```typescript
interface SurfaceStyles {
primaryColor?: string;
font?: string;
borderRadius?: number;
// ... 更多样式属性
}
```
## 迁移路径
1. **Phase 1**:实现核心渲染器和基础组件
2. **Phase 2**:添加数据绑定和事件处理
3. **Phase 3**:迁移现有插件 UI 到新系统
4. **Phase 4**:支持自定义组件扩展
## 与 A2UI 的差异
| 特性 | A2UI | ProxyCast Plugin UI |
|------|------|---------------------|
| 传输方式 | SSE/JSONL 流 | Tauri IPC |
| 渲染框架 | Lit/Angular/Flutter | React |
| 组件风格 | Material Design | TailwindCSS/shadcn |
| 数据更新 | 增量流式 | 批量更新 |
| 使用场景 | 跨平台 Agent UI | 桌面应用插件 |
## 实时更新:Tauri 事件推送
插件可以通过 Tauri 事件系统向前端推送 UI 更新,实现实时数据刷新。
### 事件发射器
```rust
use crate::plugin::{PluginUIEmitter, UIMessage, DataModelUpdate, DataEntry};
// 在 Tauri 命令或服务中使用
fn update_plugin_ui(emitter: &PluginUIEmitter, plugin_id: &str) {
// 发送数据更新
let update = DataModelUpdate {
surface_id: "my-surface".into(),
path: Some("/stats".into()),
contents: vec![
DataEntry::number("count", 42.0),
DataEntry::string("status", "healthy"),
],
};
emitter.emit_data_update(plugin_id, update).unwrap();
}
```
### 前端监听
前端通过 `usePluginUI` Hook 自动监听 `plugin-ui-message` 事件:
```typescript
// 自动处理,无需手动监听
const { surfaces, handleAction } = usePluginUI({ pluginId: 'my-plugin' });
```
### 事件载荷格式
```typescript
interface PluginUIEventPayload {
pluginId: string;
message: UIMessage; // SurfaceUpdate | DataModelUpdate | BeginRendering | DeleteSurface
}
```
## 示例插件:凭证监控
完整示例见 `src-tauri/src/plugin/examples/credential_monitor.rs`:
```rust
use crate::plugin::{PluginUI, SurfaceDefinition, ComponentDef, ChildrenDef, BoundValue};
struct CredentialMonitorPlugin { /* ... */ }
impl PluginUI for CredentialMonitorPlugin {
fn get_surfaces(&self) -> Vec<SurfaceDefinition> {
vec![SurfaceDefinition {
surface_id: "credential-monitor".into(),
root_id: "root".into(),
initial_components: vec![
ComponentDef::column("root", ChildrenDef::explicit(vec!["header", "list"])),
ComponentDef::text_literal("header", "凭证监控"),
ComponentDef::list("list", ChildrenDef::template("item", "/credentials")),
// ... 更多组件
],
initial_data: json!({ "credentials": [] }),
styles: None,
}]
}
async fn handle_action(&mut self, action: UserAction) -> Result<Vec<UIMessage>, PluginError> {
match action.name.as_str() {
"refresh" => {
// 返回数据更新消息
Ok(vec![UIMessage::DataModelUpdate(/* ... */)])
}
_ => Ok(vec![])
}
}
}
```
## 下一步计划
1. **更多组件**:Table、Tabs、Modal 等复杂组件
2. **表单验证**:支持 TextField 的验证规则
3. **主题系统**:更完善的样式定制能力
4. **插件市场**:支持从远程加载插件 UI 定义