# 踩坑与排障记录 > 用途:记录已验证、未来很可能再次遇到的问题。每条都应包含现象、原因、处理方式和验证方式。 ## 记录格式 ```md ## 问题标题 - 现象:看到什么错误或异常行为 - 原因:确认后的根因 - 处理:具体修复步骤 - 验证:如何确认修复有效 - 关联:相关文件、文档、提交或 Issue ``` ## 禁止 Data URL 持久化时不要漏掉异步任务 JSON - 现象:工程、素材、图层和元数据都已禁止 Data URL 后,服务器仍在生成高峰出现 SpacetimeDB / api-server 内存急剧膨胀甚至 OOM;读取少量正式生成任务也会造成远大于响应体的瞬时内存增长。 - 原因:同步接口 worker 化时把原请求整体序列化到 `external_generation_job.request_payload_json`,而前端又把已有 `objectKey` 下载成 Data URL 提交。任务表也是正式持久化边界;列表 procedure 若先收集完整任务行再截断,还会把 request/result 大字段在 SpacetimeDB、SDK mapper 和 BFF 多次持有。 - 处理:先在事故涉及的编辑器持久任务 JSON 上由 api-server 与 SpacetimeDB 两层递归拒绝 `data:` / `blob:` 并限制字节数;已有媒体传 `objectKey` / `resourceId` / `assetId`,本地派生图先用强唯一 key 上传。其它玩法若仍以 Data URL 作为正式请求契约,必须先资源化,不能直接扩大门禁造成玩法回归。列表、详情和 acknowledge 只走无 payload 的摘要投影,ack 不能为了同步旧字段重写大任务行;摘要错误文本也必须清除内联媒体并设硬上限,列表只能维护有界 top-N,不能先收集 owner 全量历史再截断。历史只通过迁移操作员的 dry-run + B-tree cursor 分批 procedure 压缩 `editor-canvas` 终态任务,cursor 选择读取量必须受 limit 约束,绝不全表扫描、绝不处理 pending / running;dry-run 后 apply 同一批时保持输入 cursor 不变,最后一批即使 `has_more=false` 只要仍有命中也必须 apply,只有 apply 成功后才推进到返回 cursor。SpacetimeDB CLI 2.5 的 `Option` 非空参数必须使用 SATS sum 编码;维护脚本要统一编码 `cursor_job_id`、`owner_user_id` 和 `completed_before_micros`,否则首批空 cursor 可运行,但第二批或带截止时间的调用会在写入前被拒绝。 - 发布门禁:生产发布入口必须固定 `--delete-data=never` 与 scoped `--yes=migrate,break-clients`,普通 Jenkins 参数不得暴露清库开关;需要删数据的 schema 冲突必须直接阻断并重新检查 artifact/schema,不能靠裸 `--yes` 放行。 - 验证:构造嵌套 Data URL、Blob URL 和超限 JSON 确认入队失败;检查正式 UI procedure / client record 不含 request/result payload;用 dry-run 和 apply 测试确认活动任务不变、终态普通提示词保留且内联媒体被替换;至少带一次非空 `--cursor-job-id` 与 `--completed-before-micros` 验证 CLI Option 编码,而不是只测首批空 cursor。 - 关联:`server-rs/crates/api-server/src/editor_generation_queue.rs`、`server-rs/crates/spacetime-module/src/external_generation.rs`、`server-rs/crates/api-server/src/external_generation.rs`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 ## React 测试因内部状态或实现细节正常重构就碎 - 现象:修改组件结构、按钮排序、图标库 class、提示文案或 hook 内部状态名后,React 测试大量失败,但真实用户流程和对外契约没有变化。 - 原因:测试把 `data-testid` 仪表盘、`textContent` 拼接状态、完整对象 / 数组顺序、图标 class 或长文案当成契约;这些断言绑定的是实现形状,不是用户行为或稳定边界。 - 处理:按 `React 组件测试准则` 重写到更稳定的层级。用户流程测试断言 role / label / URL / 弹窗 / callback;hook 逻辑用 `renderHook` 直接验证公开返回契约;DTO / payload 使用关键字段或 `expect.objectContaining(...)`。只有产品明确要求的可访问语义、固定顺序或渲染边界才保留精确断言。 - 验证:运行触达文件的定向 `vitest`,必要时追加 `npm run typecheck`、`npm run check:encoding` 和 `git diff --check`。 - 关联:`docs/technical/【前端测试】React组件测试准则-2026-06-26.md`、`src/components/image-editor/useCanvasGenerationDialogs.test.tsx`、`src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx`。 ## 图片画布素材库删除要匹配 sourceResourceId - 现象:素材库中删除了已经生成并进入素材库的资源,但画布上对应图层仍然存在,刷新后还可能从已保存布局里恢复。 - 原因:生成素材进入账号级素材库时可能通过 `editor_asset.sourceResourceId` 指向原项目资源;如果前端素材库映射和级联删除只比较 `sourceAssetId`、`assetObjectId`、`objectKey` 或 `src`,就会漏掉只靠项目资源 ID 关联的历史 / 后端生成图层。 - 处理:`EditorAsset` 必须保留 `sourceResourceId`;从素材库添加到画布时继续写入图层;删除素材时同时比较 `layer.resourceId` / `layer.sourceResourceId` 与 `asset.sourceResourceId`。 - 验证:`ImageCanvasEditorModel.test.ts` 覆盖素材库 source resource 保留,`useImageCanvasAssetCanvasBridge.test.tsx` 覆盖资源 ID 级联清理,`ImageCanvasEditorAssetsIntegration.test.tsx` 覆盖删除后保存的新 layout 不再包含被删图层。 - 关联:`src/components/image-editor/ImageCanvasEditorModel.ts`、`src/components/image-editor/useImageCanvasAssetCanvasBridge.ts`、`src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`。 ## 后台素材查询不要用 SQL 直查 editor_asset - 现象:后台“素材查询”报 `HTTP 400:no such table: editor_asset. If the table exists, it may be marked private.`。 - 原因:`editor_asset` 是私有 SpacetimeDB 表,后台 SQL / schema HTTP 查询面看不到私有表;即使 api-server 有后台身份,也不能把私有表当 Dashboard SQL 表直接查。 - 处理:后台素材查询走 `spacetime-module` 内的 `admin_list_editor_assets_and_return` procedure,由 `spacetime-client` typed facade 调用后再在 `api-server` 映射作者展示名和陶泥号。新增类似后台只读能力时,优先补窄 procedure / read model,不要复用 `fetch_admin_dashboard_rows` 直查私有源表。 - 验证:`cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:spacetime-schema`。 - 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`server-rs/crates/spacetime-client/src/editor_project.rs`、`server-rs/crates/api-server/src/admin.rs`。 ## 陶泥儿精选重复先查同源同媒体画布副本 - 现象:每次从项目素材中把同一个生成素材拖到画布上,`陶泥儿精选` 都多出一张看起来相同的素材。 - 原因:素材拖入画布会为图层实例准备 `editor_project_resource`;如果该素材本来带 `sourceResourceId` 指向原始生成资源,而新资源仍按普通 generated 资源公开,精选就会把原件和每次拖拽产生的同源同媒体副本都展示出来。 - 处理:创建项目资源时保留 `source_resource_id`,并在同项目已有同源同媒体资源时复用已有 resource;确需创建同源同媒体副本时默认 `public_showcase_enabled = false`。公开精选读取和前端精选模型都跳过 `sourceResourceId` 指回同一媒体原件的副本,但不要按图片地址全局去重,避免不同生成步骤共享占位图时被误合并。 - 验证:`creationShowcaseModel.test.ts` 覆盖同源同媒体副本只展示原件;`ImageCanvasEditorAssetsIntegration.test.tsx` 覆盖拖拽生成素材到画布时继续提交 `sourceResourceId`;`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 确认后端资源复用 / 精选过滤逻辑可编译。 - 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`src/components/creation-home/creationShowcaseModel.ts`、`src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`。 ## 陶泥儿精选作者丢失先查公开作者展示字段 - 现象:`/creation` 的 `陶泥儿精选` 卡片和预览弹窗中,素材下方作者名消失、只显示占位,或错误显示内部用户 ID。 - 原因:精选数据源来自公开 `editor_project_resource` 快照;如果 SpacetimeDB read model、`spacetime-client` mapper 或 `api-server` payload 任一层漏传 `authorDisplayName` / `display_name` 或 `authorPublicUserCode` / 陶泥号,前端没有可展示的公开作者字段。`owner_user_id` / `ownerUserId` / `user_id` 是内部归属字段,不是公开作者名兜底。 - 处理:`EditorProjectResourceSnapshot`、`EditorProjectResourceRecord` 和 `EditorProjectResourcePayload` 需要一路保留公开作者展示字段;前端 `creationShowcaseModel` 优先显示 `authorDisplayName` / `display_name`,没有展示名时显示 `authorPublicUserCode` / 陶泥号,绝不能兜底到内部 `ownerUserId` / `user_id`。 - 验证:`creationShowcaseModel.test.ts` 覆盖展示名优先、陶泥号兜底和内部 owner/user id 不展示;`editorProjectClient.test.ts` 覆盖公开精选接口客户端保留公开作者字段;若改动 SpacetimeDB read model,再运行 `npm run spacetime:generate`、`cargo check -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml` 和 `npm run check:spacetime-schema`。 - 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`server-rs/crates/spacetime-client/src/mapper/editor_project.rs`、`server-rs/crates/api-server/src/editor_project.rs`、`src/components/creation-home/creationShowcaseModel.ts`。 ## 陶泥儿精选瀑布流变宽先查 multi-column 容器宽度 - 现象:release `/creation` 桌面端精选卡片明显变宽,第三列被裁到屏幕外,页面内部可横向滑动;dev 看起来正常。 - 原因:精选瀑布流使用 `column-count`,当它作为 grid item 时如果没有显式 `width: 100%` / `min-width: 0`,Chrome 会用多列内容的 intrinsic width 反向撑开 grid track。线上实测 1920 视口下 section 为 `1296px`,waterfall 被撑到约 `2048px`,单卡宽约 `672px`。 - 处理:保留 multi-column 瀑布流时,`.creation-landing__asset-waterfall` 必须显式约束 `width: 100%` 和 `min-width: 0`;不要只看单张图片天然尺寸或改卡片宽度。 - 验证:Playwright / CSSOM 检查 `.creation-landing__section`、`.creation-landing__asset-waterfall`、首张 `.creation-landing__asset-card` 的 `getBoundingClientRect()`,waterfall 宽度应等于 section 宽度。 - 关联:`src/index.css`、`src/components/creation-home/CreationLandingView.tsx`。 ## 画板外部生成排队超时不是失败 - 现象:画板发起付费图片生成后,前端弹出 `生成任务仍在队列中,请稍后刷新画布查看结果`,但后端任务仍在队列或执行中,后续可能正常完成。 - 原因:画板生成已经接入后端外部生成任务队列,`queued` / `running` 是正式任务状态;旧前端轮询等待窗口到期时直接抛错,导致正常排队被提交流程 catch 成失败 UI。 - 处理:`waitForEditorGenerationQueue` 等待超时只返回“仍在后端继续执行”,调用方停止本次前端等待并保留生成中状态;只有后端任务终态为 `failed` 才展示失败。 - 验证:画板生成 workflow 测试覆盖 queueState 持续 `running` 到前端等待窗口结束时,不进入 failed、不显示该排队文案、不添加本地临时结果层。 - 关联:`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx`。 ## 画板参考图 objectKey 必须先做归属校验 - 现象:画板生成、快速编辑、图标素材或 UI 素材提取如果允许直接提交 generated objectKey,用户只要知道其他账号的私有 objectKey,就可能让 api-server 签名读取并送给外部生成供应商。 - 原因:Data URL 参考图可以直接解析,但 objectKey 是服务端私有对象引用;只校验 generated 前缀、mime 和大小不能证明它属于当前账号。 - 处理:所有编辑器参考图入口统一走 `parse_editor_reference_image(state, owner_user_id, source)`;objectKey 分支必须先在当前账号的项目资源、素材库资产或 `asset_object` 中匹配 owner / bucket / key,再读取 OSS。图标素材等额外参考图必须真实传到 provider,不只写 metadata;图片快速编辑当前不开放额外参考图,若后续重开入口也必须沿用同一归属校验。 - 验证:`cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_reference`,并用前端 workflow 测试覆盖 `referenceImageSrcs` 进入图标生成请求;若快速编辑重开额外参考图,再补对应请求覆盖。 - 关联:`server-rs/crates/api-server/src/editor_project.rs`、`server-rs/crates/spacetime-client/src/assets.rs`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`。 ## 资产换签不能把 generated 前缀当成 objectKey 授权 - 现象:主站或 External API 只要拿到另一个账号的 generated `objectKey` 就能换签,已登记的私有对象因为 key 同时命中 legacy 前缀而被匿名读取,或者 `/api/assets/read-url` 已拒绝但 `/api/assets/read-bytes` 仍能读出原始字节;后台资源预览为解决跨账号读取又误把主站入口整体放开。 - 原因:`legacyPublicPath` 与 `objectKey` 代表两种不同信任边界。前者仅用于未登记历史公开作品兼容,后者是正式对象引用;只检查 generated 前缀、或在查询 `asset_object` metadata 前直接接受 legacy 白名单,都不能证明对象公开或属于调用方。签名 URL 和 bytes proxy 如果各写一套判断也容易漂移。 - 处理:`read-url` 与 `read-bytes` 必须共用 `authorize_asset_read_target`,先按配置 bucket / 精确 key 查询 `asset_object`;metadata 一旦存在,即使 key 命中 legacy 前缀,也严格执行 `PublicRead` / owner ACL。只有 metadata 不存在且显式 `legacyPublicPath` 命中 `platform_oss::LEGACY_PUBLIC_PREFIXES` 时才允许匿名兼容;任意 `objectKey` 必须登记。External read-url 使用 API Key owner。主站和 External 的 object confirm owner 必须来自认证主体,同 bucket / key 已登记后不能改变 owner,不能让请求体 owner 接管对象。后台跨 owner 换签只用于管理员资源预览,成功后以 `admin_asset_read_url` 持久化管理员 subject、请求对象和有效期,且不得记录 signed URL;不能为此把 admin 能力下沉到主站入口。无权访问统一返回不存在,避免泄露对象是否存在。 - 验证:覆盖未登记 curated legacy public path、命中 legacy 前缀但已有私有 metadata、未登记 objectKey、公开对象、本人私有对象、跨 owner、匿名私有、External owner、confirm owner 不可变和 admin-only endpoint;对 `read-url` 与 `read-bytes` 使用同一组授权矩阵,并断言 Admin 成功换签会生成不含 signed URL 的管理员主体审计事件。 - 关联:`server-rs/crates/api-server/src/assets.rs`、`server-rs/crates/api-server/src/external_assets_api.rs`、`server-rs/crates/api-server/src/admin.rs`、`server-rs/crates/api-server/src/modules/admin.rs`。 ## 编辑器生成按钮显示泥点后仍要查真实钱包预扣 - 现象:画板生成按钮显示 `N泥点`,后端也能按模型配置计算出价格,但用户点击后钱包余额不变。 - 原因:前端展示价和后端价格计算只证明价格能被展示 / 解析;如果 handler 没有包进 `execute_billable_asset_operation_with_cost`,或异步音频发布目标没有携带本次模型价格,外部 provider 仍会被调用但不会真实扣费。 - 处理:新增或改造编辑器外部生成入口时,确认前端请求不携带 `priceMudPoints`,后端按运行时模型定价重新计算价格,并用该价格进入资产扣费 wrapper。音频提交 / 发布分离时,把后端计算出的价格写入 `AudioAssetBindingTarget.billing_points_cost`。 - 验证:结构性测试覆盖对应 handler 包含 `execute_billable_asset_operation_with_cost` 和价格变量;音频测试覆盖 `resolve_creation_audio_points_cost` 优先读取 editor target 的 `billing_points_cost`。 - 关联:`server-rs/crates/api-server/src/editor_project.rs`、`server-rs/crates/api-server/src/character_animation_assets.rs`、`server-rs/crates/api-server/src/vector_engine_audio_generation/`、`src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts`。 ## 本地 dev 启动日志先看成功锚点,不要把非阻断 warning 当失败 - 现象:`npm run dev` 启动 SpacetimeDB 时可能先打印 `static max level is off`、`Skipping tokio metrics`,或 SpacetimeDB CLI 提示存在新版本 / 当前版本较旧,看起来像启动异常。 - 原因:这些是 tracing、metrics 或 CLI 更新提示,不代表本地 dev 栈失败;同一段日志后续仍可能已经完成 `SpacetimeDB listening on 127.0.0.1:3101`、模块 publish、`api-server` `/healthz` 200、主站 Vite `3000` 和后台 Vite `3102` ready。 - 处理:排查本地 dev 栈时先确认成功锚点:`[dev:spacetime] actual`、`Updated database`、`api-server 已完成 tracing 初始化并开始监听`、`/healthz` 200、两个 Vite `ready`。只有缺少这些锚点或进程退出时,再继续查 CLI 权限、端口占用、publish 或 API 编译问题。 - 验证:`http://127.0.0.1:3101/v1/ping` 可访问、`http://127.0.0.1:8082/healthz` 返回 200、`http://127.0.0.1:3000/` 和 `http://127.0.0.1:3102/admin/` 可打开。 - 关联:`scripts/dev.mjs`、`.app/dev-stack.json`、`docs/project-memory/shared-memory/development-workflow.md`。 ## 私有兑换码不适用先查同手机号重复账号 - 现象:后台把私有兑换码配给某个陶泥号或手机号后,用户用同一手机号登录兑换仍提示 `该兑换码不适用于当前账号`。 - 原因:认证表里可能存在同一手机号的多条 `user_account`。如果认证工作集重建 `phone_to_user_id` 时让 `user_account.phone_number_e164` 后写覆盖前写,当前登录态会漂到没有 `auth_identity` 的重复账号,而兑换码白名单仍指向另一个内部 `user_id`。 - 处理:重建认证工作集时以 typed `AuthStoreProjectionView` 从 `user_account` / `auth_identity` / `refresh_session` 恢复;手机号索引以 `auth_identity(provider="phone")` 指向的账号为权威,`user_account.phone_number_e164` 只补没有 identity 的手机号;`auth_store_snapshot` 表和旧 JSON procedure 已删除,Bearer / refresh session 本进程未命中时不要再从 SpacetimeDB 导出整包状态刷新内存。线上止血先核对失败请求附近的 current session `user_id` 与兑换码 `allowed_user_ids`,不要只看手机号展示值。 - 约束:`auth_identity` 只保存登录入口身份键;手机号、昵称和头像的正式资料真相在 `user_account.phone_number_e164` / `display_name` / `avatar_url`。旧 `auth_identity.phone_e164` / `display_name` / `avatar_url` 只能作为历史回填来源,不能继续让新写入依赖这些列。 - 验证:`cargo test -p spacetime-module auth_export -- --nocapture` 应覆盖同手机号重复账号时手机号索引优先指向有 phone identity 的账号;`api-server` 中不应再存在运行期 `refresh_auth_store_from_spacetime` 调用。 - 关联:`server-rs/crates/spacetime-module/src/auth/procedures.rs`、`server-rs/crates/spacetime-module/src/auth/tables.rs`、`server-rs/crates/module-auth/src/lib.rs`。 ## API Build / Deploy 归档清单不能漏掉随包 Pingora 脚本 - 现象:`Genarrative-Api-Deploy` 在发布阶段报 `发布产物缺少 Pingora TLS 证书同步脚本: build//scripts/deploy/pingora-tls-cert-sync.mjs`。 - 原因:`scripts/build-production-release.sh` 已经把脚本复制进 `build//scripts/deploy/`,但 Jenkins API Build 的 `archiveArtifacts` 和 API Deploy 的 `copyArtifacts` 过滤清单仍可能漏掉新增随包脚本,导致 Deploy 工作区拿到的是残缺发布包。 - 处理:新增随包部署脚本时,必须同时更新 `jenkins/Jenkinsfile.production-api-build` 的归档清单、`jenkins/Jenkinsfile.production-api-deploy` 的复制清单和 `scripts/check-production-ops-guardrails.mjs` 的字符串门禁;不要在 Deploy Job 里从工作区根目录或源码 checkout 兜底补脚本。 - 验证:运行 `npm run check:production-ops`、`npm run check:production-api-release` 和 `npm run check:production-api-deploy`,确认构建包、Jenkins 归档链路和 deploy fail-fast 检查口径一致。 - 关联:`jenkins/Jenkinsfile.production-api-build`、`jenkins/Jenkinsfile.production-api-deploy`、`scripts/deploy/production-api-deploy.sh`、`scripts/check-production-ops-guardrails.mjs`。 ## 图片画布角色动作结果主类型是序列帧 - 现象:产品要求画板 `生成角色动作` 返回后按透明序列帧播放和下载,但旧实现或旧测试可能继续把结果当作预览视频处理。 - 原因:后端仍需要先生成 `previewVideoPath` 再抽帧、绿幕去背和落 OSS;如果前端把预览视频当主媒体,就会绕过已经扣绿幕的 PNG 帧,也无法按序列帧打包下载。 - 处理:角色动作结果图层主 `src` 使用 `frames[0].imageSrc`,`mediaType` 固定为 `image-sequence`,`assetKind` 固定为 `character-animation`,完整帧列表写入 `imageSequenceFrames`,`previewVideoPath` 只作为来源信息保留。单图层下载必须生成序列帧 ZIP;画布素材 ZIP 中角色动作写入 `sequences/<编号-标题>/frames/`。不得移除后端原有视频生成、抽帧、绿幕去背和帧落盘流程。 - 验证:`ImageCanvasGenerationLayerModel` 应断言动作结果 `src` 为首帧且 `mediaType="image-sequence"`;画布集成测试应出现 `画布序列帧:角色动作` 图片播放器,不应出现角色动作 `