Skip to content

fix(r20): wave3 Phase2断链(web/桌面/壳)+loop2 on-parity+对话轮默认on - #14

Merged
mycyg merged 17 commits into
mainfrom
r20/wave3
Jul 18, 2026
Merged

fix(r20): wave3 Phase2断链(web/桌面/壳)+loop2 on-parity+对话轮默认on#14
mycyg merged 17 commits into
mainfrom
r20/wave3

Conversation

@mycyg

@mycyg mycyg commented Jul 18, 2026

Copy link
Copy Markdown
Owner

R20 修复流水线 wave3:Phase 2 断链修复(web/桌面/原生壳)+ loop2 on-parity 修复 + 对话轮 loop2 默认翻 on。六条产线各自独立复验(根因测试修复前红/后绿),交叉合入后整仓集成终验全绿(pnpm -r typecheck + 全包测试 16/16 包 0 fail + cargo 121 passed)。

Phase 2B web 断链(WEB-1)

  • P1-05 邀请生命周期全链:token 移出列表重渲域不再被抹;新增未登录可达的 /invite 接受落地页(?token= 预填,不进 SPA 路由表规避计数门);新增 DELETE /api/auth/invites/:inviteId(仅管理员+租户隔离)+ web 撤销按钮。
  • P1-06:Settings 移出 adminOnly 常驻,普通成员可达自己的头像/资料/语言/AI 模式;团队管理子区仍 admin 门控。
  • P1-07:部分失败诚实化——Home/Meetings/规划草案的请求失败渲警示条+重试,未知值显「—」不参与零值统计,403 与空态区分。
  • P1-08 web 半边:项目主页成员数 + 加人选择器改用 GET /api/workspace/roster,弃用全局 /api/users。
  • P2-08:成员移出/退群/改角色二次确认 + 请求期间锁行。

Phase 2C 桌面工作台 UX(DSK-UX)

  • R19-5 自动通过策略撤销 UI、R19-23 网盘删除确认+回收站入口、R19-25 网盘行键盘可达、R19-11 presence 单源(per-user 合并)、R19-9 未读重连补拉真值、R19-40 新 DM 即时入栏。
  • R19-3 AI 改动回滚:SDK revertAgentRun + 共享 replay 组件撤销按钮(二次确认+本地客户端门控)+ 桌面 live 壳挂载点接线(消除假接线)+ web 侧「需桌面端操作」提示。

Phase 2D 原生壳(DSK-NATIVE)

  • P2-07/R19-12 OS 通知点击深链(复用 REL-6 统一 create-if-missing 路径;tauri 插件无 click 回调→命令桥兜底)、R19-16 Dock Reopen、R19-13 壳层 locale 运行时同步(重建托盘菜单+通知文案)。

loop2 on-parity + 默认翻转

  • 修 abort-during-tool 挂死(loop2 未知工具不委派 execute 注册表的真实语义缺口,opt-in resolveMissingTool 钩子;未配零行为变化)+ 对话轮 usage/elapsed 戳失。
  • CONVERSATION_TURN_LOOP2_MODE 默认翻 on(parity 已补,config/api 全绿,env.test 锁死);AGENT_RUN 保持 off(schema 翻转需迁移约 40 条 legacy 断言测试,独立排期;生产可经部署 env 覆盖)。

验证

整仓 typecheck 全包 Done;全包测试 16/16 包 0 fail(注入故障日志非失败);cargo 121 passed;ui 218 / desktop-webview 1426 / web 92 / api-client 各线复验一致。

🤖 Generated with Claude Code

mycyg and others added 17 commits July 17, 2026 15:20
R20 DSK-NATIVE 三项(桌面原生簇):

P2-07/R19-12 OS 通知点击深链
- notify.rs: 新增纯函数 deep_link_plan_for_notification_click(通知计划→深链计划,
  复用 route/window_control)。tauri-plugin-notification 2.3.3 桌面端 show() 丢弃 notify_rust
  句柄、无 click 回调——文档化降级,点击消费落 webview 命令桥。
