oh-my-muse/.agents/knowledge/external-deps-and-gotchas.md
lili 9af471289a fix(solo): 修复 2.0.0 单人版 live 后端启动(DB 口径对齐 + moderation 关停)
标准启动默认落陈旧的 muse_local(仅 V0 baseline、缺 yudao 基座、update_updated_at_column()
函数属第三个角色 muse)→ Flyway V1 CREATE OR REPLACE FUNCTION 报 "must be owner"。唯一健康
的 live 库是 root@muse_slice_live(已迁 V34、含基座+登录种子)。将 application-infra.yaml 与
start-muse-server-infra.sh 的默认库/用户改为 muse_slice_live/root(真实值仍由 infra.env 覆盖)。

S4 关停 spring.ai.model.* 时漏了 moderation;OpenAiModerationAutoConfiguration 默认
matchIfMissing=true → 全量 boot 实例化 openAiModerationModel、无 OpenAI key 直接抛
"OpenAI API key must be set" 使单体启动失败。在 monolith 真正加载的 muse-server/application.yaml
与 ai-server/application.yaml 两处补 moderation: none(单人形态输出合规走自研 MuseAiCandidateReviewService)。

workspace.spec 冒烟原缺 token 注入,被 S5 新增的 AuthGuard 拦重定向 /login → 补 test1 token 注入。
external-deps 记录 Dify 实例被游戏/Muse 共用、workspace 须隔离的约束。

验证:muse-server 干净启动(Started in 22.4s、Flyway up-to-date 零 owner error);MSW-off 创作主线
e2e 33 passed/0 failed/31 隔离(候选采纳/导入/导出/知识草稿/图谱/绑定/agent 生命周期/AI 真生成全绿);
run-p1r-verification.sh local 65 tests/0 fail BUILD SUCCESS。

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

32 KiB
Raw Blame History

knowledge:外部依赖、验收环境与工程坑(蒸馏自历史 memorys)

蒸馏自 2026-06-14 已清理的过程文档(外部验收留痕、P1 收口、studio/admin 搭建),只保留"删了会真丢"的操作事实与踩坑。凭据只记来源文件,不记明文。过时即更。

一、外部依赖与验收环境

依赖 地址 关键事实
New-API(LLM 网关) http://100.64.0.8:3000,OpenAI 兼容 /v1/chat/completions 已验收模型 MiniMax-M2.5;容器 new-api 端口 3000;其 DB 在 infra-postgres(宿主 100.64.0.8:5433)
RAGFlow(1.0 历史知识运行时) http://100.64.0.8(及 :9380) /api/v1/datasets/api/v1/retrieval;健康检查 /v1/system/healthz;2.0 下线由用户手工执行,不阻塞 S1
Dify 1.15.0(2.0 单人版目标外部服务) http://100.64.0.8:18080 官方 compose 部署在 mini-infra:/home/qingse/dify-1.15.0/docker,compose project=muse-dify;API health 返回 version=1.15.0
开发 PG / Redis 见下方凭据来源 远端 PG 15(用户确认可用,不强制 PG16);Redis 100.64.0.8:6379

Dify 状态(2026-07-07 S1):Dify 已作为 2.0 单人版目标外部服务完成基础设施部署与 live 契约验收,但生产运行时切换仍按 S2+ 后续步骤推进,不要把 S1 等同于 Muse runtime 已全面改走 Dify。当前 muse-dify 是一套独立基础设施栈,包含自己的 db/redis/weaviate/nginx/sandbox/plugin_daemon/worker/web 等容器;它不复用、不替换 Muse 既有 PostgreSQL/Redis。

凭据来源(明文不入库,只记位置):

  • 外部验收:muse-cloud/scripts/dev/p1r-external-acceptance.env(set -a; . 该文件; set +a 加载;含 New-API base/token、MUSE_AI_NEW_API_DEFAULT_MODEL_KEY=MiniMax-M2.5、RAGFlow base/key、GraphRAG 开关)。
  • 开发基础设施:~/.config/muse-repo/infra.env(PG/Redis 连接 + 真实凭据)。
  • Dify 控制台/模型/Datasets 凭据:mini-infra:/home/qingse/.config/muse-repo/dify.env 与本仓 muse-cloud/scripts/dev/p1r-external-acceptance.envMUSE_AI_DIFY_*MUSE_KNOWLEDGE_DIFY_* 块。仓内 env 按项目内网约定用于验收,日志与文档只输出长度/hash 前缀。
  • ⚠️ 安全:上述令牌仅限当前内网验收上下文;本轮 agent 工具输出曾出现 New-API 与 Dify 明文 key,用户需手工轮换 New-API token 与 Dify app/dataset keys

