oh-my-muse/docs/agent-specs/2026-06-22-handoff跨空间统一接入-execution.md
lili 5ae48383d6 docs(agent-specs): handoff 执行版 + 自审补强 targetPage 透传契约
执行版按拍板(三 owner 全补 + Market consume 端口)给出:统一前端基座(HandoffStore/
hooks/落地路由/类型收口)+ market-api verify/consume 端口 + 三 owner 兑现侧 token 回验
+ 分 P0-P3 按 owner 独立验证 + 边界/失败路径/验收/回滚。
自审补 §四.0:targetPage 是后端意图描述(非前端路由、不带 token)、token 由前端持有
附加进落地路由、authorizationSummaryId/returnUrl 透传链、刷新风险。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 08:43:33 -07:00

18 KiB
Raw Blame History

跨空间 Handoff 统一接入 —— 执行版

版本:v0.1(执行版) · 日期:2026-06-22 · 目标读者:实现 agent · 类型:执行版(契约+数据流+分步+验收,不预写不可验证的代码细节) 上游:handoff 跨空间统一接入评审版(已拍板:三 owner 全补 + Market consume 端点)。 事实基础:四轮并行只读调研(后端发起侧契约 / design-docs SSOT / studio 现状 / 后端兑现侧+consume)。所有跨模块端口形态、边界规则、CAS/幂等接入点均经源码核验。


〇、本版定位

把 design-docs 已定义、后端发起侧已就绪、但端到端从未闭合的"跨空间 Handoff 安全交接"补成真实闭环:统一前端消费层(基座)+ Market 核验/核销端口 + 三 owner(knowledge/agent/content)兑现侧 token 回验。反假绿核心:目标 owner 兑现必须服务端回验 token(属主/过期/一次性),否则红线形同虚设。

按 owner 分 4 阶段(P0 基座 → P1 knowledge → P2 agent → P3 content),每阶段独立可验证、可单独 commit/回滚。


一、目标与范围边界

目标:用户从来源空间发起 → 跳 targetPage → 目标空间凭一次性 token 换 session/precheck(服务端回验)→ 确认原子写 owner 事实 + 核销 token → 返回来源刷新。三 owner 全覆盖,红线("不信任客户端 URL 参数")首次真生效。

范围内

  • 后端 market-api 新增 MarketHandoffTokenApi(verify + consume)+ market-server 实现 + 错误码。
  • 后端三 owner 兑现侧接入回验/核销:knowledge(补)、agent(扩 + 放宽来源校验)、content(从零建 asset_use 两段式)。
  • 前端统一基座:HandoffStore(Zustand)+ 创建/落地/消费/确认/取消 hooks + token URL 卫生路由 + openapi.ts 类型收口。
  • 前端三套目标落地页 + 各域来源侧发起入口(02F 市场为主来源;02E/02D 既是目标也含发布到市场的来源,本版只做"市场→目标"方向)。
  • 各 owner 真后端 playwright e2e(正路闭环 + 负路 token 被拒)。

范围外(留后续)

  • 02C 作品台作为来源发起、02G 个人中心记录跳转(P4,本版不做;它们走同一基座,只新增适配器)。
  • 02D 现有同空间 slot bind(agentType∈{system,user} 流)不删不改语义,仅"放宽以接纳 market 来源"——见 P2 风险。
  • handoff 离线/多端同步、Return Context 的复杂回退动作(只做"返回来源刷新状态")。
  • 微服务部署形态(端口为进程内 Bean,不引入 @FeignClient)。

二、Prerequisites(前置依赖)

  1. 后端单体单 PostgreSQL(单 DataSource)——owner 落库 + Market consume 同库事务原子的前提(见 §六.1)。须实现期确认 market 与各 owner 模块共用同一事务管理器。
  2. 真后端 e2e 环境(VITE_API_MOCK=false + muse-server 48080 + muse_slice_live 种子),globalSetup 须补:已购 active license 的市场资产 + 可绑定目标(知识库/作品/agent)。
  3. BcBoundaryArchTest 当前绿(0 违例)——验收硬基线。
  4. 现有全量回归基线:e2e 47/0、vitest 82。

三、总体架构与数据流