- main.rs: 新增 focus_system_notification 命令,经 REL-6 统一 handle_deep_link_plan 落地。
- pet-window-bridge.ts + pet-surface.ts: 生产绑定补传 onSystemNotification → 命令桥,
  修复此前壳层算好的 route 落 no-op。

R19-16 Dock Reopen
- main.rs: Builder::run → build()?+App::run(callback),消费 macOS RunEvent::Reopen;
  纯函数 dock_reopen_plan(主窗隐藏→show_main_window_plan 恢复,已可见→不动)。

R19-13 壳层 locale 运行时同步
- main.rs: 新增 set_shell_locale 命令 + apply_shell_locale(更新 Mutex<WorkHubLocale>
  + 重建托盘菜单/tooltip);抽出 build_workhub_tray_menu 供安装/切语言复用。
- sse_worker.rs: 通知文案改为实时读共享 locale(current_shell_locale),不再冻结启动值;
  未触碰令牌/身份代际逻辑。
- browser.ts: 语言开关成功/回退时 invoke set_shell_locale,壳层与 webview locale 不漂移。

测试: cargo test 全绿(含新增 notify/dock_reopen/serde-default 用例);desktop-webview
1397 全绿(含新增 pet-surface onSystemNotification + bridge 命令桥用例);pnpm -r typecheck 全绿;
cargo fmt 仅格式化本次改动文件。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
R19-3: SDK method for POST /api/agent-runs/:id/revert (was the named missing piece).
R19-5: SDK method for DELETE /api/permissions/:id (auto-approve policy revocation).
Both optional (matching workbench?/putProposalFeedback? precedent) so apps/web's full
WorkHubApiClient literal mock needn't add stubs; real createApiClient always implements them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… rows

R19-23: workbench drive delete was one-click-destructive with no recovery path. Now a
two-stage confirm (arm, then click again within 5s), an honest 'move to trash' label
(soft-delete is recoverable), a 'moved to recycle bin' receipt, and a recycle-bin view
(lists deleted_items with per-item restore, honoring restore_blocked_reason).
R19-25: drive rows were unfocusable <div>s — keyboard users couldn't open files/enter
folders. Rows are now role=button tabindex=0 with Enter/Space activation + focus-visible
outline (mirrors the kanban card pattern).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Web pointed users to the desktop to revoke learned auto-approve policies, but the desktop
had no code rendering permission_policies or calling DELETE /api/permissions/:id — a
governance dead-end. The account-level settings view now lists each policy (admin-only,
from the settings VM) with a two-stage-confirm revoke wired to revokePermissionPolicy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oncile + instant new DM

R19-11: chat view kept a private onlineUserIds (own poll + reconnect refresh) while rail/
profile-card read store.onlineUserIds — same person could show two conflicting dots. Both
now read/write one shared presence handle over store.onlineUserIds; writes merge per-user
(applyPresenceToOnlineIds) so a subset-scoped poll never wrongly marks others offline.
R19-9: main/collab unread only bumped locally and never reconciled on /me reconnect
(broker resume_mode:fresh). meStream.onReconnected now re-pulls the workbench VM and
reconciles each conversation's unread to the server truth (skipping the open one), plus
refreshes DM list + inbox badge.
R19-40: a brand-new DM only surfaced after the 30s poll (bumpDmUnread no-ops on unknown
conversations). An unknown conversation-message notification now triggers ensureDmListLoaded
so the new DM lands in the left rail immediately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
管理员撤销本工作区一条未过期邀请(软删)。门控同 POST/GET /invites:鉴权先行 401 →
非管理员 403 → 非密码模式 404 → 运行时不支持 501。仓库 revoke(id) 按 id 软删无工作区谓词,
故先经 listPending(actor.workspaceId) 确认该邀请确属本工作区且仍活跃再撤——跨租户 id / 已接受 /
已过期 / 已撤销一律 404,杜绝据 id 撤别工作区的邀请。审计 auth.invite_revoked。openapi 同步。

