✨ 每个浏览器排自己的派发顺序(服务端持有并应用每端执行目标顺序) - #16
Merged
Merged
Conversation
Agent 卡片上执行目标链的每一档给出上移 / 下移(不引入拖拽库,按钮天然可键盘 操作):一次移动即 POST /v1/workspace/exec-target-order 提交 backend sync_id 排列 + 本机指纹,随后按同一指纹重新拉取该 Agent 的链,「当前」标记当场跟着 真实派发目标改变;提交或重读失败时顺序原封不动,并在这张卡上就地说明。 skipped_for_web 的档只读、不可移动,仍留在链里:它在浏览器语境下永远没有指代 对象(决策 11)。换位只发生在可移动档之间,本机档钉在原位。 取派发计划同样带上本机指纹,服务端据此重排后再挑档,浏览器不自行挑档(R19)。
规格「设备被解除授权或删除时,它的顺序行一并清除」。exec_order_repo 的 DeleteByDevice 已经写好也有仓储单测,但没有任何生产调用方—— device_svc.Revoke 只清了上报的本机路径,浏览器排的顺序在账号里长期残留。 按 LocalPathPurger 的既有接缝补第二个窄接口 ExecTargetOrderPurger: workspace_svc 出 PurgeDeviceExecTargetOrders(它是 exec_order_repo 的 唯一持有者),bootstrap 注入,Revoke 里与清路径同样 fail-open——清不掉 不回滚已经生效的撤销,只记一条 Warn。账号级的执行目标集合在同步组里, 不属于任何一台设备,不受影响。
internal/api/workspace 的包注释声明「每一个响应结构体都不带项目路径 / CLIPath / EnvJSON」,但没有任何机械检查兜住它——隔壁 internal/api/follow 有 guard_test.go 锁住名单载荷,workspace 一直空着,而本轮又往 ExecTargetItem / DispatchTierItem 里加了字段。 补 guard_test.go:按类型登记字段白名单,再从四个响应根反射深走,任何新增 字段、新增嵌套结构体或命中禁词(path/cli/env/token/secret/…)的字段名都会 立刻红。DispatchChoiceItem.Cwd 作为主动派活场景下唯一的显式例外写进白名单。 反向核对不可达的登记项,白名单不会随载荷长大而悄悄过期。 (红验证:给 ExecTargetItem 临时加 CLIPath 字段,四条断言全红后还原。) 顺带把本轮新加的三条 workspace_svc 日志正文改回英文:repo 里其余 13 条 日志正文全是英文,docs/observability.md 也要求「short, stable, lowercase description」,混入中文会让运维按同一条 grep recipe 找不全。
收尾两轴上报但当时在写边界之外的常驻发现,本次一并处理。 - 错误响应的 msg 一直是空的:cago i18n 的 DefaultLang 是全小写 "zh-cn", 而语言包注册在 "zh-CN";本服务没有设置语言的中间件,于是每一次错误构造 都回落到 langs["zh-cn"]——那是 nil map,取回零值空串。影响的是所有错误码、 所有客户端,不是某一个码缺文案。中文包改为同时注册在 i18n.DefaultLang 下。 - applyDeviceOrder 把没有 backend sync_id 的档冲到队尾,而浏览器侧把它钉在 原位,同一次排序两端结果不同。改为钉住原下标;「未覆盖补到队尾」仍只适用于 能被指代却没被排到的档。 - 新对话弹窗取 Agent 清单不带 device_fingerprint 却渲染 current,标出的是 账号顺序的赢家而非这台浏览器真正会派到的那一档,与总览页自相矛盾。
callerDeviceFingerprint 走的是 ensureWebDevice,而 Overview 挂载时无条件调它: 打开总览页 = POST /v1/oauth/device/register = 账号里凭空多一台 kind=web 设备。 e2e「真实 session 打开控制台并呈现隔离用户与真实空态」因此挂了 —— 它先 goto /overview 再断言 /devices 是空的,而这一跳自己把设备建了出来;「完整设备授权」 那条是连带受害:oracle 读到前一条(含重试)留下的两台 web 设备。 顺序的持有者是设备,但只有用户真排了一次序,这台浏览器才需要有设备身份: - 读路径(取链、取派发计划)改成纯本地读 getFingerprint(),没有就回落账号 顺序、不发任何请求。这也正是它原本注释里写的「还没注册时是 null」—— 实现与注释此前是矛盾的。 - 注册移到写路径:saveExecTargetOrder 自己 ensureWebDevice 并返回落库用的 指纹,调用方据此按自己的顺序重读。 - 排序控件不再按「有没有身份」隐藏,否则没注册过的浏览器永远等不到第一次排序。 读的是 localStorage 的指纹而不是 sessionStorage 的设备 JWT:关标签页丢的是 token,不是设备身份和它排的顺序。
sed 规则内联在 shell 单引号串里,里面的 ' 要写成 '"'"' 才转义得掉,而 ci.yml 里写成了 '"',少一对 "':单引号在 [:space:]" 之后就闭掉、双引号一路吞到行尾, 括号落在引号外,bash 直接 syntax error near unexpected token `)'。 这一步只在 failure() 下跑,所以自它落地起一次都没被执行到,直到第一次 e2e 真 挂掉——而那正是最需要截图和 server.log 的时候,证据却因为这行错误上传不了。 规则改为落到 heredoc 生成的 sed 脚本再 -f 引用:定界符加引号,内容原样落盘, 不再有转义这回事。已用带 DSN 口令 / redis-password / password= 的假 server.log 跑过,三处都正确替换、无关行不动。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
让每个浏览器能排自己这台的派发顺序,并让服务端按调用方的顺序解析派发计划。
规格在兄弟仓库:
agentre/docs/specs/2026-08-14-exec-target-order-ux.md(分支ui/2026-08-14-exec-target-order-ux)。改了什么
device_exec_target_orders,键(user_id, device_id, agent_sync_id),值是 backend sync_id 的有序数组。含一次真实迁移202608140001,值得单独看一眼。backend_sync_id——原先rank是位置性的、device_id不唯一(一台机器可挂多个 backend),没有任何键能表达「这一档排第几」。device_id去写。收尾两轴复审各修了什么
规格核验轴(
4e08ddb):exec_order_repo.DeleteByDevice有实现、有 sqlmock 测试,却没有生产调用方——device_svc.Revoke只清了本地路径,浏览器被解除授权后它排的顺序行还留着。按已有的LocalPathPurger先例补了对称的ExecTargetOrderPurger。代码评审轴(
fcf0722):internal/api/workspace/的包注释声明了 R19 不变式(响应结构体不带项目路径 / CLIPath / EnvJSON),却没有任何东西执行它,而本轮恰好加宽了那几个结构体。补上字段白名单守卫(反射深走,新嵌套结构体也会红)。加之前做过 RED 探针:临时给ExecTargetItem加CLIPath→ 4 条断言 FAIL,再还原。交付后又处理的三条常驻发现(
82dc945)msg一直是空的。 cago i18n 的DefaultLang是全小写"zh-cn",而语言包注册在"zh-CN";本服务没有设置语言的中间件,于是每一次错误构造都回落到langs["zh-cn"]——那是 nil map,取回零值空串。影响的是所有错误码、所有客户端,不是某一个码缺文案。新增的internal/pkg/code/code_test.go全量遍历语言包守住这条。applyDeviceOrder把没有 backend sync_id 的档冲到队尾,而浏览器侧把它钉在原位,同一次排序两端结果不同。改为钉住原下标;「未覆盖补到队尾」仍只适用于能被指代却没被排到的档。device_fingerprint却渲染current,标出的是账号顺序的赢家而非这台浏览器真正会派到的那一档,与总览页自相矛盾。验证
GOWORK=off make test-backend47 包全 ok、make lint0 issues.、GOWORK=off make test-frontend42 文件 524 测试,全部 EXIT=0。运行时验证跑在正式产物 + 真实 MySQL/Redis(专用 e2e 库)上,并把一台隔离桌面经真实设备流配对进来,取得两端同时在线的对照。本仓库范围内 8 条要求 holds:
show create table与规格逐字一致,含agent_sync_id的utf8mb4_0900_binchosen从claudecode变codex,CURRENT随之移动piagent, claudecode, codexsync_objects的sort_order仍是0,1,2Codex/Claude/Pi,web 同时显示piagent/claudecode/codex已知未观察
两条
skipped_for_web相关的子句未取得运行时证据,均有组件层 vitest 覆盖:skipped_for_web档只读不可移动——该状态要求同步载荷里有一个agentred_fingerprint为空的 backend,而桌面配对后会把指纹回填进去,真机路径上转瞬即逝,本次环境造不出来。另有一条未修:
e2e/webe2e/main.go:77-106的cleanupSQL()/residueSQL()不含device_exec_target_orders,且它走裸 SQL 删设备、绕过Revoke的清理,所以 e2e 跑完会留下孤儿顺序行。产品侧没有对应缺口(生产只有软撤销,没有硬删设备的代码路径)。