Live 验收 harness(opt-in,默认跳过,MUSE_P1R_EXTERNAL_ACCEPTANCE=true 才真跑):

  • muse-module-ai/.../application/muse/facade/P1rNewApiLiveAcceptanceIT.java(New-API)
  • muse-module-ai/.../application/muse/P1rImportLlmNewApiLiveAcceptanceIT.java(New-API 完整导入 LLM 解析)
  • muse-module-knowledge/.../application/muse/facade/P1rRagFlowLiveAcceptanceIT.java(RAGFlow)
  • muse-module-ai/.../application/muse/facade/P1rDifyChatLiveAcceptanceIT.java(Dify chat app)
  • muse-module-knowledge/.../application/muse/facade/P1rDifyDatasetsContractLiveIT.java(Dify Datasets create/upload/index/retrieve)
  • 另:muse-server/.../framework/api/P1rAiRuntimeEndToEndLiveAcceptanceIT.javaP1rKnowledgeRuntimeEndToEndLiveAcceptanceIT.java
  • 输出脱敏(只打 endpoint/模型/id/状态/耗时/usage/key 长度 + sha256 前 12);RAGFlow smoke 会留存 p1r-live-<时间戳> dataset 不删,Dify Datasets smoke 会留存 dify-contract-<时间戳> dataset 不删。
  • runtime client live ≠ coverage completed:仍需 Muse API 触发 + 落库 + 审计 + 失败路径证据(见 golden-journey)。

二、外部集成兼容坑(实证)

  • New-API 系统管理令牌 ≠ RAGFlow key:用前者打 RAGFlow /api/v1/datasets 返回 code=109 Authentication error,必须用独立 ragflow-* key。
  • RAGFlow 建数据集:POST /api/v1/datasetsconfig:{} 返回 code=101 Extra inputs are not permitted;只发 namecode=0。adapter 已改为 createDataset 只发 name,config 走独立 updateDatasetConfig
  • RAGFlow 文档状态轮询:用真实支持的 ?id=<docId>(单个);多 id 在 adapter 内 fail-closed 为 VALIDATION_ERROR
  • RAGFlow 无 copy/clone dataset API:复制一份 dataset 必须重走 createDataset + 逐文档上传 + startParse 重新索引,无法共享原 dataset 的索引(market KB 物化 D0-fork 据此设计,见 market-install-downstream-materialization.md)。
  • RAGFlow 检索 metadata 字段名:RAGFlow 官方契约是 metadata_condition。2026-06-28 已将 Muse HTTP 客户端从旧的 metadata_filter 修为 metadata_condition,并用请求体单测防回退。即便字段已修,跨可见性安全隔离仍不得依赖运行时 metadata 过滤,market KB 继续使用公开副本物理隔离。
  • GraphRAG attribution:默认 fail-closed(ATTRIBUTION_NOT_CONFIGURED);真跑需 MUSE_KNOWLEDGE_RAGFLOW_GRAPHRAG_ATTRIBUTION_READY=true + ..._GRAPHRAG_DATASET_ID
  • New-API/RAGFlow Java 客户端代理污染:2026-06-28 盘点时,未清代理的 Java/Python 探针曾返回 502,但 curl --noproxy '*' 与清代理 live IT 均证明 New-API/RAGFlow 可用。运行时代码已在 New-API 文本生成、导入解析、RAGFlow HTTP 客户端上显式使用 direct ProxySelector,避免 macOS/环境代理把 tailnet 内网请求转到本地代理导致假故障。
  • Dify 官方 compose 钉版:S1 使用 langgenius/dify 官方仓库 tag 1.15.0docker/ 目录,远端源码备份在 /home/qingse/dify-1.15.0,compose project 固定为 muse-dify。控制台初始化后 /console/api/setup 返回 step=finished,API health 返回 {"status":"ok","version":"1.15.0"}
  • ⚠️ Dify 实例被两个项目共用,workspace 必须隔离(2026-07-07 用户确认):同一 Dify 部署(100.64.0.8:18080)同时服务「游戏」项目与本项目「Muse」。Muse 的 app(写作透传/全书解析)、工作区级 Datasets API key、所有 dataset 必须落在 Muse 专属 workspace,禁止与游戏项目共用同一 workspace——工作区级 Datasets key 能列举/读写该 workspace 下全部 dataset,混用即跨项目数据泄漏 + 破坏安全边界。S1/S6/S7 落 MUSE_AI_DIFY_* / MUSE_KNOWLEDGE_DIFY_* 凭据前须确认其归属 workspace 是 Muse 专属;跨项目 dataset 隔离靠 workspace 边界,不靠 metadata 过滤。
  • Dify 存储类型坑:官方 .env 默认 STORAGE_TYPE=opendal + OPENDAL_FS_ROOT=storage 在当前 compose 部署下会出现 API 上传成功、worker 索引时报 File not found。已改为 STORAGE_TYPE=local + STORAGE_LOCAL_PATH=storage 并重启 api/api_websocket/worker/worker_beat/nginx;回滚备份为 mini-infra:/home/qingse/dify-1.15.0/docker/.env.s1-storage-before-local
  • Dify 模型 provider:通过 marketplace 插件 langgenius/openai_api_compatible/openai_api_compatible 连接 New-API。MiniMax-M2.5 作为 LLM,Qwen/Qwen3-Embedding-8B 作为 text-embedding,Qwen/Qwen3-Reranker-8B 作为 rerank;创建 high_quality dataset 前必须先配置 text-embedding,否则 dataset/create 会缺默认 embedding。
  • Dify Datasets key 类型:工作区级 key 从 /console/api/datasets/api-keys 创建,前缀 dataset-,可调用 /v1/datasets/v1/datasets/{id}/document/create-by-file/v1/datasets/{id}/retrieve;单 dataset key 从 /console/api/datasets/{id}/api-keys 创建,前缀通常为 ds-,用于限定某个 dataset。S1 执行计划要求的是工作区级 Datasets API key。app key 前缀 app-,只能打 app/workflow API,不能打 Datasets。
  • Dify Dataset 拓扑口径(2026-07-08):工作区级 Datasets API key 是管理钥匙,不是生产单 dataset 设计。生产至少分全局公共 dataset 与作品独立 dataset,Muse 本地保存 datasetId 映射,检索时按授权选择公共 + 当前作品 dataset 循环检索合并;禁止把公共和作品私有资料混在一个 dataset 后仅靠 metadata 当安全边界。S1 的 dify-contract-* dataset 只留作 contract evidence。
  • Dify Datasets retrieve 契约:Dify 1.15 的 /v1/datasets/{id}/retrieveretrieval_model 中必填 search_methodreranking_enable;只传 top_k/score_threshold_enabled 会 400。S1 live IT 固定 search_method=semantic_searchreranking_enable=false
  • Dify 控制台 API 坑:INIT_PASSWORD 最大长度 30;控制台登录 password 字段实际传 base64(password),不是 RSA;控制台 API 写操作需 cookie + X-CSRF-Token,不要拿 app/dataset key 调控制台端点。