- route-auth-posture 门:新路由未鉴权返回 401(fail-closed),不在公开白名单。
- auth.test 覆盖:撤销后从 pending 清单消失 / member 403 / 跨工作区·未知·重复撤销幂等 404。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
P1-05 邀请生命周期全链(web 半边):
- 令牌存活:令牌展示区移出 render 重建域,改成 [data-r18-settings-invite-token] 持久兄弟节点
  (SSR 骨架),生成邀请→重拉清单重建 body 时令牌毫发无损,始终可见可复制。
- 接受落地页:新 renderInviteAcceptScreen(packages/ui,公开未登录可达),boot() 特判 /invite
  路径渲接受屏(token+昵称+密码,?token= 预填),成功后 location.assign("/") 以新号进工作台。
  不进 SPA 路由注册表,避免动 routeTree 计数。
- 撤销:未过期邀请每行加「撤销」按钮,接 DELETE /api/auth/invites/:id,请求期间禁用防重复。

P1-06 普通成员 Settings 入口:settings 移出 adminOnly 的 admin 组、并入 team 组常驻——个人设置
(头像/资料/语言/AI 模式)人人可达;页内团队成员/邀请子区仍按 isAdmin 门控(不变)。

P1-07 部分失败诚实化:
- Home:项目清单加载失败(projects===undefined)时显式渲警示条 + 重试按钮,桌内给失败说明而非
  「还没有项目」空态;metric 未知值用「—」不参与零值统计;文案「未加载」取代误导的「稍后同步」。
- Meetings:listProjects 的 not_identified 冒泡去重认证(此前一并吞掉致掉线用户看空切换器)。
- 项目主页规划草案:403(无权,如实说明)与 5xx(警示条+重试)分开,不再都糊成「暂无草案」。

P1-08 花名册(web 半边):项目主页成员数 + 会话加人选择器改用 GET /api/workspace/roster(工作区
分页 total/members),替代全局 /api/users(跨租户 + 硬 200 截断)。

P2-08 破坏性操作二次确认:新 armConfirmButton 两段式确认范式(可单测)——成员移出/退出/移出会话
单击只武装(改标签,5 秒回退)再点才执行;改角色「选新值→点确认改角色」两步;请求期间禁用整行控件。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…der loop2=on

两个 loop2 on-parity 回归(负责人翻默认开关时发现):

1. abort-during-tool 挂死(AGENT_RUN_LOOP2_MODE=on)
   根因:pi/loop2 把「模型可见工具集(toModelTools)」与「可执行注册表(input.tools.execute)」
   合流——模型调用的工具名不在 toModelTools 里就短路成 "tool not found",从不调注册表。
   loop.ts 旧路则无条件把每个模型调用委派给 input.tools.execute(注册表才是未知工具的真门)。
   agent-runs.test 的 abort 用例用空 toModelTools + catch-all execute,on 下工具从不执行→
   toolStarted 永不 resolve→runNext 挂死(红:event loop resolved but promise pending)。
   修法:给 vendored AgentLoopConfig 加 opt-in 的 resolveMissingTool 钩子;未配时保持 pi
   "not found" 原样(零行为变化),config-builder 配一个把缺失工具委派回 input.tools.execute
   的合成工具(与已见工具共用 makePiTool)。工具真跑并阻塞后,abort 收敛到 cancelled 由
   agent-runner 循环后的 driftedRun 检查完成——与 loop.ts 完全一致(都会多跑一次模型调用再漂移)。
   该测试参数化对 off/on 各跑一遍(shadow 双验)。

