# u-desk 插件化架构设计方案

> 状态：调研完成，待实施
> 日期：2026-04-11

## 一、现状痛点

| 痛点 | 现状 | 影响 |
|------|------|------|
| **app.go God Object** | 958 行，67 个方法全在一个 struct 上 | 难以维护，新功能必须改核心文件 |
| **App.vue 硬编码映射** | `getComponent()` 是 3 个 key 的字面量对象 | 新 Tab 必须改源码 |
| **文件预览 if/else 链** | `FileEditorPanel.vue` 有 ~12 层 v-if/v-else-if | 新增文件类型需改 5+ 处 |
| **大体积功能嵌入** | draw.io 等 ~12-15MB 功能无法按需加载 | 安装包膨胀 |

## 二、架构总览

```
+================================================================+
|                      u-desk Application                         |
+=================================================================
|  Core (Go)                    Plugin Manager                     |
|  - App facade                 - Registry / Lifecycle / Loader    |
|  - ConfigStore (SQLite)       - TabRegistry                      |
|  - EventBus (Wails Events)    - PreviewRegistry                  |
+----------+----------+---------+-----------+----------+          |
           |          |         |           |          |           |
   +-------+  +------+  +-----+  +--------+  +------+  +---+      |
   |builtin: |builtin: |builtin | builtin:  | future | future     |
   | file-sys| db-cli  | md-edit| drawio    | JS/WASM| Go .so    |
   +---------+---------+--------+-----------+--------+------------+
                                |
+==============================================================+|
|                   Frontend (Vue 3)                            ||
|  PluginRegistry (TS)                                         ||
|  - TabProviders        → 替代 App.vue 硬编码映射              ||
|  - FilePreviewHandlers → 替代 FileEditorPanel if/else 链     ||
|  - ComponentLoader     → defineAsyncComponent 懒加载          ||
+==============================================================+
```

## 三、核心接口定义

### 3.1 后端插件接口（Go）

```go
// 文件路径: internal/plugin/plugin.go

// PluginID 插件唯一标识
type PluginID string

// PluginMetadata 插件元数据
type PluginMetadata struct {
    ID             PluginID  `json:"id"`
    Name           string    `json:"name"`
    Version        string    `json:"version"`
    Description    string    `json:"description"`
    Author         string    `json:"author"`
    TabKey         string    `json:"tab_key,omitempty"`      // 提供的 Tab key
    FileExtensions []string  `json:"file_extensions,omitempty"` // 处理的文件扩展名
}

// PluginCapability 插件能力标志
type PluginCapability int

const (
    CapabilityTabProvider  PluginCapability = 1 << iota // 提供 Tab 页面
    CapabilityFilePreview                               // 文件预览
)

// Plugin 核心插件接口
type Plugin interface {
    Meta() PluginMetadata
    Capabilities() PluginCapability
    Init(ctx context.Context, core CoreServices) error
    Start() error
    Stop() error
}

// TabProvider Tab 提供者接口（可选）
type TabProvider interface {
    TabDefinition() TabDef
    TabComponentPath() string
}

// FilePreviewHandler 文件预览处理接口（可选）
type FilePreviewHandler interface {
    CanPreview(filename string, mimeType string) bool
    PreviewInfo(filename string) PreviewInfo
}

// PreviewInfo 预览元信息（发送给前端）
type PreviewInfo struct {
    Type            string            `json:"type"`             // "drawio", "image", "pdf"
    Title           string            `json:"title"`
    Icon            string            `json:"icon,omitempty"`
    NeedsContainer  bool              `json:"needs_container,omitempty"`
    ContainerConfig map[string]string `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"`
}
```

### 3.2 前端插件接口（TypeScript）

```typescript
// 文件路径: frontend/src/plugin/types.ts

/** 插件能力标志 */
export enum PluginCapability {
  None = 0,
  TabProvider = 1 << 0,    // 提供 Tab
  FilePreview = 1 << 1,    // 文件预览
  Settings = 1 << 2,       // 设置页
}

