Private
Public Access
1
0

新增: SFTP直连+网站预览+OSS区域嗅探+热键+BGM播放

This commit is contained in:
2026-05-12 11:06:28 +08:00
parent 545d7a864d
commit 2a363fd729
62 changed files with 6687 additions and 660 deletions

View 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 可独立交付验证。

View 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 3Tab 注册表
**新建** `internal/plugin/tab_registry.go`
内容:
- `TabRegistry` structmu + entries map
- Register冲突检测、GetByTabKey、GetAllDefinitions按 Order 排序、GetAllProviders、Count
依赖Step 1
---
### Step 4预览注册表
**新建** `internal/plugin/preview_registry.go`
内容:
- `PreviewRegistry` structmu + handlers 切片,按 priority 降序)
- Register自动排序、Resolve遍历匹配第一个 canHandle、GetAllHandlers、Count
依赖Step 1
---
### Step 5PluginManager
**新建** `internal/plugin/manager.go`
内容:
- `Manager` structplugins 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 statetabPlugins Map + previewHandlers 数组)
- Tab APIregisterTabPlugin / getTabComponent / getAllTabDefinitions / hasTabPlugin
- Preview APIregisterPreviewHandler按 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 |

View 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 3Tab 系统插件化 + 设置面板 + 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 改为查 registryKeepAlive 动态化;接入 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
```

View File

@@ -0,0 +1,54 @@
# 决策记录
## ADR-001CoreServices 使用 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-002plugin_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 只建表

View 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 1Draw.io 验证插件),用真实插件验证骨架的完整性。

View 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 }
```

View 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 端口号) | 用户修改插件设置时 |

View 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 控制排列顺序 |
| **隔离渲染** | 每个插槽内的插件组件有独立作用域,避免样式/状态污染 |
| **优雅降级** | 插件组件报错不影响宿主 UIErrorBoundary 包裹) |
## 五、关键改造点对比
### 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>
```