2. 对话轮 usage 戳失(CONVERSATION_TURN_LOOP2_MODE=on)
   根因:loop2 对话段路径(runConversationTurnSegment)拿到 getFinalMessage().usage 却没累加,
   最终文本 contentJson 只戳 text/memory_citations,漏了 usage_tokens/elapsed_ms——聊天 UI 的
   token 数会丢(model 由 persistAndBroadcastCuuMessage 统一补,故只 usage/elapsed 丢)。
   修法:与 runLegacyTurnLoop 同口径累加 usage(跨轮 input+output,sawUsage 门)并在最终文本
   戳 usage_tokens(仅 sawUsage)+ elapsed_ms。
   顺带把两条钉 legacy 单槽 409 语义的并发测试显式 loop2Mode:"off"(loop2 P4b 有意改为入队,
   原本靠"省略即默认 off"成立,默认翻 on 后会与 gate 死锁)。

env.ts 默认保持 both off(翻转由负责人另做)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…v默认仍off)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
usage/elapsed 戳失回归已由 loop2-abort 线补齐,2 条并发测试已钉 off。
AGENT_RUN 保持默认 off(abort parity 已修但 ~40 条 agent-run 单测断言 legacy,
schema 级翻转需专门迁移那批,独立排期;生产可经部署 env 覆盖)。
新增 env.test 锁死双默认防静默漂移。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
在共享 replay 渲染组件的改动快照行加「撤销此次改动」动作:仅对未回滚快照
(reverted_at 缺失)给按钮,已回滚显示「已回滚」态;带二次确认(首点武装、5 秒内
再点才执行,沿用 decideRollbackConfirmation/网盘删除先例);调注入的
client.revertAgentRun(runId,{snapshot_id}),成功走 onReverted 让宿主重拉、失败给
可见「点此重试」。本地客户端门控复用既有 data-requires-desktop:web 端点它由既有拦截
渲成「需在桌面端操作」提示(对齐 R19-5 撤销策略),桌面端由 bindReplayRevertActions
接真回调执行。纯渲染层只吐带 data-* 的静态标记,二次确认状态机 + revert 调用 + 刷新
都在回调注入式 binder 里(不让 @workhub/ui 直依赖 SDK),用自定义最小元素接口以便无
jsdom 也能假 DOM 单测。桌面 main.ts 加薄接线 bindDesktopAgentRunReplayRevert(仅传
client/回调)。

- packages/ui/src/replay/render.ts: renderSnapshots + decideSnapshotRevertConfirmation
  + bindReplayRevertActions + i18n/CSS
- apps/desktop-webview/src/main.ts: bindDesktopAgentRunReplayRevert
- 测试:ui 218 / desktop 1426 / web 92 全绿,pnpm -r typecheck 绿

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
renderLiveGoldPathPanel(boot() 里 gold-path 详情 LIVE 渲染的唯一挂载点)在渲染 replay
面板后调 bindDesktopAgentRunReplayRevert,传真 client + onReverted 重拉回调——桌面壳是
本地客户端,「撤销此次改动」点击现在真调 POST /api/agent-runs/:id/revert(snapshot_id 走
body),成功后重拉该面板让被撤销快照翻「已回滚」。重渲前先 dispose 上一次绑定清武装计时器。
修前:live 壳渲了按钮但无 handler(假接线);修后:点击真通。

- apps/desktop-webview/src/browser.ts: replay 面板挂载点接线(仅此处)
- apps/desktop-webview/src/main.test.ts: 强化 bindDesktopAgentRunReplayRevert 测试——
  未绑定点击 0 请求(修前态基线)→ 绑定后武装→执行调 revert + onReverted 重拉回调触发
- 复验:desktop 1426 / ui 218 / web 92 全绿,pnpm -r typecheck 绿

注:browser.ts 是自执行入口(顶层 document.getElementById),无 jsdom 不可单测;binder
逻辑全在可导入的 main.ts helper 里测透,挂载点→helper 的调用由 typecheck 保证。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DSK-NATIVE 把通知 locale 从 config.locale 冻结改为每次实时读共享 Mutex<WorkHubLocale>
(R19-13 应用内切语言即刻跟随),旧源码字符串门断言 'let locale = config.locale' 失配。
门更新为断言更强的运行时不变量(current_shell_locale(app) + locale-aware plan builder),
并重生成 i18n 证据产物。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mycyg
mycyg merged commit 6cce222 into main Jul 18, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant