设计文档-CC-Switch-Web转TUI设计文档

CC-Switch-Web转TUI设计文档

这是没有源码,由Claude生成的复刻https://github.com/cp-yu/cc-switch-web网页版本。

版本:基于 tui-v0.2 分支
技术栈:Go 1.22+ + Bubble Tea + Lip Gloss + Bubble Table + Bubble List
通信方式:gRPC / JSON-RPC over Unix socket + WebSocket 事件推送
输出目录:src-tui/


目录

  1. 项目概述
  2. 技术选型
  3. 项目结构
  4. 通信层设计
  5. 核心数据模型
  6. 页面设计
  7. 交互逻辑
  8. 后端服务层
  9. 样式与主题
  10. 关键实现要点

1. 项目概述

CC Switch TUI 是一个终端界面的 AI 编程助手供应商切换器,功能与 React Web 前端等价,支持 Claude Code、Claude Desktop、Codex、Gemini CLI、OpenCode、OpenClaw、Hermes 七款 AI 工具的 API 供应商管理、快速切换、本地代理、提示词/Skills/MCP 配置、会话管理与用量统计。

核心目标: - 终端优先,无需图形界面即可运行 - 与 Web 前端功能对等,覆盖 100% 核心场景 - 复用同一套 Rust 后端(src-tauri),通过 gRPC/JSON-RPC 通信 - 保持与 Web 端一致的页面布局和交互逻辑


2. 技术选型

层级技术栈说明
TUI 框架chaing/teaMIT 协议,业界标准 Go TUI 框架,支持事件循环、VT100 渲染
样式系统charmbracelet/lipglossCSS-like 终端样式引擎,支持 border、padding、margin、color
表格组件charmbracelet/bubble-table多行/单行选择、分页、键盘导航
列表组件charmbracelet/bubbles/list单选/多选列表,支持 filtering、delegate 定制
对话框charmbracelet/bubbles/modal + textinput输入对话框、确认框、表单
进度条charmbracelet/bubbles/progress安装/同步/导入进度
spinnercharmbracelet/bubbles/spinner加载动画
表格图表rockice/go-chart终端内用量趋势 ASCII 图表
通信层nats-io/nats.gogorilla/websocket + grpc-ecosystem/grpc-gateway事件推送(WebSocket)+ RPC 调用(gRPC/JSON-RPC)
SQLitemattn/go-sqlite3modernc.org/sqlite本地配置缓存
CLI 参数charmbracelet/spf13/cobra子命令、配置文件路径等

为什么选择 Bubble Tea 生态?

  • 与 Web 端对等的设计哲学:Bubble Tea 采用 Elm 架构(Model-Update-View),与 React 的 useState/useReducer 模式天然对齐
  • Lip Gloss 提供 CSS-like 样式:Web 端的 TailwindCSS class 概念可以直接映射到 Lip Gloss 的 style.Style 链式调用
  • 成熟度:Charm 生态是 Go TUI 的事实标准,被 Docker DesktopAIDER 等项目使用

3. 项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
src-tui/
├── cmd/
│ └── cc-switch-tui/
│ └── main.go # 入口:cobra root command
├── internal/
│ ├── app/
│ │ ├── app.go # 主应用生命周期:Init/Run/Shutdown
│ │ ├── router.go # 视图路由(currentView 状态机)
│ │ └── store.go # 全局状态容器(providers, settings, proxyStatus 等)
│ │
│ ├── tui/
│ │ ├── views/ # 页面组件(对应 Web 端的 View 类型)
│ │ │ ├── provider_list.go # 主页面:供应商列表
│ │ │ ├── settings_page.go # 设置页面(Tabs)
│ │ │ ├── prompt_panel.go # 提示词管理
│ │ │ ├── skills_panel.go # Skills 管理
│ │ │ ├── session_manager.go # 会话管理
│ │ │ ├── mcp_panel.go # MCP 管理
│ │ │ ├── proxy_console.go # 代理控制台
│ │ │ ├── usage_stats.go # 用量统计
│ │ │ └── app_switcher.go # 应用切换器(Header 底部 Tabs)
│ │ │
│ │ ├── components/ # 可复用 UI 组件
│ │ │ ├── header.go # 顶部 Header 栏(App 名、视图名、状态指示)
│ │ │ ├── provider_card.go # 供应商卡片(等价于 ProviderCard)
│ │ │ ├── provider_card_list.go # 卡片列表 + 拖拽排序
│ │ │ ├── status_bar.go # 底部状态栏(代理状态、事件通知)
│ │ │ ├── banner.go # 警告横幅(EnvWarningBanner、OpenClawHealthBanner)
│ │ │ ├── tab_view.go # Tabs 组件(设置页内 6 个标签)
│ │ │ ├── form_input.go # 表单输入组件(textinput 封装)
│ │ │ ├── confirm_dialog.go # 确认对话框
│ │ │ ├── modal_dialog.go # 模态框容器
│ │ │ ├── toggle_switch.go # 开关组件(等价于 ProxyToggle)
│ │ │ ├── badge.go # 徽章组件(OMO、Slim、健康状态)
│ │ │ ├── progress_view.go # 进度视图(安装、同步)
│ │ │ └── tree_view.go # 树形视图(会话详情消息树)
│ │ │
│ │ ├── styles/ # 样式定义(等价于 TailwindCSS)
│ │ │ ├── palette.go # 颜色色板(等价于 CSS 变量)
│ │ │ ├── borders.go # 边框样式
│ │ │ └── themes.go # 明/暗主题切换
│ │ │
│ │ └── keymap/ # 快捷键定义
│ │ └── keymap.go # KeyMap 结构体 + 绑定
│ │
│ ├── api/
│ │ ├── client.go # RPC/HTTP 客户端抽象
│ │ ├── ws_client.go # WebSocket 客户端(事件订阅)
│ │ ├── providers.go # 供应商 API 调用层
│ │ ├── settings.go # 设置 API
│ │ ├── proxy.go # 代理 API
│ │ ├── mcp.go # MCP API
│ │ ├── prompts.go # 提示词 API
│ │ ├── skills.go # Skills API
│ │ ├── sessions.go # 会话 API
│ │ ├── usage.go # 用量 API
│ │ ├── failover.go # 故障转移 API
│ │ ├── auth.go # 认证 API
│ │ ├── deeplink.go # DeepLink 导入
│ │ ├── webdav.go # WebDAV 同步
│ │ └── types.go # Go 类型定义(等价于 TypeScript types)
│ │
│ └── service/
│ ├── provider.go # Provider 业务逻辑(CRUD、排序、切换)
│ ├── proxy.go # Proxy 服务(生命周期、状态机)
│ ├── skill.go # Skill 管理(安装/卸载/更新)
│ ├── mcp.go # MCP 配置管理
│ └── config.go # 配置文件读写/导入导出/备份