flowchart TD
    subgraph FE[muse-studio 统一基座 P0]
      HS["HandoffStore(Zustand)<br/>session/precheckId/expiry/returnUrl/targetOwner<br/>consumeToken/clearSession/isSessionValid"]
      L["落地路由 /handoff/land<br/>解析 ?token → replaceState 清 URL → 存 HS"]
    end

    subgraph SRC[来源空间 02F 市场]
      S1[发起绑定] --> S2["POST /assets/id/bind-precheck<br/>(已有,拿 authorizationSummaryId)"]
      S2 --> S3["POST /handoffs<br/>(拿 handoffToken + targetPage)"]
    end
    S3 -->|"跳 targetPage?token=..."| L
    L --> HS

    subgraph TGT[目标 owner 空间 P1/P2/P3]
      T1["落地页消费 token → owner precheck<br/>(请求体带 handoffToken)"] --> V
      T2[用户确认] --> T3["owner 兑现落库(凭 precheckId)<br/>同事务内核销 token"]
    end
    HS --> T1
    HS --> T2

    subgraph MKT[market-api 端口 P0 新增 · 进程内 Bean]
      V["verify(token,owner,action)<br/>属主+未过期+未消费+未取消<br/>→ 脱敏摘要 / blockedReason"]
      CN["consume(token,commandId,expectedStatus,ref)<br/>CAS pending→completed + envelope 幂等"]
    end
    T1 -.owner-server 注入.-> V
    T3 -.owner-server 注入.-> CN
    T3 --> R["返回来源刷新<br/>GET /handoffs/token → completed"]

    classDef new fill:#ffe0e0,stroke:#c33;
    classDef have fill:#d4f8d4,stroke:#2a2;
    class S2 have;
    class HS,L,S3,T1,T2,T3,V,CN new;

职责不变式(合 SSOT 边界):来源(02F)只签发 token、不写目标事实;目标 owner 消费 token 换自建 precheck、原子写 + 核销;Market 持有 token 核销 authority(verify/consume 端口);HandoffStore 只承载客户端会话态,不承载授权事实(授权事实在 owner precheck 快照里)。


四、接口 / 数据契约

4.0 URL 与上下文透传契约(自审补强:targetPage 语义)

关键澄清(实现前必读):后端 targetPage(POST /handoffs 返回)= /muse/marketplace/handoffs/targets/{targetOwner}?action=...&assetId=...&targetWorkId=...,是后端风格的"目标意图描述"(owner+action+assetId+workId),既不带 token、也不是 studio 前端路由。故前端不能 window.location=targetPage,而是:

  1. 来源发起透传链:bind-precheck 返回 authorizationSummaryId → 前端把它 + returnUrl(当前来源页,如 /market/assets/{id},供 Return Context 返回)+ targetOwner/targetAction/assetId/targetWorkId 一并传入 POST /handoffs 请求体(后端 MarketHandoffCreateReqVO 含 assetId/targetOwner/targetAction/targetWorkId/authorizationSummaryId/returnUrl/commandId,均经核验)→ 拿回 {handoffToken, targetPage}
  2. 前端落地路由拼装:前端解析 targetPage 的 owner+query,导航到前端自己的落地路由 /handoff/land/{targetOwner},token 由持有者(前端)作为 query 附加:/handoff/land/knowledge?token=<handoffToken>&assetId=...&targetWorkId=...&returnUrl=...。即 token 进 URL 是前端导航时自己加的(前端持有 token),不是 targetPage 带来的。
  3. 落地即消费:落地页 replaceState 立即清 URL 的 token(防刷新重复消费,前端-01 §9)→ 用 token 调 owner precheck(消费 token 换 precheckId)→ precheckId 存 HandoffStore 作后续凭证(token 用完即弃)。刷新风险:落地后 URL token 已清、precheckId 在内存,HandoffStore 默认不持久化 → 刷新落地页需重新发起(实现期决定是否 sessionStorage 持久化 precheckId;不持久化时落地页须对"无 session"优雅提示重发,不可崩)。
  4. content owner 命名:targetOwner=content 对应作品空间,落地路由 /handoff/land/content 映射到作品台(targetWorkId 指向目标作品);兑现走 content asset_use 两段式(§4.2)。

4.1 后端 market-api 端口(P0,放置 muse-module-market-api/.../api/handoff/)