三、前端 / 构建坑速查

  • studio(Vite + React + TS6.0):erasableSyntaxOnly 禁用 constructor 参数属性;exactOptionalPropertyTypes 下可选属性需规避写法;采纳/Diff 用 Coarse-to-Fine 分级 Diff 规避 3000+ 字正文上 O(N×M) LCS 性能爆炸;MSW 仅 import.meta.env.DEV 启用。
  • admin(Vben):museAdminApi 只拼 /muse/**(baseURL 已是 /admin-api,否则双前缀 /admin-api/admin-api);路由测试不能src/router/routes/modules/**(会被 Vite 动态路由 glob 当业务路由);Opsera pre-commit 缺失时跳过用 touch /tmp/.opsera-pre-commit-scan-passed
  • git 边界(未决):muse-admin/.gitignore 作"独立子仓库边界",源码靠 git add -f 纳入——长期纳管方式待定(见进度总账 TODO)。
  • Maven 增量编译陈旧坑(验证门禁/ArchUnit 改动时必踩,2026-06-17 实证):只改一个测试方法体/helper 的逻辑(如把 ArchUnit 谓词从 .application 临时改 .api 做反向红验证)后重跑,.class 时间戳会更新但方法体未真正重编译→ 反向测试假绿(看似规则没触发,实为跑的旧字节码)。根因=maven-compiler-plugin 的 maven-status/.../inputFiles.lst 增量跟踪没识别到变更;手动 rm target/test-classes/... 也不够(status 没更新仍跳过)。可靠复跑:rm -rf <module>/target/maven-status 或加 -Dmaven.compiler.useIncrementalCompilation=false;或干脆 mvn ... clean做"反向应红"自检时尤其要确认真重编译了,否则反假绿本身就是假绿。

四、跑 P1R real-PG 集成测试(IT)的配方 + 本机代理坑(2026-06-14 实证)

E0 分层入口(2026-06-27):默认 CI 只跑纯本地层,真实 PG / live 验收统一走脚本,避免把台账门禁、默认跳过或 mock 绿混成真验证。判据见 docs/mvp/1.0.0-真验证清单.md

# 纯本地门禁:coverage/RealApiGate/ArchUnit/契约结构,不连 DB/外部服务
bash muse-cloud/scripts/run-p1r-verification.sh local

# 真实 PG 层:非 live P1r*IT,会 flyway.clean() 指定 _test 库
set -a && . "$HOME/.config/muse-repo/infra.env" && set +a
P1R_FLYWAY_URL='jdbc:postgresql://100.64.0.8:5433/<专属_test库>' \
P1R_FLYWAY_USER=root \
  bash muse-cloud/scripts/run-p1r-verification.sh real-pg

# 外部 live 层:New-API/RAGFlow + runtime live,必须显式 opt-in
set -a
. "$HOME/.config/muse-repo/infra.env"
. "muse-cloud/scripts/dev/p1r-external-acceptance.env"
set +a
export MUSE_P1R_EXTERNAL_ACCEPTANCE=true
P1R_FLYWAY_URL='jdbc:postgresql://100.64.0.8:5433/<专属_test库>' \
P1R_FLYWAY_USER=root \
  bash muse-cloud/scripts/run-p1r-verification.sh external-live

脚本硬闸:真实 database name 必须以 _test 结尾;目标 _test 库必须经人类 DDL 闸门预建,脚本只做存在性预检、不自动 CREATE DATABASE;JDBC URL 不得携带 password/token/secret/api-key query;密码只走 P1R_FLYWAY_PASSWORDMUSE_POSTGRES_PASSWORD;输出只打印脱敏 URL。

结论先行:P1 后端 P1r*IT(completed-approval / Flyway 迁移 / events-publish outbox / live-acceptance)可在共享远端 PG 上真跑验证2026-06-27 RC fresh real-PG 层:bash muse-cloud/scripts/run-p1r-verification.sh real-pg 真连 _test 库、显式排除 live 外部链路,194 用例 0F/0E/0S,BUILD SUCCESS;缺 URL、非 _test、URL 携带敏感 query、缺目标库、live 未 opt-in 均会在 Maven 前失败。2026-06-27 RC fresh external-live 层:bash muse-cloud/scripts/run-p1r-verification.sh external-live server live 8/0F/0E/0S, module live 4/0F/0E/1S,BUILD SUCCESS;New-API completion、AI runtime usage/quota/attribution、RAGFlow health/upload/parse/chunks/retrieval、knowledge runtime、market KB fork 物化与 after-commit 均真打通过,唯一 skip=GraphRAG attribution 未配置,不得计为 passed。本轮修 2 个测试上下文落后产品演进的预存红:① P1rContentPlanningCompletedApprovalITTARGET_VERSION=21 停在 V21,但 savePlanningItem→captureFieldSnapshot 已依赖 V25 新增的 muse_content_planning_field_snapshot 表(commit 94a2379)→ 写入 relation does not exist 500;修=推进 TARGET_VERSION 21→31 + migrationsExecuted 断言 21→31(与 P1rContentCoreCompletedApprovalIT V30、P1rAiRuntimeEndToEndLiveAcceptanceIT V31 同向,核心断言不放宽)。② live-acceptance HTTP 代理坑(见下条)。经验:*CompletedApprovalIT 各自硬编 TARGET_VERSION,只迁到其所测路径需要的版本(V14~V31 混杂);产品给某 service 新增依赖表后,对应 IT 的 target 须跟进,否则该 IT(且仅该 IT)红——其他停在低版本的 IT 不受影响是因其路径不碰新表。此前 P1rKnowledgeFlywayMigrationIT 硬编码版本断言随 schema 增长失效(V14→V21→V23 复发两次)已修为动态:读本次 Flyway MigrateResult.targetSchemaVersion/migrationsExecuted 自适应(commit 4d46d7a)。

**▲ stale jar / argLine 假红(2026-06-27 RC 实证):**手工绕过 run-p1r-verification.sh 直跑 muse-server IT 时,不要只在 Maven 顶层传 -Dp1r.flyway.url 或代理清理参数。surefire forked JVM 可能拿不到这些系统属性,导致 JDBC 仍走旧代理或缺 flyway 参数;应把 p1r.flyway.url/user/locations 和代理清理一起放入 -DargLine。另外,改过任一上游 module 后跑 muse-server IT 必须加 -am 或先 mvn install,否则 server 会链接本地仓库旧 jar,表现为“代码已改但 IT 仍跑旧行为”的 stale-jar 假红。本轮 P1rMarketGovernanceWriteCompletedApprovalIT 就因未 -am 先复现旧 recall 断言,加 -am 后 targeted 6/0、全量 real-PG 194/0。

▲ 最大坑(排查极久):本机 env HTTP_PROXY=HTTPS_PROXY=http://127.0.0.1:7897 → JVM 取 socksProxyHost=127.0.0.1:7897,把所有 Java socket 走 SOCKS;转发 HTTP 正常,但破坏 PostgreSQL 原始线协议(发完 StartupMessage 即被 EOF,认证前关闭,极像 pg_hba/密码错)。 nc/curl 直连正常,只有 JDBC/JVM 中招;且 env -u HTTP_PROXY 清不掉(socksProxyHost 由 JVM 层注入)。

  • 判定 PG 真可达:printf '\x00\x00\x00\x08\x04\xd2\x16\x2f' | nc -w5 100.64.0.8 5433 | xxdN 即 PG 在说话(纯 nc 直连,不经代理)。
  • 修复:给 forked 测试 JVM 加 -DargLine='-DsocksProxyHost= -DsocksProxyPort='(清空 SOCKS,JVM 直连 tailnet);代码内可用 new Socket(java.net.Proxy.NO_PROXY)

▲ 第二代理坑(2026-06-26 实证,live-acceptance IT 专属):上一条只清 socksProxyHost,但 macOS 系统网络代理 127.0.0.1:7897 还会被 JVM 自动注入 http.proxyHost/https.proxyHost(-XshowSettings:properties -version 可见,即便 shell 已 unset HTTP_PROXY 且 mise exec 环境无代理)。 java.net.http.HttpClient(RagFlow/NewApi runtime client 用它,HttpClient.newBuilder()...build() 无显式 .proxy(...))默认走 ProxySelector.getDefault() → 经死代理 7897 → RagFlow health 报 RAGFLOW_UNAVAILABLE、New-API completions 报 provider_5xx / 502(retryable=true)。关键诊断:http.nonProxyHosts 不含 tailnet 100.64.0.8,故这些请求不被 bypass;curl --noproxy '*' 直打同地址 health/completions 全 200(证明外部服务健康、是 JVM 客户端走代理),极易误判成"外部环境波动/限流"。纯 PG IT 不中此坑(PG 走原始 socket 受 socksProxyHost 管,已被上一条清掉;不碰 HTTP 客户端)。

  • 修复:live-acceptance 的 argLine 须同时清 HTTP 代理:-DargLine='-DsocksProxyHost= -DsocksProxyPort= -Dhttp.proxyHost= -Dhttp.proxyPort= -Dhttps.proxyHost= -Dhttps.proxyPort= -Djava.net.useSystemProxies=false'。本轮加此参后 4 个 live-acceptance(NewApi 2/2、RagFlow 2/2[1 GraphRAG skip]、AiRuntime 3/3、KnowledgeRuntime 3/3)全绿。
  • 判定外部服务真可达(绕本机代理):curl -s --noproxy '*' http://100.64.0.8/v1/system/healthz(RAGFlow 回 {"status":"ok",...})、curl -s --noproxy '*' -X POST http://100.64.0.8:3000/v1/chat/completions -H "Authorization: Bearer <token>" -d '{"model":"MiniMax-M2.5","messages":[{"role":"user","content":"ping"}],"max_tokens":5}'(回 200 + completion)。两者 200 而 live IT 报 unavailable/5xx → 必是 JVM 代理坑,非外部波动。

▲ New-API 管理口 live 验收补充(2026-06-27 实证,E5 member): New-API 管理 API 与 OpenAI 兼容 completion 不是同一类调用,Account 侧 RealNewApiAccountFacade 会调用 /api/user/search/api/user//api/user/{id} 创建/刷新用户与配额。管理口鉴权从 NEW_API_SYSTEM_MANAGEMENT_TOKEN 读取,New-Api-User 必须是数值管理员用户 id,两者都只走 env,不得写入仓库/日志。排查时发现即便清了 shell 代理,Java/Python 仍可能读取 macOS 系统代理;curl --noproxy '*' 直连成功但 Java HttpClient 失败时,优先检查是否显式设置 Proxy.NO_PROXY/ProxySelectorRealNewApiAccountFacadeLiveAcceptanceIT 必须显式 MUSE_ACCOUNT_NEW_API_LIVE_ACCEPTANCE=true 才计为 live passed;默认 skip 是诚实跳过,不能当绿证。

跑单个 IT 的配方(已验证):

export MUSE_POSTGRES_PASSWORD=<root 密码;来自 infra.env / 用户提供,勿入库>
mise exec --cd muse-cloud -- mvn -pl muse-server -am test \
  -Dtest=<P1r...IT> -Dsurefire.failIfNoSpecifiedTests=false \
  -Dp1r.flyway.url='jdbc:postgresql://100.64.0.8:5433/<专属_test库>' \
  -Dp1r.flyway.user=root -Dp1r.flyway.locations='filesystem:sql/muse' \
  -DargLine='-DsocksProxyHost= -DsocksProxyPort='

要点:① 用 test 阶段 + -Dtest=<IT类>(surefire 显式点名即可跑 *IT),别用 verify/integration-test——那会触发 package/spring-boot repackage 把 member-server 等打成 fat jar,破坏跨模块编译(package ... does not exist),且 surefire 会先跑到平台陈旧红(QiniuSmsClientTest 时区)而中止整个 reactor。② p1r.flyway.url 库名必须 _test 结尾(IT 有 assertTestDatabaseUrl 守卫 + 会 flyway.clean() DROP 全库,绝不能指 muse_local / muse);各 IT 用各自专属 _test 库(muse_p1r_*_test 系列已存在;缺的用 root CREATE DATABASE,跑完可 DROP)。③ p1r.flyway.locations 必须恰为 filesystem:sql/muse(IT 断言该精确字符串,再从 user.dir 上溯定位 sql/muse)。④ 密码只走 env(MUSE_POSTGRES_PASSWORD/P1R_FLYWAY_PASSWORD;IT 有 assertNoPasswordSystemProperties,用 -D 传会红)。⑤ live-acceptance IT 需 set -a; . scripts/dev/p1r-external-acceptance.env; set +a 载入 token,且部分用例额外需 MUSE_P1R_EXTERNAL_ACCEPTANCE=true 才真跑(否则 assumeTrue 跳过、显示 Skipped)。⑥ 整套 -Dtest='P1r*IT' 批量跑必须加 -DreuseForks=false:P1rMarketFlywayMigrationITSystem.setProperty("p1r.flyway.url", maskedUrl) 脱敏覆盖,surefire 默认 fork 复用下污染同 fork 后续 IT 的系统属性 → 后续 IT 假报"缺少 p1r.flyway.url";每类独立 fork 即净(2026-06-15 实证:默认复用→多 IT 假红;-DreuseForks=false→23/23 全绿)。⑦ argLine 内可一并传 -Dp1r.flyway.url/user/locations(本轮即如此),与 maven -D 等效到 forked JVM。

远端 PG 实为 PostgreSQL 17.10(§一表内"15"为旧记,以此为准),超管用户 root;本机无本地 PG(127.0.0.1:5432 关闭)。批量顺序跑见交付报告(禁止并发 maven,会损坏 target)。

五、MVP #1「AI 候选采纳」纵切:后端门禁 + 活体全栈三大阻塞(2026-06-14 实证)

后端机械门禁 IT(已绿,确定性):muse-server/.../api/P1rContentMergeSuggestionIT——真实 PG + 真实 AiSuggestionMergeProjectionFacade(@Bean 直接实例化注入真实 MuseAiSuggestionMapper,规避 @ConditionalOnBean 时序;配套 @MapperScan("cn.iocoder.muse.module.ai.dal.mysql.muse"))读真种 muse_ai_suggestion,经 MockMvc 打 POST /app-api/muse/works/{workId}/blocks/{blockId}/suggestion-merges。5 路径:正向写 Canonical(revision1→2)/revision 冲突/幂等回放/非 pending/「审」缺失。跑法同 §四配方,-Dtest=P1rContentMergeSuggestionIT,需预建 muse_p1r_merge_slice_test(IT 自身 flyway.clean+迁移)。可合并候选的种子契约见 IT 内 insertMergeableSuggestion:status=pending、source_status=active、数值 authorization_snapshot_id、content_snapshot 带 content、diff_summary 带齐 outputComplianceResultId/staticCheckResultId/licenseRestrictionSnapshot(facade 缺一即判 unavailable)。

起活体全栈 muse-server 的三大阻塞(到「用户在 app 里真能用」):

  1. 远端 dev 库缺 yudao 基座 schema:muse_local 有 Muse 表(muse_content_*/muse_ai_suggestion)但system_tenant 等基座表、无 flyway_schema_history;muse_slice_live(新建)全空。muse-server 装配 system/infra 等模块,Flyway 只迁 Muse(V1-V21,sql/muse),基座 schema 未 Flyway 化(对抗复盘已记),故全栈 app 无法服务租户校验请求。起活体须先 provision 基座(yudao SQL dump)。【2026-06-14 已解,见 §六】:基座已固化为 sql/dev/yudao-base-*-postgres.sql 灌入 muse_slice_live,全栈 muse-server 已成功启动并 curl 活体跑通 AI候选→Canonical 采纳;后端 merge 链路另有真实 PG IT 证明。
  2. studio AI 流 parser 漂移(已修复 2026-06-14):connectAIStream(src/lib/sse.ts)原按 JSON data.type 单行分发,真后端(MuseAiTaskStreamServiceImpl 用 Spring SseEmitter.name(event).data(json))把事件名放 SSE event: 行(event:chunk/quality_check/done/error + data 行 JSON 无 type)→ 旧实现真实生成流全收不到。修复=复用同文件 createEventStreamParser(按 event: 行分发 + id/comment/多行 data 拼接),SSEEventHandler 回调契约不变。连带 gotcha:done.data 的 taskId/suggestionId 后端是 Long→JSON number,而前端全链路(onDone 类型、采纳请求体 MergeBlockSuggestionRequest、OpenAPI)按 string——已在 sse.ts done 分支于解析边界 String() 归一(缺失值保持 undefined 以维持「无候选→禁用采纳」判定);mock/契约测试改用数字 suggestionId 复现后端形态。验证用 ./node_modules/.bin/ 的 tsc -b --force / vitest run(47) / vite build 全绿(corepack pnpm exec 会触发 install 校验失败,勿用)。
  3. AI 运行时未写「审」字段(已修复 2026-06-14,chip task_092cfc32):原 createRuntimeSuggestion 不写 outputComplianceResultId/staticCheckResultId/licenseRestrictionSnapshot 且 source_status='verified'(merge 要 'active')→ 真实生成的候选不可合并。现新增 MuseAiCandidateReviewService 做真实轻量审(输出合规扫描/静态检查/许可快照,通过才给可追溯审结果 ID),createRuntimeSuggestion 写齐三审字段(通过时)+ 顶层 content(脱敏 summary,供 facade 可用性)+ source_status='active' + 数值授权快照(envelope id 为数值时落 BIGINT 列,沿用 MuseSuggestionServiceImpl 的 envelope=authz 约定)。遗留:非数值 envelope id 暂无法落 BIGINT 授权快照列(已知列能力限制),待授权快照建模收口;完整 provider 正文按数据主权不持久化,采纳走 merge_after_edit(用户回传所审最终正文)。门禁 P1rContentMergeGeneratedSuggestionIT(真实 PG)证明:经真实 createShadowSuggestionCandidate 产出的候选可被 mergeBlockSuggestion 采纳写入 Canonical。