/** Tab 插件定义 */
export interface TabPluginDefinition {
  key: string
  title: string
  icon?: string
  componentLoader: () => Promise<Component>  // 异步组件加载器
  defaultVisible?: boolean
  order?: number
}

/** 文件预览处理器定义 */
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'
  src?: string
  htmlContent?: string
  props?: Record<string, any>
}
```

## 四、插件管理器设计

### 4.1 后端 PluginManager

```go
// internal/plugin/manager.go

type Manager struct {
    mu         sync.RWMutex
    plugins    map[PluginID]Plugin
    core       CoreServices
    tabReg     *TabRegistry
    previewReg *PreviewRegistry
    ctx        context.Context
}

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) GetPluginInfos() []PluginMetadata   // 获取插件列表
func (m *Manager) ResolvePreview(filename string) (*PreviewInfo, error)
func (m *Manager) Shutdown() error
```

### 4.2 前端注册中心

```typescript
// frontend/src/plugin/registry.ts

import { reactive } from 'vue'

const state = reactive({
  tabPlugins: new Map<string, TabPluginDefinition>(),
  previewHandlers: [] as FilePreviewHandlerDefinition[],
})

export function registerTabPlugin(def: TabPluginDefinition): void
export function getTabComponent(key: string): (() => Promise<Component>) | null
export function getAllTabDefinitions(): TabPluginDefinition[]
export function registerPreviewHandler(handler: FilePreviewHandlerDefinition): void
export function resolvePreviewHandler(filename: string): FilePreviewHandlerDefinition | null
```

## 五、关键改造点

### 5.1 FileEditorPanel.vue 重构（最大收益）

**改造前**：12 层 v-if/v-else-if 链（image/video/audio/pdf/html/md/excel/word/csv/text/binary）

**改造后**：
```vue
<template>
  <!-- iframe 类型（draw.io 等） -->
  <iframe v-if="renderConfig?.type === 'iframe'" :src="renderConfig.src" />
  <!-- Vue 组件类型 -->
  <component v-else-if="previewComponent" :is="previewComponent" v-bind="previewProps" />
</template>

<script setup>
import { resolvePreviewHandler } from '@/plugin/registry'

const handler = computed(() =>
  props.config.currentFileName ? resolvePreviewHandler(props.config.currentFileName) : null
)
const previewComponent = computed(() => handler.value?.getComponent?.())
const renderConfig = computed(() => handler.value?.getRenderConfig?.(filePath))
</script>
```

### 5.2 App.vue 改造

**改造前**：
```ts
const getComponent = (key) => ({ 'file-system': FileSystem, 'db-cli': DbCli }[key])
```

**改造后**：
```ts
import '@/plugin/built-in'  // 副作用：执行内置插件注册
import { getTabComponent } from '@/plugin/registry'

const getComponent = (key) => {
  const loader = getTabComponent(key)
  return loader ? defineAsyncComponent(loader) : null
}
```

### 5.3 Draw.io 插件示例（首个验证插件）

**后端** (`internal/plugin/builtin/drawio_plugin.go`)：
```go
type DrawIoPlugin struct{ server *http.Server }