接口 MarketHandoffTokenApi(进程内 @Service Bean,无 @FeignClient;DTO/record 放 api/handoff/dto,不含 DO、不含 tenantId——tenantId 由实现侧从 TenantContextHolder 取):

方法 入参(DTO) 出参(脱敏 record) 语义
verify handoffToken(明文)、expectedTargetOwnerexpectedTargetAction validstatustargetOwnertargetActionassetIdtargetWorkIdauthorizationSummaryIdexpiresAtblockedReason 只读:按 sha256(token) 查 → 属主(当前登录==ownerUserId)→ effectiveStatus 未过期/未消费/未取消 → owner/action 匹配。不回 token。失败返 valid=false+blockedReason(不抛,供 owner 决定降级 or 阻断)
consume handoffTokencommandIdexpectedStatus、目标事实引用(targetWorkId/bindingRef) consumedstatus(=completed) 写:CAS expectedStatus(pending/owner_precheck_created)→completed+completedAt;envelope 幂等(operationId=consumeHandoff,targetKey=sha256(token));属主隔离同 requireOwnedHandoffupdatedRows!=1MARKET_HANDOFF_NOT_CONSUMABLE

实现 MarketHandoffTokenApiImpl(放 market-server/.../api/handoff/,@Service):

  • verify 复用 MuseMarketHandoffEventMapper.selectByTokenHash + effectiveStatus(现有,MarketHandoffServiceImpl:460-467)。
  • consume 复用 updateLifecycleByExpectedStatus(现有 CAS mapper,:79-91,无需新 mapper)+ MarketCommandService envelope(照搬 cancelHandoff 结构 MarketHandoffServiceImpl:196-219);方法须 @Transactional(reserveCommand 是 MANDATORY 传播)。
  • 新增错误码 MARKET_HANDOFF_NOT_CONSUMABLE / MARKET_HANDOFF_ALREADY_CONSUMED(market-api ErrorCodeConstants 段 1_044_000_0xx)。

4.2 三 owner 兑现侧改动

owner 现状 改动
knowledge(P1,最小) 已有 POST /works/{id}/knowledge-bindings/prechecks(含 handoffToken,但只存 hash 不验真)+ createKnowledgeBinding(凭 kbBindPrecheckId) knowledge-server pom 加 market-api;② precheck 路径(MuseKnowledgeBindingService:265-267)注入 MarketHandoffTokenApi,market_kb 时调 verify(替换"只算 hash");③ createBinding 落库成功处调 consume 核销;现有 handoffHash 保留作审计
agent(P2,风险最高) 纯同空间:POST /works/{id}/agent-slots/{slotKey}/prechecks+/bind,ReqVO 无 handoffToken;ServiceImpl 强制 agentType∈{system,user}+ownerUserId 一致(MuseAgentSlotServiceImpl:221-233) ai-server pom 加 market-api;② AgentSlotPrecheckReqVOhandoffToken(market 来源时 service 层必填)、precheck DO 加 sourceOwner/handoffHash;③ precheck market 来源时调 verify;④ 放宽 :221-233 来源校验以接纳 market 来源 agent(关键风险,见 §六.3);⑤ bind 落库成功调 consume
content(P3,最大) 完全缺失:零 handoffToken/asset_use/precheck,无两段式 content-server pom 加 market-api;② 从零建两段式(照搬 knowledge 形态 + content 的 commandId/expectedRevision 约定):POST /muse/works/{id}/asset-use-prechecks(ReqVO:commandId/handoffToken/sourceAssetId/authorizationSummaryId/purposes → verify → precheckId 落库)+ POST /muse/works/{id}/asset-uses(凭 precheckId+expectedWorkRevision → consume → 落资产使用事实);③ 新增 DO/Mapper/Service/错误码

4.3 前端统一基座(P0)

