收敛 content→可读文本为单一口径并强制传 manifest(review 第 2、3 条)
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m10s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m43s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m28s
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m13s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m0s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / Frontend tests (pull_request) Failing after 4m24s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m2s
Project CI / Native shell tests (pull_request) Successful in 8m31s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m10s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m43s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m28s
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m13s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m0s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / Frontend tests (pull_request) Failing after 4m24s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m2s
Project CI / Native shell tests (pull_request) Successful in 8m31s
- directCodexContentToPromptText 的 assets 改为必填,去掉 = [] 默认值:漏传素材清单从运行期文案退化变回编译期错误 - queuedChatTurnLabel 的 assets 与 ComposerTurnQueue 的 assets 属性同样必填,队列 chip 与消息正文共用同一个派生(chip 只多做压单行与限长) - 断言改为显式传 [] 或 manifest:resourceReferences、chat-composer 用例按新签名收敛 - 更新 pitfalls 条目与 decision-log(canonical content 有效性只判整条 content、投影逐字透传、可读文本单一口径必填 assets)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)]),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 做主视觉');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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`。
|
||||
|
||||
Reference in New Issue
Block a user