54d0fb75ea
新增 GDExtension 自动引导、GDScript 执行和实例隔离缓存 接入 AGC 插件开关、Runner、Agent 工具与权限审计 完善执行回执确认、不确定状态阻断及卸载恢复 补齐 Windows 分发资源、定向测试与实机验收文档
1084 lines
34 KiB
TypeScript
1084 lines
34 KiB
TypeScript
import type { GameCreationAppAssetKind } from './generated/GameCreationAppAssetKind';
|
||
|
||
export type { GameCreationAppAssetKind } from './generated/GameCreationAppAssetKind';
|
||
|
||
export const GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION =
|
||
'game-creation-app.manifest.v1';
|
||
export const GAME_CREATION_AGENT_RUN_SCHEMA_VERSION =
|
||
'game-creator-agent-run.v1';
|
||
export const GAME_CREATION_AGENT_RUN_MAX_PASSES = 3;
|
||
export const GAME_CREATION_AGENT_TOOL_CALL_MAX = 128;
|
||
|
||
export type GameCreationAppPermission = 'auto' | 'confirm' | 'deny';
|
||
|
||
export interface GameCreationAppCommandDescriptor {
|
||
id: string;
|
||
permission: GameCreationAppPermission;
|
||
}
|
||
|
||
export const GAME_CREATION_APP_COMMANDS = [
|
||
{ id: 'help.show', permission: 'auto' },
|
||
{ id: 'project.create', permission: 'confirm' },
|
||
{ id: 'project.rename', permission: 'confirm' },
|
||
{ id: 'project.status', permission: 'auto' },
|
||
{ id: 'project.index', permission: 'auto' },
|
||
{ id: 'project.checkpoint', permission: 'confirm' },
|
||
{ id: 'project.diff', permission: 'auto' },
|
||
{ id: 'project.git_inspect', permission: 'auto' },
|
||
{ id: 'project.git_commit', permission: 'confirm' },
|
||
{ id: 'project.patchset', permission: 'confirm' },
|
||
{ id: 'project.restore', permission: 'confirm' },
|
||
{ id: 'project.verify', permission: 'confirm' },
|
||
{ id: 'project.bootstrap', permission: 'confirm' },
|
||
{ id: 'project.export_package', permission: 'confirm' },
|
||
{ id: 'project.export_list', permission: 'auto' },
|
||
{ id: 'project.policy_read', permission: 'auto' },
|
||
{ id: 'project.policy_write', permission: 'confirm' },
|
||
{ id: 'task.list', permission: 'auto' },
|
||
{ id: 'task.create', permission: 'confirm' },
|
||
{ id: 'task.update', permission: 'confirm' },
|
||
{ id: 'agent.trace_read', permission: 'auto' },
|
||
{ id: 'agent.run_status', permission: 'auto' },
|
||
{ id: 'agent.compact', permission: 'auto' },
|
||
{ id: 'agent.kill', permission: 'confirm' },
|
||
{ id: 'agent.retry', permission: 'confirm' },
|
||
{ id: 'agent.resume', permission: 'confirm' },
|
||
{ id: 'agent.delegate', permission: 'confirm' },
|
||
{ id: 'agent.spawn_isolated', permission: 'confirm' },
|
||
{ id: 'agent.schedule_ready', permission: 'confirm' },
|
||
{ id: 'agent.capabilities', permission: 'auto' },
|
||
{ id: 'agent.audit', permission: 'auto' },
|
||
{ id: 'llm.config_check', permission: 'auto' },
|
||
{ id: 'mcp.call', permission: 'auto' },
|
||
{ id: 'game.generate_draft', permission: 'confirm' },
|
||
{ id: 'game.run_local', permission: 'confirm' },
|
||
{ id: 'file.list', permission: 'auto' },
|
||
{ id: 'file.read', permission: 'auto' },
|
||
{ id: 'file.write', permission: 'confirm' },
|
||
{ id: 'file.delete', permission: 'confirm' },
|
||
{ id: 'asset.list', permission: 'auto' },
|
||
{ id: 'asset.upload', permission: 'confirm' },
|
||
{ id: 'asset.register', permission: 'confirm' },
|
||
{ id: 'image.inspect', permission: 'auto' },
|
||
{ id: 'preview.start', permission: 'confirm' },
|
||
{ id: 'preview.validate', permission: 'auto' },
|
||
{ id: 'preview.open', permission: 'confirm' },
|
||
{ id: 'preview.stop', permission: 'auto' },
|
||
{ id: 'preview.status', permission: 'auto' },
|
||
{ id: 'command.run_limited', permission: 'confirm' },
|
||
{ id: 'command.exec', permission: 'confirm' },
|
||
{ id: 'command.output_read', permission: 'auto' },
|
||
{ id: 'command.start', permission: 'confirm' },
|
||
{ id: 'command.poll', permission: 'auto' },
|
||
{ id: 'command.stdin', permission: 'confirm' },
|
||
{ id: 'command.terminate', permission: 'confirm' },
|
||
{ id: 'cocos.editor.execute', permission: 'confirm' },
|
||
{ id: 'godot.editor.execute', permission: 'confirm' },
|
||
{ id: 'canvas.project_open', permission: 'confirm' },
|
||
{ id: 'canvas.project_sync', permission: 'confirm' },
|
||
{ id: 'canvas.asset_import', permission: 'confirm' },
|
||
{ id: 'canvas.asset_generate', permission: 'confirm' },
|
||
{ id: 'canvas.export_import', permission: 'confirm' },
|
||
{ id: 'memory.read', permission: 'auto' },
|
||
{ id: 'memory.write', permission: 'confirm' },
|
||
{ id: 'memory.delete', permission: 'confirm' },
|
||
{ id: 'conversation.read', permission: 'auto' },
|
||
{ id: 'conversation.write', permission: 'auto' },
|
||
] as const satisfies readonly GameCreationAppCommandDescriptor[];
|
||
|
||
export interface GameCreationAgentCapabilityDescriptor {
|
||
id: string;
|
||
area: 'user' | 'agent-runtime' | 'local-runtime' | 'dev-runtime';
|
||
title: string;
|
||
platforms?: readonly ('linux' | 'macos' | 'windows')[];
|
||
}
|
||
|
||
export const GAME_CREATION_AGENT_CAPABILITIES = [
|
||
{ id: 'chat', area: 'user', title: '聊天入口' },
|
||
{ id: 'project-supervisor', area: 'agent-runtime', title: '项目总控 Agent' },
|
||
{
|
||
id: 'persistent-user-input',
|
||
area: 'agent-runtime',
|
||
title: '持久用户澄清请求',
|
||
},
|
||
{ id: 'file-upload', area: 'user', title: '上传文件' },
|
||
{ id: 'built-in-commands', area: 'agent-runtime', title: '内置命令调用' },
|
||
{ id: 'llm-draft-generation', area: 'agent-runtime', title: 'LLM 草案生成' },
|
||
{
|
||
id: 'provider-web-search',
|
||
area: 'agent-runtime',
|
||
title: 'Provider 原生联网检索',
|
||
},
|
||
{
|
||
id: 'context-compaction',
|
||
area: 'agent-runtime',
|
||
title: '持久上下文压缩',
|
||
},
|
||
{
|
||
id: 'mcp-tools',
|
||
area: 'agent-runtime',
|
||
title: 'Runner 托管的 MCP 动态工具',
|
||
},
|
||
{ id: 'task-decomposition', area: 'agent-runtime', title: '任务拆分' },
|
||
{ id: 'orchestration', area: 'agent-runtime', title: '任务编排' },
|
||
{
|
||
id: 'agent-loop',
|
||
area: 'agent-runtime',
|
||
title: 'Planner / Generator / Evaluator 循环',
|
||
},
|
||
{
|
||
id: 'task-graph-agenda',
|
||
area: 'agent-runtime',
|
||
title: '任务图议程',
|
||
},
|
||
{
|
||
id: 'evaluator-repair-routing',
|
||
area: 'agent-runtime',
|
||
title: 'Evaluator 返工路由',
|
||
},
|
||
{
|
||
id: 'tool-call-budget',
|
||
area: 'agent-runtime',
|
||
title: '工具调用预算',
|
||
},
|
||
{
|
||
id: 'multi-agent-collaboration',
|
||
area: 'agent-runtime',
|
||
title: '多智能体协作',
|
||
},
|
||
{
|
||
id: 'role-level-collaboration',
|
||
area: 'agent-runtime',
|
||
title: '组内角色协作',
|
||
},
|
||
{
|
||
id: 'isolated-subagents',
|
||
area: 'agent-runtime',
|
||
title: '动态隔离子 Agent',
|
||
},
|
||
{ id: 'quality-review', area: 'agent-runtime', title: '质量评审' },
|
||
{ id: 'run-lifecycle', area: 'agent-runtime', title: 'Run 生命周期控制' },
|
||
{
|
||
id: 'repair-loop-carryover',
|
||
area: 'agent-runtime',
|
||
title: '返工范围与 Carry-over',
|
||
},
|
||
{ id: 'short-term-memory', area: 'agent-runtime', title: '短期记忆' },
|
||
{ id: 'long-term-memory', area: 'agent-runtime', title: '长期记忆' },
|
||
{
|
||
id: 'conversation-history',
|
||
area: 'agent-runtime',
|
||
title: '对话记录上下文',
|
||
},
|
||
{ id: 'local-artifacts', area: 'local-runtime', title: '本地产物保存' },
|
||
{ id: 'project-checkpoints', area: 'local-runtime', title: '项目快照与恢复' },
|
||
{ id: 'project-index', area: 'local-runtime', title: '本地项目索引' },
|
||
{
|
||
id: 'repository-startup-context',
|
||
area: 'local-runtime',
|
||
title: '仓库启动上下文',
|
||
},
|
||
{
|
||
id: 'persistent-process-sessions',
|
||
area: 'local-runtime',
|
||
title: 'Runner 托管的持久进程会话',
|
||
},
|
||
{ id: 'local-preview', area: 'local-runtime', title: '本地 HTTP 预览' },
|
||
{
|
||
id: 'browser-validation',
|
||
area: 'local-runtime',
|
||
title: '浏览器试玩验证',
|
||
},
|
||
{
|
||
id: 'visual-inspection',
|
||
area: 'local-runtime',
|
||
title: '模型视觉检查',
|
||
},
|
||
{
|
||
id: 'canvas-project-sync',
|
||
area: 'local-runtime',
|
||
title: '画板项目资源同步',
|
||
},
|
||
{ id: 'developer-window', area: 'dev-runtime', title: '开发窗口' },
|
||
{ id: 'persistent-runner', area: 'dev-runtime', title: '独立持久 Runner' },
|
||
{
|
||
id: 'command-exec',
|
||
area: 'dev-runtime',
|
||
title: '结构化项目命令执行(Linux 工作区沙箱)',
|
||
},
|
||
{
|
||
id: 'os-workspace-sandbox',
|
||
area: 'dev-runtime',
|
||
title: 'Linux OS 强制工作区沙箱',
|
||
platforms: ['linux'],
|
||
},
|
||
{
|
||
id: 'command-output-read',
|
||
area: 'dev-runtime',
|
||
title: '命令输出分页回查',
|
||
},
|
||
{ id: 'guardrails', area: 'dev-runtime', title: '权限 Gate' },
|
||
{ id: 'project-policy', area: 'dev-runtime', title: '项目级权限策略' },
|
||
{ id: 'trace-log', area: 'dev-runtime', title: '执行日志' },
|
||
] as const satisfies readonly GameCreationAgentCapabilityDescriptor[];
|
||
|
||
export interface GameCreationAppLimitedRunCommandDescriptor {
|
||
id: string;
|
||
title: string;
|
||
}
|
||
|
||
export const GAME_CREATION_APP_LIMITED_RUN_COMMANDS = [
|
||
{ id: 'game.static_smoke', title: '静态入口自检' },
|
||
] as const satisfies readonly GameCreationAppLimitedRunCommandDescriptor[];
|
||
|
||
export type GameCreationAppAgentGroup =
|
||
| 'design'
|
||
| 'art'
|
||
| 'code'
|
||
| 'balance'
|
||
| 'audio'
|
||
| 'publishing';
|
||
|
||
export type GameCreationAppTaskStatus =
|
||
| 'pending'
|
||
| 'running'
|
||
| 'waiting-for-confirmation'
|
||
| 'completed'
|
||
| 'failed';
|
||
|
||
export interface GameCreationAppTaskState {
|
||
id: string;
|
||
title: string;
|
||
group: GameCreationAppAgentGroup;
|
||
role: string;
|
||
status: GameCreationAppTaskStatus;
|
||
dependencies: string[];
|
||
artifacts: string[];
|
||
acceptanceCriteria: string[];
|
||
}
|
||
|
||
export const GAME_CREATION_APP_SEED_TASKS = [
|
||
{
|
||
id: 'design-director',
|
||
title: '拆解创作方向',
|
||
group: 'design',
|
||
role: 'Director',
|
||
status: 'pending',
|
||
dependencies: [],
|
||
artifacts: ['.agent/spec.md'],
|
||
acceptanceCriteria: ['创作目标、范围和专业组分工明确'],
|
||
},
|
||
{
|
||
id: 'art-director',
|
||
title: '确定视觉方向与规范图',
|
||
group: 'art',
|
||
role: 'Director',
|
||
status: 'pending',
|
||
dependencies: [],
|
||
artifacts: [
|
||
'.agent/passes/pass-*/groups/art/director.md',
|
||
'assets/art-spec.png',
|
||
],
|
||
acceptanceCriteria: [
|
||
'角色、场景和 UI 的统一视觉方向明确,且已生成可复用的 1:1 图标规范图',
|
||
],
|
||
},
|
||
{
|
||
id: 'design-foundation',
|
||
title: '确定玩法规格与界面原型',
|
||
group: 'design',
|
||
role: 'Gameplay',
|
||
status: 'pending',
|
||
dependencies: ['design-director', 'art-director'],
|
||
artifacts: [
|
||
'memory/project.md',
|
||
'game/game_design.md',
|
||
'assets/ui-prototype.png',
|
||
],
|
||
acceptanceCriteria: [
|
||
'核心循环、胜负条件和第一版关卡目标明确,且已基于规范图生成可读的 16:9 横屏界面原型图',
|
||
],
|
||
},
|
||
{
|
||
id: 'balance-director',
|
||
title: '确定数值口径',
|
||
group: 'balance',
|
||
role: 'Director',
|
||
status: 'pending',
|
||
dependencies: ['design-foundation'],
|
||
artifacts: ['.agent/passes/pass-*/groups/balance/director.md'],
|
||
acceptanceCriteria: ['难度、节奏和得分口径可指导数值表'],
|
||
},
|
||
{
|
||
id: 'balance-seed',
|
||
title: '生成初版数值',
|
||
group: 'balance',
|
||
role: 'Difficulty',
|
||
status: 'pending',
|
||
dependencies: ['balance-director'],
|
||
artifacts: ['game/balance.json'],
|
||
acceptanceCriteria: ['速度、生命、得分和难度参数可被程序组读取'],
|
||
},
|
||
{
|
||
id: 'art-asset-plan',
|
||
title: '生成首版美术素材',
|
||
group: 'art',
|
||
role: 'Asset',
|
||
status: 'pending',
|
||
dependencies: ['art-director', 'design-foundation'],
|
||
artifacts: ['assets/manifest.art.json', 'assets/art-spritesheet.png'],
|
||
acceptanceCriteria: [
|
||
'角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记',
|
||
],
|
||
},
|
||
{
|
||
id: 'art-polish',
|
||
title: '检查美术可用性',
|
||
group: 'art',
|
||
role: 'Polish',
|
||
status: 'pending',
|
||
dependencies: ['art-asset-plan'],
|
||
artifacts: ['.agent/passes/pass-*/groups/art/polish.md'],
|
||
acceptanceCriteria: ['首版资产不阻塞可玩原型和后续画板精修'],
|
||
},
|
||
{
|
||
id: 'audio-director',
|
||
title: '确定声音方向',
|
||
group: 'audio',
|
||
role: 'Director',
|
||
status: 'pending',
|
||
dependencies: ['design-foundation'],
|
||
artifacts: ['.agent/passes/pass-*/groups/audio/director.md'],
|
||
acceptanceCriteria: ['BGM 氛围和交互音效边界明确'],
|
||
},
|
||
{
|
||
id: 'audio-asset-plan',
|
||
title: '规划音乐音效',
|
||
group: 'audio',
|
||
role: 'SFX',
|
||
status: 'pending',
|
||
dependencies: ['audio-director'],
|
||
artifacts: ['assets/manifest.audio.json'],
|
||
acceptanceCriteria: ['BGM 和核心交互音效需求已明确'],
|
||
},
|
||
{
|
||
id: 'code-director',
|
||
title: '拆解程序实现',
|
||
group: 'code',
|
||
role: 'Director',
|
||
status: 'pending',
|
||
dependencies: [],
|
||
artifacts: ['.agent/passes/pass-*/groups/code/director.md'],
|
||
acceptanceCriteria: ['渲染、输入、状态和数据读取边界明确'],
|
||
},
|
||
{
|
||
id: 'code-prototype',
|
||
title: '生成可运行原型',
|
||
group: 'code',
|
||
role: 'Code',
|
||
status: 'pending',
|
||
dependencies: [
|
||
'code-director',
|
||
'balance-seed',
|
||
'art-polish',
|
||
'audio-asset-plan',
|
||
],
|
||
artifacts: ['game/'],
|
||
acceptanceCriteria: ['本地 Web 游戏项目可以通过 HTTP server 打开'],
|
||
},
|
||
{
|
||
id: 'quality-review',
|
||
title: '执行质量评审',
|
||
group: 'code',
|
||
role: 'Review',
|
||
status: 'pending',
|
||
dependencies: ['code-prototype'],
|
||
artifacts: ['.agent/findings.md', '.agent/run.latest.json'],
|
||
acceptanceCriteria: [
|
||
'玩法、资产、数值、程序和发布包装通过跨专业组质量评审',
|
||
],
|
||
},
|
||
{
|
||
id: 'preview-readiness',
|
||
title: '执行静态自检',
|
||
group: 'code',
|
||
role: 'Preview',
|
||
status: 'pending',
|
||
dependencies: ['quality-review'],
|
||
artifacts: ['.agent/logs/command.log', '.agent/run.latest.json'],
|
||
acceptanceCriteria: ['HTML 自包含且通过本地静态 smoke'],
|
||
},
|
||
{
|
||
id: 'preview-playtest',
|
||
title: '预览并试玩验收',
|
||
group: 'code',
|
||
role: 'Playtest',
|
||
status: 'pending',
|
||
dependencies: ['preview-readiness'],
|
||
artifacts: ['.agent/logs/preview.log'],
|
||
acceptanceCriteria: ['预览不是空白页,主循环和基础输入可用'],
|
||
},
|
||
{
|
||
id: 'publish-strategy',
|
||
title: '整理运营定位',
|
||
group: 'publishing',
|
||
role: 'Director',
|
||
status: 'pending',
|
||
dependencies: ['preview-playtest'],
|
||
artifacts: ['.agent/passes/pass-*/groups/publishing/director.md'],
|
||
acceptanceCriteria: ['标题、卖点、标签和封面方向明确'],
|
||
},
|
||
{
|
||
id: 'publish-package',
|
||
title: '整理发布包装',
|
||
group: 'publishing',
|
||
role: 'Publish',
|
||
status: 'pending',
|
||
dependencies: ['publish-strategy'],
|
||
artifacts: ['exports/README.md'],
|
||
acceptanceCriteria: ['标题、简介、标签、封面需求和导出检查已完成'],
|
||
},
|
||
] satisfies readonly GameCreationAppTaskState[];
|
||
|
||
export function createGameCreationAppSeedTasks(): GameCreationAppTaskState[] {
|
||
return GAME_CREATION_APP_SEED_TASKS.map((task) => ({
|
||
...task,
|
||
dependencies: [...task.dependencies],
|
||
artifacts: [...task.artifacts],
|
||
acceptanceCriteria: [...task.acceptanceCriteria],
|
||
}));
|
||
}
|
||
|
||
export type GameCreationAppAssetSourceKind =
|
||
| 'uploaded'
|
||
| 'generated'
|
||
| 'canvas';
|
||
|
||
export interface GameCreationAppAssetSource {
|
||
kind: GameCreationAppAssetSourceKind;
|
||
canvasProjectId?: string | null;
|
||
resourceId?: string | null;
|
||
assetObjectId?: string | null;
|
||
taskId?: string | null;
|
||
prompt?: string | null;
|
||
model?: string | null;
|
||
generationRoute?: string | null;
|
||
generationKind?: string | null;
|
||
referenceResourceIds?: string[];
|
||
}
|
||
|
||
export interface GameCreationAppAssetManifestEntry {
|
||
id: string;
|
||
kind: GameCreationAppAssetKind;
|
||
mediaType: string;
|
||
localPath: string;
|
||
source: GameCreationAppAssetSource;
|
||
imageSequenceFrames?: Array<{
|
||
imageSrc: string;
|
||
objectKey?: string | null;
|
||
assetObjectId?: string | null;
|
||
width: number;
|
||
height: number;
|
||
}> | null;
|
||
imageSequenceDurationMs?: number | null;
|
||
category?: GameCreationAppAssetCategory;
|
||
tags?: string[];
|
||
}
|
||
|
||
/**
|
||
* UI 设计图(原型图与页面设计图)资产的唯一 kind。
|
||
*
|
||
* 常量类型钉在生成的 kind union 上:Rust 侧改名/改词表 → 生成 union 变化 → 这里直接编译报错,
|
||
* 不存在"手写常量与契约悄悄分叉"的可能。
|
||
*/
|
||
export const GAME_CREATION_APP_UI_DESIGN_ASSET_KIND: GameCreationAppAssetKind =
|
||
'ui-design';
|
||
|
||
/** UI Editor JSON 文档资产的唯一 kind 与媒体类型。 */
|
||
export const GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND: GameCreationAppAssetKind =
|
||
'ui-design-doc';
|
||
export const GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE =
|
||
'application/json' as const;
|
||
|
||
/**
|
||
* 该 manifest 资产是否是 UI 编辑器文档:`kind` + `mediaType` 双条件是唯一定义。
|
||
*
|
||
* 判据原本散在三处(资源画布入口、UI 编辑器桥接、资源引用入口),任一处改了 kind /
|
||
* mediaType 口径就会出现「能引用但打不开」这类分叉,所以只保留这一份实现。
|
||
* `kind` 走严格解析:认不出的值收口成 `unknown`,不等于文档 kind。
|
||
*/
|
||
export function isGameCreationAppUiDesignDocAsset(
|
||
asset: Pick<GameCreationAppAssetManifestEntry, 'kind' | 'mediaType'>,
|
||
): boolean {
|
||
return (
|
||
parseGameCreationAppAssetKind(asset.kind, 'ui-design-doc.asset') ===
|
||
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND &&
|
||
asset.mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 运行期 kind 列表:穷举 Record 保证它不可能与生成 union 分叉(漏一个/多一个都编译报错)。
|
||
* 键顺序即生成声明顺序。kind 字符串表只有这一份运行期副本;输入解析只接受 canonical 值。
|
||
*/
|
||
const GAME_CREATION_APP_ASSET_KIND_MEMBERS: Record<
|
||
GameCreationAppAssetKind,
|
||
true
|
||
> = {
|
||
image: true,
|
||
scene: true,
|
||
character: true,
|
||
'character-animation': true,
|
||
icon: true,
|
||
'icon-spritesheet': true,
|
||
'icon-spec': true,
|
||
'ui-design': true,
|
||
'ui-design-doc': true,
|
||
'publication-material': true,
|
||
spec: true,
|
||
video: true,
|
||
audio: true,
|
||
'sound-effect': true,
|
||
'background-music': true,
|
||
font: true,
|
||
document: true,
|
||
code: true,
|
||
unknown: true,
|
||
};
|
||
|
||
export const GAME_CREATION_APP_ASSET_KINDS: readonly GameCreationAppAssetKind[] =
|
||
Object.keys(
|
||
GAME_CREATION_APP_ASSET_KIND_MEMBERS,
|
||
) as GameCreationAppAssetKind[];
|
||
|
||
const GAME_CREATION_APP_AUDIO_ASSET_KIND_MEMBERS: Record<
|
||
Extract<
|
||
GameCreationAppAssetKind,
|
||
'audio' | 'sound-effect' | 'background-music'
|
||
>,
|
||
true
|
||
> = {
|
||
audio: true,
|
||
'sound-effect': true,
|
||
'background-music': true,
|
||
};
|
||
|
||
const GAME_CREATION_APP_VISUAL_ASSET_KIND_MEMBERS: Record<
|
||
Extract<
|
||
GameCreationAppAssetKind,
|
||
| 'image'
|
||
| 'scene'
|
||
| 'character'
|
||
| 'character-animation'
|
||
| 'icon'
|
||
| 'icon-spritesheet'
|
||
| 'icon-spec'
|
||
| 'ui-design'
|
||
>,
|
||
true
|
||
> = {
|
||
image: true,
|
||
scene: true,
|
||
character: true,
|
||
'character-animation': true,
|
||
icon: true,
|
||
'icon-spritesheet': true,
|
||
'icon-spec': true,
|
||
'ui-design': true,
|
||
};
|
||
|
||
export function isGameCreationAppAssetAudioKind(
|
||
kind: GameCreationAppAssetKind,
|
||
): boolean {
|
||
return kind in GAME_CREATION_APP_AUDIO_ASSET_KIND_MEMBERS;
|
||
}
|
||
|
||
export function isGameCreationAppAssetVisualKind(
|
||
kind: GameCreationAppAssetKind,
|
||
): boolean {
|
||
return kind in GAME_CREATION_APP_VISUAL_ASSET_KIND_MEMBERS;
|
||
}
|
||
|
||
const GAME_CREATION_APP_ASSET_KIND_SET = new Set<string>(
|
||
GAME_CREATION_APP_ASSET_KINDS,
|
||
);
|
||
|
||
export function isGameCreationAppAssetKind(
|
||
raw: unknown,
|
||
): raw is GameCreationAppAssetKind {
|
||
return typeof raw === 'string' && GAME_CREATION_APP_ASSET_KIND_SET.has(raw);
|
||
}
|
||
|
||
/**
|
||
* 严格解析外部 kind 字符串(manifest / API):只认 canonical 值,认不出的收口成 `unknown`
|
||
* 并把**原始串**打到 `console.warn`(Rust 侧同类留痕走 `app_log!`)。
|
||
*
|
||
* 只接受精确的 canonical 值,其他输入一律收口为 `unknown`。
|
||
*/
|
||
export function parseGameCreationAppAssetKind(
|
||
raw: unknown,
|
||
context: string,
|
||
): GameCreationAppAssetKind {
|
||
if (isGameCreationAppAssetKind(raw)) {
|
||
return raw;
|
||
}
|
||
|
||
if (typeof raw !== 'string') {
|
||
console.warn('GameCreationApp 资源 kind 不是字符串,已收口为 unknown', {
|
||
rawType: typeof raw,
|
||
sourceContext: context,
|
||
});
|
||
return 'unknown';
|
||
}
|
||
|
||
if (raw !== 'unknown') {
|
||
console.warn('未知的 GameCreationApp 资源 kind,已收口为 unknown', {
|
||
rawKind: raw,
|
||
sourceContext: context,
|
||
});
|
||
}
|
||
return 'unknown';
|
||
}
|
||
|
||
export const GAME_CREATION_APP_ASSET_CATEGORIES = [
|
||
'ui-interaction',
|
||
'character',
|
||
'scene',
|
||
'audio',
|
||
'document',
|
||
'unclassified',
|
||
] as const;
|
||
|
||
export type GameCreationAppAssetCategory =
|
||
(typeof GAME_CREATION_APP_ASSET_CATEGORIES)[number];
|
||
|
||
/**
|
||
* kind 到功能分类的默认映射,按生成的 kind union **穷举**:少一个成员编译不过。
|
||
* 认不出的 kind 收口成 `unknown` 后同样落「待归类」,不做别名归一。
|
||
*/
|
||
export const GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND: Record<
|
||
GameCreationAppAssetKind,
|
||
GameCreationAppAssetCategory
|
||
> = {
|
||
image: 'unclassified',
|
||
scene: 'scene',
|
||
character: 'character',
|
||
'character-animation': 'character',
|
||
icon: 'ui-interaction',
|
||
'icon-spritesheet': 'ui-interaction',
|
||
'icon-spec': 'ui-interaction',
|
||
'ui-design': 'ui-interaction',
|
||
'ui-design-doc': 'ui-interaction',
|
||
'publication-material': 'unclassified',
|
||
spec: 'document',
|
||
video: 'unclassified',
|
||
'sound-effect': 'audio',
|
||
'background-music': 'audio',
|
||
audio: 'audio',
|
||
font: 'document',
|
||
document: 'document',
|
||
code: 'unclassified',
|
||
unknown: 'unclassified',
|
||
};
|
||
|
||
export function normalizeGameCreationAppAssetCategory(
|
||
value: unknown,
|
||
): GameCreationAppAssetCategory | null {
|
||
if (typeof value !== 'string') return null;
|
||
const normalized = value.trim();
|
||
return (GAME_CREATION_APP_ASSET_CATEGORIES as readonly string[]).includes(
|
||
normalized,
|
||
)
|
||
? (normalized as GameCreationAppAssetCategory)
|
||
: null;
|
||
}
|
||
|
||
export function gameCreationAppAssetCategoryForKind(
|
||
kind: GameCreationAppAssetKind,
|
||
): GameCreationAppAssetCategory {
|
||
return GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND[kind];
|
||
}
|
||
|
||
/** 解析外部 kind 后再按共享枚举派生分类;内部业务逻辑应传入已解析的枚举。 */
|
||
export function gameCreationAppAssetCategoryForRawKind(
|
||
raw: unknown,
|
||
context: string,
|
||
): GameCreationAppAssetCategory {
|
||
return gameCreationAppAssetCategoryForKind(
|
||
parseGameCreationAppAssetKind(raw, context),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* manifest 资产的最终功能分类。
|
||
*
|
||
* 优先级:落盘 `category`(用户可在「分类与标签」面板手动设置,是权威值)→ 按 `kind` 派生。
|
||
* 历史 manifest 缺少 category、或(内存对象上的)category 非法时都退回 kind 派生;
|
||
* 注意「落盘值非法」这一支已经不可达:Rust 读侧对未知 `category` 失败关闭(见
|
||
* `server-rs/crates/shared-contracts/src/game_creation_app.rs` 的
|
||
* `GameCreationAppAssetManifestEntryWire`),比本客户端新的值会让整份 manifest 直接读不出来。
|
||
*
|
||
* **例外(唯一的覆盖窗口)**:落盘值是 `unclassified`,而该资产 `kind` 能派生出明确的
|
||
* 非 `unclassified` 分类时,采用派生值。这条规则用于自愈「历史上被系统错误写成
|
||
* unclassified」的存量数据——典型例子是 UI 资产落盘 `unclassified` 而 kind 已是 `ui-design`。
|
||
* kind 认不出时只收口成 `unknown`(派生同样是 `unclassified`)并留痕原始串。
|
||
*
|
||
* 盲区:用户**手动**把一个 kind 已能明确分类的资产设成「待归类」时,该手动值会被这条
|
||
* 规则覆盖。这是有意接受的取舍——「手动设为待归类」本身意图边缘,且 kind 已经表达了分类;
|
||
* 而反过来漏掉这条规则,所有历史误判都无法自愈。
|
||
* kind 派生结果本身就是 `unclassified` 的(如 `image` / `video` / `code`)不受影响,
|
||
* 仍然信任落盘值。
|
||
*
|
||
* **这是「读显示」口径,不是「写回」口径。** 需要回写 manifest 的调用方必须用
|
||
* `gameCreationAppAssetPersistedCategory`,否则会把自愈值写回落盘、把「只改标签」
|
||
* 变成静默改分类。Rust 侧由 `game_creation_app_asset_effective_category` 实现同一口径
|
||
* (Agent 投影走 Rust 那条)。两侧的决策矩阵分别在
|
||
* `packages/shared/src/contracts/gameCreationApp.test.ts`(本文件)与
|
||
* `shared-contracts` 的 `EFFECTIVE_CATEGORY_CONTRACT` 用例里逐条钉住。
|
||
*/
|
||
export function gameCreationAppAssetCategory(
|
||
asset: Pick<GameCreationAppAssetManifestEntry, 'kind' | 'category'>,
|
||
): GameCreationAppAssetCategory {
|
||
const persisted = normalizeGameCreationAppAssetCategory(asset.category);
|
||
const derivedFromKind = gameCreationAppAssetCategoryForKind(
|
||
parseGameCreationAppAssetKind(asset.kind, 'asset-category.display'),
|
||
);
|
||
if (persisted === null) {
|
||
return derivedFromKind;
|
||
}
|
||
if (persisted === 'unclassified' && derivedFromKind !== 'unclassified') {
|
||
return derivedFromKind;
|
||
}
|
||
return persisted;
|
||
}
|
||
|
||
/**
|
||
* 回写 manifest 用的落盘分类:**不套用读时自愈**,只做「缺失或非法 → 按 kind 派生」的兜底。
|
||
*
|
||
* 与 `gameCreationAppAssetCategory` 的分工是「读显示 / 写回」:显示层可以自愈历史上的误判值,
|
||
* 但写入层必须原样保留落盘值——「编辑标签」面板若回传自愈值,就把「只改标签」变成了
|
||
* 静默改分类(真机已发生:同一条资产出现 `unclassified` 与 `ui-interaction` 两种落盘值)。
|
||
*
|
||
* Rust 侧 manifest 反序列化(`game_creation_app.rs` 的 `GameCreationAppAssetManifestEntry`
|
||
* `Deserialize`)在字段缺失时用同一套 `kind` 派生;未知分类值则在读侧失败关闭(不再退化成
|
||
* 派生值),所以从 manifest 读进来的资产本函数结果必然等于它的落盘 `category`。
|
||
* 剩下这一支「非法值 → 派生」只服务内存对象(手工构造的资产),不是落盘口径的兜底。
|
||
*/
|
||
export function gameCreationAppAssetPersistedCategory(
|
||
asset: Pick<GameCreationAppAssetManifestEntry, 'kind' | 'category'>,
|
||
): GameCreationAppAssetCategory {
|
||
return (
|
||
normalizeGameCreationAppAssetCategory(asset.category) ??
|
||
gameCreationAppAssetCategoryForKind(
|
||
parseGameCreationAppAssetKind(asset.kind, 'asset-category.persisted'),
|
||
)
|
||
);
|
||
}
|
||
|
||
export function gameCreationAppAssetTags(
|
||
asset: Pick<GameCreationAppAssetManifestEntry, 'tags'>,
|
||
): string[] {
|
||
return Array.isArray(asset.tags)
|
||
? asset.tags.filter((tag): tag is string => typeof tag === 'string')
|
||
: [];
|
||
}
|
||
|
||
export function normalizeGameCreationAppAssetTags(
|
||
tags: readonly string[],
|
||
): string[] {
|
||
const normalized: string[] = [];
|
||
for (const tag of tags) {
|
||
const trimmed = tag.trim();
|
||
if (!trimmed || normalized.includes(trimmed)) continue;
|
||
normalized.push(trimmed);
|
||
}
|
||
return normalized;
|
||
}
|
||
|
||
export const GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION =
|
||
'game-creator-resource-layout.v1' as const;
|
||
export const GAME_CREATION_RESOURCE_LAYOUT_MAX_SAFE_REVISION = 9_007_199_254_740_991;
|
||
export const GAME_CREATION_RESOURCE_LAYOUT_MIN_COORDINATE = -1_000_000;
|
||
export const GAME_CREATION_RESOURCE_LAYOUT_MAX_COORDINATE = 1_000_000;
|
||
|
||
export function isSafeProjectResourceCanvasLayoutRevision(
|
||
value: unknown,
|
||
): value is number {
|
||
return (
|
||
typeof value === 'number' &&
|
||
Number.isSafeInteger(value) &&
|
||
value >= 0 &&
|
||
value <= GAME_CREATION_RESOURCE_LAYOUT_MAX_SAFE_REVISION
|
||
);
|
||
}
|
||
|
||
export function isSafeProjectResourceCanvasCoordinate(
|
||
value: unknown,
|
||
): value is number {
|
||
return (
|
||
typeof value === 'number' &&
|
||
Number.isSafeInteger(value) &&
|
||
value >= GAME_CREATION_RESOURCE_LAYOUT_MIN_COORDINATE &&
|
||
value <= GAME_CREATION_RESOURCE_LAYOUT_MAX_COORDINATE
|
||
);
|
||
}
|
||
|
||
export type ProjectResourceCanvasLayoutMode = 'dependency' | 'type';
|
||
|
||
/**
|
||
* 资源画布分区轴:6 类资源资产功能分类 + 末尾独立的「项目版本」栏目。
|
||
*
|
||
* 项目版本不是资源资产,6 类资产分类轴对它不适用,因此固定在末尾单独成栏,
|
||
* 不与「待归类」混在一起。
|
||
*/
|
||
export const PROJECT_RESOURCE_CANVAS_SECTIONS = [
|
||
...GAME_CREATION_APP_ASSET_CATEGORIES,
|
||
'version',
|
||
] as const;
|
||
|
||
export type ProjectResourceCanvasCategory =
|
||
(typeof PROJECT_RESOURCE_CANVAS_SECTIONS)[number];
|
||
|
||
/**
|
||
* 旧四 / 五栏目分区值(扩展名 + mediaType 口径)的读兼容白名单。
|
||
*
|
||
* `document` / `audio` / `version` 在新旧口径下同名,只有 `art` / `code` 是旧口径独有值。
|
||
* 这五个值是布局 sidecar 的历史取值,只用于继续读取既有坐标,不作为写入值。
|
||
*/
|
||
export const LEGACY_PROJECT_RESOURCE_CANVAS_SECTIONS = [
|
||
'code',
|
||
'document',
|
||
'version',
|
||
'art',
|
||
'audio',
|
||
] as const;
|
||
|
||
export type LegacyProjectResourceCanvasSection =
|
||
(typeof LEGACY_PROJECT_RESOURCE_CANVAS_SECTIONS)[number];
|
||
|
||
/** 布局 sidecar 的 `section` 字段在磁盘上可能出现的全部取值:现行分区 + 旧栏目白名单。 */
|
||
export type PersistedProjectResourceCanvasSection =
|
||
| ProjectResourceCanvasCategory
|
||
| LegacyProjectResourceCanvasSection;
|
||
|
||
export function isProjectResourceCanvasCategory(
|
||
value: unknown,
|
||
): value is ProjectResourceCanvasCategory {
|
||
return (
|
||
typeof value === 'string' &&
|
||
(PROJECT_RESOURCE_CANVAS_SECTIONS as readonly string[]).includes(value)
|
||
);
|
||
}
|
||
|
||
export function isLegacyProjectResourceCanvasSection(
|
||
value: unknown,
|
||
): value is LegacyProjectResourceCanvasSection {
|
||
return (
|
||
typeof value === 'string' &&
|
||
(LEGACY_PROJECT_RESOURCE_CANVAS_SECTIONS as readonly string[]).includes(
|
||
value,
|
||
)
|
||
);
|
||
}
|
||
|
||
export interface ProjectResourceCanvasPosition {
|
||
resourceId: string;
|
||
/**
|
||
* sidecar 里可能仍是旧栏目取值,读取时必须经
|
||
* `normalizeResourceCanvasPosition` 归一到现行分区后再使用。
|
||
*/
|
||
section: PersistedProjectResourceCanvasSection;
|
||
x: number;
|
||
y: number;
|
||
manuallyPlaced: boolean;
|
||
}
|
||
|
||
export interface ProjectResourceCanvasLayout {
|
||
schemaVersion: typeof GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION;
|
||
projectId: string;
|
||
mode: ProjectResourceCanvasLayoutMode;
|
||
revision: number;
|
||
positions: ProjectResourceCanvasPosition[];
|
||
updatedAt: number;
|
||
}
|
||
|
||
export type UpdateProjectResourceCanvasLayoutResult =
|
||
| {
|
||
status: 'updated';
|
||
layout: ProjectResourceCanvasLayout;
|
||
}
|
||
| {
|
||
status: 'conflict';
|
||
layout: ProjectResourceCanvasLayout;
|
||
};
|
||
|
||
export type GameCreationAppPreviewStatus =
|
||
| 'stopped'
|
||
| 'starting'
|
||
| 'running'
|
||
| 'failed';
|
||
|
||
export interface GameCreationAppPreviewState {
|
||
status: GameCreationAppPreviewStatus;
|
||
url?: string | null;
|
||
port?: number | null;
|
||
}
|
||
|
||
export interface GameCreationAppCommandRunState {
|
||
commandId: string;
|
||
status: 'completed' | 'failed';
|
||
output: string;
|
||
logPath: string;
|
||
updatedAt: number;
|
||
}
|
||
|
||
export type GameIterationVersionCreatedReason =
|
||
| 'initial'
|
||
| 'resource-replacement'
|
||
| 'agent-revision';
|
||
|
||
export interface GameIterationVersionResourceBinding {
|
||
slotId: string;
|
||
resourceId: string;
|
||
}
|
||
|
||
export interface GameIterationVersion {
|
||
versionId: string;
|
||
parentVersionId: string | null;
|
||
projectRevision: number;
|
||
resourceBindings: GameIterationVersionResourceBinding[];
|
||
createdReason: GameIterationVersionCreatedReason;
|
||
/**
|
||
* 创建时间,单位是 **Unix 秒**。
|
||
*
|
||
* 写入侧为 `project/manifest.rs` 的 `created_at: unix_timestamp()`,该函数返回
|
||
* `duration.as_secs()`,全仓版本条目统一按秒落盘。渲染时必须换算成毫秒
|
||
* (`new Date(createdAt * 1000)`),不要把秒值直接交给 `Date`。
|
||
*/
|
||
createdAt: number;
|
||
editPrompt?: string | null;
|
||
}
|
||
|
||
export interface GameCreationAppManifest {
|
||
schemaVersion: string;
|
||
projectId: string;
|
||
name: string;
|
||
goal?: string | null;
|
||
tasks: GameCreationAppTaskState[];
|
||
assets: GameCreationAppAssetManifestEntry[];
|
||
preview?: GameCreationAppPreviewState | null;
|
||
commandRuns?: GameCreationAppCommandRunState[];
|
||
versions?: GameIterationVersion[];
|
||
godotProjectRoot?: string | null;
|
||
cocosProjectRoot?: string | null;
|
||
}
|
||
|
||
export interface GameCreationAgentToolCallTrace {
|
||
toolId: string;
|
||
status: string;
|
||
inputPaths: string[];
|
||
outputPaths: string[];
|
||
summary: string;
|
||
}
|
||
|
||
export interface GameCreationAgentRunStep {
|
||
pass: number;
|
||
agent: string;
|
||
phase: string;
|
||
taskId: string | null;
|
||
group: GameCreationAppAgentGroup | null;
|
||
role: string | null;
|
||
status: string;
|
||
inputPaths: string[];
|
||
outputPaths: string[];
|
||
summary: string;
|
||
toolCalls: GameCreationAgentToolCallTrace[];
|
||
}
|
||
|
||
export interface GameCreationAgentArtifactTrace {
|
||
path: string;
|
||
sizeBytes: number;
|
||
checksum: string;
|
||
}
|
||
|
||
export interface GameCreationAgentRepairRouteTrace {
|
||
issue: string;
|
||
taskIds: string[];
|
||
reason: string;
|
||
}
|
||
|
||
export interface GameCreationAgentRunTaskGraphTrace {
|
||
goal: string;
|
||
readyTaskIds: string[];
|
||
activeTaskIds: string[];
|
||
carriedTaskIds: string[];
|
||
repairFocus: string[];
|
||
repairRoutes: GameCreationAgentRepairRouteTrace[];
|
||
tasks: GameCreationAppTaskState[];
|
||
}
|
||
|
||
export interface GameCreationAgentPassPlanTrace {
|
||
pass: number;
|
||
mode: 'initial' | 'repair' | string;
|
||
summary: string;
|
||
activeTaskIds: string[];
|
||
carriedTaskIds: string[];
|
||
dependencyWaves: string[][];
|
||
repairFocus: string[];
|
||
repairRoutes: GameCreationAgentRepairRouteTrace[];
|
||
}
|
||
|
||
export interface GameCreationAgentRunTrace {
|
||
schemaVersion: typeof GAME_CREATION_AGENT_RUN_SCHEMA_VERSION;
|
||
runId: string;
|
||
commandId: string;
|
||
status: string;
|
||
lifecycleStatus?: string | null;
|
||
passes: number;
|
||
maxPasses: number;
|
||
toolCallCount: number;
|
||
maxToolCalls: number;
|
||
stopReason: string;
|
||
goal: string;
|
||
coordination: string;
|
||
steps: GameCreationAgentRunStep[];
|
||
artifacts: GameCreationAgentArtifactTrace[];
|
||
taskGraph: GameCreationAgentRunTaskGraphTrace;
|
||
passPlans: GameCreationAgentPassPlanTrace[];
|
||
nextStep: string;
|
||
error: string | null;
|
||
updatedAt: number;
|
||
}
|
||
|
||
export function selectGameCreationAppReadyTasks(
|
||
manifest: Pick<GameCreationAppManifest, 'tasks'>,
|
||
): GameCreationAppTaskState[] {
|
||
const tasks =
|
||
manifest.tasks.length > 0
|
||
? manifest.tasks
|
||
: createGameCreationAppSeedTasks();
|
||
const completed = new Set(
|
||
tasks.filter((task) => task.status === 'completed').map((task) => task.id),
|
||
);
|
||
|
||
return tasks.filter(
|
||
(task) =>
|
||
task.status === 'pending' &&
|
||
task.dependencies.every((dependency) => completed.has(dependency)),
|
||
);
|
||
}
|
||
|
||
export function createGameCreationAppManifest(
|
||
projectId: string,
|
||
name: string,
|
||
): GameCreationAppManifest {
|
||
return {
|
||
schemaVersion: GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION,
|
||
projectId,
|
||
name,
|
||
goal: null,
|
||
tasks: createGameCreationAppSeedTasks(),
|
||
assets: [],
|
||
};
|
||
}
|