新增: SFTP直连+网站预览+OSS区域嗅探+热键+BGM播放
This commit is contained in:
66
docs/04-功能迭代/GO-DESK-9.插件系统/README.md
Normal file
66
docs/04-功能迭代/GO-DESK-9.插件系统/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# GO-DESK-9: 插件系统
|
||||
|
||||
> 状态:Phase 0 已实施完成,待推进 Phase 1
|
||||
> 创建日期:2026-05-01
|
||||
> 前置文档:`../../02-架构设计/插件化架构方案.md`(初版调研)
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与动机
|
||||
|
||||
### 1.1 当前痛点
|
||||
|
||||
| 痛点 | 现状 | 影响 |
|
||||
|------|------|------|
|
||||
| **app.go God Object** | 825 行,47 个方法全在一个 struct 上 | 难以维护,新功能必须改核心文件 |
|
||||
| **App.vue 硬编码映射** | `getComponent()` 只有 2 个 key 的字面量对象 | 新 Tab 必须改源码 |
|
||||
| **FileEditorPanel if/else 链** | 10 层 v-if/v-else-if | 新增文件类型需改 5+ 处 |
|
||||
| **前后端配置断层** | 后端定义 3 个 Tab,前端硬编码只保留 file-system | 新 Tab 无法透传到前端 |
|
||||
| **无扩展机制** | 所有功能编译时固定 | 无法按需加载,安装包膨胀 |
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
建立**两层插件体系**(内置 + 外部市场),使 u-desk 从"单体应用"演进为**可扩展平台**。
|
||||
|
||||
## 二、文档结构
|
||||
|
||||
```
|
||||
GO-DESK-9.插件系统/
|
||||
├── README.md ← 本文件(总览)
|
||||
├── 设计文档/
|
||||
│ ├── 架构设计.md ← 系统形态、两层体系、设置面板原型
|
||||
│ ├── 接口定义.md ← Go 后端 + TS 前端完整接口
|
||||
│ ├── 数据模型.md ← plugin_state 表 DDL 与存储策略
|
||||
│ └── 复杂度与价值评估.md ← 投入产出分析 + 远期风险预警
|
||||
├── 任务规划/
|
||||
│ ├── 实施路线图.md ← Phase 0~5 全景时间线与范围
|
||||
│ └── Phase0-基础设施.md ← Phase 0 详细步骤与验证标准
|
||||
└── 决策记录/
|
||||
└── README.md ← 关键技术决策(adapter 模式等)
|
||||
```
|
||||
|
||||
## 三、快速导航
|
||||
|
||||
| 如果你想看 | 去哪里 |
|
||||
|-----------|--------|
|
||||
| 系统长什么样 | `设计文档/架构设计.md` |
|
||||
| UI 插槽怎么切 | `设计文档/架构设计.md` → 第四章 |
|
||||
| 接口怎么定义的 | `设计文档/接口定义.md` |
|
||||
| 数据库存什么 | `设计文档/数据模型.md` |
|
||||
| 复杂度值不值 | `设计文档/复杂度与价值评估.md` |
|
||||
| 分几步做、每步做什么 | `任务规划/实施路线图.md` |
|
||||
| Phase 0 具体怎么动手 | `任务规划/Phase0-基础设施.md` |
|
||||
| 为什么选这个方案不选那个 | `决策记录/README.md` |
|
||||
|
||||
## 四、里程碑概览
|
||||
|
||||
```
|
||||
Phase 0 ████████████ 基础设施骨架(当前目标)
|
||||
Phase 1 ████ Draw.io 验证插件
|
||||
Phase 2 █████████████ 预览系统重构
|
||||
Phase 3 █████████████ Tab 插件化 + 设置面板 + UI 插槽
|
||||
Phase 4 ██████ 外部插件支持
|
||||
Phase 5 ░░░░░░░░░░░░░ 插件市场(远景)
|
||||
```
|
||||
|
||||
每个 Phase 可独立交付验证。
|
||||
149
docs/04-功能迭代/GO-DESK-9.插件系统/任务规划/Phase0-基础设施.md
Normal file
149
docs/04-功能迭代/GO-DESK-9.插件系统/任务规划/Phase0-基础设施.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Phase 0:基础设施骨架
|
||||
|
||||
## 目标
|
||||
|
||||
建好管道,不改现有功能。验证编译通过 + API 可调用。
|
||||
|
||||
**不包含**:无 UI 变化、无真实插件注册、无设置面板。
|
||||
|
||||
---
|
||||
|
||||
## 文件清单与实施顺序
|
||||
|
||||
### Step 1:核心接口定义
|
||||
|
||||
**新建** `internal/plugin/plugin.go`
|
||||
|
||||
内容:PluginID、PluginSource、PluginMetadata、PluginCapability、Plugin 接口、TabProvider、FilePreviewHandler、PreviewInfo、TabDef、CoreServices 接口定义。
|
||||
|
||||
依赖:无
|
||||
|
||||
---
|
||||
|
||||
### Step 2:适配器(避免方法泄漏)
|
||||
|
||||
**新建** `internal/plugin/adapter.go`
|
||||
|
||||
内容:`adapter` 结构体实现 `CoreServices` 接口的 6 个方法。构造函数 `NewAdapter(app *App) CoreServices`。
|
||||
|
||||
关键设计:不在 App 上直接实现 CoreServices(会导致 6 个内部方法被 Wails v3 自动暴露为前端 API),而是通过独立 adapter 封装。
|
||||
|
||||
依赖:Step 1
|
||||
|
||||
---
|
||||
|
||||
### Step 3:Tab 注册表
|
||||
|
||||
**新建** `internal/plugin/tab_registry.go`
|
||||
|
||||
内容:
|
||||
- `TabRegistry` struct(mu + entries map)
|
||||
- Register(冲突检测)、GetByTabKey、GetAllDefinitions(按 Order 排序)、GetAllProviders、Count
|
||||
|
||||
依赖:Step 1
|
||||
|
||||
---
|
||||
|
||||
### Step 4:预览注册表
|
||||
|
||||
**新建** `internal/plugin/preview_registry.go`
|
||||
|
||||
内容:
|
||||
- `PreviewRegistry` struct(mu + handlers 切片,按 priority 降序)
|
||||
- Register(自动排序)、Resolve(遍历匹配第一个 canHandle)、GetAllHandlers、Count
|
||||
|
||||
依赖:Step 1
|
||||
|
||||
---
|
||||
|
||||
### Step 5:PluginManager
|
||||
|
||||
**新建** `internal/plugin/manager.go`
|
||||
|
||||
内容:
|
||||
- `Manager` struct(plugins map + core + tabReg + previewReg + ctx + initialized)
|
||||
- NewManager、Register(自动分发到子注册表 + 回滚)、InitAll、StartByTabKey
|
||||
- GetPluginInfos、ResolvePreview(附加 PluginID 到响应)、GetTabDefinitions、Shutdown
|
||||
|
||||
依赖:Step 1 ~ 4 全部
|
||||
|
||||
---
|
||||
|
||||
### Step 6:前端类型定义
|
||||
|
||||
**新建** `frontend/src/plugin/types.ts`
|
||||
|
||||
内容:PluginCapability enum、PluginMetadata、TabPluginDefinition、FilePreviewHandlerDefinition、RenderConfig 接口。
|
||||
|
||||
依赖:无
|
||||
|
||||
---
|
||||
|
||||
### Step 7:前端注册中心
|
||||
|
||||
**新建** `frontend/src/plugin/registry.ts`
|
||||
|
||||
内容:
|
||||
- Vue reactive state(tabPlugins Map + previewHandlers 数组)
|
||||
- Tab API:registerTabPlugin / getTabComponent / getAllTabDefinitions / hasTabPlugin
|
||||
- Preview API:registerPreviewHandler(按 priority 插入)/ resolvePreviewHandler / getAllPreviewHandlers
|
||||
- 调试工具:getRegistryStats
|
||||
|
||||
依赖:Step 6
|
||||
|
||||
---
|
||||
|
||||
### Step 8:集成到 App
|
||||
|
||||
**修改** `app.go`
|
||||
|
||||
改动点:
|
||||
|
||||
1. **新增字段**:`pluginMgr *plugin.Manager`(在 `mu` 字段之后)
|
||||
2. **ServiceStartup 中**(步骤 4 之后):初始化 pluginMgr
|
||||
```go
|
||||
fmt.Println("[启动] 初始化插件管理器...")
|
||||
a.pluginMgr = plugin.NewManager(plugin.NewAdapter(a))
|
||||
```
|
||||
3. **ServiceShutdown 末尾**:关闭 pluginMgr
|
||||
```go
|
||||
if a.pluginMgr != nil { a.pluginMgr.Shutdown() }
|
||||
```
|
||||
4. **新增绑定方法**(Wails v3 自动暴露前端):
|
||||
- `GetPluginInfos() ([]map[string]interface{}, error)` — 返回空数组或插件列表
|
||||
- `ResolvePreview(req ResolvePreviewRequest) (map[string]interface{}, error)` — 解析文件预览处理器
|
||||
- `ResolvePreviewRequest{Filename string}` 请求结构体
|
||||
|
||||
依赖:Step 5
|
||||
|
||||
---
|
||||
|
||||
## 验证方案
|
||||
|
||||
### 编译验证
|
||||
|
||||
```bash
|
||||
go build ./internal/plugin/
|
||||
go build .
|
||||
cd frontend && npx vue-tsc --noEmit
|
||||
```
|
||||
|
||||
### 运行时验证
|
||||
|
||||
| # | 操作 | 预期结果 |
|
||||
|---|------|---------|
|
||||
| V1 | 运行应用 | 日志出现 `[启动] 初始化插件管理器...` |
|
||||
| V2 | 关闭应用 | 日志出现 `[插件管理器] 已关闭` |
|
||||
| V3 | DevTools: `GetPluginInfos()` | 返回 `{success:true, data:[]}` |
|
||||
| V4 | DevTools: `ResolvePreview({filename:'test'})` | 返回 `{success:false, message:"no preview handler..."}` |
|
||||
| V5 | 前端 import registry | 无 TS 错误 |
|
||||
|
||||
### 边界情况
|
||||
|
||||
| 场景 | 预期行为 |
|
||||
|------|---------|
|
||||
| pluginMgr 为 nil 调用 GetPluginInfos | 返回空数组,不 panic |
|
||||
| pluginMgr 为 nil 调用 ResolvePreview | 返回 success:false,不 panic |
|
||||
| TabRegistry.Register 同一 TabKey 两次 | 返回冲突错误 |
|
||||
| PreviewRegistry.Resolve 无匹配文件 | 返回 nil, "" |
|
||||
| Manager.Shutdown 无插件注册 | 正常返回 nil |
|
||||
167
docs/04-功能迭代/GO-DESK-9.插件系统/任务规划/实施路线图.md
Normal file
167
docs/04-功能迭代/GO-DESK-9.插件系统/任务规划/实施路线图.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# 实施路线图
|
||||
|
||||
## 总览
|
||||
|
||||
```
|
||||
Phase 0 ████████████ 基础设施骨架
|
||||
Phase 1 ████ Draw.io 验证插件
|
||||
Phase 2 █████████████ 预览系统重构
|
||||
Phase 3 █████████████ Tab 插件化 + 设置面板
|
||||
Phase 4 ██████ 外部插件支持
|
||||
Phase 5 ░░░░░░░░░░░░░ 插件市场(远景)
|
||||
```
|
||||
|
||||
每个 Phase 可独立交付验证。
|
||||
|
||||
---
|
||||
|
||||
## Phase 0:基础设施骨架
|
||||
|
||||
**目标**:建好管道,不改现有功能。验证编译通过 + API 可调用。
|
||||
|
||||
详细步骤见 [Phase0-基础设施.md](./Phase0-基础设施.md)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:首个内置插件验证(Draw.io)
|
||||
|
||||
**目标**:用第一个真实插件验证整条链路端到端打通。
|
||||
|
||||
| 步骤 | 文件 | 操作 |
|
||||
|------|------|------|
|
||||
| 1 | `internal/plugin/builtin/drawio_plugin.go` | 新建 DrawIoPlugin 实现 |
|
||||
| 2 | `frontend/src/plugin/built-in/drawio-handler.ts` | 新建前端 handler 注册 |
|
||||
| 3 | `app.go` | 在 ServiceStartup 中 Register(DrawIoPlugin) |
|
||||
| 4 | `FileEditorPanel.vue` | 在 v-if 链末尾追加 drawio 分支 |
|
||||
|
||||
**验证标准**:打开 `.drawio` 文件 → 显示 iframe 预览 → 其他文件不受影响。
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:文件预览系统重构
|
||||
|
||||
**目标**:将全部 10 种内置预览迁移到插件注册表,消除 v-if 链。
|
||||
|
||||
| 步骤 | 文件 | 操作 |
|
||||
|------|------|------|
|
||||
| 1 | `frontend/src/plugin/built-in/preview-handlers.ts` | 新建,注册 image/video/audio/pdf/html/md/excel/word/csv/text/code 共 12 个处理器 |
|
||||
| 2 | `FileEditorPanel.vue` | 模板重写为 `<component :is>` + `<iframe>` 双分支 |
|
||||
| 3 | `registry.ts` | 被 FileEditorPanel 实际导入使用 |
|
||||
|
||||
**收益**:新增文件类型只需写一个 TS 文件(~20 行),零改动核心组件。
|
||||
|
||||
---
|
||||
|
||||
## Phase 3:Tab 系统插件化 + 设置面板 + UI 插槽
|
||||
|
||||
**目标**:App.vue 不再硬编码,设置面板支持插件管理,建立 UI 插槽体系。
|
||||
|
||||
| 步骤 | 文件 | 操作 |
|
||||
|------|------|------|
|
||||
| 1 | `frontend/src/plugin/built-in/tabs.ts` | 注册 file-system / markdown-editor 等内置 Tab |
|
||||
| 2 | `frontend/src/plugin/built-in/index.ts` | 统一副作用入口 |
|
||||
| 3 | `frontend/src/plugin/slots.ts` | UISlotRegistry 实现(插槽注册/查询) |
|
||||
| 4 | `App.vue` | getComponent 改为查 registry;KeepAlive 动态化;接入 slot: titlebar-extra / sidebar-left / toolbar-extra 等 |
|
||||
| 5 | `stores/config.ts` | loadConfig 合并插件 Tab(修复前后端断层) |
|
||||
| 6 | `SettingsPanel.vue` | 新增「插件管理」Tab 页(列表 + 启用/禁用) |
|
||||
| 7 | `app.go` | 新增 SetPluginEnabled 绑定方法;PluginMetadata 新增 UISlots 字段 |
|
||||
| 8 | `service/config_service.go` | defaultTabConfig 改为动态合并插件 Tab |
|
||||
| 9 | SQLite | 写入 plugin_state 初始数据 |
|
||||
|
||||
> UI 插槽详细设计见 `设计文档/架构设计.md` 第四章
|
||||
|
||||
---
|
||||
|
||||
## Phase 4:外部插件支持
|
||||
|
||||
**目标**:用户可安装 .zip 格式的外部插件包。
|
||||
|
||||
### 插件包格式 (.uplugin)
|
||||
|
||||
```
|
||||
my-plugin-v1.0.0.uplugin (ZIP)
|
||||
├── manifest.json # 插件清单(必须)
|
||||
├── plugin.wasm # WASM 入口(跨平台首选)
|
||||
├── assets/ # 静态资源
|
||||
└── frontend/ # 前端组件(可选)
|
||||
```
|
||||
|
||||
### manifest.json 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "com.example.my-plugin",
|
||||
"name": "My Plugin",
|
||||
"version": "1.0.0",
|
||||
"entry": "plugin.wasm",
|
||||
"capabilities": ["file-preview"],
|
||||
"file_extensions": [".xyz"],
|
||||
"min_app_version": "0.4.0",
|
||||
"permissions": ["filesystem:read"],
|
||||
"checksum_sha256": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### Manager 新增能力
|
||||
|
||||
```go
|
||||
InstallPlugin(packagePath string) error // 解压 + 校验 + 注册
|
||||
UninstallPlugin(id PluginID) error // 停止 + 删除 + 清理
|
||||
SetPluginEnabled(id, enabled) error // 启用/停用
|
||||
ScanInstalledPlugins() error // 扫描恢复
|
||||
```
|
||||
|
||||
### 安全模型
|
||||
|
||||
| 层级 | 措施 | Phase |
|
||||
|------|------|-------|
|
||||
| 签名校验 | checksum_sha256 安装时验证 | Phase 4 |
|
||||
| 权限声明 | permissions 列表安装时展示确认 | Phase 4 |
|
||||
| 沙箱执行 | WASM 沙箱 / 子进程隔离 | Phase 5 |
|
||||
| 版本兼容 | min_app_version 检查 | Phase 4 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 5:插件市场(远景)
|
||||
|
||||
### 整体架构
|
||||
|
||||
```
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ 插件市场服务端 │ │ u-desk 客户端 │
|
||||
│ │◄───────►│ │
|
||||
│ · 插件仓库 CRUD │ HTTPS │ · 浏览/搜索 │
|
||||
│ · 元数据 API │ │ · 一键安装 │
|
||||
│ · 包文件分发 │ │ · 自动更新检查 │
|
||||
│ · 签名 & 信誉 │ │ · 评分/评论(预留) │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
### 服务端职责(优先级排序)
|
||||
|
||||
| P0 | P1 | P2 | P3 |
|
||||
|----|----|----|----|
|
||||
| 插件仓库 CRUD | 搜索过滤 | 开发者门户 | 评分评论 |
|
||||
| 包分发 CDN | 自动更新通知 | 审核流程 | |
|
||||
| 版本管理 | | | |
|
||||
| 签名验证 | | | |
|
||||
|
||||
### 客户端 MarketplaceClient API
|
||||
|
||||
```typescript
|
||||
interface MarketplaceClient {
|
||||
searchPlugins(query, category?): Promise<MarketplacePlugin[]>
|
||||
getPluginDetail(id): Promise<MarketplacePluginDetail>
|
||||
installPlugin(id): Promise<InstallProgress>
|
||||
uninstallPlugin(id): Promise<void>
|
||||
checkUpdates(): Promise<PluginUpdateInfo[]>
|
||||
updatePlugin(id): Promise<InstallProgress>
|
||||
}
|
||||
```
|
||||
|
||||
### 更新机制(复用现有 UpdateAPI)
|
||||
|
||||
```
|
||||
应用更新(已有):CheckUpdate → DownloadUpdate → VerifyUpdateFile → InstallUpdateWithHash
|
||||
插件更新(新增):CheckPluginUpdates → DownloadPlugin → VerifyPlugin → InstallPlugin
|
||||
```
|
||||
54
docs/04-功能迭代/GO-DESK-9.插件系统/决策记录/README.md
Normal file
54
docs/04-功能迭代/GO-DESK-9.插件系统/决策记录/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 决策记录
|
||||
|
||||
## ADR-001:CoreServices 使用 adapter 模式而非 App 直接实现
|
||||
|
||||
**日期**:2026-05-01
|
||||
**状态**:已采纳
|
||||
**背景**:插件需要访问 App 的内部服务(ctx、mainWindow、filesystem 等),需要定义 CoreServices 接口。
|
||||
|
||||
**选项**:
|
||||
|
||||
| 方案 | 做法 | 优点 | 缺点 |
|
||||
|------|------|------|------|
|
||||
| A. App 直接实现 | App struct 添加 6 个公开方法 | 简单,无需额外类型 | Wails v3 将这 6 个方法全部暴露给前端 API(信息泄漏) |
|
||||
| B. adapter 模式 | 新建独立 adapter 结构体实现接口 | 零新增公开方法;松耦合 | 多一个文件 (~40 行) |
|
||||
|
||||
**决策**:选 **B. adapter 模式**
|
||||
|
||||
**理由**:
|
||||
1. Wails v3 将 `application.NewService(app)` 的**所有导出方法**自动暴露给前端
|
||||
2. `Context()`、`MainWindow()`、`FileSystem()`、`ConfigAPI()` 是内部实现细节,不应成为前端 API
|
||||
3. adapter 仅 40 行代码,代价极小
|
||||
|
||||
**后果**:
|
||||
- 新增 `internal/plugin/adapter.go` 文件
|
||||
- App 零新增公开方法
|
||||
- Manager 通过 `NewAdapter(a)` 获取 CoreServices
|
||||
|
||||
---
|
||||
|
||||
## ADR-002:plugin_state 使用独立表而非复用 app_config KV
|
||||
|
||||
**日期**:2026-05-01
|
||||
**状态**:已采纳
|
||||
**背景**:插件的启用状态、版本、安装路径等数据需要持久化存储。
|
||||
|
||||
**选项**:
|
||||
|
||||
| 方案 | 做法 | 适用阶段 |
|
||||
|------|------|---------|
|
||||
| A. 复用 app_config 表 | key 前缀 `plugin_enabled:xxx` / `plugin_config:xxx` | 插件数 < 20 时够用 |
|
||||
| B. 独立 plugin_state 表 | 专用表,结构化字段 | 插件市场场景必需 |
|
||||
|
||||
**决策**:选 **B. 独立表**(Phase 0 就建好 schema)
|
||||
|
||||
**理由**:
|
||||
1. 插件市场远景需要存储 source/install_path/version/installed_at 等结构化字段,KV 模式查询和索引能力不足
|
||||
2. Phase 0 建表成本极低(只需在 AutoMigrate 加一个 model)
|
||||
3. 后续从 KV 迁移到独立表的数据迁移成本高且易出错
|
||||
4. 与 app_config 职责分离清晰:一个管全局配置,一个管插件实例状态
|
||||
|
||||
**后果**:
|
||||
- 新增 `PluginState` GORM model
|
||||
- `sqlite.go` AutoMigrate 增加一项
|
||||
- Phase 3 才开始写入数据,Phase 0 只建表
|
||||
81
docs/04-功能迭代/GO-DESK-9.插件系统/设计文档/复杂度与价值评估.md
Normal file
81
docs/04-功能迭代/GO-DESK-9.插件系统/设计文档/复杂度与价值评估.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 复杂度与价值评估
|
||||
|
||||
> 评估时间:Phase 0 实施完成后(2026-05-03)
|
||||
> 评估范围:已实施的 Phase 0 + 远景 Phase 1~5
|
||||
|
||||
---
|
||||
|
||||
## 一、Phase 0 已投入的复杂度
|
||||
|
||||
### 1.1 新增代码量
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `internal/plugin/plugin.go` | ~150 | 核心接口与类型定义 |
|
||||
| `internal/plugin/adapter.go` | ~48 | CoreServices 适配器 |
|
||||
| `internal/plugin/tab_registry.go` | ~98 | Tab 注册表(线程安全) |
|
||||
| `internal/plugin/preview_registry.go` | ~80 | 预览注册表(线程安全) |
|
||||
| `internal/plugin/manager.go` | ~196 | 插件管理器(生命周期) |
|
||||
| `frontend/src/plugin/types.ts` | ~70 | TS 类型定义 |
|
||||
| `frontend/src/plugin/registry.ts` | ~112 | 前端注册中心(Vue reactive) |
|
||||
| `app.go` 改动 | ~40 | 集成 PluginManager + 2 个绑定方法 |
|
||||
| **合计** | **~794** | |
|
||||
|
||||
### 1.2 架构复杂度增量
|
||||
|
||||
| 维度 | 具体表现 | 严重程度 | 已解决方式 |
|
||||
|------|---------|----------|-----------|
|
||||
| **Wails v3 方法泄露** | App struct 上所有导出方法自动暴露给前端 | 高 | adapter 模式隔离,CoreServices 独立实现(ADR-001) |
|
||||
| **Vue 3 响应式陷阱** | `Map`/`Set` 在 `reactive()` 内不触发更新 | 中 | 使用 `Record<string, T>` 替代 Map(已踩坑记录) |
|
||||
| **并发安全** | Manager 多处读写共享状态 | 中 | 全量 RWMutex 保护 + copy-then-iterate 模式 |
|
||||
| **两阶段回滚** | Register 时 Tab 成功但 Preview 失败需清理 | 低 | `tabRegistered` 标志位 + `Unregister()` 回滚 |
|
||||
| **前后端类型桥接** | Go struct → `map[string]interface{}` → TS | 低 | json.Marshal round-trip 统一处理 |
|
||||
|
||||
### 1.3 运行时开销
|
||||
|
||||
| 开销项 | 数值 | 说明 |
|
||||
|--------|------|------|
|
||||
| 启动时内存 | ~0(仅创建 Manager 和空 Registry) | Phase 0 不注册任何插件 |
|
||||
| 锁竞争 | 无(Phase 0 无并发注册场景) | 为后续并发预留 |
|
||||
| 方法调用链路 | App → pluginMgr → Registry | 多一层间接调用,可忽略 |
|
||||
|
||||
## 二、Phase 0 获得的价值
|
||||
|
||||
### 2.1 直接收益
|
||||
|
||||
| 收益点 | 说明 | 对应痛点 |
|
||||
|--------|------|---------|
|
||||
| **扩展骨架就绪** | 注册表、管理器、适配器全部可用 | app.go God Object |
|
||||
| **新功能零侵入** | 未来新增预览格式 / Tab 页无需改 App.vue | 硬编码映射、v-if 链 |
|
||||
| **懒启动支持** | Tab 插件按 `Start()` 按需激活 | 安装包膨胀、启动慢 |
|
||||
| **优先级调度** | 预览处理器按 priority 排序匹配 | 无法控制预览顺序 |
|
||||
| **内置/外置统一接口** | 同一套 Plugin 接口服务两类插件 | 两套逻辑 |
|
||||
| **失败自动清理** | Register 部分失败时回滚已注册资源 | 残留脏状态 |
|
||||
|
||||
### 2.2 战略价值
|
||||
|
||||
| 价值维度 | 说明 |
|
||||
|----------|------|
|
||||
| **可演进性** | 从单体应用平滑过渡到平台型应用,每步可独立验证 |
|
||||
| **文档资产** | 完整设计文档 + ADR + 接口定义 + 数据模型,新人可快速接手 |
|
||||
| **技术债务清零** | 解决了 app.go 35+ 方法的 God Object 问题根源(不再往 App 加方法) |
|
||||
| **市场基础** | Phase 0 的接口体系直接支撑 Phase 4~5 的外部插件和插件市场 |
|
||||
|
||||
## 三、远期复杂度预警(Phase 3~5)
|
||||
|
||||
| Phase | 主要复杂度来源 | 风险等级 | 缓解策略 |
|
||||
|-------|---------------|----------|---------|
|
||||
| **Phase 3** UI Slot 动态注入 | 9 个插槽的组件动态加载、生命周期协调、布局冲突 | 中 | 先做 2~3 个核心插槽验证,不全量铺开 |
|
||||
| **Phase 4** 外部插件加载 | `.uplugin` 包解析、沙箱隔离、版本兼容、签名校验 | 高 | 参考 VS Code Extension Host 架构,独立进程运行 |
|
||||
| **Phase 5** 插件市场 | 服务端 API、审核流程、支付、权限管理 | 极高 | 作为独立项目推进,不影响桌面端主迭代 |
|
||||
|
||||
## 四、总体评价
|
||||
|
||||
```
|
||||
投入: ~800 行代码 + 6 个新文件 + 1 个文件改动
|
||||
回报: 扩展能力从 0 → 完整骨架,后续每步增量开发
|
||||
风险: Phase 0 本身风险已全部识别并解决
|
||||
ROI: ★★★★☆ — 当前阶段性价比极高
|
||||
```
|
||||
|
||||
**结论**:Phase 0 是一次高 ROI 的基础设施投资。真正的复杂度在 Phase 4(外部插件),但那是独立里程碑,可以单独做 go/no-go 决策。当前阶段建议继续推进 Phase 1(Draw.io 验证插件),用真实插件验证骨架的完整性。
|
||||
227
docs/04-功能迭代/GO-DESK-9.插件系统/设计文档/接口定义.md
Normal file
227
docs/04-功能迭代/GO-DESK-9.插件系统/设计文档/接口定义.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# 接口定义
|
||||
|
||||
## 一、后端接口(Go)
|
||||
|
||||
> 文件位置:`internal/plugin/plugin.go`
|
||||
|
||||
```go
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// ========== 类型定义 ==========
|
||||
|
||||
type PluginID string
|
||||
|
||||
type PluginSource string
|
||||
|
||||
const (
|
||||
SourceBuiltin PluginSource = "builtin"
|
||||
SourceMarket PluginSource = "market"
|
||||
)
|
||||
|
||||
// PluginMetadata 插件元数据(JSON 序列化传给前端)
|
||||
type PluginMetadata struct {
|
||||
ID PluginID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
Source PluginSource `json:"source"`
|
||||
TabKey string `json:"tab_key,omitempty"`
|
||||
FileExtensions []string `json:"file_extensions,omitempty"`
|
||||
InstallPath string `json:"install_path,omitempty"`
|
||||
InstalledAt time.Time `json:"installed_at,omitempty"`
|
||||
UISlots []string `json:"ui_slots,omitempty"` // 声明占用的 UI 插槽(Phase 3)
|
||||
}
|
||||
|
||||
// PluginCapability 插件能力标志位
|
||||
type PluginCapability int
|
||||
|
||||
const (
|
||||
CapabilityNone PluginCapability = 0
|
||||
CapabilityTabProvider PluginCapability = 1 << iota
|
||||
CapabilityFilePreview
|
||||
CapabilitySettings
|
||||
)
|
||||
|
||||
func (c PluginCapability) Has(cap PluginCapability) bool {
|
||||
return c&cap != 0
|
||||
}
|
||||
|
||||
// PreviewInfo 预览元信息
|
||||
type PreviewInfo struct {
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
NeedsContainer bool `json:"needs_container,omitempty"`
|
||||
ContainerConfig map[string]any `json:"container_config,omitempty"`
|
||||
SupportsEdit bool `json:"supports_edit"`
|
||||
PreloadHint string `json:"preload_hint,omitempty"`
|
||||
}
|
||||
|
||||
// TabDef Tab 定义
|
||||
type TabDef struct {
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
// ========== 核心接口 ==========
|
||||
|
||||
// CoreServices 插件可访问的核心服务(由 adapter 实现,不挂在 App 上)
|
||||
type CoreServices interface {
|
||||
Context() context.Context
|
||||
MainWindow() *application.WebviewWindow
|
||||
EmitEvent(eventName string, data ...any)
|
||||
FileSystem() any
|
||||
ConfigAPI() any
|
||||
GetFileServerURL() string
|
||||
}
|
||||
|
||||
// Plugin 核心插件接口(所有插件必须实现)
|
||||
type Plugin interface {
|
||||
Meta() PluginMetadata
|
||||
Capabilities() PluginCapability
|
||||
Init(ctx context.Context, core CoreServices) error
|
||||
Start() error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// TabProvider Tab 提供者接口(可选,实现 CapabilityTabProvider 时需同时实现)
|
||||
type TabProvider interface {
|
||||
Plugin
|
||||
TabDefinition() TabDef
|
||||
TabComponentPath() string
|
||||
}
|
||||
|
||||
// FilePreviewHandler 文件预览处理器接口(可选,实现 CapabilityFilePreview 时需同时实现)
|
||||
type FilePreviewHandler interface {
|
||||
Plugin
|
||||
CanPreview(filename string, mimeType string) bool
|
||||
PreviewInfo(filename string) PreviewInfo
|
||||
}
|
||||
```
|
||||
|
||||
## 二、PluginManager API
|
||||
|
||||
> 文件位置:`internal/plugin/manager.go`
|
||||
|
||||
```go
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
plugins map[PluginID]Plugin
|
||||
core CoreServices
|
||||
tabReg *TabRegistry
|
||||
previewReg *PreviewRegistry
|
||||
ctx context.Context
|
||||
initialized bool
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
func NewManager(core CoreServices) *Manager
|
||||
func (m *Manager) Register(p Plugin) error // 注册 + 自动分发到子注册表
|
||||
func (m *Manager) InitAll(ctx context.Context) error // 初始化所有插件
|
||||
func (m *Manager) StartByTabKey(tabKey string) error // 按 Tab 懒启动
|
||||
func (m *Manager) Shutdown() error // 停止所有插件
|
||||
|
||||
// 查询
|
||||
func (m *Manager) GetPluginInfos() []PluginMetadata // 前端展示用
|
||||
func (m *Manager) ResolvePreview(filename string) (*PreviewInfo, PluginID)
|
||||
func (m *Manager) GetTabDefinitions() []TabDef
|
||||
|
||||
// 外部插件管理(Phase 4+)
|
||||
func (m *Manager) InstallPlugin(packagePath string) error
|
||||
func (m *Manager) UninstallPlugin(id PluginID) error
|
||||
func (m *Manager) SetPluginEnabled(id PluginID, enabled bool) error
|
||||
func (m *Manager) CheckPluginUpdates() []PluginUpdateInfo
|
||||
```
|
||||
|
||||
## 三、前端接口(TypeScript)
|
||||
|
||||
> 文件位置:`frontend/src/plugin/types.ts`
|
||||
|
||||
```typescript
|
||||
/** 插件能力标志位 */
|
||||
export enum PluginCapability {
|
||||
None = 0,
|
||||
TabProvider = 1 << 0,
|
||||
FilePreview = 1 << 1,
|
||||
Settings = 1 << 2,
|
||||
}
|
||||
|
||||
export type PluginSource = 'builtin' | 'market'
|
||||
|
||||
/** 后端返回的插件元信息 */
|
||||
export interface PluginMetadata {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
description: string
|
||||
author: string
|
||||
source: PluginSource
|
||||
tab_key?: string
|
||||
file_extensions?: string[]
|
||||
install_path?: string
|
||||
installed_at?: string
|
||||
ui_slots?: string[]
|
||||
}
|
||||
|
||||
/** Tab 插件定义(前端注册用) */
|
||||
export interface TabPluginDefinition {
|
||||
key: string
|
||||
title: string
|
||||
icon?: string
|
||||
componentLoader: () => Promise<Component>
|
||||
defaultVisible?: boolean
|
||||
order?: number
|
||||
keepAlive?: boolean
|
||||
}
|
||||
|
||||
/** 文件预览处理器定义 */
|
||||
export interface FilePreviewHandlerDefinition {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
priority: number
|
||||
canHandle: (filename: string) => boolean
|
||||
getComponent?: () => Promise<Component>
|
||||
getRenderConfig?: (filePath: string) => RenderConfig
|
||||
supportsEdit?: boolean
|
||||
}
|
||||
|
||||
/** 渲染配置 */
|
||||
export interface RenderConfig {
|
||||
type: 'iframe' | 'html' | 'custom' | 'image' | 'video' | 'audio'
|
||||
src?: string
|
||||
htmlContent?: string
|
||||
props?: Record<string, unknown>
|
||||
}
|
||||
```
|
||||
|
||||
## 四、前端 Registry API
|
||||
|
||||
> 文件位置:`frontend/src/plugin/registry.ts`
|
||||
|
||||
```typescript
|
||||
// === Tab 插件 ===
|
||||
function registerTabPlugin(def: TabPluginDefinition): void
|
||||
function getTabComponent(key: string): (() => Promise<Component>) | null
|
||||
function getAllTabDefinitions(): TabPluginDefinition[]
|
||||
function hasTabPlugin(key: string): boolean
|
||||
|
||||
// === 文件预览 ===
|
||||
function registerPreviewHandler(handler: FilePreviewHandlerDefinition): void
|
||||
function resolvePreviewHandler(filename: string): FilePreviewHandlerDefinition | null
|
||||
function getAllPreviewHandlers(): ReadonlyArray<FilePreviewHandlerDefinition>
|
||||
|
||||
// === 调试 ===
|
||||
function getRegistryStats(): { tabCount, previewHandlerCount, tabKeys, handlerIds }
|
||||
```
|
||||
73
docs/04-功能迭代/GO-DESK-9.插件系统/设计文档/数据模型.md
Normal file
73
docs/04-功能迭代/GO-DESK-9.插件系统/设计文档/数据模型.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# 数据模型
|
||||
|
||||
## 一、plugin_state 表(新增)
|
||||
|
||||
```sql
|
||||
CREATE TABLE plugin_state (
|
||||
plugin_id TEXT PRIMARY KEY,
|
||||
source TEXT NOT NULL DEFAULT 'builtin',
|
||||
enabled INTEGER DEFAULT 1,
|
||||
config TEXT,
|
||||
install_path TEXT,
|
||||
version TEXT,
|
||||
installed_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_plugin_source ON plugin_state(source);
|
||||
CREATE INDEX idx_plugin_enabled ON plugin_state(enabled);
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `plugin_id` | TEXT PK | 对应 PluginMetadata.ID,如 `"builtin-drawio"` |
|
||||
| `source` | TEXT | `'builtin'` 或 `'market'` |
|
||||
| `enabled` | INTEGER | 1=启用 0=停用 |
|
||||
| `config` | TEXT | JSON 格式的插件私有配置 |
|
||||
| `install_path` | TEXT | 外部插件磁盘路径(内置为 NULL) |
|
||||
| `version` | TEXT | 当前安装的版本号 |
|
||||
| `installed_at` | DATETIME | 安装时间 |
|
||||
| `updated_at` | DATETIME | 最后状态变更时间 |
|
||||
|
||||
## 二、与现有表的关系
|
||||
|
||||
```
|
||||
app_config 表(已有) plugin_state 表(新增)
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ key='tab_config'│ │ plugin_id PK │
|
||||
│ value=JSON │ ← 合并Tab →│ source │
|
||||
│ (全局配置) │ │ enabled │
|
||||
├──────────────┤ │ version │
|
||||
│ key='plugin_ │ ← 全局设置→│ config │
|
||||
│ global' │ │ install_path │
|
||||
│ (Phase 3 新增)│ └──────────────┘
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
- `app_config`:存全局应用配置(tab_config、plugin_global 等)
|
||||
- `plugin_state`:存每个插件的运行状态和独立配置
|
||||
- 两者通过 `plugin_id` 关联
|
||||
|
||||
## 三、初始种子数据
|
||||
|
||||
应用首次启动时由 Go 代码写入:
|
||||
|
||||
```sql
|
||||
INSERT OR IGNORE INTO plugin_state (plugin_id, source, enabled, version) VALUES
|
||||
('builtin-file-system', 'builtin', 1, '0.4.0'),
|
||||
('builtin-markdown', 'builtin', 1, '0.4.0'),
|
||||
('builtin-drawio', 'builtin', 0, '1.0.0');
|
||||
```
|
||||
|
||||
## 四、配置存储策略
|
||||
|
||||
| 存储位置 | 内容 | 写入时机 |
|
||||
|---------|------|---------|
|
||||
| `app_config['tab_config']` | Tab 可见性/排序/默认值 | Phase 3 动态合并 |
|
||||
| `app_config['plugin_global']` | 插件全局设置(自动更新间隔等) | Phase 3 |
|
||||
| `plugin_state.enabled` | 每个插件的启停状态 | Phase 3 SetPluginEnabled |
|
||||
| `plugin_state.version` | 当前安装版本 | 安装/更新时 |
|
||||
| `plugin_state.config` | 插件私有配置(如 Draw.io 端口号) | 用户修改插件设置时 |
|
||||
250
docs/04-功能迭代/GO-DESK-9.插件系统/设计文档/架构设计.md
Normal file
250
docs/04-功能迭代/GO-DESK-9.插件系统/设计文档/架构设计.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# 架构设计
|
||||
|
||||
## 一、架构全景
|
||||
|
||||
```
|
||||
+==================================================================+
|
||||
| u-desk Application |
|
||||
+===================================================================+
|
||||
| Core (Go) Plugin Manager |
|
||||
| - App facade - Registry / Lifecycle / Loader |
|
||||
| - ConfigStore (SQLite) - Builtin Plugins (编译时) |
|
||||
| - EventBus (Wails Events) - External Plugins (运行时) |
|
||||
| - UpdateEngine - Marketplace Client |
|
||||
+----------+----------+---------+-----------+-----------+ |
|
||||
| | | | | |
|
||||
+-------+ +------+ +-----+ +--------+ +------+ +----+ |
|
||||
|builtin: |builtin: |builtin | builtin: |extern:|extern |
|
||||
| file-sys| md-edit| drawio | db-cli | user-A| user-B |
|
||||
+---------+---------+--------+-----------+--------+--------------+
|
||||
|
|
||||
+=================================================================+|
|
||||
| Frontend (Vue 3) ||
|
||||
| PluginRegistry (TS) ||
|
||||
| - TabProviders → 替代 App.vue 硬编码映射 ||
|
||||
| - FilePreviewHandlers → 替代 FileEditorPanel if/else 链 ||
|
||||
| - ComponentLoader → defineAsyncComponent 懒加载 ||
|
||||
| - PluginSettingsUI → 设置面板插件管理区块 ||
|
||||
+=================================================================+
|
||||
```
|
||||
|
||||
## 二、两层插件体系
|
||||
|
||||
| 维度 | 内置插件 (Builtin) | 外部插件 (External/Market) |
|
||||
|------|-------------------|--------------------------|
|
||||
| **来源** | 编译时打包进二进制 | 运行时从市场安装 |
|
||||
| **位置** | `internal/plugin/builtin/` | 用户数据目录 `plugins/` |
|
||||
| **生命周期** | 随应用启动 | 用户控制 |
|
||||
| **管理操作** | 启用 / 禁用 | 安装 / 卸载 / 启用 / 禁用 / 更新 |
|
||||
| **更新方式** | 随应用版本更新 | 独立更新(复用 UpdateAPI) |
|
||||
| **安全级别** | 完全信任(同进程) | 沙箱隔离(Phase 5) |
|
||||
| **示例** | 文件系统、Markdown、Draw.io | 第三方预览器、云存储同步 |
|
||||
|
||||
## 三、设置面板形态(最终态)
|
||||
|
||||
```
|
||||
┌─ 设置 ──────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ┌─ Tab 配置 ─┐ ┌─ 版本更新 ─┐ ┌─ 插件管理 ─┐ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ 拖拽排序 │ │ 自动检查 │ │ 内置插件 │ │
|
||||
│ │ 显隐控制 │ │ 手动检查 │ │ ✓ Draw.io │ │
|
||||
│ │ 默认选择 │ │ 下载/安装 │ │ ✓ Markdown │ │
|
||||
│ │ │ │ │ │ ○ 数据库CLI │ │
|
||||
│ └─────────────┘ └─────────────┘ │ │ │
|
||||
│ │ 外部插件 │ │
|
||||
│ │ ✓ 云盘同步 v2.1│ │
|
||||
│ │ ○ 思维导图(停用)│ │
|
||||
│ │ │ │
|
||||
│ │ [浏览插件市场] │ │
|
||||
│ └──────────────┘ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 四、UI 插槽体系(Slot System)
|
||||
|
||||
### 4.1 当前布局(硬编码)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ 标题栏 + Tab 导航 + 窗口控制 │ ← 固定区域,不可扩展
|
||||
├─────────────────────────────────┤
|
||||
│ │
|
||||
│ <component :is="activeTab"> │ ← 内容区,getComponent() 硬编码
|
||||
│ │
|
||||
├─────────────────────────────────┤
|
||||
│ 更新通知 / 设置抽屉 │ ← 固定区域
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
**问题**:所有 UI 区域都是硬编码的。新插件无法在标题栏加按钮、无法在工具栏扩展、无法在侧边栏注入面板。
|
||||
|
||||
### 4.2 插件化后的插槽布局
|
||||
|
||||
```
|
||||
┌── slot: titlebar-extra ──────────────────────────────┐
|
||||
│ [u-desk] [文件管理 | Markdown] [🔌插件A] [🔌插件B] [—][□][×] │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ slot: sidebar-left │ slot: main-content │ slot: sidebar-right │
|
||||
│ │ │ │
|
||||
│ [内置侧边栏] │ <component :is="activeTab"> │ (预留) │
|
||||
│ ───────────── │ │ │
|
||||
│ [插件侧边面板] │ │ │
|
||||
│ │ │ │
|
||||
├────────────────────┴───────────────────────────────────┴──────────────────────┤
|
||||
│ slot: toolbar-extra: [📋] [🔍] [🔌插件工具按钮...] │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ slot: status-bar: [就绪] [行: 128] [编码: UTF-8] [🔌插件状态项...] │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
右键菜单 → slot: context-menu-extra(追加菜单项)
|
||||
设置面板 → slot: settings-panel(内嵌区块)
|
||||
```
|
||||
|
||||
### 4.3 插槽定义表
|
||||
|
||||
| 插槽 ID | 位置 | 控制方式 | 谁可注册 | Phase |
|
||||
|---------|------|---------|---------|-------|
|
||||
| `titlebar-extra` | 标题栏右侧(窗口控制按钮左侧) | 声明式:PluginMetadata.uiSlots[] | 内置 + 插件 | 3 |
|
||||
| `tab-bar` | Tab 导航条内部 | 自动:CapabilityTabProvider 插件自动合并 | PluginManager 驱动 | 1 |
|
||||
| `main-content` | 中央主内容区 | 自动:getComponent() 查 registry | registry 驱动 | 1 |
|
||||
| `sidebar-left` | 左侧边栏底部(FileSystem Sidebar 之后) | 声明式 | 插件 | 3 |
|
||||
| `sidebar-right` | 右侧边栏(预览区旁) | 声明式 | 插件 | 3 |
|
||||
| `toolbar-extra` | 工具栏末尾追加按钮 | 声明式 | 插件 | 2 |
|
||||
| `status-bar` | 底部状态栏追加信息 | 声明式 | 插件 | 3 |
|
||||
| `context-menu-extra` | 右键菜单追加项 | 声明式 | 插件 | 2 |
|
||||
| `settings-panel` | 设置面板内嵌区块 | 声明式:CapabilitySettings | 插件 | 3 |
|
||||
|
||||
### 4.4 插槽注册机制(前端)
|
||||
|
||||
```typescript
|
||||
// frontend/src/plugin/slots.ts
|
||||
|
||||
interface SlotDefinition {
|
||||
id: string // 插槽 ID
|
||||
component?: Component // 注入的 Vue 组件
|
||||
position?: 'prepend' | 'append' | 'before' | 'after' // 插入位置
|
||||
order?: number // 同一插槽内的排序权重
|
||||
}
|
||||
|
||||
interface UISlotRegistry {
|
||||
/** 插件声明要占用哪些插槽 */
|
||||
registerSlot(pluginId: string, def: SlotDefinition): void
|
||||
|
||||
/** App.vue 查询某插槽有哪些组件 */
|
||||
getSlotComponents(slotId: string): SlotDefinition[]
|
||||
|
||||
/** 所有已注册的插槽概览 */
|
||||
getAllSlots(): Map<string, SlotDefinition[]>
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 App.vue 插槽改造示意
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 标题栏 -->
|
||||
<div class="titlebar">
|
||||
<div class="titlebar-left">u-desk</div>
|
||||
<a-tabs v-model="activeTab" class="header-tabs">...</a-tabs>
|
||||
<!-- 插槽: titlebar-extra -->
|
||||
<component
|
||||
v-for="slot in getSlotComponents('titlebar-extra')"
|
||||
:key="slot.id"
|
||||
:is="slot.component"
|
||||
/>
|
||||
<WindowControls />
|
||||
</div>
|
||||
|
||||
<!-- 主区域 -->
|
||||
<div class="main-layout">
|
||||
<!-- 插槽: sidebar-left -->
|
||||
<component
|
||||
v-for="slot in getSlotComponents('sidebar-left')"
|
||||
:is="slot.component"
|
||||
/>
|
||||
|
||||
<!-- 插槽: main-content(核心) -->
|
||||
<KeepAlive :include="cacheableComponents">
|
||||
<component :is="getComponent(activeTab)" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏插槽等... -->
|
||||
</template>
|
||||
```
|
||||
|
||||
### 4.6 后端接口支持
|
||||
|
||||
```go
|
||||
// PluginMetadata 新增字段
|
||||
type PluginMetadata struct {
|
||||
// ... 现有字段 ...
|
||||
UISlots []string `json:"ui_slots,omitempty"` // 声明占用的 UI 插槽列表
|
||||
}
|
||||
|
||||
// Manager 新增方法
|
||||
func (m *Manager) GetUISlotContents(slotID string) []UISlotEntry
|
||||
```
|
||||
|
||||
### 4.7 切割原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **最小暴露** | 只开放必要的插槽,不把整个布局变成"配置驱动" |
|
||||
| **位置固定** | 每个插槽的位置在 App.vue 中固定,插件只能决定"往里放什么",不能移动插槽本身 |
|
||||
| **顺序可控** | 同一插槽多插件通过 order 控制排列顺序 |
|
||||
| **隔离渲染** | 每个插槽内的插件组件有独立作用域,避免样式/状态污染 |
|
||||
| **优雅降级** | 插件组件报错不影响宿主 UI(ErrorBoundary 包裹) |
|
||||
|
||||
## 五、关键改造点对比
|
||||
|
||||
### 5.1 App.vue 改造(Phase 3)
|
||||
|
||||
**改造前**:
|
||||
```typescript
|
||||
import FileSystem from './components/FileSystem/index.vue'
|
||||
import MarkdownEditor from './views/markdown-editor/index.vue'
|
||||
|
||||
const getComponent = (key: string) => ({
|
||||
'file-system': FileSystem,
|
||||
'markdown-editor': MarkdownEditor
|
||||
}[key] || null)
|
||||
```
|
||||
|
||||
**改造后**:
|
||||
```typescript
|
||||
import '@/plugin/built-in' // 副作用:执行所有内置插件注册
|
||||
import { getTabComponent } from '@/plugin/registry'
|
||||
|
||||
const getComponent = (key: string) => {
|
||||
const loader = getTabComponent(key)
|
||||
return loader ? defineAsyncComponent(loader) : null
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 FileEditorPanel.vue 改造(Phase 2)
|
||||
|
||||
**改造前**:10 层 v-if/v-else-if 链(binary/image/video/audio/pdf/excel/word/csv/html/md/text)
|
||||
|
||||
**改造后**:
|
||||
```vue
|
||||
<template>
|
||||
<div class="editor-content">
|
||||
<iframe v-if="renderConfig?.type === 'iframe'" :src="renderConfig.src" />
|
||||
<component v-else-if="previewComponent" :is="previewComponent" v-bind="previewProps" />
|
||||
<CodeEditor v-else ... />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { resolvePreviewHandler } from '@/plugin/registry'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const handler = computed(() =>
|
||||
props.config.currentFileName ? resolvePreviewHandler(props.config.currentFileName) : null
|
||||
)
|
||||
const previewComponent = computed(() => handler.value?.getComponent?.())
|
||||
const renderConfig = computed(() => handler.value?.getRenderConfig?.(props.filePath ?? ''))
|
||||
</script>
|
||||
```
|
||||
Reference in New Issue
Block a user