release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
This commit is contained in:
+2
-1
@@ -4,8 +4,9 @@ CTMS 文档入口只展示当前仍会影响开发、发布和运维决策的内
|
||||
|
||||
## 当前约束
|
||||
|
||||
- [`desktop-project-plan.md`](desktop-project-plan.md): 桌面端 Tauri 项目边界、阶段计划与必读约束
|
||||
- [`../AGENTS.md`](../AGENTS.md): Agent 执行约束、桌面端当前边界、分支治理和常用质量门禁
|
||||
- [`desktop-phase-1-design.md`](desktop-phase-1-design.md): 桌面端第一阶段 macOS 在线客户端详细方案
|
||||
- [`desktop-local-cache-plan.md`](desktop-local-cache-plan.md): 桌面端在线辅助缓存、请求去重、失效和后续持久化缓存边界
|
||||
- [`branch-governance.md`](branch-governance.md): 长期分支治理规则
|
||||
- [`guides/release-checklist.md`](guides/release-checklist.md): 发布前检查项与回归门禁
|
||||
- [`guides/client-release.md`](guides/client-release.md): Web/桌面端统一版本、构建与发布流程
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
状态: `active`
|
||||
适用范围: Web 与 macOS Desktop 统一客户端发布
|
||||
最后更新: `2026-07-01`
|
||||
最后更新: `2026-07-14`
|
||||
|
||||
本清单用于第一、二阶段桌面端能力完成后的准发布稳定化。它不引入离线登录、本地业务数据存储、内嵌后端服务或离线同步。
|
||||
本清单用于第一、二阶段桌面端能力完成后的准发布稳定化。当前允许按 `docs/desktop-local-cache-plan.md` 引入在线辅助本地缓存,但不引入离线登录、离线写入、本地业务权威数据、内嵌后端服务或离线同步。
|
||||
|
||||
## 1. 发布链路门禁
|
||||
|
||||
@@ -28,10 +28,12 @@ npm run desktop:build:app
|
||||
|
||||
- [ ] `frontend/package.json`、`package-lock.json`、Tauri 配置、Cargo manifest/lock 版本一致。
|
||||
- [ ] `VITE_BUILD_CHANNEL=release` 和 `VITE_BUILD_COMMIT=<release tag commit>` 由 CI 注入,且 `npm run release:env:check` 通过。
|
||||
- [ ] 在正式 release tag 和签名环境中执行 `npm run desktop:release-readiness:check`,确认 tag、构建元数据、签名/公证变量、updater 私钥和生产 artifact HTTPS 基址齐备。
|
||||
- [ ] macOS app 已签名和公证。
|
||||
- [ ] updater `.sig` 使用组织 CI secret 或密钥库中的私钥生成,私钥未进入仓库。
|
||||
- [ ] 设置 `TAURI_SIGNING_PRIVATE_KEY`、`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 和 Apple 签名/公证变量后,以 `REQUIRE_DESKTOP_SIGNING=true` 再次执行 `npm run release:env:check`,随后执行 `npm run desktop:build -- --bundles app`。
|
||||
- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:check -- --feed <latest.json> --artifacts-dir <artifact-dir>`。
|
||||
- [ ] 设置 `TAURI_SIGNING_PRIVATE_KEY`、`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 和 Apple 签名/公证变量后,以 `REQUIRE_DESKTOP_SIGNING=true` 再次执行 `npm run release:env:check`,随后执行 `npm run desktop:build:macos-release -- --ci`。
|
||||
- [ ] 正式 updater feed 先执行 `npm run desktop:update-feed:create -- --artifact <CTMS.app.tar.gz> --base-url <versioned-https-artifact-prefix> --output-dir <release-dir>` 生成 `latest.json` 与 `SHA256SUMS.txt`。
|
||||
- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir>`,并确认 checksum manifest、updater artifact、`.sig` 和 `latest.json` 均通过校验。
|
||||
- [ ] 不可变制品先上传,`latest.json` 最后原子替换;若 feed 校验未通过,不替换线上 `latest.json`。
|
||||
- [ ] Web 与 Desktop 制品记录同一产品版本、Git 标签和完整提交 SHA。
|
||||
|
||||
@@ -42,20 +44,39 @@ npm run desktop:build:app
|
||||
- [ ] Tauri bundle 启用 `app`、`dmg` 和 updater artifacts。
|
||||
- [ ] updater public key 已配置。
|
||||
- [ ] CSP 禁止 wildcard source、`unsafe-eval`、宽泛 HTTP API 访问和 `object-src`。
|
||||
- [ ] `script-src` 保持仅允许自身;ONLYOFFICE 仅在 `frame-src` 增加 HTTPS 和 localhost/127.0.0.1 开发来源。
|
||||
- [ ] ONLYOFFICE frame 地址只由 `frontend/src/runtime/onlyoffice.ts` 使用已校验服务器 origin 与固定 `/onlyoffice-host.html` 生成。
|
||||
- [ ] ONLYOFFICE 宿主消息同时校验 `origin`、`source` 和一次性内存 nonce,配置不进入本地缓存或请求去重。
|
||||
- [ ] Tauri capability 不包含 shell 权限、持久文件系统 scope 或宽泛目录读写。
|
||||
- [ ] 文件系统与 opener scope 只允许 `$TEMP/ctms-desktop/**`。
|
||||
- [ ] Tauri capability 不包含 `remote` 授权,远程 ONLYOFFICE frame 无文件、通知、对话框或 opener 权限。
|
||||
- [ ] 文件系统仅开放临时文件所需的 read/write/mkdir/remove 命令,且文件系统与 opener scope 只允许 `$TEMP/ctms-desktop/**`。
|
||||
- [ ] 单实例插件先于其他桌面插件注册。
|
||||
- [ ] Tauri command 白名单仅包含凭据和更新命令。
|
||||
- [ ] macOS 首个顶层 submenu 为应用菜单,包含关于、设置、服务、隐藏和退出;文件菜单保持独立。
|
||||
- [ ] macOS 红色按钮保持真正关闭窗口的系统语义,Dock/Finder reopen 事件在需要时重建、显示并聚焦主窗口。
|
||||
- [ ] 桌面快捷键只通过受控 command 同步到原生菜单,不向 WebView 开放原始窗口或菜单控制权限。
|
||||
- [ ] 桌面主题支持跟随系统、明亮和暗黑,并通过受控 command 同步窗口外观。
|
||||
- [ ] Tauri command 白名单仅包含凭据、更新和已审计的桌面 UI 窄命令。
|
||||
- [ ] 前端源码不通过 query string 传递 token。
|
||||
- [ ] `ctms_token` 只允许由 `secureSessionStorage` 处理。
|
||||
- [ ] 登录表单密码不写入 `localStorage` 或 `sessionStorage`;Web 端只使用浏览器凭据管理能力,Desktop 端只使用系统凭据库。
|
||||
- [ ] 本地缓存能力只通过 `frontend/src/runtime/desktopDataCache.ts` 或后续同名 runtime 入口暴露,业务模块不直接调用 Tauri 存储 API、SQLite、IndexedDB、Cache Storage 或文件系统。
|
||||
- [ ] 本地缓存命名空间包含 server origin、user id 和 cache schema version。
|
||||
- [ ] 本地缓存不保存 token、密码、Authorization/Bearer 文本、下载凭据、临时授权 URL 或系统通知正文。
|
||||
- [ ] 系统通知只能通过 `frontend/src/runtime/notifications.ts` 发送,标题和正文保持通用。
|
||||
- [ ] 通知 capability 只暴露权限查询、权限请求和发送通知,不使用 `notification:default`。
|
||||
- [ ] opener capability 只允许打开 `$TEMP/ctms-desktop/**` 下的临时文件,不开放 URL 或 reveal 权限。
|
||||
- [ ] updater capability 不直接暴露给 WebView,自动更新只走受控 Tauri command。
|
||||
- [ ] 更新弹窗 release notes 过滤 URL、token 查询参数和 Authorization/Bearer 形态文本。
|
||||
- [ ] CI release 候选 workflow 包含 version/runtime/desktop/ui/type/unit/build/desktop app smoke 门禁。
|
||||
- [ ] signed macOS release candidate workflow 只允许从 `vX.Y.Z` tag 运行,并包含签名环境检查、Universal macOS 构建、update feed 生成、checksum 校验和 verified release directory 上传。
|
||||
|
||||
人工复审还必须确认:
|
||||
|
||||
- [ ] token 不出现在 URL、日志、系统通知正文、下载链接或持久化业务缓存中。
|
||||
- [ ] 密码不出现在 URL、日志、系统通知正文、诊断信息或明文浏览器存储中。
|
||||
- [ ] 桌面端通知正文只显示通用内容,不包含项目、文件或版本详情。
|
||||
- [ ] 服务端权限、审计和业务数据持久化仍由 FastAPI 后端裁决。
|
||||
- [ ] 本地缓存只作为服务端响应副本,mutation、审批、权限判断和审计判断仍以后端结果为准。
|
||||
- [ ] Web 运行时不直接导入 Tauri API。
|
||||
|
||||
## 3. 端到端回归矩阵
|
||||
@@ -63,16 +84,27 @@ npm run desktop:build:app
|
||||
| 场景 | Web | macOS Desktop | 预期 |
|
||||
| --- | --- | --- | --- |
|
||||
| 登录与项目恢复 | 必测 | 必测 | 登录成功后恢复可访问项目;401 后重新登录 |
|
||||
| 记住密码 | 必测 | 必测 | Web 使用浏览器凭据管理/自动填充;Desktop 使用系统凭据库;未勾选时不继续写入保存密码 |
|
||||
| 30 天免登录 | 不适用 | 必测 | 关闭并重启 App 后复用系统凭据库中的后端在线会话;超过 30 天或 `/me` 校验失败后重新登录 |
|
||||
| 服务器地址未配置 | 不适用 | 必测 | 自动进入服务器设置,不进入业务页 |
|
||||
| 服务器地址切换 | 不适用 | 必测 | 清除当前会话和项目上下文,要求重新登录 |
|
||||
| 服务端不可达 | 必测 | 必测 | 显示可恢复错误,不进入离线模式 |
|
||||
| 本地缓存命中 | 必测 | 必测 | `/me` 校验通过后可先展示缓存再后台刷新 |
|
||||
| 本地缓存清理 | 必测 | 必测 | 登出、切换服务器、切换用户、401/403 后旧命名空间不可读取 |
|
||||
| mutation 缓存失效 | 必测 | 必测 | 新增、编辑、删除或审批成功后相关列表、详情、统计和图表缓存失效 |
|
||||
| 附件上传 | 必测 | 必测 | Web 使用浏览器文件选择,Desktop 使用原生选择 |
|
||||
| 附件下载/保存/打开 | 必测 | 必测 | 使用 Authorization header;无 `?token=` |
|
||||
| Office 附件/版本只读预览 | 必测 | 必测 | 独立全幅页面;不下载到本地;无编辑、复制、打印、下载入口;标签失活和服务器切换立即销毁 |
|
||||
| ONLYOFFICE 服务不可用 | 必测 | 必测 | 显示可重试错误,不回退 iframe 或自动下载;原保存/打开功能不受影响 |
|
||||
| 临时文件清理 | 不适用 | 必测 | 启动时清理 `$TEMP/ctms-desktop/**` |
|
||||
| 系统通知开启 | 不适用 | 必测 | 用户主动开启后请求 OS 权限并创建订阅 |
|
||||
| 系统通知拒绝 | 不适用 | 必测 | 开关回退,提示系统权限未开启 |
|
||||
| 通知领取与 ack | 不适用 | 必测 | 显示成功后 ack;失败等待租约重试 |
|
||||
| 单实例重复启动 | 不适用 | 必测 | 恢复、显示并聚焦主窗口 |
|
||||
| 关闭窗口后 Dock 重开 | 不适用 | 必测 | 红色关闭按钮真正关闭主窗口;点击 Dock 后重建、显示并聚焦 |
|
||||
| 原生应用菜单 | 不适用 | 必测 | CTMS、文件、编辑、显示、导航、窗口和帮助菜单顺序及职责符合 macOS 习惯 |
|
||||
| 自定义快捷键同步 | 不适用 | 必测 | 设置修改后原生菜单立即显示并响应新的后退、前进和刷新快捷键 |
|
||||
| 跟随系统主题 | 不适用 | 必测 | 系统明暗模式变化时 WebView、标题栏和系统控件同步更新 |
|
||||
| 自动更新检查 | 不适用 | 必测 | release 通道按当前 CTMS origin 派生清单 |
|
||||
| 更新稍后提醒 | 不适用 | 必测 | 同版本 24 小时内不重复提示 |
|
||||
| 更新安装失败 | 不适用 | 必测 | 不打断业务录入,显示可排障错误 |
|
||||
@@ -80,20 +112,26 @@ npm run desktop:build:app
|
||||
## 4. 桌面体验验收
|
||||
|
||||
- [ ] 登录页显示当前桌面服务器地址,长 URL 不撑破登录面板。
|
||||
- [ ] 30 天免登录仍只保存系统凭据库会话记录,不把 token 写入 URL、日志、通知正文或业务缓存。
|
||||
- [ ] 记住密码与 30 天免登录使用独立凭据记录;服务器切换后不复用旧服务器保存的密码。
|
||||
- [ ] 服务器设置页显示当前服务器、连接检查状态、HTTP 错误、超时和网络失败原因。
|
||||
- [ ] 个人中心显示客户端类型、版本、平台、构建通道、提交、服务器和能力状态。
|
||||
- [ ] 个人中心可复制诊断信息,内容不包含 token 或业务敏感数据。
|
||||
- [ ] 系统偏好或个人中心提供本地缓存记录数、容量和最近清理时间,且支持手动清理。
|
||||
- [ ] 通知开关显示 OS 权限状态。
|
||||
- [ ] 手动检查更新能反馈“已是最新版本”、未启用更新或检查失败。
|
||||
- [ ] 关键弹窗、表单、按钮在最小窗口尺寸 `1180x760` 下不重叠、不溢出。
|
||||
- [ ] “设置…”位于 macOS CTMS 应用菜单并使用 `⌘,`;退出位于应用菜单而不是文件菜单。
|
||||
- [ ] “显示”和“导航”菜单中的快捷键与桌面偏好保存值一致。
|
||||
- [ ] 更新弹窗只显示版本、发布日期和通用 release notes,不展示 token、下载链接或业务详情。
|
||||
|
||||
## 5. 不允许项
|
||||
|
||||
- [ ] 不实现离线登录、离线浏览、离线队列或离线同步。
|
||||
- [ ] 不在桌面端保存 CTMS 业务数据副本。
|
||||
- [ ] 不内嵌 FastAPI、PostgreSQL、SQLite 或本地业务 API 镜像。
|
||||
- [ ] 不绕过后端做本地权限裁决或本地审计回放。
|
||||
- [ ] 不实现离线登录、离线写入、离线队列或离线同步。
|
||||
- [ ] 不在 `/me` 校验通过前展示业务缓存。
|
||||
- [ ] 不把本地缓存作为 CTMS 业务权威数据或本地优先数据源。
|
||||
- [ ] 不内嵌 FastAPI、PostgreSQL 或本地业务 API 镜像。
|
||||
- [ ] 不绕过后端做本地权限裁决、本地审计判定或审计回放。
|
||||
|
||||
## 6. 2026-07-01 收尾验证记录
|
||||
|
||||
@@ -125,3 +163,101 @@ npm run desktop:build:app
|
||||
- 签名后的 updater artifacts、`.sig`、checksum manifest 和 `latest.json` 在真实发布目录内通过 `npm run desktop:update-feed:check`。
|
||||
- 不可变制品上传完成后,再原子替换线上 `latest.json`。
|
||||
- Desktop 端到端人工回归矩阵、最小窗口体验验收和系统通知/自动更新真实环境验证。
|
||||
|
||||
## 7. 2026-07-02 发布稳定化推进记录
|
||||
|
||||
本次推进补齐了发布链路自动化,不改变桌面端产品边界:
|
||||
|
||||
- 新增 `npm run desktop:build:macos-release`,封装 Universal macOS `app`/`dmg` release candidate 构建命令。
|
||||
- 新增 `npm run desktop:update-feed:create`,从签名 updater artifact 和 `.sig` 生成 `latest.json`、复制发布目录文件并生成 `SHA256SUMS.txt`。
|
||||
- `npm run desktop:update-feed:check` 在传入 `--artifacts-dir` 时要求并校验 `SHA256SUMS.txt`。
|
||||
- 新增 `npm run desktop:release-readiness:check`,在正式签名候选构建前检查 release tag、构建元数据、签名/公证变量、updater 私钥和生产 artifact HTTPS 基址。
|
||||
- 新增 `.github/workflows/desktop-release-candidate.yml`,在 release tag 上执行签名候选构建、feed 生成、feed 校验并上传 verified release directory。
|
||||
- `npm run desktop:release:check` 已检查上述脚本和 workflow,避免发布链路回退。
|
||||
|
||||
仍未自动完成、正式发布前必须人工确认:
|
||||
|
||||
- Apple Developer 凭据、证书、签名身份、公证结果和组织 updater 私钥。
|
||||
- 生产下载源的不可变制品上传和线上 `latest.json` 原子替换。
|
||||
- 真实环境下的自动更新安装、系统通知、单实例和完整人工回归。
|
||||
|
||||
## 8. 2026-07-02 端到端回归优化记录
|
||||
|
||||
本轮端到端优化仍保持在线桌面客户端边界,不引入离线、本地业务存储或本地权限裁决。
|
||||
|
||||
已完成的自动化收口:
|
||||
|
||||
- 服务器地址切换时,桌面设置页调用 `auth.logout({ rememberCurrentStudy: false })`,避免退出时把旧服务器项目记入当前用户的最近项目;随后继续清除当前项目上下文。
|
||||
- 系统偏好连接页与独立服务器设置页保持同一切换服务器语义,切换时不记忆旧服务器项目并清除当前项目上下文。
|
||||
- 系统通知轮询在部分通知显示失败或系统通知未实际发起时,先 ack 已成功发起系统通知的通知,再让失败项通过租约重试,贴合“显示成功后 ack;失败等待重试”的回归预期。
|
||||
- 自动更新管理器新增稍后提醒 24 小时抑制、安装失败可重试、检查失败不打断业务和未启用更新状态的单元覆盖。
|
||||
- 附件 API 新增 blob 下载、multipart 上传和删除端点单元覆盖,确保下载凭据继续由 axios Authorization header 承载而不是进入 URL。
|
||||
- 文件任务反馈 helper 新增选择、保存、取消保存和打开的单元覆盖,约束桌面保存/打开继续走 `frontend/src/runtime/` 适配层。
|
||||
- Keychain/凭据库会话新增旧浏览器 token 迁移、未配置服务器不读取凭据、本地 30 天上限、服务器切换删除旧服务器凭据,以及恢复 token 必须先经 `/me` 校验的单元覆盖。
|
||||
- 桌面发布门禁新增单实例重复启动处理校验,要求重复启动时恢复、显示并聚焦 `main` 窗口。
|
||||
- 会话刷新后的跨窗口 token 更新不再写入 `localStorage` fallback,只通过内存态 BroadcastChannel 通知,避免 token 进入明文广播缓存。
|
||||
- `desktop:release:check` 新增 session broadcast 静态门禁,防止 `TOKEN_UPDATED` payload 回退写入 `localStorage`。
|
||||
- 新增相关单元测试覆盖服务器切换不记忆旧项目、通知权限未授权不领取、部分通知失败时只 ack 成功项、系统通知未发起时不 ack、自动更新失败恢复路径、附件文件流契约、30 天在线会话恢复边界、单实例恢复行为和 token 广播存储边界。
|
||||
|
||||
仍需人工或真实环境验证:
|
||||
|
||||
- Keychain/凭据库 30 天在线会话恢复。
|
||||
- 原生附件上传、下载、保存和打开。
|
||||
- 系统通知拒绝路径的 OS 级交互。
|
||||
- 单实例重复启动聚焦主窗口。
|
||||
- 签名 release 构建下的自动更新 feed、验签、安装和重启实物流。
|
||||
|
||||
## 9. 2026-07-02 安全边界复审记录
|
||||
|
||||
本轮安全复审在端到端自动化收口之后推进,不改变桌面端在线客户端边界。
|
||||
|
||||
已完成的安全边界收口:
|
||||
|
||||
- Tauri notification capability 从 `notification:default` 收敛为 `notification:allow-is-permission-granted`、`notification:allow-request-permission` 和 `notification:allow-notify`。
|
||||
- `desktop:release:check` 新增 capability 最小化约束,拒绝 `notification:default`、opener URL/reveal 权限和 WebView 直连 updater 权限。
|
||||
- `desktop:release:check` 将 token URL 检查扩展到 `token` 与 `access_token` 查询参数,并扩大日志敏感词检查到 `token`、`access_token`、`authorization` 和 `bearer`。
|
||||
- 更新弹窗 release notes 增加清理逻辑,过滤 URL、token 查询参数、`access_token`、`Authorization` 和 `Bearer` 形态文本,避免 feed 内容把下载链接或凭据样式文本带入 UI。
|
||||
- 凭据库 Rust 单测新增带凭据 server origin 拒绝,以及 Keychain/Credential Manager account 不暴露原始服务器 origin 的覆盖。
|
||||
|
||||
仍需人工复审确认:
|
||||
|
||||
- 真实生产 release notes 内容保持通用,不写入项目、文件、下载链接或敏感业务详情。
|
||||
- 正式签名、公证和 updater feed 环境继续使用组织 secret,不在日志、artifact 或配置中泄露私钥材料。
|
||||
|
||||
## 10. 2026-07-02 桌面体验收口记录
|
||||
|
||||
本轮体验收口聚焦登录、服务器设置、个人中心诊断、系统偏好和最小窗口布局稳定性,不改变业务能力边界。
|
||||
|
||||
已完成的体验收口:
|
||||
|
||||
- 个人中心新增只读客户端诊断信息,展示客户端类型、版本、平台、构建通道、提交、服务器和能力状态,并支持复制诊断信息。
|
||||
- 登录页在桌面最小窗口附近收紧左右分栏 padding 和卡片宽度,长服务器地址继续在登录面板内换行,不撑破布局。
|
||||
- 服务器设置页增加面板内滚动和健康检查 URL 换行约束,避免 `1180x760` 下长 URL 或错误信息溢出。
|
||||
- 系统偏好通知/更新控制区允许换行,长状态说明使用 `overflow-wrap`,避免按钮和权限状态标签在窄视口重叠。
|
||||
- 个人中心对话框内容区改为内部滚动,诊断值使用强制换行,避免新增诊断信息后超过最小窗口高度。
|
||||
- 通知权限运行时改为优先读取 WebView `Notification.permission` 的 `granted`/`denied` 状态;请求权限返回 `default` 时保持“待授权”,不再误标为“已拒绝”。
|
||||
- 系统偏好通知页已移除授权成功态的固定标签文案,开启状态只由开关表达;待授权、已拒绝和不可用状态继续显示提示标签。
|
||||
- 系统偏好通知页新增“测试通知”命令,完整走 `frontend/src/runtime/notifications.ts` -> `@tauri-apps/plugin-notification` 的系统通知发送链路;测试通知文案为固定通用内容,不包含项目、文件、token 或其他敏感业务信息。
|
||||
- `showSystemNotification` 和测试通知调用会在发送前确认系统通知权限,只有真正发起系统通知时才返回成功;桌面通知轮询据此只 ack 已实际发起系统通知的后端通知。
|
||||
- 真实 macOS `.app` 已验证系统偏好通知开关可调用系统通知权限并生效。验证截图见 `/Users/zcc/Library/Application Support/CleanShot/media/media_boKznpjOwt/CleanShot 2026-07-02 at 11.07.52@2x.png`。
|
||||
- `Layout.desktop.test.ts` 新增静态契约覆盖个人中心诊断、登录页最小窗口断点、服务器设置滚动和偏好页控制区换行约束。
|
||||
- `notifications.test.ts` 新增授权、拒绝、待授权、取消授权请求和测试通知发送链路的单元覆盖。
|
||||
|
||||
仍需人工体验验收:
|
||||
|
||||
- 在真实 macOS `.app` 中以 `1180x760` 检查登录页、服务器设置、个人中心、系统偏好和更新弹窗无重叠、无横向溢出。
|
||||
- 在真实系统通知权限拒绝流程中确认权限状态提示、开关回退和错误提示与 OS 状态一致。
|
||||
|
||||
## 11. 2026-07-13 macOS 原生体验适配记录
|
||||
|
||||
本轮继续保留 Tauri 和共享 Vue 前端,不引入 Swift 业务 UI,也不改变 Windows 内测边界:
|
||||
|
||||
- macOS 新增标准 CTMS 应用菜单,将关于、设置、服务、隐藏和退出等项目收敛到首个应用 submenu;文件、编辑、显示、导航、窗口和帮助菜单保持独立。
|
||||
- macOS 红色关闭按钮保持真正关闭主窗口的系统语义;Tauri `RunEvent::Reopen` 在 Dock/Finder 再激活时从受控配置重建、显示并聚焦主窗口,重复启动继续复用同一恢复 helper。
|
||||
- 新增受控 `desktop_menu_set_shortcuts` command,只接受后退、前进和刷新三项经过双层校验的快捷键,并同步原生菜单 accelerator。
|
||||
- 桌面主题新增“跟随系统”,WebView 监听 `prefers-color-scheme`,受控 `desktop_window_set_theme` command 同步 Tauri 窗口外观;未向 WebView 增加原始窗口权限。
|
||||
- `desktop:release:check` 已增加应用菜单、Dock reopen、关闭后重建、快捷键同步、系统主题和新增 command 白名单约束。
|
||||
|
||||
真实 `.app` 仍需人工验证:红色按钮真正关闭主窗口后 Dock 重建、菜单顺序、个人中心自身关闭按钮、编辑菜单文本输入行为、自定义快捷键即时同步,以及系统明暗模式切换时的标题栏一致性。
|
||||
|
||||
本轮未引入以下条件性 Swift 能力:Quick Look 需先确认附件审阅频率和 Windows 降级语义;Touch ID 会改变现有 30 天会话恢复体验,需先确定凭据访问控制策略;通知点击回跳需先定义固定、无敏感信息的动作和目标页面。三项均不是当前发布前置条件。
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# CTMS 桌面端本地缓存执行方案
|
||||
|
||||
## 目标
|
||||
|
||||
桌面端新增本地缓存的目标是降低重复网络请求、改善页面回访和 App 重启后的等待时间。当前项目不涉及受试者隐私问题,后端返回的 CTMS 业务 GET 数据均可进入本地缓存候选范围,但缓存仍只是服务端响应副本,不是业务权威数据。
|
||||
|
||||
本方案不改变以下边界:
|
||||
|
||||
- 登录、会话恢复和用户身份确认仍必须依赖后端 token 校验和 `/me`。
|
||||
- 新增、编辑、删除、审批、导入、导出、通知 ack、权限判断和审计判断仍必须以后端结果为准。
|
||||
- 不实现离线登录、离线写入队列、冲突解决、本地优先工作流、本地 API 镜像或内嵌后端服务。
|
||||
- token、登录密码、附件下载凭据、临时授权 URL、Authorization/Bearer 文本不得写入本地缓存、URL、日志或系统通知正文。
|
||||
|
||||
## 缓存范围
|
||||
|
||||
默认可缓存:
|
||||
|
||||
- 字典、枚举、菜单、权限摘要、用户可见组织/中心、应用配置、系统元数据。
|
||||
- 项目、任务、机构、用户、角色、报表、监控页面等业务 GET 响应。
|
||||
- 页面查询结果、分页列表、详情页只读数据和图表数据。
|
||||
|
||||
默认不缓存:
|
||||
|
||||
- POST、PUT、PATCH、DELETE 等 mutation 请求响应,除非只用于立即失效相关 GET 缓存。
|
||||
- 文件下载二进制正文、一次性下载链接、带签名的临时 URL。
|
||||
- token、密码、验证码、MFA 信息、Authorization header、Cookie 原文。
|
||||
- 系统通知正文和可能包含临时凭据的 release notes 或错误日志。
|
||||
|
||||
## 架构约束
|
||||
|
||||
- 统一入口命名为 `desktopDataCache`,位于 `frontend/src/runtime/`,并通过 `clientRuntime` 暴露。
|
||||
- 业务模块不得直接调用 Tauri 存储 API、文件系统、SQLite、IndexedDB 或 Cache Storage 来实现桌面缓存。
|
||||
- Web 与 Desktop 共用业务代码,平台差异只在 runtime 适配层之后收敛。
|
||||
- Tauri command 只做窄职责存储读写、清理、容量统计或目录定位,不包含 CTMS 业务规则。
|
||||
- 如果引入新的 Tauri command、capability、CSP 或存储插件,必须同步评估 `frontend/scripts/verify-desktop-release.mjs`、`npm run runtime:check` 和桌面发布检查清单。
|
||||
|
||||
## 缓存命名和数据模型
|
||||
|
||||
缓存命名空间必须包含:
|
||||
|
||||
- `serverOrigin`:规范化后的后端服务地址,不包含用户名、密码、token 或 query 凭据。
|
||||
- `userId`:后端 `/me` 确认后的用户标识。
|
||||
- `schemaVersion`:缓存结构版本,用于升级时整体失效。
|
||||
- `requestSignature`:请求方法、路径、排序后的 query、稳定序列化后的 body 摘要和必要的业务上下文。
|
||||
|
||||
每条缓存记录至少包含:
|
||||
|
||||
- `payload`:后端响应副本。
|
||||
- `status`:HTTP 状态码,仅缓存成功的可展示响应。
|
||||
- `headers`:只保存白名单响应头,例如 `ETag`、`Last-Modified`、`Cache-Control`、业务版本头。
|
||||
- `createdAt`、`updatedAt`、`expiresAt`、`lastAccessedAt`。
|
||||
- `sourceEndpoint` 和 `cacheTags`,用于诊断和精准失效。
|
||||
|
||||
## 缓存策略
|
||||
|
||||
第一优先级是请求减量:
|
||||
|
||||
- 同一会话内相同 GET 请求合并为一个 in-flight promise。
|
||||
- 页面返回、tab 切换和重复组件挂载时优先命中短时内存缓存。
|
||||
- 页面可使用 stale-while-revalidate:先显示本地副本,再后台刷新,刷新失败时保留旧数据并显示可恢复错误。
|
||||
|
||||
第二优先级是持久化缓存:
|
||||
|
||||
- 仅在后端会话恢复和 `/me` 成功后启用业务缓存读取。
|
||||
- 默认缓存 GET 响应;mutation 成功后通过 tags 或 endpoint 规则失效相关 GET 缓存。
|
||||
- TTL 先按数据类型配置,缺省值建议 5 分钟;字典和元数据可延长到 24 小时;高频业务列表建议 1 到 5 分钟。
|
||||
- 设置总容量上限和 LRU 清理策略,避免缓存无限增长。
|
||||
|
||||
第三优先级是 HTTP 条件请求:
|
||||
|
||||
- 后端为稳定数据提供 `ETag` 或 `Last-Modified`。
|
||||
- 客户端对有验证器的缓存请求带 `If-None-Match` 或 `If-Modified-Since`。
|
||||
- 收到 304 时刷新缓存元数据,不重复下载 payload。
|
||||
|
||||
## 失效和清理
|
||||
|
||||
必须整体清理:
|
||||
|
||||
- 用户登出。
|
||||
- 切换服务器。
|
||||
- 切换用户。
|
||||
- 后端返回 401 或 403。
|
||||
- `/me` 返回的用户标识、角色、权限版本或租户上下文变化。
|
||||
- `schemaVersion` 升级。
|
||||
|
||||
必须精准失效:
|
||||
|
||||
- mutation 成功后,清理相关详情、列表、统计和图表缓存。
|
||||
- 权限、角色、组织、项目状态变化后,清理依赖这些上下文的页面缓存。
|
||||
- 后端返回明确业务版本头或失效事件时,按 tags 清理。
|
||||
|
||||
应提供用户入口:
|
||||
|
||||
- 系统偏好或个人中心诊断信息展示缓存大小、记录数、最近清理时间。
|
||||
- 提供“清理本地缓存”按钮。
|
||||
- 清理提示只描述缓存状态,不包含具体业务内容。
|
||||
|
||||
## 分阶段实施
|
||||
|
||||
### 当前落地状态
|
||||
|
||||
2026-07-08 已完成阶段 1 的首批实现:
|
||||
|
||||
- 新增 `frontend/src/runtime/desktopDataCache.ts`,提供内存级缓存 scope、TTL、tag 失效、清理和统计能力,并通过 `clientRuntime.dataCache` 暴露。
|
||||
- `frontend/src/api/axios.ts` 已支持 GET 并发去重、显式 GET 内存缓存、mutation 成功后默认清理缓存、401/403 清理缓存、服务器切换清理缓存。
|
||||
- 缓存清理、scope 切换和 tag 失效会递增 generation;GET 响应返回时若 generation 已变化,不再写入缓存,避免旧用户、旧服务器或 mutation 前响应回填到新缓存空间。
|
||||
- `/api/v1/auth/login-key` 明确禁用 GET 去重,避免复用登录 challenge。
|
||||
- GET 去重和缓存 key 已包含 schema version、server baseURL、请求参数、非敏感请求头、responseType、paramsSerializer 和 withCredentials;包含敏感 header 或 axios auth 的请求不会进入去重或缓存。
|
||||
- 登录态已在 `/me` 成功后绑定缓存 scope,登出时清空 scope 和缓存。
|
||||
- 首批缓存接入范围包括项目列表/详情、项目配置、权限摘要、系统权限、权限模板、机构、成员、项目概览和仪表盘摘要。
|
||||
- `npm run runtime:check` 和 `npm run desktop:release:check` 已增加底层缓存 API 边界检查,业务模块不得直接调用 `indexedDB`、`caches.open`、`CacheStorage` 或 SQLite 入口。
|
||||
|
||||
仍未完成:
|
||||
|
||||
- 持久化缓存尚未落地,当前缓存只存在于本次前端运行内存中。
|
||||
- 后端 `ETag` / `Last-Modified` 和客户端 304 处理尚未落地。
|
||||
- 系统偏好或个人中心缓存诊断与手动清理入口尚未落地。
|
||||
- mutation 失效当前以默认全量清理为主,后续再按 tags 收敛为精准失效。
|
||||
|
||||
### 阶段 0:请求观测和缓存清单
|
||||
|
||||
- 梳理 `frontend/src/api/`、Pinia store 和高频页面的 GET 请求。
|
||||
- 标记每个接口的缓存 tags、TTL、mutation 失效关系和是否需要后端 ETag。
|
||||
- 记录启动、登录后首页、常用列表和详情页的请求数量作为基线。
|
||||
|
||||
### 阶段 1:请求去重和内存缓存
|
||||
|
||||
- 在 API 客户端层增加 GET 请求 in-flight 去重。
|
||||
- 增加短 TTL 内存缓存,先覆盖字典、菜单、权限摘要、组织/中心和常用列表。
|
||||
- 增加单元测试覆盖请求合并、TTL 过期、mutation 后失效。
|
||||
|
||||
### 阶段 2:runtime 持久化缓存
|
||||
|
||||
- 在 `frontend/src/runtime/desktopDataCache.ts` 定义平台无关接口。
|
||||
- Web 端提供内存或 IndexedDB 实现;Desktop 端通过 runtime 封装 IndexedDB、Cache Storage 或 Tauri app data 存储。
|
||||
- 增加命名空间、容量限制、LRU、schema version、手动清理和诊断 API。
|
||||
- 更新 `runtime:check` 和桌面发布检查,禁止业务页面直接使用底层缓存存储。
|
||||
|
||||
### 阶段 3:页面接入
|
||||
|
||||
- 先接入基础数据和全局布局依赖接口。
|
||||
- 再接入项目列表、任务列表、报表图表、详情页只读数据。
|
||||
- 为 mutation 建立集中失效规则,避免页面各自手写清理逻辑。
|
||||
|
||||
### 阶段 4:后端条件请求
|
||||
|
||||
- 为稳定 GET 接口补 `ETag` 或 `Last-Modified`。
|
||||
- 前端 API 客户端接入 304 处理。
|
||||
- 增加后端和前端测试,确认 304 不改变业务数据语义。
|
||||
|
||||
### 阶段 5:诊断、验收和发布门禁
|
||||
|
||||
- 在个人中心或系统偏好加入缓存诊断和清理入口。
|
||||
- 验证登出、切换服务器、切换用户、401/403、权限变化和版本升级清理行为。
|
||||
- 执行相关质量门禁,并把新增缓存边界纳入发布检查清单。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- App 重启并完成 `/me` 校验后,已访问过的常用页面可以先展示缓存再后台刷新。
|
||||
- 同一路由重复进入、组件重复挂载、多个组件请求同一资源时,不产生重复并发请求。
|
||||
- 登出、切换服务器、切换用户、401/403 后不能读取旧命名空间缓存。
|
||||
- mutation 成功后相关列表、详情、统计和图表缓存被失效或刷新。
|
||||
- token、密码、Authorization/Bearer、下载凭据不会出现在缓存文件、IndexedDB、日志、URL 或通知正文中。
|
||||
- 业务模块不直接调用 Tauri API、IndexedDB、SQLite、Cache Storage 或文件系统实现缓存。
|
||||
|
||||
## 质量门禁
|
||||
|
||||
本地缓存相关实现至少执行:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run runtime:check
|
||||
npm run desktop:release:check
|
||||
npm run ui:contract
|
||||
npm run type-check
|
||||
npm run test:unit
|
||||
npm run build
|
||||
```
|
||||
|
||||
如果新增或修改后端条件请求、缓存头、权限版本或失效事件,还需执行对应后端测试。若新增 Tauri command、capability、CSP 或存储插件,还需执行 `npm run desktop:build:app` 并在真实 macOS `.app` 中验证缓存清理和诊断入口。
|
||||
@@ -34,13 +34,15 @@
|
||||
- macOS:Keychain。
|
||||
- Windows:Credential Manager。
|
||||
|
||||
桌面端登录使用后端签发的在线会话 token,可在系统凭据库中保存最长 30 天,以支持重启 App 后免输入密码。启动恢复后仍必须使用后端 token 校验和 `/auth/me` 用户状态校验;服务端不可达或会话被后端拒绝时不得进入离线模式。
|
||||
|
||||
Rust 仅暴露固定 service 下的读取、写入、删除命令。凭据 account 使用规范化服务端 origin 的 SHA-256,避免明文服务端地址散落在系统凭据项名称中。
|
||||
|
||||
应用挂载前异步初始化 token:
|
||||
|
||||
1. Web 端继续读取 `localStorage.ctms_token`。
|
||||
2. 桌面端先删除 legacy `localStorage.ctms_token`。
|
||||
3. 若 legacy token 仍有效,则迁移到系统凭据库。
|
||||
3. 若 legacy token 仍有效,则迁移为带 30 天本机到期时间的系统凭据库会话记录。
|
||||
4. 若迁移或读取凭据失败,则内存 token 置空并要求重新登录,不回退明文存储。
|
||||
|
||||
登出、服务器切换、认证失效时必须同步清除内存 token 和当前服务端 origin 对应的系统凭据。
|
||||
@@ -75,12 +77,12 @@ API:
|
||||
- `POST /api/v1/desktop-notifications/ack`
|
||||
- `POST /api/v1/desktop-notifications/{distribution_id}/read`
|
||||
|
||||
claim 跨有效项目查询匹配当前用户或角色的活动文件分发,使用五分钟租约、唯一约束和事务防重。客户端显示系统通知后 ack;显示失败则等待租约到期后重试。
|
||||
claim 跨有效项目查询当前用户未读且未解决的通用业务提醒,使用五分钟租约、唯一约束和事务防重。客户端显示系统通知后 ack;显示失败则等待租约到期后重试。系统通知只是通用提醒 Feed 的可选投递通道,不单独维护文件分发提醒来源。
|
||||
|
||||
系统通知正文只显示通用内容:
|
||||
|
||||
- 标题:`CTMS 文件更新`
|
||||
- 正文:`有新的文件版本待查看`
|
||||
- 标题:`CTMS 待办提醒`
|
||||
- 正文:`有新的业务提醒待查看`
|
||||
|
||||
项目、文件、版本等详细信息仅在应用内列表显示,避免锁屏泄露。
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
# CTMS 桌面端项目计划书
|
||||
|
||||
## 目的
|
||||
|
||||
本文档是 CTMS 桌面端工作的长期方向约束。每次开始任何桌面端相关任务前,都必须先阅读本文档,包括 Tauri 初始化、macOS 打包、Windows 适配、桌面端存储、文件集成、系统通知、安全边界和发布流程。
|
||||
|
||||
桌面端必须服务于现有 CTMS Web 应用和后端架构。目标是为当前 CTMS 服务提供原生桌面入口,而不是创建一个独立的离线产品。
|
||||
|
||||
## 不可突破的边界
|
||||
|
||||
- 技术路线固定为 Tauri。
|
||||
- 第一开发目标是 macOS 桌面端。
|
||||
- Windows 仍只作为第二阶段兼容性验证目标,未获明确批准前不发布正式安装包。
|
||||
- 当前桌面端工作只允许在第一、二阶段边界内做修复、稳定化、体验收口和发布准备,不新增第三阶段能力。
|
||||
- 不做离线功能。
|
||||
- 不在桌面 App 内嵌本地后端服务。
|
||||
- 不在桌面 App 内嵌或分发本地数据库来保存 CTMS 业务数据。
|
||||
- 不实现离线同步、冲突解决、本地业务数据队列、本地优先工作流。
|
||||
- 不把 CTMS 业务 UI 拆成一套独立的桌面端产品;除非桌面能力确实需要小范围适配层。
|
||||
- FastAPI 后端仍是业务权威来源。权限裁决、审计判断、认证、业务数据持久化都保持在服务端。
|
||||
|
||||
## 当前技术基线
|
||||
|
||||
桌面端工作基于当前 CTMS Web 技术栈:
|
||||
|
||||
- 前端:Vue 3、Vite、TypeScript、Element Plus、Pinia、Vue Router、Axios、ECharts。
|
||||
- 后端:FastAPI、Uvicorn、SQLAlchemy、Alembic。
|
||||
- 数据库:PostgreSQL。
|
||||
- 当前部署:Docker Compose、nginx 托管前端静态资源、nginx 反代 `/api`。
|
||||
|
||||
桌面端应复用现有前端代码和 API 契约。任何共享适配层都应保持小而明确,并可测试。
|
||||
|
||||
## 已完成阶段边界
|
||||
|
||||
第一阶段 macOS 在线桌面壳和第二阶段原生能力主体改造已经形成。详细历史方案见 [`desktop-phase-1-design.md`](desktop-phase-1-design.md) 和 [`desktop-phase-2-design.md`](desktop-phase-2-design.md)。
|
||||
|
||||
后续不再按第一阶段空白项目初始化 Tauri,也不再扩展第二阶段以外的新桌面产品能力。当前允许推进的工作仅包括:
|
||||
|
||||
- 修复既有 Tauri、运行时适配层、文件、通知、凭据、更新、菜单/快捷键和打包问题。
|
||||
- 稳定 Web 与 Desktop 共用业务代码,确保平台差异继续收敛在 `frontend/src/runtime/` 后面。
|
||||
- 完成 macOS 正式发布前的签名、公证、updater 签名、制品发布、CI 门禁和人工回归。
|
||||
- 做 Windows 第二阶段兼容性验证,但不发布正式 Windows 安装包。
|
||||
|
||||
仍然不允许:
|
||||
|
||||
- 离线登录、离线浏览、离线队列、离线同步或本地优先工作流。
|
||||
- 本地 PostgreSQL、SQLite、IndexedDB 业务数据缓存或本地 API 镜像。
|
||||
- 内嵌 Python/FastAPI 后端服务。
|
||||
- 绕过后端做本地权限裁决、本地审计缓存或审计回放。
|
||||
- 为桌面端复制或重写一套独立业务 UI。
|
||||
|
||||
## 架构方向
|
||||
|
||||
使用运行时适配层,不在业务页面里散落平台判断。
|
||||
|
||||
当前已采用并必须继续保持的适配层边界:
|
||||
|
||||
- `platform`:识别 Web、macOS 桌面端、Windows 桌面端。
|
||||
- `apiBaseUrl`:分别解析 Web 和桌面端的服务端 API 地址。
|
||||
- `desktopServerConfig`:管理桌面服务端地址配置和切换事件。
|
||||
- `secureSessionStorage`:隔离浏览器 token 存储与桌面系统凭据库。
|
||||
- `files`:隔离浏览器上传下载与原生文件能力。
|
||||
- `notifications`:隔离 Web 通知与桌面系统通知。
|
||||
- `updates`:隔离桌面自动更新检查与安装入口。
|
||||
- `appMetadata`:在可用时提供桌面 App 版本、平台、构建通道。
|
||||
- `desktopMenu` 和 `desktopUiPreferences`:承接桌面菜单命令、最近访问和收藏等桌面体验状态。
|
||||
- `clientRuntime`:作为业务侧获取平台能力的聚合入口。
|
||||
|
||||
业务模块应调用这些适配层,而不是直接调用 Tauri API。Tauri command 应保持窄职责,不包含 CTMS 业务规则。
|
||||
|
||||
## 安全与合规方向
|
||||
|
||||
- 将桌面端视为受监管业务系统的在线客户端。
|
||||
- 非本地服务连接优先使用 HTTPS。
|
||||
- 认证与授权决策保留在后端。
|
||||
- 审计敏感决策保留在后端。
|
||||
- 敏感凭据必须继续使用明确批准的安全存储方案。
|
||||
- 不向前端暴露宽泛文件系统访问权限。
|
||||
- Tauri 权限保持最小化,并按功能精确授权。
|
||||
- 每个新增 Tauri command 都需要被视为桌面端安全边界的一部分进行审查。
|
||||
|
||||
## 分支与工作区
|
||||
|
||||
桌面端工作在以下位置开发:
|
||||
|
||||
- Worktree:`/Users/zcc/MyCTMS/ctms-dev/worktrees/ctms-desktop`
|
||||
- 短期分支:Agent 默认使用 `codex/<任务名称>`,也可按任务类型使用 `feature/*`、`fix/*`、`docs/*` 或 `release-prep/*`;所有普通桌面端工作都必须从最新 `dev` 创建
|
||||
|
||||
当前代码状态:Tauri 基线和第二阶段原生能力已经形成,后续桌面端工作默认是第二阶段范围内的修复、稳定化、体验收口和发布准备。`codex/ctms-desktop` 是历史临时集成分支,不再作为当前工作线;不得继续向该分支提交、变基或推送新的桌面端工作,除非用户明确要求做收尾或删除分支。
|
||||
|
||||
## 2026-07-01 审查结论与后续方向
|
||||
|
||||
本轮审查结论:桌面端已经完成从 Tauri 接入基线到第二阶段原生能力的主体改造,不再按空白桌面项目推进。当前重点不是扩展离线或本地业务能力,而是围绕既有在线桌面客户端做准发布稳定化。
|
||||
|
||||
已形成的能力边界:
|
||||
|
||||
- Tauri 工程、macOS App/DMG/updater artifacts 配置已经存在。
|
||||
- `frontend/src/runtime/` 已作为平台能力统一入口,业务代码不得绕过该适配层直接使用 Tauri API。
|
||||
- 桌面服务器地址、API baseURL、客户端元数据请求头、安全 session 存储、原生文件能力、系统通知、单实例、菜单/快捷键和自动更新入口已经形成。
|
||||
- 后端已经包含桌面通知订阅/投递状态、相关 API 和客户端诊断请求头支持。
|
||||
- 已有 `npm run version:check`、`npm run runtime:check`、`npm run desktop:release:check` 等门禁用于约束版本、运行时边界和桌面发布安全边界。
|
||||
|
||||
后续优化优先级:
|
||||
|
||||
1. 发布稳定化:补齐 macOS 签名、公证、组织 updater 私钥签名、release tag 构建变量注入、不可变制品上传和 `latest.json` 原子替换。
|
||||
2. 端到端回归:按 `docs/audits/desktop-release-stabilization-checklist.md` 覆盖服务器配置、服务器切换清会话、Keychain/凭据库、附件上传下载、系统通知、单实例和自动更新失败恢复。
|
||||
3. 安全复审:持续确认 token 不进入 URL、日志、系统通知正文、下载链接或明文持久化;Tauri command、capability、CSP 和 updater 改动必须同步评估发布门禁。
|
||||
4. 桌面体验收口:重点检查登录、服务器设置、个人中心诊断信息、通知开关、更新弹窗和最小窗口 `1180x760` 下的布局稳定性。
|
||||
5. CI 与发布流程:Web 与 Desktop 必须从同一提交、同一语义化版本号和同一正式标签构建;发布候选应执行本文档列出的相关质量门禁。
|
||||
6. Windows 兼容验证:仅作为第二阶段兼容性目标,验证 Credential Manager、路径处理、通知/updater 编译、WebView2 和安装器假设;未获明确批准前不发布正式 Windows 安装包。
|
||||
|
||||
如果后续任务试图新增离线登录、本地业务数据存储、内嵌后端、本地业务队列、离线同步或绕过后端权限审计,应先修改并评审本计划书,不能直接实现。
|
||||
|
||||
## 当前质量门禁
|
||||
|
||||
前端或桌面端代码变更应按影响范围执行相关检查。发布、桌面端适配层、Tauri 配置或安全边界相关变更至少考虑:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run version:check
|
||||
npm run runtime:check
|
||||
npm run desktop:release:check
|
||||
npm run ui:contract
|
||||
npm run type-check
|
||||
npm run test:unit
|
||||
npm run build
|
||||
npm run desktop:build:app
|
||||
```
|
||||
|
||||
正式桌面发布构建仍必须使用组织批准的 updater 签名私钥和 Apple 签名/公证流程;未签名或 ad-hoc 构建只能作为内部验证构建描述。文档-only 变更可以不执行完整代码门禁,但必须在结果说明中明确未运行。
|
||||
|
||||
## 每次开发前必须执行的检查
|
||||
|
||||
开始任何桌面端相关任务前:
|
||||
|
||||
- 先阅读本文档。
|
||||
- 确认任务属于第一阶段或第二阶段。
|
||||
- 确认任务不会引入离线能力。
|
||||
- 确认实现不会破坏 Web 运行时。
|
||||
- 确认 Tauri API 使用被隔离在适配层之后,除非有明确记录的理由。
|
||||
- 确认不会重复初始化 Tauri 或绕过既有 `frontend/src/runtime/` 运行时边界。
|
||||
- 涉及 Tauri 权限、CSP、updater、凭据、文件或通知能力时,确认桌面发布检查脚本和发布清单是否需要同步更新。
|
||||
|
||||
如果用户请求与本文档冲突,先停止实现并确认范围,不要直接推进。
|
||||
@@ -147,12 +147,12 @@ bash scripts/install.sh release --base-url https://ctms.example.com
|
||||
|
||||
```text
|
||||
1. 检查 docker、docker compose、openssl、curl。
|
||||
2. 检查 .env;已存在则保留不改,不存在则新建。
|
||||
3. 新建 main/release 的 .env 时自动生成 JWT 和 RSA 私钥。
|
||||
2. 检查 .env;保留已有身份与登录密钥,补齐并默认启用标准 ONLYOFFICE 配置,不存在则新建。
|
||||
3. 所有环境自动生成独立的 ONLYOFFICE JWT 与稳定实例标识;新建 main/release 的 .env 时同时生成登录 JWT 和 RSA 私钥。
|
||||
4. main/release 先构建并执行 backend-init,确保数据库迁移使用当前代码中的 Alembic revision。
|
||||
5. 启动容器;默认执行 docker compose up -d --build,--skip-build 时执行 docker compose up -d。
|
||||
5. 默认构建并启动数据库、后端、前端/Nginx 和 ONLYOFFICE;ONLYOFFICE 不再使用可选 Compose Profile。
|
||||
6. 执行 docker compose run --rm backend python -m alembic upgrade head。
|
||||
7. 检查容器状态、后端环境、/health、/api/v1/auth/login-key。
|
||||
7. 检查全部容器状态、后端环境、/health、/api/v1/auth/login-key 和 /onlyoffice/healthcheck。
|
||||
```
|
||||
|
||||
更新进入“执行部署更新”后会持续显示 Docker 镜像拉取与构建进度:交互终端使用滚动实时输出窗口,CI、远程面板或管道环境直接流式输出构建日志,并在两种模式下显示累计耗时。
|
||||
@@ -164,6 +164,7 @@ git checkout
|
||||
git pull
|
||||
docker compose down -v
|
||||
删除 pg_data
|
||||
删除 backend/app/uploads
|
||||
删除数据库 volume
|
||||
自动安装系统依赖
|
||||
```
|
||||
@@ -194,6 +195,12 @@ ENV=development
|
||||
JWT_SECRET_KEY=dev-secret
|
||||
LOGIN_RSA_KEY_ID=default
|
||||
LOGIN_RSA_PRIVATE_KEY=
|
||||
ONLYOFFICE_ENABLED=true
|
||||
ONLYOFFICE_JWT_SECRET=<替换为独立的随机密钥,至少 32 个字符>
|
||||
ONLYOFFICE_INTERNAL_URL=http://onlyoffice
|
||||
ONLYOFFICE_STORAGE_BASE_URL=http://backend:8000
|
||||
ONLYOFFICE_INSTANCE_ID=ctms-dev-<替换为随机实例标识>
|
||||
ONLYOFFICE_CONFIG_TTL_SECONDS=300
|
||||
EOF
|
||||
```
|
||||
|
||||
@@ -315,6 +322,12 @@ ENV=production
|
||||
JWT_SECRET_KEY=<替换为 JWT 密钥>
|
||||
LOGIN_RSA_KEY_ID=main-YYYYMMDD
|
||||
LOGIN_RSA_PRIVATE_KEY=<替换为 RSA 私钥单行文本>
|
||||
ONLYOFFICE_ENABLED=true
|
||||
ONLYOFFICE_JWT_SECRET=<替换为不同于 JWT_SECRET_KEY 的独立随机密钥>
|
||||
ONLYOFFICE_INTERNAL_URL=http://onlyoffice
|
||||
ONLYOFFICE_STORAGE_BASE_URL=http://backend:8000
|
||||
ONLYOFFICE_INSTANCE_ID=ctms-main-<替换为随机实例标识>
|
||||
ONLYOFFICE_CONFIG_TTL_SECONDS=300
|
||||
EOF
|
||||
```
|
||||
|
||||
@@ -494,6 +507,12 @@ ENV=production
|
||||
JWT_SECRET_KEY=<替换为生产 JWT 密钥>
|
||||
LOGIN_RSA_KEY_ID=release-YYYYMMDD
|
||||
LOGIN_RSA_PRIVATE_KEY=<替换为生产 RSA 私钥单行文本>
|
||||
ONLYOFFICE_ENABLED=true
|
||||
ONLYOFFICE_JWT_SECRET=<替换为不同于 JWT_SECRET_KEY 的独立生产随机密钥>
|
||||
ONLYOFFICE_INTERNAL_URL=http://onlyoffice
|
||||
ONLYOFFICE_STORAGE_BASE_URL=http://backend:8000
|
||||
ONLYOFFICE_INSTANCE_ID=ctms-release-<替换为随机实例标识>
|
||||
ONLYOFFICE_CONFIG_TTL_SECONDS=300
|
||||
EOF
|
||||
```
|
||||
|
||||
|
||||
@@ -106,8 +106,8 @@ npm run desktop:build:app
|
||||
npm run desktop:build:app
|
||||
```
|
||||
|
||||
正式桌面发布构建仍必须设置 updater 签名私钥后执行
|
||||
`npm run desktop:build -- --bundles app`。
|
||||
正式桌面发布构建仍必须设置 updater 签名私钥和 Apple 签名/公证变量后,先执行
|
||||
`npm run desktop:release-readiness:check`,再执行 `npm run desktop:build:macos-release -- --ci`。
|
||||
|
||||
后端改动应补充执行受影响模块的后端测试、迁移检查和接口回归。
|
||||
|
||||
|
||||
@@ -30,12 +30,19 @@ The runtime contract currently provides:
|
||||
- file picker/save/open adapters
|
||||
- desktop system notification adapters
|
||||
- desktop updater adapters
|
||||
- native desktop application menu and shortcut synchronization
|
||||
- desktop window lifecycle and system appearance synchronization
|
||||
|
||||
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 must remain behind
|
||||
`frontend/src/runtime/` and explicit capability flags.
|
||||
|
||||
Desktop menu accelerators and window appearance are synchronized through narrow
|
||||
Tauri commands owned by `frontend/src/runtime/`. macOS close/reopen behavior is
|
||||
handled by the Tauri event loop so a genuinely closed main window can be rebuilt from the Dock;
|
||||
these concerns must not move into Vue business modules.
|
||||
|
||||
## Version Change
|
||||
|
||||
Update every client manifest with one command:
|
||||
@@ -58,6 +65,10 @@ Manual edits that leave these files inconsistent fail CI.
|
||||
|
||||
## Stabilization Gates
|
||||
|
||||
Client builds require Node.js 22.13.0 or newer. The development frontend container,
|
||||
Web CI, macOS release candidates, Windows internal validation, and the production
|
||||
Web image use the same Node 22.13 baseline.
|
||||
|
||||
Client release candidates must pass the shared Web checks and the Desktop
|
||||
release/security gate before promotion:
|
||||
|
||||
@@ -122,8 +133,8 @@ The release pipeline must:
|
||||
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. verify the feed with `npm run desktop:update-feed:check -- --feed <latest.json> --artifacts-dir <artifact-dir>`;
|
||||
5. generate `latest.json` and a checksum manifest with `npm run desktop:update-feed:create`;
|
||||
6. verify the feed with `npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir>`;
|
||||
7. upload immutable artifacts first;
|
||||
8. atomically replace `latest.json` last.
|
||||
|
||||
@@ -131,6 +142,13 @@ For Universal macOS artifacts, `latest.json` must provide both
|
||||
`darwin-aarch64` and `darwin-x86_64` entries pointing at the same Universal
|
||||
update package.
|
||||
|
||||
The signed release candidate workflow lives at
|
||||
`.github/workflows/desktop-release-candidate.yml`. It must be run from a
|
||||
matching `vX.Y.Z` tag and produces a verified release directory as a GitHub
|
||||
artifact. That artifact is still only a release candidate; the release owner
|
||||
must upload immutable files to the production download origin and replace
|
||||
`latest.json` atomically after validation.
|
||||
|
||||
## Windows Build Readiness
|
||||
|
||||
Second-phase Windows work is limited to CI compatibility validation. A
|
||||
@@ -147,6 +165,16 @@ Windows validation must cover:
|
||||
- notification and updater compilation;
|
||||
- future code-signing requirements.
|
||||
|
||||
The manual internal Windows validation workflow lives at
|
||||
`.github/workflows/desktop-windows-internal.yml`. It is `workflow_dispatch`
|
||||
only, runs on `windows-latest`, injects `VITE_BUILD_CHANNEL` and
|
||||
`VITE_BUILD_COMMIT` from the selected branch context, executes the shared client
|
||||
and Desktop safety gates, builds an unsigned NSIS installer with updater
|
||||
artifacts disabled, and uploads the `.exe` plus `SHA256SUMS.txt` as a GitHub
|
||||
Actions artifact. This workflow is for internal compatibility verification
|
||||
only; it must not generate `latest.json`, update feeds, signed release
|
||||
directories, or formal Windows release artifacts.
|
||||
|
||||
## Required Checks
|
||||
|
||||
```bash
|
||||
@@ -167,8 +195,10 @@ export TAURI_SIGNING_PRIVATE_KEY="$UPDATER_PRIVATE_KEY"
|
||||
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$UPDATER_PRIVATE_KEY_PASSWORD"
|
||||
export REQUIRE_DESKTOP_SIGNING=true
|
||||
npm run release:env:check
|
||||
npm run desktop:build -- --bundles app
|
||||
npm run desktop:update-feed:check -- --feed src-tauri/target/release/bundle/latest.json --artifacts-dir src-tauri/target/release/bundle
|
||||
npm run desktop:release-readiness:check
|
||||
npm run desktop:build:macos-release -- --ci
|
||||
npm run desktop:update-feed:create -- --artifact <CTMS.app.tar.gz> --base-url <versioned-https-artifact-prefix> --output-dir <release-dir>
|
||||
npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir>
|
||||
```
|
||||
|
||||
The Desktop build must run on macOS for the current first-phase target. A signed
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# ONLYOFFICE 标准组件运行说明
|
||||
|
||||
ONLYOFFICE Document Server 是 CTMS 的标准部署组件,用于附件和文档版本预览,以及共享库在线协作编辑。PDF、图片、另存为和桌面端“打开”仍使用各自原有链路。
|
||||
|
||||
## 默认安装
|
||||
|
||||
执行标准安装即可同时安装 CTMS 与 ONLYOFFICE:
|
||||
|
||||
```bash
|
||||
bash scripts/install.sh dev
|
||||
```
|
||||
|
||||
安装脚本会自动完成以下工作:
|
||||
|
||||
- 首次安装时生成独立的 256-bit 随机 `ONLYOFFICE_JWT_SECRET`,写入根目录 `.env`,且不在终端回显。
|
||||
- 自动生成稳定、按部署环境隔离的实例标识并写入 `.env`。
|
||||
- 将 `.env` 权限设置为 `0600`;该文件已被 Git 忽略。
|
||||
- 默认构建并启动 backend、Document Server 与 Nginx,不再使用可选 Compose Profile。
|
||||
- 将 Document Server 纳入容器状态、后端配置和 HTTP 健康检查。
|
||||
- 后续启动默认复用密钥,防止后端和 Document Server 因签名密钥漂移而无法通信。
|
||||
|
||||
开发环境需要单独重建、验证或主动轮换密钥时,可使用兼容维护脚本:
|
||||
|
||||
```bash
|
||||
bash scripts/onlyoffice-dev-up.sh
|
||||
bash scripts/onlyoffice-dev-up.sh --rotate-secret
|
||||
```
|
||||
|
||||
轮换会强制重建相关容器,已签发但尚未使用的短时预览配置会立即失效。生产环境不使用该自动生成流程,仍必须由部署密钥管理系统显式提供密钥。
|
||||
|
||||
可用 `ONLYOFFICE_IMAGE` 覆盖默认的 `onlyoffice/documentserver:9.4.0.1`。默认派生镜像只增加 Noto Sans CJK/Noto Serif CJK,不包含微软字体。
|
||||
|
||||
## 配置边界
|
||||
|
||||
- Compose 安装中的 `ONLYOFFICE_ENABLED` 默认 `true`;安装或更新旧环境时也会自动启用。
|
||||
- `ONLYOFFICE_INTERNAL_URL` 默认 `http://onlyoffice`,只供后端健康探测。
|
||||
- `ONLYOFFICE_STORAGE_BASE_URL` 默认 `http://backend:8000`,只允许后端生成固定的内部文件地址。
|
||||
- `ONLYOFFICE_CONFIG_TTL_SECONDS` 默认 300 秒,允许范围为 60–900 秒。
|
||||
- 内部内容接口不经过 Nginx 公网入口,仅接受 `AuthorizationJwt`,且 JWT 必须绑定到请求的精确 URL。
|
||||
- 配置响应禁止缓存;JWT、内部文件 URL 和磁盘路径不得进入日志、审计详情或桌面缓存。
|
||||
- `onlyoffice_data`、`onlyoffice_lib` 和 `onlyoffice_logs` 不是 CTMS 业务权威数据,不纳入业务备份或恢复来源。
|
||||
|
||||
## 生产部署要求
|
||||
|
||||
标准生产安装同样包含 ONLYOFFICE,并自动生成独立随机密钥和稳定、唯一的 `ONLYOFFICE_INSTANCE_ID`。正式上线前仍必须确认商业许可或 Community 版本授权边界、批准镜像、容量与备份监控策略,并完成 Web、macOS 和 Windows 内测端的真实 DOCX/XLSX/PPTX/WPS/ET/DPS 预览验证。
|
||||
|
||||
故障回退可临时关闭 `ONLYOFFICE_ENABLED`;下一次标准安装或更新会按标准组件策略重新启用。回退不得影响附件下载、另存为、打开、PDF 或图片预览。Community 与商业版本的授权边界以 [ONLYOFFICE 官方许可说明](https://helpcenter.onlyoffice.com/docs/faq/docs-community.aspx) 为准。
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
## 1.1 容器构建校验
|
||||
- [ ] `docker compose config` 渲染成功
|
||||
- [ ] 后端 `/code/app/uploads` 已持久化挂载到宿主机 `backend/app/uploads`
|
||||
- [ ] `docker compose build backend frontend` 成功
|
||||
- [ ] `docker compose run --rm --build backend-init` 成功
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# 系统监测简易运维指南
|
||||
|
||||
## 定位与访问边界
|
||||
|
||||
系统监测用于单实例或小规模 CTMS 部署的日常巡检、访问追踪和初步故障定位,不替代集中式指标、日志和告警平台。监测页面及 `/api/v1/permission-monitoring/*` 仅允许系统管理员访问,项目 PM 不具备系统级监测权限。
|
||||
|
||||
## 健康检查
|
||||
|
||||
- `GET /health`:进程存活探针,不访问数据库,适合判断 HTTP 服务是否仍在响应。
|
||||
- `GET /readyz`:服务就绪探针,会执行数据库查询;数据库不可用时返回 `503`。Nginx、安装脚本和后端容器健康检查均使用或暴露此探针。
|
||||
- `GET /api/v1/permission-monitoring/health`:管理员健康详情,包含权限检查质量、缓存、进程内告警、数据库延迟、权限/安全日志写入队列、留存任务和监测表数据量。
|
||||
|
||||
部署后的最小检查:
|
||||
|
||||
```bash
|
||||
curl -fsS http://127.0.0.1:8888/health
|
||||
curl -fsS http://127.0.0.1:8888/readyz
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
## 数据留存
|
||||
|
||||
后台任务在应用启动后立即清理一次,之后按固定间隔执行。默认策略:
|
||||
|
||||
- 权限访问日志和安全访问日志:90 天。
|
||||
- 权限小时指标:400 天。
|
||||
- 账号登录活动:180 天。
|
||||
- 清理周期:86400 秒(每天)。
|
||||
|
||||
可通过后端环境变量调整:
|
||||
|
||||
```text
|
||||
MONITORING_ACCESS_LOG_RETENTION_DAYS=90
|
||||
MONITORING_METRIC_RETENTION_DAYS=400
|
||||
MONITORING_RETENTION_INTERVAL_SECONDS=86400
|
||||
MONITORING_IP_GEO_FALLBACK_ENABLED=true
|
||||
MONITORING_IP_GEO_FALLBACK_TIMEOUT_SECONDS=2.5
|
||||
MONITORING_IP_GEO_FALLBACK_CACHE_SECONDS=604800
|
||||
MONITORING_IP_GEO_FALLBACK_MAX_LOOKUPS=10
|
||||
USER_LOGIN_ACTIVITY_RETENTION_DAYS=180
|
||||
USER_SESSION_ONLINE_SECONDS=300
|
||||
```
|
||||
|
||||
访问日志留存范围为 7–3650 天,指标留存范围为 30–3650 天,清理周期范围为 60–604800 秒。修改后需重启后端。正式环境部署新版本前必须先执行 Alembic migration。
|
||||
|
||||
账号管理页的“在线”状态由服务端会话心跳计算:最近 `USER_SESSION_ONLINE_SECONDS` 秒内成功心跳且未退出的会话视为在线。登录记录保存客户端类型、版本、服务端观测到的登录 IP、登录/最近活动/退出时间;IP 属地由本地离线数据库按需解析,不发送到外部服务。登录记录接口仅限系统管理员访问并禁止 HTTP 缓存,不保存 Token 或密码,并与登录活动一起按 `USER_LOGIN_ACTIVITY_RETENTION_DAYS` 清理。
|
||||
|
||||
登录 IP 只接受可信反向代理提供的转发地址。Docker Compose 默认信任容器常用的 `172.16.0.0/12` 和 `192.168.0.0/16` 内部网段;其他部署必须通过 `TRUSTED_PROXY_CIDRS` 明确配置实际代理网段,不得直接信任任意来源的 `X-Forwarded-For`。
|
||||
|
||||
## 来源地图服务器位置
|
||||
|
||||
来源地图中的服务器标记不使用固定城市或前端坐标。后端启动时按以下顺序确定部署服务器的公网地址,并缓存定位结果:
|
||||
|
||||
1. `MONITORING_SERVER_PUBLIC_IP` 明确指定的公网 IP,适用于禁止主动访问公网探测服务的生产网络。
|
||||
2. `FRONTEND_PUBLIC_URL` 主机名解析出的公网 IP。
|
||||
3. `MONITORING_PUBLIC_IP_DISCOVERY_URLS` 配置的公网出口 IP 查询服务。
|
||||
|
||||
默认公网查询服务为 `https://api64.ipify.org,https://icanhazip.com`,单次超时 2.5 秒。解析成功后通过本地 ip2region 数据库确定属地并转换为地图坐标;本地库没有坐标时复用下述 IP2Location.io 受控兜底。两条链路均失败时 API 返回 `server_location: null`,地图不会使用任意默认城市代替。默认打开全球视图,以便服务器与访问来源分属不同国家时仍能完整显示飞线。
|
||||
|
||||
## 访问来源坐标兜底
|
||||
|
||||
访问来源始终先使用本地 ip2region 和内置行政区质心解析。只有公网 IP 已识别、但本地链路无法得到地图坐标时,后端才调用 IPAddress.my 使用的 IP2Location.io 官方 JSON API `https://api.ip2location.io/` 补充经纬度;私网、回环、链路本地和无效地址不会发送给第三方。
|
||||
|
||||
兜底默认启用,单次请求最多查询 10 个尚未缓存的公网 IP,最多并发 5 个请求,超时 2.5 秒。成功结果缓存 7 天,失败结果缓存 1 小时;第三方不可用时继续返回本地解析结果,不影响访问来源接口。可设置 `MONITORING_IP_GEO_FALLBACK_ENABLED=false` 完全禁用外部查询。无密钥模式受服务方每日额度限制;如需配置 API Key,使用 `MONITORING_IP_GEO_FALLBACK_API_KEY`,后端通过 `Authorization: Bearer` 发送,禁止把密钥写入 URL 或日志。
|
||||
|
||||
受限网络建议显式配置:
|
||||
|
||||
```text
|
||||
MONITORING_SERVER_PUBLIC_IP=<部署服务器公网IP>
|
||||
MONITORING_PUBLIC_IP_DISCOVERY_URLS=
|
||||
```
|
||||
|
||||
公网 IP 仅用于服务端定位,不写入来源分析 API 响应或浏览器界面。
|
||||
|
||||
## 日志完整性与隐私
|
||||
|
||||
- 每个请求由服务端生成 UUID 请求标识,并通过响应头 `X-Request-ID` 返回;权限日志和安全日志以该标识消除同一请求的重复统计。
|
||||
- 写入队列保持非阻塞;队列满或数据库批次写入最终失败时不阻塞业务请求,但会累计丢弃量、失败批次、队列占用和最近错误时间,并在管理员健康页显示降级。
|
||||
- 请求正文不采集。请求头仅保留运维白名单字段,认证、Cookie、密钥类字段统一脱敏;查询参数中的令牌、账号、受试者/患者、姓名、邮箱、电话、证件和地址类值会替换为 `[redacted]`。
|
||||
- IP、User-Agent 和请求路径仍属于运维审计数据,应按管理员最小授权和上述留存策略管理,不应复制到公开工单或通知正文。
|
||||
|
||||
## 常见异常处理
|
||||
|
||||
| 现象 | 首要检查 | 建议处理 |
|
||||
| --- | --- | --- |
|
||||
| `/health` 正常、`/readyz` 返回 503 | 数据库容器、连接串、迁移状态 | 检查 `docker compose ps`、数据库日志和 `alembic current` |
|
||||
| 日志写入器降级 | 队列占用、丢弃量、失败批次、最近错误 | 检查数据库连接和容量;恢复后确认队列回落并重启以清零进程累计计数 |
|
||||
| 留存任务异常 | 最近成功/错误时间 | 检查数据库权限、表结构和后端日志,确认 migration 已升级 |
|
||||
| 页面显示“可能是上次成功结果” | 对应 API 或网络请求 | 使用刷新按钮重试,并结合 `/readyz` 与浏览器网络面板定位 |
|
||||
| 安全事件突然增多 | 分类、来源 IP、路径和状态码 | 优先核对敏感路径探测、5xx 和无效令牌,不要仅按 4xx 总量或地理位置判断 |
|
||||
|
||||
## 当前边界与后续升级
|
||||
|
||||
权限缓存指标、告警列表和写入器累计计数仍为单进程内存状态,重启后会重置;当前也不负责短信、邮件、Slack 等外部通知。需要多实例部署、跨重启趋势、值班通知或长期容量分析时,应接入 Prometheus/OpenTelemetry、集中日志和 Alertmanager 类告警链路,并保留本模块作为管理员快速诊断入口。
|
||||
@@ -0,0 +1,76 @@
|
||||
# ONLYOFFICE 在线协作模块
|
||||
|
||||
## 范围
|
||||
|
||||
在线协作是共享库下的独立业务模块,不复用附件、文档管理、文件版本管理或 eTMF 的数据表和文件标识。当前支持新建或导入 `DOCX`、`XLSX`、`PPTX`,通过 ONLYOFFICE 进行多人共同编辑,并把保存结果固化为不可变修订。
|
||||
|
||||
原有附件与文档版本仍保持只读预览流程,不会自动进入协作空间,也不会因协作回调被覆盖。
|
||||
|
||||
## 数据与权限边界
|
||||
|
||||
- `collaboration_files` 保存协作文件元数据、当前修订指针和统一的内容操作权限设置。
|
||||
- `collaboration_revisions` 保存不可变文件修订;每次有效保存或历史恢复都会创建新修订。
|
||||
- `collaboration_members` 保存文件级编辑者和管理者。
|
||||
- `collaboration_edit_requests` 保存系统内项目成员发起的编辑权限申请及审批结果;同一成员、同一文件同时只允许存在一条待处理申请。
|
||||
- `notifications` 是收件人级通用通知表,统一保存通知类别、优先级、内部操作路径、业务来源、去重键、已读和业务关闭状态。编辑权限申请只通知当前有效的文件所有者和文件管理者;申请处理后,同一业务来源的所有收件人通知一并关闭。
|
||||
- `collaboration_sessions` 使用稳定的文件代次生成 ONLYOFFICE `document.key`,同一代次的用户进入同一共同编辑会话。
|
||||
- `collaboration_callback_receipts` 对回调做幂等确认。
|
||||
- `collaboration_share_links` 保存单文件公开链接的启停状态、查看/编辑权限、有效期策略和密码哈希;不保存明文密码或共享令牌。
|
||||
- 项目接口权限只控制在线协作模块入口、项目级新建/导入/文件夹操作,以及账号能否被授予文件角色;文件创建后的编辑、管理、导出和所有权转让统一由文件身份判定,项目角色不再隐式覆盖文件角色。
|
||||
- 文件所有者始终拥有编辑、管理、导出和转让能力;文件管理者始终拥有编辑、管理和导出能力,但不能转让所有权;文件编辑者可以编辑,导出能力由文件“权限设置”开关控制。系统管理员继续保留平台级完整访问能力。
|
||||
- 授予编辑者、管理者或转让所有权时,目标账号必须是当前项目的有效成员,并具备对应项目角色资格;不合格账号在联系人列表中不可选择,后端同时拒绝绕过界面的授权请求。
|
||||
- 文件管理者可开启“允许申请编辑权限”。只读项目成员可从 CTMS 顶栏或 ONLYOFFICE 的“请求编辑”入口提交申请;批准后写入文件级编辑者授权,拒绝或重复处理均有明确状态和审计记录。匿名链接访问者不能申请系统账号权限。
|
||||
- 顶栏铃铛通过通用通知 Feed 展示当前收件人的未关闭通知并记录已读状态;编辑权限申请通知可直接进入对应文件的“权限设置”,审批结果作为一次性信息通知申请人。AE、监查问题、文件回执和里程碑时效提醒也由同一 Feed 返回,具体规则见 `docs/project-notifications.md`。
|
||||
- Excel 文件可通过“允许所有协作者添加、删除工作表”控制工作簿结构。关闭时 CTMS 在新的不可变修订中启用工作簿结构保护并推进文件代次;重新开启时恢复文件原有的工作簿保护状态。该开关作用于所有协作者且不影响单元格内容编辑权限。
|
||||
- 仅文档所有者和系统管理员可转让所有权。目标联系人必须具备文件管理者资格;转让后目标联系人升级为文件管理者,原所有者保留管理者身份,所有权变更写入审计。
|
||||
- 文件编辑器“下载为”由文件导出规则判定:所有者和管理者始终允许,编辑者受文件开关控制,并由 CTMS 网页/桌面文件运行时保存到本机且记录下载审计。“另存为…”还会在项目工作区创建独立协作文件,因此额外要求项目级 `collaboration:create` 权限。
|
||||
|
||||
## 公开分享链接
|
||||
|
||||
- 文件管理者可开启链接,并选择只读或协作编辑、1 天/7 天/30 天/永久有效及可选访问密码;设置修改后自动保存,无需额外提交整张表单。
|
||||
- 文件操作菜单以一个“访问与权限”弹窗统一管理账号授权、匿名访问和“权限设置”;下载、打印、另存和复制不在两种访问方式下重复配置。
|
||||
- “权限设置”中的内容操作开关只作用于已授权编辑者和匿名访问者。所有者、文件管理者和系统管理员始终保留导出能力;外部链接不具备向项目工作区另存副本的权限。
|
||||
- 共享令牌只放在网页 URL 的 fragment(`#...`)中,不进入服务器请求路径、查询参数或本地缓存;前端调用公开 API 时通过会被审计层脱敏的请求头传递。
|
||||
- 每个文件首次创建分享记录后使用固定共享地址;修改权限、密码、有效期或关闭后重新开启都不会改变 URL。关闭期间同一地址暂停访问,重新开启后恢复访问。
|
||||
- 链接密码仅保存 bcrypt 哈希;验证成功后签发短时、链接记录绑定的内存访问凭证。连续错误达到阈值后链接密码验证会临时锁定。
|
||||
- 外部共享页不依赖 CTMS 登录状态,不提供工作区“另存为”;开启允许导出后可下载到访问者本机,编辑链接仍通过现有 ONLYOFFICE 会话写入不可变修订。
|
||||
- `JWT_SECRET_KEY` 轮换属于平台级全局失效操作,会使现有公开分享链接和短时访问凭证失效;生产环境必须按安全变更流程评估影响并提前通知链接使用者。
|
||||
|
||||
## 保存流程
|
||||
|
||||
1. 浏览器请求 `/api/v1/studies/{study_id}/collaboration/files/{file_id}/editor-config`。
|
||||
2. 后端生成带 JWT 的编辑配置、内部内容地址和回调地址。
|
||||
3. Document Server 从内部内容接口读取当前修订。
|
||||
4. 共同编辑期间使用 fast 模式;强制保存产生新修订并更新会话恢复基线。
|
||||
5. 最后一位编辑者退出后,状态 2 回调产生最终修订并推进文件代次;下一次编辑使用新的 `document.key`。
|
||||
6. 重复回调通过指纹幂等处理;旧代次回调不会覆盖当前文件。
|
||||
|
||||
内部内容和回调接口不经过 Nginx 公网入口,只接受 `AuthorizationJwt`。回调结果文件仅允许从配置的 Document Server 内部源获取,禁止重定向、凭据 URL 和任意主机。
|
||||
|
||||
## 本地开发
|
||||
|
||||
标准开发安装会直接启动 ONLYOFFICE:
|
||||
|
||||
```bash
|
||||
bash scripts/install.sh dev
|
||||
```
|
||||
|
||||
安装脚本会生成或复用独立的开发 JWT 密钥和稳定实例标识,默认启动 Document Server,并验证后端配置和 HTTP 健康状态。`bash scripts/onlyoffice-dev-up.sh` 仅用于开发环境单独重建、验证或轮换密钥。协作修订保存在现有后端上传卷下的独立 `collaboration/` 目录。
|
||||
|
||||
关键配置:
|
||||
|
||||
- `ONLYOFFICE_ENABLED`
|
||||
- `ONLYOFFICE_JWT_SECRET`
|
||||
- `ONLYOFFICE_INTERNAL_URL`
|
||||
- `ONLYOFFICE_STORAGE_BASE_URL`
|
||||
- `ONLYOFFICE_INSTANCE_ID`
|
||||
- `ONLYOFFICE_CONFIG_TTL_SECONDS`
|
||||
- `COLLABORATION_MAX_FILE_BYTES`
|
||||
|
||||
生产标准安装同样包含 ONLYOFFICE;正式上线前必须按既有发布要求确认镜像许可、独立 JWT 密钥、内部网络、容量、备份与监控。
|
||||
|
||||
参考:
|
||||
|
||||
- [ONLYOFFICE 共同编辑模式](https://api.onlyoffice.com/docs/docs-api/get-started/how-it-works/co-editing/)
|
||||
- [ONLYOFFICE 回调处理器](https://api.onlyoffice.com/docs/docs-api/usage-api/callback-handler/)
|
||||
- [ONLYOFFICE 文档权限](https://api.onlyoffice.com/docs/docs-api/usage-api/config/document/permissions/)
|
||||
@@ -0,0 +1,72 @@
|
||||
# 项目提醒中心
|
||||
|
||||
## 产品边界
|
||||
|
||||
项目提醒是既有业务状态面向明确收件人的可操作投影,不是独立任务、日历或聊天系统。源业务记录和业务审计始终是权威数据;提醒的阅读、桌面投递均不能替代 AE 上报、问题关闭、文件回执、审批或其他业务动作。
|
||||
|
||||
当前范围只包括已认证在线客户端:
|
||||
|
||||
- 网页端和桌面端共用当前项目的通用提醒 Feed。
|
||||
- 顶栏铃铛展示摘要,`/project/notifications` 提供当前提醒、未读和待处理筛选。已经解决或因权限变化失效的提醒不再向客户端返回。
|
||||
- 桌面系统通知是用户主动开启的可选投递渠道,只显示不含项目、文件、受试者或风险详情的通用文案。
|
||||
- 不提供离线提醒、本地业务权威数据、个人自由定时任务、短信、即时通讯或浏览器 Push。
|
||||
- 管理员权限监控告警、登录会话提醒和客户端更新“稍后提醒”不进入业务提醒中心。
|
||||
|
||||
## 状态模型
|
||||
|
||||
`notifications` 是收件人级通用提醒表:
|
||||
|
||||
- `read_at`:用户已经看过提醒。
|
||||
- `resolved_at`:源业务已经完成、关闭、失效,或一次性信息通知已经被阅读。
|
||||
- `requires_action`:区分业务待办和一次性信息通知。
|
||||
- `due_at`:用于展示业务截止时间;业务规则仍以源记录为准。
|
||||
- `source_type`、`source_id`、`source_version` 和 `dedupe_key`:用于来源追踪、幂等同步、阶段升级和自动关闭。
|
||||
|
||||
阅读待处理提醒不会写入 `resolved_at`。业务动作完成后由对应服务即时关闭提醒,定时同步还会对遗漏或权限变化进行对账。一次性信息通知在首次阅读时归档。
|
||||
|
||||
桌面投递状态继续保存在 `desktop_notification_deliveries`,但通过 `notification_id` 关联通用提醒。投递成功只记录 `delivered_at`,不自动标记业务提醒已读或已处理;提醒升级或重新打开时可以重新进入桌面投递队列。
|
||||
|
||||
桌面客户端每 60 秒执行一次在线投递检查:先确认本机系统权限,再读取当前账号的服务端订阅,随后领取待投递提醒、调用系统通知并确认实际成功投递的提醒。任一系统通知调用失败时不会确认该条提醒;网络或投递失败采用指数退避,最长 15 分钟。用户可在“设置 → 通知”查看本次客户端运行期间的最近检查、最近投递和当前链路状态,并主动执行一次真实业务提醒检查。
|
||||
|
||||
## 当前规则
|
||||
|
||||
| 来源 | 收件人 | 触发和升级 | 自动关闭 |
|
||||
|---|---|---|---|
|
||||
| 协作编辑申请 | 文件所有者、文件管理者 | 申请创建 | 申请批准或拒绝 |
|
||||
| 协作申请结果 | 申请人 | 申请批准或拒绝 | 申请人阅读后归档 |
|
||||
| 文件分发与回执 | 指定用户、指定角色、中心联系人 | 新分发;3 天内到期;逾期 | 用户完成接收回执、分发关闭或权限失效 |
|
||||
| AE 上报时效 | 具备 AE 读取权限且在中心范围内的项目成员 | 3 天内到期;逾期;按数量增加重新提醒 | 数量归零或权限失效 |
|
||||
| 监查问题整改 | 具备监查问题读取权限且在中心范围内的项目成员 | 3 天内到期;逾期;按数量增加重新提醒 | 数量归零或权限失效 |
|
||||
| 项目里程碑 | 里程碑负责人 | 7 天内到期;逾期;日期或状态变化重新提醒 | 完成、负责人变化或日期移出提醒窗口 |
|
||||
| 受试者访视窗口 | 具备访视更新权限的有效项目成员;CRA 仅限负责中心 | 窗口开始前 3 天、窗口进行中;错过窗口后升级 | 录入实际访视、取消访视、受试者结束、中心停用或权限/中心范围失效 |
|
||||
|
||||
所有规则都要求当前有效项目成员和对应业务权限。系统管理员不会仅因平台权限而批量接收所有项目提醒;需要作为有效项目成员或明确业务收件人进入提醒范围。
|
||||
|
||||
日期型 AE、里程碑和访视窗口规则按 `Asia/Shanghai` 业务日计算,带时区的截止时间统一按 UTC 存储和比较。若未来引入项目级时区,应由项目配置替代固定业务时区,不能使用浏览器本地时区驱动服务端规则。
|
||||
|
||||
访视提醒按访视记录生成并可跳转到对应受试者详情,但提醒标题和正文不包含受试者编号、姓名、中心名称或其他可识别信息。收件人资格以 `visits:update` 及其前置权限为准,避免仅具备只读权限的 PV、QA、CTA 等角色被动接收受试者访视提醒。
|
||||
|
||||
## 同步与诊断
|
||||
|
||||
服务端启动提醒同步任务,默认每 300 秒对所有有效项目成员执行幂等同步;间隔通过 `NOTIFICATION_SYNC_INTERVAL_SECONDS` 配置,允许 60–3600 秒。PostgreSQL advisory lock 防止多个后端进程重复执行全量任务。
|
||||
|
||||
用户读取 Feed 时还会执行一次当前用户轻量对账,用于弥补短时任务失败。单个成员同步失败不会中断其他成员;失败写入 `ctms.notifications` 日志。提醒创建、更新和自动关闭本身不替代源业务审计。
|
||||
|
||||
## 桌面隐私边界
|
||||
|
||||
系统通知固定为:
|
||||
|
||||
- 标题:`CTMS 待办提醒`
|
||||
- 正文:`有新的业务提醒待查看`
|
||||
|
||||
系统通知 API 不接受动态标题或正文。项目名、文件名、版本、受试者、AE、监查问题等详情只允许在完成 token 校验和项目权限确认后的应用内 Feed 中展示;访视提醒即使在应用内 Feed 也不直接显示受试者标识,只通过受控详情路径访问。
|
||||
|
||||
## 系统通知授权边界
|
||||
|
||||
系统权限和服务端订阅是两个独立条件:
|
||||
|
||||
- 首次开启时,客户端先请求操作系统授权;只有操作系统返回已授权,才开启当前账号的服务端订阅。
|
||||
- 用户在操作系统中撤销权限后,服务端订阅可以仍然保持开启,但客户端不会领取提醒,设置页会明确显示“系统权限已拒绝”。
|
||||
- macOS 或 Windows 已拒绝权限时,系统通常不会再次展示授权框。设置页分别引导用户前往“系统设置 → 通知 → CTMS”或“设置 → 系统 → 通知 → CTMS”,返回后通过“重新检测权限”恢复链路。
|
||||
- “发送测试通知”只验证本机通知能力,不访问业务提醒队列;“立即检查业务提醒”会走服务端订阅、领取、系统投递和确认的真实路径。
|
||||
- CTMS 不申请打开任意 URL、执行系统命令或读取操作系统通知内容的额外 Tauri 权限。授权引导仅展示设置路径,避免为便利入口扩大桌面原生能力边界。
|
||||
Reference in New Issue
Block a user