feat(desktop): implement phase 2 native capabilities
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled

This commit is contained in:
Cheng Zhou
2026-06-30 21:21:55 +08:00
parent 7c721d4e5c
commit 628ff8828b
64 changed files with 3516 additions and 222 deletions
+6 -13
View File
@@ -2,11 +2,14 @@
状态: `snapshot`
适用范围: `storage-persistence`
最后更新: `2026-02-27`
最后更新: `2026-06-30`
- 扫描目录: `/Users/zcc/MyCTMS/ctms-project/frontend/src`
- 总发现数: `46`
- 高风险: `4` / 中风险: `0` / 低风险: `42`
- 总发现数: `36`
- 高风险: `4` / 中风险: `0` / 低风险: `32`
> 2026-06-30 第二阶段更新:认证 token 已迁移到 `frontend/src/runtime/secureSessionStorage.ts`
> 业务入口不再直接从 `localStorage` 读取 `ctms_token`,附件下载不再使用 query-token。
## 明细
@@ -18,13 +21,6 @@
| high | business-draft | `frontend/src/views/admin/ProjectDetail.vue` | 2984 | `localStorage.removeItem` | `storageKey.value` | 立项配置草稿本地兜底,需显式提示未落库 |
| low | ui-preference | `frontend/src/components/Layout.vue` | 227 | `localStorage.getItem` | `"ctms_sidebar_collapsed"` | UI偏好/上下文缓存 |
| low | ui-preference | `frontend/src/components/Layout.vue` | 390 | `localStorage.setItem` | `"ctms_sidebar_collapsed"` | UI偏好/上下文缓存 |
| low | auth-session | `frontend/src/components/ThreadList.vue` | 72 | `localStorage.getItem` | `"ctms_token"` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/components/attachments/AttachmentList.vue` | 132 | `localStorage.getItem` | `"ctms_token"` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/components/attachments/AttachmentList.vue` | 137 | `localStorage.getItem` | `"ctms_token"` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/components/attachments/AttachmentList.vue` | 168 | `localStorage.getItem` | `"ctms_token"` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/components/fees/FeeAttachmentPanel.vue` | 169 | `localStorage.getItem` | `"ctms_token"` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/components/fees/FeeAttachmentPanel.vue` | 174 | `localStorage.getItem` | `"ctms_token"` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/components/fees/FeeAttachmentPanel.vue` | 223 | `localStorage.getItem` | `"ctms_token"` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/session/sessionManager.ts` | 33 | `localStorage.setItem` | `"ctms_auth_broadcast"` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/session/sessionManager.ts` | 181 | `sessionStorage.removeItem` | `LOGOUT_REASON_STORAGE_KEY` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/session/sessionManager.ts` | 184 | `sessionStorage.setItem` | `LOGOUT_REASON_STORAGE_KEY` | 认证/会话数据(非业务主数据) |
@@ -49,9 +45,6 @@
| low | ui-preference | `frontend/src/store/study.ts` | 167 | `localStorage.removeItem` | `STUDY_ROLE_KEY` | UI偏好/上下文缓存 |
| low | ui-preference | `frontend/src/store/study.ts` | 174 | `localStorage.setItem` | `SITE_KEY` | UI偏好/上下文缓存 |
| low | ui-preference | `frontend/src/store/study.ts` | 176 | `localStorage.removeItem` | `SITE_KEY` | UI偏好/上下文缓存 |
| low | auth-session | `frontend/src/utils/auth.ts` | 4 | `localStorage.getItem` | `TOKEN_KEY` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/utils/auth.ts` | 7 | `localStorage.setItem` | `TOKEN_KEY` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/utils/auth.ts` | 11 | `localStorage.removeItem` | `TOKEN_KEY` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/utils/auth.ts` | 17 | `localStorage.setItem` | `CREDENTIAL_KEY` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/utils/auth.ts` | 24 | `localStorage.getItem` | `CREDENTIAL_KEY` | 认证/会话数据(非业务主数据) |
| low | auth-session | `frontend/src/utils/auth.ts` | 38 | `localStorage.removeItem` | `CREDENTIAL_KEY` | 认证/会话数据(非业务主数据) |
+160
View File
@@ -0,0 +1,160 @@
# CTMS 桌面端第二阶段执行设计
## 范围与边界
第二阶段基于已合入 `dev` 的 Tauri 基线推进。每个评审单元从最新 `dev` 创建短期 `codex/*` 分支,合入后删除,不建立长期桌面主线。
本阶段只增强在线桌面客户端能力:
- 安全运行时与诊断。
- 原生文件选择、保存、打开。
- 服务端持久系统通知。
- 签名更新、发布流水线与 Windows 打包准备。
本阶段明确不实现离线登录、离线缓存、本地业务数据库、本地业务队列、后台业务同步或本地权限裁决。所有业务数据、权限、审计和认证判定仍以 FastAPI 后端为准。
## 运行时边界
共享业务模块只能通过 `frontend/src/runtime/` 使用平台能力。非 runtime 模块不得直接导入 Tauri API。
`ClientRuntime` 第二阶段能力包括:
- `secureSessionStorage`
- `files`
- `notifications`
- `updates`
- `capabilities.secureSessionStorage/nativeFiles/systemNotifications/automaticUpdates`
能力标志只在对应运行时可用或初始化成功后开启。Web 端继续使用浏览器能力,不申请系统通知权限,不启动桌面通知轮询,也不触发 updater。
## 安全会话存储
桌面端 token 存入系统凭据库:
- macOSKeychain。
- WindowsCredential Manager。
Rust 仅暴露固定 service 下的读取、写入、删除命令。凭据 account 使用规范化服务端 origin 的 SHA-256,避免明文服务端地址散落在系统凭据项名称中。
应用挂载前异步初始化 token
1. Web 端继续读取 `localStorage.ctms_token`
2. 桌面端先删除 legacy `localStorage.ctms_token`
3. 若 legacy token 仍有效,则迁移到系统凭据库。
4. 若迁移或读取凭据失败,则内存 token 置空并要求重新登录,不回退明文存储。
登出、服务器切换、认证失效时必须同步清除内存 token 和当前服务端 origin 对应的系统凭据。
## 原生文件能力
公共接口固定为:
```ts
pickFiles(options): Promise<File[]>
saveFile({ suggestedName, mimeType, data }): Promise<"saved" | "cancelled">
openFile({ suggestedName, mimeType, data }): Promise<void>
```
桌面端使用 Tauri dialog、fs、opener 插件。权限范围只覆盖用户当次选择的文件路径和 `$TEMP/ctms-desktop/**`,不启用 persisted-scope,不开放目录遍历或 shell。外部打开文件时写入随机临时目录,并使用净化后的文件名;启动和退出时清理临时目录,用户主动保存的文件不自动删除。
下载与预览统一先通过 Axios Bearer 请求取得 Blob,再交给文件适配器。附件全局下载接口只接受 `Authorization` header,不再接受 `?token=`
## 系统通知
通知订阅由用户在个人设置中主动开启。开启时才请求 OS 权限并创建服务端订阅;`enabled_at` 设为当前时间,不补发历史分发记录。关闭后客户端停止领取通知,重新开启时重新设定起点。
后端新增:
- `desktop_notification_subscriptions`
- `desktop_notification_deliveries`
API
- `GET/PUT /api/v1/desktop-notifications/subscription`
- `POST /api/v1/desktop-notifications/claim`
- `POST /api/v1/desktop-notifications/ack`
- `POST /api/v1/desktop-notifications/{distribution_id}/read`
claim 跨有效项目查询匹配当前用户或角色的活动文件分发,使用五分钟租约、唯一约束和事务防重。客户端显示系统通知后 ack;显示失败则等待租约到期后重试。
系统通知正文只显示通用内容:
- 标题:`CTMS 文件更新`
- 正文:`有新的文件版本待查看`
项目、文件、版本等详细信息仅在应用内列表显示,避免锁屏泄露。
## 诊断元数据与 CORS
Axios 请求附加:
- `X-CTMS-Client-Type`
- `X-CTMS-Client-Version`
- `X-CTMS-Client-Platform`
- `X-CTMS-Build-Channel`
- `X-CTMS-Build-Commit`
后端安全访问日志保存这些 nullable 字段,并支持按客户端类型/版本筛选。这些字段只用于诊断与排障,不参与授权。
CORS origin 由环境变量白名单控制,并显式允许桌面 origin、开发 origin 和上述请求头。
Tauri CSP 禁止远程脚本和 shell 入口,仅允许 HTTPS API、本地开发地址、blob 预览与必要资源。
## 单实例
单实例插件必须最先注册。重复启动时只恢复、显示并聚焦主窗口,不处理命令行参数、深链或业务动作。
## 自动更新与发布
正式 release 构建启用 updater。客户端从当前 CTMS origin 派生固定清单路径:
```text
/desktop-updates/stable/latest.json
```
生产只允许 HTTPS;本地测试只允许 localhost HTTP。
更新检查策略:
- 启动延迟 30 秒检查。
- 之后每 6 小时检查。
- 发现更新时展示版本和发布说明。
- 用户确认后下载、验签、安装并重启。
- 用户选择“稍后”后,同版本 24 小时内不再提示。
- 不做静默安装,不中断正在录入的业务流程。
Tauri 配置必须嵌入 updater 公钥并生成 updater artifacts。生产私钥只能存放在组织密钥库或 CI secret,不进仓库。macOS 更新制品为 `.app.tar.gz``.sig`
release tag 流水线应从同一提交构建 Web 与桌面端:
1. 校验 `frontend/package.json`、Tauri 配置、Cargo manifest/lock 版本一致。
2. 构建 macOS Universal。
3. 完成 Apple 签名和公证。
4. 使用 updater 私钥签名更新包。
5. 生成 DMG、更新包、签名、`latest.json` 与校验清单。
6. 先上传不可变制品,最后原子替换 `latest.json`
`latest.json` 同时提供 `darwin-aarch64``darwin-x86_64`,指向同一个 Universal 更新制品。
## Windows 准备
第二阶段只验证 Windows x64 NSIS 构建兼容,不发布正式 Windows 安装包。
验证范围:
- Windows Credential Manager。
- 路径净化与临时目录限制。
- 系统通知编译兼容。
- updater 编译兼容。
- WebView2 前置条件。
- 用户级安装假设。
- 后续代码签名要求。
## 验收重点
- `localStorage`、URL、日志和系统通知正文不包含 token。
- 附件下载不再接受 query-token。
- 业务模块不直接导入 Tauri API。
- Web 构建、类型检查和单测不回归。
- macOS `.app` 构建可重复。
- Keychain 会话、文件入口、通知权限、重复启动聚焦和签名更新链路完成端到端验证。
+5 -2
View File
@@ -67,6 +67,8 @@
第二阶段在不改变在线优先产品边界的前提下,增加原生桌面能力。
详细方案见 [`desktop-phase-2-design.md`](desktop-phase-2-design.md)。
范围:
- 在附件等场景中引入原生文件选择、下载、打开能力。
@@ -124,9 +126,10 @@
桌面端工作在以下位置开发:
- Worktree`/Users/zcc/MyCTMS/ctms-dev/worktrees/ctms-desktop`
- 分支:`codex/ctms-desktop`
- 短期分支:`codex/<任务名称>`,必须从最新 `dev` 创建
除非发布计划另有说明,桌面端分支应持续与 `dev` 对齐。
`codex/ctms-desktop` 仅是第一阶段 Tauri 基线临时集成分支。基线合入
`dev` 后,第二阶段工作不得继续把它作为长期桌面主线。
## 每次开发前必须执行的检查
+55 -3
View File
@@ -26,11 +26,15 @@ The runtime contract currently provides:
- app version, source commit, build channel, and client type metadata
- explicit capability flags
- Desktop server address configuration
- secure session storage
- file picker/save/open adapters
- desktop system notification adapters
- desktop updater adapters
Business modules must use this public runtime entry point. They must not inspect
Tauri globals or import Tauri packages directly. Native files, notifications,
secure session storage, and automatic updates remain disabled until their
second-phase adapters are implemented and reviewed.
secure session storage, and automatic updates must remain behind
`frontend/src/runtime/` and explicit capability flags.
## Version Change
@@ -71,6 +75,51 @@ VITE_BUILD_COMMIT="$(git rev-parse HEAD)"
These values support diagnostics but do not replace the semantic version.
## Desktop Updater
Formal Desktop release builds must use Tauri updater signatures. The application
embeds the updater public key in `frontend/src-tauri/tauri.conf.json`; the
private key must live only in the organization key vault or CI secret store.
Runtime update checks derive the feed from the currently configured CTMS origin:
```text
/desktop-updates/stable/latest.json
```
Production update feeds must use HTTPS. Local update testing may use HTTP only
for `localhost`, `127.0.0.1`, or `::1`.
The release pipeline must:
1. build from the accepted release tag and commit;
2. build macOS Universal desktop artifacts;
3. sign and notarize the macOS app;
4. produce updater artifacts and `.sig` files with the updater private key;
5. generate `latest.json` and a checksum manifest;
6. upload immutable artifacts first;
7. atomically replace `latest.json` last.
For Universal macOS artifacts, `latest.json` must provide both
`darwin-aarch64` and `darwin-x86_64` entries pointing at the same Universal
update package.
## Windows Build Readiness
Second-phase Windows work is limited to CI compatibility validation. A
`windows-latest` x64 NSIS build may be produced for verification, but it is not
a formal deliverable until Windows code signing and release support are
approved.
Windows validation must cover:
- WebView2 runtime prerequisite behavior;
- user-level installer assumptions;
- Windows Credential Manager session storage;
- path handling and temporary file cleanup;
- notification and updater compilation;
- future code-signing requirements.
## Required Checks
```bash
@@ -81,9 +130,12 @@ npm run runtime:check
npm run type-check
npm run test:unit
npm run build
export TAURI_SIGNING_PRIVATE_KEY="$UPDATER_PRIVATE_KEY"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$UPDATER_PRIVATE_KEY_PASSWORD"
npm run desktop:build -- --bundles app
```
The Desktop build must run on macOS for the current first-phase target. A signed
or notarized public release additionally requires the Apple credentials defined
by the release owner; an unsigned internal build is not a formal distribution.
by the release owner. A formal second-phase desktop release also requires the
updater signing key; unsigned internal builds are not formal distributions.