From 51c4174b73b0e1d3e291d5d510e384914d00d712 Mon Sep 17 00:00:00 2001 From: Linghong Date: Wed, 16 Sep 2026 05:10:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AGC=20Rust=20shard=204?= =?UTF-8?q?=20=E7=9A=84=E4=B8=A4=E4=B8=AA=20CI=20=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - direct_registered_taonier_runtime_image_paths 通用平台图兜底补齐 canonical 切片排除:assets/art-spritesheet-slices/ 下的切片只经完整 manifest/receipt/内容校验放行,内容被篡改的切片不再绕过完成门禁 - direct_completion_scans_npm_scene_modules 测试改为保留 fixture 有效切片,不再依赖被修复的兜底漏洞 - mixed_project_history_rows 过时测试按 d48d09905 收紧后的契约改为断言 DirectProject 链对 legacy 行失败关闭 --- .../src-tauri/src/agent/direct_runtime/mod.rs | 14 ++++++++----- .../src/project/conversation/tests.rs | 21 +++++++------------ 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index 7f743d89b..f0e482126 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -2555,6 +2555,15 @@ fn direct_registered_taonier_runtime_image_paths(root: &Path) -> Vec { .generation_route .as_deref() .is_some_and(|route| route.starts_with("/api/external/v1/editor/")) + // Canonical slices are admitted only through + // `direct_registered_taonier_slice_paths` after the full slice + // manifest/receipt/content validation. This generic fallback + // must not re-admit a slice whose bytes no longer match the + // registered receipt. + && !(asset.kind == "art-spritesheet-slice" + && asset + .local_path + .starts_with("assets/art-spritesheet-slices/")) }) .filter_map(|asset| { let path = direct_normalized_project_asset_path(&asset.local_path)?; @@ -8220,11 +8229,6 @@ mod tests { "const player = new Image(); player.src = '/assets/art-spritesheet-slices/player.png';", ) .expect("scene"); - std::fs::write( - root.join("assets/art-spritesheet-slices/player.png"), - tiny_opaque_png(), - ) - .expect("slice"); assert_eq!( direct_game_sources_referenced_taonier_assets(&root), diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/conversation/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project/conversation/tests.rs index 9bad4a007..993c1c378 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/conversation/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/conversation/tests.rs @@ -272,7 +272,7 @@ fn project_history_skips_direct_project_rows_but_keeps_other_broken_rows_failing } #[test] -fn mixed_project_history_rows_stay_readable_from_both_sides() { +fn mixed_project_history_rows_read_by_generic_chain_but_fail_closed_for_direct_project() { let root = unique_conversation_test_root(); init_local_game_project_at(&root, "project-1", "写侧统一测试").expect("init project"); write_project_history(&root, &[DIRECT_PROJECT_ROW]); @@ -309,19 +309,12 @@ fn mixed_project_history_rows_stay_readable_from_both_sides() { vec!["模式切换后由通用写入器补写的回复", "带 id 的旧格式回复"] ); - // 反向:DirectProject 链把同一份文件里的两种行都读出来,混合文件不构成毒化。 - let direct_items = crate::agent::read_direct_project_history_items_at(&root) - .expect("DirectProject must keep reading the mixed history"); - assert_eq!( - direct_items - .iter() - .map(|item| item["content"][0]["text"].as_str().unwrap_or_default()) - .collect::>(), - vec![ - "再加一个按钮", - "模式切换后由通用写入器补写的回复", - "带 id 的旧格式回复", - ] + // 反向:DirectProject 链只接受 response_item 信封,混合文件里的 legacy 行让它失败关闭。 + let error = crate::agent::read_direct_project_history_items_at(&root) + .expect_err("DirectProject must fail closed on legacy rows"); + assert!( + error.starts_with("DirectProject 历史记录类型无效"), + "{error}" ); std::fs::remove_dir_all(root).ok(); -- 2.52.0 From c2a5a655e660070ef408b0dde92c97b13b78d9f3 Mon Sep 17 00:00:00 2001 From: Linghong Date: Wed, 16 Sep 2026 05:34:37 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20PR375=20=E7=9A=84=20Ru?= =?UTF-8?q?st=20=E5=A5=91=E7=BA=A6=E4=B8=8E=E8=B5=84=E6=BA=90=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐 Direct 回合事件新增字段的测试构造 将 JSON 资源恢复为文档预览分支 按当前工具条动作组逻辑更新分隔线回归测试 --- .../src-tauri/src/agent/direct_runtime/mod.rs | 2 + .../resourceCardPreviewModel.ts | 8 ++-- .../selectedLayerToolbarDividerDedupe.test.ts | 37 +++++-------------- 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index cb82aec15..ab733b185 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -5721,6 +5721,8 @@ mod tests { activity: None, accumulated_text: Some("partial".to_string()), tool_calls: None, + reasoning_text: None, + stream_items: None, updated_at: 42, }) .expect("serialize direct update"); diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceCardPreviewModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceCardPreviewModel.ts index c336f025b..c97057461 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceCardPreviewModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceCardPreviewModel.ts @@ -218,11 +218,11 @@ const markdownExtension = /\.(md|markdown|mdx)$/iu; * * 与 `resourceProjectionModel` 的 `gameCodeExtension` 是两份口径,刻意不复用: * 那份用于**筛选与归属**,改动会波及画布栏目与计数;这份只决定**卡面怎么画**。 - * 这里按用户口径把 `.json` / `.yaml` / `.toml` / `.xml` / `.html` / `.css` / `.sql` - * 一并算代码 —— 它们在上游被登记成「文档」,但卡面当代码画更干净。 + * 这里按用户口径把 `.yaml` / `.toml` / `.xml` / `.html` / `.css` / `.sql` + * 一并算代码;JSON 规格属于文档预览。 */ const cardCodeExtension = - /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|rs|py|go|java|kt|kts|cs|cpp|cc|cxx|c|h|hpp|swift|php|rb|lua|sh|bash|zsh|ps1|psm1|json|jsonc|ya?ml|toml|xml|html?|css|scss|less|sql|graphql|gql|vue|svelte)$/iu; + /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|rs|py|go|java|kt|kts|cs|cpp|cc|cxx|c|h|hpp|swift|php|rb|lua|sh|bash|zsh|ps1|psm1|ya?ml|toml|xml|html?|css|scss|less|sql|graphql|gql|vue|svelte)$/iu; /** 纯文本:按纯文本处理,显示前几行但不做标记清理。 */ const plainTextExtension = /\.(txt|text|csv|tsv|log|ini|conf|cfg|properties|env)$/iu; @@ -256,7 +256,7 @@ export function projectResourceCardPreviewKind( return 'document'; } // Markdown / 代码在扩展名这一层就分流,不再依赖上游登记类型: - // 上游把 `.json` / `.tsx` 这类文件登记成「文档」,卡面却该按代码画。 + // 上游把 JSON 规格登记成「文档」,卡面按文档预览;代码文件按扩展名分流。 if (markdownExtension.test(resource.path)) { return 'document'; } diff --git a/apps/ai-game-creator-shell/tests/selectedLayerToolbarDividerDedupe.test.ts b/apps/ai-game-creator-shell/tests/selectedLayerToolbarDividerDedupe.test.ts index feca4d761..6a6d33315 100644 --- a/apps/ai-game-creator-shell/tests/selectedLayerToolbarDividerDedupe.test.ts +++ b/apps/ai-game-creator-shell/tests/selectedLayerToolbarDividerDedupe.test.ts @@ -3,21 +3,17 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; /** - * 选中资源工具条上的「两条分隔线」回归钉子。 + * 选中资源工具条上的分隔线分组回归钉子。 * * 成因:共享工具条 `ImageCanvasSelectedLayerToolbarView.tsx` 会在 `showQuickEdit` 时于 * 「快速编辑」之后输出一条分隔线,同时为 `extraActions` 自动生成一条前置分隔线 * (`const extraActionDivider = extraActions ? … : null`)。AGC 资源卡是"快速编辑可用 + * 中间那些动作未接通不渲染"的组合,两条于是直接相邻,用户看到两条竖线。 * - * 修法是样式层去重(相邻的两条只显示一条)。这条用例钉住"规则确实存在",避免以后被 - * 顺手删掉又回到两条线;同时钉住成因——若哪天共享工具条不再自动生成前置分隔线, - * 这条断言会失败并提醒重新评估去重规则是否还需要。 + * 当前实现直接按动作组是否存在输出分隔线,避免相邻分隔线产生。这条用例钉住当前 + * 条件分组,避免后续重构时再次出现空动作组之间的重复分隔线。 */ describe('选中资源工具条的分隔线去重', () => { - const repoRootCss = () => - readFileSync(new URL('../../../src/index.css', import.meta.url), 'utf8'); - const sharedToolbarSource = () => readFileSync( new URL( @@ -27,30 +23,15 @@ describe('选中资源工具条的分隔线去重', () => { 'utf8', ); - it('相邻的两条分隔线只显示一条', () => { - const css = repoRootCss().replace(/\s+/g, ' '); - expect(css).toContain( - '.image-canvas-editor__floating-toolbar-divider + .image-canvas-editor__floating-toolbar-divider { display: none; }', - ); - }); - - it('共享工具条确实会为 extraActions 自动生成前置分隔线(去重规则的成因)', () => { - expect(sharedToolbarSource()).toContain( - 'const extraActionDivider = extraActions ?', - ); - }); - - it('「快速编辑」之后那条分隔线仍由共享工具条自己输出(去重的另一端)', () => { + it('分隔线由动作组条件控制,不依赖相邻分隔线的 CSS 去重', () => { const source = sharedToolbarSource(); expect(source).toContain('const showQuickEdit = isActionSupported('); - // 快速编辑按钮与它后面那条分隔线必须同时存在于 showQuickEdit 分支里。 - const quickEditBlock = source.slice( - source.indexOf('{showQuickEdit ? ('), - source.indexOf('{canRasterEdit && isActionSupported('), + expect(source).toContain('const hasEditingActions ='); + expect(source).toContain( + '{showQuickEdit && hasEditingActions ? divider : null}', ); - expect(quickEditBlock).toContain('label="快速编辑"'); - expect(quickEditBlock).toContain( - 'image-canvas-editor__floating-toolbar-divider', + expect(source).toContain( + '{(showQuickEdit || hasEditingActions || canRedraw || hasExtraActions) &&', ); }); }); -- 2.52.0 From c1e3e3bf8df558e3d33ad7c1fb856490459fb4b0 Mon Sep 17 00:00:00 2001 From: Linghong Date: Wed, 16 Sep 2026 05:38:11 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E7=BB=9F=E4=B8=80=20Direct=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E8=B0=83=E7=94=A8=E6=97=B6=E9=97=B4=E6=88=B3=E4=B8=BA?= =?UTF-8?q?=E6=AF=AB=E7=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在前端模型入口兼容 Unix 秒时间戳并统一为毫秒 保持耗时展示使用秒和分钟 补充时间戳归一化回归测试 --- apps/ai-game-creator-shell/src/App.tsx | 3 +++ .../features/project-workspace/directTurnPresentation.ts | 5 ++++- .../tests/directTurnPresentation.test.ts | 7 +++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 2438d17f8..ee075cfa5 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -244,6 +244,7 @@ import { type DirectThreadSubscriptionBootstrap, isDirectTurnInProgress, } from './features/project-workspace/directThreadEvents'; +import { normalizeDirectTimestamp } from './features/project-workspace/directTurnPresentation'; import type { DirectCodexUserContentPart } from './features/project-workspace/generated'; import { appendMemoryContent, @@ -1010,6 +1011,8 @@ export function App({ const normalized: GameCreatorDirectToolCall = { ...call, id, + startedAt: normalizeDirectTimestamp(call.startedAt), + updatedAt: normalizeDirectTimestamp(call.updatedAt), detail: call.detail ?? { changes: [] }, }; // 起点时间取更早的那个:`completed` 事件不一定带 startedAt。 diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/directTurnPresentation.ts b/apps/ai-game-creator-shell/src/features/project-workspace/directTurnPresentation.ts index 217646a9f..2449331d5 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/directTurnPresentation.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/directTurnPresentation.ts @@ -26,7 +26,8 @@ export type DirectTurnPresentation = { endedAt: number; }; -export function directMessageTimestamp(value: number | undefined) { +/** 统一 Direct 时间戳为 Unix 毫秒;历史/异常 payload 可能传 Unix 秒。 */ +export function normalizeDirectTimestamp(value: number | undefined) { const timestamp = value && Number.isFinite(value) && value > 0 ? value < 100_000_000_000 @@ -36,6 +37,8 @@ export function directMessageTimestamp(value: number | undefined) { return Number.isFinite(new Date(timestamp).getTime()) ? timestamp : 0; } +export const directMessageTimestamp = normalizeDirectTimestamp; + /** 从唯一呈现来源分区,完成后只把最终回复及失败提示留在过程折叠区之外。 */ export function splitDirectTurnContent(turn: DirectTurnPresentation) { const assistants = turn.messages.filter( diff --git a/apps/ai-game-creator-shell/tests/directTurnPresentation.test.ts b/apps/ai-game-creator-shell/tests/directTurnPresentation.test.ts index 74d8c8426..f47f263a2 100644 --- a/apps/ai-game-creator-shell/tests/directTurnPresentation.test.ts +++ b/apps/ai-game-creator-shell/tests/directTurnPresentation.test.ts @@ -8,6 +8,7 @@ import type { import { buildDirectTurnPresentations, directMessageTimestamp, + normalizeDirectTimestamp, splitDirectTurnContent, } from '../src/features/project-workspace/directTurnPresentation'; @@ -58,6 +59,12 @@ const build = ( }); describe('DirectProject 回合唯一呈现', () => { + it('把 Unix 秒时间戳归一化为毫秒,已有毫秒值保持不变', () => { + expect(normalizeDirectTimestamp(1_800_000_000)).toBe(1_800_000_000_000); + expect(normalizeDirectTimestamp(1_800_000_000_000)).toBe(1_800_000_000_000); + expect(directMessageTimestamp(1_800_000_000)).toBe(1_800_000_000_000); + }); + it('历史仍有未加载切片时,不把那些回合的工具流追加到当前页末尾', () => { const rows = build( [user('one')], -- 2.52.0