本机替代与凭据(联调用):远端 Redis 需密码(未提供)→ 本机 redis-server(brew 8.x)+ --requirepass 替代(Redis 仅 session/cache 管道,非被测对象),infra.env 指 127.0.0.1。mock 登录:application-local.yaml muse.security.mock-enable=true,令牌 = mockSecret(test)+userId,即 test1=userId 1;tenant 来自请求头 getTenantId(request)~/.config/muse-repo/infra.env(仓库外,永不入库)本会话已填 root PG 密码 + 本机 redis;注意 app 默认连用户 muse_dev(密码未知),联调改 MUSE_POSTGRES_USERNAME=root安全:该文件含明文 root 口令,轮换/清理由用户掌握。

六、起全栈 muse-server(单体): 已成功启动 + 活体合并实证(2026-06-14 达成)

结论(里程碑):muse-server 本项目史上首次以单进程单体成功启动(Started MuseServerApplication in 17.985 seconds,Tomcat:48080),并在真实 HTTP + mock 鉴权 + 租户过滤 + 真实 PG(muse_slice_live)上端到端跑通 MVP #1 纵切:AI 候选(Shadow,muse_ai_suggestion)→ POST /app-api/muse/works/{w}/blocks/{b}/suggestion-merges(accept_as_is)→ Canonical 块正文真实写入(content_text=AI候选正文、revision 1→2、来源归因 ai_suggestion + lineage 落库),负路径(陈旧 expectedBlockRevision)被乐观锁拒(code 1041000002 Block版本冲突)。全程无 permissive stub,真实失败关闭