├── pkg/ # 可导出包
│ └── version/
│ └── version.go # 版本信息

├── proto/ # gRPC 协议定义(可选)
│ └── ccswitch.proto

├── go.mod
├── go.sum
├── config.example.yaml # 默认配置模板
└── Makefile # 构建脚本

4. 通信层设计

4.1 通信架构

1
2
3
4
5
6
┌─────────────────┐         ┌──────────────────┐         ┌─────────────────┐
│ src-tui (Go) │────────▶│ src-tauri (Rust) │ │ 文件系统/代理 │
│ TUI 客户端 │ gRPC │ 后端服务层 │ │ │
│ │◀────────│ SQLite + 事件 │ │ │
│ Model/Update │ WS事件 │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘

4.2 通信协议

采用 双通道 设计:

  1. 请求-响应通道(gRPC / JSON-RPC):用于所有 CRUD 操作
    • 使用 Unix Domain Socket(本地桌面)或 TCP(远程模式)
    • 与 Web 端的 invoke(command, payload) 一一映射
    • 每个 Tauri command 在 Go 侧有一个对应的方法
  2. 事件推送通道(WebSocket):用于实时事件
    • 订阅 provider-switcheduniversal-provider-syncedwebdav-sync-status-updated 等事件
    • 收到事件后触发 Update() 刷新对应数据

4.3 RPC 方法映射

Go TUI 的 api/ 层直接映射 Web 端的 API 模块,每个模块一个文件:

Go API 模块等价 Web API 模块后端命令
api/providers.goprovidersApiget_providers, add_provider, update_provider
api/universal.gouniversalProvidersApiget_universal_providers, upsert_universal_provider
api/settings.gosettingsApiget_settings, save_settings, webdav_sync_upload
api/proxy.goproxyApistart_proxy_server, get_proxy_status, switch_proxy_provider
api/failover.gofailoverApiget_failover_queue, add_to_failover_queue
api/mcp.gomcpApiget_mcp_servers, upsert_mcp_server
api/prompts.gopromptsApiget_prompts, upsert_prompt
api/skills.goskillsApiget_installed_skills, install_skill_unified
api/sessions.gosessionsApilist_sessions, get_session_messages
api/usage.gousageApiget_usage_summary, get_request_logs
api/auth.goauthApi, copilotApiauth_start_login, copilot_start_device_flow

4.4 WebSocket 事件订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在 app.go 中初始化
wsClient := ws.NewClient(config.WSURL)
wsClient.Subscribe("provider-switched", func(evt ws.Event) {
// 刷新当前应用的供应商列表
store.Providers.Refresh()
})
wsClient.Subscribe("universal-provider-synced", func(evt ws.Event) {
store.UniversalProviders.Refresh()
})
wsClient.Subscribe("webdav-sync-status-updated", func(evt ws.Event) {
// 在状态栏显示 Toast
store.ShowToast(evt.Data.Message)
})

5. 核心数据模型

Go 侧类型定义与 Web 端 TypeScript 类型一一映射:

5.1 Provider(供应商)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type Provider struct {
ID string `json:"id"`
Name string `json:"name"`
SettingsConfig map[string]interface{} `json:"settingsConfig"`
WebsiteURL *string `json:"websiteUrl,omitempty"`
Category *ProviderCategory `json:"category,omitempty"`
CreatedAt *int64 `json:"createdAt,omitempty"`
SortIndex *int `json:"sortIndex,omitempty"`
Notes *string `json:"notes,omitempty"`
Meta *ProviderMeta `json:"meta,omitempty"`
Icon *string `json:"icon,omitempty"`
IconColor *string `json:"iconColor,omitempty"`
InFailoverQueue bool `json:"inFailoverQueue"`
}

type ProviderCategory string

const (
CategoryOfficial ProviderCategory = "official"
CategoryCnOfficial ProviderCategory = "cn_official"
CategoryCloudProvider ProviderCategory = "cloud_provider"
CategoryAggregator ProviderCategory = "aggregator"
CategoryThirdParty ProviderCategory = "third_party"
CategoryCustom ProviderCategory = "custom"
CategoryOMO ProviderCategory = "omo"
CategoryOMOSlim ProviderCategory = "omo-slim"
)

