# 架构设计

## 一、架构全景

```
+==================================================================+
|                     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>
```