下文为打通全栈装配的 6 类硬阻塞实测复盘(可复用):

已拆(可复用):

  1. build:member-server repackage 无 classifier → fat jar → 下游 market 编译期 package does not exist,致 mvn package 全项目破损(仅 test 阶段不触发,故 CI test 假绿)。修:member-server repackage 配 <classifier>exec</classifier>(commit 1f89007)。
  2. postgres 基座:本仓原无 yudao postgres 基座 dump(只有 H2 测试 schema)。已翻译 system/infra/member 测试 schema→PostgreSQL(49 表)+ 最小种子,固化 muse-cloud/sql/dev/yudao-base-schema-postgres.sql + yudao-base-seed-postgres.sql(实测 0 失败)。灌库后 app Flyway(--spring.flyway.baseline-on-migrate=true)成功补 Muse V1-V21、Tomcat 起、Spring 初始化。muse_slice_live 现已含 基座+Muse V1-V21+种子(可复用)。

第 3 墙诊断订正(实测推翻"Feign RPC"初判):boot 初卡 PermissionCommonApi 无 bean,初判为 yudao-cloud Feign 跨模块 RPC 未桥接——实测证伪。真因是 system/infra/ai-server 也是无 classifier 的 repackage fat jar,嵌入 muse-server fat jar 后类不可加载(同 member 墙)。配 classifier 后(commit eea10b7),PermissionApi extends PermissionCommonApi 的本地 PermissionApiImpl(@Primary @RestController)即注册,bean 问题消失——无需任何 Feign 桥接/stub。随后逐层实测拆除:① datasource 报 EOFException = 本机 SOCKS 代理坑,须给 java-DsocksProxyHost= -DsocksProxyPort=(清 JVM system property;env -u HTTP_PROXY 清不掉)→ PG 连通;② Flyway V1-V21 在 muse_slice_live 迁移成功;③ security/permission/tenant 装配通过;④ codegen 缺 muse.codegen.import-enable(--muse.codegen.import-enable=false 补)。

