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

205 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 跨空间 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)<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`(明文)、`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 放宽。