执行版按拍板(三 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>
18 KiB
跨空间 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(前置依赖)
- 后端单体单 PostgreSQL(单 DataSource)——owner 落库 + Market consume 同库事务原子的前提(见 §六.1)。须实现期确认 market 与各 owner 模块共用同一事务管理器。
- 真后端 e2e 环境(
VITE_API_MOCK=false+ muse-server 48080 +muse_slice_live种子),globalSetup 须补:已购 active license 的市场资产 + 可绑定目标(知识库/作品/agent)。 BcBoundaryArchTest当前绿(0 违例)——验收硬基线。- 现有全量回归基线: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,而是:
- 来源发起透传链: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}。 - 前端落地路由拼装:前端解析 targetPage 的 owner+query,导航到前端自己的落地路由
/handoff/land/{targetOwner},token 由持有者(前端)作为 query 附加:/handoff/land/knowledge?token=<handoffToken>&assetId=...&targetWorkId=...&returnUrl=...。即 token 进 URL 是前端导航时自己加的(前端持有 token),不是 targetPage 带来的。 - 落地即消费:落地页
replaceState立即清 URL 的 token(防刷新重复消费,前端-01 §9)→ 用 token 调 owner precheck(消费 token 换precheckId)→precheckId存 HandoffStore 作后续凭证(token 用完即弃)。刷新风险:落地后 URL token 已清、precheckId 在内存,HandoffStore 默认不持久化 → 刷新落地页需重新发起(实现期决定是否 sessionStorage 持久化 precheckId;不持久化时落地页须对"无 session"优雅提示重发,不可崩)。 - 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(明文)、expectedTargetOwner、expectedTargetAction |
valid、status、targetOwner、targetAction、assetId、targetWorkId、authorizationSummaryId、expiresAt、blockedReason |
只读:按 sha256(token) 查 → 属主(当前登录==ownerUserId)→ effectiveStatus 未过期/未消费/未取消 → owner/action 匹配。不回 token。失败返 valid=false+blockedReason(不抛,供 owner 决定降级 or 阻断) |
consume |
handoffToken、commandId、expectedStatus、目标事实引用(targetWorkId/bindingRef) |
consumed、status(=completed) |
写:CAS expectedStatus(pending/owner_precheck_created)→completed+completedAt;envelope 幂等(operationId=consumeHandoff,targetKey=sha256(token));属主隔离同 requireOwnedHandoff。updatedRows!=1→MARKET_HANDOFF_NOT_CONSUMABLE |
实现 MarketHandoffTokenApiImpl(放 market-server/.../api/handoff/,@Service):
- verify 复用
MuseMarketHandoffEventMapper.selectByTokenHash+effectiveStatus(现有,MarketHandoffServiceImpl:460-467)。 - consume 复用
updateLifecycleByExpectedStatus(现有 CAS mapper,:79-91,无需新 mapper)+MarketCommandServiceenvelope(照搬cancelHandoff结构MarketHandoffServiceImpl:196-219);方法须@Transactional(reserveCommand 是 MANDATORY 传播)。 - 新增错误码
MARKET_HANDOFF_NOT_CONSUMABLE/MARKET_HANDOFF_ALREADY_CONSUMED(market-apiErrorCodeConstants 段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;② AgentSlotPrecheckReqVO 加 handoffToken(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 被拒。
六、边界与失败路径
- 原子性(load-bearing):owner 兑现落库 + Market consume 须同 DB 事务(单体单 DataSource)。owner 兑现方法
@Transactional,内部调MarketHandoffTokenApi.consume(传播 REQUIRED,加入 owner 事务)→ owner 写失败则 consume 一并回滚,无"核销了但没绑定"窗口。实现期须验证 market 与 owner 模块共用同一事务管理器;若不共用则降级为"先 consume 后写 + 失败补偿",并在执行记录声明(非默认路径)。 - token 不泄露:端口出参一律脱敏;明文 token 仅入参瞬时传递,market-server 内部即转 hash;owner DO 只存 handoffHash。
- agent 来源校验放宽(P2 最高风险):
MuseAgentSlotServiceImpl:221-233现强制agentType∈{system,user}+ownerUserId 一致。放宽须精确:仅当 precheck 携带已 verify 通过的 market handoff token 时,才接纳 market 来源 agent;非 handoff 路径维持原校验。保护节点不可绑、AI 授权隔离(ArchUnit)不可破——放宽只开"合法 handoff 来源"这一道,不是普遍放开 scope。须加针对性单测(伪造来源/无 token 走 market 分支→拒)。 - 失败路径全覆盖(对齐流程-02B §13):token 过期→verify 返 blocked、落地页提示重新发起;重复消费→consume CAS
updatedRows!=1→幂等返回或ALREADY_CONSUMED;预检失败→不写事实、显示原因+返回点;用户取消→cancel(CAS expectedStatus)、0 写。 - fail-closed:verify 任何不确定(token 不存在/owner 不匹配/Market 端口异常)→ owner 一律阻断兑现,绝不伪造"已绑定"。
七、验证方法(per phase)
- 后端:market verify/consume IT(真实 PG,正路 + 负路:伪造/过期/跨属主/已消费/已取消 token;consume CAS + envelope 幂等回放);各 owner 兑现侧 IT(verify 接入 + consume 核销 + 同事务回滚);
BcBoundaryArchTest0 违例。 - 前端: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,重放被幂等/拒绝。 BcBoundaryArchTest0 违例;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。
十、执行版自审遗留 / 待拍板
- (待拍板·语义确认)consume 触发形态:用户拍板"显式 consume 端点供目标 owner 调用",本执行版据此定为 owner 域兑现事务内调
market-apiconsume 端口(非前端 HTTP consume,原子性更优,§六.1)。若你原意是前端 HTTPPOST /handoffs/{token}/consume(前端在确认后单独调),请纠正——那将是非原子,需额外处理"consume 成功但 owner 写失败"补偿。 - (实现期验证)同事务前提:§六.1 的"market consume 与 owner 落库同事务"依赖单 DataSource;实现首步须验证,不成立则走补偿降级并声明。
- (P2 风险点)agent 来源校验放宽边界:§六.3——放宽须仅限"已 verify 的 market handoff 来源",且保护节点/AI 授权隔离不破。这是全工程风险最高改动,建议 P2 单独走一轮 review。
- (范围确认) 本版只做"市场→三 owner"方向;02C 来源发起、02G 记录跳转、各域"发布到市场"反向 handoff 留 P4(走同一基座 + 适配器)。
- (契约细节) verify/consume 的 DTO 字段、各 owner precheck 请求体新增字段,以 §四 列出的为骨架,实现期按真实 RespVO 对齐(不预写代码)。
下一步:确认 §十.1(consume 形态)后即按 P0→P1 实现;P2 前单独 review §六.3 的 agent 放宽。