已拆 ③ facade 条件装配(Spring 条件注解可靠性):@Service/@Component 上的 @ConditionalOnBean/@ConditionalOnMissingBean 不可靠(Spring 官方仅保证 @Bean 方法上有效),在"从未装配过的单体"里扫描顺序致非确定性"无 bean"失败(首发 ContentFileFacade,随后 ContentAiSuggestionFacade 等切片关键 facade 接连暴露)。修法(commit d09d529,均真实失败关闭、非 permissive stub):①6 个全默认内容 facade(ContentFile/KnowledgeDraft/Meta/ParseJob/PlanningCandidate/StyleCheck)→ 改由 MonolithFacadeFallbackAutoConfiguration(@AutoConfiguration+@Bean @ConditionalOnMissingBean 返回接口默认实现,真 impl 仍优先);②真 impl facade(AiSuggestionMergeProjectionFacade 等 4 个 @Primary)去 @ConditionalOnBean,恒注册;③Unavailable 兜底(4 个 source-owner + ToolGrantApproval + MetaImpact,均扩 abstract 方法不能自动配)就地去 @ConditionalOnMissingBean

已拆 ④ 跨模块 bean 名冲突(@Resource 按名注入):@Resource 优先按字段名注入;member 的 AccountExportServiceImpl.exportTaskMapper 与 content 的 ExportTaskMapper bean 撞名 → 单体内注入到错类型(JDK 代理 type mismatch,boot 失败)。修:字段重命名 accountExportTaskMapper(commit d09d529,全仓扫描无其它撞名)。