放置 契约
openapi.ts 类型收口 src/types/openapi.ts 导出 BindPrecheckResult/HandoffCreateResult/HandoffStatusResult(取自 market.ts);删 useMarket 手写 MarketBindPrecheckResult,统一用导出类型(消除 id number↔uuid string 漂移)
HandoffStore src/features/handoff/store/(新) Zustand(对齐前端-05 §25.3):state={session, precheckId, precheckExpiry, returnUrl, targetOwner, targetAction, assetId, targetWorkId};action=consumeToken/setPrecheck/clearSession/isSessionValid
通用 hooks src/features/handoff/hooks/(新) useCreateHandoff()(POST /handoffs)、useHandoffStatus(token)(GET)、useCancelHandoff()(POST cancel,CAS)。各域目标消费用各自 owner hook(knowledge/agent/content),由"目标消费适配器"装配
落地路由 src/app/routes + src/features/handoff/pages/HandoffLandingPage(新) 路由 /handoff/land(或按 owner 分 /handoff/land/:targetOwner):解析 ?token query → 立即 replaceState 清除 token query(前端-01 §9 红线)→ 存 HandoffStore → 按 targetOwner 分发到对应 owner 落地组件
owner 落地组件 各 feature 下(P1/P2/P3 分别新增) knowledge/agent/content 落地组件:从 HandoffStore 取 token → 调 owner precheck(带 handoffToken)→ 展示预检 → 用户确认 → owner 兑现 → 跳 returnUrl
来源发起 02F MarketAssetDetailPage(扩) bind-precheck 之后接 useCreateHandoff → 跳 targetPage;targetOwner 枚举严格取后端白名单 {agent,knowledge,content}×{slot_bind,bind,asset_use,governance_handle}

五、分阶段落地

每阶段:后端契约先行(端口/兑现侧)→ 前端接线 → 真后端 e2e(正路+负路)→ tsc/eslint/vitest/边界门全绿 → commit + 回写总账。

P0 基座(前后端,无端到端 e2e,以单测/IT + 边界门为验收)

  • 后端:MarketHandoffTokenApi(verify+consume)+ 实现 + 错误码;market 单测/IT(verify 正路+负路:伪造/过期/跨属主/已取消 token 被拒;consume CAS + 幂等回放)。
  • 前端:openapi.ts 类型收口 + HandoffStore + 通用 hooks + 落地路由框架(replaceState 清 token);vitest(Store 一次性/过期判定、落地路由 token 清除)。
  • 验收:BcBoundaryArchTest 绿;tsc/eslint/vitest 绿;market verify/consume IT 绿。

P1 knowledge(首条端到端闭环)

  • 后端:knowledge precheck 调 verify、createBinding 调 consume(同事务核销)。
  • 前端:02F 发起(bind-precheck→POST /handoffs→跳)+ knowledge 落地组件(消费→precheck→确认→createBinding→返回)。
  • e2e handoff-knowledge.spec:市场资产→发起→跳知识库落地→消费→预检→确认→muse_knowledge_binding 落库→返回刷新 completed;负路:伪造/过期 token 落地→被拒、0 写。

P2 agent(含来源校验放宽,风险最高)

  • 后端:precheck 加 handoffToken + DO 加来源字段 + verify + 放宽 agentType 校验 + bind 调 consume。
  • 前端:agent 落地组件 + 02F/02D 发起(targetOwner=agent,action=slot_bind)。
  • e2e handoff-agent.spec:市场 agent→作品槽位绑定;负路:token 被拒 + 放宽后仍须保护节点不可绑(MuseAgentSlot 保护节点守卫不破)。

P3 content(asset_use 从零建)

  • 后端:asset-use-prechecks + asset-uses 两段式 + verify/consume + DO/Mapper/错误码。
  • 前端:content 落地组件 + 02F 发起(targetOwner=content,action=asset_use)。
  • e2e handoff-content.spec:市场资产→作品资产使用;负路 token 被拒。

六、边界与失败路径

  1. 原子性(load-bearing):owner 兑现落库 + Market consume 须同 DB 事务(单体单 DataSource)。owner 兑现方法 @Transactional,内部调 MarketHandoffTokenApi.consume(传播 REQUIRED,加入 owner 事务)→ owner 写失败则 consume 一并回滚,无"核销了但没绑定"窗口。实现期须验证 market 与 owner 模块共用同一事务管理器;若不共用则降级为"先 consume 后写 + 失败补偿",并在执行记录声明(非默认路径)。
  2. token 不泄露:端口出参一律脱敏;明文 token 仅入参瞬时传递,market-server 内部即转 hash;owner DO 只存 handoffHash。
  3. agent 来源校验放宽(P2 最高风险):MuseAgentSlotServiceImpl:221-233 现强制 agentType∈{system,user}+ownerUserId 一致。放宽须精确:仅当 precheck 携带已 verify 通过的 market handoff token 时,才接纳 market 来源 agent;非 handoff 路径维持原校验。保护节点不可绑、AI 授权隔离(ArchUnit)不可破——放宽只开"合法 handoff 来源"这一道,不是普遍放开 scope。须加针对性单测(伪造来源/无 token 走 market 分支→拒)。
  4. 失败路径全覆盖(对齐流程-02B §13):token 过期→verify 返 blocked、落地页提示重新发起;重复消费→consume CAS updatedRows!=1→幂等返回或 ALREADY_CONSUMED;预检失败→不写事实、显示原因+返回点;用户取消→cancel(CAS expectedStatus)、0 写。
  5. fail-closed:verify 任何不确定(token 不存在/owner 不匹配/Market 端口异常)→ owner 一律阻断兑现,绝不伪造"已绑定"。

