diff --git a/docs/mvp/1.0.0-RC发布说明.md b/docs/mvp/1.0.0-RC发布说明.md index d676d718..229f0586 100644 --- a/docs/mvp/1.0.0-RC发布说明.md +++ b/docs/mvp/1.0.0-RC发布说明.md @@ -51,7 +51,7 @@ Muse 1.0.0 线 A 已具备最小可用闭环:作品写作、AI 真生成、候 - GraphRAG attribution live IT 未配置独立 opt-in,当前 honest skipped,不计为线 A passed。 - `market-install-kb-retrieval.spec.ts` D0-fork UI 物化用例仍 `test.fixme`,后端 D0-fork/KB handoff/召回已有真 PG 与 e2e 切片证据。 - Admin 未跑 Playwright e2e;本轮覆盖为 typecheck + Vitest + 后端真 PG。 -- Quality eval 执行器、完整导入/导出向导、高级版本 diff、偏好/通知、安全写操作、撤权独立入口后置线 B。 +- 完整导入向导已在 RC 后补到上传扫描→AI owner 解析→章节确认→Knowledge Draft 链路,并继续补齐 New-API LLM 全书深解析、docx/epub 抽取、`_test` real-PG V34+ 回归和浏览器 MSW-off 全链 e2e。偏好/通知、真实改密/2FA/session service 会话吊销仍后置线 B。Quality eval 本地确定性执行器、版本历史只读 diff、导出范围/凭证下载 UI 与真实 FileService 下载 e2e、账户安全事件处理动作、市场治理/撤权申诉独立入口已在 RC 后补;真实评估集管理/LLM judge、版本恢复/高级回滚仍后置。 - pay/report/bpm/mp 仍不纳入 1.0.0 线 A。 - Meta impact preview 的生产 owner usage contributor 仍要求 all-real;本轮 Meta lifecycle IT 的 `VERIFIED_ZERO` 只证明状态机和装配门。 - 当前 Node v26.3.0 超出 Admin package engines 推荐范围,但本轮 typecheck/Vitest 已通过;发布流水建议使用 package engines 声明的 Node 版本。 diff --git a/docs/mvp/1.0.0-交付计划.md b/docs/mvp/1.0.0-交付计划.md index ac29021b..58118857 100644 --- a/docs/mvp/1.0.0-交付计划.md +++ b/docs/mvp/1.0.0-交付计划.md @@ -24,13 +24,13 @@ | 域 | 完成度* | 最强(已真闭环) | 最痛(阻断点) | 盘点方 | |---|---|---|---|---| -| market | ~86% | 前台主干真IT+e2e双证、KB物化D0-fork、agent 物化、KB 召回触达 | 生产者 UI/副本回收/下架撤权细补偿后置 | claude深盘 | -| content | ~78% | 写作/自动保存/AI采纳真PG+MSW-off e2e;E1 已补采纳归档、改后合并入口、IndexedDB 草稿键、旧知识草稿失效、工作台入口与版本历史 | EditorPage 仍 demo 壳;完整导入/导出向导后置 | codex | +| market | ~86% | 前台主干真IT+e2e双证、KB物化D0-fork、agent 物化、KB 召回触达,RC 后补市场治理/撤权申诉独立入口 | 生产者 UI/副本回收/下架撤权细补偿后置 | claude深盘 | +| content | ~88% | 写作/自动保存/AI采纳真PG+MSW-off e2e;E1 已补采纳归档、改后合并入口、IndexedDB 草稿键、旧知识草稿失效、工作台入口、版本历史只读 diff、导出范围/凭证下载 UI;RC 后完整导入向导已补 New-API LLM 全书解析、docx/epub 抽取、real-PG V34+ 与浏览器 MSW-off 全链证据;导出下载已补真实 FileService 浏览器 e2e | EditorPage 仍 demo 壳;版本恢复/高级回滚后置 | codex | | knowledge | ~78% | KB物化、enable/disable真PG;E3 已补 Studio 检索/绑定/上传与 Source 消费真 e2e | 图谱/发布深面仍薄;召回触达已物化 KB 归 E4 | codex | -| ai | ~60% | 生成/SSE/采纳/槽位真e2e | Agent CRUD/版本/解绑缺、拒绝不打后端、评测断 | codex | -| admin | ~55% | 市场治理真IT、e2e真连PG行反查 | 权限治理P0双断层、AI配置前端全占位 | claude深盘 | -| member | ~50% | account读侧真IT扎实 | 写侧/扣减未闭环、用量无写源、pay禁用 | claude深盘 | -| meta | ~45% | planning垂直切片真e2e | 字段值校验未落、新增schema不通、API无真IT | codex | +| ai | ~80% | 生成/SSE/采纳/拒绝/Agent 生命周期/槽位 bind-unbind 真 e2e,RC 后补 Quality eval 本地执行器 | 真实评估集管理/LLM judge、grant/runtime 深层隔离后置 | codex | +| admin | ~75% | 市场治理真IT、system 管理员治理与 AI 配置写链路已接 | Admin Playwright 未跑;部分 AI 命令仍 preview-only | claude深盘 | +| member | ~70% | account 读侧真IT、AI 用量/配额写侧闭环、RC 后补安全事件处理动作入口 | pay 禁用;真实改密/2FA/session service、偏好通知后置 | claude深盘 | +| meta | ~82% | planning 垂直切片、字段值校验、新增 schema 草稿与生命周期真 PG | usage contributor 全真实建模与可视化 schema 编辑器后置 | codex | | 横切 | ~40% | infra文件真字节往返 | 统一Schema漂移、report/bpm/mp禁用、传播worker未落 | codex | *完成度为相对判断(供排序与定基准),非精确度量。 @@ -63,7 +63,7 @@ **核心闭环(必须真可上线)**:注册登录 → 建作品 → 写作/自动保存 → AI 生成 → 采纳(归档)→ 知识库(建/绑/检索)→ 市场(发现/安装/用)。 **1.0.0 做**:见第 5 节 E0–E7。 -**1.0.0 不做(后置线 B)**:pay/自助支付(用 admin 人工开通会员)、report 报表大屏、bpm 工作流、mp 公众号、安全写操作(改密/2FA/会话)、撤权独立入口、高级版本 diff、统一横切 Schema 重构、Source 全自治传播、可视化 schema 编辑器、偏好与通知页。 +**1.0.0 不做(后置线 B)**:pay/自助支付(用 admin 人工开通会员)、report 报表大屏、bpm 工作流、mp 公众号、真实安全账号能力(改密/2FA/session service 会话吊销;安全事件处理动作已在 RC 后补)、管理员侧下架/撤权细补偿、版本恢复/高级回滚(只读 diff 已在 RC 后补)、统一横切 Schema 重构、Source 全自治传播、可视化 schema 编辑器、偏好与通知页。 --- @@ -82,8 +82,8 @@ ### E1 · content 创作闭环补齐 - T1.1 ✅ Accept Suggestion 归档:AI owner 写 accepted decision,P1r IT 扩到校验 suggestion status 与 decision archive(2026-06-27 real-PG 切片 6/0F/0E/0S)。 - T1.2 ✅ 修改后合并前端入口已补(finalContent 编辑提交);旧知识草稿失效已由 Knowledge owner API 接管,Content 正文变更后将关联 pending draft 标为 `conflicted/needs_recheck`(2026-06-27 单测 47/0F/0E,real-PG `P1rContentMergeSuggestionIT` 5/0F/0E/0S)。 -- T1.3 ✅ 工作台壳已补知识/导入/导出/记录最小入口:知识跳转作品知识页,导入/导出创建后端任务,记录跳转账户中心(2026-06-27 前端本地门 12/0F/0E;MSW-off Playwright accept-suggestion 2/0F/0E)。 -- T1.4 ✅ IndexedDB `workId+blockId+revision` 草稿键对账已补;版本历史最小 API/UI 已补:保存/采纳写入 `muse_content_block_revision_snapshot`,只读 `GET /blocks/{blockId}/revisions` 展示 Canonical revision 快照(2026-06-27 real-PG `P1rContentCoreCompletedApprovalIT` 13/0F/0E/0S)。 +- T1.3 ✅ 工作台壳已补知识/导入/导出/记录入口:知识跳转作品知识页,导入可初始化本地正文,导出支持范围/格式选择、任务查询与下载凭证消费,记录跳转账户中心(2026-06-27 前端本地门 11/0F/0E;MSW-off Playwright accept-suggestion 2/0F/0E;2026-06-28 真后端 `export-download.spec.ts` 1/0F/0E)。 +- T1.4 ✅ IndexedDB `workId+blockId+revision` 草稿键对账已补;版本历史最小 API/UI 已补:保存/采纳写入 `muse_content_block_revision_snapshot`,只读 `GET /blocks/{blockId}/revisions` 展示 Canonical revision 快照,RC 后追加前端只读版本 diff 对比(2026-06-27 real-PG `P1rContentCoreCompletedApprovalIT` 13/0F/0E/0S;Studio 目标 Vitest `BlockRevisionPanel.test.tsx` 2/0F/0E)。 - ✔ done:写作→AI→采纳(候选离开 Active)→工作台各入口可用;2026-06-27 MSW-off Playwright 真后端旅程 `accept-suggestion.spec.ts` 2/0F/0E,正路真 New-API 生成 suggestionId=79,采纳后 Block revision 160→161,候选归档 `accepted`。 ### E2 · ai 智能体生命周期与闭环 @@ -91,8 +91,8 @@ - T2.2 ✅ Agent 版本读/切换/归档:Studio 版本面板接真实后端,支持 list/activate/archive 非当前版本;DB 核验 `current_version_id` 与版本 `status`(2026-06-27 `agent-create.spec.ts` 1/0F/0E)。 - T2.3 ✅ Slot 解绑:Studio 槽位面板接 `unbind`,真后端把 revision 2→3、status→`archived`、响应 `sourceStatus=unbound`(2026-06-27 `agent-slot-bind.spec.ts` 1/0F/0E)。 - T2.4 ✅ 候选拒绝接后端:Studio reject 调 `POST /suggestions/{id}/reject`,AI owner 写 `rejected` + decision archive(2026-06-27 `accept-suggestion.spec.ts` 新增 reject 真生成候选 suggestionId=81)。 -- T2.5 ✅ 范围收口:候选处置闭环已用真生成采纳/拒绝 e2e 覆盖;独立“候选列表页”不属于线 A 必要路径;质量评测执行器明确降级出 1.0.0 线 A,后置线 B(避免把 eval-run 入队半成品伪装成可用)。 -- ✔ done:agent 全生命周期可管;采纳/拒绝候选处置闭环真 e2e;质量评测执行器不计入线 A done。 +- T2.5 ✅ 范围收口:候选处置闭环已用真生成采纳/拒绝 e2e 覆盖;独立“候选列表页”不属于线 A 必要路径;RC 后已补质量评测本地确定性执行器,能把 evaluation queued job 推进到 completed/failed 并写 sample result/聚合摘要;真实评估集管理与 LLM judge 后置。 +- ✔ done:agent 全生命周期可管;采纳/拒绝候选处置闭环真 e2e;质量评测已具备最小执行闭环,深度 judge 不计入线 A done。 ### E3 · knowledge studio 与检索 - T3.1 ✅ Studio 检索触发 + 绑定/上传主流程 e2e:`KnowledgeRetrievalPanel` 接真实检索端点,绑定读回、market KB handoff、资料上传均由 MSW-off Playwright 真后端覆盖。 diff --git a/docs/mvp/进度总账.md b/docs/mvp/进度总账.md index 51d9af3e..8aa62fdd 100644 --- a/docs/mvp/进度总账.md +++ b/docs/mvp/进度总账.md @@ -23,9 +23,9 @@ **round-2 加固(2026-06-14,据 Opus 评审;2026-06-27 E0 续固)**:CI 触发分支 master→main(此前 CI 从不运行)、BC 门通用化 + 整改 knowledge 违例、loop 装机械牙、覆盖门去魔法数、文档诚实化、openapi-diff materialize 为独立 workflow。E0 将 Maven CI 移到仓库根 `.github/workflows/maven.yml`,并用 `run-p1r-verification.sh` + [真验证清单](1.0.0-真验证清单.md) 区分 local-ci / real-PG / external-live / mock-only / skipped。E0 fresh 证据:local 61/0F/0E/0S,根 CI 等价 `mvn -B package --file pom.xml` 3060/0F/0E/148S,real-PG 189/0F/0E/0S,external-live server 8/0F/0E/0S + module 4/0F/0E/1S(GraphRAG attribution 未配置跳过)。 -**E1 content 创作闭环切片(2026-06-27)**:已补 AI suggestion 采纳归档、前端“改后合并”入口、IndexedDB 草稿键对账、旧知识草稿失效、工作台知识/导入/导出/记录最小入口、Block 版本历史最小 API/UI。Content merge 写 Canonical 与来源归因后,必须由 AI owner 写 `accepted` 状态、accepted decision archive、AI command、business audit;AI owner 不可用时整笔 merge 回滚,避免 Canonical 已写但候选仍 pending。Content 正文变更后通过 Knowledge owner API 将关联 pending draft 标为 `conflicted/needs_recheck` 并写 Knowledge draft decision archive;通知失败不回滚 Canonical 主写,Knowledge confirm 端仍按来源状态 fail-closed。Studio `CandidatePanel` 支持编辑最终正文并按 `accept_as_is`/`modify_then_merge` 提交,IndexedDB 草稿键改为 `workId+blockId+revision` 防跨作品/版本污染。`saveBlock`/`mergeBlockSuggestion` 现在写 `muse_content_block_revision_snapshot`,工作台“历史”Tab 只读展示 Canonical revision 快照;导入/导出入口创建真实任务,知识/记录入口跳转对应工作台。fresh 证据:后端局部单测 `ContentSourceServiceTest` 25/0F/0E、`ContentAppServiceTest` 19/0F/0E、`MuseKnowledgeDraftInvalidationServiceTest` 3/0F/0E、`AiSuggestionMergeProjectionFacadeTest` 7/0F/0E;契约/覆盖门 `ContractFirstGateTest` 4/0F/0E + `P1rApiCoverageReportTest` 8/0F/0E;real-PG `P1rContentCoreCompletedApprovalIT` 13/0F/0E/0S + `P1rContentMergeSuggestionIT` 5/0F/0E/0S + `P1rContentMergeGeneratedSuggestionIT` 1/0F/0E/0S;studio `tsc -b --force` 通过、lint 0 errors、Vitest 12/12,追加 `AIPanel.contract.test.tsx` + `sse.test.ts` 22/0F/0E。共享 PG 写入闸门批准后已补跑 MSW-off Playwright 真后端 `accept-suggestion.spec.ts` 2/0F/0E:正路真 New-API 生成 suggestionId=79、authz `rpe-local-*`、Block revision 160→161、AI owner accepted decision archive 非空;负路 stale revision 业务冲突。边界:`muse-studio/src/types/content.ts` 生成类型因 openapi-typescript 版本漂移未同步,hook 内暂维护 `BlockRevision` 最小类型;完整导入上传/导出下载 UI 后置。 +**E1 content 创作闭环切片(2026-06-27)**:已补 AI suggestion 采纳归档、前端“改后合并”入口、IndexedDB 草稿键对账、旧知识草稿失效、工作台知识/导入/导出/记录入口、Block 版本历史最小 API/UI。Content merge 写 Canonical 与来源归因后,必须由 AI owner 写 `accepted` 状态、accepted decision archive、AI command、business audit;AI owner 不可用时整笔 merge 回滚,避免 Canonical 已写但候选仍 pending。Content 正文变更后通过 Knowledge owner API 将关联 pending draft 标为 `conflicted/needs_recheck` 并写 Knowledge draft decision archive;通知失败不回滚 Canonical 主写,Knowledge confirm 端仍按来源状态 fail-closed。Studio `CandidatePanel` 支持编辑最终正文并按 `accept_as_is`/`modify_then_merge` 提交,IndexedDB 草稿键改为 `workId+blockId+revision` 防跨作品/版本污染。`saveBlock`/`mergeBlockSuggestion` 现在写 `muse_content_block_revision_snapshot`,工作台“历史”Tab 只读展示 Canonical revision 快照;导入可创建真实任务,导出支持范围/格式选择、任务查询与下载凭证消费,知识/记录入口跳转对应工作台。fresh 证据:后端局部单测 `ContentSourceServiceTest` 25/0F/0E、`ContentAppServiceTest` 19/0F/0E、`MuseKnowledgeDraftInvalidationServiceTest` 3/0F/0E、`AiSuggestionMergeProjectionFacadeTest` 7/0F/0E;契约/覆盖门 `ContractFirstGateTest` 4/0F/0E + `P1rApiCoverageReportTest` 8/0F/0E;real-PG `P1rContentCoreCompletedApprovalIT` 13/0F/0E/0S + `P1rContentMergeSuggestionIT` 5/0F/0E/0S + `P1rContentMergeGeneratedSuggestionIT` 1/0F/0E/0S;studio `tsc -b --force` 通过、lint 0 errors、Vitest 12/12,追加 `AIPanel.contract.test.tsx` + `sse.test.ts` 22/0F/0E;导出 UI 追加 `useWorks.test.tsx`+`ExportWorkModal.test.tsx` 11/0F/0E、目标 ESLint 0 errors。共享 PG 写入闸门批准后已补跑 MSW-off Playwright 真后端 `accept-suggestion.spec.ts` 2/0F/0E:正路真 New-API 生成 suggestionId=79、authz `rpe-local-*`、Block revision 160→161、AI owner accepted decision archive 非空;负路 stale revision 业务冲突。边界:`muse-studio/src/types/content.ts` 生成类型因 openapi-typescript 版本漂移未同步,hook 内暂维护 `BlockRevision` 最小类型;完整导入向导已在 RC 后补,见下方记录;真后端导出下载 e2e 已在 2026-06-28 补跑通过,FileApi 异常仍按后端合同 fail-closed。 -**E2 ai 智能体生命周期与候选处置闭环(2026-06-27)**:已补用户自建 Agent update/archive、初始版本自动创建、版本列表/激活/归档非当前版本、作品槽位 unbind、Studio reject 调后端 AI owner 决策归档。Agent update 会创建下一 active version 并更新 `current_version_id`,archive Agent 同步归档仍 active 的槽位绑定;slot unbind 将绑定行 revision+1 且 `status=archived`,运行时回到默认能力;WorkspacePage “放弃修改”不再只清本地候选,而是真打 `POST /suggestions/{id}/reject` 写 `rejected` 与 decision archive。启动修红:最新单体启动时暴露 `muse.codegen.importEnable` 缺省,已在 `muse-server/src/main/resources/application.yaml` 补 `import-enable:false`,随后 48080 成功启动并 `GET /app-api/muse/agents` smoke 返回 `code=0`。fresh 证据:后端 AI targeted `MuseAgentServiceTest`+`MuseAgentSlotServiceTest`+Controller annotation 63/0F/0E;契约门 `ContractFirstGateTest` 4/0F/0E;studio `tsc -b --force` 通过、目标 ESLint 0 errors、Vitest 29/0F/0E;MSW-off Playwright 真后端 `agent-create.spec.ts`+`agent-slot-bind.spec.ts`+`accept-suggestion.spec.ts` 5/0F/0E:采纳真生成 suggestionId=80、Block revision 161→162、accepted decision archive 非空;拒绝真生成 suggestionId=81、`status=rejected`、decision archive 非空;Agent 生命周期 DB 核验 v1/v2 current 切换与 v2 archived;Slot unbind DB 核验 revision 2→3、status archived、响应 `sourceStatus=unbound`。边界:质量评测执行器未落地,明确降级出 1.0.0 线 A、后置线 B;本轮未跑 studio 全量 e2e。 +**E2 ai 智能体生命周期与候选处置闭环(2026-06-27)**:已补用户自建 Agent update/archive、初始版本自动创建、版本列表/激活/归档非当前版本、作品槽位 unbind、Studio reject 调后端 AI owner 决策归档。Agent update 会创建下一 active version 并更新 `current_version_id`,archive Agent 同步归档仍 active 的槽位绑定;slot unbind 将绑定行 revision+1 且 `status=archived`,运行时回到默认能力;WorkspacePage “放弃修改”不再只清本地候选,而是真打 `POST /suggestions/{id}/reject` 写 `rejected` 与 decision archive。启动修红:最新单体启动时暴露 `muse.codegen.importEnable` 缺省,已在 `muse-server/src/main/resources/application.yaml` 补 `import-enable:false`,随后 48080 成功启动并 `GET /app-api/muse/agents` smoke 返回 `code=0`。fresh 证据:后端 AI targeted `MuseAgentServiceTest`+`MuseAgentSlotServiceTest`+Controller annotation 63/0F/0E;契约门 `ContractFirstGateTest` 4/0F/0E;studio `tsc -b --force` 通过、目标 ESLint 0 errors、Vitest 29/0F/0E;MSW-off Playwright 真后端 `agent-create.spec.ts`+`agent-slot-bind.spec.ts`+`accept-suggestion.spec.ts` 5/0F/0E:采纳真生成 suggestionId=80、Block revision 161→162、accepted decision archive 非空;拒绝真生成 suggestionId=81、`status=rejected`、decision archive 非空;Agent 生命周期 DB 核验 v1/v2 current 切换与 v2 archived;Slot unbind DB 核验 revision 2→3、status archived、响应 `sourceStatus=unbound`。边界:本轮未跑 studio 全量 e2e。 **E3 knowledge studio 与检索闭环(2026-06-27)**:已补 Studio 主动检索面板、no_dataset 可见治理提示、自建知识库上传真实后端读回、market KB handoff 正负路、AI 生成消费 Knowledge Source 的反查证据。新增 V33 迁移允许 `muse_knowledge_bind_precheck.kb_id` 在 market_kb 预检阶段为空,修正“本地 installed_ref KB 尚未物化但预检先落库”的 schema 漂移;前端 `useKnowledge` 归一真实后端 numeric `documentId/kbId`,修复上传后列表被类型守卫过滤。fresh 证据:后端 knowledge targeted 25/0F/0E/0S;契约/迁移门 `P1rKnowledgeMigrationSqlTest`+`ContractFirstGateTest`+`P1rApiCoverageReportTest` 21/0F/0E/0S,coverage JSON 当前 242/242;studio knowledge Vitest 19/0F/0E、目标 ESLint 0 errors、`tsc -b --force` 通过;MSW-off Playwright 真后端 `knowledge-retrieval.spec.ts`+`knowledge-bindings.spec.ts`+`handoff-knowledge.spec.ts`+`market-handoff-precheck.spec.ts` 6/0F/0E;`ai-generation.spec.ts` 1/0F/0E,真 New-API 生成 suggestionId=82,并反查 `source_summary.contextAssembly` 中 `chunkCount>0`、`retrievedKbIds`、`authorizationSnapshotIds` 非空。边界:知识图谱/发布深面仍后置;已物化 KB 的召回触达归 E4。 @@ -41,7 +41,17 @@ **RC 基线记录(2026-06-27 11:05 PDT)**:`v1.0.0-rc.1` 指向 `3585219 feat(mvp): 收束1.0.0线A交付闭环`,分支 `dev/1.0.0` 已推送到 `origin/dev/1.0.0`。**mini-desktop 部署已完成(2026-06-27 11:28 PDT)**:实际入口 `root@100.64.0.7`,部署目录 `/opt/muse-rc1`,端口后端 `48081` / Studio `4175` / Admin `4176`,保留既有 `48080/4173/4174` 其它项目进程。远端补齐 OpenJDK 21 与本机 loopback Redis;后端业务 smoke `GET /app-api/muse/agents|works|knowledge-bases` 均 `code=0`,Studio/Admin HTML 均 HTTP 200,经 `4175/app-api/muse/works` 与 `4176/admin-api/muse/ai/agents` 反代均 `code=0`。部署细节与边界见 [1.0.0-RC发布说明 §4](1.0.0-RC发布说明.md#4-mini-desktop-部署)。 -**RC 后补功能:本地文本导入初始化正文(2026-06-27)**:已补 `POST /app-api/muse/works/{workId}/import-tasks` 的 inline text 模式,支持 Studio 选择 `.txt/.md` 或粘贴文本,后端按 Markdown/CN/EN 章节标题保守拆章,并在同一事务内创建 `muse_content_import_task`、`muse_content_chapter`、`muse_content_block`、Block 来源归因、Content Events outbox、`muse_content_block_revision_snapshot`,同时更新 work `import_status/chapter_count/word_count/revision`。外部上传模式仍要求 `uploadUrl` 并继续走 FileService 扫描 fail-closed;AI 全书解析 Shadow/章节确认向导仍后置,不把解析结果直接写 Canonical。fresh 证据:后端 `ContentImportParseServiceTest`+`AppContentImportParseControllerTest` 22/0F/0E,契约/覆盖门 `ContractFirstGateTest`+`P1rApiCoverageReportTest` 12/0F/0E,真实 PG `_test` 库 `P1rContentImportTaskCompletedApprovalIT` 4/0F/0E/0S(已确认需在 surefire forked JVM 显式清 `socksProxyHost/socksProxyPort` 并透传 `p1r.flyway.*`,此前 EOF 为 forked JVM 代理残留导致的假红);Studio `useWorks.test.tsx` 9/0F/0E、`tsc -b --force` 通过、目标 ESLint 0 errors。边界:外部上传扫描成功链路、AI 全书解析 Shadow/章节确认向导、导出下载凭证/范围选择 UI 后置。 +**RC 后补功能:本地文本导入初始化正文(2026-06-27)**:已补 `POST /app-api/muse/works/{workId}/import-tasks` 的 inline text 模式,支持 Studio 选择 `.txt/.md` 或粘贴文本,后端按 Markdown/CN/EN 章节标题保守拆章,并在同一事务内创建 `muse_content_import_task`、`muse_content_chapter`、`muse_content_block`、Block 来源归因、Content Events outbox、`muse_content_block_revision_snapshot`,同时更新 work `import_status/chapter_count/word_count/revision`。fresh 证据:后端 `ContentImportParseServiceTest`+`AppContentImportParseControllerTest` 22/0F/0E,契约/覆盖门 `ContractFirstGateTest`+`P1rApiCoverageReportTest` 12/0F/0E,真实 PG `_test` 库 `P1rContentImportTaskCompletedApprovalIT` 4/0F/0E/0S(已确认需在 surefire forked JVM 显式清 `socksProxyHost/socksProxyPort` 并透传 `p1r.flyway.*`,此前 EOF 为 forked JVM 代理残留导致的假红);Studio `useWorks.test.tsx` 9/0F/0E、`tsc -b --force` 通过、目标 ESLint 0 errors。完整上传/解析/确认向导见下一条。 + +**RC 后补功能:完整导入向导后端 owner/parse 链路(2026-06-28)**:已补 Studio 完整导入向导与后端 owner 实现。Infra 新增 `POST /app-api/infra/file/upload-returning-path` 返回稳定存储 path;Content 外部上传任务只接受 infra 稳定 path 并通过 FileApi 读真字节扫描,非 passed fail-closed,不再接受任意外链 URL;AI 新增 import parse owner API 与 `muse_ai_parse_job` / `muse_ai_chapter_parse_result`,创建 parse job 后读取上传文件,支持 `.txt/.md/.markdown/.docx/.epub`,默认按 Markdown/CN/EN 标题确定性拆章;当 `parseConfig.mode=full_book` 或 `strategy=llm_full_book` 时调用 New-API 全书解析,401/403 fail-closed,408/429/5xx/连接超时可重试并带退避,provider 原文和凭据不落日志/证据。Knowledge 新增 draft creation owner API,章节确认只创建 pending Knowledge Draft,不直写 Local KB Canonical。Studio `ImportTextModal` 升级为上传扫描 → AI 全书解析 → 章节确认 → Knowledge Draft 的向导,同时保留快速文本导入,并支持失败可重试解析任务。fresh 证据:后端 targeted `RealContentFileFacadeTest`+`ContentImportParseServiceTest`+`MuseAiImportParseServiceTest`+`NewApiMuseAiImportLlmParserTest`+`MuseKnowledgeDraftCreationApiImplTest`+`AppFileControllerTest` 通过;AI New-API parser live `P1rImportLlmNewApiLiveAcceptanceIT` 显式 opt-in 1/0F/0E/0S;真实 PostgreSQL `_test` 库 `P1rContentImportWizardCompletedApprovalIT` 已 clean+migrate 到 V34+,docx 确定性链路默认跑 2 tests / 0F / 0E / 1S,New-API live LLM 完整链路显式 opt-in 1/0F/0E/0S;Studio targeted Vitest `useWorks.test.tsx`+`ExportWorkModal.test.tsx` 12/0F/0E、`tsc -b --force` 通过;追加浏览器 MSW-off Playwright 真后端 `import-wizard.spec.ts` 1/0F/0E(真实 48080 + 共享 `muse_slice_live` + New-API),DB 反查 parse job completed、2 条 confirmed Shadow、2 条 pending Knowledge Draft、Canonical entity 计数不变。边界:Knowledge Draft 进入 Local KB Canonical 仍需用户后续显式审核确认;FileApi/对象存储异常按合同 fail-closed,不伪造解析成功。 + +**RC 后补功能:作品导出范围选择与凭证下载 UI(2026-06-28)**:Studio 工作台导出从单按钮升级为导出弹窗,支持 TXT/DOCX/EPUB、全部/指定章节、元数据/规划开关;`useCreateExportTask` 按现有后端契约提交 `includeChapters/includeMetadata/includePlanning`,`useExportTask` 轮询未完成任务,`api.download` 统一携带 `X-API-Version`/tenant/Bearer 并解析 `application/octet-stream` 与 JSON 业务错误,完成态凭 `downloadCredentialId` 取回导出包。MSW 补 `GET /export-tasks/{taskId}` 与 `/downloads/{credentialId}` 二进制响应,防止单测只验证“任务创建”假绿。fresh 证据:Studio `./node_modules/.bin/tsc -b --force` 通过,目标 Vitest `useWorks.test.tsx`+`ExportWorkModal.test.tsx` 12/0F/0E,目标 ESLint 0 errors;追加浏览器 MSW-off Playwright 真后端 `export-download.spec.ts` 1/0F/0E,真实 48080 + 共享 `muse_slice_live` + 本地 FileService master,DB 反查导出任务 completed、`package_ref/credential_id` 非空,浏览器 `download` 事件读取到的 TXT 文件包含真实作品标题和正文片段。修复测试口径问题:前端代码已 `response.blob()` 消费下载响应后,测试不再二次读取同一 `Response.text()`,改为验证用户实际下载文件;清理 SQL 不再误限定 `infra_file.tenant_id=1`。边界:后端 FileApi/对象存储不可用时仍按合同 fail-closed,不伪造下载包。 + +**RC 后补功能:Block 版本历史只读 diff(2026-06-27)**:Studio 工作台历史面板从单版本快照列表升级为任意两版只读对比,默认展示最新版本与前一版本,支持选择基准/目标版本,按段落/句子拆分后用 LCS 计算新增、删除、不变行数,并保留原有快照列表与详情。该能力只消费既有 `GET /app-api/muse/works/{workId}/blocks/{blockId}/revisions`,不新增恢复/回滚写入口,避免绕过 Content Canonical 写入边界。fresh 证据:目标 Vitest `BlockRevisionPanel.test.tsx` 2/0F/0E;`BlockRevisionPanel.test.tsx`+`useWorks.test.tsx` 11/0F/0E;Studio `tsc -b --force` 通过;目标 ESLint 0 errors。边界:版本恢复、高级回滚、跨章节 diff 仍后置。 + +**RC 后补功能:Quality eval 最小执行器(2026-06-27)**:AI 质量评估不再停在 `evaluation` job 入队。新增 `MuseQualityEvaluationWorker` + `MuseQualityEvaluationExecutor`,按数据库 `FOR UPDATE SKIP LOCKED` 原子领取 queued evaluation job,默认启用;执行器只接受合成/脱敏 datasetReference(`dataset://`/`synthetic://`/`redacted://`/含 redacted/smoke),用质量策略维度做确定性本地评分,写入 `muse_ai_evaluation_sample_result` 样本结果、聚合 `metricSummary`,并把 `muse_ai_evaluation_run` / `muse_ai_job` 推进到 `completed` 或 `failed`。审计只记录 dataset hash/安全分类和聚合摘要,不记录私有正文、原始 dataset 引用、Prompt/Response 或 provider body;不调用外部 LLM。fresh 证据:AI target tests `MuseQualityEvaluationWorkerTest`+`MuseAiJobMapperTest`+`MuseQualityPolicyServiceTest` 18/0F/0E;`git diff --check` 通过。边界:真实评估集管理、外发校验凭证、LLM judge、评估结果对比/发布建议仍后置。 + +**RC 后补功能:账户安全事件处理与市场治理申诉独立入口(2026-06-27)**:Studio 个人中心安全事件不再只有单一“确认”,改为消费既有 `POST /app-api/muse/account/security-events/{eventId}/acknowledge` 契约内四类 action:`acknowledged/password_changed/session_revoked/false_positive`,写入时带 `commandId` 幂等键与用户 note,成功后展示后端 `riskSummary/nextSteps` 并刷新安全事件列表。Market 新增独立 `/market/appeals` 治理与申诉入口,复用真实 `GET /marketplace/appeals`、`POST /marketplace/appeals/{appealId}/supplements`、`POST /marketplace/appeals/{appealId}/withdraw`,侧边栏与市场页均可直达;该页只处理发布者/使用者面对下架、召回、撤权等治理结果后的申诉/补充/撤回,不暴露管理员执行撤权/召回/下架命令。fresh 证据:Studio target Vitest `BlockRevisionPanel.test.tsx`+`useAccount.test.tsx`+`useMarketPublish.test.tsx`+`MarketAppealCenter.test.tsx` 4 files / 11 tests passed;Studio `tsc -b --force` 通过;目标 ESLint 0 errors;`git diff --check` 通过。边界:偏好/通知页仍无 app 写契约,真实改密/2FA/session service 会话吊销仍后置;撤权执行仍在 admin 治理侧,普通用户侧只做治理结果申诉。 **market 写路径整改(2026-06-14,ultracode)**:member 暴露写端口 `MuseAccountRecordProjectionApi` + DTO(member-server 实现读写自有 DAL、tenantId 由实现侧从上下文注入防伪造、事务沿用调用方),market 5 类改消费端口、移除 member.dal 依赖 → `KNOWN_VIOLATION_EXEMPTIONS` 清空、BC 门全绿。**附带修复**:round-2 重构 `ContentKnowledgeWorkOwnerFacade` 时遗留的旧测试 `KnowledgeWorkOwnerFacadeTest`(仍断言旧 WorkMapper 行为)已删除,其装配守卫/兜底两用例并入 `ContentKnowledgeWorkOwnerFacadeTest`(5/0F)——此为 round-2 一处假绿(当时构建在平台时区用例处中止、未真正跑到 knowledge),现已补正。 diff --git a/docs/项目功能与进度总览.md b/docs/项目功能与进度总览.md index 9c3a62bf..838f2922 100644 --- a/docs/项目功能与进度总览.md +++ b/docs/项目功能与进度总览.md @@ -31,13 +31,13 @@ | 模块 | 完成度 | 后端 | 前端 studio | 前端 admin | 接口门(机械) | 关键风险/缺口 | |---|:---:|:---:|:---:|:---:|:---:|---| -| **AI 编排** (ai) | 78% | ✅ 真实 | ✅ 核心生命周期/运行时真 e2e | — | 48/48 | 质量评测执行器后置;grant/runtime 物理隔离仅最小 ArchUnit | -| **作品/正文** (content) | 89% | ✅ 真实 | ✅ 候选链/工作台入口/版本历史已接 | — | 52/52 | E1 黄金旅程 MSW-off Playwright 2/2;EditorPage 仍 demo 壳 | +| **AI 编排** (ai) | 80% | ✅ 真实 | ✅ 核心生命周期/运行时真 e2e | — | 48/48 | Quality eval 本地执行器已补;LLM judge 与 grant/runtime 物理隔离仍后置 | +| **作品/正文** (content) | 89% | ✅ 真实 | ✅ 候选链/工作台入口/版本历史只读 diff 已接 | — | 52/52 | E1 黄金旅程 MSW-off Playwright 2/2;EditorPage 仍 demo 壳 | | **知识库** (knowledge) | 82% | ✅ 真实 | 🟡 检索/绑定/上传/召回阻断已接真后端 | — | 67/67 | E3/E4 真 e2e 已覆盖核心;图谱/发布深面仍薄 | -| **市场** (market) | 86% | ✅ 真实 | 🟡 消费端+handoff 已真连,生产端缺 | ✅ 真实 | 32/32 | agent 物化/KB 召回触达已闭环;发布/申诉生产者 UI 缺 | +| **市场** (market) | 87% | ✅ 真实 | 🟡 消费端+handoff+治理申诉入口已真连,生产端仍薄 | ✅ 真实 | 32/32 | agent 物化/KB 召回触达已闭环;发布生产者深面仍缺 | | **元引擎/治理** (meta) | 82% | ✅ 真实 | 🟡 用户端引擎缺 | ✅ 真实 | 16/16 | **影响预览 Unavailable → 发布链运行期阻塞**;facade-api 契约未建 | | **事件/SSE** (events) | 88% | ✅ 真实 | 🟡 统一客户端未接 | — | 1/1 | `connectEventStream` 仅自测、无组件消费 | -| **账户/会员** (member) | 68% | ✅ 真实 | 🔴 ~21% UI | 🟡 受限桩 | 33/33 | 21 端点无 UI;New-API/FileService facade Unavailable | +| **账户/会员** (member) | 70% | ✅ 真实 | 🟡 安全事件处理/用量权益已接,深面仍缺 | 🟡 受限桩 | 33/33 | 偏好通知/真实改密/2FA/session service 仍缺;New-API/FileService facade Unavailable | | **平台底座** (gateway/system/infra…) | 80% | ✅ 真实 | ✅ 真实 | ✅ 真实 | — | 网关运行期转发未验(Nacos);pay/bpm/mp/report ⚪默认隐藏、0 覆盖 | | **整体** | **~80%** | **~82%** | **~40%** | 良好 | **242/242** | 前端用户端深面缺口 + 跨 BC 集成仍需 E5-E7 | @@ -59,7 +59,7 @@ | 影响预览(迁移影响范围) | 🔴 | 🟡 | facade 恒 Unavailable;admin 读侧显示 0 affected | | 系统级智能体与功能编排 | ✅ | 🟡 | 后端链路完整;部分 owner 计数硬编码 0 | | 全局知识库授权 | ✅ | 🟡 | 后端就绪 | -| 质量门控与效果观测 | 🟡 | 🟡 | 评估作业已起,LLM-judge 深度待明 | +| 质量门控与效果观测 | 🟡 | 🟡 | 评估作业已可本地执行并产出样本/聚合结果;LLM-judge 深度待明 | | 市场审核与治理(审核/下架/召回/申诉) | ✅ | ✅ | index.vue 1166 行生产级;IT 全绿 | | 用户/角色/权限/数据范围 | ✅ | ✅ | 继承 yudao,真实 | | New-API 网关用户与用量 | 🟡 | 🟡 | 桩待真 New-API provider | @@ -75,8 +75,9 @@ | **写作台·AI 候选生成 + 接受(原样/改后/丢弃)** | ✅ | ✅ | WorkspacePage 接通 `suggestion-merges` 与 `suggestions/{id}/reject`;E1 已补原样/改后合并、采纳归档与旧知识草稿失效,E2 已补拒绝归档,真实生成归 external-live 层复跑 | | 作品规划台(设定/大纲/世界/角色/文风) | ✅ | 🟡 | 后端就绪;FE 部分 | | 知识与一致性(草稿确认/修正/冲突检查) | ✅ | 🟡 | KnowledgeDraftPanel 已有确认/忽略/重验入口;E1 已补 Content 正文变更后旧草稿 `needs_recheck` | -| 导入解析(上传/按章确认→知识草稿) | ✅ | 🟡 | E1 已补工作台最小导入任务入口;完整上传/解析确认向导仍后置 | -| 导出交付(范围/格式/授权校验) | ✅ | 🟡 | E1 已补工作台 TXT 导出任务入口;下载凭证与范围选择 UI 后置 | +| 导入解析(上传/按章确认→知识草稿) | ✅ | ✅ | 完整上传扫描→AI owner 解析→章节确认→Knowledge Draft 向导已补;支持 txt/md、docx/epub 抽取与 New-API LLM 全书解析,浏览器 MSW-off 全链已真验 | +| 导出交付(范围/格式/授权校验) | ✅ | ✅ | 工作台已补格式/范围选择、任务查询与下载凭证消费;真后端 FileService 下载 e2e 已通过 | +| 版本历史与对比 | ✅ | ✅ | 只读历史快照 + 任意两版 diff 已接;恢复/回滚仍后置 | | 记录与用量 / 作品设置 | ✅ | 🟡 | E1 已补工作台记录入口;账户中心记录面仍按 member 进度推进 | ### 3. 智能体工作台(ai) — 后端 ✅ / 前端 ✅(核心生命周期) @@ -114,7 +115,7 @@ | 资产详情·许可条款·收藏 | ✅ | ✅ | 详情页全量许可 + 收藏(e2e market-asset-detail 1/1) | | 发布者控制面(草稿/提交/审核/上架) | ✅ | 🔴 | **草稿保存已通(e2e 8/8),其余生产者 UI 缺** | | Handoff 一次性令牌 | ✅ | ✅ | knowledge/agent/content 三类消费端均已真后端 e2e;E4 补 agent 物化与 KB 召回阻断 | -| 申诉(提交/补充/撤回/状态) | ✅ | 🔴 | 后端 IT 全绿;FE 无 | +| 申诉(提交/补充/撤回/状态) | ✅ | ✅ | 独立 `/market/appeals` 治理与申诉入口已接;管理员执行撤权/召回仍在 admin 侧 | | 市场审核治理(管理端) | ✅ | ✅(admin) | 见 §1 | ### 6. 个人中心(account/member) — 后端 ✅ / 前端 🔴(~21%) @@ -123,7 +124,7 @@ | 功能 | 后端 | 前端 studio | 备注 | |---|:---:|:---:|---| | 账户总览 | 🟡 | 🟡 | 部分诚实占位(emailVerified/mfaEnabled 硬编 false) | -| 个人资料 / 安全登录 / 偏好通知 | 🟡 | 🔴 | session 吊销未接 session service | +| 个人资料 / 安全登录 / 偏好通知 | 🟡 | 🟡 | 安全事件处理动作已接;偏好通知、真实改密/2FA/session service 会话吊销仍缺 | | Token 与用量总览 | ✅ | ✅ | `/account/usage` 真接 | | 权益与配额 | ✅ | ✅ | `/account/entitlements` 真接 | | 授权/购买/安装绑定/发布记录 | ✅ | 🟡 | 购买/授权/发布记录三件套已接(PersonalCenter 真接,e2e account-publish-records 1/1);安装绑定等其余记录仍缺 | diff --git a/muse-cloud/muse-module-content/.agent b/muse-cloud/muse-module-content/.agent index 076f586b..316e313c 100644 --- a/muse-cloud/muse-module-content/.agent +++ b/muse-cloud/muse-module-content/.agent @@ -7,4 +7,4 @@ - **out-of-scope**:知识入库(归 knowledge BC)、AI 生成(归 ai BC)。 - **现状**:只读评估 82%;后端齐全,契约高度一致。 - **2026-06-17 验收债**:批1 已补 work/chapter/block 生命周期 11 op 真实 PG 证据;批20-28 已补 planning candidate 5 + meta-projection 3 + style-check 2 + chapter-parse-result 决策 2 + parse-job 生命周期/创建 5 + import-task 2 + export-task 4 + 管理端任务列表 2 + mergeBlockSuggestion 1,均真实 PG 绿且证据已人工批准并翻 completed。批26 `P1rContentExportTaskCompletedApprovalIT` 补 export-task 簇 4 op(getExportTask/exportWork/createExportTask/downloadExportPackage)并修正 `ContentExportServiceImpl` public `exportWork/createExportTask` 事务边界,IT 已用 command_log 0 残留断言验证预占命令回滚。批27 扩展 `P1rContentAdminReadCompletedApprovalIT` 补 adminListImportTasks/adminListExportTasks:真实 `/admin-api` + method security + 租户行拦截器,读 `muse_content_import_task`/`muse_content_export_task` 真实行,覆盖分页/筛选/workTitle 回填/跨租户不泄露/RBAC/API version/no-write,并补齐老 harness 专属 `_test` 库自动创建。批28 扩展 `P1rContentParseJobLifecycleCompletedApprovalIT` 补 createParseJob 守卫/fail-closed/事务回滚证据,并修正 `P1rContentMergeSuggestionIT` 老 harness 专属 `_test` 库自动创建后复验 mergeBlockSuggestion 真实写 Canonical 正文链路。content/account/market needs_verification 验收债均已清零;coverage JSON completed 已按人工批准翻转,且 2026-06-19 已补 `testFiles` 证据锚点门禁。 -- **关键风险 / TODO**:Content 验收债已补齐真实 PG 证据;外部 owner(FileService/解析投影等)未接入的路径仍按合同 fail-closed,需要配置真实外部依赖或 owner 服务后才能从 fail-closed 升级为完整成功路径。**2026-06-25:AI suggestion 采纳已从 fail-closed 升级为真成功(ADR-020 方案 A,commit a16c596)**——此前真生成候选 `authorization_snapshot_id=null` 恒被 `mergeBlockSuggestion` 拒(1041001001),根因 authz 列 BIGINT 存不下字符串 runtime envelope `rpe-local-uuid`;V30 把 `muse_content_block_source_attribution`+`muse_ai_suggestion` authz 列 BIGINT→VARCHAR(128)、放开 `ContentSourceServiceImpl` 两道数值门(删 requireNumericAuthorizationSnapshot/parseAuthorizationSnapshotId/parseLongQuietly,字符串直落、外部 owner 非数值编号不再丢)、`BlockSourceAttributionDO.authorizationSnapshotId` 改 String。活体 merge code=0+revision 自增、IT `P1rContentMergeGeneratedSuggestion` 假绿(注入数值 9001)转真绿。详见总账 2026-06-25 / ADR-020。**2026-06-27:E1 采纳归档 + 旧知识草稿失效切片已补**——`mergeBlockSuggestion` 写 Canonical/来源归因后必须让 AI owner 归档 accepted decision;AI owner 不可用则整笔回滚,避免正文已写但 suggestion 仍 pending。Content 正文变更后经 Knowledge owner API 将关联 pending draft 标为 `conflicted/needs_recheck`,通知失败不回滚 Canonical 主写但 confirm 端 fail-closed。P1r 真 PG `P1rContentMergeSuggestionIT` 5/0F/0E/0S 已校验 suggestion `accepted`、decision archive、AI command/audit、Knowledge draft decision 与幂等 replay;`P1rContentMergeGeneratedSuggestionIT` 1/0F/0E/0S 回归生成候选采纳。**2026-06-27:E1 工作台入口 + 版本历史已补**——`saveBlock`/`mergeBlockSuggestion` 写 `muse_content_block_revision_snapshot`,`GET /app-api/muse/works/{workId}/blocks/{blockId}/revisions` 只读返回 Canonical revision 快照,Studio 工作台“历史”Tab 展示;工作台知识/导入/导出/记录最小入口已接。P1r 真 PG `P1rContentCoreCompletedApprovalIT` 13/0F/0E/0S 校验版本历史、owner/tenant 守卫和纯查询不写;前端 `tsc`/lint/Vitest 12/12 绿;MSW-off Playwright `accept-suggestion.spec.ts` 2/0F/0E 验证真 New-API suggestionId=79 采纳后 revision 160→161、AI owner accepted decision archive 非空。**2026-06-27:E7 Meta 动态字段校验已从 fail-closed 升级为真成功**——`RealContentMetaFacade.validateDynamicFields` 读取 Meta active projection 校验 required/type/enum/range/regex/deprecated/unknown field 并返回 routeSuggestions,`P1rContentMetaProjectionClusterCompletedApprovalIT` 4/0F/0E/0S 覆盖 no-schema fail-closed 与 active projection 成功规则命中且 0 Content 写。**2026-06-27:RC 后本地文本导入初始化正文已补**——`CreateImportTaskReqVO.contentText` 支持 txt/markdown inline 模式,后端保守拆章并同事务写 import task/chapter/block/source attribution/outbox/block revision snapshot/work 统计;Studio 导入弹窗可选 `.txt/.md` 或粘贴正文。验证:Content import/controller 单测 22/0F/0E、契约/覆盖门 12/0F/0E、Studio hook 9/0F/0E、tsc/目标 lint 绿;真实 PG `_test` 库 `P1rContentImportTaskCompletedApprovalIT` 4/0F/0E/0S 已绿(需在 surefire forked JVM 清 `socksProxyHost/socksProxyPort` 并透传 `p1r.flyway.*`,此前 JDBC EOF 是代理残留假红)。仍缺:外部上传扫描成功链路、AI 全书解析/章节确认向导、导出下载凭证/范围选择 UI 后置。 +- **关键风险 / TODO**:Content 验收债已补齐真实 PG 证据;外部 owner(FileService/解析投影等)未接入的路径仍按合同 fail-closed,需要配置真实外部依赖或 owner 服务后才能从 fail-closed 升级为完整成功路径。**2026-06-25:AI suggestion 采纳已从 fail-closed 升级为真成功(ADR-020 方案 A,commit a16c596)**——此前真生成候选 `authorization_snapshot_id=null` 恒被 `mergeBlockSuggestion` 拒(1041001001),根因 authz 列 BIGINT 存不下字符串 runtime envelope `rpe-local-uuid`;V30 把 `muse_content_block_source_attribution`+`muse_ai_suggestion` authz 列 BIGINT→VARCHAR(128)、放开 `ContentSourceServiceImpl` 两道数值门(删 requireNumericAuthorizationSnapshot/parseAuthorizationSnapshotId/parseLongQuietly,字符串直落、外部 owner 非数值编号不再丢)、`BlockSourceAttributionDO.authorizationSnapshotId` 改 String。活体 merge code=0+revision 自增、IT `P1rContentMergeGeneratedSuggestion` 假绿(注入数值 9001)转真绿。详见总账 2026-06-25 / ADR-020。**2026-06-27:E1 采纳归档 + 旧知识草稿失效切片已补**——`mergeBlockSuggestion` 写 Canonical/来源归因后必须让 AI owner 归档 accepted decision;AI owner 不可用则整笔回滚,避免正文已写但 suggestion 仍 pending。Content 正文变更后经 Knowledge owner API 将关联 pending draft 标为 `conflicted/needs_recheck`,通知失败不回滚 Canonical 主写但 confirm 端 fail-closed。P1r 真 PG `P1rContentMergeSuggestionIT` 5/0F/0E/0S 已校验 suggestion `accepted`、decision archive、AI command/audit、Knowledge draft decision 与幂等 replay;`P1rContentMergeGeneratedSuggestionIT` 1/0F/0E/0S 回归生成候选采纳。**2026-06-27:E1 工作台入口 + 版本历史已补**——`saveBlock`/`mergeBlockSuggestion` 写 `muse_content_block_revision_snapshot`,`GET /app-api/muse/works/{workId}/blocks/{blockId}/revisions` 只读返回 Canonical revision 快照,Studio 工作台“历史”Tab 展示;工作台知识/导入/导出/记录最小入口已接。P1r 真 PG `P1rContentCoreCompletedApprovalIT` 13/0F/0E/0S 校验版本历史、owner/tenant 守卫和纯查询不写;前端 `tsc`/lint/Vitest 12/12 绿;MSW-off Playwright `accept-suggestion.spec.ts` 2/0F/0E 验证真 New-API suggestionId=79 采纳后 revision 160→161、AI owner accepted decision archive 非空。**2026-06-27:E7 Meta 动态字段校验已从 fail-closed 升级为真成功**——`RealContentMetaFacade.validateDynamicFields` 读取 Meta active projection 校验 required/type/enum/range/regex/deprecated/unknown field 并返回 routeSuggestions,`P1rContentMetaProjectionClusterCompletedApprovalIT` 4/0F/0E/0S 覆盖 no-schema fail-closed 与 active projection 成功规则命中且 0 Content 写。**2026-06-27:RC 后本地文本导入初始化正文已补**——`CreateImportTaskReqVO.contentText` 支持 txt/markdown inline 模式,后端保守拆章并同事务写 import task/chapter/block/source attribution/outbox/block revision snapshot/work 统计;Studio 导入弹窗可选 `.txt/.md` 或粘贴正文。验证:Content import/controller 单测 22/0F/0E、契约/覆盖门 12/0F/0E、Studio hook 9/0F/0E、tsc/目标 lint 绿;真实 PG `_test` 库 `P1rContentImportTaskCompletedApprovalIT` 4/0F/0E/0S 已绿(需在 surefire forked JVM 清 `socksProxyHost/socksProxyPort` 并透传 `p1r.flyway.*`,此前 JDBC EOF 是代理残留假红)。**2026-06-27:RC 后作品导出范围选择与凭证下载 UI 已补**——Studio 导出弹窗支持 TXT/DOCX/EPUB、全部/指定章节、元数据/规划开关;hook 接 `createExportTask/getExportTask/downloads/{credentialId}`,`api.download` 解析 octet-stream 与 JSON 业务错误。验证:Studio `tsc -b --force`、目标 Vitest 12/0F/0E、目标 ESLint 0 errors;2026-06-28 已补 `export-download.spec.ts` MSW-off 真后端 FileService 下载 e2e 1/0F/0E,FileApi 不可用仍按后端合同 fail-closed。**2026-06-27:RC 后 Block 版本历史只读 diff 已补**——Studio 历史面板支持任意两版基准/目标选择,按段落/句子做 LCS diff 并展示新增/删除/不变统计;只读消费版本历史 API,不新增恢复/回滚写入口。验证:目标 Vitest 2/0F/0E、`tsc`、目标 ESLint 绿。**2026-06-28:RC 后完整导入向导已补**——Infra 上传返回稳定 path,Content 外部导入任务只接受 infra path 并用 FileApi 真字节扫描;AI owner 持久化 parse job 与章节 Shadow;Knowledge owner 在章节确认时只产 pending Draft,不写 Local KB Canonical;Studio 向导串起上传扫描→解析→章节确认。验证见总账 2026-06-28,含浏览器 MSW-off 导入全链 e2e。2026-06-28 已补 `export-download.spec.ts` MSW-off 真后端 FileService 下载 e2e 1/0F/0E。仍缺:版本恢复/高级回滚。 diff --git a/muse-cloud/muse-module-knowledge/.agent b/muse-cloud/muse-module-knowledge/.agent index c417ce19..b20f11d2 100644 --- a/muse-cloud/muse-module-knowledge/.agent +++ b/muse-cloud/muse-module-knowledge/.agent @@ -6,4 +6,4 @@ - **边界(不可违反)**:**进 Local KB 唯一入口=用户显式确认草稿或手动修正**;全书解析章节确认只产/推 Draft,不写正式知识;来源优先级 Local KB > 用户绑定 > 市场/全局;依赖 RAGFlow GraphRAG(不可用时 Unavailable 优雅降级);守 BC 边界不碰他域 `.dal`([bc-boundaries](../../.agents/rules/bc-boundaries.md))。 - **out-of-scope**:正文写入(归 content)。 - **现状**:只读评估 78%;后端最完整模块之一(67 端点对齐契约),RAGFlow 真 HTTP + SSRF 防护;installed-KB 删除记录回显 bug 已修(2026-06-19:`MuseKnowledgeBindingMapperTest` + `MuseInstalledKnowledgeBaseServiceTest` 10/10,删除时写 `deleted=true` 且列表排除 `binding_status=deleted`);impact preview 已按 `muse_knowledge_source_binding_projection.owner_user_id` 输出 installed/source-binding owner 审计计数(2026-06-19:`MuseKnowledgeBaseServiceTest` + `MuseKnowledgeSourceBindingProjectionMapperTest` 30/30);source binding projection 回填闭环已修(2026-06-19:绑定确认为权威回填点写读模型行、unbind 按 bindingId 作用域撤销不误伤同源跨作品投影、唯一键补 work_id 修跨作品复用误判冲突〔V24〕;`MuseKnowledgeBindingServiceTest` + `MuseKnowledgeSourceBindingProjectionMapperTest` 11/11、整套件 214/214 绿);**读回端到端坐实 + unbind 真软删修复(2026-06-19)**:嵌入式 DB(H2)往返证 bind 写投影→`selectActiveByWorkId` 读回该来源、unbind 后读回消失、同源他作品投影存活;并修 unbind 软删真 bug——`deleted` 是 `@TableLogic`,实体 `setDeleted(true)` 被 MP 普通 update 剥离成空操作,改 `setSql("deleted = true")` 才真软删(`MuseKnowledgeSourceBindingProjectionRoundTripTest` 2/2、整套件 216/216 绿;新增 knowledge 模块首套嵌入式 DB 测试基建)。**GET bindings 读端点已补(2026-06-19)**:`AppMuseKnowledgeBindingController` 加 `GET /muse/works/{workId}/knowledge-bindings`→`listKnowledgeBindings`(requireWorkOwner 防 IDOR + selectActiveByWorkId 读回投影,id→String;happy-path + 越权 fail-closed 单测,整套件 218/218 绿);**bindings 读回后端腿(写→读→端点)已闭环,仅余 FE hook**。**FE 读回面板已加(2026-06-19)**:studio `useKnowledgeBindings` hook + `KnowledgeBindingsPanel`(镜像 DraftPanel)接入 `KnowledgePage`,契约+组件测试经 vitest 验证(全 studio 50/50、tsc 干净);live playwright e2e 需全栈 app(env 受限)未跑。bindings 写→读→端点→FE 读回展示链(除 live e2e)全通。**live e2e 已写 ready 未跑(2026-06-19)**:`muse-studio/e2e/knowledge-bindings.spec.ts`(镜像 graph spec,`playwright --list` 编译有效);活体受阻=远端 PG 宿主 100.64.0.8 离线(不在 tailnet)、无本地 PG 兜底,PG 宿主恢复后按 [.agents/knowledge §六] 起栈可跑。 **文本内容安全扫描已实现 + 摄入 scanning 关卡打通(2026-06-23,commit ec7637d)**:`KnowledgeContentScanService`(同步纯文本字符扫描——L1 非法 UTF-8/null/危险 C0-C1 控制符/Bidi 覆盖/体量硬阻断,L2 NFC+去零宽+换行规整,L3 prompt 注入软标记不阻断;特殊字符判定全用十六进制码点避免源码字面不可见字符),`KnowledgeFileFacade.materialize` 接入(passed→materialized+规整内容入 RAG、blocked→隔离带 reasonCode、扫描异常→fail-closed),替换原恒 `scanBlocked` 占位;`MaterializedFile.scanBlocked` 加 reasonCode/message(连带改 P1r live IT 构造签名)。单测 ScanService 9 + Facade 接线 3 + DocumentService 17 + P-B RetrievalApiImpl 11 全绿。**端到端真验(48080 + muse_slice_live + RAGFlow/New-API env 配齐)**:上传纯文本 document 4 → `scan_status=passed` → 处理链推进 → RAGFlow dataset(5950e9ee)+document(595cb63e)创建成功,坐实 scan 关卡 + muse→RAGFlow 集成贯通(对比 document 2 旧 jar 卡 SCAN_SERVICE_UNAVAILABLE、document 3 RAGFlow 未配 CONFIG_MISSING)。**E3 studio 与检索闭环已完成(2026-06-27)**:新增主动检索 App 端点与 `KnowledgeRetrievalPanel`,no_dataset 显式治理提示,自建 KB 上传真实后端读回,market KB handoff 正负路真 e2e;AI 生成 e2e 反查 `source_summary.contextAssembly` 证实 Knowledge Source 被真实消费。fresh 证据:knowledge targeted 25/0F/0E/0S,契约/迁移门 21/0F/0E/0S,studio Vitest 19/0F/0E、ESLint/tsc 通过,MSW-off Playwright knowledge 6/0F/0E + ai-generation 1/0F/0E。 -- **关键风险 / TODO**:前端图谱/发布深面仍薄;Task4 剩余 owner 缺口已收窄为 **document owner count / export_task_owner** 仍不能伪造,需后续 owner 事实或外部服务接入;已物化 KB 的召回触达/检索停用已在 E4 闭环。**2026-06-27:E4 market KB 召回触达已补并真验**:Knowledge 新增 `KnowledgeMarketSourceStatusChangedConsumer` 消费 Market `MarketAssetSourceStatusChangedEvent`,将本地 installed_ref KB 的 source binding projection 更新为 `recalled/blocked`,检索来源状态门返回 `omittedSources` 而不再送入 RAGFlow;MSW-off Playwright `market-kb-recall.spec.ts` 1/0F/0E,后端单测 `MuseKnowledgeSourceEventServiceTest`+`KnowledgeMarketSourceStatusChangedConsumerTest` 9/0F/0E。副本资源回收/更细补偿仍后置。**2026-06-27:E1 旧知识草稿失效 owner API 已补**:`MuseKnowledgeDraftInvalidationApi` 由 knowledge-api 定义、knowledge-server owner 实现;Content 正文变更只传 work/chapter/block/revision/commandId,不跨 BC 传正文。Knowledge 将匹配的 pending draft 更新为 `conflicted` + `source_action_policy=needs_recheck`,并写 `muse_knowledge_draft_decision(decision_type=recheck)` 作为审计归档;invalid command 跳过并 warn,Content 通知失败不回滚 Canonical 主写,confirm 端仍按 `needs_recheck` fail-closed。验证:`MuseKnowledgeDraftInvalidationServiceTest` 3/0F/0E;`P1rContentMergeSuggestionIT` real-PG 5/0F/0E/0S 穿透验证采纳后旧 draft 失效与 replay 不重复。**parse 轮询 worker 已补齐并端到端真验(2026-06-23,commit 635045c)**:`MuseKnowledgeParseStatusPollWorker`(@Scheduled + 开关 `muse.knowledge.parse-poll-worker.enabled`,跨租户捞 parsing 任务轮询 RAGFlow 文档状态→run=done 推 version processingStatus=searchable、failed 标败、processing 保持、RAGFlow 不可达/异常不误判)+ `ProcessingTaskService.markRagflowParseCompleted/markRagflowParsePolling` + `Mapper.selectParsingForPoll`;worker 接线 7 测试 + 回归 40 全绿;启用开关后 document 4 自动 parsing→searchable(isSearchable=true),摄入链 **scan→RAGFlow→worker→searchable** 全程贯通(此前"RAGFlow backend 故障"系本机 HTTP 代理误诊,RAGFlow 实始终正常,真因=muse 缺 parse 轮询 worker)。 +- **关键风险 / TODO**:前端图谱/发布深面仍薄;Task4 剩余 owner 缺口已收窄为 **document owner count / export_task_owner** 仍不能伪造,需后续 owner 事实或外部服务接入;已物化 KB 的召回触达/检索停用已在 E4 闭环。**2026-06-27:E4 market KB 召回触达已补并真验**:Knowledge 新增 `KnowledgeMarketSourceStatusChangedConsumer` 消费 Market `MarketAssetSourceStatusChangedEvent`,将本地 installed_ref KB 的 source binding projection 更新为 `recalled/blocked`,检索来源状态门返回 `omittedSources` 而不再送入 RAGFlow;MSW-off Playwright `market-kb-recall.spec.ts` 1/0F/0E,后端单测 `MuseKnowledgeSourceEventServiceTest`+`KnowledgeMarketSourceStatusChangedConsumerTest` 9/0F/0E。副本资源回收/更细补偿仍后置。**2026-06-27:E1 旧知识草稿失效 owner API 已补**:`MuseKnowledgeDraftInvalidationApi` 由 knowledge-api 定义、knowledge-server owner 实现;Content 正文变更只传 work/chapter/block/revision/commandId,不跨 BC 传正文。Knowledge 将匹配的 pending draft 更新为 `conflicted` + `source_action_policy=needs_recheck`,并写 `muse_knowledge_draft_decision(decision_type=recheck)` 作为审计归档;invalid command 跳过并 warn,Content 通知失败不回滚 Canonical 主写,confirm 端仍按 `needs_recheck` fail-closed。验证:`MuseKnowledgeDraftInvalidationServiceTest` 3/0F/0E;`P1rContentMergeSuggestionIT` real-PG 5/0F/0E/0S 穿透验证采纳后旧 draft 失效与 replay 不重复。**2026-06-28:导入章节确认 Draft owner API 已补**:`MuseKnowledgeDraftCreationApi` 由 knowledge-api 定义、knowledge-server owner 实现;Content/AI 只传章节 Shadow 结果与 sourceRef,Knowledge 负责创建 pending draft 与幂等 command,不写 Local KB Canonical,仍需用户后续显式确认草稿才进入正式知识。验证:`MuseKnowledgeDraftCreationApiImplTest` 与完整导入链路 targeted 后端测试通过。**parse 轮询 worker 已补齐并端到端真验(2026-06-23,commit 635045c)**:`MuseKnowledgeParseStatusPollWorker`(@Scheduled + 开关 `muse.knowledge.parse-poll-worker.enabled`,跨租户捞 parsing 任务轮询 RAGFlow 文档状态→run=done 推 version processingStatus=searchable、failed 标败、processing 保持、RAGFlow 不可达/异常不误判)+ `ProcessingTaskService.markRagflowParseCompleted/markRagflowParsePolling` + `Mapper.selectParsingForPoll`;worker 接线 7 测试 + 回归 40 全绿;启用开关后 document 4 自动 parsing→searchable(isSearchable=true),摄入链 **scan→RAGFlow→worker→searchable** 全程贯通(此前"RAGFlow backend 故障"系本机 HTTP 代理误诊,RAGFlow 实始终正常,真因=muse 缺 parse 轮询 worker)。 diff --git a/muse-cloud/muse-module-market/.agent b/muse-cloud/muse-module-market/.agent index 6967cf82..c8a87545 100644 --- a/muse-cloud/muse-module-market/.agent +++ b/muse-cloud/muse-module-market/.agent @@ -6,4 +6,4 @@ - **边界(不可违反)**:**授权 ≠ 所有权转移**;市场**非**源事实 owner,目标事实回目标 owner;Work 资产仅只读且排除出 install(`INSTALLABLE_ASSET_TYPES=Set.of(agent,knowledge_base)`);守 BC 边界不碰他域 `.dal`([bc-boundaries](../../.agents/rules/bc-boundaries.md))。 - **out-of-scope**:完整电商支付结算 DRM(项目非目标);模板化/参考写入/AI 上下文受 Feature Gate 默认关闭。 - **现状**:只读评估 82%;核心不变式已被代码强制。**生产者飞轮已端到端打通并活体证(2026-06-15)**:发布草稿→检查→提交→上架状态可视化→申诉(提交/补充材料/撤回)→申诉态回显(`MarketPublish`+`market-publish.spec.ts` 8/8,见[总账](../../docs/mvp/进度总账.md) §五C)。本会话新增 3 处 app-api 契约:`PublishRecordItem.marketAssetId`(物化资产 id,解申诉 assetId 映射 gap)、`GET /marketplace/appeals`(我的申诉列表)、记录 `appealStatus` 回填(`selectLatestByAssetIdAndUser`)。**2026-06-16 补真实 PG 验收证据**:`P1rMarketPublishProducerCompletedApprovalIT` 覆盖发布生产侧 5 op,真实 PG 3/3 绿;`P1rMarketAdminAppealCompletedApprovalIT` 覆盖管理端申诉 3 op(adminListAppeals/adminGetAppeal/adminResolveAppeal),真实 PG 6/6 绿;`P1rMarketAdminPublishReviewCompletedApprovalIT` 覆盖管理端发布审核 3 op(adminListPublishRequests/adminApprovePublishRequest/adminRejectPublishRequest),真实 PG 6/6 绿;`P1rMarketProducerAppealCompletedApprovalIT` 覆盖生产者申诉写路 3 op(submitAppeal/supplementAppeal/withdrawAppeal,app 入口 + owner 隔离 + 脱敏 + 状态 CAS + 冲突回滚),真实 PG 7/7 绿(独立复跑)。**2026-06-17 续补**:`P1rMarketAdminAssetReadsCompletedApprovalIT` 覆盖管理端资产治理读 2 op(adminListMarketAssets/adminGetMarketAsset:tenant 隔离 + assetType/listingStatus(not_listed↔draft 归一)/publisherId 过滤、详情资产快照 + 授权/安装/绑定/受影响任务计数全 0(关联表空)、缺资产/跨租户→MARKET_ASSET_NOT_EXISTS、API version/RBAC(muse:market:asset:query)fail-closed、纯读 no-write),真实 PG 2/2 绿(独立复跑)。`P1rMarketGovernanceWriteCompletedApprovalIT` 覆盖管理端资产治理写 3 op(adminPreviewGovernanceImpact/adminDelistAsset/adminRecallAsset:影响预览→凭预览下架 listed→delisted 的 expectedStatus CAS→凭预览召回 listed→recalled + 目标 owner 传播 fail-closed 如实记录 recalled/recorded/TARGET_OWNER_UNAVAILABLE;命令幂等、合同枚举 @Pattern→BAD_REQUEST、缺预览/CAS 阻断/缺资产/API version/RBAC fail-closed 0 写),真实 PG 4/4 绿(独立复跑)。`P1rMarketplaceDiscoveryReadsCompletedApprovalIT` 覆盖 marketplace 发现读 3 op(listMarketplaceAssets/listMarketplaceRecommendations/getGovernanceImpact:App 可见性隔离 listed 公开/非公开仅 publisher 自见 + assetType 过滤、fallback 推荐 + 理由、治理影响读 publisher/受影响者门禁 非属主无关联→MARKET_RESOURCE_FORBIDDEN、缺资产/API version fail-closed、纯读 no-write),真实 PG 4/4 绿(独立复跑)。`P1rMarketLicenseInstallCompletedApprovalIT` 覆盖采纳链 2 op(purchaseAsset/installMarketplaceAsset:购买写授权快照+购买事实+命令并跨 BC 同步 purchase/license 两条 Account 投影、安装要求已购授权+仅 agent/kb 可安装+写 installed 记录、命令幂等、未上架/缺资产/授权类型不符/无授权/作品不可安装/API version fail-closed 0 写),真实 PG 3/3 绿(独立复跑);证据已人工批准并翻 completed。`P1rMarketHandoffClusterCompletedApprovalIT` 覆盖 handoff 簇 4 op(createBindPrecheck/createMarketplaceHandoff/getHandoffStatus/cancelHandoff:已购 active 授权快照→来源授权摘要(handoffReady)→一次性 handoff token(明文不入库、只存 sha256 hash,事件快照不含明文)→只读回显 pending→expectedStatus CAS 取消(pending→cancelled)的端到端链;bind/handoff 回放幂等、cancel 终态后再取消被取消性守卫拒绝且不双写;非法 version/缺资产/无授权(授权≠可达)/目标 owner 白名单外(LocalMarketTargetOwnerFacade 本地目标页 fail-closed)/跨属主 token 读取与取消(属主隔离 RESOURCE_FORBIDDEN)/取消 CAS 期望态不匹配(reserveCommand MANDATORY 预占随事务回滚)失败路径 0 写),真实 PG 3/3 绿(独立复跑);至此 market needs_verification 28 op 全补 dedicated 真实 PG 证据,证据已人工批准并翻 completed。 -- **关键风险 / TODO**:① **资产物化只在 admin 审核通过(`markListed`)发生**——submit 阶段 `publish_request.asset_id=draft.id` 占位,故未上架资产无 `muse_market_asset` 行,申诉(`requireAsset`)仅对已物化(曾上架)资产可达;② 申诉证据材料附件上传(`attachmentIds` 当前前端置空);③ 上架后下架/召回的生产者自助入口;④ **market 验收债已清零(2026-06-17)**:批15-19 补 14 op(资产治理读 2 + 治理写 3 + marketplace 发现读 3 + 采纳链 purchase/install 2 + handoff 簇 4)叠加本战役前 14 op(发布生产 5 + 管理端申诉 3 + 发布审核 3 + 生产者申诉 3)= market needs_verification 28 op 全有 dedicated 真实 PG 证据;**台账 completed flip 与覆盖门禁批准集合已按人工批准同步,2026-06-19 已补 `testFiles` 证据锚点门禁**。⑤ **2026-06-25:install 用户可见假绿修复(isAcquired/isInstalled enrich,commit 37e7a8d)**——`MarketAssetQueryServiceImpl` `toCard`/`userActions` 此前把 isAcquired/isInstalled/canInstall 裸硬编 false(install 后端真实已验、但读模型不回填用户真实态),致 studio 安装按钮(`assetType!=='work' && isAcquired && !isInstalled`)真后端永不渲染、批17 IT 把 false 断言为正确(假绿固化)。修=仿 `isFavorite` enrich isAcquired(active 授权)/isInstalled(installation)/isInstallable,**与 install 写端口 `MarketInstallServiceImpl` 严格同源**(`selectActiveByOwnerAndAsset` + `INSTALLABLE_ASSET_TYPES`,避免"显示可装但点了 MARKET_LICENSE_NOT_EXISTS"新假绿);批量加载避放大 isFavorite 既有 N+1;批17 IT 改三态;前端补 onError;UI e2e `market-install.spec` 连跑 2 次。活体真验 purchase→isAcquired=T→install→isInstalled=T(详总账 2026-06-25 / memory muse-market-install-isacquired-enrich)。**教训:读模型 enrich 必须与写端口校验同源,否则显示态与可操作态背离=新假绿。** ⑥ **2026-06-27:E4 agent 物化 + KB 召回触达已补并真验**:Market 召回 `recallAsset` 写 `source_status_event` 后发布 `MarketAssetSourceStatusChangedEvent`,目标 owner 自治消费;agent handoff 只签 market token/assetId,不把发布者 agent id 交给客户端。Studio 真 e2e 覆盖 handoff-agent、market-kb-recall、market-asset-detail、governance-impact、handoff-precheck 6/0F/0E。⑦ **2026-06-27:E6 restore 分支真 PG IT 已补**:`P1rMarketAdminAppealCompletedApprovalIT` 扩到 7/0F/0E/0S,覆盖申诉 `restored` 分支凭治理预览恢复资产、preview consumed、asset relisted、restore governance action、command/event 幂等与快照 `impactPreviewId`。仍缺:生产者端发布/申诉 UI、KB 副本资源回收、下架/撤权更细补偿。 +- **关键风险 / TODO**:① **资产物化只在 admin 审核通过(`markListed`)发生**——submit 阶段 `publish_request.asset_id=draft.id` 占位,故未上架资产无 `muse_market_asset` 行,申诉(`requireAsset`)仅对已物化(曾上架)资产可达;② 申诉证据材料附件上传(`attachmentIds` 当前前端置空);③ 上架后下架/召回的生产者自助入口;④ **market 验收债已清零(2026-06-17)**:批15-19 补 14 op(资产治理读 2 + 治理写 3 + marketplace 发现读 3 + 采纳链 purchase/install 2 + handoff 簇 4)叠加本战役前 14 op(发布生产 5 + 管理端申诉 3 + 发布审核 3 + 生产者申诉 3)= market needs_verification 28 op 全有 dedicated 真实 PG 证据;**台账 completed flip 与覆盖门禁批准集合已按人工批准同步,2026-06-19 已补 `testFiles` 证据锚点门禁**。⑤ **2026-06-25:install 用户可见假绿修复(isAcquired/isInstalled enrich,commit 37e7a8d)**——`MarketAssetQueryServiceImpl` `toCard`/`userActions` 此前把 isAcquired/isInstalled/canInstall 裸硬编 false(install 后端真实已验、但读模型不回填用户真实态),致 studio 安装按钮(`assetType!=='work' && isAcquired && !isInstalled`)真后端永不渲染、批17 IT 把 false 断言为正确(假绿固化)。修=仿 `isFavorite` enrich isAcquired(active 授权)/isInstalled(installation)/isInstallable,**与 install 写端口 `MarketInstallServiceImpl` 严格同源**(`selectActiveByOwnerAndAsset` + `INSTALLABLE_ASSET_TYPES`,避免"显示可装但点了 MARKET_LICENSE_NOT_EXISTS"新假绿);批量加载避放大 isFavorite 既有 N+1;批17 IT 改三态;前端补 onError;UI e2e `market-install.spec` 连跑 2 次。活体真验 purchase→isAcquired=T→install→isInstalled=T(详总账 2026-06-25 / memory muse-market-install-isacquired-enrich)。**教训:读模型 enrich 必须与写端口校验同源,否则显示态与可操作态背离=新假绿。** ⑥ **2026-06-27:E4 agent 物化 + KB 召回触达已补并真验**:Market 召回 `recallAsset` 写 `source_status_event` 后发布 `MarketAssetSourceStatusChangedEvent`,目标 owner 自治消费;agent handoff 只签 market token/assetId,不把发布者 agent id 交给客户端。Studio 真 e2e 覆盖 handoff-agent、market-kb-recall、market-asset-detail、governance-impact、handoff-precheck 6/0F/0E。⑦ **2026-06-27:E6 restore 分支真 PG IT 已补**:`P1rMarketAdminAppealCompletedApprovalIT` 扩到 7/0F/0E/0S,覆盖申诉 `restored` 分支凭治理预览恢复资产、preview consumed、asset relisted、restore governance action、command/event 幂等与快照 `impactPreviewId`。⑧ **2026-06-27 RC 后补治理/撤权申诉独立入口**:Studio 新增 `/market/appeals`,侧边栏“治理”和市场页可直达,消费 `GET /marketplace/appeals`、补充材料、撤回申诉真实 hook;该入口只处理发布者/使用者面对下架/召回/撤权结果后的申诉,不暴露管理员执行撤权/召回命令。验证:`useMarketPublish.test.tsx` 2/0F/0E、`MarketAppealCenter.test.tsx` 2/0F/0E、组合目标 Vitest 20/0F/0E、`tsc`/目标 ESLint 通过。仍缺:更完整生产者发布深面、申诉附件上传、KB 副本资源回收、下架/撤权更细补偿。 diff --git a/muse-cloud/muse-module-member/.agent b/muse-cloud/muse-module-member/.agent index 0735bc66..6cb3c2cc 100644 --- a/muse-cloud/muse-module-member/.agent +++ b/muse-cloud/muse-module-member/.agent @@ -7,4 +7,5 @@ - **out-of-scope**:他域事实写入。 - **现状**:只读评估 68%。**2026-06-15 活体订正**:单体内 `MarketAccountProjectionFacade` 由 market 的 `MarketAccountProjectionProvider` 真实提供,purchases/licenses/publish-records 三端可读投影,不是 `*_UNAVAILABLE` 缺口。**2026-06-16 补真实 PG 验收证据**:`P1rAccountMarketRecordsCompletedApprovalIT` 覆盖 appListPurchases/appListLicenses/appListPublishRecords,真实 PG 3/3 绿;`P1rAccountQuotaRequestCompletedApprovalIT` 覆盖 appCreateQuotaRequest/appGetQuotaRequest/adminCreateQuotaRequest(命令幂等+commandId 冲突回滚+owner/tenant 隔离+RBAC+类型白名单),真实 PG 5/5 绿(独立复跑);`P1rAccountUsageObservabilityCompletedApprovalIT` 覆盖 getAppUsage/appGetIntegrationCallByCorrelation/adminGetIntegrationCallByCorrelation(用量按 owner 聚合 token/pending/anomaly/byModel+跨用户隔离+period 白名单、外部调用 app owner 守卫/admin 无 owner 限制+RBAC、三 op 纯读 no-write、缺用户/not-found/API version/RBAC fail-closed),真实 PG 4/4 绿(独立复跑);`P1rAccountNewApiBindingClusterCompletedApprovalIT` 覆盖 getAppNewApiBinding/adminListNewApiBindings/appRecheckNewApiBinding(绑定摘要 owner 读 + active→bound 归一、admin 列表以账户用户为分页主表 + EXISTS/NOT EXISTS 状态筛选 + RBAC、两读 no-write、recheck 单体 New-API 不可用正确 fail-closed:命令预占回滚但失败调用 REQUIRES_NEW 独立落库),真实 PG 4/4 绿(独立复跑);`P1rAccountExportDownloadClusterCompletedApprovalIT` 覆盖 appCreateExportTask/appGetExportTask/appDownloadExport(FileService 未接入时非敏感导出落 queued 任务+命令+审计、不伪造 completed/凭证、敏感类型缺 step-up fail-closed、命令幂等;get owner 读 + 跨 owner not found + no-write;download 凭证门禁 + FileService 不可用正确 fail-closed:@Transactional 回滚一次性消费、不写审计),真实 PG 4/4 绿(独立复跑);`P1rAccountAdminReadsCompletedApprovalIT` 覆盖 adminListUsageRecords/adminGetCallAttributionJob(用量以账户用户为分页主表库内聚合 token/pending/failed/attributionStatus、调用归因 job 按 jobId 读 job+items 归一状态+分桶计数、RBAC+API version fail-closed、纯读 no-write),真实 PG 3/3 绿(独立复跑);`P1rAccountAdminWritesCompletedApprovalIT` 覆盖 adminCreateNewApiBinding/adminCreateCallAttributionJob(binding 单体 New-API 不可用正确 fail-closed:命令回滚+失败调用 REQUIRES_NEW 独立落库+binding 不写;attribution job 基于已存在 correlationId 调用建 queued job+命令幂等+callIds 白名单/expectedCallRevision/缺调用记录/RBAC/API version fail-closed),真实 PG 3/3 绿(独立复跑);`P1rAccountSecurityEventAckCompletedApprovalIT` 覆盖 appAcknowledgeSecurityEvent(owner 校验事件归属 + 追加表写处理轨迹 + 基础事件 acknowledged 置真 + 命令幂等 + 审计;非法 action/跨 owner NOT_FOUND/非数字 eventId/缺用户/API version fail-closed 且 0 写),真实 PG 2/2 绿(独立复跑);`P1rAccountAdminPurchaseRecordsCompletedApprovalIT` 覆盖 adminListPurchaseRecords(跨模块 base-package=cn.iocoder.muse.module + 单体内 market 的 `MarketAccountProjectionProvider` 真实判定 purchase 投影可用,读端只读 Account 自有投影表 muse_account_record_projection;无 userId→跨用户全量、userId 过滤→owner 限定 + source_status→admin status 归一 + 资产信息从 title/snapshot 回填、非法 status/API version/RBAC(muse:account:query) fail-closed、纯读 no-write,Account 投影写端口 stub 越界反假绿),真实 PG 2/2 绿(独立复跑);证据已人工批准并翻 completed。 - **E5 member 写侧闭环(2026-06-27)**:AI 生成入口通过 `MuseAccountUsageApi` 预占配额,成功后写 usage record 并累加 quota used,失败终态释放预占;`AccountQuotaRequestWorker` 消费 queued quota request,短事务 claim/terminal、New-API 管理口调用在事务外执行,成功写 completed、失败写 failed + integration unavailable;`AccountAttributionWorker` 消费 queued attribution job,按 usage correlation 写 attribution item 并终态化。`RealNewApiAccountFacade` 已接 `/api/user/search`、`/api/user/`、`/api/user/{id}` 管理口,默认配置 fail-closed,显式 live 验证通过。fresh 证据:targeted 单测 120/0F/0E;account real-PG 16/0F/0E/0S;New-API 管理口 live 1/0F/0E/0S;AI runtime live 3/0F/0E/0S 反查 usage/quota/attribution 全落库。边界:workers 默认 disabled,生产启用需显式配置。 +- **2026-06-27 RC 后补安全事件处理入口**:Studio 个人中心安全事件从单一“确认”扩为契约内 `acknowledged/password_changed/session_revoked/false_positive` 四类处理动作,写 `POST /account/security-events/{eventId}/acknowledge` 时带 commandId 和 note,成功后展示后端 `riskSummary/nextSteps` 并刷新列表。验证:Studio `useAccount.test.tsx` 5/0F/0E,组合目标 Vitest 20/0F/0E,`tsc -b --force` 与目标 ESLint 通过。边界:这不是完整改密/2FA/session service,会话吊销真实联动仍后置;偏好通知仍无 app 写契约,不伪造。 - **关键风险 / TODO**:account needs_verification 验收债**已清零**:末位 adminListPurchaseRecords 经 `P1rAccountAdminPurchaseRecordsCompletedApprovalIT`(跨模块 base-package=cn.iocoder.muse.module + import MarketAccountProjectionProvider)真实 PG 2/2 绿补证;批3(市场记录读)+ 批7-14 account needs_verification op 全部补真实 PG 证据、证据已人工批准并翻 completed。截至 2026-06-17,content/market/account 三域 needs_verification 验收债均已补齐真实 PG 证据,coverage JSON completed flip 与覆盖门禁批准集合已按人工批准同步,2026-06-19 已补 `testFiles` 证据锚点门禁。前端仍有深页未全覆盖真实后端。 diff --git a/muse-studio/src/api/mocks/handlers/account.ts b/muse-studio/src/api/mocks/handlers/account.ts index f6f3ef2a..da54c923 100644 --- a/muse-studio/src/api/mocks/handlers/account.ts +++ b/muse-studio/src/api/mocks/handlers/account.ts @@ -1,6 +1,16 @@ import { http, HttpResponse } from 'msw'; import type { AccountProfile, AppEntitlementDetail, AppUsageSummary, ProfileUpdateRequest } from '@/types/openapi'; +interface MockSecurityEvent { + eventId: string; + eventType: string; + severity: string; + occurredAt: string; + description: string; + acknowledged: boolean; + acknowledgedAction?: string; +} + let mockProfile: AccountProfile = { userId: 'u-20260524', nickname: '青瑟', @@ -81,6 +91,27 @@ const mockUsage: AppUsageSummary = { ], }; +const INITIAL_SECURITY_EVENTS: MockSecurityEvent[] = [ + { + eventId: 'sec-1', + eventType: 'login_anomaly', + severity: 'warning', + occurredAt: '2026-06-27T08:30:00Z', + description: '检测到一次异地登录风险,请确认是否本人操作。', + acknowledged: false, + }, + { + eventId: 'sec-2', + eventType: 'sensitive_export', + severity: 'critical', + occurredAt: '2026-06-27T09:00:00Z', + description: '导出凭证触发敏感来源校验。', + acknowledged: false, + }, +]; + +let mockSecurityEvents: MockSecurityEvent[] = structuredClone(INITIAL_SECURITY_EVENTS); + /** 重置个人中心 Mock 状态,便于后续测试隔离。 */ export function resetMockDb(): void { mockProfile = { @@ -89,6 +120,7 @@ export function resetMockDb(): void { publicPenName: '青瑟写作实验室', version: 3, }; + mockSecurityEvents = structuredClone(INITIAL_SECURITY_EVENTS); } export const accountHandlers = [ @@ -136,4 +168,56 @@ export const accountHandlers = [ data: { bindingStatus: 'unbound', boundAt: null, lastSyncAt: null, canRecheck: false }, }); }), + + // GET /app-api/muse/account/security-events — 当前账户安全事件摘要,敏感信息已在后端脱敏。 + http.get('/app-api/muse/account/security-events', () => { + return HttpResponse.json({ + code: 0, + msg: 'success', + data: { + total: mockSecurityEvents.length, + pageNo: 1, + pageSize: 50, + list: mockSecurityEvents.map((event) => ({ + eventId: event.eventId, + eventType: event.eventType, + severity: event.severity, + occurredAt: event.occurredAt, + description: event.description, + acknowledged: event.acknowledged, + })), + }, + }); + }), + + // POST /app-api/muse/account/security-events/:eventId/acknowledge — 安全事件处理动作,契约内四种 action 都走同一写链路。 + http.post('/app-api/muse/account/security-events/:eventId/acknowledge', async ({ params, request }) => { + const eventId = String(params.eventId); + const body = (await request.json()) as { action?: string; note?: string }; + const event = mockSecurityEvents.find((item) => item.eventId === eventId); + if (!event) { + return HttpResponse.json({ code: 40440, msg: '安全事件不存在', data: null }, { status: 404 }); + } + + event.acknowledged = true; + event.acknowledgedAction = body.action ?? 'acknowledged'; + return HttpResponse.json({ + code: 0, + msg: 'success', + data: { + eventId, + acknowledged: true, + action: event.acknowledgedAction, + acknowledgedAt: new Date().toISOString(), + riskSummary: + event.acknowledgedAction === 'session_revoked' + ? '已记录会话撤销处理;真实会话服务不可用时由后端返回后续建议。' + : '已记录安全事件处理动作。', + nextSteps: + event.acknowledgedAction === 'password_changed' + ? ['重新登录高风险设备', '观察后续异常事件'] + : ['继续观察账户安全状态'], + }, + }); + }), ]; diff --git a/muse-studio/src/api/mocks/handlers/market.ts b/muse-studio/src/api/mocks/handlers/market.ts index 37b19e2d..356d718e 100644 --- a/muse-studio/src/api/mocks/handlers/market.ts +++ b/muse-studio/src/api/mocks/handlers/market.ts @@ -3,6 +3,18 @@ import type { MarketAssetCard, MarketAssetDetail } from '@/types/openapi'; type AssetType = MarketAssetCard['assetType']; +interface MockAppeal { + appealId: string; + assetId: string; + appealType: string; + status: string; + reason: string | null; + resolution: string | null; + submittedAt: number | null; + canSupplement: boolean; + canWithdraw: boolean; +} + const INITIAL_ASSETS: MarketAssetDetail[] = [ { assetId: '20000000-0000-4000-8000-000000000001', @@ -108,11 +120,27 @@ const INITIAL_ASSETS: MarketAssetDetail[] = [ }, ]; +const INITIAL_APPEALS: MockAppeal[] = [ + { + appealId: 'appeal-revoke-1', + assetId: '20000000-0000-4000-8000-000000000002', + appealType: 'license_revocation', + status: 'supplementing', + reason: '授权撤权影响正在使用的智能体,请复核许可范围。', + resolution: null, + submittedAt: Date.parse('2026-06-27T10:00:00Z'), + canSupplement: true, + canWithdraw: true, + }, +]; + let mockAssets = structuredClone(INITIAL_ASSETS); +let mockAppeals = structuredClone(INITIAL_APPEALS); /** 重置市场 Mock 数据,保证后续测试互不污染。 */ export function resetMockDb(): void { mockAssets = structuredClone(INITIAL_ASSETS); + mockAppeals = structuredClone(INITIAL_APPEALS); } function toAssetCard(asset: MarketAssetDetail): MarketAssetCard { @@ -316,4 +344,91 @@ export const marketHandlers = [ }, }); }), + + // GET /app-api/muse/marketplace/my-publish-records — 发布者资产记录,含可申诉治理态。 + http.get('/app-api/muse/marketplace/my-publish-records', () => { + return HttpResponse.json({ + code: 0, + msg: 'success', + data: { + total: 1, + list: [ + { + assetId: 'source-agent-1', + marketAssetId: '20000000-0000-4000-8000-000000000002', + assetName: '悬疑伏笔回收智能体', + assetType: 'agent', + version: '2.0.1', + status: 'recalled', + nextAction: 'appeal', + governanceStatus: 'recalled', + appealStatus: null, + exposureRange: 'account_install', + occurredAt: '2026-06-27T10:00:00Z', + }, + ], + }, + }); + }), + + // GET /app-api/muse/marketplace/appeals — 我的治理申诉列表。 + http.get('/app-api/muse/marketplace/appeals', () => { + return HttpResponse.json({ code: 0, msg: 'success', data: mockAppeals }); + }), + + // POST /app-api/muse/marketplace/appeals/:appealId/withdraw — 带 expectedStatus 乐观锁撤回申诉。 + http.post('/app-api/muse/marketplace/appeals/:appealId/withdraw', async ({ params, request }) => { + const body = (await request.json()) as { expectedStatus?: string }; + const appeal = mockAppeals.find((item) => item.appealId === params.appealId); + if (!appeal) { + return HttpResponse.json({ code: 40441, msg: '申诉不存在', data: null }, { status: 404 }); + } + if (body.expectedStatus && body.expectedStatus !== appeal.status) { + return HttpResponse.json({ code: 40941, msg: '申诉状态已变化,请刷新后重试', data: null }, { status: 409 }); + } + appeal.status = 'closed'; + appeal.canSupplement = false; + appeal.canWithdraw = false; + appeal.resolution = '用户主动撤回'; + return HttpResponse.json({ code: 0, msg: 'success', data: true }); + }), + + // POST /app-api/muse/marketplace/appeals/:appealId/supplements — 补充申诉材料。 + http.post('/app-api/muse/marketplace/appeals/:appealId/supplements', async ({ params }) => { + const appeal = mockAppeals.find((item) => item.appealId === params.appealId); + if (!appeal) { + return HttpResponse.json({ code: 40441, msg: '申诉不存在', data: null }, { status: 404 }); + } + appeal.status = 'reviewing'; + appeal.canSupplement = false; + appeal.canWithdraw = true; + return HttpResponse.json({ code: 0, msg: 'success', data: true }); + }), + + // POST /app-api/muse/marketplace/appeals — 对下架/召回/撤权等治理结果提交申诉。通用创建路由必须放在具体子路由之后。 + http.post('/app-api/muse/marketplace/appeals', async ({ request }) => { + const body = (await request.json()) as { assetId?: string; appealType?: string; reason?: string }; + const appeal: MockAppeal = { + appealId: `appeal-${crypto.randomUUID().slice(0, 8)}`, + assetId: body.assetId ?? 'unknown', + appealType: body.appealType ?? 'usage_impact', + status: 'pending', + reason: body.reason ?? null, + resolution: null, + submittedAt: Date.now(), + canSupplement: false, + canWithdraw: true, + }; + mockAppeals = [appeal, ...mockAppeals]; + return HttpResponse.json({ + code: 0, + msg: 'success', + data: { + appealId: appeal.appealId, + status: appeal.status, + processingDeadline: '2026-07-01T00:00:00Z', + supplementEntryAvailable: appeal.canSupplement, + }, + }); + }), ]; diff --git a/muse-studio/src/app/routes/index.tsx b/muse-studio/src/app/routes/index.tsx index 7f756f7f..e3069ff1 100644 --- a/muse-studio/src/app/routes/index.tsx +++ b/muse-studio/src/app/routes/index.tsx @@ -8,6 +8,7 @@ import AgentPage from '@/pages/AgentPage'; import MarketPage from '@/pages/MarketPage'; import MarketAssetDetailPage from '@/pages/MarketAssetDetailPage'; import MarketPublishPage from '@/pages/MarketPublishPage'; +import MarketAppealsPage from '@/pages/MarketAppealsPage'; import AccountPage from '@/pages/AccountPage'; import HandoffLandingPage from '@/features/handoff/pages/HandoffLandingPage'; @@ -29,6 +30,7 @@ export const router = createBrowserRouter([ { path: 'market', element: }, { path: 'market/assets/:assetId', element: }, { path: 'market/publish', element: }, + { path: 'market/appeals', element: }, { path: 'account', element: }, { path: 'handoff/land/:targetOwner', element: }, ], diff --git a/muse-studio/src/components/layout/Sidebar.tsx b/muse-studio/src/components/layout/Sidebar.tsx index d6773baa..61f6c0de 100644 --- a/muse-studio/src/components/layout/Sidebar.tsx +++ b/muse-studio/src/components/layout/Sidebar.tsx @@ -15,6 +15,7 @@ const navItems: NavItem[] = [ { to: '/knowledge', label: '知识库', icon: '📚' }, { to: '/agents', label: '智能体', icon: '🤖' }, { to: '/market', label: '市场', icon: '🏪' }, + { to: '/market/appeals', label: '治理', icon: '⚖️' }, { to: '/account', label: '个人', icon: '👤' }, ]; diff --git a/muse-studio/src/features/account/components/PersonalCenter.tsx b/muse-studio/src/features/account/components/PersonalCenter.tsx index b0ffd04a..54c600f0 100644 --- a/muse-studio/src/features/account/components/PersonalCenter.tsx +++ b/muse-studio/src/features/account/components/PersonalCenter.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { + type SecurityEventAction, useAccountEntitlements, useAccountLicenses, useAccountNewApiBinding, @@ -20,6 +21,16 @@ function severityBadgeClass(severity: string): string { return 'bg-slate-100 text-slate-500'; } +const securityActionLabels: Record = { + acknowledged: '知晓', + password_changed: '已改密', + session_revoked: '撤销会话', + false_positive: '误报', +}; + +/** 安全事件处理动作按钮,严格限于后端契约允许的 action 枚举。 */ +const securityActions: SecurityEventAction[] = ['acknowledged', 'password_changed', 'session_revoked', 'false_positive']; + /** New-API 绑定状态 → 中文文案。 */ const newApiBindingLabels: Record = { bound: '已绑定', @@ -48,6 +59,7 @@ export default function PersonalCenter() { const updateProfile = useUpdateAccountProfile(); const [nickname, setNickname] = useState(''); const [publicPenName, setPublicPenName] = useState(''); + const [securityActionResult, setSecurityActionResult] = useState(null); const activeNickname = nickname || profile?.nickname || ''; const activePenName = publicPenName || profile?.publicPenName || ''; @@ -62,6 +74,23 @@ export default function PersonalCenter() { }); }; + const handleSecurityAction = (eventId: string, action: SecurityEventAction) => { + setSecurityActionResult(null); + acknowledgeEvent.mutate( + { + eventId, + action, + note: securityActionLabels[action], + }, + { + onSuccess: (result) => { + const nextSteps = result.nextSteps?.length ? ` 后续:${result.nextSteps.join('、')}` : ''; + setSecurityActionResult(`${securityActionLabels[action]}已记录。${result.riskSummary ?? ''}${nextSteps}`); + }, + } + ); + }; + return (
@@ -213,6 +242,16 @@ export default function PersonalCenter() {

安全事件

+ {securityActionResult && ( +

+ {securityActionResult} +

+ )} + {acknowledgeEvent.isError && ( +

+ 处理失败:{acknowledgeEvent.error.message} +

+ )} {(securityEvents?.list?.length ?? 0) === 0 ? (

暂无安全事件

) : ( @@ -233,16 +272,30 @@ export default function PersonalCenter() {
- {!evt.acknowledged && ( + {!evt.acknowledged && securityActions.map((action) => ( - )} + ))} { // 契约:命中账户发布记录端点(账户台账视角,异于 market my-publish-records)。 expect(urls[0]).toContain('/app-api/muse/account/publish-records'); }); + + it('处理安全事件应支持撤销会话动作并刷新列表', async () => { + const { result: eventsHook } = renderHook(() => useAccountSecurityEvents(), { wrapper }); + await waitFor(() => expect(eventsHook.current.isSuccess).toBe(true)); + expect(eventsHook.current.data?.list[0]?.acknowledged).toBe(false); + + const { result: acknowledgeHook } = renderHook(() => useAcknowledgeSecurityEvent(), { wrapper }); + acknowledgeHook.current.mutate({ + eventId: 'sec-1', + action: 'session_revoked', + note: '用户确认撤销相关会话', + }); + + await waitFor(() => expect(acknowledgeHook.current.isSuccess).toBe(true)); + expect(acknowledgeHook.current.data?.action).toBe('session_revoked'); + expect(acknowledgeHook.current.data?.riskSummary).toContain('会话撤销'); + + await waitFor(() => expect(eventsHook.current.data?.list[0]?.acknowledged).toBe(true)); + }); }); diff --git a/muse-studio/src/features/account/hooks/useAccount.ts b/muse-studio/src/features/account/hooks/useAccount.ts index b4861bfa..c900b39a 100644 --- a/muse-studio/src/features/account/hooks/useAccount.ts +++ b/muse-studio/src/features/account/hooks/useAccount.ts @@ -142,8 +142,8 @@ export interface AccountSecurityEvent { eventType: string; /** info | warning | critical(后端兜底枚举,未知收敛为 warning)。 */ severity: string; - /** LocalDateTime 序列化为 epoch 毫秒。 */ - occurredAt?: number; + /** 后端 OpenAPI 声明为 date-time 字符串,前端只做展示透传,不参与安全判断。 */ + occurredAt?: string; description?: string; acknowledged?: boolean; } @@ -163,23 +163,43 @@ export interface SecurityEventAckResult { eventId: string; acknowledged: boolean; action: string; - acknowledgedAt?: number; + acknowledgedAt?: string; riskSummary?: string; + nextSteps?: string[]; +} + +/** 安全事件处理动作,严格对齐 OpenAPI 的 SecurityEventAcknowledgeRequest.action 枚举。 */ +export type SecurityEventAction = 'acknowledged' | 'password_changed' | 'session_revoked' | 'false_positive'; + +/** 安全事件处理请求:eventId 来自服务端列表,action/note 由用户显式选择。 */ +export interface SecurityEventAckRequest { + eventId: string; + action?: SecurityEventAction; + note?: string; } /** - * 确认安全事件(写路,action=acknowledged“已知晓”)。 + * 处理安全事件(写路,默认 action=acknowledged)。 * 每次发新 commandId(后端按 commandId 幂等去重),成功后失活列表查询使状态翻为「已确认」。 */ export function useAcknowledgeSecurityEvent() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (eventId: string) => - api.post(`/account/security-events/${eventId}/acknowledge`, { + mutationFn: (request: string | SecurityEventAckRequest) => { + const normalized: SecurityEventAckRequest = + typeof request === 'string' ? { eventId: request, action: 'acknowledged' } : request; + const action = normalized.action ?? 'acknowledged'; + const trimmedNote = normalized.note?.trim(); + const body: { commandId: string; action: SecurityEventAction; note?: string } = { commandId: crypto.randomUUID(), - action: 'acknowledged', - }), + action, + }; + if (trimmedNote) { + body.note = trimmedNote; + } + return api.post(`/account/security-events/${normalized.eventId}/acknowledge`, body); + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['accountSecurityEvents'] }); }, diff --git a/muse-studio/src/features/editor/components/BlockRevisionPanel.test.tsx b/muse-studio/src/features/editor/components/BlockRevisionPanel.test.tsx new file mode 100644 index 00000000..09837355 --- /dev/null +++ b/muse-studio/src/features/editor/components/BlockRevisionPanel.test.tsx @@ -0,0 +1,75 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { api } from '@/api/client'; +import { resetMockDb } from '@/api/mocks/handlers/content'; +import BlockRevisionPanel from './BlockRevisionPanel'; +import React from 'react'; + +const renderWithClient = (ui: React.ReactElement) => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, gcTime: 0 }, mutations: { retry: false } }, + }); + return render({ui}); +}; + +const docJson = (text: string) => + JSON.stringify({ + type: 'doc', + content: text.split('\n').map((line) => ({ + type: 'paragraph', + content: [{ type: 'text', text: line }], + })), + }); + +async function saveRevision(commandId: string, expectedRevision: number, text: string) { + await api.put('/works/1/blocks/block-1-1-main', { + commandId, + expectedRevision, + content: docJson(text), + sourceSnapshot: { + sourceType: 'user_original', + sourceId: 'block-1-1-main', + contentRevision: expectedRevision, + }, + auditReason: 'vitest revision diff', + }); +} + +describe('BlockRevisionPanel 组件', () => { + beforeEach(() => resetMockDb()); + + it('应在两个版本之间展示新增和删除 diff', async () => { + await saveRevision('vitest-revision-diff-1', 1, '第一行保留\n第二行删除'); + await saveRevision('vitest-revision-diff-2', 2, '第一行保留\n第三行新增'); + + renderWithClient(); + + expect(await screen.findByText('版本对比')).toBeInTheDocument(); + expect(screen.getByText('新增 1')).toBeInTheDocument(); + expect(screen.getByText('删除 1')).toBeInTheDocument(); + expect(screen.getByText('不变 1')).toBeInTheDocument(); + const diffRegion = screen.getByRole('region', { name: '版本对比结果' }); + expect(within(diffRegion).getByText('第二行删除')).toBeInTheDocument(); + expect(within(diffRegion).getByText('第三行新增')).toBeInTheDocument(); + }); + + it('切换基准版本后应重新计算 diff 摘要', async () => { + await saveRevision('vitest-revision-diff-a', 1, '旧版本'); + await saveRevision('vitest-revision-diff-b', 2, '中间版本'); + await saveRevision('vitest-revision-diff-c', 3, '最新版本'); + + renderWithClient(); + + expect(await screen.findByText('版本对比')).toBeInTheDocument(); + const diffRegion = screen.getByRole('region', { name: '版本对比结果' }); + expect(within(diffRegion).getByText('中间版本')).toBeInTheDocument(); + expect(within(diffRegion).getByText('最新版本')).toBeInTheDocument(); + + fireEvent.change(screen.getByLabelText('基准版本'), { target: { value: '2' } }); + + await waitFor(() => expect(within(diffRegion).getByText('旧版本')).toBeInTheDocument()); + expect(within(diffRegion).getByText('最新版本')).toBeInTheDocument(); + expect(within(diffRegion).queryByText('中间版本')).not.toBeInTheDocument(); + }); +}); diff --git a/muse-studio/src/features/editor/components/BlockRevisionPanel.tsx b/muse-studio/src/features/editor/components/BlockRevisionPanel.tsx index 8ef28e1f..383845f6 100644 --- a/muse-studio/src/features/editor/components/BlockRevisionPanel.tsx +++ b/muse-studio/src/features/editor/components/BlockRevisionPanel.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from 'react'; import { useBlockRevisions } from '../hooks/useWorks'; +import type { BlockRevision } from '../hooks/useWorks'; interface BlockRevisionPanelProps { workId: string; @@ -8,25 +9,123 @@ interface BlockRevisionPanelProps { function previewText(content: string): string { try { - const parsed = JSON.parse(content) as { content?: Array<{ content?: Array<{ text?: string }> }> }; + const parsed = JSON.parse(content) as { content?: unknown[] }; const text = parsed.content - ?.flatMap((node) => node.content ?? []) - .map((node) => node.text ?? '') - .join(''); + ?.map((node) => collectText(node).trim()) + .filter(Boolean) + .join('\n'); return text && text.trim().length > 0 ? text : content; } catch { return content; } } +/** 递归提取 Tiptap 节点文本;段落分隔由 previewText 的顶层遍历负责保留。 */ +function collectText(node: unknown): string { + if (typeof node !== 'object' || node === null) return ''; + const current = node as { content?: unknown[]; text?: string }; + const ownText = typeof current.text === 'string' ? current.text : ''; + const childText = Array.isArray(current.content) ? current.content.map(collectText).join('') : ''; + return `${ownText}${childText}`; +} + +interface DiffLine { + key: string; + type: 'added' | 'removed' | 'unchanged'; + text: string; +} + +/** + * 把正文拆成对比单元:优先按自然换行,单行长文本再按中英文句末标点切分。 + * 这里仅做只读展示,不参与任何 Canonical 写入或版本恢复。 + */ +function splitDiffUnits(content: string): string[] { + const text = previewText(content).replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim(); + if (!text) return []; + const lines = text.split('\n').map((line) => line.trim()).filter(Boolean); + if (lines.length > 1) return lines; + return text + .split(/(?<=[。!?;!?;])\s*/) + .map((line) => line.trim()) + .filter(Boolean); +} + +/** + * 使用 LCS 生成稳定的逐行 diff,避免简单字符串包含导致的误判。 + * 版本历史正文通常很短,O(n*m) 足够且更容易审计。 + */ +function buildDiffLines(baseContent: string, targetContent: string): DiffLine[] { + const base = splitDiffUnits(baseContent); + const target = splitDiffUnits(targetContent); + const dp = Array.from({ length: base.length + 1 }, () => Array(target.length + 1).fill(0)); + + for (let i = base.length - 1; i >= 0; i -= 1) { + for (let j = target.length - 1; j >= 0; j -= 1) { + dp[i]![j] = base[i] === target[j] ? dp[i + 1]![j + 1]! + 1 : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!); + } + } + + const lines: DiffLine[] = []; + let i = 0; + let j = 0; + while (i < base.length && j < target.length) { + if (base[i] === target[j]) { + lines.push({ key: `u-${i}-${j}`, type: 'unchanged', text: base[i]! }); + i += 1; + j += 1; + } else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) { + lines.push({ key: `r-${i}-${j}`, type: 'removed', text: base[i]! }); + i += 1; + } else { + lines.push({ key: `a-${i}-${j}`, type: 'added', text: target[j]! }); + j += 1; + } + } + while (i < base.length) { + lines.push({ key: `r-${i}-${j}`, type: 'removed', text: base[i]! }); + i += 1; + } + while (j < target.length) { + lines.push({ key: `a-${i}-${j}`, type: 'added', text: target[j]! }); + j += 1; + } + return lines; +} + +function revisionLabel(revision: BlockRevision): string { + return `revision ${revision.revision}`; +} + /** Block 正文版本历史面板;只读展示,不提供恢复写入。 */ export default function BlockRevisionPanel({ workId, blockId }: BlockRevisionPanelProps) { const { data = [], isLoading, isError } = useBlockRevisions(workId, blockId); const [selectedRevision, setSelectedRevision] = useState(null); + const [baseRevision, setBaseRevision] = useState(null); + const [targetRevision, setTargetRevision] = useState(null); const selected = useMemo( () => data.find((revision) => revision.revision === selectedRevision) ?? data[0], [data, selectedRevision] ); + const target = useMemo( + () => data.find((revision) => revision.revision === targetRevision) ?? data[0], + [data, targetRevision] + ); + const base = useMemo( + () => data.find((revision) => revision.revision === baseRevision) ?? data[1] ?? data[0], + [data, baseRevision] + ); + const diffLines = useMemo( + () => (base && target ? buildDiffLines(base.content, target.content) : []), + [base, target] + ); + const diffSummary = useMemo( + () => ({ + added: diffLines.filter((line) => line.type === 'added').length, + removed: diffLines.filter((line) => line.type === 'removed').length, + unchanged: diffLines.filter((line) => line.type === 'unchanged').length, + }), + [diffLines] + ); if (!blockId) { return
请在左侧大纲选择章节,查看正文版本历史。
; @@ -47,6 +146,79 @@ export default function BlockRevisionPanel({ workId, blockId }: BlockRevisionPan

版本历史

仅展示已写入 Canonical 的正文快照

+ {base && target && ( +
+
+ + +
+
+ 新增 {diffSummary.added} + 删除 {diffSummary.removed} + 不变 {diffSummary.unchanged} +
+
+
+ 版本对比 +
+ {diffLines.length === 0 ? ( +
两个版本正文一致。
+ ) : ( +
+ {diffLines.map((line) => ( +
+ + {line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' '} + + {line.text} +
+ ))} +
+ )} +
+
+ )}
{data.map((revision) => ( diff --git a/muse-studio/src/features/market/components/MarketAppealCenter.test.tsx b/muse-studio/src/features/market/components/MarketAppealCenter.test.tsx new file mode 100644 index 00000000..f62c1a85 --- /dev/null +++ b/muse-studio/src/features/market/components/MarketAppealCenter.test.tsx @@ -0,0 +1,53 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { MemoryRouter } from 'react-router-dom'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { resetMockDb } from '@/api/mocks/handlers/market'; +import MarketAppealCenter from './MarketAppealCenter'; + +const createTestQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { retry: false, gcTime: 0 }, + mutations: { retry: false }, + }, + }); + +let testQueryClient = createTestQueryClient(); + +function renderPage() { + return render( + + + + + + ); +} + +describe('MarketAppealCenter', () => { + beforeEach(() => { + resetMockDb(); + testQueryClient = createTestQueryClient(); + }); + + it('应展示撤权申诉并允许补充材料', async () => { + renderPage(); + + expect(await screen.findByText('撤权申诉')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '补充材料' })); + fireEvent.change(screen.getByPlaceholderText(/补充说明/), { target: { value: '补充新的授权证明' } }); + fireEvent.click(screen.getByRole('button', { name: '提交补充' })); + + await waitFor(() => expect(screen.getByText(/材料已补充/)).toBeInTheDocument()); + }); + + it('应允许从独立入口撤回申诉', async () => { + renderPage(); + + expect(await screen.findByText('撤权申诉')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '撤回' })); + + await waitFor(() => expect(screen.getByText(/已撤回/)).toBeInTheDocument()); + }); +}); diff --git a/muse-studio/src/features/market/components/MarketAppealCenter.tsx b/muse-studio/src/features/market/components/MarketAppealCenter.tsx new file mode 100644 index 00000000..9fe38edb --- /dev/null +++ b/muse-studio/src/features/market/components/MarketAppealCenter.tsx @@ -0,0 +1,222 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { + useMyAppeals, + useSupplementAppeal, + useWithdrawAppeal, + type MyAppealItem, +} from '../hooks/useMarketPublish'; + +/** 申诉状态 → 中文徽标(对齐后端 MarketAppealServiceImpl 申诉状态机)。 */ +const APPEAL_STATUS_META: Record = { + pending: { label: '待处理', className: 'bg-amber-50 text-amber-700' }, + reviewing: { label: '审核中', className: 'bg-blue-50 text-blue-700' }, + supplementing: { label: '待补充材料', className: 'bg-amber-50 text-amber-700' }, + maintained: { label: '维持原判', className: 'bg-slate-100 text-slate-500' }, + restored: { label: '已恢复', className: 'bg-emerald-50 text-emerald-700' }, + partially_restored: { label: '部分恢复', className: 'bg-emerald-50 text-emerald-700' }, + closed: { label: '已关闭', className: 'bg-slate-100 text-slate-500' }, +}; + +function appealStatusMeta(status: string): { label: string; className: string } { + return APPEAL_STATUS_META[status] ?? { label: status, className: 'bg-slate-100 text-slate-500' }; +} + +function formatSubmittedAt(value: number | null): string { + if (!value) return '—'; + return new Date(value).toLocaleString(); +} + +function appealTypeLabel(type: string): string { + if (type === 'license_revocation') return '撤权申诉'; + if (type === 'recall') return '召回申诉'; + if (type === 'delist') return '下架申诉'; + if (type === 'review_rejection') return '审核驳回申诉'; + if (type === 'usage_impact') return '使用影响申诉'; + return type; +} + +interface AppealRowProps { + appeal: MyAppealItem; + onWithdraw: (appealId: string, expectedStatus: string) => void; + onSupplement: (appealId: string, description: string) => void; + withdrawPending: boolean; + supplementPending: boolean; +} + +/** 单条申诉处理行:补充/撤回都通过后端命令接口执行,前端不直接改状态。 */ +function AppealRow({ + appeal, + onWithdraw, + onSupplement, + withdrawPending, + supplementPending, +}: AppealRowProps) { + const [supplementOpen, setSupplementOpen] = useState(false); + const [description, setDescription] = useState('已按要求补充材料与证据,请复核。'); + const status = appealStatusMeta(appeal.status); + + return ( +
  • +
    +
    +
    + + {appealTypeLabel(appeal.appealType)} + + + {status.label} + + 提交时间:{formatSubmittedAt(appeal.submittedAt)} +
    +
    {appeal.reason || '未填写申诉理由'}
    + {appeal.resolution &&
    处理结论:{appeal.resolution}
    } +
    资产 ID:{appeal.assetId}
    +
    +
    + {appeal.canSupplement && ( + + )} + {appeal.canWithdraw && ( + + )} +
    +
    + + {supplementOpen && ( +
    +