第 3 墙诊断总订正:初判"cloud→monolith 需 Feign RPC 桥接"被完全证伪——真因全是 ①②③④ 这类装配/打包/命名问题。PermissionApiImpl(@Primary)IS-A PermissionCommonApi,类可加载后 bean 自然满足,无需任何 Feign 桥接/stub

起全栈配方(已实测打通,可直接复用):

# 1) 建库灌基座+种子(root):base-schema + base-seed → muse_slice_live;app Flyway 补 Muse V1-V21
# 2) 本机 redis:redis-server --port 6379 --requirepass museredis
# 3) infra.env:MUSE_POSTGRES_USERNAME=root + root 口令、MUSE_POSTGRES_DATABASE=muse_slice_live、MUSE_REDIS_HOST=127.0.0.1
# 4) 起(务必清代理 JVM SOCKS,否则破坏 PG;codegen.import-enable 必给;关 AI 调度避免联调噪声):
cd muse-cloud; set -a; . ~/.config/muse-repo/infra.env; set +a
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy \
  mise exec -- java -DsocksProxyHost= -DsocksProxyPort= -Dhttp.proxyHost= -Dhttps.proxyHost= \
  -Dmuse.ai.runtime.dispatcher.initial-delay-ms=86400000 -Dmuse.ai.runtime.dispatcher.fixed-delay-ms=86400000 \
  -jar muse-server/target/muse-server.jar --spring.profiles.active=local,infra \
  --spring.flyway.baseline-on-migrate=true --muse.codegen.import-enable=false &
