使用 go:embed 嵌入迁移 SQL 到二进制,移除 runtime.Caller 源码路径依赖, server 和 desktop 发布产物均可在无源码目录环境下完成数据库初始化和迁移。
344 lines
14 KiB
Markdown
344 lines
14 KiB
Markdown
# 桌面应用
|
||
|
||
## Purpose
|
||
|
||
TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源打包为单一可执行文件
|
||
|
||
## Requirements
|
||
|
||
### Requirement: 桌面应用启动
|
||
|
||
系统 SHALL 支持作为桌面应用启动,将后端服务与前端静态资源打包为单一可执行文件。
|
||
|
||
#### Scenario: 双击启动
|
||
|
||
- **WHEN** 用户双击桌面应用可执行文件
|
||
- **THEN** 系统使用 `gofrs/flock` 尝试获取排他文件锁
|
||
- **AND** 锁文件路径为系统临时目录下的 `nex-gateway.lock`
|
||
- **AND** 系统启动后端服务
|
||
- **AND** 系统托盘图标出现
|
||
- **AND** 浏览器自动打开 `http://localhost:9826` 显示管理界面
|
||
|
||
#### Scenario: 单实例检查
|
||
|
||
- **WHEN** 用户尝试启动第二个实例
|
||
- **THEN** 系统检测到已有实例持有文件锁
|
||
- **AND** 显示错误提示"已有 Nex 实例运行"
|
||
- **AND** 新实例退出
|
||
|
||
#### Scenario: 退出释放锁
|
||
|
||
- **WHEN** 用户点击托盘菜单"退出"
|
||
- **THEN** 系统释放文件锁
|
||
- **AND** 应用进程退出
|
||
|
||
### Requirement: 系统托盘
|
||
|
||
系统 SHALL 提供跨平台系统托盘功能,支持托盘图标和菜单。图标格式 SHALL 根据平台自动选择。
|
||
|
||
#### Scenario: 托盘图标显示
|
||
|
||
- **WHEN** 桌面应用启动成功
|
||
- **THEN** 系统根据平台加载正确的图标格式
|
||
- **AND** 在 Windows 上加载 ICO 格式图标(`assets/icon.ico`)
|
||
- **AND** 在 macOS 和 Linux 上加载 PNG 格式图标(`assets/icon.png`)
|
||
- **AND** 托盘图标 tooltip 显示 `Nex`
|
||
|
||
#### Scenario: 托盘菜单显示
|
||
|
||
- **WHEN** 用户点击托盘图标(左键或右键)
|
||
- **THEN** 显示托盘菜单
|
||
- **AND** 菜单包含"打开管理界面"选项
|
||
- **AND** 菜单包含"状态: 运行中"选项(禁用状态)
|
||
- **AND** 菜单包含"端口: 9826"选项(禁用状态)
|
||
- **AND** 菜单包含"退出"选项
|
||
|
||
#### Scenario: 打开管理界面
|
||
|
||
- **WHEN** 用户点击托盘菜单"打开管理界面"
|
||
- **THEN** 系统在浏览器中打开 `http://localhost:9826`
|
||
|
||
#### Scenario: 浏览器打开失败
|
||
|
||
- **WHEN** 系统无法打开浏览器(浏览器未安装等)
|
||
- **THEN** 托盘菜单仍可正常使用
|
||
- **AND** 用户可手动访问 `http://localhost:9826`
|
||
|
||
#### Scenario: 退出应用
|
||
|
||
- **WHEN** 用户点击托盘菜单"退出"
|
||
- **THEN** 系统优雅关闭后端服务
|
||
- **AND** 托盘图标消失
|
||
- **AND** 应用进程退出
|
||
|
||
### Requirement: 静态文件服务
|
||
|
||
系统 SHALL 通过 Gin 同时服务 API、协议代理和前端静态资源。
|
||
|
||
#### Scenario: API 请求路由
|
||
|
||
- **WHEN** 请求路径以 `/api/` 或 `/health` 开头
|
||
- **THEN** 请求由现有业务 handler 处理或返回 API 风格 404
|
||
|
||
#### Scenario: 版本接口路由
|
||
|
||
- **WHEN** desktop 模式收到 `GET /api/version` 请求
|
||
- **THEN** 请求 SHALL 由版本信息 handler 处理
|
||
- **THEN** 响应 SHALL 为 API JSON 响应
|
||
- **THEN** 请求 SHALL NOT 返回前端 `index.html`
|
||
|
||
#### Scenario: 协议代理请求路由
|
||
|
||
- **WHEN** 请求路径以 `/openai/` 或 `/anthropic/` 开头
|
||
- **THEN** 请求 SHALL 被视为协议代理请求或返回 API 风格 404
|
||
- **THEN** 请求 SHALL NOT 返回前端 `index.html`
|
||
|
||
#### Scenario: OpenAI 代理路由
|
||
|
||
- **WHEN** desktop 模式收到 `/openai/v1/chat/completions` 请求
|
||
- **THEN** 请求 SHALL 进入 ProxyHandler
|
||
- **THEN** ProxyHandler SHALL 获取 clientProtocol 为 `openai`
|
||
|
||
#### Scenario: Anthropic 代理路由
|
||
|
||
- **WHEN** desktop 模式收到 `/anthropic/v1/messages` 请求
|
||
- **THEN** 请求 SHALL 进入 ProxyHandler
|
||
- **THEN** ProxyHandler SHALL 获取 clientProtocol 为 `anthropic`
|
||
|
||
#### Scenario: 静态资源路由
|
||
|
||
- **WHEN** 请求路径为 `/assets/*`
|
||
- **THEN** 返回嵌入的前端静态资源文件
|
||
- **THEN** 请求 SHALL NOT 被协议代理路由处理
|
||
|
||
#### Scenario: PNG Favicon 路由
|
||
|
||
- **WHEN** 请求路径为 `/icon.png`
|
||
- **THEN** 返回来源于统一应用图标的 PNG favicon 资源
|
||
- **THEN** 请求 SHALL NOT 被协议代理路由处理
|
||
|
||
#### Scenario: SPA 路由回退
|
||
|
||
- **WHEN** 请求路径不匹配任何 API、协议代理或静态资源路由
|
||
- **THEN** 返回 `index.html`(支持前端 SPA 路由)
|
||
|
||
### Requirement: 端口冲突检测
|
||
|
||
系统 SHALL 在启动前检测端口是否可用。
|
||
|
||
#### Scenario: 端口可用
|
||
|
||
- **WHEN** 端口 9826 未被占用
|
||
- **THEN** 服务正常启动
|
||
|
||
#### Scenario: 端口被占用
|
||
|
||
- **WHEN** 端口 9826 已被其他程序占用
|
||
- **THEN** 显示错误提示"端口 9826 已被占用"
|
||
- **AND** 应用退出
|
||
|
||
### Requirement: 跨平台构建
|
||
|
||
系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台和架构分离或参数化,中间构建产物文件名 SHALL 保持可区分目标平台和架构,最终桌面发布资产文件名 SHALL 包含统一版本号、组件、平台、架构和格式信息。
|
||
|
||
#### Scenario: macOS 构建
|
||
|
||
- **WHEN** 执行 macOS desktop 构建命令且当前版本为 `1.2.3`
|
||
- **THEN** 系统 SHALL 生成 macOS arm64 和 amd64 桌面可执行文件
|
||
- **AND** 系统 SHALL 使用 `lipo` 生成 macOS universal 桌面可执行文件
|
||
- **AND** 系统 SHALL 生成可打包为 `.app` bundle 的 macOS desktop 产物
|
||
- **AND** 最终 macOS desktop 发布资产文件名 SHALL 包含 `1.2.3`、`macos` 和 `universal`
|
||
|
||
#### Scenario: Windows 构建
|
||
|
||
- **WHEN** 执行 Windows desktop 构建命令且当前版本为 `1.2.3`
|
||
- **THEN** 系统 SHALL 生成 Windows amd64 desktop 可执行文件
|
||
- **AND** Windows desktop 构建 SHALL 使用 `-H=windowsgui` linker flag 隐藏控制台窗口
|
||
- **AND** 最终 Windows desktop 发布资产文件名 SHALL 包含 `1.2.3`、`windows` 和 `amd64`
|
||
|
||
#### Scenario: Linux 构建
|
||
|
||
- **WHEN** 执行 Linux desktop 构建命令且当前版本为 `1.2.3`
|
||
- **THEN** 系统 SHALL 生成 Linux amd64 和 arm64 desktop 可执行文件
|
||
- **AND** Linux desktop 构建 SHALL 使用 CGO 和 GTK/AppIndicator 构建依赖
|
||
- **AND** 最终 Linux desktop 发布资产文件名 SHALL 包含 `1.2.3`、`linux` 和对应架构标识
|
||
|
||
### Requirement: Linux 桌面发布安装包
|
||
|
||
系统 SHALL 为 Linux desktop amd64 和 arm64 生成 tar.gz、AppImage、deb 和 rpm 发布安装包,并 SHALL 在安装包中包含标准桌面集成元数据。
|
||
|
||
#### Scenario: Linux desktop tar.gz 裸包
|
||
|
||
- **WHEN** 构建 Linux desktop 发布资产
|
||
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.tar.gz`
|
||
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.tar.gz`
|
||
|
||
#### Scenario: Linux desktop AppImage 包
|
||
|
||
- **WHEN** 构建 Linux desktop AppImage 发布资产
|
||
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.AppImage`
|
||
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.AppImage`
|
||
- **AND** AppImage SHALL 包含 desktop entry、应用图标和 desktop 可执行文件
|
||
- **AND** AppImage SHALL 依赖目标系统提供 GTK3、Ayatana AppIndicator 和运行 AppImage 所需的 runtime/FUSE 能力
|
||
|
||
#### Scenario: Linux desktop deb 包
|
||
|
||
- **WHEN** 构建 Linux desktop deb 发布资产
|
||
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.deb`
|
||
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.deb`
|
||
- **AND** deb 包 SHALL 将可执行文件安装到 `/usr/bin/nex`
|
||
- **AND** deb 包 SHALL 将 desktop entry 安装到 `/usr/share/applications/nex.desktop`
|
||
- **AND** deb 包 SHALL 将 hicolor 图标安装到 `/usr/share/icons/hicolor/.../apps/nex.png`
|
||
- **AND** deb 包 SHALL 声明 `libgtk-3-0`、`libayatana-appindicator3-1` 和 `xdg-utils` 运行时依赖
|
||
- **AND** deb 包 metadata 的架构字段 SHALL 使用 `amd64` 或 `arm64`
|
||
|
||
#### Scenario: Linux desktop rpm 包
|
||
|
||
- **WHEN** 构建 Linux desktop rpm 发布资产
|
||
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.rpm`
|
||
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.rpm`
|
||
- **AND** rpm 包 SHALL 将可执行文件安装到 `/usr/bin/nex`
|
||
- **AND** rpm 包 SHALL 将 desktop entry 安装到 `/usr/share/applications/nex.desktop`
|
||
- **AND** rpm 包 SHALL 将 hicolor 图标安装到 `/usr/share/icons/hicolor/.../apps/nex.png`
|
||
- **AND** rpm 包 SHALL 声明 `gtk3`、`libayatana-appindicator-gtk3` 和 `xdg-utils` 运行时依赖
|
||
- **AND** rpm 包 metadata 的架构字段 SHALL 使用 `x86_64` 或 `aarch64`
|
||
|
||
### Requirement: macOS DMG 打包
|
||
|
||
系统 SHALL 为 macOS desktop universal `.app` 生成 unsigned DMG 安装包,并 SHALL 保留 universal zip 发布资产。
|
||
|
||
#### Scenario: macOS universal zip 包
|
||
|
||
- **WHEN** 构建 macOS desktop 发布资产且当前版本为 `1.2.3`
|
||
- **THEN** 系统 SHALL 生成 `nex-desktop_1.2.3_macos_universal.zip`
|
||
- **AND** zip 包 SHALL 包含 `Nex.app`
|
||
|
||
#### Scenario: macOS universal DMG 包
|
||
|
||
- **WHEN** 构建 macOS desktop DMG 发布资产且当前版本为 `1.2.3`
|
||
- **THEN** 系统 SHALL 生成 `nex-desktop_1.2.3_macos_universal.dmg`
|
||
- **AND** DMG SHALL 包含 `Nex.app`
|
||
- **AND** DMG SHALL 包含指向 `/Applications` 的快捷方式
|
||
- **AND** DMG SHALL NOT 要求 macOS 签名或 notarization 才能完成构建
|
||
|
||
#### Scenario: macOS universal 架构校验
|
||
|
||
- **WHEN** macOS desktop universal 可执行文件生成完成
|
||
- **THEN** 系统 SHALL 验证该可执行文件包含 amd64 和 arm64 架构
|
||
|
||
### Requirement: macOS .app 打包
|
||
|
||
系统 SHALL 支持打包为 macOS `.app` bundle,并使 bundle 元数据中的版本字段来源于统一版本号而非硬编码值。
|
||
|
||
#### Scenario: .app 结构
|
||
|
||
- **WHEN** 执行 macOS 桌面打包脚本
|
||
- **THEN** 生成 `Nex.app` 目录结构
|
||
- **AND** 包含 `Contents/Info.plist` 元数据
|
||
- **AND** 包含 `Contents/MacOS/nex` 可执行文件
|
||
- **AND** 包含 `Contents/Resources/icon.icns` 图标
|
||
- **AND** `Info.plist` 中 `LSUIElement` 为 `true`(不显示 Dock 图标)
|
||
|
||
#### Scenario: bundle 版本元数据同步
|
||
|
||
- **WHEN** 当前统一版本号为 `1.2.3`
|
||
- **THEN** `Info.plist` 中 `CFBundleShortVersionString` SHALL 为 `1.2.3`
|
||
- **AND** `Info.plist` 中 `CFBundleVersion` SHALL 为 `1.2.3`
|
||
- **AND** 打包流程 SHALL NOT 使用硬编码版本值
|
||
|
||
### Requirement: Windows 原生对话框
|
||
|
||
系统 SHALL 在 Windows 上使用 `user32.dll` 的 `MessageBoxW` API 显示错误对话框,替代 `msg *` 命令。
|
||
|
||
#### Scenario: 错误提示对话框
|
||
|
||
- **WHEN** 应用在 Windows 上遇到启动错误(端口占用、配置加载失败等)
|
||
- **THEN** 使用 `MessageBoxW` 显示模态对话框
|
||
- **AND** 对话框标题栏显示应用名称
|
||
- **AND** 对话框包含错误描述文本
|
||
- **AND** 对话框显示错误图标(MB_ICONERROR)
|
||
|
||
#### Scenario: 非 Windows 平台不受影响
|
||
|
||
- **WHEN** 应用运行在 macOS 或 Linux 上
|
||
- **THEN** 错误对话框仍使用平台原有实现(osascript / zenity)
|
||
|
||
### Requirement: Linux 对话框降级策略
|
||
|
||
系统 SHALL 在 Linux 上按优先级检测并使用可用的对话框工具,确保在不同桌面环境下都能显示对话框。
|
||
|
||
#### Scenario: zenity 可用
|
||
- **WHEN** 系统检测到 `zenity` 命令可用
|
||
- **THEN** 使用 zenity 显示 GTK 风格对话框
|
||
- **AND** 错误对话框使用 `zenity --error` 命令
|
||
- **AND** 信息对话框使用 `zenity --info` 命令
|
||
|
||
#### Scenario: kdialog 可用
|
||
- **WHEN** zenity 不可用且 `kdialog` 命令可用
|
||
- **THEN** 使用 kdialog 显示 KDE 风格对话框
|
||
- **AND** 错误对话框使用 `kdialog --error` 命令
|
||
- **AND** 信息对话框使用 `kdialog --msgbox` 命令
|
||
|
||
#### Scenario: notify-send 可用
|
||
- **WHEN** zenity 和 kdialog 均不可用且 `notify-send` 命令可用
|
||
- **THEN** 使用 notify-send 显示系统通知
|
||
- **AND** 错误通知使用 `-u critical` 参数
|
||
- **AND** 信息通知使用默认参数
|
||
|
||
#### Scenario: xmessage 可用
|
||
- **WHEN** zenity、kdialog、notify-send 均不可用且 `xmessage` 命令可用
|
||
- **THEN** 使用 xmessage 显示基础 X11 对话框
|
||
- **AND** 对话框居中显示(`-center` 参数)
|
||
|
||
#### Scenario: 无对话框工具可用
|
||
- **WHEN** 所有对话框工具均不可用
|
||
- **THEN** 降级到标准错误输出
|
||
- **AND** 输出格式为 `错误: <title>: <message>`
|
||
|
||
#### Scenario: 工具检测缓存
|
||
- **WHEN** 应用启动
|
||
- **THEN** 系统检测一次可用对话框工具
|
||
- **AND** 检测结果缓存在包级变量
|
||
- **AND** 后续对话框调用直接使用缓存结果,不重复检测
|
||
|
||
### Requirement: macOS AppleScript 字符转义
|
||
|
||
系统 SHALL 对 AppleScript 对话框中的特殊字符进行转义,确保脚本正确执行。
|
||
|
||
#### Scenario: 转义反斜杠
|
||
- **WHEN** 对话框消息包含反斜杠字符 `\`
|
||
- **THEN** 转义为 `\\`
|
||
|
||
#### Scenario: 转义双引号
|
||
- **WHEN** 对话框消息包含双引号字符 `"`
|
||
- **THEN** 转义为 `\"`
|
||
|
||
#### Scenario: 多行文本处理
|
||
- **WHEN** 对话框消息包含换行符 `\n`
|
||
- **THEN** AppleScript 正确显示多行文本
|
||
|
||
### Requirement: 桌面应用打包迁移资源
|
||
|
||
桌面应用 SHALL 在打包安装后仍能访问数据库迁移资源,并 SHALL 在首次启动时完成数据库初始化和迁移。
|
||
|
||
#### Scenario: 打包安装后首次启动执行迁移
|
||
|
||
- **WHEN** 用户从 macOS DMG 安装并首次启动 `Nex.app`
|
||
- **THEN** 系统 SHALL 初始化默认配置和数据库
|
||
- **THEN** 系统 SHALL 使用打包在应用内的迁移资源执行 SQLite 迁移
|
||
- **THEN** 系统 SHALL NOT 尝试访问构建机源码路径或仓库源码路径
|
||
- **THEN** 系统 SHALL 成功启动后端服务、托盘和管理界面
|
||
|
||
#### Scenario: .app 包含运行时必需迁移资源
|
||
|
||
- **WHEN** 执行 macOS 桌面打包脚本
|
||
- **THEN** `Nex.app` SHALL 包含启动后端服务所需的数据库迁移资源
|
||
- **THEN** 迁移资源 SHALL 随应用移动到任意安装位置后仍可用
|
||
- **THEN** `.app` SHALL NOT 依赖构建目录、源码目录或 GitHub Actions runner 路径
|
||
|
||
#### Scenario: DMG 安装后运行时资源完整
|
||
|
||
- **WHEN** 用户从 DMG 将 `Nex.app` 拖入 `/Applications` 并启动
|
||
- **THEN** 应用 SHALL 能访问数据库迁移资源
|
||
- **THEN** 应用 SHALL NOT 因 `migrations/sqlite` 或 `migrations/mysql` 文件系统目录不存在而启动失败
|