1
0
Files
nex/openspec/specs/desktop-app/spec.md

534 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 桌面应用
## Purpose
TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源打包为单一可执行文件
## Requirements
### Requirement: Desktop 启动失败报告
Desktop SHALL 将无法进入可用状态的启动失败统一转换为包含阶段、用户消息和底层原因的启动错误,并通过统一报告器提示用户。
#### Scenario: 配置加载或验证失败
- **WHEN** desktop 启动时 `~/.nex/config.yaml` 无法解析或配置验证失败
- **THEN** 系统 SHALL 生成 `config` 阶段启动错误
- **THEN** 错误提示 SHALL 包含配置文件路径和失败原因
- **THEN** 应用 SHALL 退出
#### Scenario: 日志初始化失败
- **WHEN** desktop 启动时完整 logger 初始化失败
- **THEN** 系统 SHALL 生成 `logger` 阶段启动错误
- **THEN** 错误提示 SHALL 描述日志初始化失败并包含可操作的路径或权限线索
- **THEN** 应用 SHALL 退出
#### Scenario: 数据库初始化或迁移失败
- **WHEN** desktop 启动时数据库打开、连接、初始化或迁移失败
- **THEN** 系统 SHALL 生成 `database``migration` 阶段启动错误
- **THEN** 错误提示 SHALL 描述数据库初始化失败或迁移失败
- **THEN** 错误提示 SHALL NOT 暴露 MySQL password、完整 DSN 或 API key 等敏感信息
- **THEN** 应用 SHALL 退出
#### Scenario: 嵌入资源或内部组件初始化失败
- **WHEN** desktop 启动时前端嵌入资源、协议 adapter 或其他内部组件初始化失败
- **THEN** 系统 SHALL 生成对应启动阶段的启动错误
- **THEN** 错误提示 SHALL 描述应用初始化失败并建议查看日志或重新安装
- **THEN** 应用 SHALL 退出
#### Scenario: 启动失败提示降级链
- **WHEN** desktop 生成 fatal 启动错误
- **THEN** 系统 SHALL 先尝试平台系统通知
- **THEN** 系统 SHALL 在通知不可用或返回失败时尝试平台模态弹窗
- **THEN** 系统 SHALL 在模态弹窗不可用或返回失败时输出到 stderr 或可用启动日志
- **THEN** 每一次提示通道失败 SHALL 被记录但 SHALL NOT 阻止后续 fallback
#### Scenario: 提示通道调用前可用性检查
- **WHEN** desktop 启动失败报告器准备调用任一系统通知或模态弹窗通道
- **THEN** 系统 SHALL 在调用前检查该通道对应命令、系统 API 或图形会话条件是否可用
- **THEN** 系统 SHALL 在通道不可用时跳过该通道并进入下一 fallback
- **THEN** 系统 SHALL NOT 因某个通知或弹窗工具缺失而中断后续降级链
#### Scenario: 浏览器打开失败为非 fatal
- **WHEN** desktop 后端服务和托盘已启动但浏览器自动打开失败
- **THEN** 系统 SHALL 记录 warning
- **THEN** 系统 SHALL 尝试通过非 fatal 提示告知用户可手动访问 `http://localhost:<server.port>`
- **THEN** 应用 SHALL 继续运行
### Requirement: macOS 通知和对话框降级策略
系统 SHALL 在 macOS 上优先使用通知中心提示 desktop 启动错误,并在通知不可用时降级到 AppleScript 模态告警。
#### Scenario: macOS 通知可用
- **WHEN** desktop 在 macOS 上生成启动错误且通知命令可用
- **THEN** 系统 SHALL 使用 `osascript display notification` 发送系统通知
- **THEN** 通知 SHALL 包含应用名称和错误描述文本
#### Scenario: macOS 通知失败
- **WHEN** macOS 系统通知命令不可用或返回失败
- **THEN** 系统 SHALL 使用 `osascript display alert` 显示模态告警
- **THEN** 模态告警 SHALL 使用 critical 错误语义
- **THEN** 告警 SHALL 包含应用名称和错误描述文本
#### Scenario: macOS UI 提示均失败
- **WHEN** macOS 系统通知和模态告警均失败
- **THEN** 系统 SHALL 降级输出到 stderr 或可用启动日志
### Requirement: Linux 启动错误提示降级策略
系统 SHALL 在 Linux 上为 desktop 启动错误按优先级使用可用的通知或对话框工具,优先使用系统通知栏,再降级到模态弹窗,最后降级到标准错误输出。该策略 SHALL 不移除既有通用信息提示能力。
#### Scenario: notify-send 可用
- **WHEN** desktop 在 Linux 上生成启动错误且 `notify-send` 命令可用并存在图形会话
- **THEN** 使用 `notify-send` 显示系统通知
- **AND** 错误通知使用 `-u critical` 参数
- **AND** 通知 SHALL 包含应用名称和错误描述文本
#### Scenario: kdialog passive popup 可用
- **WHEN** `notify-send` 不可用或返回失败且 `kdialog` 命令可用
- **THEN** 系统 SHALL 优先尝试使用 `kdialog --passivepopup` 显示系统通知式提示
#### Scenario: zenity 可用
- **WHEN** Linux 通知工具不可用或返回失败且系统检测到 `zenity` 命令可用
- **THEN** 使用 zenity 显示 GTK 风格错误对话框
- **AND** 错误对话框使用 `zenity --error` 命令
#### Scenario: kdialog 模态对话框可用
- **WHEN** `zenity` 不可用或返回失败且 `kdialog` 命令可用
- **THEN** 使用 kdialog 显示 KDE 风格错误对话框
- **AND** 错误对话框使用 `kdialog --error` 命令
#### Scenario: xmessage 可用
- **WHEN** `zenity``kdialog` 模态对话框均不可用或返回失败且 `xmessage` 命令可用
- **THEN** 使用 xmessage 显示基础 X11 对话框
- **AND** 对话框居中显示(`-center` 参数)
#### Scenario: 无 UI 工具可用
- **WHEN** 所有通知和对话框工具均不可用、图形会话不存在或所有 UI 提示均返回失败
- **THEN** 降级到标准错误输出
- **AND** 输出格式为 `错误: <title>: <message>`
#### Scenario: 工具检测缓存
- **WHEN** desktop 在 Linux 上启动
- **THEN** 系统检测一次可用通知和对话框工具
- **AND** 检测结果缓存在包级变量
- **AND** 后续提示调用直接使用缓存结果,不重复检测
### Requirement: 桌面应用启动
系统 SHALL 支持作为桌面应用启动,将后端服务与前端静态资源打包为单一可执行文件,并使用启动配置中的 `server.port` 作为本次运行端口。
#### Scenario: 双击启动
- **WHEN** 用户双击桌面应用可执行文件
- **THEN** 系统从 `~/.nex/config.yaml` 和默认值加载启动配置快照
- **AND** 系统使用 `gofrs/flock` 尝试获取排他文件锁
- **AND** 锁文件路径为系统临时目录下的 `nex-gateway.lock`
- **AND** 系统使用启动配置中的 `server.port` 启动后端服务
- **AND** 未配置 `server.port` 时默认端口为 9826
- **AND** 系统托盘图标出现
- **AND** 浏览器自动打开 `http://localhost:<server.port>` 显示管理界面
#### Scenario: 单实例检查
- **WHEN** 用户尝试启动第二个实例
- **THEN** 系统检测到已有实例持有文件锁
- **AND** 显示错误提示"已有 Nex 实例运行"
- **AND** 新实例退出
#### Scenario: 退出释放锁
- **WHEN** 用户点击托盘菜单"退出"
- **THEN** 系统释放文件锁
- **AND** 应用进程退出
### Requirement: 系统托盘
系统 SHALL 提供跨平台系统托盘功能,支持托盘图标和菜单。图标格式 SHALL 根据平台自动选择,端口显示 SHALL 使用启动配置中的 `server.port`。托盘初始化失败 SHALL 被视为 desktop fatal 启动失败。
#### 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** 菜单包含"端口: <server.port>"选项(禁用状态)
- **AND** 菜单包含"退出"选项
#### Scenario: 打开管理界面
- **WHEN** 用户点击托盘菜单"打开管理界面"
- **THEN** 系统在浏览器中打开 `http://localhost:<server.port>`
#### Scenario: 托盘 ready 后自动打开浏览器
- **WHEN** desktop 启动并完成托盘图标、菜单和点击事件初始化
- **THEN** 系统 SHALL 将托盘标记为 ready
- **THEN** 系统 SHALL 在托盘 ready 后自动打开 `http://localhost:<server.port>`
- **THEN** 系统 SHALL NOT 在托盘初始化失败时自动打开浏览器
#### Scenario: 浏览器打开失败
- **WHEN** 系统无法打开浏览器(浏览器未安装等)
- **THEN** 托盘菜单仍可正常使用
- **AND** 用户可手动访问 `http://localhost:<server.port>`
- **AND** 系统 SHALL 尝试显示非 fatal 提示,告知用户手动访问地址
#### Scenario: 退出应用
- **WHEN** 用户点击托盘菜单"退出"
- **THEN** 系统优雅关闭后端服务
- **AND** 托盘图标消失
- **AND** 应用进程退出
#### Scenario: 托盘初始化失败
- **WHEN** desktop 启动时托盘未在限定时间内 ready、托盘图标资源无法加载或托盘菜单无法完成初始化
- **THEN** 系统 SHALL 生成 `tray` 阶段启动错误
- **THEN** 系统 SHALL 通过启动失败提示降级链提示用户
- **THEN** 应用 SHALL 退出
### 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 在启动时获取启动配置中的 `server.port` 对应监听端口,并使用同一个 listener 启动后端服务,避免端口预检测与实际监听之间的竞态。
#### Scenario: 配置端口可用
- **WHEN** 启动配置中的 `server.port` 未被占用
- **THEN** 系统 SHALL 成功创建该端口的 listener
- **THEN** 后端服务 SHALL 使用该 listener 正常启动
#### Scenario: 配置端口被占用
- **WHEN** 启动配置中的 `server.port` 已被其他程序占用
- **THEN** 系统 SHALL 生成 `port` 阶段启动错误
- **THEN** 错误提示 SHALL 包含"端口 <server.port> 已被占用"
- **AND** 应用退出
#### Scenario: 单实例优先于端口监听
- **WHEN** 用户尝试启动第二个 desktop 实例
- **THEN** 系统 SHALL 优先检测到已有实例持有文件锁
- **THEN** 系统 SHALL 显示"已有 Nex 实例运行"相关错误提示
- **THEN** 系统 SHALL NOT 将该场景误报为端口占用
### Requirement: 桌面配置源隔离和启动快照
desktop SHALL 仅在启动时从默认配置文件 `~/.nex/config.yaml` 和默认值加载运行时配置,并将加载结果作为本次运行的 immutable runtime snapshot。
#### Scenario: Desktop 仅使用默认配置文件
- **WHEN** desktop 启动
- **THEN** SHALL 从 `~/.nex/config.yaml` 加载配置
- **THEN** SHALL 在配置文件不存在时使用默认值
- **THEN** SHALL 使用默认值补齐配置文件未设置的配置项
#### Scenario: Desktop 不支持 CLI 配置源
- **WHEN** desktop 启动时传入 `--server-port 9000``--database-path /tmp/test.db``--config /tmp/custom.yaml`
- **THEN** SHALL 忽略这些参数
- **THEN** SHALL NOT 将这些参数应用到运行时配置
- **THEN** SHALL NOT 使用 `--config` 指定的配置文件路径
#### Scenario: Desktop 不支持环境变量配置源
- **WHEN** desktop 启动环境中存在 `NEX_SERVER_PORT``NEX_DATABASE_PATH``NEX_LOG_LEVEL` 或其他 `NEX_*` 环境变量
- **THEN** SHALL NOT 将这些环境变量应用到运行时配置
- **THEN** SHALL 使用默认配置文件和默认值确定运行时配置
#### Scenario: Desktop 忽略未知启动参数
- **WHEN** desktop 启动时传入未知命令行参数
- **THEN** SHALL NOT 因未知参数导致配置加载失败
- **THEN** SHALL 继续使用默认配置文件和默认值加载配置
#### Scenario: 配置文件修改仅下次启动生效
- **WHEN** desktop 已启动并正在处理请求
- **AND** 用户修改 `~/.nex/config.yaml` 中的 `server.port``database.*``log.*` 或 timeout 配置
- **THEN** 当前运行中的 desktop SHALL NOT 重新加载配置文件
- **THEN** 当前运行中的 HTTP server、数据库连接、日志器和请求处理 SHALL NOT 因配置文件修改而重建或中断
- **THEN** 修改后的配置 SHALL 在下一次 desktop 启动时生效
#### Scenario: 配置文件无效
- **WHEN** desktop 启动时 `~/.nex/config.yaml` 存在但内容无法解析或验证失败
- **THEN** SHALL 显示包含配置文件路径和失败原因的错误提示
- **THEN** SHALL 退出应用
- **THEN** SHALL NOT 静默回退默认配置继续启动
### Requirement: Desktop 前端同源 API 访问
desktop 内嵌前端 SHALL 使用同源相对路径访问后端 API不主动发现、缓存或覆盖 desktop 端口。
#### Scenario: 同源 API 请求
- **WHEN** desktop 浏览器页面打开在 `http://localhost:<server.port>`
- **THEN** 前端 SHALL 使用 `/api/*``/openai/*``/anthropic/*` 等相对路径访问同一 origin
- **THEN** 前端 SHALL NOT 硬编码 desktop 端口
#### Scenario: 重启后新端口访问
- **WHEN** 用户修改配置文件中的 `server.port` 并重启 desktop
- **THEN** desktop SHALL 按新端口启动并打开 `http://localhost:<new-port>`
- **THEN** 前端 SHALL 继续使用当前 origin 的相对路径访问 API
- **THEN** 前端 SHALL NOT 扫描端口、读取本地缓存端口或请求端口发现接口
### 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: Windows 通知优先
- **WHEN** 应用在 Windows 上遇到启动错误(端口占用、配置加载失败等)
- **THEN** 系统 SHALL 优先尝试发送 Windows 系统通知
- **THEN** 通知 SHALL 包含应用名称和错误描述文本
#### Scenario: 错误提示对话框
- **WHEN** Windows 系统通知不可用或返回失败
- **THEN** 使用 `MessageBoxW` 显示模态对话框
- **AND** 对话框标题栏显示应用名称
- **AND** 对话框包含错误描述文本
- **AND** 对话框显示错误图标MB_ICONERROR
- **AND** 对话框 SHALL 尽量置于前台或顶层显示
#### Scenario: Windows UI 提示均失败
- **WHEN** Windows 系统通知和 `MessageBoxW` 均失败
- **THEN** 系统 SHALL 记录提示失败原因
- **THEN** 系统 SHALL 降级输出到可用启动日志
#### Scenario: 非 Windows 平台不受影响
- **WHEN** 应用运行在 macOS 或 Linux 上
- **THEN** 错误提示 SHALL 使用对应平台的通知和弹窗降级策略
### Requirement: macOS AppleScript 字符转义
系统 SHALL 对 AppleScript 通知、模态告警和对话框中的特殊字符进行转义,确保脚本正确执行。
#### Scenario: 转义反斜杠
- **WHEN** AppleScript 提示文本包含反斜杠字符 `\`
- **THEN** 转义为 `\\`
#### Scenario: 转义双引号
- **WHEN** AppleScript 提示文本包含双引号字符 `"`
- **THEN** 转义为 `\"`
#### Scenario: 多行文本处理
- **WHEN** AppleScript 提示文本包含换行符 `\n`
- **THEN** AppleScript 通知或模态告警 SHALL 正确显示多行文本或保留可读换行语义
### 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` 文件系统目录不存在而启动失败