5.2 AppId(应用类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
type AppId string

const (
AppClaude AppId = "claude"
AppClaudeDesktop AppId = "claude-desktop"
AppCodex AppId = "codex"
AppGemini AppId = "gemini"
AppOpenCode AppId = "opencode"
AppOpenClaw AppId = "openclaw"
AppHermes AppId = "hermes"
)

var AllAppIDs = []AppId{AppClaude, AppClaudeDesktop, AppCodex, AppGemini, AppOpenCode, AppOpenClaw, AppHermes}

5.3 McpServer(MCP 服务器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type McpServer struct {
ID string `json:"id"`
Name string `json:"name"`
Server McpServerSpec `json:"server"`
Apps McpApps `json:"apps"`
Description *string `json:"description,omitempty"`
Tags []string `json:"tags,omitempty"`
Homepage *string `json:"homepage,omitempty"`
}

type McpServerSpec struct {
Type *string `json:"type,omitempty"` // "stdio" | "http" | "sse"
Command *string `json:"command,omitempty"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env,omitempty"`
CWD *string `json:"cwd,omitempty"`
URL *string `json:"url,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
}

type McpApps struct {
Claude bool `json:"claude"`
ClaudeDesktop bool `json:"claude-desktop,omitempty"`
Codex bool `json:"codex"`
Gemini bool `json:"gemini"`
OpenCode bool `json:"opencode"`
OpenClaw bool `json:"openclaw"`
Hermes bool `json:"hermes"`
}

5.4 全局状态容器(Store)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Store struct {
mu sync.RWMutex

// ── 当前视图 ──
CurrentView View
ActiveApp AppId

// ── 数据缓存 ──
Providers map[AppId]map[string]*Provider // appId -> map[id]*Provider
CurrentProvider map[AppId]string // appId -> currentProviderID
Settings *Settings
ProxyStatus *ProxyStatus
McpServers []*McpServer
Prompts map[AppId][]*Prompt
InstalledSkills []*InstalledSkill
Sessions []*SessionMeta
UniversalProviders map[string]*UniversalProvider
UsageSummary *UsageSummary
FailoverQueue map[AppId][]FailoverQueueItem

// ── UI 状态 ──
Loading map[string]bool // operation key -> loading
Toasts []string // 待显示的通知
ErrorMsg string // 当前错误信息
SelectedCards map[string]bool // 多选供应商(批量操作)
ModalOpen bool // 模态框是否打开
ModalType ModalType // 模态框类型
}

6. 页面设计

6.1 主页面(供应商列表)

等价于 Web 端的 ProviderList.tsx + ProviderCard.tsx

界面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────────┐
│ CC Switch [代理开关] [设置] [?] │ ← Header
├─────────────────────────────────────────────────────────────────┤
│ [Claude] [Claude Desktop] [Codex] [Gemini] [OpenCode] │ ← AppSwitcher (Tabs)
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ AgnesAI [使用中] [🔧] [✏] [⧉] [🗑] │ ← ProviderCard
│ │ api.agnes.ai blue icon │ ← 蓝色高亮(当前激活)
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ OpenRouter [编辑] [复制] [测试] [删除] │ ← 默认态
│ │ openrouter.ai gray icon │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ Together AI [编辑] [复制] [测试] [删除] │ ← 默认态
│ │ together.ai orange icon │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ 代理: 已停止 │ 故障转移: 关 │ 供应商: 3 │ [+ 添加] (↓) │ ← StatusBar
└─────────────────────────────────────────────────────────────────┘

Header 设计

1
2
3
4
┌──────────────────────────────────────────────────────────────────────────┐
│ CC Switch [代理开关] [设置] [?] │
│ ^^^^^^^ 当前激活供应商在代理模式下为绿色(emerald),否则为蓝色(blue) │
└──────────────────────────────────────────────────────────────────────────┘
  • LogoCC Switch,代理模式下 text-emerald,普通模式 text-blue
  • 右侧工具栏(等价于 Web 端 Header 右侧):
    • ProxyToggle[代理开关],显示当前代理状态(已启动/已停止)
    • FailoverToggle[故障转移],显示开关状态
    • Settings:进入设置页
    • Help/?:快捷键帮助

AppSwitcher 设计

在 Header 下方,以 TabView 组件呈现应用标签栏:

1
2
[ Claude ] [Claude Desktop] [ Codex ] [ Gemini ] [ OpenCode ] [ OpenClaw ] [ Hermes ]
└──当前选中──
  • 通过 Settings.VisibleApps 控制哪些 App 显示
  • 当前选中 App 高亮(背景色 background-muted
  • 切换时触发 fade 动画(通过清屏 + 重绘实现)
  • 持久化到本地文件 ~/.cc-switch/tui-last-app

工具栏图标组

等价于 Web 端的 工具栏图标组,根据当前 App 动态变化:

默认 AppOpenCode AppOpenClaw AppHermes App
Skills 🔧提示词 📖会话 🕐MCP M
提示词 📖MCP M工作区 📁记忆 🧠
会话 🕐环境变量 🔑Web UI 📊
MCP M工具 🛡
Agents ⚙
会话 🕐

实现:每个 App 定义一组 action keymap,切换 App 时更新 KeyMap 显示。

ProviderCard 供应商卡片

等价于 Web 端 ProviderCard.tsx,使用 Lip Gloss 绘制圆角矩形卡片:

1
2
3
4
┌──────────────────────────────────────────────────────────────┐
│ ≡ AgnesAI ● 使用中 │ 🔧编辑 │ ⧉复制 │ 🗑删除 │
│ api.agnes.ai │
└──────────────────────────────────────────────────────────────┘

卡片分区:

区域内容样式
左侧拖拽柄 (GripVertical)默认隐藏,hover/focus 时显示
图标8 字符宽方框,显示图标或首字母iconColor 着色
名称供应商名fontBold
状态标签使用中/OMO/Slim/需要路由彩色背景 + 反白文字
URL/备注API 地址italic + text-blue
用量用量数据(如果缓存了)text-muted
操作按钮编辑/复制/测试/删除默认 text-muted,hover 时 text-default

卡片状态颜色(等价于 Web 端): - 当前激活(普通模式):border-blue-500 + bg-blue-500/10 - 当前激活(代理接管模式):border-emerald-500 + bg-emerald-500/10 - 默认(非激活):border-default,focus 时 border-active

操作按钮交互: - 点击卡片进入”操作模式”:卡片右侧显示操作按钮列表 - 每个操作绑定快捷键:e 编辑、c 复制、t 测试、d 删除 - 按 Esc 退出操作模式

状态栏(StatusBar)

1
2
3
┌──────────────────────────────────────────────────────────────────┐
│ 代理: ● 已启动 │ 故障转移: 关 │ Claude: 3个 │ [+ 添加 ▼] │
└──────────────────────────────────────────────────────────────────┘
  • 左侧:代理状态(绿色圆点=运行中,红色=停止)
  • 中间:故障转移状态、各 App 供应商数量
  • 右侧:添加按钮(TabEnter 弹出菜单)

6.2 设置页面

等价于 SettingsPage.tsx,使用 TabView 实现 6 个标签页。

界面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──────────────────────────────────────────────────────────────────┐
│ 设置 [保存] [重启] │
├──────────────────────────────────────────────────────────────────┤
│ [通用] [路由] [认证] [高级] [使用统计] [关于] │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ─── 通用 ─── │
│ │
│ 界面语言: [中文 ▼] │
│ 外观主题: [深色 ▼] │
│ 主页面应用: [✓ Claude] [✓ Claude Desktop] [ ] Codex ... │
│ Skills 存储: [cc-switch ▼] │
│ 窗口行为: [✓ 最小化到托盘] │
│ 首选终端: [默认 ▼] │
│ │
├──────────────────────────────────────────────────────────────────┤
│ ↑↓ 导航 Tab 切换标签 Enter 编辑 Esc 返回 │
└──────────────────────────────────────────────────────────────────┘

6 个标签页

标签Go TUI 实现等价 Web 组件
通用form_input.go 表单settings-general
路由折叠面板(可展开/收起)settings-route
认证账号列表 + 操作按钮settings-auth
高级文件路径输入 + 操作按钮settings-advanced
使用统计ASCII 表格 + 简单图表settings-usage
关于固定文本输出settings-about

路由标签页(折叠面板)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌──────────────────────────────────────────────────────────────────┐
│ ─── 路由 ─── │
│ │
│ [▶] 本地路由(代理服务器) │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ 状态: ● 已启动 │ 端口: 17666 ││
│ │ [ 启动 ] [ 停止 ] ││
│ └──────────────────────────────────────────────────────────────┘│
│ │
│ [▶] 自动故障转移 │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ 状态: [关闭 ▼] ││
│ │ Claude 队列: [AgnesAI, OpenRouter] ││
│ └──────────────────────────────────────────────────────────────┘│
│ │
│ [▶] 整流器 │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ [✓] 请求 Thinking 签名 ││
│ │ [✓] 请求 Thinking 预算 ││
│ └──────────────────────────────────────────────────────────────┘│
│ │
│ [▶] 全局出站代理 │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ 代理地址: [http://127.0.0.1:7890 ▼] ││
│ │ [ 测试连接 ] ││
│ └──────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────┘

使用 bubble-tableRow 组件 + 展开/收起状态实现折叠面板。


6.3 提示词管理

等价于 PromptPanel.tsx

界面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──────────────────────────────────────────────────────────────────┐
│ 提示词管理 [添加提示词] │
├──────────────────────────────────────────────────────────────────┤
│ 共 3 个 · 已启用 2 个 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ system-prompt [已启用 ✓] │ [编辑] [删除] │ │
│ │ 系统级提示词 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ custom-instructions [已启用 ✓] │ [编辑] [删除] │ │
│ │ 自定义指令 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ project-specific [已禁用] │ [编辑] [删除] │ │
│ │ 项目特定提示词 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────────────┤
│ ↑↓ 选择 e 编辑 d 删除 t 切换开关 i 导入 Esc 返回 │
└──────────────────────────────────────────────────────────────────┘
  • 使用 bubble-list 渲染提示词列表
  • 每条记录:名称 + 描述(可选)+ 启用开关 + 操作按钮
  • 编辑时使用 modal_dialog + textinput 多行编辑器
  • 空状态:居中显示友好提示文本

6.4 Skills 管理

等价于 UnifiedSkillsPanel.tsx + SkillsPage.tsx

管理视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──────────────────────────────────────────────────────────────────┐
│ Skills 管理 [批量更新] │
├──────────────────────────────────────────────────────────────────┤
│ Claude: 5 | Codex: 3 | Gemini: 2 | OpenCode: 0 | OpenClaw: 0 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ [ 从备份中恢复 ] [ 从ZIP安装 ] [ 导入已有 ] [ 发现技能 ] │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ github-co-pilot [✓ Claude] [✓ Codex] [ ]... │ │
│ │ 版本: v2.1.0 │ v2.0.0 → v2.1.0 (更新可用) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ coding-patterns [✓ Claude] [✓ Gemini] │ │
│ │ 版本: v1.0.0 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────────────┤
│ ↑↓ 选择 i 安装 u 卸载 s 更新 r 恢复 Esc 返回 │
└──────────────────────────────────────────────────────────────────┘

发现视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──────────────────────────────────────────────────────────────────┐
│ 技能发现 [刷新] [添加仓库] │
├──────────────────────────────────────────────────────────────────┤
│ 搜索: [_____________________________ 搜索] │
├──────────────────────────────────────────────────────────────────┤
│ 已配置仓库: [github-co-pilot/main] [custom-skills/dev] │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ awesome-prompt 一个很棒的提示词技能 │ │
│ │ github/co-pilot/main [ 安装 ] │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ code-reviewer 自动代码审查 │ │
│ │ github/co-pilot/main [ 安装 ] │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────────────┤
│ ↑↓ 选择 i 安装 Esc 返回 │
└──────────────────────────────────────────────────────────────────┘

6.5 会话管理

等价于 SessionManagerPage.tsx,采用双栏布局

界面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
┌──────────────────────────────────────────────────────────────────────┐
│ 会话管理 [批量删除] [刷新] │
├──────────────────────────────────────────────────────────────────────┤
│ 应用筛选: [全部 ▼] │
├──────────────────────┬───────────────────────────────────────────────┤
│ 会话列表 (左栏) │ 会话详情 (右栏) │
│ │ │
│ ┌────────────────┐ │ ┌─ AgnesAI Session ──────────────────────┐ │
│ │ 2026-06-11 │ │ │ │ │
│ │ 重构供应商切换逻辑 │ │ 项目: /home/user/project │ │
│ │ 09:32 │ │ │ │ │
│ ├────────────────┤ │ ───────────────────────────────────────── │
│ │ 2026-06-10 │ │ │ 恢复命令: │ │
│ │ 修复代理高并发 │ │ │ claude --resume abc123 │ │
│ │ 14:15 │ │ │ │ │
│ ├────────────────┤ │ ───────────────────────────────────────── │
│ │ 2026-06-10 │ │ │ 对话记录: │ │
│ │ 添加 Skills 管理 │ │ │ │ │
│ │ 11:00 │ │ │ User [14:15] │ │
│ └────────────────┘ │ │ "帮我看一下代理的代码..." │ │
│ │ │ │ │
│ 共 42 个会话 │ │ AI [14:16] │ │
│ │ │ "好的,我来帮你查看代理代码..." │ │
│ │ │ │ │
│ │ │ User [14:18] │ │
│ │ │ "帮我修复一下..." │ │
│ │ │ │ │
│ │ │ AI [14:19] │ │
│ │ │ "已完成修复,以下是关键改动..." │ │
│ │ │ │ │
│ │ │ [打开终端 ▼] [ 删除会话 ] │ │
│ │ └───────────────────────────────────────┘ │
├──────────────────────┴──────────────────────────────────────────────┤
│ ← → 切换面板 ↑↓ 导航 m 多选 d 删除 r 恢复终端 Esc 返回 │
└──────────────────────────────────────────────────────────────────────┘

双栏实现:使用 lipglossJoinHorizontal 将两个 bubble-list 并排放置,中间用 分隔。面板宽度按终端宽度自动分配(左栏 35%,右栏 65%)。

会话详情: - 使用树形结构展示消息(用户消息 vs AI 消息用不同前缀符号和颜色区分) - 滚动通过 bubble-list 的 delegate 实现


6.6 MCP 服务器管理

等价于 UnifiedMcpPanel.tsx

界面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──────────────────────────────────────────────────────────────────┐
│ MCP 服务器管理 [导入已有] [添加] │
├──────────────────────────────────────────────────────────────────┤
│ Claude: 3 | Codex: 1 | Gemini: 0 | OpenCode: 0 | OpenClaw: 0 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ filesystem stdio │ [✓ Claude] [✓ Codex] │ │
│ │ 本地文件系统访问 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ github-server http │ [✓ Claude] │ │
│ │ https://mcp.github.com │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ≡ custom-script stdio │ [✓ Gemini] │ │
│ │ /usr/local/bin/mcp-script │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────────────┤
│ ↑↓ 选择 e 编辑 a 添加 i 导入 t 切换应用 d 删除 │
└──────────────────────────────────────────────────────────────────┘

添加 MCP 服务器向导

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
┌──────────────────────────────────────────────────────────────────┐
│ 添加 MCP 服务器 步骤 1/3: 选择类型 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 请选择服务器类型: │
│ │
│ [▶] stdio 本地命令行服务器 │
│ command + args + env │
│ │
│ [ ] http HTTP 远程服务器 │
│ url + headers │
│ │
│ [ ] sse SSE 远程服务器 │
│ url + headers │
│ │
├──────────────────────────────────────────────────────────────────┤
│ ↑↓ 选择 Enter 继续 Esc 取消 │
└──────────────────────────────────────────────────────────────────┘

──────────────── 步骤 2/3: 填写配置 ────────────────

┌──────────────────────────────────────────────────────────────────┐
│ 名称: [___________________________] │
│ 命令: [/usr/local/bin/mcp-filesystem] │
│ 参数: [--allowed-dir, /home/user/projects] │
│ 环境变量: │
│ KEY VALUE │
│ GITHUB_TOKEN [ghp_xxxxxxxxxxxx*****] │
│ [+ 添加变量] │
│ │
│ 应用启用: │
│ [✓] Claude [ ] Claude Desktop [ ] Codex [ ] Gemini │
│ [ ] OpenCode [ ] OpenClaw [ ] Hermes │
│ │
├──────────────────────────────────────────────────────────────────┤
│ [◀ 上一步] [保存] [取消] │
└──────────────────────────────────────────────────────────────────┘

6.7 代理控制台

等价于 Web 端的 ProxyToggle + FailoverToggle + 路由设置页面。

界面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
┌──────────────────────────────────────────────────────────────────┐
│ 代理控制台 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ─── 本地路由 ─── │
│ │
│ 状态: ● 已启动 │ 端口: 17666 │ PID: 12345 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Claude 代理模式: │ │
│ │ 接管: [✓ 开启] │ │
│ │ 当前供应商: AgnesAI │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ [ 启动代理 ] [ 停止代理 ] [ 切换供应商 ] │
│ │
│ ─── 故障转移 ─── │
│ │
│ 全局故障转移: [关闭 ▼] │
│ │
│ Claude 故障转移队列: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 1. AgnesAI (当前) │ │
│ │ 2. OpenRouter │ │
│ │ 3. Together AI │ │
│ └───────────────────────────────────────────────────────────┘ │
│ [ 添加供应商 ] [ 移除选中 ] [ 调整顺序 ] │
│ │
│ ─── 熔断器 ─── │
│ │
│ 当前状态: │
│ ┌─────────────┬─────────┬───────────┬──────────┐ │
│ │ 供应商 │ 状态 │ 失败次数 │ 熔断阈值 │ │
│ ├─────────────┼─────────┼───────────┼──────────┤ │
│ │ AgnesAI │ ● 正常 │ 0 │ 5 │ │
│ │ OpenRouter │ ● 正常 │ 2 │ 5 │ │
│ │ Together AI │ ● 正常 │ 0 │ 5 │ │
│ └─────────────┴─────────┴───────────┴──────────┘ │
│ [ 重置熔断器 ] │
│ │
│ ─── 全局出站代理 ─── │
│ │
│ 代理地址: [http://127.0.0.1:7890] [ 测试连接 ] │
│ │
├──────────────────────────────────────────────────────────────────┤
│ ↑↓ 导航 Enter 切换 Esc 返回 │
└──────────────────────────────────────────────────────────────────┘

6.8 用量统计

等价于 UsageSummary 页面。

界面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
┌──────────────────────────────────────────────────────────────────┐
│ 用量统计 [刷新] [同步会话] │
├──────────────────────────────────────────────────────────────────┤
│ 时间范围: [最近 7 天 ▼] │ 应用: [全部 ▼] │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ─── 用量汇总 ─── │
│ │
│ 总请求数: 1,234 总 Token: 4,567,890 │
│ 总输入 Token: 2,345,678 总输出 Token: 2,222,212 │
│ 总费用: $12.34 平均延迟: 1.2s │
│ │
│ ─── 每日趋势 ─── │
│ │
│ 请求数(每日): │
│ 100 ┤ ██ │
│ 80 ┤ ██ ██ │
│ 60 ┤ ██ ██ ██ ██ ██ ██ │
│ 40 ┤ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ │
│ 20 ┤ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ │
│ 0 ┼──────────────────────────────────────────────────────── │
│ 05/01 05/05 05/10 05/15 05/20 05/25 05/30 │
│ │
│ ─── 按供应商统计 ─── │
│ │
│ ┌─────────────────┬──────────┬───────────┬──────────┐ │
│ │ 供应商 │ 请求数 │ Token │ 费用 │ │
│ ├─────────────────┼──────────┼───────────┼──────────┤ │
│ │ AgnesAI │ 800 │ 3,000,000 │ $8.50 │ │
│ │ OpenRouter │ 300 │ 1,200,000 │ $2.80 │ │
│ │ Together AI │ 134 │ 367,890 │ $1.04 │ │
│ └─────────────────┴──────────┴───────────┴──────────┘ │
│ │
│ ─── 请求日志 ─── │
│ │
│ ┌──────┬────────────┬───────┬───────┬────────┬────────┐ │
│ │ 时间 │ 供应商 │ 模型 │ 方法 │ 耗时 │ 状态 │ │
│ ├──────┼────────────┼───────┼───────┼────────┼────────┤ │
│ │ 09:32│ AgnesAI │ claude│ POST │ 1.2s │ 200 │ │
│ │ 09:31│ OpenRouter│ claude│ POST │ 2.1s │ 200 │ │
│ └──────┴────────────┴───────┴───────┴────────┴────────┘ │
│ 共 1234 条 │ 第 1/62 页 [ 上一页 ] [ 下一页 ] │
│ │
├──────────────────────────────────────────────────────────────────┤
│ ↑↓ 导航 Enter 查看详情 p 分页 Esc 返回 │
└──────────────────────────────────────────────────────────────────┘

6.9 添加/编辑供应商对话框

等价于 AddProviderDialog.tsx / EditProviderDialog.tsx

界面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌──────────────────────────────────────────────────────────────────┐
│ 添加供应商 供应商: [预设 ▼] │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 名称: [___________________________] │
│ 分类: [第三方 ▼] │
│ API Key: [sk-xxxxxxxxxxxxxxxxxxxxxxxx*****] │
│ Base URL: [https://api.example.com] │
│ 官网/API地址: [https://example.com] │
│ 备注: [___________________________] │
│ │
│ ─── 模型配置 ─── │
│ 默认模型: [claude-4-sonnet-202605xx ▼] │
│ [+ 添加模型] │
│ │
│ ─── 图标与颜色 ─── │
│ 图标: [ ● A ● B ● C ● D ... ] (选择预设图标) │
│ 颜色: [■ 蓝色] [■ 橙色] [■ 绿色] [■ 紫色] [■ 红色] │
│ │
│ ─── 高级选项 ─── │
│ [✓] 自动选择最快端点 │
│ API 格式: [Anthropic ▼] │
│ 成本倍率: [1.0x] │
│ [+ 添加自定义端点] │
│ [✓] 通用配置片段 │
│ │
├──────────────────────────────────────────────────────────────────┤
│ [取消] [ 保存并同步 ] [ 保存 ] │
└──────────────────────────────────────────────────────────────────┘

表单交互模式: - 单行输入:使用 textinput.Model(单行) - 多行输入(备注):使用 textarea.Model(多行) - 下拉选择:使用 bubble-table 的单行选择模式 - 枚举选择(颜色、图标):使用选项列表 - 多值字段(模型列表、自定义端点):使用嵌套列表 + textinput 编辑


7. 交互逻辑

7.1 应用切换

等价于 Web 端 5.1 应用切换

1
2
3
4
5
1. 用户在 AppSwitcher 中按 ← → 切换应用标签
2. store.ActiveApp = newApp
3. 持久化到 ~/.cc-switch/tui-last-app
4. 触发 store.Providers.Refresh(appId)
5. 重新渲染 ProviderList(通过清屏 + 完整重绘实现淡入效果)

TUI 实现细节: - 使用 tea.KeyMsg 捕获 arrow left / arrow right 事件 - AppSwitcher 使用 bubble-table 的单行模式渲染 - 切换时不触发完整重绘,只更新当前视图组件的 Model

7.2 供应商切换

等价于 Web 端 5.2 供应商切换

1
2
3
4
5
1. 用户选中供应商卡片,按 Enter 或点击"使用中"按钮
2. 调用 providersApi.switch(providerID, appID)
3. 后端写入配置文件,发送 provider-switched 事件
4. 前端通过 WebSocket 收到事件,触发 refetch()
5. 刷新供应商列表,高亮更新当前激活供应商

TUI 实现细节: - 选中卡片后,按 Enter 触发切换 - 切换前显示确认提示(如果供应商不在故障转移队列) - 切换成功后在状态栏显示 ✓ 已切换到 AgnesAI - 切换失败显示红色错误信息

7.3 代理模式工作流

等价于 Web 端 5.3 代理模式工作流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
启动代理:
1. 用户在 Header 按 p 或进入代理控制台
2. 调用 proxyApi.startProxyServer()
3. 后端返回监听端口
4. 状态栏显示 ● 代理已启动 (绿色)
5. Logo 颜色变为绿色

切换代理供应商:
1. 用户在代理控制台按 s 或点击"切换供应商"
2. 弹出供应商列表选择(使用 bubble-list)
3. 选择后调用 proxyApi.switchProxyProvider(appType, providerId)
4. 无需重启应用,实时生效

故障转移:
1. 在路由设置页开启"自动故障转移"
2. 添加供应商到故障转移队列(通过 MCP 管理界面)
3. 代理自动监控供应商健康
4. 故障时自动切换到下一优先级供应商

7.4 键盘快捷键体系

等价于 Web 端 5.5 键盘快捷键,TUI 以键盘为中心设计:

快捷键功能等价 Web
AppSwitcher 切换应用点击 App 标签
列表/卡片导航鼠标滚轮
Enter选中/激活/确认点击
e编辑当前选中项点击编辑按钮
d删除当前选中项点击删除按钮
c复制当前选中项点击复制按钮
t切换开关(提示词/技能应用)点击 Toggle
s切换当前供应商 / 搜索点击”使用中”
p打开/进入代理控制台ProxyToggle 开关
m多选模式(会话/MCP 管理)批量管理
a添加新项添加按钮
i导入导入按钮
r刷新/恢复刷新按钮
u更新/升级更新按钮
Tab在设置页内切换标签点击 Tab
Shift+Tab反向切换标签-
/全局搜索(供应商列表)搜索框
?快捷键帮助-
Esc返回上一页 / 退出对话框/模式Escape
q退出应用-
,打开设置页Cmd+,
h在供应商列表中进入操作模式(显示操作按钮)hover 显示
1-9数字快捷选中第 N 个供应商-
w在设置页中切换到”通用”标签-
b切换到”路由”标签-
a切换到”认证”标签-
v切换到”高级”标签-
g切换到”使用统计”标签-
o切换到”关于”标签-

快捷键组织原则: - 页面级快捷键(如 , 打开设置):绑定到全局 KeyMap - 组件级快捷键(如供应商列表内的 e d c):绑定到对应 View 的 KeyMap - 使用 key.Group 组织分组,在快捷键帮助中显示 - 操作模式(hover 等效)下,卡片操作按钮绑定 1-9 数字键

7.5 实时事件监听

等价于 Web 端 5.7 实时事件推送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在 app.go 中订阅事件
func (a *App) initEventBus() {
a.ws.Subscribe("provider-switched", func(data any) {
evt := data.(ProviderSwitchedEvent)
// 刷新对应应用的供应商列表
a.store.Providers.Refresh(evt.AppType)
// 显示状态通知
a.store.PushToast(fmt.Sprintf("✓ 已切换到 %s", evt.ProviderName))
})

a.ws.Subscribe("universal-provider-synced", func(data any) {
a.store.UniversalProviders.Refresh()
})

a.ws.Subscribe("webdav-sync-status-updated", func(data any) {
a.store.PushToast(fmt.Sprintf("WebDAV: %s", data.(string)))
})
}

Toast 通知机制: - 状态栏上方显示 1-3 行 Toast 文本 - 持续显示 3 秒后淡出(通过清屏 + 部分重绘) - 错误类 Toast 显示为红色,信息类为默认色


8. 后端服务层

Go TUI 不直接操作文件系统,而是通过 RPC 调用 Rust 后端的命令。但为了减少通信开销,TUI 会在本地维护一份配置缓存。

8.1 架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
┌──────────────────────────────────────────┐
│ TUI 业务层 (service/) │
│ │
│ ProviderService │
│ ├── CRUD 操作(调用 RPC) │
│ ├── 排序维护(本地 + RPC 持久化) │
│ ├── 切换逻辑(+ WebSocket 事件监听) │
│ └── 缓存管理(内存缓存 + 失效策略) │
│ │
│ ProxyService │
│ ├── 生命周期管理 │
│ ├── 状态机(stopped → starting → running)│
│ └── 故障转移队列维护 │
│ │
│ SkillService │
│ ├── 安装/卸载/更新(带进度) │
│ └── 仓库管理 │
│ │
│ ConfigService │
│ ├── 导入/导出 │
│ ├── DB 备份/恢复 │
│ └── WebDAV 同步 │
└──────────────┬───────────────────────────┘
│ gRPC / JSON-RPC

┌──────────────────────────────────────────┐
│ 通信层 (api/) │
│ │
│ Client ──┬─ gRPC/HTTP 请求 │
│ └─ WebSocket 事件订阅 │
└──────────────┬───────────────────────────┘
│ Unix Socket / TCP

┌──────────────────────────────────────────┐
│ src-tauri (Rust 后端) │
│ │
│ Tauri Commands / gRPC Handlers │
│ ┌──────────────────────────────────┐ │
│ │ SQLite 数据库 │ │
│ │ 配置文件读写 │ │
│ │ 代理服务器 │ │
│ │ 事件广播 (WebSocket) │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────┘

8.2 缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type CacheEntry struct {
Data interface{}
ExpiresAt time.Time
}

type Cache struct {
providers map[string]*CacheEntry // key: appId
settings *CacheEntry
proxyStatus *CacheEntry
mcpServers *CacheEntry
}

// 失效策略
// - Provider 数据:变更时主动失效(CRUD 操作后)
// - 设置数据:保存后主动失效
// - 代理状态:每 5 秒自动失效(轮询)
// - WebSocket 事件触发即时失效

9. 样式与主题

9.1 颜色映射(TailwindCSS → Lip Gloss)

Web (Tailwind)TUI (Lip Gloss)ANSI
text-blue-500styles.Bluelipgloss.Color("#3b82f6")
text-emerald-500styles.Emeraldlipgloss.Color("#10b981")
text-orange-500styles.Orangelipgloss.Color("#f97316")
text-purple-500styles.Purplelipgloss.Color("#a855f7")
bg-mutedstyles.MutedBGlipgloss.Color("#374151")
text-mutedstyles.Mutedlipgloss.Color("#6b7280")
border-borderstyles.Borderlipgloss.Color("#4b5563")
border-border-activestyles.BorderActivelipgloss.Color("#9ca3af")
text-defaultstyles.Defaultlipgloss.Color("#e5e7eb")

9.2 主题支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type Theme string

const (
ThemeLight Theme = "light"
ThemeDark Theme = "dark"
ThemeSystem Theme = "system"
)

type Palette struct {
// 前景色
Default lipgloss.Color
Muted lipgloss.Color
Blue lipgloss.Color
Emerald lipgloss.Color
Orange lipgloss.Color
Purple lipgloss.Color
Red lipgloss.Color
Yellow lipgloss.Color

// 背景色
Background lipgloss.Color
MutedBG lipgloss.Color
CardBG lipgloss.Color

// 边框
Border lipgloss.Color
BorderActive lipgloss.Color

// 高亮
CurrentBorder lipgloss.Color // 当前激活供应商边框
CurrentBG lipgloss.Color // 当前激活供应商背景

// 状态
Success lipgloss.Color
Error lipgloss.Color
Warning lipgloss.Color
}

支持 256 色/真彩色检测:启动时通过 termenv 检测终端能力,自动选择调色板。

9.3 样式等价映射(CSS → Lip Gloss)

Web 样式Lip Gloss 等价
rounded-xlBorder: border.Normal()
bg-mutedBackground: palette.MutedBG
text-blue-500/60Foreground: palette.Blue(半透明)
from-blue-500/10 to-transparentBackground: palette.CurrentBG
font-semiboldBold: true
text-smWidth: 动态计算(单行字符数)
hover:border-border-activeFocus 时切换 Border: palette.BorderActive
backdrop-blur-md bg-background/80Background: palette.Background(无半透明支持,终端限制)

终端限制说明: - 终端不支持 backdrop-blurborder-radius(需使用圆角字符 ┌ ─ ┐ │ └ ─ ┘) - 不支持半透明背景色(需使用 256 色近似) - 不支持 CSS transition(通过清屏 + 帧率控制模拟淡入)


10. 关键实现要点

10.1 供应商卡片列表渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// provider_list.go 核心渲染逻辑
func (v *ProviderListView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
v.width = msg.Width
v.height = msg.Height
return v, nil
case tea.KeyMsg:
switch msg.Type {
case tea.KeyArrowDown:
v.cursor = min(v.cursor+1, len(v.cards)-1)
return v, nil
case tea.KeyArrowUp:
v.cursor = max(v.cursor-1, 0)
return v, nil
case 's':
// 切换当前供应商
return v, v.cmdSwitchProvider()
case 'e':
// 编辑当前供应商
return v, v.cmdEditProvider()
}
}
return v, nil
}

func (v *ProviderListView) View() string {
var b strings.Builder

// Header
b.WriteString(styles.Header("供应商列表"))

// AppSwitcher
b.WriteString(v.renderAppSwitcher())

// Cards
for i, card := range v.cards {
if i < v.scrollOffset || i >= v.scrollOffset+v.visibleRows {
continue
}
active := i == v.cursor
b.WriteString(v.renderCard(card, active))
}

// StatusBar
b.WriteString(styles.StatusBar(v.proxyStatus))

return b.String()
}

10.2 模态框/对话框实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 添加供应商对话框使用嵌套 Model
type AddProviderModel struct {
modal bubblesModal.Model
name textinput.Model
apiKey textinput.Model
baseURL textinput.Model
// ... 更多输入字段
selectedCategory ProviderCategory
selectedIcon string
selectedColor lipgloss.Color
}

func (m AddProviderModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "tab":
m.focusNext() // 切换到下一个输入字段
return m, nil
case "shift+tab":
m.focusPrev()
return m, nil
case "enter":
if m.atLastField() {
return m, m.cmdSave()
}
m.focusNext()
return m, nil
case "esc":
return m, cmdQuit
}
}
// 更新各个 textinput/textarea 字段
return m, nil
}

10.3 进度反馈

对于安装/同步/导入等长时间操作,使用 spinner + 状态文本:

1
2
3
4
5
6
7
8
9
10
11
12
13
func (v *SkillsView) installSkill(id string) tea.Cmd {
sp := spinner.New()
sp.Style = lipgloss.NewStyle().Foreground(styles.Yellow)
v.spinner = sp
v.loading = true
return tea.Sequence(
sp.Tick,
func() tea.Msg {
skill, err := v.api.InstallUnified(id, v.appID)
return skillInstallMsg{skill: skill, err: err}
},
)
}

10.4 终端尺寸响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Msg) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
a.width = msg.Width
a.height = msg.Height
// 根据宽度动态调整布局
if msg.Width < 80 {
a.layout = LayoutNarrow // 窄屏:单栏布局
} else {
a.layout = LayoutWide // 宽屏:双栏布局
}
}
return a, nil
}

10.5 最小终端要求

指标要求
宽度80 字符(推荐 120+)
高度24 行(推荐 40+)
颜色256 色(真彩色优先)
终端模拟器支持 VT100/ANSI 转义序列
字体等宽字体(推荐 Nerd Font 图标)

附:Web TUI 功能对等矩阵

Web 功能TUI 支持实现方式
供应商 CRUDbubble-table + modal
供应商拖拽排序~数字排序(等效功能)
一键切换供应商Enter 键
本地代理控制代理控制台视图
故障转移队列管理界面
提示词管理bubble-list + 模态框
Skills 管理bubble-list + 模态框
Skills 发现搜索 + bubble-list
会话管理双栏 bubble-list
MCP 管理bubble-list + 向导
用量统计ASCII 表格 + 图表
设置页面TabView + 表单
WebDAV 同步配置界面
配置导入导出文件路径输入
DeepLink 导入命令行参数 + 自动解析
环境变量冲突检测启动时检测 + 横幅
OAuth 登录~终端中显示设备码 URL
用量查询脚本~文本编辑器 + 测试
多应用支持AppSwitcher
主题切换明/暗主题
多语言i18n 包
快捷键帮助?