func (p *DrawIoPlugin) Meta() PluginMetadata {
    return PluginMetadata{
        ID: "builtin-drawio", Name: "Draw.io 查看器",
        FileExtensions: []string{"drawio", "dio"},
    }
}
func (p *DrawIoPlugin) Capabilities() PluginCapability { return CapabilityFilePreview }
func (p *DrawIoPlugin) CanPreview(filename, _ string) bool {
    ext := filepath.Ext(filename)
    return ext == ".drawio" || ext == ".dio"
}
```

**前端** (`frontend/src/plugin/built-in/drawio-handler.ts`)：
```ts
registerPreviewHandler({
  id: 'drawio-preview', priority: 95,
  canHandle: (f) => /\.drawio?$/i.test(f),
  getRenderConfig: (path) => ({
    type: 'iframe',
    src: `http://localhost:18765/drawio/index.html?chrome=0&lightbox=1&stealth=1#R${xmlContent}`
  })
})
```

## 六、分阶段实施路径

### Phase 0：基础设施搭建

不改现有功能，只建立骨架：

| 文件 | 内容 |
|------|------|
| `internal/plugin/plugin.go` | 核心 Plugin/TabProvider/FilePreviewHandler 接口 |
| `internal/plugin/manager.go` | PluginManager 实现 |
| `internal/plugin/tab_registry.go` | Tab 注册表 |
| `internal/plugin/preview_registry.go` | 预览处理器注册表 |
| `frontend/src/plugin/types.ts` | TS 类型定义 |
| `frontend/src/plugin/registry.ts` | 前端注册中心 |

### 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` | 小改：引入 pluginMgr，注册 DrawIoPlugin |
| 4 | `FileEditorPanel.vue` | 微改：if/else 末尾追加 drawio 分支 |

**验证标准**：打开 `.drawio` 文件 → 显示预览 → 其他文件不受影响

### Phase 2：文件预览系统重构

将全部 12 种预览迁移到插件注册表：

1. 在 `preview-handlers.ts` 注册所有内置处理器
2. `FileEditorPanel.vue` template 改为 `<component :is>` / `<iframe>`
3. `filePreviewHandlers.js` 的 Excel/Word/CSV 逻辑拆分到对应组件

### Phase 3：Tab 系统插件化

1. `built-in/index.ts` 注册 3 个内置 Tab
2. `App.vue` getComponent 改为查 registry
3. KeepAlive include 动态化

### Phase 4：app.go 瘦身（可选延后）

方法签名保留（Wails v2 绑定要求），实现委托给 pluginMgr。

## 七、新增/变更文件清单

```
新增:
  internal/plugin/
    plugin.go              # 接口定义
    manager.go             # PluginManager
    tab_registry.go        # Tab 注册表
    preview_registry.go    # 预览注册表
    builtin/
      drawio_plugin.go     # Draw.io 插件（Phase 1）
  frontend/src/plugin/
    types.ts               # TS 接口
    registry.ts            # 注册中心
    built-in/
      drawio-handler.ts    # Draw.io 前端 handler（Phase 1）
      preview-handlers.ts  # 全部内置预览注册（Phase 2）
      index.ts             # 内置 Tab 注册（Phase 3）

修改:
  app.go                           # 引入 pluginMgr（Phase 1）
  frontend/src/.../FileEditorPanel.vue  # 追加插件入口 / 重写
  frontend/src/App.vue                 # getComponent 改 registry
```

## 八、风险与应对

| 风险 | 应对策略 |
|------|----------|
| Wails v2 无法动态绑定 API 方法 | App 上预留 `PluginCall(id, method, params)` 统一分发 |
| FileEditorPanel 改造影响面大 | Phase 1 只在 if/else 末尾追加；Phase 2 用 feature flag 切换新旧路径 |
| 包体积膨胀（draw.io ~12MB） | 条件编译 `go build tags` 或未来外部下载缓存 |
| 过度抽象增加复杂度 | YAGNI 原则：只在确实需要扩展点时才加接口 |

## 九、Wails v3 兼容性预留

Wails v3 (alpha.74) 主要变化对插件架构的影响：

| 维度 | v2 | v3 | 影响 |
|------|----|----|------|
| 绑定方式 | struct 方法自动生成 | 手动注册 handler | 更有利于插件化 |
| 前端调用 | `window.go.main.App.Xxx()` | `window.go.invoke()` | 需适配层 |
| 事件系统 | `runtime.EventsEmit` | 类似但 API 不同 | 需抽象层 |

建议在 eventbus 中封装一层 Wails 版本抽象，切换 v3 时只需替换底层实现。