七、验证方法(per phase)

  • 后端:market verify/consume IT(真实 PG,正路 + 负路:伪造/过期/跨属主/已消费/已取消 token;consume CAS + envelope 幂等回放);各 owner 兑现侧 IT(verify 接入 + consume 核销 + 同事务回滚);BcBoundaryArchTest 0 违例。
  • 前端:vitest(HandoffStore 一次性/过期;落地路由 token 清除;各 hook 端点契约);tsc/eslint 绿。
  • 端到端(真后端 playwright,MSW-off):每 owner 一条正路闭环 + 一条负路(伪造/过期 token 被拒、0 写事实)。落库以真实表断言(knowledge_binding / agent_slot binding / content asset_use)。
  • 回归:全量 e2e 不低于基线(P0 起 47,逐阶段 +1~2)、vitest 不低于 82;02F 既有 bind-precheck 只读展示 + market-* 不破。

八、完成条件

  • 三 owner 各有一条真后端 e2e 闭环 + 负路(token 被拒)绿。
  • 红线可证:伪造/过期/跨属主 token 在目标 owner 服务端被拒(不再是"只存 hash 不验真")。
  • token 一次性可证:consume 后 GET /handoffs/{token}=completed,重放被幂等/拒绝。
  • BcBoundaryArchTest 0 违例;owner→market 仅经 market-api 端口。
  • 全量 e2e/vitest 无回归;总账回写各阶段。

九、回滚

  • 前端基座为新增模块,02F 发起按钮可 feature-gate;落地路由独立。
  • 后端 owner 兑现侧:knowledge 回验可回退到"只存 hash"(=现状,但红线不生效,属已知降级,发布决策须标注);agent 来源校验放宽可回退(market 来源兑现失效,回同空间流);content asset_use 为纯新增,可整段移除。
  • Market verify/consume 端口为纯增量(owner_precheck_created/completed 本是死状态),不影响现有发起侧 4 端点。
  • 回滚粒度 = 阶段(P0/P1/P2/P3 各自 commit),逐阶段可独立 revert。

十、执行版自审遗留 / 待拍板

  1. (待拍板·语义确认)consume 触发形态:用户拍板"显式 consume 端点供目标 owner 调用",本执行版据此定为 owner 域兑现事务内调 market-api consume 端口(非前端 HTTP consume,原子性更优,§六.1)。若你原意是前端 HTTP POST /handoffs/{token}/consume(前端在确认后单独调),请纠正——那将是非原子,需额外处理"consume 成功但 owner 写失败"补偿。
  2. (实现期验证)同事务前提:§六.1 的"market consume 与 owner 落库同事务"依赖单 DataSource;实现首步须验证,不成立则走补偿降级并声明。
  3. (P2 风险点)agent 来源校验放宽边界:§六.3——放宽须仅限"已 verify 的 market handoff 来源",且保护节点/AI 授权隔离不破。这是全工程风险最高改动,建议 P2 单独走一轮 review。
  4. (范围确认) 本版只做"市场→三 owner"方向;02C 来源发起、02G 记录跳转、各域"发布到市场"反向 handoff 留 P4(走同一基座 + 适配器)。
  5. (契约细节) verify/consume 的 DTO 字段、各 owner precheck 请求体新增字段,以 §四 列出的为骨架,实现期按真实 RespVO 对齐(不预写代码)。

下一步:确认 §十.1(consume 形态)后即按 P0→P1 实现;P2 前单独 review §六.3 的 agent 放宽。