From 6eed6bd13f0d98404442dd93844d40995166f859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 17 Sep 2026 14:02:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E6=95=9B=20content=E2=86=92=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=96=87=E6=9C=AC=E4=B8=BA=E5=8D=95=E4=B8=80=E5=8F=A3?= =?UTF-8?q?=E5=BE=84=E5=B9=B6=E5=BC=BA=E5=88=B6=E4=BC=A0=20manifest?= =?UTF-8?q?=EF=BC=88review=20=E7=AC=AC=202=E3=80=813=20=E6=9D=A1=EF=BC=89?= =?UTF-8?q?=20-=20directCodexContentToPromptText=20=E7=9A=84=20assets=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=BF=85=E5=A1=AB=EF=BC=8C=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=20=3D=20[]=20=E9=BB=98=E8=AE=A4=E5=80=BC=EF=BC=9A=E6=BC=8F?= =?UTF-8?q?=E4=BC=A0=E7=B4=A0=E6=9D=90=E6=B8=85=E5=8D=95=E4=BB=8E=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=9C=9F=E6=96=87=E6=A1=88=E9=80=80=E5=8C=96=E5=8F=98?= =?UTF-8?q?=E5=9B=9E=E7=BC=96=E8=AF=91=E6=9C=9F=E9=94=99=E8=AF=AF=20-=20qu?= =?UTF-8?q?euedChatTurnLabel=20=E7=9A=84=20assets=20=E4=B8=8E=20ComposerTu?= =?UTF-8?q?rnQueue=20=E7=9A=84=20assets=20=E5=B1=9E=E6=80=A7=E5=90=8C?= =?UTF-8?q?=E6=A0=B7=E5=BF=85=E5=A1=AB=EF=BC=8C=E9=98=9F=E5=88=97=20chip?= =?UTF-8?q?=20=E4=B8=8E=E6=B6=88=E6=81=AF=E6=AD=A3=E6=96=87=E5=85=B1?= =?UTF-8?q?=E7=94=A8=E5=90=8C=E4=B8=80=E4=B8=AA=E6=B4=BE=E7=94=9F=EF=BC=88?= =?UTF-8?q?chip=20=E5=8F=AA=E5=A4=9A=E5=81=9A=E5=8E=8B=E5=8D=95=E8=A1=8C?= =?UTF-8?q?=E4=B8=8E=E9=99=90=E9=95=BF=EF=BC=89=20-=20=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=98=BE=E5=BC=8F=E4=BC=A0=20[]=20=E6=88=96?= =?UTF-8?q?=20manifest=EF=BC=9AresourceReferences=E3=80=81chat-composer=20?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E6=8C=89=E6=96=B0=E7=AD=BE=E5=90=8D=E6=94=B6?= =?UTF-8?q?=E6=95=9B=20-=20=E6=9B=B4=E6=96=B0=20pitfalls=20=E6=9D=A1?= =?UTF-8?q?=E7=9B=AE=E4=B8=8E=20decision-log=EF=BC=88canonical=20content?= =?UTF-8?q?=20=E6=9C=89=E6=95=88=E6=80=A7=E5=8F=AA=E5=88=A4=E6=95=B4?= =?UTF-8?q?=E6=9D=A1=20content=E3=80=81=E6=8A=95=E5=BD=B1=E9=80=90?= =?UTF-8?q?=E5=AD=97=E9=80=8F=E4=BC=A0=E3=80=81=E5=8F=AF=E8=AF=BB=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E5=8D=95=E4=B8=80=E5=8F=A3=E5=BE=84=E5=BF=85=E5=A1=AB?= =?UTF-8?q?=20assets=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/project-workspace/ComposerControls.tsx | 4 ++-- .../features/project-workspace/chatComposerQueue.ts | 9 +++++---- .../project-workspace/resourceReferences.ts | 10 +++++++++- .../tests/appSurface/chat-composer.suite.ts | 10 +++++----- .../tests/resourceReferences.test.ts | 13 ++++++++----- docs/project-memory/shared-memory/decision-log.md | 10 ++++++++++ docs/project-memory/shared-memory/pitfalls.md | 1 + 7 files changed, 40 insertions(+), 17 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ComposerControls.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ComposerControls.tsx index 410ee2da7..292ddd245 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ComposerControls.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ComposerControls.tsx @@ -197,8 +197,8 @@ export function ComposerTurnQueue({ onCancel, }: { turns: readonly QueuedChatTurn[]; - /** 与聊天输入区同源的素材清单:chip 文案里的 `@` 引用按它展开成显示名。 */ - assets?: readonly GameCreationAppAssetManifestEntry[]; + /** 与聊天消息渲染同源的素材清单:chip 文案里的 `@` 引用按它展开成显示名。 */ + assets: readonly GameCreationAppAssetManifestEntry[]; onCancel: (id: string) => void; }) { if (turns.length === 0) { diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/chatComposerQueue.ts b/apps/ai-game-creator-shell/src/features/project-workspace/chatComposerQueue.ts index 2d88fa4ec..3132e0602 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/chatComposerQueue.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/chatComposerQueue.ts @@ -75,14 +75,15 @@ export function chatQueueFullNotice(): string { } /** - * 队列 chip 上显示的文字:单行、有长度上限。 + * 队列 chip 上显示的文字:与真实消息同一个派生(`directCodexContentToPromptText`), + * 再压成单行并限长。 * - * `assets` 必须与聊天输入区同源(`manifest.assets`):`@` 引用要按显示名展开,否则 - * chip 上会露出 `@asset:…` 这样的内部 id,与用户输入时看到的 `@显示名` 对不上。 + * `assets` 与聊天消息渲染同源(`manifest.assets`)且必填:`@` 引用按显示名展开,chip + * 与消息正文逐字一致,不会露出 `@asset:…` 这种内部 id。 */ export function queuedChatTurnLabel( turn: QueuedChatTurn, - assets: readonly GameCreationAppAssetManifestEntry[] = [], + assets: readonly GameCreationAppAssetManifestEntry[], ): string { const text = directCodexContentToPromptText(turn.userItem.content, assets) .trim() diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts b/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts index f6a8f6712..614c3f651 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts @@ -89,9 +89,17 @@ export const EMPTY_CHAT_COMPOSER_DRAFT: ChatComposerDraft = { content: [], }; +/** + * canonical content → 可读文本:**消息正文、队列 chip、润色判据、出站 prompt 只有这一个口径**。 + * + * `assets` 是当前项目的 manifest 且必填:`agc_resource_reference` 按 `@显示名` 展开,与用户 + * 在编辑器里看到、以及聊天消息里渲染出来的字面量逐字一致。没有默认值 —— 漏传素材清单会 + * 静默退化成 `@内部 id`(用户看到的文案与真正落盘的 prompt 分叉)。只有素材已不在清单里时 + * 才回落到 `resourceId`,那是「引用指向已消失的素材」的兜底而不是常态。 + */ export function directCodexContentToPromptText( content: readonly DirectCodexUserContentPart[], - assets: readonly GameCreationAppAssetManifestEntry[] = [], + assets: readonly GameCreationAppAssetManifestEntry[], ) { const labels = new Map( assets.map((asset) => [asset.id, resourceDisplayName(asset)]), diff --git a/apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts index 53f8df2bd..ba027ccfa 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts @@ -223,8 +223,6 @@ export function registerChatComposerControlTests() { expect(queuedChatTurnLabel(turn, manifestAssets)).toBe( '用这张图改一下@hero', ); - // 素材清单不传(老调用方)时退回稳定 id,不抛错。 - expect(queuedChatTurnLabel(turn)).toBe('用这张图改一下@asset:hero'); }); it('keeps queued chat turns in FIFO order and drops only the cancelled one', () => { @@ -241,8 +239,10 @@ export function registerChatComposerControlTests() { // FIFO:先入先出,不丢、不乱序。 const firstOut = dequeueChatTurn(queue); expect(firstOut.next?.clientTurnId).toBe('client-1'); - expect(firstOut.next && queuedChatTurnLabel(firstOut.next)).toBe('第一条'); - expect(firstOut.rest.map((turn) => queuedChatTurnLabel(turn))).toEqual([ + expect(firstOut.next && queuedChatTurnLabel(firstOut.next, [])).toBe( + '第一条', + ); + expect(firstOut.rest.map((turn) => queuedChatTurnLabel(turn, []))).toEqual([ '第二条', '第三条', ]); @@ -250,7 +250,7 @@ export function registerChatComposerControlTests() { // 单条取消只移除那一条,顺序不变。 expect( removeQueuedChatTurn(firstOut.rest, 'turn-2').map((turn) => - queuedChatTurnLabel(turn), + queuedChatTurnLabel(turn, []), ), ).toEqual(['第三条']); expect(removeQueuedChatTurn(firstOut.rest, 'turn-missing')).toHaveLength(2); diff --git a/apps/ai-game-creator-shell/tests/resourceReferences.test.ts b/apps/ai-game-creator-shell/tests/resourceReferences.test.ts index 1e7048cfc..90f987e9b 100644 --- a/apps/ai-game-creator-shell/tests/resourceReferences.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceReferences.test.ts @@ -96,11 +96,14 @@ describe('DirectProject user Response item', () => { it('展示用文本由 content 派生,引用按 @ 显示名展开', () => { expect( - directCodexContentToPromptText([ - { type: 'input_text', text: '用 ' }, - { type: 'agc_resource_reference', resourceId: 'asset-hero' }, - { type: 'input_text', text: ' 做主视觉' }, - ]), + directCodexContentToPromptText( + [ + { type: 'input_text', text: '用 ' }, + { type: 'agc_resource_reference', resourceId: 'asset-hero' }, + { type: 'input_text', text: ' 做主视觉' }, + ], + [], + ), ).toBe('用 @asset-hero 做主视觉'); }); }); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 6a444a5c2..8bb3685fe 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -8775,3 +8775,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 决策:DirectProject 本地上传不区分图片与其它文件。用户使用同一个文件入口,前端不按 `mediaType` 做图片判断,所有上传文件统一写成 `agc_attachment_reference`;项目资源 `@` 引用仍使用 `agc_resource_reference`。 - 原因:原 `agc_image_reference` 与附件 payload 完全相同,Rust wire 投影也把两者合并成同一段文本,不能代表真实的多模态图片输入。保留该 discriminator 只会制造错误语义。 - 边界:本次不新增 `input_image`,不保留图片类型兼容分支,不迁移旧历史;图片多模态能力未来单独设计独立 payload 与 wire 投影。 + +## 2026-09-17 DirectProject canonical content 的有效性只判整条 content + +- 决策:canonical user item 的有效输入判据只落在**整条 content** 上——只要有一段非空白文本、或任何一个非文本 part 就算有效输入;单个纯空白 `input_text`(段落分隔、软换行、chip 后的分隔空格)是合法 part。Rust `validate_direct_codex_user_item` 的 `content_has_meaningful_input` 与前端 `hasMeaningfulDirectCodexContent` 同口径,`wire.rs` 的「不能转换为空 prompt」只作兜底。 +- 决策:编辑器投影层(`ResourceReferenceInput` 的 `collectDraftParts`)原样透传编辑器节点:不做空白过滤,也不与相邻 part 合并。前端不替用户改写他输入的内容,canonical content 与编辑器内容逐字对应。 +- 决策:content → 可读文本只有 `directCodexContentToPromptText(content, assets)` 一个口径,`assets`(当前项目 manifest)必填:消息正文、队列 chip 文案、润色判据、草稿持久化与出站 prompt 全部由它派生,`agc_resource_reference` 按 `@显示名` 展开,只有素材已不在清单里时才回落 `resourceId`。 +- 原因:`3c7b02b9f` 为了让 content 通过「空 `input_text`」校验而在投影层丢空白 part,把引用后的段落分隔一起丢了(`@素材` 与下一段粘成一个词);`41366dd71` 又把消息 / 队列 / 快速编辑的文本派生切到这条投影上,缺陷扩散到界面与出站 prompt。 +- 边界:不新增 content part 类型,不迁移历史(历史 content 原样回放),不为旧口径保留兼容分支;前端仍不发整条全空白的一轮,app-server 输入里出现纯空白 text item 由本决定接受。 +- 验证:Rust `validation.rs` / `wire.rs` 用例「单个纯空白 part 通过校验、整条全空白拒绝」;AGC 侧 `resourceReferenceInput.test.tsx`、`resourceReferences.test.ts`、`appSurface/project-development.suite.ts`(Godot 回合)、`projectResourceLiveIntegration.test.tsx` 改为按逐字投影断言,`ai-game-creator-shell:typecheck` 与定向 vitest 通过。 +- 关联规范:`docs/project-memory/plans/【里程碑】DirectProject canonical content严格边界-2026-09-16.md`。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index a12abf8b1..6e19018f1 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -5630,5 +5630,6 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **现象**:快速编辑面板里 `@` 插了素材再点「修改」,出站 `derive_local_project_resource` 的 `prompt` 是 `把夜色改成星空@source-rules`,而界面 chip 与聊天输入区显示的是 `@rules`;仓库自带的 `projectResourceLiveIntegration` 用例因此长期是红的(`expected '把夜色改成星空@rules' to be '把夜色改成星空@source-rules'`)。同一根因还让排队消息 chip 显示 `@asset:…`。 - **原因**:`directCodexContentToPromptText(content, assets)` 的 `assets` 有默认值 `[]`,漏传不报错、只是把 `agc_resource_reference` 退化成 `${resourceId}`。`ResourceReferenceInput` 自己读草稿的四处都带了 `manifest.assets`,而它的三个消费方漏传:`chatComposerQueue.queuedChatTurnLabel`(宿主 `ComposerTurnQueue` 也没接素材清单)、`project-development/index.tsx` 的 `applyResourceQuickEditPrompt` 与 `applyResourceQuickEditDraft`;后两个的 `useCallback` 依赖里同样没有 `manifest.assets`,改完还会读到旧清单。 - **处理**:三个消费方全部补上素材清单并进依赖数组——`queuedChatTurnLabel(turn, assets)` + `ComposerTurnQueue` 新增 `assets` 属性(由 `ProjectSupervisorView` 传 `chatProjectAssets`)、快速编辑的两处改用 `manifest.assets`。改「比较用的草稿文本」与「落进面板的文本」必须同一个口径,否则 `replaceText` 会每次输入都重跑一遍。 +- **加固**:`directCodexContentToPromptText(content, assets)` 与 `queuedChatTurnLabel(turn, assets)` 的 `assets` 改为**必填**(删掉 `= []` 默认值),测试里刻意不传 manifest 的地方显式写 `[]`。理由:默认值把「漏传素材清单」从编译期错误降级成运行期文案退化,正是本条缺陷的入口;队列 chip 与消息正文从此共用同一个派生(`queuedChatTurnLabel` 只多做「压成单行 + 限长」)。 - **验证**:`apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx` 的「快速编辑提示词里能 @ 出资源选择器」由红转绿(该文件 29 passed);`tests/appSurface/chat-composer.suite.ts` 新增「队列 chip 的 @ 引用按 manifest 显示名展开」;`appSurface.test.ts` 467 passed、定向 51 passed、`npm run ai-game-creator-shell:typecheck`、`npm run check:encoding` 通过。 - **关联**:`apps/ai-game-creator-shell/src/features/project-workspace/chatComposerQueue.ts`、`ComposerControls.tsx`、`ProjectSupervisorView.tsx`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx`。