# 注:muse-server.jar 须为 thin lib(已配 classifier=exec 于 member/system/infra/ai-server)+ muse-server 自身 repackage 产出可执行 jar。

活体证明命令(curl 真打 48080;务必 --noproxy '*' 绕本机 HTTP 代理):

# 健康:GET 列表(mock 鉴权 test1=userId1,租户 1)
curl -s --noproxy '*' -H "Authorization: Bearer test1" -H "tenant-id: 1" -H "X-API-Version: 1" \
  "http://localhost:48080/app-api/muse/works?pageNo=1&pageSize=5"        # → {"code":0,...}
# 采纳:AI候选→Canonical(种子 work=1/block=1/suggestion=1;commandId 用 uuidgen)
curl -s --noproxy '*' -X POST -H "Authorization: Bearer test1" -H "tenant-id: 1" -H "X-API-Version: 1" \
  -H "Content-Type: application/json" \
  -d '{"commandId":"<uuid>","suggestionId":1,"expectedBlockRevision":1,"decisionType":"accept","mergeMode":"accept_as_is","sourceSnapshot":{"sourceType":"ai_suggestion"},"auditReason":"smoke"}' \
  "http://localhost:48080/app-api/muse/works/1/blocks/1/suggestion-merges"  # → code:0,newRevision:2,sourceAttribution+lineage
# 实测(2026-06-14):reload GET .../blocks 块 content_text=AI候选正文、revision=2;陈旧 revision=99 → code 1041000002 乐观锁拒(反假绿验证)

启动配置要点(workflow 推导):mock 登录 token=test1;system_tenant(id=1) 必需(已在种子);MuseAiRuntimeJobDispatcher 每秒查 muse_ai_job 无开关→用超大 delay 熄火;app-api 租户来自 tenant-id 头(默认 1)。

七、本地验证 openapi-diff 破坏性变更门禁(2026-06-17 实证)

.github/workflows/openapi-diff.ymldocker run tufin/oasdiff:v1.19.1 在 PR 上逐域比对 base/head 契约;本地无 Docker/GHA 无法整跑,但 oasdiff 是 Go 工具,可 go install 同款版本验证「检测逻辑 + 逐域循环聚合」。2026-06-19 已从 muse-cloud/.github/workflows/ 移到仓库根,否则自建 Gitea Actions 不会扫描触发。

# 一键复现(自举 oasdiff@v1.19.1 + 临时 venv 装 pyyaml,零全局污染;~首次 go install 稍慢)
bash muse-cloud/scripts/verify-openapi-diff.sh   # → 5 场景全绿即门禁逻辑正确
  • 坑1:CLI 须 subcommand 式 oasdiff breaking <base> <revision> --fail-on ERR(v1.7+ 由 flag 式 -base/-revision 改来);workflow 与脚本均用此式。
  • 坑2:版本须钉 tufin/oasdiff:v1.19.1(机械门禁不可用 :latest,否则上游 CLI/判定漂移会悄悄改门禁口径)。脚本 OASDIFF_VERSION 须与 workflow 标签一致。
  • 坑3:go install 自报 "version main" 属正常——release ldflags 仅官方构建注入,源码默认 version=main;二进制行为仍是所钉 tag 的。
  • 坑4:跨文件 $ref 靠目录结构:events 契约 $ref: '../openapi-base.yaml',故 base/head 必须各自保留 docs/api-contracts/ 完整目录(workflow 用 git worktree 检出整树、脚本用 cp -R 整目录),单独拷 openapi.yaml 会解析失败。
  • 实证(oasdiff v1.19.1,全部 7 域真实契约,5/5):无变更/新增可选参数/新增端点→放行(rc=0);参数变必填(request-parameter-became-required)/删端点(api-path-removed-without-deprecation)→拦截(rc=1)。
  • 残留(脚本覆盖不到):远端 Gitea Actions 触发管道(pull_request paths 触发、base.sha worktree、docker 镜像拉取)须一次真实 PR(改 docs/api-contracts/**)首跑确认。未首跑前不得宣称"CI 已实拦破坏性变更",只能称"检测逻辑+循环逻辑已本地实证、root workflow 已接线"。