diff --git a/.github/workflows/release-all.yml b/.github/workflows/release-all.yml index 666d4b318..601c3bbbf 100644 --- a/.github/workflows/release-all.yml +++ b/.github/workflows/release-all.yml @@ -545,12 +545,40 @@ jobs: set -euo pipefail owner="$(printf '%s' "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')" tag="${{ steps.release-version.outputs.tag }}" + prerelease_input="${{ github.event.inputs.prerelease || 'auto' }}" + + case "$prerelease_input" in + true) + prerelease=true + ;; + false) + prerelease=false + ;; + auto) + if [[ "$tag" == *-* ]]; then + prerelease=true + else + prerelease=false + fi + ;; + *) + echo "invalid prerelease input: $prerelease_input" >&2 + exit 1 + ;; + esac + + publish_tags=("$tag") + if [[ "$prerelease" == "false" ]]; then + publish_tags+=(stable latest) + fi - docker tag codexmanager-service:release "ghcr.io/${owner}/codexmanager-service:${tag}" - docker tag codexmanager-web:release "ghcr.io/${owner}/codexmanager-web:${tag}" + for publish_tag in "${publish_tags[@]}"; do + docker tag codexmanager-service:release "ghcr.io/${owner}/codexmanager-service:${publish_tag}" + docker tag codexmanager-web:release "ghcr.io/${owner}/codexmanager-web:${publish_tag}" - docker push "ghcr.io/${owner}/codexmanager-service:${tag}" - docker push "ghcr.io/${owner}/codexmanager-web:${tag}" + docker push "ghcr.io/${owner}/codexmanager-service:${publish_tag}" + docker push "ghcr.io/${owner}/codexmanager-web:${publish_tag}" + done - name: List release assets run: find release-assets -type f | sort diff --git "a/.teamwork/discussions/2026-07-25_\344\270\212\346\270\270v0.5.0\345\267\256\345\274\202\345\267\241\346\243\200.md" "b/.teamwork/discussions/2026-07-25_\344\270\212\346\270\270v0.5.0\345\267\256\345\274\202\345\267\241\346\243\200.md" new file mode 100644 index 000000000..78b4e2d39 --- /dev/null +++ "b/.teamwork/discussions/2026-07-25_\344\270\212\346\270\270v0.5.0\345\267\256\345\274\202\345\267\241\346\243\200.md" @@ -0,0 +1,68 @@ +# 2026-07-25 上游 v0.5.0 差异巡检结论 + +## 巡检背景 + +- 旧基准:`upstream/main = a614b559`(记录于 2026-07-07 差异结论)。 +- 新基准:`upstream/main = e630b349 修改nginx的配置`(对应 tag v0.5.0,2026-07-23)。 +- 增量规模:`a614b559..e630b349` 新增 **223 个提交**(含 merge);总分叉 upstream 领先 CE 420、CE 领先 upstream 284。 +- CE 定位不变:本地桌面端 + service/web 的 Codex 账号池管理器 + 网关转发;剥离作者推广、赞助导流、远程 author content、AtomGit 推广、发行渠道宣传。 +- 合并策略不变:只做语义移植,不直接 merge upstream,逐提交按"业务能力 / UI 样式 / 推广内容 / 发行渠道"拆分。 + +## CE 现状核对(已读代码确认) + +| 能力 | CE 现状 | 上游现状 | +| --- | --- | --- | +| 模型目录 | 自研 `model_catalog_models`(迁移 047)+ `model_price_rules` / `model_sources` / `model_groups` / `model_options` | `model_catalog_v2` 事务存储 + 不可变整数价格快照 | +| 账号级代理 | 无 proxy 模块 | `account_proxy` + `proxy_testing/jobs` + ipwhois 地理 | +| Codex Skills | `cm-skills/cm-imagegen` 仅图片生成 | skills marketplace / 仓库安装 / skills.sh | +| device_code 登录 | 已有(20 处,auth_login/auth_tokens/rpc) | 有增量 | +| sub2api 导入 | 已有(account_import + add-account-modal) | 新 JSON 格式 + agent_identities 加固 | +| 网关 aggregate/proxy 语义 | 已有(protocol/aggregate_api、proxy.rs、candidates) | hybrid rotation 尊重纯聚合路由 | +| Lite 模式图片工具冲突 | `codex_headers.rs` 未处理 | 已修 | +| reset credit / hourly usage / automatic update / codex_projects / AboutCodexManagerCard | 均无 | 均有 | + +## 分类清单 + +### 🟢 可复用(低风险语义移植,本轮执行) + +| 上游提交 | 说明 | 置信度 | +| --- | --- | --- | +| `22a47d89` | avoid Responses Lite conflict with auto-injected image tool,纯网关 header 修复(42 行),CE 缺失 | 高 | +| `a127e27a` | honor aggregate-only routes in hybrid rotation,CE 有 aggregate/proxy 语义,修复混合轮换绕过纯聚合路由 | 中 | +| `2c61d38e` / `afe86293` | 桌面 dev server 清理 / NVM 环境保留,启动器健壮性小项 | 中 | + +### 🟡 需移植(有对应架构但命名/结构不同,需语义改写) + +| 上游提交 | 说明 | 置信度 | +| --- | --- | --- | +| `2c1fa090` + `715eb90a` | GPT-5.6 官方定价对齐 + GPT Image 2,CE 走自研 `model_price_rules`,需按 CE 结构补种子,不照搬 v2 迁移 | 高 | +| `692ab34e` + `482f7ffa` | 新 sub2api JSON 格式 / 加固导入,CE 有 sub2api 导入,但上游依赖 `agent_identities` 表(迁移 122),需评估是否连带引入 | 中 | +| `7796f900` | keep image generation connections alive,依赖上游 `transport-settings.ts`(CE 无),需落到 CE 网关设置 | 中 | +| Device Code 增量 | CE 已有 `device_code`,需 diff 上游是否有超出部分 | 低(待核对) | + +### 🔴 需重构(架构分叉大,禁止整包,逐能力/页面级评估) + +- **model_catalog_v2 全族**:`f0b5deb3` / `3bec379f` / `a6e44682` / `c9f59d18` / `90635813` / `18fbb6ab` 等。CE 走自研 catalog,v2 事务存储 + 不可变整数价格快照与 CE 冲突,需逐能力语义比对。 +- **账号级代理 + ipwhois 地理**:`56c7c418` / `585a8f8a` / `4e8f4542` / `13889ff` 等约 15 提交。CE 完全无 proxy 模块,属大特性,需独立设计评审。 +- **Codex Skills 市场/仓库/skills.sh**:`22256fe1` / `cc1a0e5c` / `d733b884` / `3edb03de`。CE `cm-skills` 仅 imagegen。 +- **reset credit 额度重置**:`1197113d` / `b38691ca` / `ca93803d`。CE 无该服务流与 UI。 +- **hourly/bucketed 用量分析 + 交互曲线**:`07cc39a4` / `76d7090f` / `6a372450`。CE 无分桶用量分析数据模型。 +- **automatic update checker**:`04deb7a` / `27300e9f` 族。清理版是否需要自动更新需先定性。 +- **Codex 项目启动器 + 账号组路由**:`0c957303`(`codex_projects.rs` 1459 行)。CE 无,大特性。 +- **keep window UI mounted**:`f2131ca3` / `f2887c19`。性能特性,需结合 CE 渲染优化重估。 + +### ⚫ 不移植(推广/发行内容,符合 CE 剥离原则) + +- `a555d900` / `a670ec09` AboutCodexManagerCard(作者信息卡 + i18n)。 +- `e630b349` nginx 配置、`65b935e4` / `3d72ffe5` release、`f571de28` sponsor copy 精简。 +- README 链接整理类。 + +## 本轮执行计划 + +按风险从低到高执行 🟢 项,逐项独立 commit + 定向测试: + +1. `22a47d89` Lite/图片工具冲突(纯 header,风险最低)。 +2. `2c1fa090` GPT-5.6 定价(按 CE `model_price_rules` 结构)。 +3. `a127e27a` 聚合路由混合轮换。 + +🟡 中风险项与 🔴 重构项在本轮后按需另立子任务评估。 diff --git "a/.teamwork/discussions/2026-08-06_\344\270\212\346\270\270v0.5.2\345\220\216\347\273\255\345\267\256\345\274\202\345\267\241\346\243\200.md" "b/.teamwork/discussions/2026-08-06_\344\270\212\346\270\270v0.5.2\345\220\216\347\273\255\345\267\256\345\274\202\345\267\241\346\243\200.md" new file mode 100644 index 000000000..3a9c71662 --- /dev/null +++ "b/.teamwork/discussions/2026-08-06_\344\270\212\346\270\270v0.5.2\345\220\216\347\273\255\345\267\256\345\274\202\345\267\241\346\243\200.md" @@ -0,0 +1,94 @@ +# 上游 v0.5.2 后续差异巡检 + +审计身份:【CodeX-GPT】 + +## 基准证据 + +- 2026-08-06 已执行 `git fetch upstream --prune --tags` 与 `git ls-remote upstream`;两者一致,最新 `upstream/main` 为 `cdf0c5f2 docs: adjust sponsor listing`,即 `v0.5.2-19-gcdf0c5f2`,没有未拉取的新提交。 +- `v0.5.2..upstream/main` 共 19 个提交;`e630b349..upstream/main` 共 95 个图提交,其中 76 个非合并、19 个合并。 +- 当前 merge-base 为 `2c43779c`;以本轮审计起点 `854cb987` 统计,CE 独有 313、上游独有 515。不能直接 merge 或整包 cherry-pick。 +- 2026-08-07 再次 fetch 后,`upstream/main` 仍为 `cdf0c5f2`,正式发布仍为 `v0.5.2`;`#422`(`01c83c1b`)和 `#423`(`dc35d40f`)未变化,`#421` 已从 `41da1800` 更新为 `e3e5eb0b`,三者均未进入 `upstream/main`。 + +## 已完成语义移植 + +1. `c98047eb`:原生会话锚点与 `prompt_cache_key` 对齐。 +2. `3cd72771`:失败请求保留诊断明细但排除 Token、费用和配额汇总。 +3. `6d72131e`:按真实窗口时长应用配额保护,并隔离单账号并发设置。 +4. `0f4f79bd`:按 CE 发布 workflow 同步正式容器稳定标签。 +5. `552e3f4d`:按 CE 日志页布局加入分页跳转与窄内容区换行。 +6. `2aa9bec3`:补齐 `login_sessions.group_name` 的 CE 兼容迁移及存取 SQL。 +7. `7716828f`:failover 删除账号绑定的完整 reasoning/加密/compaction 历史项,兼容缺少 `encrypted_content` 的空壳。 +8. `85cf8c73`:WebSocket 遵循应用代理优先、标准环境代理回退和 `NO_PROXY` 旁路规则。 +9. `1354b888`:provider 配置只覆盖 CE 管理字段,保留用户自定义字段。 +10. CE 自有收口:现代主题 chart tooltip 禁用背景模糊,窄屏自动收起侧边栏并修正主内容/加载遮罩宽度;账号工具栏回归覆盖 K12、不可用筛选和重置时间换行。 +11. `646095c4` 语义移植:按 CE 列、菜单和分页拆分账号主表与固定操作栏,系统 Chrome 覆盖窄屏、横向滚动、动态行高、空状态与分页。 +12. `a127e27a` 相关风险收口:使用真实 SQLite/HTTP 和 CE 模型来源映射覆盖纯聚合、账号轮转拒绝聚合模型、账号 404 后聚合兜底,不引入上游 `model_catalog_v2`。 +13. `84802156` 拆分移植:仅采用桌面文件日志上限,单文件 8 MiB、当前文件加最多 4 个归档;不引入耦合的启动备份、诊断 RPC 与设置 UI。 +14. PR `#421` 语义移植:保留 CE 全局单代理优先于稳定代理池的既有规则,补齐环境/Windows/macOS 静态系统代理、连接阶段超时和失败日志;无效显式代理 fail-closed,代理凭据统一脱敏。 +15. 蓝军审计后的 WebSocket 边界修补:401 后令牌刷新改为受连接截止时间约束的阻塞任务;系统代理 matcher 的 Basic/SOCKS 凭据在转换为 WebSocket 代理 URL 时保留;路由查询连接不跨越网络握手生命周期持有,避免单连接池配置下刷新自相等待。 +16. PR `#421` 最新增量 `e3e5eb0b`:按所选平台 Key 的真实路由能力写入 `supports_websockets`,服务启动和相关 Key 路由设置变化时仅在内容变化后修复旧 profile;不复制上游自动终止 Codex `app-server` 的实现。 + +## 已确认不需移植 + +- `454fe220` 的最终托盘语义已在 CE 实现;`06b141f9` 是其前置中间修复,不单独移植。 +- `2a252139` 的 `startup.html` 开发态导航不适用,CE 窗口直接使用开发服务地址;`1dbe17da` 的 3005->3006 自建开发代理也不存在于 CE。 +- 作者页、赞助/AtomGit/远程 author content、整包 README/docs 推广不移植;README 的 Linux.do 认可社区入口保留。 +- `15a90056` / `ee62ccf4` 的入站 zstd 解压边界不适用 CE 当前无入站解压路径;`9c6ab64d` 删除图片工具自动注入与 CE 已确认的图片兼容行为冲突。 +- `02c17c45`、上游 model catalog 所有权/导出重构、Skills 市场和项目启动器均过大或与 CE 架构不一致,只抽取可复现的小问题。 + +## 待处理与条件评估 + +| 优先级 | 上游提交/主题 | 当前结论 | +| --- | --- | --- | +| P2 | `e44bfe35` 持久化 UI 缩放 | 功能尚未实现,独立评估 75%~125% 与 Tauri 主窗口持久化。 | +| P2 | `1617272a` GPT-5.6 价格 | 上游数值与 CE 已确认规则冲突,等待可核验官方来源。 | +| P2 | PR `#422` 模型级 Fast 策略 | 先明确模型策略、API Key 覆盖、客户端字段和聚合路由的最终优先级,再按 CE 迁移序列设计。 | +| P2 | PR `#423` 短期/小时用量趋势 | 短期预设可按 CE 日级接口单独评估;默认小时曲线依赖上游模型序列接口和前端组件,不能只移植前端补丁。 | + +## 开放 PR 增量(尚未进入上游 main) + +| PR | 规模 | CE 独立审计结论 | +| --- | --- | --- | +| `#421` / `e3e5eb0b` Responses WebSocket 与 profile 启动同步 | 相对 `upstream/main` 为 12 文件,+460/-40 | 已完成 CE 语义移植并补强显式代理 fail-closed、畸形代理凭据脱敏、终态 failover 超时和 profile 能力同步;蓝军发现的 401 刷新超时和系统代理认证丢失也已修正。上游启动时无条件原子重写配置后再按 mtime 终止旧进程,会把无内容变化的 Manager 重启也转化为 Codex 会话中断,CE 明确不采用。 | +| `#422` / `01c83c1b` 模型级 Fast 策略 | 16 文件,+557/-20 | 功能方向可用于 Luna/Terra 等模型,但上游依赖其迁移号 `129` 与 model catalog 结构;CE 当前迁移序列只到 `078`。此外 `block` 仍允许 API Key 内部 Fast 覆盖,与“模型级最终决策”描述冲突,先保留为 P2 设计复核。 | +| `#423` / `dc35d40f` 管理员短期用量与趋势稳定性 | 6 文件,+85/-5 | 新增当天/滚动 24 小时/72 小时预设、默认小时粒度及 Recharts `Brush` 索引修复;CE 当前 RPC 仅返回日级 `dailyUsage`,且内联 `AreaChart` 没有对应 `Brush`,不能直接 cherry-pick。 | + +### `#423` 的 CE 适配边界 + +1. “当天”与滚动 24/72 小时预设可以复用 CE 已支持的任意时间范围查询,但必须补充跨自然日、DST 和空数据测试。 +2. “默认按小时”不是纯 UI 选项。上游依赖 `seriesUsage`、`seriesBucketSeconds` 和模型序列类型;CE 需要先设计连续 RPC 字段、存储查询成本与 Web/Tauri 兼容,再决定是否实现。 +3. 上游 `finiteChartIndex`、图表重建 key 和 `Brush` 防御只针对上游 `AdminUsageTrendChart`,对 CE 当前 `DailyTokenLineChart` 不构成可直接移植的修复。 + +### `#421` 必须保留的 CE 边界 + +1. 代理优先级保持 CE 既有语义:“CodexManager 全局单代理 > 账号稳定分流代理池 > 环境代理 > Windows/macOS 静态系统代理 > 直连”。 +2. 无效显式代理不得因系统代理可用而静默掩盖;错误内容不得泄露代理凭据。 +3. 首帧校验通过后即初始化日志;TCP、代理隧道或 WebSocket 握手失败/超时都必须产生可见终态。 +4. 连接超时复用现有 `upstream_connect_timeout`,不得影响 HTTP/SSE 与流式空闲超时。 +5. `supports_websockets` 必须由当前平台 Key 的协议、轮转方式和实际上游共同决定;修复旧 profile 时先比较内容,禁止仅因 Manager 重启刷新 mtime,更禁止自动终止用户正在运行的 Codex 进程。 + +### `#422` 实施前未决语义 + +1. `block` 是禁止客户端字段,还是禁止最终上游 `priority`;两者不能混为一个策略名称。 +2. 模型策略、API Key 覆盖和客户端字段的优先级必须在日志 `service_tier_source` 中可追溯。 +3. 纯聚合、账号池和混合路由都需基于最终选中模型应用同一策略,不能把不存在于 CE 目录的上游模型误判为 404。 +4. 迁移必须使用 CE 连续版本号并覆盖旧数据库默认 `passthrough`,不得照搬上游文件名。 + +## 开放 Issue 信号 + +- 上游当前有 13 个开放 Issue,没有对应的新主分支提交;它们是风险线索,不等于可直接移植的实现。 +- `#394`:用户从 `v0.4.0` 升级后桌面端无响应,上游讨论怀疑旧数据库兼容,但尚无复现证据或迁移修复;删除应用数据会造成配置丢失,CE 不采用该处理方式,继续以迁移回归和启动日志定位。 +- `#258`:账号轮询或聚合 API 路由持续 503,未关联 PR;CE 已通过真实 Storage 模型映射和混合回退回归覆盖对应本地风险,仍需等待上游新的可复现证据。 +- `#284`:Claude Code 经网关出现约 300 秒 thinking,讨论仍指向远端、网络或代理链路;CE 的 WebSocket 连接超时覆盖首次和 failover 的候选/令牌/代理/握手阶段,不替代流式空闲超时与 SSE 保活。 +- `#252`:Windows 11 锁屏恢复后 UI 停滞仍无稳定复现,且有 `v0.5.1` 不复现反馈;没有可验证补丁,保留观察而不做猜测性修改。 + +## 验证记录 + +- `cargo test -p codexmanager-service --lib -- --test-threads=1`:1096 通过、0 失败。 +- service 集成测试中 `gateway_logs` 29、`default_addr` 10、`e2e` 1、`rpc` 44、`shutdown_flag` 1 逐目标通过;`app_settings` 相关筛选中 env override 6、runtime sync 6、formal-use lock 1 均通过。`app_settings` 全目标运行至 13 项通过且无失败后触及 10 分钟门限,未将其记为 31 项全通过。 +- 其余 workspace 包通过:core 138 通过、1 忽略;web 18;start 2;db-optimize 8。 +- `cargo test -p codexmanager-service codex_profile --lib -- --test-threads=1`:15 通过、0 失败。 +- `cargo fmt --all --check`:通过。 +- `corepack pnpm run test:runtime`:117 通过、0 失败。 +- `apps` TypeScript、相关 ESLint、生产构建通过;系统 Chrome 下 `accounts-action-rail` 与 `accounts-toolbar` Playwright `2/2` 通过,包含 390/640/1024px 固定操作栏、动态行高、K12、不可用筛选和重置时间布局。 +- `cargo check --manifest-path apps/src-tauri/Cargo.toml`:通过。 diff --git "a/.teamwork/discussions/2026-08-07_PR421\345\220\257\345\212\250\345\220\214\346\255\245\347\213\254\347\253\213\345\256\241\350\256\241.md" "b/.teamwork/discussions/2026-08-07_PR421\345\220\257\345\212\250\345\220\214\346\255\245\347\213\254\347\253\213\345\256\241\350\256\241.md" new file mode 100644 index 000000000..fe865f642 --- /dev/null +++ "b/.teamwork/discussions/2026-08-07_PR421\345\220\257\345\212\250\345\220\214\346\255\245\347\213\254\347\253\213\345\256\241\350\256\241.md" @@ -0,0 +1,181 @@ +# PR #421 启动同步独立审计 + +## 审计范围 + +- 上游提交:`e3e5eb0b024660c9c7176701070afcb45af47077`(`fix(gateway): sync WebSocket profile and stale app-server on startup`)。 +- CE 基线:分支 `audit/fix-followups`,任务开始时 `HEAD=854cb98754845f8c2f7389d27747a8cde7995900`。 +- 审计方式:静态阅读提交、CE 基线、审计期间出现的未提交候选补丁,以及 `sysinfo 0.30.13` 平台实现。为避免误终止用户正在运行的 Codex 进程,未执行真实进程终止测试。 +- 审计期间检测到其他执行方正在修改 `crates/service/src/codex_profile.rs`、`crates/service/src/apikey/apikey_update_model.rs`、`crates/service/src/lifecycle/startup.rs`。这些改动不视为 CE 基线,本文单独标为“在途候选补丁”。 + +## 结论 + +**裁决:部分语义移植,不直接移植自动终止进程方案。置信度:高。** + +应移植的语义: + +1. 为 Manager 管理的 `[model_providers.cm]` 显式写入 `supports_websockets`。 +2. 该值必须调用 CE 已有的 `gateway_supports_official_responses_websocket(&ApiKey)` 统一判定,不能仅因本地网关支持 WebSocket 就硬编码为 `true`。 +3. 服务正常启动时,在数据库初始化、环境配置加载和持久化运行时设置恢复完成后,对仍由 Manager 管理的活动 gateway profile 做一次**内容感知**的配置修复。 +4. 活动平台 Key 的路由策略、显式上游地址或相关协议配置改变后,重新计算活动 profile 的能力字段;全局 `CODEXMANAGER_UPSTREAM_BASE_URL` 热更新也会影响无显式上游地址的 Key,需要纳入触发边界。 + +不应移植的内容: + +1. 不移植 `reload_stale_codex_app_servers` 的启动时自动进程扫描和终止。 +2. 不以 `config.toml` 的 mtime 与进程秒级启动时间作为自动终止依据。 +3. 不在启动路径使用 `System::new_all()` 扫描完整系统信息。 +4. 不在 Windows 上通过 `Signal::Term` 失败后回退到强制 `kill()`。 +5. 不重新读取活动 profile 状态来决定终止对象;上游该做法存在跨进程状态变化时的目标漂移窗口。 + +## 关键发现 + +### 1. `supports_websockets` 的写入是必要修复,但上游提交依赖 CE 尚未具备的前置语义 + +上游 `e3e5eb0b` 新增的启动同步建立在此前已有实现之上: + +- `crates/service/src/codex_profile.rs::patch_config_for_gateway` 已接收 `supports_websockets: bool`。 +- `crates/service/src/codex_profile.rs::gateway_supports_websockets` 从存储读取平台 Key。 +- `crates/service/src/gateway/mod.rs::gateway_supports_official_responses_websocket` 执行统一能力判定。 + +CE 基线中的 `codex_profile.rs::apply_gateway` 和 `patch_config_for_gateway` 只写 `base_url` 与 `wire_api = "responses"`,没有写 `supports_websockets`。因此只移植 `e3e5eb0b` 的启动函数不能闭环,首次应用 gateway profile 时仍会生成缺少能力字段的配置。 + +CE 的统一判定函数当前采用以下保守语义: + +- `/v1/responses` 必须解析为 OpenAI 兼容协议; +- `aggregate_api` 与 `hybrid` 路由明确返回 `false`; +- 仅有效上游为 ChatGPT backend 时返回 `true`。 + +这与 `crates/service/src/http/responses_websocket.rs::authorize_websocket_request` 的实际路由能力门禁一致,也与现有 `hybrid_responses_websocket_returns_426` 测试方向一致。尤其不能因为 CE 支持混合模式就把混合模式的 WebSocket 能力错误标为 `true`;当前混合模式仍没有 WebSocket 聚合上游 failover 语义。 + +建议写入时机: + +1. `apply_gateway` 已完成 Key 存在性、启用状态、secret 非空校验后,直接使用同一个 `ApiKey` 计算能力,再原子写入配置。 +2. 平台 Key 更新完成后,仅当活动 profile 指向同一 Key,且 `rotation_strategy`、`upstream_base_url` 或未来真正参与该判定的协议字段发生变化时同步。 +3. 启动同步必须位于 `start_server` 中的 `sync_runtime_settings_from_storage()` 之后,确保无显式上游地址的 Key 使用恢复后的全局上游配置判定。 +4. 不应放进 `start_one_shot_server`,避免 RPC 测试和一次性服务产生 profile 写入副作用。 + +补充边界:平台 Key 更新函数当前由多次数据库写入组成。若数据库更新已经成功,随后 profile 文件同步失败,不应简单用 `?` 把整个 RPC 表述为“更新失败”,否则用户会重试一个事实上已部分成功的操作。最小方案应将 profile 同步作为带结构化警告的 best-effort 后置步骤,或让接口明确返回“数据库已更新、profile 同步失败”。 + +### 2. 启动时 profile 重同步应当移植,但必须改成“仅内容变化时写入” + +上游新增: + +- `codex_profile.rs::sync_active_gateway_profile_config_from_storage` +- `codex_profile.rs::active_gateway_profile_dir` +- `lifecycle/startup.rs::sync_active_gateway_profile_config_on_startup` + +其正面价值是修复升级前已存在、仍缺少 `supports_websockets` 的 gateway profile,并且启动错误只记录 warning,不阻止服务监听。 + +但上游函数无论内容是否变化都会调用 `write_atomic` 并返回 `Ok(true)`。随后 `sync_active_gateway_profile_config_on_startup` 读取刚更新的 mtime 并执行旧进程终止。这意味着:只要持久化状态仍为 gateway,**每次 CodexManager 启动都会把所有更早启动且共享该 `CODEX_HOME` 的 Codex app-server 判为 stale**,即使配置语义完全没变。 + +最小移植必须满足: + +- 解析并补丁当前 TOML 后,先比较输出与原内容;完全相同则不写文件、不改变 mtime,返回 `updated=false`。 +- `bool` 的含义应是“内容发生变化”,不能混用为“存在活动 gateway profile”。更稳妥的是返回包含 `active`、`updated`、`profile_dir`、`warning` 的结构体。 +- 启动同步只修复 Manager 拥有的字段:`model_provider`、`model_providers.cm.base_url`、`wire_api`、`supports_websockets`;保留同表中的用户扩展字段。 +- 使用已有原子写入,失败只告警,不阻止服务启动。 +- 在依据持久化状态写文件前,至少核验 Manager marker 的 `writer`、`mode`、`api_key_id` 与 profile 路径,避免数据库复制、手工恢复或陈旧状态重新接管不再由 Manager 管理的配置。 +- 直接使用已经打开的存储读取 `codex_profile.state`,让 JSON 解析错误显式返回;不要通过会再次打开存储且吞掉解析错误的 `load_state()` 间接读取。 + +审计时观察到的在途候选补丁已经采用“传入同一存储、内容相同不写、启动不终止进程”的方向,整体符合本结论;仍需补齐 marker 所有权校验、全局上游热更新触发和测试。 + +### 3. 不通过 `config.toml` mtime 自动终止 app-server + +上游 `crates/service/src/codex_runtime.rs` 新增: + +- `reload_stale_codex_app_servers` +- `matching_app_server_pids` +- `signal_root_processes` +- `process_started_at_or_before_config` +- `resolve_codex_home_from_environment` +- `same_path` + +命令名、`app-server` 精确参数和 `CODEX_HOME` 路径三重过滤使普通误匹配概率不高,但仍不足以支撑无确认的自动终止,主要风险如下。 + +#### 误杀与行为风险 + +- `process.start_time()` 只有秒级精度,上游将“与配置同一秒启动”的进程也视为 stale。配置写入后、同一秒内启动的新进程可能被误终止。 +- 系统时间调整、未来 mtime、FAT/exFAT 或网络文件系统时间粒度,会导致错误命中或漏判。 +- 从进程快照筛选到按 PID 发信号之间存在 TOCTOU;极端情况下 PID 已复用,代码没有在发信号前重新核验启动时间、命令和环境。 +- 未设置 `CODEX_HOME` 时回退到 `USERPROFILE`/`HOME`,会命中所有共享默认 `~/.codex` 的 Codex 客户端。它们未必属于启动本次 Manager 的用户工作流,但会一起中断。 +- 相对路径、符号链接、Windows 短路径及 canonicalize 失败主要造成漏判;若相对 `CODEX_HOME` 来自不同工作目录,还可能造成错误等价判断。 +- 启动同步后再次调用 `active_gateway_profile_dir()` 会二次读取状态。另一个 Manager 进程若恰好切换 profile,可能导致“写 A、按 B 的 mtime 终止 B”的目标漂移。 +- `start_server` 在真正绑定 HTTP 端口前执行该副作用。重复启动的第二个服务即使最终因端口占用失败,仍可能先终止 Codex app-server。 + +#### 跨平台与权限风险 + +- `sysinfo 0.30.13` 在 Windows 只支持 `Signal::Kill`;`Signal::Term` 返回 `None`。上游随即回退到 `process.kill()`,其 Windows 实现调用 `taskkill.exe /PID /F`,属于强制终止,不是温和重载。 +- Linux/macOS 使用 `SIGTERM`,会中断正在进行的会话;父客户端是否可靠重启 app-server 不在本提交的控制与测试范围内。 +- 读取其他权限级别进程的命令行或环境可能失败,导致静默漏判;发信号权限不足只产生 warning,此时配置已更新但旧 app-server 仍持有旧能力。 +- macOS 沙箱/App Store 构建中的进程信号能力受限;容器中还受 PID namespace 和挂载 profile 的关系影响。 + +#### 性能风险 + +- `System::new_all()` 等价于 `RefreshKind::everything()`,会刷新 CPU、内存和全部进程信息,而这里只需要少数进程字段。 +- 该全量扫描发生在 HTTP 服务开始监听前;进程较多、终端防护较重或权限查询较慢时,会直接增加启动延迟。 +- Windows 每个命中 PID 还会启动一次 `taskkill.exe` 子进程。 + +#### 测试缺口 + +上游新增测试只覆盖: + +- 秒级 stale 纯函数; +- Unix epoch 前时间; +- 配置文件缺失时跳过; +- 启动同步能够补入 `supports_websockets=true`。 + +没有覆盖: + +- 配置未变化时不改 mtime、不终止; +- Windows 强制终止语义; +- 权限不足、PID 复用、同秒新进程、未来 mtime; +- 多个共享 `CODEX_HOME` 的客户端; +- 用户关闭 reload 偏好后仍被启动逻辑终止; +- 重复 Manager 实例在端口绑定失败前产生副作用; +- Linux、Windows、macOS 的真实子进程集成行为。 + +上游原有 profile 切换已经提供 `reloadAfterSwitch` 用户偏好与 `runtimeReload` 结果反馈,而 `e3e5eb0b` 的启动自动终止绕过了该偏好。CE 当前又没有这套用户确认和结果展示协议,因此更不应只移植后端终止逻辑。 + +### 4. CE 已有更稳妥的运行时机制,但存在一个独立鉴权缺口 + +CE 已有机制: + +- `gateway::gateway_supports_official_responses_websocket` 是统一能力判定入口。 +- `http/responses_websocket.rs::authorize_websocket_request` 在每次 WebSocket 握手时重新读取实际平台 Key,并按当前路由能力返回 `426`;配置字段不是最终安全边界。 +- 普通 HTTP 请求通过 `gateway/local_validation/auth.rs::load_active_api_key` 校验平台 Key 必须为 `active`。 +- profile 写入使用 Manager marker、备份和原子替换;前端已有“重新应用本地网关”显式操作。 +- CE 当前不自动终止 Codex 进程,配置变化由用户重启会话生效。这比无确认强制终止更稳妥。 + +独立发现:`responses_websocket.rs::authorize_websocket_request` 在 `find_api_key_by_hash` 后没有像普通 HTTP 路径一样检查 `api_key.status == "active"`。因此已禁用 Key 仍可能进入 WebSocket 路由能力判定。该问题不是 `e3e5eb0b` 引入的,也不应通过把 status 混入 `supports_websockets` 能力字段来掩盖;应单独补齐 WebSocket 鉴权状态检查及“禁用 Key 返回 403”的测试。 + +## 建议的最小实现 + +1. 修改 `codex_profile.rs::patch_config_for_gateway`,新增显式 `supports_websockets: bool` 参数并保留非 Manager 专属扩展字段。 +2. 在 `apply_gateway` 校验 Key 和 secret 后,调用现有统一 helper 计算能力并写入配置。 +3. 新增只处理 config 的同步函数:读取同一存储中的活动状态,校验 gateway 模式、Key、profile marker 与目标路径,重新计算能力,仅在内容改变时原子写入。 +4. 在 `lifecycle/startup.rs::start_server` 中,于 `sync_runtime_settings_from_storage()` 之后调用同步函数;失败记录结构化 warning,服务继续启动。 +5. 平台 Key 相关路由字段更新后,对匹配的活动 profile 做 best-effort 同步;全局上游地址运行时变更后也触发一次。 +6. 配置确实改变时,记录“需要重启 Codex 会话才能让现有 app-server 读取新能力”的状态或日志。后续如需自动重载,应新增用户显式确认、可关闭偏好和结果反馈,不能放入无条件启动流程。 +7. 单独修复 WebSocket 对 disabled Key 的鉴权检查。 + +最小测试门禁: + +- `patch_config_for_gateway` 的 true/false、已有旧值覆盖、扩展字段保留测试。 +- account + 官方上游为 true;aggregate、hybrid、自定义上游为 false。 +- 活动 Key 匹配才同步;其他 Key、direct 模式、无状态、损坏状态、marker 不匹配均不写。 +- 配置内容不变时 mtime 不变;缺字段时仅写一次,第二次启动不再写。 +- 启动同步使用恢复后的全局上游地址。 +- profile 文件只读或写入失败时服务仍能启动并产生日志。 +- disabled 平台 Key 的 WebSocket 握手返回 403。 + +## 明确不移植清单 + +- `crates/service/src/codex_runtime.rs` 的自动进程枚举与信号发送。 +- `active_gateway_profile_dir()` 二次读取状态后再选择终止目标。 +- `mtime >= process_start_time` 即可终止的规则。 +- Windows `SIGTERM` 不支持时回退 `taskkill /F`。 +- `System::new_all()` 全量系统刷新。 +- 未经用户确认、没有可关闭偏好、没有前端结果反馈的任何进程终止行为。 + +## 最终裁决 + +`e3e5eb0b` 解决的“旧 gateway profile 未声明 WebSocket 能力”问题真实存在,CE 应移植配置能力计算和启动修复语义;但自动终止 app-server 的实现把配置一致性问题升级成跨平台进程管理副作用,风险与收益不匹配。CE 的最小可靠方案是:**统一判定、按变化写入、启动 best-effort 修复、提示用户重启,不自动杀进程。** diff --git "a/.teamwork/progress/2026-07-11_DST\346\227\245\347\272\247Rollup\345\256\241\350\256\241\344\277\256\345\244\215.md" "b/.teamwork/progress/2026-07-11_DST\346\227\245\347\272\247Rollup\345\256\241\350\256\241\344\277\256\345\244\215.md" new file mode 100644 index 000000000..b3dadc544 --- /dev/null +++ "b/.teamwork/progress/2026-07-11_DST\346\227\245\347\272\247Rollup\345\256\241\350\256\241\344\277\256\345\244\215.md" @@ -0,0 +1,41 @@ +# DST 日级 Rollup 审计修复进度 + +## 状态 + +✅ 已完成,等待主代理独立审计 + +## 审计结论 + +- 原实现以“当前本地零点 + 固定 86400 秒”反推历史日边界,在 America/New_York 春季切换日会错过 1 小时,秋季切换日会跨入相邻自然日。 +- SQLite 表中的 `day_start` 足以保存真实本地日零点,不需要修改表结构;问题位于边界生成与 mixed 查询切分。 +- 修复采用显式 `(day_start, day_end)` 区间,由服务层使用 `chrono::Local` 生成本地自然日,core 只按传入边界聚合。 +- 主审复核发现首版会从最早 pending 日期枚举到今天;稀疏多年数据会产生大量空日事务。修订为只读取受维护批次上限约束的 pending 明细时间戳,并仅生成真实存在数据的本地日期。 + +## 实施项 + +- [x] 增加最早未固化明细查询,维护任务从该明细所在本地日开始按日期顺序 rollup。 +- [x] 增加显式本地日区间的维护入口,保留原固定秒数入口兼容既有调用。 +- [x] 增加显式日区间的 daily / user / source mixed 查询。 +- [x] Dashboard 默认七日范围按本地日期偏移,不再以秒数回退。 +- [x] 增加 America/New_York 春季 23 小时与秋季 25 小时测试,覆盖明细删除前后汇总一致性。 +- [x] 增加跨 25 年稀疏 pending 数据测试,确认不会枚举中间空日。 +- [x] 完成格式化、core/service 定向测试与提交。 + +## 验证结果 + +- `cargo fmt --all --check`:通过。 +- `cargo test -p codexmanager-core storage::request_logs::tests::`:17 项通过。 +- `cargo check -p codexmanager-service`:通过,无警告。 +- `cargo test -p codexmanager-service dashboard`:首次运行的 4 项 dashboard 单元测试全部通过;命令随后未正常退出并持续占用测试可执行文件。再次链接时出现 `LNK1104` 文件占用,属于本机残留测试进程锁,不是编译错误。 + +## 风险控制 + +- 未修改 SQLite 表结构与历史 rollup 数据。 +- 无法解析本地午夜时返回错误并停止维护,不使用 UTC 对齐猜测替代本地日边界。 +- 维护仍受原批次上限约束,未完成的日级明细会留待下轮继续处理。 + +## 已知粒度限制 + +- 日级 rollup 只保存完整本地自然日聚合,不保存小时级切片。 +- 历史自定义查询若只覆盖某日首半日或尾半日,而该日原始明细已被 retention 清理,现有表结构无法精确回答该半日用量。 +- mixed 查询只对完整且已关闭的本地自然日读取 rollup;首尾不完整区间继续读取明细。明细不存在时返回可验证的空结果,不把整日 rollup 伪造成半日数据。 diff --git "a/.teamwork/progress/2026-07-11_\345\207\272\345\217\243\350\257\212\346\226\255\344\270\216\345\211\215\347\253\257\345\220\214\346\255\245_\350\277\233\345\272\246.md" "b/.teamwork/progress/2026-07-11_\345\207\272\345\217\243\350\257\212\346\226\255\344\270\216\345\211\215\347\253\257\345\220\214\346\255\245_\350\277\233\345\272\246.md" new file mode 100644 index 000000000..c9cb03230 --- /dev/null +++ "b/.teamwork/progress/2026-07-11_\345\207\272\345\217\243\350\257\212\346\226\255\344\270\216\345\211\215\347\253\257\345\220\214\346\255\245_\350\277\233\345\272\246.md" @@ -0,0 +1,25 @@ +# 出口诊断与前端状态同步进度 + +执行身份:【CodeX-GPT】 + +## 已完成 + +- 新增固定白名单多源出口诊断,使用确定性轮换起点、单源独立超时、总超时和单飞缓存。 +- 网络错误与 `5xx` 最多同源重试一次;`4xx`、无效 JSON 或字段错误直接切源。 +- 启动异步首检、区域阻断节流复检、存在阻断账号时低频兜底均不阻塞启动或请求热路径。 +- 诊断只展示给管理员,不根据 IP 结果修改账号状态;普通日志不记录完整 IP 或经纬度。 +- 补齐 Service RPC、Tauri command、Web command、设置页最小展示和手动刷新。 +- 用量刷新完成事件及手动刷新收口后会同步失效 `accounts/list`,避免账号状态展示滞后。 + +## 已执行验证 + +- `cargo fmt --all -- --check` +- `cargo test -p codexmanager-service network_diagnostics --lib -j1`:5 项通过。 +- `cargo test -p codexmanager-service member_actor_cannot_call_admin_only_rpc --lib -j1`:1 项通过。 +- `node --test tests/account-list-cache.test.mjs tests/transport-web-commands.test.mjs tests/tauri-command-registry.test.mjs`:23 项通过。 +- `apps/node_modules/.bin/tsc.CMD --noEmit`:通过(临时复用主工作树依赖,未复制依赖目录)。 +- `node --test tests/i18n-page-coverage.test.mjs`:3 项通过,出口诊断与刷新完成提示的英/韩/俄文案已补齐。 + +## 待主审 + +- 主代理复核完整 diff,并在集成分支执行更大范围 workspace / 前端构建门禁。 diff --git "a/.teamwork/progress/2026-07-11_\345\211\215\347\253\257Web\345\256\241\350\256\241\344\277\256\345\244\215.md" "b/.teamwork/progress/2026-07-11_\345\211\215\347\253\257Web\345\256\241\350\256\241\344\277\256\345\244\215.md" new file mode 100644 index 000000000..d0fd617b9 --- /dev/null +++ "b/.teamwork/progress/2026-07-11_\345\211\215\347\253\257Web\345\256\241\350\256\241\344\277\256\345\244\215.md" @@ -0,0 +1,43 @@ +# 前端 / Web 审计问题修复 + +执行方:【CodeX-GPT】前端修复子代理 + +## 状态 + +✅ 已完成,等待主代理独立审核。 + +## 修改摘要 + +- 补齐账号、平台 Key、聚合 API、模型池来源和请求错误摘要共 5 个 Web command 映射。 +- 删除 API Key command 模块中错误的模型价格重复键,保留服务端真实的 `modelPriceRules/*` RPC 映射。 +- 新增 Web command 静态调用完整性、跨模块重复命令键及关键 RPC 名称回归测试。 +- 按混合路由正确语义更新首页与日志页测试:当前路由模式不应遮挡已经写入 CodexManager 的历史统计。 +- 删除 Tauri 生产构建对既有 `out/index.html` 的跳过逻辑,确保每次打包都重新生成静态产物。 +- 将 `/platform-mode` 同步加入 `ROOT_PAGE_PATHS`,并增加回归覆盖。 + +## 验证结果 + +- `corepack pnpm -C apps exec node --test tests/transport-web-commands.test.mjs tests/dashboard-direct-mode.test.mjs tests/tauri-command-registry.test.mjs tests/top-level-routes.test.mjs`:32 项全部通过。 +- `corepack pnpm -C apps run test:runtime`:113 项全部通过。 +- `corepack pnpm -C apps run build`:未通过;当前 worktree 的 `apps/node_modules` 是指向仓库外部目录的联接,Turbopack 拒绝越出 filesystem root。 +- `corepack pnpm -C apps run build --webpack`:通过,15 个静态页面全部生成,包含 `/platform-mode`。 +- `git diff --check`:通过。 + +## Git 提交 + +- 分支:`audit/frontend-fixes` +- 提交:本文件与修复代码同一提交,最终哈希以该分支 Git 历史为准。 + +## 未验证项 + +- 未执行完整 Tauri 安装包构建;本轮已用脚本回归测试确认生产前置脚本不再因旧 `out/index.html` 提前退出。 +- 未在无目录联接的独立依赖目录下重跑 Turbopack;Webpack 生产构建已通过,可证明 TypeScript、静态导出和页面生成正常。 + +## 自检 + +- 功能完整性:通过。 +- 代码质量与防回归:通过。 +- 文档同步:已写入本进度文件;未新增环境变量或改变用户启动流程,无需修改 README。 +- 依赖检查:未新增依赖。 +- 安全审查:未引入敏感信息或权限放宽。 +- 可测试性:完整 runtime 门禁恢复为全绿。 diff --git "a/.teamwork/progress/2026-07-11_\345\217\221\345\270\203\345\267\245\345\205\267\345\256\241\350\256\241\344\277\256\345\244\215.md" "b/.teamwork/progress/2026-07-11_\345\217\221\345\270\203\345\267\245\345\205\267\345\256\241\350\256\241\344\277\256\345\244\215.md" new file mode 100644 index 000000000..c11216183 --- /dev/null +++ "b/.teamwork/progress/2026-07-11_\345\217\221\345\270\203\345\267\245\345\205\267\345\256\241\350\256\241\344\277\256\345\244\215.md" @@ -0,0 +1,36 @@ +# 发布与数据库工具审计修复进度 + +## 任务状态 + +- 状态:✅ 已完成,等待主代理独立审计 +- 执行方:【CodeX-GPT】发布/工具修复子代理 +- 分支:`audit/release-fixes` + +## 修复范围 + +1. 将 Release Compose 与中、英、韩、俄部署文档中的 GHCR 镜像归属统一为 CE 仓库所有者 `CreatorEdition` 对应的小写命名空间 `creatoredition`。 +2. 移除 `db-optimize` 的本机数据库默认路径,要求显式提供数据库文件。 +3. 将数据库工具默认行为改为只读检查;仅显式传入 `--vacuum` 时才允许 checkpoint 与 VACUUM 锁库操作。 +4. 为缺失路径、未知参数、冲突参数和执行失败提供中文错误信息与非零退出码。 +5. 保留 `db-optimize` 作为 workspace 成员以便定向维护和测试,但从 `default-members` 移除,避免普通 workspace 构建误带一次性运维工具;Release workflow 使用显式 `-p` 构建服务产物,因此不受影响。 + +## 历史治理说明 + +- `.teamwork/sync/` 是当前多 AI 协作协议的一部分,本次不删除、不改写同步状态文件。 +- 本次不修改或删除任何 Git tag。 +- 数据库工具此前存在本机绝对路径和默认执行 VACUUM 的高风险行为;该历史问题仅在本进度文件记录,不扩散到发布产物说明。 + +## 验证结果 + +- ✅ `cargo fmt --all --check` +- ✅ `cargo check -p db-optimize` +- ✅ `cargo test -p db-optimize`:8 项测试全部通过,覆盖默认只读检查、显式 VACUUM、路径不存在不创建空库及参数错误。 +- ✅ 可执行命令验证:无参数退出码为 2,未知参数退出码为 2,`--help` 退出码为 0,均输出中文说明。 +- ✅ `cargo metadata --no-deps --format-version 1`:`db-optimize` 仍是 workspace 成员,但不属于 `workspace_default_members`。 +- ✅ Release workflow 仍通过 `${{ github.repository_owner }}` 转小写后发布镜像;Compose 与四语部署文档已统一到 `ghcr.io/creatoredition/`。 +- ✅ `rg -n 'ghcr\.io/qxcnm/' docker docs` 无匹配。 + +## 未验证项 + +- 未执行完整 `cargo test --workspace`;本次 Rust 改动隔离在 `db-optimize`,已运行定向 check/test。 +- 未实际推送 GHCR 镜像或拉起 Release Compose;需要主代理在集成分支或 Release workflow 中验证远端包权限与镜像可拉取性。 diff --git "a/.teamwork/progress/2026-07-11_\345\220\216\347\253\257\345\256\211\345\205\250\347\274\223\345\255\230\345\256\241\350\256\241\344\277\256\345\244\215.md" "b/.teamwork/progress/2026-07-11_\345\220\216\347\253\257\345\256\211\345\205\250\347\274\223\345\255\230\345\256\241\350\256\241\344\277\256\345\244\215.md" new file mode 100644 index 000000000..f3af43494 --- /dev/null +++ "b/.teamwork/progress/2026-07-11_\345\220\216\347\253\257\345\256\211\345\205\250\347\274\223\345\255\230\345\256\241\350\256\241\344\277\256\345\244\215.md" @@ -0,0 +1,44 @@ +# 后端安全与候选缓存审计修复进度 + +## 执行身份 + +- 【CodeX-GPT】后端修复子代理 +- 分支:`audit/backend-fixes` + +## 已完成 + +1. 所有服务层账号删除路径在数据库删除成功后立即调用 `invalidate_candidate_cache`: + - 单账号删除; + - 指定账号批量删除; + - 按封禁、不可用免费账号及状态批量清理。 +2. 候选快照缓存改为按“数据库路径 + `LowQuotaCandidateMode`”独立保存,避免 `NormalOnly` 与 `AppendFallback` 相互驱逐。 +3. candidate cache single-flight 改为按同一缓存键互斥,不同候选模式可以并行刷新。 +4. accounts 鉴权模式下,从成员 RPC allowlist 移除所有全局账号池读取、更新、用量刷新、Token 刷新与预热方法。 +5. OAuth 成功日志改为无上下文标识的结构化事件 `event=oauth_login_persisted`,不再记录数据库路径、OAuth state、账号、workspace、ChatGPT 标识及回调地址。 + +## 回归测试 + +- 新增单删与批删后候选缓存立即失效测试。 +- 新增两种候选模式交替读取时缓存互不驱逐测试。 +- 新增相同模式 single-flight、不同模式并行刷新测试。 +- 新增成员访问全局账号池均返回 `permission_denied`,且账号字段保持不变测试。 + +## 验证结果 + +- `cargo fmt --all --check`:通过。 +- 候选缓存、删除缓存失效、成员权限定向测试:全部通过。 +- `cargo test -p codexmanager-service`: + - lib:1052/1052 通过; + - app_settings:30/30 通过; + - default_addr:10/10 通过; + - e2e:1/1 通过; + - gateway_logs:26/26 通过; + - gateway_logs 测试进程在输出全部通过后未自动退出,已精确终止本工作树对应进程,因此整条命令退出码为 1; + - 后续单独执行 rpc:44/44 通过,shutdown_flag:1/1 通过;rpc 同样在输出全部通过后未自动退出并被精确终止。 + +## 未处理与交接 + +- DST 固定 86400 秒的日级 rollup 已由主代理拆给独立子代理,本分支未修改 `request_token_stats` 或 maintenance,避免并行冲突。 +- 主代理需独立检查 diff、复跑关键测试并决定是否集成。 + +> **交接提示**:本任务代码已完成,建议由【CodeX-GPT】主审代理执行独立代码审计与集成。 diff --git "a/.teamwork/progress/2026-07-11_\345\256\232\344\273\267\345\210\206\345\261\202\344\277\256\345\244\215\350\277\233\345\272\246.md" "b/.teamwork/progress/2026-07-11_\345\256\232\344\273\267\345\210\206\345\261\202\344\277\256\345\244\215\350\277\233\345\272\246.md" new file mode 100644 index 000000000..f6983d9ad --- /dev/null +++ "b/.teamwork/progress/2026-07-11_\345\256\232\344\273\267\345\210\206\345\261\202\344\277\256\345\244\215\350\277\233\345\272\246.md" @@ -0,0 +1,21 @@ +# 定价分层修复进度 + +执行身份:【CodeX-GPT】 + +## ✅ 已完成 + +- 请求日志费用估算改为显式采用最终 `effective_service_tier`,同时覆盖 HTTP 与 Responses WebSocket 路径。 +- `fast` 规范为 `priority`;空值、`auto`、`default` 和未知 tier 保守回退 `standard`。 +- `model_price_rules.billing_mode` 已参与实际匹配,同一模型的 Standard / Priority 规则可共存。 +- 按官方附件逐模型补齐 18 条 Priority 种子,并补齐 `gpt-4.1-mini`、`gpt-4.1-nano`、`gpt-4o-mini`、`gpt-4o-2024-05-13` 的 Standard 特异规则。 +- 种子版本升级为 `2026-07-11-tiered-v2`,新版优先级高于旧版种子,已有数据库会自动补齐并立即采用新规则。 +- 价格规则 API 与前端类型已暴露 `billingMode`;当前模型编辑弹窗仍维持单价格编辑交互,后续如需同时编辑两档可单独扩展 UI。 +- 历史请求费用不自动重算。 + +## 验证 + +- `cargo check -p codexmanager-service`:通过。 +- `cargo test -p codexmanager-service quota::model_pricing::tests --lib`:14 通过。 +- `cargo test -p codexmanager-service request_log_uses_final_effective_tier_for_http_and_ws_costs --lib`:1 通过。 +- `cargo fmt --all --check`:通过。 +- `apps/node_modules/.bin/tsc.cmd --noEmit -p apps/tsconfig.json`:通过。 diff --git "a/.teamwork/progress/2026-08-06_GHCR\347\250\263\345\256\232\346\240\207\347\255\276\345\217\221\345\270\203.md" "b/.teamwork/progress/2026-08-06_GHCR\347\250\263\345\256\232\346\240\207\347\255\276\345\217\221\345\270\203.md" new file mode 100644 index 000000000..a75b478b7 --- /dev/null +++ "b/.teamwork/progress/2026-08-06_GHCR\347\250\263\345\256\232\346\240\207\347\255\276\345\217\221\345\270\203.md" @@ -0,0 +1,19 @@ +# GHCR 稳定标签发布进度 + +执行与审计身份:【CodeX-GPT】 + +## 完成内容 + +- CE 正式版容器发布除版本号外同步推送 `stable` 与 `latest`。 +- 预发布在自动识别或显式 `prerelease=true` 时只推送版本号,不覆盖稳定浮动标签。 +- 显式 `prerelease=false` 仍按正式版处理,和 GitHub Release 现有输入语义一致。 +- 保留 CE 的 `ghcr.io/creatoredition` 镜像归属与内联 workflow,不引入上游发布脚本。 + +## 验证 + +- 使用 `js-yaml` 解析 `.github/workflows/release-all.yml`:通过。 +- 抽取 workflow 原始 Bash 片段并使用模拟 `docker` 验证: + - `v1.2.3 + auto`:两个镜像各推版本号、`stable`、`latest`。 + - `v1.2.3-rc.1 + auto`:两个镜像各只推版本号。 + - `v1.2.3-rc.1 + false`:两个镜像各推三个标签。 + - `v1.2.3 + true`:两个镜像各只推版本号。 diff --git "a/.teamwork/progress/2026-08-06_\345\244\261\350\264\245\350\257\267\346\261\202\347\224\250\351\207\217\351\232\224\347\246\273.md" "b/.teamwork/progress/2026-08-06_\345\244\261\350\264\245\350\257\267\346\261\202\347\224\250\351\207\217\351\232\224\347\246\273.md" new file mode 100644 index 000000000..3cdb6ddb7 --- /dev/null +++ "b/.teamwork/progress/2026-08-06_\345\244\261\350\264\245\350\257\267\346\261\202\347\224\250\351\207\217\351\232\224\347\246\273.md" @@ -0,0 +1,21 @@ +# 失败请求用量隔离进度 + +执行身份:【CodeX-GPT】 + +## 完成内容 + +- 语义移植上游 `3cd72771`,新增 `request_token_stats.usage_included` 与成功明细局部索引。 +- 非 2xx 请求继续保留日志和 Token 明细,但 Dashboard、日志汇总、模型/Key 汇总、费用与平台 Key 配额只统计 2xx 请求。 +- 日级与长期压缩写入时对失败请求的 Token/费用归零,同时保留请求数、成功数和错误数;日级 marker 仍覆盖成功与失败明细,避免失败记录反复汇总。 +- 迁移会按关联请求状态回填现存明细,并清零 `success_count = 0` 的历史日级汇总桶。 + +## 验证 + +- `cargo test -p codexmanager-core --lib`:97 passed,1 ignored。 +- `cargo test -p codexmanager-core --test storage request_token_stats_rollups_use_owner_and_actual_source_precedence`:通过。 +- `cargo test -p codexmanager-service --test rpc rpc_requestlog_list_and_summary_support_pagination`:通过。 +- 新增 200 / 499 / 502 回归,验证明细标记、日志汇总、按 Key/模型汇总、配额及长期压缩前后口径一致。 + +## 已知边界 + +- 已经进入旧长期汇总且原始明细已被 retention 删除的数据没有状态维度,无法精确反推历史失败 Token;本次保证仍保留明细、可识别的日级桶与后续写入均使用新口径。 diff --git "a/.teamwork/progress/2026-08-06_\350\257\267\346\261\202\346\227\245\345\277\227\345\210\206\351\241\265\350\267\263\350\275\254.md" "b/.teamwork/progress/2026-08-06_\350\257\267\346\261\202\346\227\245\345\277\227\345\210\206\351\241\265\350\267\263\350\275\254.md" new file mode 100644 index 000000000..73ca29b71 --- /dev/null +++ "b/.teamwork/progress/2026-08-06_\350\257\267\346\261\202\346\227\245\345\277\227\345\210\206\351\241\265\350\267\263\350\275\254.md" @@ -0,0 +1,23 @@ +# 请求日志分页跳转进度 + +执行与审计身份:【CodeX-GPT】 + +## 完成内容 + +- 语义移植上游 `552e3f4d` 的首页与页码跳转能力,并按 CE 当前日志页工具栏重新布局。 +- 首页、上一页、下一页和确认使用 Lucide 图标按钮,均保留可访问名称和悬停说明。 +- 跳转页码只接受安全整数,自动钳制到 `1..totalPages`;空值或无效值恢复当前页。 +- 窄内容区会在控件组内部换行,并隐藏可由 `aria-label` 替代的辅助标签,避免横向溢出。 +- Playwright 启动命令改为 `corepack pnpm`,避免本地无 TTY 包装器误触发依赖重装。 + +## 验证 + +- `corepack pnpm run build:desktop`:Next 生产构建通过。 +- `tsc --noEmit`:通过。 +- 相关日志页、测试与 Playwright 配置 ESLint:0 error、0 warning。 +- `node --test apps/tests/i18n-page-coverage.test.mjs`:3 passed。 +- Playwright `request-logs-duration.spec.ts`:1 passed,覆盖第 4 页、越界钳制到第 5 页、首页缓存回退和 390px 分页区域零横向溢出。 + +## 独立发现 + +- 390px 下应用侧边栏仍固定占位,导致主内容宽度约 118px;分页控件已能在该极端宽度内无溢出,但应用外壳响应式需要作为独立任务处理。 diff --git "a/.teamwork/progress/2026-08-06_\351\242\235\345\272\246\347\252\227\345\217\243\344\270\216\345\271\266\345\217\221\350\256\276\347\275\256\344\277\256\345\244\215.md" "b/.teamwork/progress/2026-08-06_\351\242\235\345\272\246\347\252\227\345\217\243\344\270\216\345\271\266\345\217\221\350\256\276\347\275\256\344\277\256\345\244\215.md" new file mode 100644 index 000000000..7c16612ea --- /dev/null +++ "b/.teamwork/progress/2026-08-06_\351\242\235\345\272\246\347\252\227\345\217\243\344\270\216\345\271\266\345\217\221\350\256\276\347\275\256\344\277\256\345\244\215.md" @@ -0,0 +1,22 @@ +# 额度窗口与并发设置修复进度 + +执行与审计身份:【CodeX-GPT】 + +## 完成内容 + +- 语义移植上游 `6d72131e` 中适用于 CE 的配额保护修复,不引入 CE 已删除的 SQL 候选预筛路径。 +- 候选配额保护改为按 `window_minutes` 判断短周期与长周期,正确覆盖 300 分钟、10080 分钟、窗口字段互换和仅长窗口账号。 +- 旧快照缺失时长时继续使用 primary=短周期、secondary=长周期的兼容约定。 +- `CODEXMANAGER_ACCOUNT_MAX_INFLIGHT` 从通用 `envOverrides` 保留键中剔除,专用设置不再写入进程环境变量;真实启动环境变量仍保持最高优先级。 + +## 主代理审计 + +- 已核对 CE 候选池仅在内存快照中判断低额度,上游 `crates/core` 的 SQL 预筛实现不适用于当前 CE 架构,因此没有直接复制。 +- 已补充旧快照缺失窗口时长回归,确认兼容分支与长短窗口互换分支均由行为测试覆盖。 +- 已复核专用持久化设置、旧 `envOverrides` 与真实进程环境变量三者的优先级。 + +## 验证 + +- `cargo fmt --all --check`:通过。 +- `cargo test -p codexmanager-service gateway::selection::tests --lib`:18 passed。 +- `cargo test -p codexmanager-service --test app_settings -- --test-threads=1`:31 passed。 diff --git a/.teamwork/sync/gpt-audit-result.md b/.teamwork/sync/gpt-audit-result.md index 7fa77dd1a..b5832ae9a 100644 --- a/.teamwork/sync/gpt-audit-result.md +++ b/.teamwork/sync/gpt-audit-result.md @@ -1,69 +1,22 @@ -# GPT审计结果:hardening/main 合并到 main +# P2 桌面界面缩放主代理审计结论 -## 📊 审计执行 -- **审计人**: CodeX-GPT -- **审计日期**: 2026-06-14 -- **审计方法**: git ls-remote 直查远程 + 提交来源核对 +主审身份:【CodeX-GPT】 +时间:2026-08-07T20:32:54+08:00 -## ✅ 审计项 +## 审计处理 -### 审计1:远程分支SHA一致性 -✅ **通过** -``` -远程 main: a10a82fe -远程 hardening/main: a10a82fe -``` -两分支远程 SHA 完全相同,确认合并已推送成功。 +1. 子代理 P2“共享设置响应乱序”成立:设置页新增 Promise 串行队列,后续保存会在前一请求完成或失败后再发送;系统 Chrome 回归验证两个不同设置入口的最大并发请求数为 1。 +2. 子代理 P3“滑杆中断未收口”成立:新增 `pointercancel` 回滚、`blur` 提交和新输入清除取消标记;浏览器回归覆盖取消不写入、失焦写入。 +3. 子代理 P3“Tauri 真窗口测试缺口”成立但不阻断:当前无可注入的真实 WebView 测试壳;主审已核对 `main` 标签、启动同步、设置热更新和 `show_main_window` 重建前应用路径,并完成 Tauri 54/54 与隔离 `cargo check`。 -### 审计2:合并提交来源 -✅ **通过** -- 2c43779c..a10a82fe 共 118 个提交 -- 全部来自 hardening/main -- 无意外引入的外部提交 +## 独立验证 -### 审计3:合并方式 -✅ **通过** -- Fast-forward 合并,无 merge commit -- 历史线性,符合任务要求 +- service 单元测试 `1097/1097`,`app_settings` 集成测试 `33/33`。 +- Tauri 单元测试 `54/54`,隔离目标 `cargo check` 通过。 +- 前端 runtime `120/120`,TypeScript、生产构建通过;全量 ESLint 0 error(19 条既有 warning)。 +- 系统 Chrome Playwright `4/4`:桌面步进/重置、取消/失焦、360px 窄屏、Web 隐藏、失败回滚和共享写入串行化。 +- `cargo fmt --all --check`、`git diff --check` 通过。 -### 审计4:工作区状态 -✅ **通过** -- 仅协作文件(opus-to-gpt.md/status.json)有改动,属正常 -- 无代码文件被意外修改 +## 结论 -### 审计5:当前分支 -✅ **通过** -- 当前在 hardening/main(工作分支) -- 符合任务要求(合并后切回) - -## 📝 最终决策 - -### ✅ 通过 - 合并成功 - -**理由**: -1. ✅ 远程 main 与 hardening/main SHA 一致(a10a82fe) -2. ✅ 118个提交全部来自 hardening/main,无污染 -3. ✅ Fast-forward 无 merge commit -4. ✅ 工作区干净,无意外代码改动 -5. ✅ 已切回 hardening/main 工作分支 - -**与Claude报告一致性**: 100% -- Claude报告的网络中断后重试,远程已确认成功 -- 所有验证数据一致 - -## 🎯 结论 - -main 现已包含全部 fork 强化工作,可作为发布分支。 - -**用户现在可以用 `main` 触发 GitHub Release workflow**,打出的包将包含全部改进。 - -### 触发参数(更新) -``` -tag: v0.3.9 -ref: main ← 现在 main 已是完整版 -prerelease: auto -``` - ---- -**审计人**: CodeX-GPT -**审计结论**: ✅ 批准 +PASS。独立审计提出的可修复问题已收口,未发现阻断提交和更新 PR 的剩余问题。 diff --git a/.teamwork/sync/gpt-to-opus.md b/.teamwork/sync/gpt-to-opus.md index 50356bba7..befea7d96 100644 --- a/.teamwork/sync/gpt-to-opus.md +++ b/.teamwork/sync/gpt-to-opus.md @@ -1,101 +1,22 @@ -# 给 Claude/Opus 的执行任务:J 项 usage_snapshots 无变化写入去重 +# P2 桌面界面缩放独立审计 -发起方:CodeX-GPT -任务时间:2026-06-24T16:31:02+08:00 -工作目录:`C:\code\CodeX\Codex-Manager-CE` -目标分支:`hardening/main` +发起身份:【CodeX-GPT】 +时间:2026-08-07 -## 背景 +## 审计范围 -`task.md` 的 J 项仍真实存在:生产写入路径 `crates/service/src/usage/usage_snapshot_store.rs::store_usage_snapshot()` 每次刷新都会: +只读审计当前工作树中“桌面界面缩放”相关改动,禁止修改业务代码、暂存或提交。 -1. `parse_usage_snapshot()` -2. 构造 `UsageSnapshotRecord` -3. `storage.insert_usage_snapshot(&record)` -4. `prune_usage_snapshots_for_account(account_id, retain)` +重点检查: -即使服务端返回的用量关键字段完全没变化,也会 INSERT 新行再 prune。大账号池 + 高频刷新时会造成 `usage_snapshots` 和 WAL 写放大。 - -## 目标 - -完成 J 项:用量快照在关键字段未变化时不要 append 新行。 - -## 约束 - -1. 不要改变对外用量状态语义:`apply_status_from_snapshot()` 仍应基于本次解析结果执行。 -2. 不要因为 skipped insert 而丢失“最近刷新时间”的表达能力。推荐方案是更新最新行 `captured_at`,或者通过等价轻量路径维持 latest 语义。 -3. 比较关键字段时应覆盖: - - `used_percent` - - `window_minutes` - - `resets_at` - - `secondary_used_percent` - - `secondary_window_minutes` - - `secondary_resets_at` - - `credits_json` -4. `credits_json` 比较要避免简单字符串格式差异造成误判。如果当前项目没有规范化 JSON helper,可以使用 `serde_json::Value` 语义比较;若决定做字符串比较,必须解释风险并补测试覆盖稳定序列化。 -5. 不要 `git add .`;只暂存本任务相关文件。 -6. 新增注释必须使用简体中文。 -7. 不要把密钥、cookie、token、身份证号、手机号写入协作文件。 - -## 推荐实现方向 - -小步方案: - -1. 在 storage 层新增一个方法,例如: - - `update_latest_usage_snapshot_captured_at_for_account(account_id, captured_at) -> Result` - - 或 `upsert_usage_snapshot_if_changed(&record) -> Result` -2. 在 service 层 `store_usage_snapshot()` 中读取 `latest_usage_snapshot_for_account(account_id)`,比较关键字段。 -3. 如果字段未变: - - 不调用 `insert_usage_snapshot()` - - 更新最新快照 `captured_at` 为本次时间 - - 可跳过 `prune_usage_snapshots_for_account()`,因为没有新增行 - - 仍调用 `apply_status_from_snapshot(storage, &record)` -4. 如果字段变化: - - 保持原 insert + prune 行为 - -更优方案: - -- 把“比较 + insert/update”尽量收口到 storage,service 只处理解析和状态更新。 -- 但不要做大范围表结构迁移,除非你证明必须。 - -## 必须验证 - -至少运行并记录: - -```powershell -cargo test -p codexmanager-core --lib usage_snapshot -cargo test -p codexmanager-service --lib usage_snapshot -cargo check -p codexmanager-service -``` - -如果 filter 不匹配导致 `0 tests`,必须换成真实能跑到新增/修改测试的命令,不能把 `0 tests` 当通过。 - -建议新增测试覆盖: - -- 相同关键字段连续 store 两次,`usage_snapshot_count_for_account(account_id)` 不增加。 -- 相同关键字段第二次 store 后,最新行 `captured_at` 更新为新时间或 latest 语义能体现新刷新。 -- 任一关键字段变化时会新增快照。 -- `credits_json` 语义相同但字段顺序不同不应新增快照(如果采用语义比较)。 +1. `ui.zoom_factor` 的 75%~125% 边界、5% 步进、非有限值回退与持久化是否一致。 +2. Tauri 是否只缩放 `main`,并覆盖启动恢复、设置热更新及轻量托盘销毁后重建。 +3. Web 运行壳是否隐藏且不会由页面主动写入桌面专属设置。 +4. 前端异步保存是否存在竞态、失败后状态不回滚、窄屏溢出或无障碍问题。 +5. 测试是否能支撑实现结论,是否遗漏高风险边界。 ## 交付要求 -完成后写入 `.teamwork/sync/opus-to-gpt.md`,至少包含: - -- 修改摘要 -- 产出文件 -- 关键设计取舍 -- 实际运行的验证命令与结果 -- git commit hash -- 未验证项或剩余风险 - -然后将 `.teamwork/sync/status.json` 更新为 `waiting_for_gpt`,`last_actor` 写你的身份。 - -## 审计提醒 - -CodeX-GPT 会独立复核: - -1. 是否真的避免相同快照 append。 -2. 是否保留 latest/captured_at 语义。 -3. 是否仍在字段变化时新增记录。 -4. 是否没有把测试 fixture 的 `insert_usage_snapshot()` 当生产路径误改。 -5. diff 是否只包含 J 项相关文件。 +- Findings 优先,按严重度列出文件与行号;没有阻断问题时明确说明。 +- 独立读取 `git diff`,不得复述主代理结论替代审计。 +- 将结果写入 `.teamwork/sync/opus-to-gpt.md`,再将 `status.json` 改为 `waiting_for_gpt`。 diff --git a/.teamwork/sync/opus-to-gpt.md b/.teamwork/sync/opus-to-gpt.md index 382cbdf01..303ce2b0e 100644 --- a/.teamwork/sync/opus-to-gpt.md +++ b/.teamwork/sync/opus-to-gpt.md @@ -1,52 +1,30 @@ -# J 项 usage_snapshots 无变化写入去重结果 +# P2 桌面界面缩放独立审计结果 -执行身份:CodeX-GPT -接手时间:2026-06-24T17:28:09+08:00 -任务:`usage-snapshot-dedup-j` +审计身份:【CodeX-GPT-5.2 子代理】 +交付时间:2026-08-07T20:32:54+08:00 -## 接手原因 +## 结论 -Claude/Opus 执行方完成了核心半成品,但未完成协议交付、未确认 service 测试、未运行 `cargo check`、未写协作结果、未更新状态、未提交,并带入了多处 unrelated rustfmt/行尾噪声。CodeX-GPT 已关闭执行方、清理无关 diff 并完成审计收口。 +PASS:未发现阻断发布的 P0/P1 问题。服务端归一化与持久化、Tauri 主窗口恢复、Web 隔离的实现逻辑基本完整。 -## 修改摘要 +## Findings -- storage 层新增 `update_latest_usage_snapshot_captured_at_for_account(account_id, captured_at)`,用于在相同快照去重时维护最新刷新时间语义。 -- service 层 `store_usage_snapshot()` 先读取账号最新快照,比较关键字段: - - `used_percent` - - `window_minutes` - - `resets_at` - - `secondary_used_percent` - - `secondary_window_minutes` - - `secondary_resets_at` - - `credits_json` -- 如果关键字段未变化:不再 `insert_usage_snapshot()`,只更新最新行 `captured_at`,并继续基于本次解析结果执行 `apply_status_from_snapshot()`。 -- 如果关键字段变化:保持原 insert + prune 行为。 -- `credits_json` 使用 `serde_json::Value` 语义比较,避免对象字段顺序不同导致误判为变化。 +1. **P2:共享设置保存存在响应乱序风险** + - `updateSettings` 同时供多个设置入口使用;并发请求若较旧响应后返回,会用完整快照覆盖 Query Cache 和 Store。 + - 涉及 `apps/src/app/settings/page.tsx` 与缩放组件调用链。 +2. **P3:滑杆中断操作可能留下未持久化显示值** + - 仅在 `pointerup` / `keyup` 提交时,`pointercancel` 或窗口失焦可能只更新草稿。 + - 涉及 `apps/src/app/settings/components/appearance-tab-content.tsx`。 +3. **P3:Tauri 生命周期测试仍有平台运行时残余风险** + - 单元测试验证了缓存归一化和重建调用路径,但未在真实 WebView 中自动化验证 `set_zoom`、主窗口销毁重建及托盘预览不缩放。 -## 产出文件 +## 已确认 -- `crates/core/src/storage/usage.rs` -- `crates/service/src/usage/usage_snapshot_store.rs` -- `task.md` -- `.teamwork/sync/opus-to-gpt.md` -- `.teamwork/sync/status.json` +- 服务端范围、步进、非有限值回退和持久化正确。 +- Tauri 只按 `main` 标签应用缩放,并在主窗口重建前读取缓存。 +- Web 设置页隐藏控件,写入函数也有桌面运行时保护。 +- 360px 布局未见固定宽度溢出。 -## 验证结果 +## 子代理未验证项 -- `cargo test -p codexmanager-core --lib usage_snapshot -- --nocapture` -> 6 passed。 -- `cargo test -p codexmanager-service --lib usage_snapshot -- --nocapture` -> 5 passed。 -- `cargo check -p codexmanager-service` -> Finished(仅既有 warning)。 -- `git diff --check` -> 通过。 -- `rustfmt --edition 2021 crates/core/src/storage/usage.rs crates/service/src/usage/usage_snapshot_store.rs` -> 通过。 - -## 审计结论 - -- 相同关键字段连续写入不会增加 `usage_snapshots` 行数。 -- 相同关键字段第二次写入会更新 latest `captured_at`。 -- 任一关键字段变化仍新增快照。 -- `credits_json` 语义相同但字段顺序不同不会新增快照。 -- 执行方误带入的 unrelated rustfmt/行尾噪声已清理,未纳入提交。 - -## 剩余风险 - -- latest 快照的比较与更新时间是两步操作,极端同账号并发刷新下仍可能存在竞态窗口;当前 token/usage 刷新路径已有批次调度和账号级刷新语义,风险可接受。若后续观测到同账号并发写入,可进一步把 compare/update/insert 收口到 storage transaction。 +- 子代理限时内未取得 Rust 聚焦测试终态;由主代理独立执行完整门禁。 diff --git a/.teamwork/sync/status.json b/.teamwork/sync/status.json index 7235ca6d6..989486c3f 100644 --- a/.teamwork/sync/status.json +++ b/.teamwork/sync/status.json @@ -1,16 +1,16 @@ { "status": "completed", - "task": "usage-snapshot-dedup-j", - "created_at": "2026-06-24T16:31:02+08:00", - "last_update": "2026-06-24T17:28:09+08:00", - "iteration": 1, + "task": "p2-desktop-ui-zoom-audit-20260807", + "created_at": "2026-08-07T09:51:02+08:00", + "last_update": "2026-08-07T20:32:54+08:00", + "iteration": 0, "max_iterations": 3, "last_actor": "CodeX-GPT", "current_agent": "CodeX-GPT", "next_agent": null, - "workflow": "J项usage_snapshots无变化写入去重", - "description": "相同用量快照不再append新行,只更新latest captured_at,降低usage_snapshots/WAL写放大", - "priority": "P0", + "workflow": "P2 桌面界面缩放只读独立审计", + "description": "审计设置持久化、Tauri 窗口生命周期、Web 隔离和前端交互", + "priority": "P2", "phase": "completed", "audit_result": "PASS", "tests_status": "PASS" diff --git "a/.teamwork/tasks/2026-07-11_\345\256\232\344\273\267\345\210\206\345\261\202\344\270\216\345\210\267\346\226\260\347\212\266\346\200\201\344\277\256\345\244\215.md" "b/.teamwork/tasks/2026-07-11_\345\256\232\344\273\267\345\210\206\345\261\202\344\270\216\345\210\267\346\226\260\347\212\266\346\200\201\344\277\256\345\244\215.md" new file mode 100644 index 000000000..03b47183f --- /dev/null +++ "b/.teamwork/tasks/2026-07-11_\345\256\232\344\273\267\345\210\206\345\261\202\344\270\216\345\210\267\346\226\260\347\212\266\346\200\201\344\277\256\345\244\215.md" @@ -0,0 +1,26 @@ +# 定价分层与刷新状态修复 + +## 状态 + +🔄 进行中 + +## 分工 + +- 定价子代理:Priority/Standard 规则匹配、官方种子、费用估算测试。 +- 刷新子代理:永久错误过滤、Token 成功后的用量验证和状态恢复。 +- 区域与前端子代理:出口 IP 诊断、事件触发策略、账号状态缓存同步和 UI。 +- 主代理:独立审计、冲突处理、最终门禁和 PR 更新。 + +## 刷新子项进度 + +- ✅ `refresh_token_expired` 已改为长退避后的低频复检;仅 reused / invalidated / invalid_grant / app_session_terminated 永久过滤。 +- ✅ 后台 Token 轮询明确跳过 `disabled` / `banned`;手动单账号刷新保留为显式恢复通道。 +- ✅ Token 成功后不直接激活账号,仅对可恢复故障异步排队真实用量验证,并绕过陈旧用量失败冷却。 +- 🔄 等待主代理独立审计与集成门禁。 + +## 约束 + +- IP 信息仅用于诊断,不直接把账号恢复为 active。 +- 账号恢复必须以 Token 与用量接口的真实结果为依据。 +- Priority 价格必须逐模型录入,禁止统一倍率推导。 +- 不自动重算历史账单,除非用户另行授权。 diff --git "a/.teamwork/tasks/2026-07-11_\345\256\241\350\256\241\351\227\256\351\242\230\344\277\256\345\244\215.md" "b/.teamwork/tasks/2026-07-11_\345\256\241\350\256\241\351\227\256\351\242\230\344\277\256\345\244\215.md" new file mode 100644 index 000000000..4d71338bc --- /dev/null +++ "b/.teamwork/tasks/2026-07-11_\345\256\241\350\256\241\351\227\256\351\242\230\344\277\256\345\244\215.md" @@ -0,0 +1,38 @@ +# 2026-07-11 审计问题修复任务 + +## 目标 + +修复 CodeX-GPT 独立审计确认的前端/Web、后端安全与缓存、发布/工具治理问题,并由主代理完成二次审计、测试和 PR。 + +## 分工 + +### 前端/Web 子任务 + +- 补齐 5 个 Web command 映射。 +- 删除模型价格错误重复映射并增加完整性测试。 +- 更新 direct-mode 统计测试,恢复 `test:runtime`。 +- 修复 Tauri 因旧 `out/index.html` 跳过构建的问题。 +- 同步 `/platform-mode` 根路由清单。 + +### 后端安全与缓存子任务 + +- 单删、批删账号后立即失效候选缓存。 +- 将候选缓存和 single-flight 按 low quota mode 隔离。 +- 收紧 member 对全局账号池 RPC 的权限。 +- 删除 OAuth 登录成功日志中的敏感标识。 +- 为上述行为补定向测试。 + +### 发布/工具治理子任务 + +- 将 release compose 和多语言部署文档切换到 CE GHCR 镜像。 +- 移除 `db-optimize` 的本机默认路径。 +- 默认只检查,不自动执行 `VACUUM`;破坏性操作必须显式参数授权。 +- 补脚本/文档验证。 + +## 主代理审计要求 + +1. 检查每个子分支的 diff 与提交范围。 +2. 独立运行前端 runtime/build、Rust 定向及 workspace 测试。 +3. 检查权限边界、缓存失效、日志脱敏和部署镜像。 +4. 只将审计通过的提交集成到 `audit/fix-followups`。 +5. 推送后创建 PR,并以【CodeX-GPT】身份提交审计评论。 diff --git "a/.teamwork/tasks/2026-08-06_P1\345\267\256\345\274\202\346\224\266\345\217\243.md" "b/.teamwork/tasks/2026-08-06_P1\345\267\256\345\274\202\346\224\266\345\217\243.md" new file mode 100644 index 000000000..7ba59432a --- /dev/null +++ "b/.teamwork/tasks/2026-08-06_P1\345\267\256\345\274\202\346\224\266\345\217\243.md" @@ -0,0 +1,27 @@ +# P1 差异收口 + +审计身份:【CodeX-GPT】 + +状态:已完成,相关发布门禁已通过,等待提交。 + +## 目标 + +1. 按 CE 当前账号表格结构实现独立操作栏,并覆盖窄屏、横向滚动与动态行高。 +2. 为混合路由补真实模型来源映射回归,证明纯聚合模型不会错误进入账号池。 +3. 只移植上游桌面诊断中的文件日志上限,不整包引入启动备份、诊断 UI 或设置模型。 +4. 拆分移植开放 PR `#421` 中 CE 缺失的系统代理回退、WebSocket 连接超时、握手失败日志和 Codex profile 能力同步,同时保留 CE 全局代理优先级。 + +## 协作约束 + +- 子任务写集互不重叠;子代理不得提交、推送或修改 `task.md`、README、CHANGELOG。 +- 主代理必须逐文件检查 diff、独立运行测试,并在必要时重写补丁后才能通过。 +- 禁止直接复制上游大补丁;只保留 CE 可验证的功能语义。 + +## 完成摘要 + +1. 账号主表与操作栏按 CE 现有列、分页和菜单拆分,系统 Chrome 覆盖 390/640/1024px、横向滚动、动态行高、空状态和分页。 +2. 混合路由新增真实 SQLite/HTTP 回归,覆盖纯聚合映射、账号轮转拒绝纯聚合映射、账号 404 后聚合兜底。 +3. 桌面文件日志限制为单文件 8 MiB,保留当前文件与最多 4 个归档,常规总量约 40 MiB;未移植上游耦合的诊断 UI。 +4. Responses WebSocket 补齐环境/Windows/macOS 静态系统代理、连接阶段超时和失败日志;无效显式代理 fail-closed,代理凭据不会进入错误与日志。 +5. 蓝军发现的 401 同步刷新阻塞与系统代理认证丢失已由主代理复核修正;最新 service 单元测试 1096/1096、`gateway_logs` 29/29、前端 runtime 117/117、系统 Chrome 2/2、生产构建与 Tauri `cargo check` 均通过。 +6. PR `#421` 更新到 `e3e5eb0b` 后,CE 补齐平台 Key 到 `supports_websockets` 的能力映射、启动修复和相关路由设置变更同步;重复同步不会重写文件,且不采用上游自动终止 Codex `app-server` 的高副作用实现。WebSocket 握手同时复用 HTTP 平台 Key 的启用状态、Token 配额与钱包预检,禁用 Key 返回 403。 diff --git "a/.teamwork/tasks/2026-08-06_\344\270\212\346\270\270\345\242\236\351\207\217\345\244\215\346\240\270.md" "b/.teamwork/tasks/2026-08-06_\344\270\212\346\270\270\345\242\236\351\207\217\345\244\215\346\240\270.md" new file mode 100644 index 000000000..0a1606937 --- /dev/null +++ "b/.teamwork/tasks/2026-08-06_\344\270\212\346\270\270\345\242\236\351\207\217\345\244\215\346\240\270.md" @@ -0,0 +1,27 @@ +# 上游增量复核 + +审计身份:【CodeX-GPT】 + +## 目标 + +- 复核 `upstream/main` 的真实远端状态,区分最新增量与长期累计分叉。 +- 对 `v0.5.2..upstream/main` 和 `e630b349..upstream/main` 做语义分类,不直接合并或复制上游代码。 +- 子代理只读分析;任何后续代码修改均由主代理独立审计并执行门禁。 + +## 并行分工 + +1. 前端:图表 tooltip、账号操作栏与窄屏响应式。 +2. 桌面端:持久化缩放、托盘窗口与开发代理健壮性。 +3. 累计差异:筛选此前记录可能遗漏的安全、数据正确性和网关兼容修复。 + +## 当前基准 + +- `upstream/main`: `cdf0c5f2 docs: adjust sponsor listing` +- `v0.5.2..upstream/main`: 19 个提交 +- 当前 CE / upstream 分叉:CE 独有 308,上游独有 515 + +## 复核结果 + +- 前端审计确认 `57cad704` 图表 tooltip 与窄屏侧边栏问题适用于 CE,主代理已完成语义移植并通过系统 Chrome 验收;`646095c4` 独立操作栏保留为 P1 待处理。 +- 桌面审计确认 `e44bfe35` 持久化缩放保留为 P2;`454fe220` 托盘最终语义已等价存在,`2a252139` 与 `1dbe17da` 的开发态路径不适用。 +- 累计差异中确认 `1354b888` provider 自定义字段丢失,主代理已完成语义移植和 Rust 回归测试;混合路由、诊断/日志上限和 GPT-5.6 价格仍按独立待办处理。 diff --git "a/.teamwork/tasks/2026-08-06_\345\256\214\346\210\220\344\270\216\346\234\252\345\256\214\346\210\220\344\273\273\345\212\241\345\256\241\350\256\241.md" "b/.teamwork/tasks/2026-08-06_\345\256\214\346\210\220\344\270\216\346\234\252\345\256\214\346\210\220\344\273\273\345\212\241\345\256\241\350\256\241.md" new file mode 100644 index 000000000..ce331acc8 --- /dev/null +++ "b/.teamwork/tasks/2026-08-06_\345\256\214\346\210\220\344\270\216\346\234\252\345\256\214\346\210\220\344\273\273\345\212\241\345\256\241\350\256\241.md" @@ -0,0 +1,25 @@ +# 完成与未完成任务审计 + +状态:已完成;主代理已独立复核提交历史、PR 状态、测试输出与上游引用。 + +## 目标 + +并行盘点当前 PR、上游差异与发布门禁,产出可由主代理独立复核的证据清单。 + +## 子任务 + +1. 审计 `task.md`、CHANGELOG、progress 与提交历史,区分已完成、待合并、真实未完成。 +2. 对比当前 `upstream/main` 与本分支,复核 `task.md` 中上游迁移分类是否仍准确,列出下一批最小可实施项。 +3. 审计测试、CI、发布状态与代码中的 TODO/FIXME,列出发布阻塞项和可立即处理项。 + +## 约束 + +- 子代理只读审计,不修改文件。 +- 每项结论必须给出文件、提交、PR、测试或命令证据。 +- 主代理必须重新检查证据后才能采纳。 + +## 结果 + +- 上游基准、提交计数与下一批移植优先级已写入 `.teamwork/discussions/2026-08-06_上游v0.5.2后续差异巡检.md`。 +- 已完成 `c98047eb` 与 `3cd72771` 的 CE 语义移植;剩余工作已收敛到 `task.md`。 +- PR #1 仍为 Draft,正式发布前仍需完成 PR 合并、完整门禁和版本化 CHANGELOG 小节。 diff --git "a/.teamwork/tasks/2026-08-07_P2\346\241\214\351\235\242\347\225\214\351\235\242\347\274\251\346\224\276.md" "b/.teamwork/tasks/2026-08-07_P2\346\241\214\351\235\242\347\225\214\351\235\242\347\274\251\346\224\276.md" new file mode 100644 index 000000000..9fb73454e --- /dev/null +++ "b/.teamwork/tasks/2026-08-07_P2\346\241\214\351\235\242\347\225\214\351\235\242\347\274\251\346\224\276.md" @@ -0,0 +1,40 @@ +# P2 桌面界面缩放 + +执行身份:【CodeX-GPT】 + +状态:已完成 + +## 目标 + +1. 按 CE 当前设置存储与 Tauri 命令边界持久化桌面主窗口缩放比例。 +2. 缩放范围固定为 75%~125%,步进 5%,无效输入回退 100%。 +3. 桌面设置页提供滑杆、增减与重置控件;Web 运行壳不显示缩放控件。 +4. 覆盖首次启动、设置热更新、重启恢复、托盘预览触发设置读取及窄屏布局。 + +## 不移植边界 + +- 不直接复制上游组件代码;复用 CE 当前设置卡片、命令适配与测试结构。 +- 不用 CSS `zoom` 模拟桌面缩放,真实缩放只由 Tauri 主窗口 `set_zoom` 执行。 +- 不让 Web 模式写入或展示桌面专属缩放选项。 + +## 实施结果 + +1. 服务端新增 `ui.zoom_factor`,统一处理 75%~125% 边界、5% 步进和非有限值回退,并通过设置快照读写。 +2. Tauri 仅对 `main` 调用 `set_zoom`,在启动、设置读取/写入和轻量托盘主窗口重建前同步缓存值;`tray-preview` 保持原尺寸。 +3. 外观页仅在桌面运行壳展示滑杆、增减和重置控件,窄屏分行布局;保存失败恢复到服务端快照,取消拖动回滚,失焦提交。 +4. Web 运行壳不展示缩放控件,并由页面写入入口显式检查 `isDesktopRuntime`。 +5. 设置页共享保存请求按发起顺序串行执行,避免不同设置响应乱序覆盖完整快照。 + +## 验证 + +- service 单元测试:`1097/1097`;`app_settings` 集成测试:`33/33`。 +- Tauri 单元测试:`54/54`;隔离目标 `cargo check` 通过。 +- 前端 runtime:`120/120`;TypeScript、生产构建通过;全量 ESLint 0 error(19 条既有 warning)。 +- 系统 Chrome Playwright:`4/4`,覆盖桌面步进/重置、取消/失焦、360px 窄屏、Web 隐藏、保存失败回滚和共享写入串行化。 +- `cargo fmt --all --check`、`git diff --check` 通过。 + +## 独立审计 + +- 子代理提出共享设置响应乱序、滑杆中断收口和 Tauri 真窗口测试残余风险。 +- 主代理确认前两项成立并完成修复及浏览器回归;Tauri 真窗口自动化缺口记录为非阻断平台残余风险。 +- 主代理最终审计:PASS。 diff --git a/Cargo.lock b/Cargo.lock index 722076768..071f8bebb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,6 +306,7 @@ dependencies = [ "env_logger", "eventsource-stream", "futures-util", + "hyper-util", "log", "os_info", "rand 0.8.5", diff --git a/Cargo.toml b/Cargo.toml index 5750969d7..2474faa09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,12 @@ members = [ "crates/web", "crates/db-optimize" ] +default-members = [ + "crates/core", + "crates/service", + "crates/start", + "crates/web" +] exclude = [ "apps/src-tauri" ] diff --git a/README.md b/README.md index 94b468fd7..1c0f1be9e 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ - 模型管理:维护结构化模型目录、远端并入、自定义模型、空目录自动远端拉取开关、`visibility` / `supportedInApi` 管理,以及桌面端 Codex 缓存同步 / Web 端缓存导出 - 聚合 API:管理第三方最小转发上游,支持创建、编辑、测试连通性、供应商名称、顺序优先级,以及按 Codex / Claude 分类展示 - 插件中心:路由为 `/plugins/`,支持内置精选、企业私有、自定义源三种市场模式,并提供插件清单、任务、日志与 Rhai 对接接口 -- 设置页:支持“系统推导”按钮、单账号并发上限、上游代理、请求总超时、流式空闲超时、SSE 保活间隔,以及更保守的高并发退化策略;实验性上游 WebSocket 可通过 `CODEXMANAGER_USE_WEBSOCKET_UPSTREAM=1` 开启,默认关闭 +- 设置页:支持桌面主窗口 75%~125% 界面缩放、“系统推导”按钮、单账号并发上限、上游代理、请求总超时、流式空闲超时、SSE 保活间隔,以及更保守的高并发退化策略;通用页可查看和手动刷新出口 IP / 国家 / ASN 诊断,该结果仅用于排障,不会直接改变账号状态;实验性上游 WebSocket 可通过 `CODEXMANAGER_USE_WEBSOCKET_UPSTREAM=1` 开启,默认关闭,并在未配置应用代理时支持标准环境代理及 Windows/macOS 静态系统代理兜底 - 系统内部接口总表:列出当前桌面端与服务端所有可对接命令、RPC 方法、以及插件内建函数 - 本地服务:自动拉起、可自定义端口与监听地址 - 本地网关:为 Codex CLI、Gemini CLI、Claude Code 和第三方工具提供统一 OpenAI 兼容入口;Gemini 请求可转发到 `/v1/responses`,并兼容 SSE、tools、MCP、skill、请求总超时与流式空闲超时等调用链路 @@ -88,7 +88,7 @@ - 平台 Key:按模型、推理等级、服务等级绑定平台 Key,并查看调用日志 - 模型管理:桌面端修改后会自动同步本地 `~/.codex/models_cache.json`;设置页可控制本地目录为空时是否自动从远端拉取 - 插件中心:`/plugins/` 路由,内置精选 / 企业私有 / 自定义源市场切换,插件安装、启停、任务、日志、Rhai 对接 -- 设置页:统一管理端口、监听地址、代理、请求超时、SSE 保活、主题、自动更新、后台行为 +- 设置页:统一管理端口、监听地址、代理、请求超时、SSE 保活、主题、桌面界面缩放、自动更新、后台行为 ### Service 版 - `codexmanager-service`:提供本地 OpenAI 兼容网关 diff --git a/apps/package.json b/apps/package.json index 71a9c5c66..8bd6a1406 100644 --- a/apps/package.json +++ b/apps/package.json @@ -10,7 +10,7 @@ "build:desktop": "next build", "test:e2e": "playwright test", "test:navigation": "playwright test tests/navigation-cache.spec.ts", - "test:runtime": "node --test tests/runtime-capabilities.test.mjs tests/gateway-endpoints.test.mjs tests/transport-errors.test.mjs tests/gateway-settings.test.mjs tests/transport-web-commands.test.mjs tests/codex-profile-cache.test.mjs tests/i18n-page-coverage.test.mjs tests/account-list-cache.test.mjs tests/tauri-command-registry.test.mjs tests/dashboard-direct-mode.test.mjs tests/settings-theme-preview.test.mjs tests/models-search-style.test.mjs tests/codex-cli-onboarding-density.test.mjs tests/release-notes-action.test.mjs tests/next-dev-runtime-rewrites.test.mjs tests/switch-contrast-style.test.mjs tests/rpc-http.test.mjs tests/request.test.mjs tests/app-updates.test.mjs tests/account-auth.test.mjs tests/account-maintenance.test.mjs tests/startup-snapshot.test.mjs tests/app-bootstrap-startup.test.mjs tests/dialog-layout.test.mjs tests/usage-response.test.mjs tests/ccswitch.test.mjs tests/billing-mode-lock.test.mjs tests/top-level-routes.test.mjs tests/timeout.test.mjs", + "test:runtime": "node --test tests/runtime-capabilities.test.mjs tests/gateway-endpoints.test.mjs tests/transport-errors.test.mjs tests/gateway-settings.test.mjs tests/transport-web-commands.test.mjs tests/codex-profile-cache.test.mjs tests/i18n-page-coverage.test.mjs tests/account-list-cache.test.mjs tests/tauri-command-registry.test.mjs tests/desktop-log-bounds.test.mjs tests/dashboard-direct-mode.test.mjs tests/settings-theme-preview.test.mjs tests/settings-zoom.test.mjs tests/models-search-style.test.mjs tests/codex-cli-onboarding-density.test.mjs tests/release-notes-action.test.mjs tests/next-dev-runtime-rewrites.test.mjs tests/switch-contrast-style.test.mjs tests/chart-tooltip-compositing.test.mjs tests/app-frame-responsive.test.mjs tests/rpc-http.test.mjs tests/request.test.mjs tests/app-updates.test.mjs tests/account-auth.test.mjs tests/account-maintenance.test.mjs tests/startup-snapshot.test.mjs tests/app-bootstrap-startup.test.mjs tests/dialog-layout.test.mjs tests/usage-response.test.mjs tests/ccswitch.test.mjs tests/billing-mode-lock.test.mjs tests/top-level-routes.test.mjs tests/timeout.test.mjs", "start": "next start", "lint": "eslint" }, diff --git a/apps/playwright.config.ts b/apps/playwright.config.ts index dcbd2fc3b..87cf9a2cd 100644 --- a/apps/playwright.config.ts +++ b/apps/playwright.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ video: "retain-on-failure", }, webServer: { - command: "pnpm run build:desktop && node tests/support/static-server.mjs", + command: "corepack pnpm run build:desktop && node tests/support/static-server.mjs", url: `http://${LOCAL_TEST_HOST}:${PORT}`, reuseExistingServer: false, timeout: 120_000, diff --git a/apps/src-tauri/Cargo.lock b/apps/src-tauri/Cargo.lock index f221f8aa7..0025c5501 100644 --- a/apps/src-tauri/Cargo.lock +++ b/apps/src-tauri/Cargo.lock @@ -840,6 +840,7 @@ dependencies = [ "env_logger", "eventsource-stream", "futures-util", + "hyper-util", "log", "os_info", "rand 0.8.5", diff --git a/apps/src-tauri/scripts/before-build.mjs b/apps/src-tauri/scripts/before-build.mjs index 89917c8be..2ab3dc243 100644 --- a/apps/src-tauri/scripts/before-build.mjs +++ b/apps/src-tauri/scripts/before-build.mjs @@ -22,10 +22,6 @@ function hasFrontendPackage(dir) { return existsSync(resolve(dir, "package.json")); } -function hasBuiltFrontendDist(dir) { - return existsSync(resolve(dir, "out", "index.html")); -} - function canConnect(host, port, timeoutMs = 1000) { return new Promise((resolvePromise) => { const socket = new net.Socket(); @@ -289,11 +285,6 @@ if (!frontendDir) { process.exit(1); } -if (task === "build:desktop" && hasBuiltFrontendDist(frontendDir)) { - console.log(`前端产物已存在,跳过重复构建: ${resolve(frontendDir, "out", "index.html")}`); - process.exit(0); -} - if (task === "dev:desktop") { if (await hasReusableDesktopDevServer()) { console.log(`检测到现有前端开发服务,直接复用: http://${desktopDevHost}:${desktopDevPort}`); diff --git a/apps/src-tauri/src/app_shell/mod.rs b/apps/src-tauri/src/app_shell/mod.rs index abd251902..f33265280 100644 --- a/apps/src-tauri/src/app_shell/mod.rs +++ b/apps/src-tauri/src/app_shell/mod.rs @@ -14,4 +14,4 @@ pub(crate) use state::{ KEEP_ALIVE_FOR_LIGHTWEIGHT_CLOSE, LIGHTWEIGHT_MODE_ON_CLOSE_TO_TRAY, TRAY_AVAILABLE, }; pub(crate) use tray::{notify_existing_instance_focused, setup_tray}; -pub(crate) use window::show_main_window; +pub(crate) use window::{show_main_window, sync_main_window_zoom_from_settings}; diff --git a/apps/src-tauri/src/app_shell/startup.rs b/apps/src-tauri/src/app_shell/startup.rs index 2da356db6..bf0db4460 100644 --- a/apps/src-tauri/src/app_shell/startup.rs +++ b/apps/src-tauri/src/app_shell/startup.rs @@ -1,6 +1,7 @@ use crate::commands::settings::sync_window_runtime_state_from_settings; use super::state::TRAY_AVAILABLE; +use super::window::sync_main_window_zoom_from_settings; /// 函数 `sync_startup_window_state` /// @@ -13,7 +14,7 @@ use super::state::TRAY_AVAILABLE; /// /// # 返回 /// 无 -pub(crate) fn sync_startup_window_state() { +pub(crate) fn sync_startup_window_state(app: &tauri::AppHandle) { if let Ok(mut settings) = codexmanager_service::app_settings_get_with_overrides( Some( codexmanager_service::current_close_to_tray_on_close_setting() @@ -22,5 +23,6 @@ pub(crate) fn sync_startup_window_state() { Some(TRAY_AVAILABLE.load(std::sync::atomic::Ordering::Relaxed)), ) { sync_window_runtime_state_from_settings(&mut settings); + sync_main_window_zoom_from_settings(app, &settings); } } diff --git a/apps/src-tauri/src/app_shell/window.rs b/apps/src-tauri/src/app_shell/window.rs index 715e91510..eb4a844f0 100644 --- a/apps/src-tauri/src/app_shell/window.rs +++ b/apps/src-tauri/src/app_shell/window.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use tauri::webview::Color; use tauri::window::{Effect, EffectState, EffectsBuilder}; @@ -15,6 +15,7 @@ const TRAY_PREVIEW_WIDTH: f64 = 360.0; const TRAY_PREVIEW_HEIGHT: f64 = 390.0; const TRAY_PREVIEW_MARGIN: f64 = 8.0; static MAIN_WINDOW_CREATED_ONCE: AtomicBool = AtomicBool::new(false); +static MAIN_WINDOW_ZOOM_PERCENT: AtomicU8 = AtomicU8::new(100); struct MainWindowRef { window: tauri::WebviewWindow, @@ -46,6 +47,7 @@ pub(crate) fn show_main_window(app: &tauri::AppHandle) { navigate_recreated_main_window_to_app(&main_window.window); } let window = main_window.window; + apply_main_window_zoom(&window, cached_main_window_zoom_factor()); if let Err(err) = window.show() { log::warn!("show main window failed: {}", err); return; @@ -54,6 +56,48 @@ pub(crate) fn show_main_window(app: &tauri::AppHandle) { let _ = window.set_focus(); } +/// 从设置快照同步并应用桌面主窗口缩放比例。 +pub(crate) fn sync_main_window_zoom_from_settings( + app: &tauri::AppHandle, + settings: &serde_json::Value, +) { + let Some(zoom_factor) = update_zoom_cache_from_settings( + &MAIN_WINDOW_ZOOM_PERCENT, + settings, + ) else { + return; + }; + let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) else { + return; + }; + apply_main_window_zoom(&window, zoom_factor); +} + +/// 读取当前缓存的主窗口缩放比例。 +fn cached_main_window_zoom_factor() -> f64 { + f64::from(MAIN_WINDOW_ZOOM_PERCENT.load(Ordering::Relaxed)) / 100.0 +} + +/// 从设置快照更新整数百分比缓存。 +fn update_zoom_cache_from_settings( + cache: &AtomicU8, + settings: &serde_json::Value, +) -> Option { + let raw = settings + .get("zoomFactor") + .and_then(serde_json::Value::as_f64)?; + let normalized = codexmanager_service::normalize_ui_zoom_factor(raw); + cache.store((normalized * 100.0).round() as u8, Ordering::Relaxed); + Some(normalized) +} + +/// 将缩放比例应用到指定的主窗口实例。 +fn apply_main_window_zoom(window: &tauri::WebviewWindow, zoom_factor: f64) { + if let Err(err) = window.set_zoom(zoom_factor) { + log::warn!("apply main window zoom failed: {}", err); + } +} + pub(crate) fn hide_tray_preview_window(app: &tauri::AppHandle) { if let Some(window) = app.get_webview_window(TRAY_PREVIEW_WINDOW_LABEL) { if let Err(err) = window.hide() { @@ -265,7 +309,11 @@ fn resolve_tray_preview_position( #[cfg(test)] mod tests { - use super::{resolve_tray_preview_position, should_navigate_recreated_main_window_to_app}; + use super::{ + resolve_tray_preview_position, should_navigate_recreated_main_window_to_app, + update_zoom_cache_from_settings, + }; + use std::sync::atomic::{AtomicU8, Ordering}; use tauri::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalRect, PhysicalSize, Rect}; #[test] @@ -311,4 +359,21 @@ mod tests { #[cfg(not(debug_assertions))] assert!(!should_navigate_recreated_main_window_to_app(true, true)); } + + #[test] + fn main_window_zoom_cache_uses_normalized_settings_and_ignores_missing_values() { + let cache = AtomicU8::new(100); + + assert_eq!( + update_zoom_cache_from_settings(&cache, &serde_json::json!({ "zoomFactor": 0.82 })), + Some(0.8) + ); + assert_eq!(cache.load(Ordering::Relaxed), 80); + + assert_eq!( + update_zoom_cache_from_settings(&cache, &serde_json::json!({})), + None + ); + assert_eq!(cache.load(Ordering::Relaxed), 80); + } } diff --git a/apps/src-tauri/src/commands/mod.rs b/apps/src-tauri/src/commands/mod.rs index f7ead3b40..05b03d19e 100644 --- a/apps/src-tauri/src/commands/mod.rs +++ b/apps/src-tauri/src/commands/mod.rs @@ -7,6 +7,7 @@ pub mod apikey; pub mod codex_profile; pub mod dashboard; pub mod login; +pub mod network_diagnostics; pub mod plugin; pub mod quota; mod registry; diff --git a/apps/src-tauri/src/commands/network_diagnostics.rs b/apps/src-tauri/src/commands/network_diagnostics.rs new file mode 100644 index 000000000..066adc4c8 --- /dev/null +++ b/apps/src-tauri/src/commands/network_diagnostics.rs @@ -0,0 +1,17 @@ +use crate::commands::shared::rpc_call_in_background; + +/// 读取服务端缓存的出口网络诊断。 +#[tauri::command] +pub async fn service_network_diagnostics_get( + addr: Option, +) -> Result { + rpc_call_in_background("networkDiagnostics/get", addr, None).await +} + +/// 异步请求服务端刷新出口网络诊断。 +#[tauri::command] +pub async fn service_network_diagnostics_refresh( + addr: Option, +) -> Result { + rpc_call_in_background("networkDiagnostics/refresh", addr, None).await +} diff --git a/apps/src-tauri/src/commands/registry.rs b/apps/src-tauri/src/commands/registry.rs index a0d201c79..98510693a 100644 --- a/apps/src-tauri/src/commands/registry.rs +++ b/apps/src-tauri/src/commands/registry.rs @@ -58,6 +58,9 @@ macro_rules! invoke_handler { crate::commands::usage::service_usage_list, crate::commands::usage::service_usage_aggregate, crate::commands::usage::service_usage_refresh, + // network diagnostics + crate::commands::network_diagnostics::service_network_diagnostics_get, + crate::commands::network_diagnostics::service_network_diagnostics_refresh, // quota crate::commands::quota::service_quota_overview, crate::commands::quota::service_quota_model_usage, diff --git a/apps/src-tauri/src/commands/settings/ui.rs b/apps/src-tauri/src/commands/settings/ui.rs index 20dedc9d4..d982f8dd3 100644 --- a/apps/src-tauri/src/commands/settings/ui.rs +++ b/apps/src-tauri/src/commands/settings/ui.rs @@ -1,4 +1,5 @@ use crate::app_storage::apply_runtime_storage_env; +use crate::app_shell::sync_main_window_zoom_from_settings; use super::tray_state::{ effective_close_to_tray_requested, sync_window_runtime_state_from_settings, tray_available, @@ -20,6 +21,7 @@ pub fn app_close_to_tray_on_close_get(app: tauri::AppHandle) -> bool { apply_runtime_storage_env(&app); if let Ok(mut settings) = codexmanager_service::app_settings_get() { sync_window_runtime_state_from_settings(&mut settings); + sync_main_window_zoom_from_settings(&app, &settings); } codexmanager_service::current_close_to_tray_on_close_setting() && tray_available() } @@ -44,6 +46,7 @@ pub fn app_close_to_tray_on_close_set(app: tauri::AppHandle, enabled: bool) -> b }); if let Ok(mut settings) = codexmanager_service::app_settings_set(Some(&payload)) { sync_window_runtime_state_from_settings(&mut settings); + sync_main_window_zoom_from_settings(&app, &settings); } codexmanager_service::current_close_to_tray_on_close_setting() && tray_available() } @@ -71,6 +74,7 @@ pub async fn app_settings_get(app: tauri::AppHandle) -> Result["t"]; - const ACCOUNT_MANAGER_QUERY_KEYS = { status: ["account-manager", "status"] as const, users: ["account-manager", "users"] as const, @@ -595,7 +593,8 @@ export default function AccountManagerPage() { ); const status = statusQuery.data; - const users = usersQuery.data?.items ?? []; + const userItems = usersQuery.data?.items; + const users = useMemo(() => userItems ?? [], [userItems]); const totalUsers = usersQuery.data?.total ?? status?.appUserCount ?? users.length; const currentUserPage = usersQuery.data?.page ?? userPage; const currentUserPageSize = usersQuery.data?.pageSize ?? userPageSizeNumber; diff --git a/apps/src/app/accounts/accounts-page-helpers.tsx b/apps/src/app/accounts/accounts-page-helpers.tsx index c6eecf746..0638b709c 100644 --- a/apps/src/app/accounts/accounts-page-helpers.tsx +++ b/apps/src/app/accounts/accounts-page-helpers.tsx @@ -458,7 +458,7 @@ export function AccountStatusCell({ account }: { account: Account }) { ()); + const [rowHeights, setRowHeights] = useState>({}); + const rowKeySignature = JSON.stringify(rowKeys); + + const registerMainRow = useCallback( + (rowKey: string) => (element: HTMLTableRowElement | null) => { + if (element) { + mainRowsRef.current.set(rowKey, element); + } else { + mainRowsRef.current.delete(rowKey); + } + }, + [], + ); + useLayoutEffect(() => { + let animationFrame: number | null = null; + const activeRowKeys = JSON.parse(rowKeySignature) as string[]; + + const measureRows = () => { + const nextHeights: Record = {}; + for (const rowKey of activeRowKeys) { + const mainRow = mainRowsRef.current.get(rowKey); + if (!mainRow) { + continue; + } + + const height = Math.ceil(mainRow.getBoundingClientRect().height); + if (height > 0) { + nextHeights[rowKey] = height; + } + } + + setRowHeights((currentHeights) => { + const nextKeys = Object.keys(nextHeights); + const unchanged = + nextKeys.length === Object.keys(currentHeights).length && + nextKeys.every( + (rowKey) => currentHeights[rowKey] === nextHeights[rowKey], + ); + return unchanged ? currentHeights : nextHeights; + }); + }; + + const scheduleMeasurement = () => { + if (animationFrame !== null) { + cancelAnimationFrame(animationFrame); + } + animationFrame = requestAnimationFrame(() => { + animationFrame = null; + measureRows(); + }); + }; + + measureRows(); + const resizeObserver = + typeof ResizeObserver === "undefined" + ? null + : new ResizeObserver(scheduleMeasurement); + for (const rowKey of activeRowKeys) { + const mainRow = mainRowsRef.current.get(rowKey); + if (mainRow) { + resizeObserver?.observe(mainRow); + } + } + window.addEventListener("resize", scheduleMeasurement); + + return () => { + resizeObserver?.disconnect(); + window.removeEventListener("resize", scheduleMeasurement); + if (animationFrame !== null) { + cancelAnimationFrame(animationFrame); + } + }; + }, [rowKeySignature]); + + return { + registerMainRow, + actionRowStyle: (rowKey: string) => { + const height = rowHeights[rowKey]; + return height ? { height: `${height}px` } : undefined; + }, + }; +} + export interface AccountsPageViewProps { accounts: Account[]; planTypes: PlanTypeOption[]; @@ -223,7 +309,6 @@ export interface AccountsPageViewProps { export function AccountsPageView(props: AccountsPageViewProps) { const { t } = useI18n(); const { - accounts, planTypes, totalAccounts, isLoading, @@ -329,6 +414,152 @@ export function AccountsPageView(props: AccountsPageViewProps) { cleanupStatusDraft.includes(option.id) ? total + option.count : total, 0, ); + const accountRows = visibleAccounts.map((account, index) => { + const filteredIndex = filteredAccountIndexMap.get(account.id) ?? -1; + return { + account, + rowKey: `account:${safePage}:${index}:${account.id}`, + quotaItems: buildQuotaSummaryItems(account, t), + modelPoolDisplayText: `${t("模型池")}: ${ + account.modelSlugs.length + ? account.modelSlugs.slice(0, 2).join(", ") + : t("全部 API 模型") + }${ + account.modelSlugs.length > 2 + ? ` +${account.modelSlugs.length - 2}` + : "" + }`, + canMoveUp: canReorderAccounts && filteredIndex > 0, + canMoveDown: + canReorderAccounts && + filteredIndex !== -1 && + filteredIndex < filteredAccounts.length - 1, + }; + }); + const tableRowKeys = isLoading + ? Array.from({ length: 5 }, (_, index) => `loading:${safePage}:${index}`) + : accountRows.length > 0 + ? accountRows.map(({ rowKey }) => rowKey) + : [`empty:${safePage}`]; + const { registerMainRow, actionRowStyle } = + useSynchronizedAccountRowHeights(tableRowKeys); + + const renderAccountActions = (account: Account) => { + const statusAction = getAccountStatusAction(account, t); + const StatusActionIcon = statusAction.icon; + const isRefreshingCurrentAccount = isRefreshingAccountId === account.id; + const isRefreshingCurrentRt = isRefreshingRtAccountId === account.id; + return ( +
+ + + } + nativeButton={false} + /> + } + nativeButton={false} + disabled={!isServiceReady} + title={t("更多账号操作")} + aria-label={t("更多账号操作")} + > + + {t("更多账号操作")} + + + + refreshAccount(account.id)} + > + + {t("刷新用量")} + + refreshAccountRt(account.id)} + > + + {t("刷新 AT/RT")} + RT + + + + account.preferred + ? clearPreferredAccount(account.id) + : setPreferredAccount(account.id) + } + > + + {account.preferred ? t("取消优先") : t("设为优先")} + + + statusAction.action && + toggleAccountStatus( + account.id, + statusAction.action === "enable", + account.status, + ) + } + > + + {statusAction.label} + + + handleDeleteSingle(account)} + > + {t("删除")} + + + + +
+ ); + }; return (
@@ -772,10 +1003,15 @@ export function AccountsPageView(props: AccountsPageViewProps) { - - - - +
+
+
+ + + 0 && @@ -785,24 +1021,35 @@ export function AccountsPageView(props: AccountsPageViewProps) { } onCheckedChange={toggleSelectAllVisible} /> - - - {t("账号信息")} - - - {t("额度详情")} - - {t("顺序")} - {t("状态")} - - {t("操作")} - - - - + + + {t("账号信息")} + + + {t("额度详情")} + + + {t("顺序")} + + + {t("状态")} + + + + {isLoading ? ( - Array.from({ length: 5 }).map((_, index) => ( - + tableRowKeys.map((rowKey) => ( + @@ -822,14 +1069,14 @@ export function AccountsPageView(props: AccountsPageViewProps) { - - - )) - ) : visibleAccounts.length === 0 ? ( - - + ) : accountRows.length === 0 ? ( + +

{t("未找到符合条件的账号")}

@@ -837,35 +1084,34 @@ export function AccountsPageView(props: AccountsPageViewProps) { ) : ( - visibleAccounts.map((account) => { - const quotaItems = buildQuotaSummaryItems(account, t); - const statusAction = getAccountStatusAction(account, t); - const StatusActionIcon = statusAction.icon; - const modelPoolText = account.modelSlugs.length - ? account.modelSlugs.slice(0, 2).join(", ") - : t("全部 API 模型"); - const modelPoolDisplayText = `${t("模型池")}: ${modelPoolText}${ - account.modelSlugs.length > 2 - ? ` +${account.modelSlugs.length - 2}` - : "" - }`; - const isRefreshingCurrentAccount = - isRefreshingAccountId === account.id; - const isRefreshingCurrentRt = - isRefreshingRtAccountId === account.id; - const filteredIndex = - filteredAccountIndexMap.get(account.id) ?? -1; - const canMoveUp = canReorderAccounts && filteredIndex > 0; - const canMoveDown = - canReorderAccounts && - filteredIndex !== -1 && - filteredIndex < filteredAccounts.length - 1; - return ( - + accountRows.map( + ({ + account, + rowKey, + quotaItems, + modelPoolDisplayText, + canMoveUp, + canMoveDown, + }) => ( + toggleSelect(account.id)} + aria-label={`${t("选择账号")} ${ + account.label || account.name || account.id + }`} /> @@ -933,6 +1179,7 @@ export function AccountsPageView(props: AccountsPageViewProps) { } onClick={() => void handleMoveAccount(account, "up")} title={t("上移一位")} + aria-label={t("上移一位")} > @@ -950,6 +1197,7 @@ export function AccountsPageView(props: AccountsPageViewProps) { void handleMoveAccount(account, "down") } title={t("下移一位")} + aria-label={t("下移一位")} > @@ -964,129 +1212,88 @@ export function AccountsPageView(props: AccountsPageViewProps) { } onClick={() => openAccountEditor(account)} title={t("编辑账号信息")} + aria-label={t("编辑账号信息")} >
- + - -
- - - - - - - - refreshAccount(account.id)} - > - - {t("刷新用量")} - - refreshAccountRt(account.id)} - > - - {t("刷新 AT/RT")} - RT - - - - account.preferred - ? clearPreferredAccount(account.id) - : setPreferredAccount(account.id) - } - > - - {account.preferred ? t("取消优先") : t("设为优先")} - - - statusAction.action && - toggleAccountStatus( - account.id, - statusAction.action === "enable", - account.status, - ) - } - > - - {statusAction.label} - - - handleDeleteSingle(account)} - > - {t("删除")} - - - - -
-
- ); - }) + ), + ) )} -
-
+ + +
+ +
+ + + + + {t("操作")} + + + + + {isLoading ? ( + tableRowKeys.map((rowKey, index) => ( + + + + + + )) + ) : accountRows.length === 0 ? ( + + + + + ) : ( + accountRows.map(({ account, rowKey }) => ( + + + {renderAccountActions(account)} + + + )) + )} + +
+
+ diff --git a/apps/src/app/globals.css b/apps/src/app/globals.css index 4df9cb564..5f660aa23 100644 --- a/apps/src/app/globals.css +++ b/apps/src/app/globals.css @@ -348,11 +348,8 @@ [data-appearance='modern'] [data-slot="dropdown-menu-content"], [data-appearance='modern'] [data-slot="dropdown-menu-sub-content"], [data-appearance='modern'] [data-slot="select-content"], -[data-appearance='modern'] [data-slot="chart-tooltip"], [data-appearance='modern'] [data-slot="tooltip-content"], -[data-appearance='modern'] .sonner-toast, -[data-appearance='modern'] .table-sticky-action-head, -[data-appearance='modern'] .table-sticky-action-cell { +[data-appearance='modern'] .sonner-toast { backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); border-color: var(--glass-border); @@ -368,14 +365,28 @@ [data-appearance='modern'] [data-slot="dropdown-menu-content"], [data-appearance='modern'] [data-slot="dropdown-menu-sub-content"], [data-appearance='modern'] [data-slot="select-content"], -[data-appearance='modern'] [data-slot="chart-tooltip"], [data-appearance='modern'] [data-slot="tooltip-content"], -[data-appearance='modern'] .sonner-toast, -[data-appearance='modern'] .table-sticky-action-head, -[data-appearance='modern'] .table-sticky-action-cell { +[data-appearance='modern'] .sonner-toast { background: var(--glass-floating); } +[data-appearance='modern'] .account-actions-rail { + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); + background: var(--glass-floating); + border-color: var(--glass-border); +} + +/* 移动中的图表提示层禁用背景模糊,避免 WebView2 留下旧合成帧。 */ +[data-appearance='modern'] [data-slot="chart-tooltip"] { + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: color-mix(in srgb, var(--popover) 96%, var(--background)); + border-color: var(--glass-border); + box-shadow: 0 12px 28px -20px rgb(0 0 0 / 0.52); + contain: paint; +} + [data-appearance='modern'] [data-slot="card-footer"], [data-appearance='modern'] [data-slot="dialog-footer"], [data-appearance='modern'] [data-slot="table-footer"] { @@ -472,30 +483,68 @@ tr { height: 100%; } -.table-sticky-action-head, -.table-sticky-action-cell { - position: sticky; - right: 0; +.account-pool-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 7rem; + min-width: 0; + width: 100%; +} + +.account-pool-main { + min-width: 0; + overflow: hidden; +} + +.account-pool-main [data-slot="table-container"] { + overscroll-behavior-inline: contain; +} + +.account-pool-main-table { + min-width: 56rem; +} + +.account-pool-main [data-slot="table-body"] [data-slot="table-row"] { + height: 3rem; +} + +.account-actions-rail { + position: relative; + z-index: 2; + min-width: 0; border-left: 1px solid var(--border); + background: rgb(var(--surface-rgb) / 0.98); box-shadow: -10px 0 16px -14px rgb(15 23 42 / 0.18); } -.table-sticky-action-head { - z-index: 4; - background: rgb(var(--surface-rgb) / 0.98); +.account-actions-rail [data-slot="table-container"] { + height: 100%; + overflow: hidden; } -.table-sticky-action-cell { - z-index: 3; - background: rgb(var(--surface-rgb) / 0.96); +.account-actions-table { + width: 100%; + table-layout: fixed; } -[data-slot="table-body"] [data-slot="table-row"]:hover .table-sticky-action-cell { - background: var(--table-row-hover-bg); +.account-action-rail-head, +.account-action-rail-cell { + width: 100%; + padding-right: 0.25rem; + padding-left: 0.25rem; } -[data-slot="table-body"] [data-slot="table-row"][data-state="selected"] .table-sticky-action-cell { - background: var(--table-row-selected-bg); +.account-action-rail-cell { + height: 100%; +} + +.account-action-rail-cell .table-action-cell { + min-height: 2rem; +} + +@media (max-width: 639px) { + .account-pool-layout { + grid-template-columns: minmax(0, 1fr) 6rem; + } } /* 🚀 Performance Mode Logic (Fixed Selectors) */ @@ -527,8 +576,7 @@ body.low-transparency [data-slot="button"][data-variant="outline"], body.low-transparency [data-slot="input"], body.low-transparency [data-slot="textarea"], body.low-transparency [data-slot="select-trigger"], -body.low-transparency .table-sticky-action-head, -body.low-transparency .table-sticky-action-cell, +body.low-transparency .account-actions-rail, body.low-transparency .sonner-toast { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; diff --git a/apps/src/app/logs/page-sections.tsx b/apps/src/app/logs/page-sections.tsx index d8ebde4d1..939f111b5 100644 --- a/apps/src/app/logs/page-sections.tsx +++ b/apps/src/app/logs/page-sections.tsx @@ -1,6 +1,18 @@ "use client"; -import { AlertTriangle, CheckCircle2, Database, RefreshCw, Trash2, Zap } from "lucide-react"; +import { + AlertTriangle, + CheckCircle2, + ChevronLeft, + ChevronRight, + ChevronsLeft, + CornerDownLeft, + Database, + RefreshCw, + Trash2, + Zap, +} from "lucide-react"; +import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -21,7 +33,6 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { buildStaticRouteUrl } from "@/lib/utils/static-routes"; import { formatTsFromSeconds } from "@/lib/utils/usage"; import { cn } from "@/lib/utils"; import { @@ -78,8 +89,10 @@ export function RequestLogsTabContent({ onEndTimeChange, onClearTimeRange, onPageSizeChange, + onFirstPage, onPreviousPage, onNextPage, + onJumpPage, }: { t: TranslateFn; isAdminMode: boolean; @@ -113,9 +126,29 @@ export function RequestLogsTabContent({ onEndTimeChange: (value: string) => void; onClearTimeRange: () => void; onPageSizeChange: (value: string | null) => void; + onFirstPage: () => void; onPreviousPage: () => void; onNextPage: () => void; + onJumpPage: (page: number) => void; }) { + const [jumpPageInput, setJumpPageInput] = useState(String(currentPage)); + + useEffect(() => { + setJumpPageInput(String(currentPage)); + }, [currentPage]); + + const submitJumpPage = () => { + const normalizedInput = jumpPageInput.trim(); + const parsedPage = normalizedInput ? Number(normalizedInput) : Number.NaN; + const targetPage = Number.isSafeInteger(parsedPage) + ? Math.min(totalPages, Math.max(1, parsedPage)) + : currentPage; + setJumpPageInput(String(targetPage)); + if (targetPage !== currentPage) { + onJumpPage(targetPage); + } + }; + return (
@@ -404,17 +437,23 @@ export function RequestLogsTabContent({ -
+
{t("共")} {summary.filteredCount} {t("条匹配日志")}
-
-
- +
+
+ {t("每页显示")}
-
+
+ -
+
{t("第")} {currentPage} / {totalPages} {t("页")}
+
{ + event.preventDefault(); + submitJumpPage(); + }} + > + + setJumpPageInput(event.target.value)} + onBlur={() => { + if (!jumpPageInput.trim()) { + setJumpPageInput(String(currentPage)); + } + }} + /> + + {t("页")} + + +
diff --git a/apps/src/app/logs/page.tsx b/apps/src/app/logs/page.tsx index 671c87bbf..f3db4b8e3 100644 --- a/apps/src/app/logs/page.tsx +++ b/apps/src/app/logs/page.tsx @@ -35,9 +35,10 @@ import { fromDateTimeLocalValue, } from "./page-helpers"; import { buildSummaryPlaceholder } from "./page-cells"; -import { AccountListResult, ApiKey, RequestLogListResult, StartupSnapshot } from "@/types"; +import { ApiKey, RequestLogListResult, StartupSnapshot } from "@/types"; const REQUEST_LOG_LIST_REFETCH_INTERVAL_MS = 30_000; +const EMPTY_REQUEST_LOGS: RequestLogListResult["items"] = []; function LogsPageContent() { const { t } = useI18n(); @@ -161,7 +162,7 @@ function LogsPageContent() { }, }); - const logs = logsResult?.items || []; + const logs = logsResult?.items || EMPTY_REQUEST_LOGS; const apiKeyLookupIds = useMemo(() => { const ids = logs .map((item) => String(item.keyId || "").trim()) @@ -459,8 +460,10 @@ function LogsPageContent() { setPageSize(value || "10"); setPage(1); }} + onFirstPage={() => setPage(1)} onPreviousPage={() => setPage(Math.max(1, currentPage - 1))} onNextPage={() => setPage(Math.min(totalPages, currentPage + 1))} + onJumpPage={setPage} /> diff --git a/apps/src/app/settings/components/appearance-tab-content.tsx b/apps/src/app/settings/components/appearance-tab-content.tsx index 1c957d63d..fb7276958 100644 --- a/apps/src/app/settings/components/appearance-tab-content.tsx +++ b/apps/src/app/settings/components/appearance-tab-content.tsx @@ -1,6 +1,7 @@ "use client"; -import { Check, Palette } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { Check, Minus, MonitorUp, Palette, Plus, RotateCcw } from "lucide-react"; import { APPEARANCE_PRESETS } from "@/lib/appearance"; import { Button } from "@/components/ui/button"; import { @@ -11,6 +12,13 @@ import { CardTitle, } from "@/components/ui/card"; import { cn } from "@/lib/utils"; +import { + normalizeUiZoomFactor, + UI_ZOOM_FACTOR_DEFAULT, + UI_ZOOM_FACTOR_MAX, + UI_ZOOM_FACTOR_MIN, + UI_ZOOM_FACTOR_STEP, +} from "@/lib/ui-zoom"; import { THEMES } from "@/app/settings/settings-page-helpers"; import { ThemePreviewSwatch } from "@/app/settings/components/theme-preview-swatch"; @@ -20,19 +28,148 @@ interface AppearanceTabContentProps { t: TranslateFn; theme: string | undefined; appearancePreset: string | null | undefined; + isDesktopRuntime: boolean; + zoomFactor: number; onThemeChange: (nextTheme: string) => void; onAppearancePresetChange: (nextPreset: string) => void; + onZoomFactorChange: (nextZoomFactor: number) => Promise; } export function AppearanceTabContent({ t, theme, appearancePreset, + isDesktopRuntime, + zoomFactor, onThemeChange, onAppearancePresetChange, + onZoomFactorChange, }: AppearanceTabContentProps) { + const normalizedZoomFactor = normalizeUiZoomFactor(zoomFactor); + const [zoomDraft, setZoomDraft] = useState(normalizedZoomFactor); + const [isZoomSaving, setIsZoomSaving] = useState(false); + const zoomCommitCancelledRef = useRef(false); + + useEffect(() => { + setZoomDraft(normalizedZoomFactor); + }, [normalizedZoomFactor]); + + /** + * 提交桌面缩放;失败时恢复最近一次服务端快照。 + */ + const commitZoomFactor = async (value: number) => { + const normalized = normalizeUiZoomFactor(value); + setZoomDraft(normalized); + if (isZoomSaving || normalized === normalizedZoomFactor) return; + + setIsZoomSaving(true); + try { + await onZoomFactorChange(normalized); + } catch { + setZoomDraft(normalizedZoomFactor); + } finally { + setIsZoomSaving(false); + } + }; + return ( <> + {isDesktopRuntime ? ( + + +
+ + {t("界面缩放")} +
+ 75% - 125% +
+ +
+
+ + { + zoomCommitCancelledRef.current = false; + setZoomDraft(Number(event.currentTarget.value) / 100); + }} + onPointerDown={() => { + zoomCommitCancelledRef.current = false; + }} + onPointerUp={(event) => + void commitZoomFactor(Number(event.currentTarget.value) / 100) + } + onPointerCancel={() => { + zoomCommitCancelledRef.current = true; + setZoomDraft(normalizedZoomFactor); + }} + onKeyUp={(event) => + void commitZoomFactor(Number(event.currentTarget.value) / 100) + } + onBlur={(event) => { + if (zoomCommitCancelledRef.current) { + zoomCommitCancelledRef.current = false; + return; + } + void commitZoomFactor(Number(event.currentTarget.value) / 100); + }} + disabled={isZoomSaving} + className="h-2 min-w-0 cursor-pointer accent-primary disabled:cursor-wait" + aria-label={t("界面缩放比例")} + /> + +
+
+ + {Math.round(zoomDraft * 100)}% + + +
+
+
+
+ ) : null} +
diff --git a/apps/src/app/settings/components/network-diagnostics-card.tsx b/apps/src/app/settings/components/network-diagnostics-card.tsx new file mode 100644 index 000000000..f7b5072fc --- /dev/null +++ b/apps/src/app/settings/components/network-diagnostics-card.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Globe2, RefreshCw } from "lucide-react"; +import { toast } from "sonner"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + networkDiagnosticsClient, + type NetworkDiagnosticsSnapshot, +} from "@/lib/api/network-diagnostics"; +import { getAppErrorMessage } from "@/lib/api/transport"; + +const QUERY_KEY = ["network-diagnostics"] as const; + +function formatCheckedAt(timestamp: number | null): string { + if (!timestamp) return "--"; + return new Date(timestamp * 1000).toLocaleString(); +} + +export function NetworkDiagnosticsCard({ + t, + enabled, +}: { + t: (value: string) => string; + enabled: boolean; +}) { + const queryClient = useQueryClient(); + const diagnostics = useQuery({ + queryKey: QUERY_KEY, + queryFn: () => networkDiagnosticsClient.get(), + enabled, + staleTime: 30_000, + refetchInterval: (query) => + query.state.data?.refreshing ? 1_000 : false, + }); + const refresh = useMutation({ + mutationFn: () => networkDiagnosticsClient.refresh(), + onSuccess: (snapshot) => { + queryClient.setQueryData(QUERY_KEY, snapshot); + toast.success( + snapshot.refreshScheduled + ? t("已开始刷新出口诊断") + : t("已使用最近的出口诊断结果"), + ); + }, + onError: (error: unknown) => { + toast.error(`${t("出口诊断刷新失败")}: ${getAppErrorMessage(error)}`); + }, + }); + const snapshot = diagnostics.data; + const isRefreshing = refresh.isPending || snapshot?.refreshing; + const regionLabel = [snapshot?.country, snapshot?.countryCode] + .filter(Boolean) + .join(" / ") || "--"; + + return ( + + +
+ + {t("出口网络诊断")} + {snapshot?.enabled === false ? ( + {t("已关闭")} + ) : null} +
+ + {t("用于核对当前服务出口,不会根据 IP 结果自动改变账号状态。")} + +
+ +
+
+
{t("出口 IP")}
+ {snapshot?.ip || "--"} +
+
+
{t("国家或地区")}
+
{regionLabel}
+
+
+
ASN
+
{snapshot?.asn ? `AS${snapshot.asn}` : "--"}
+
+
+
{t("网络组织")}
+
{snapshot?.organization || "--"}
+
+
+
{t("检测时间")}
+
{formatCheckedAt(snapshot?.checkedAt || null)}
+
+
+
{t("检测来源")}
+
{snapshot?.source || "--"}
+
+
+ {snapshot?.error ? ( +

{snapshot.error}

+ ) : null} +
+

+ {t("查询沿用当前 OpenAI 上游代理;外部诊断服务失败不会影响账号刷新或网关请求。")} +

+ +
+
+
+ ); +} diff --git a/apps/src/app/settings/page.tsx b/apps/src/app/settings/page.tsx index 6230a09f3..814d73c4e 100644 --- a/apps/src/app/settings/page.tsx +++ b/apps/src/app/settings/page.tsx @@ -26,6 +26,7 @@ import { applyAppearancePreset, normalizeAppearancePreset, } from "@/lib/appearance"; +import { normalizeUiZoomFactor } from "@/lib/ui-zoom"; import { AppSettings, BackgroundTaskSettings } from "@/types"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { @@ -72,6 +73,7 @@ import { ServiceListenCard, } from "@/app/settings/components/general-tab-cards"; import { GeneralBasicsCard } from "@/app/settings/components/general-basics-card"; +import { NetworkDiagnosticsCard } from "@/app/settings/components/network-diagnostics-card"; import { TasksTabContent } from "@/app/settings/components/tasks-tab-content"; import { CUSTOM_WORKER_MODE_VALUE, @@ -400,11 +402,19 @@ function AdminSettingsPage() { !canAccessManagementRpc || Boolean(snapshot) || isSnapshotError, ); + const settingsSaveQueueRef = useRef>(Promise.resolve()); const updateSettings = useMutation({ mutationFn: (patch: Partial & { _silent?: boolean }) => { const actualPatch = { ...patch }; delete actualPatch._silent; - return appClient.setSettings(actualPatch); + const saveTask = settingsSaveQueueRef.current.then(() => + appClient.setSettings(actualPatch), + ); + settingsSaveQueueRef.current = saveTask.then( + () => undefined, + () => undefined, + ); + return saveTask; }, onSuccess: (nextSnapshot, variables) => { queryClient.setQueryData(["app-settings-snapshot"], nextSnapshot); @@ -955,6 +965,20 @@ function AdminSettingsPage() { ); }; + /** + * 仅在桌面运行壳中持久化主窗口缩放比例。 + */ + const handleZoomFactorChange = async (nextZoomFactor: number) => { + if (!isDesktopRuntime || !snapshot) return; + + const normalized = normalizeUiZoomFactor(nextZoomFactor); + if (normalized === normalizeUiZoomFactor(snapshot.zoomFactor)) return; + await updateSettings.mutateAsync({ + zoomFactor: normalized, + _silent: true, + }); + }; + /** * 函数 `updateBackgroundTasks` * @@ -1406,12 +1430,17 @@ function AdminSettingsPage() { canCloseToTray={canCloseToTray} updateSettings={updateSettings} /> - + + diff --git a/apps/src/components/layout/app-frame.tsx b/apps/src/components/layout/app-frame.tsx index 120da9aad..b4d19ad36 100644 --- a/apps/src/components/layout/app-frame.tsx +++ b/apps/src/components/layout/app-frame.tsx @@ -6,9 +6,11 @@ import { Header } from "@/components/layout/header"; import { PageKeepAliveViewport } from "@/components/layout/page-keep-alive-viewport"; import { RouteTransitionOverlay } from "@/components/layout/route-transition-overlay"; import { Sidebar } from "@/components/layout/sidebar"; +import { useAppStore } from "@/lib/store/useAppStore"; import { normalizeRoutePath } from "@/lib/utils/static-routes"; const TRAY_PREVIEW_PATH = "/tray-preview"; +const NARROW_VIEWPORT_QUERY = "(max-width: 639px)"; export function isTrayPreviewPath(pathname: string): boolean { return normalizeRoutePath(pathname) === TRAY_PREVIEW_PATH; @@ -17,6 +19,7 @@ export function isTrayPreviewPath(pathname: string): boolean { export function AppFrame({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const isTrayPreview = isTrayPreviewPath(pathname); + const setSidebarOpen = useAppStore((state) => state.setSidebarOpen); useEffect(() => { document.documentElement.classList.toggle("tray-preview-mode", isTrayPreview); @@ -27,6 +30,21 @@ export function AppFrame({ children }: { children: React.ReactNode }) { }; }, [isTrayPreview]); + useEffect(() => { + const narrowViewport = window.matchMedia(NARROW_VIEWPORT_QUERY); + const collapseSidebar = () => { + if (narrowViewport.matches) { + setSidebarOpen(false); + } + }; + + collapseSidebar(); + narrowViewport.addEventListener("change", collapseSidebar); + return () => { + narrowViewport.removeEventListener("change", collapseSidebar); + }; + }, [setSidebarOpen]); + if (isTrayPreview) { return
{children}
; } @@ -36,7 +54,7 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
-
+
diff --git a/apps/src/components/layout/page-keep-alive-viewport.tsx b/apps/src/components/layout/page-keep-alive-viewport.tsx index 94c4e161b..6525b9e61 100644 --- a/apps/src/components/layout/page-keep-alive-viewport.tsx +++ b/apps/src/components/layout/page-keep-alive-viewport.tsx @@ -57,7 +57,7 @@ function PagePanelFallback({ title }: { title: string }) {
diff --git a/apps/src/components/layout/sidebar.tsx b/apps/src/components/layout/sidebar.tsx index 341dac92b..6e6dec951 100644 --- a/apps/src/components/layout/sidebar.tsx +++ b/apps/src/components/layout/sidebar.tsx @@ -179,8 +179,9 @@ export function Sidebar() { return (
@@ -219,7 +220,7 @@ export function Sidebar() {
-
+