# 跨空间 Handoff 统一接入 —— 执行版
> 版本:v0.1(执行版) · 日期:2026-06-22 · 目标读者:实现 agent · 类型:执行版(契约+数据流+分步+验收,不预写不可验证的代码细节)
> 上游:[handoff 跨空间统一接入评审版](2026-06-22-handoff跨空间统一接入-review.md)(已拍板:三 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。
---
## 三、总体架构与数据流
```mermaid
flowchart TD
subgraph FE[muse-studio 统一基座 P0]
HS["HandoffStore(Zustand)
session/precheckId/expiry/returnUrl/targetOwner
consumeToken/clearSession/isSessionValid"]
L["落地路由 /handoff/land
解析 ?token → replaceState 清 URL → 存 HS"]
end
subgraph SRC[来源空间 02F 市场]
S1[发起绑定] --> S2["POST /assets/id/bind-precheck
(已有,拿 authorizationSummaryId)"]
S2 --> S3["POST /handoffs
(拿 handoffToken + targetPage)"]
end
S3 -->|"跳 targetPage?token=..."| L
L --> HS
subgraph TGT[目标 owner 空间 P1/P2/P3]
T1["落地页消费 token → owner precheck
(请求体带 handoffToken)"] --> V
T2[用户确认] --> T3["owner 兑现落库(凭 precheckId)
同事务内核销 token"]
end
HS --> T1
HS --> T2
subgraph MKT[market-api 端口 P0 新增 · 进程内 Bean]
V["verify(token,owner,action)
属主+未过期+未消费+未取消
→ 脱敏摘要 / blockedReason"]
CN["consume(token,commandId,expectedStatus,ref)
CAS pending→completed + envelope 幂等"]
end
T1 -.owner-server 注入.-> V
T3 -.owner-server 注入.-> CN
T3 --> R["返回来源刷新
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=&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`(明文)、`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**)+ `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`;② `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 被拒。
---
## 六、边界与失败路径
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 放宽。