Merge codex/ai-game-creator-app:融合客户端运行时配置与 LLM api_kind 链路

以 codex/ai-game-creator-app 的客户端配置文件架构(game-creator.config.json)为底,
融合当前分支的 LlmRunRequest/LlmApiKind(openai_responses/openai_chat/anthropic)维度。

手工融合 6 个冲突文件:
- src-tauri/src/main.rs:LLM 配置统一走客户端配置文件,配置字段 protocol→apiKind,
  读取/解析/校验改为 parse_game_creator_llm_api_kind,丢弃 env 读取分支,测试改用配置文件路径
- game-creator.config.json / App.tsx / appSurface.test.ts:配置 schema protocol→apiKind,
  配置面板下拉项改为三种 api_kind
- scripts/smoke-agent-run-local-provider + check-config:smoke 写入 apiKind,drift 校验同步
- 三份文档(decision-log / 技术方案 / 开发运维):改用客户端配置文件 + apiKind 口径

验证:cargo check 全目标通过;63/67 Rust 单测通过(4 个为既有 mock-server flaky 集成测试,
经核实在合并前分支上同样间歇失败,与本次合并无关);TS typecheck、vitest(45)、check-config drift 全通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 18:02:22 +08:00
13 changed files with 1381 additions and 434 deletions
+1
View File
@@ -36,6 +36,7 @@ temp*build*/
/apps/ai-game-creator-shell/src-tauri/logs/
/apps/ai-game-creator-shell/logs/
/apps/ai-game-creator-shell/.llm-drafts/
/apps/ai-game-creator-shell/game-creator.config.local.json
/apps/mobile-shell/.expo/
/apps/mobile-shell/.expo-export-smoke/
/server-rs/.spacetimedb/
@@ -0,0 +1,16 @@
{
"llm": {
"apiKey": "",
"baseUrl": "https://api.openai.com/v1",
"model": "gpt-4.1",
"apiKind": "openai_responses",
"stream": false,
"requestTimeoutMs": 180000,
"maxRetries": 0,
"retryBackoffMs": 500
},
"editorApi": {
"baseUrl": "http://127.0.0.1:8082",
"apiKey": ""
}
}
+2 -2
View File
@@ -7,8 +7,8 @@
"dev": "npm --prefix ../.. exec tauri -- dev",
"dev-server": "node scripts/start-dev-server.mjs",
"build": "npm --prefix ../.. exec tauri -- build",
"llm-status": "node scripts/run-cli-with-env.mjs --llm-status",
"agent-run": "node scripts/run-cli-with-env.mjs --agent-run",
"llm-status": "node scripts/run-cli-with-config.mjs --llm-status",
"agent-run": "node scripts/run-cli-with-config.mjs --agent-run",
"agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs",
"typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs"
},
@@ -64,6 +64,7 @@ function assertNoOpenAiApiKeys(paths) {
assertNoOpenAiApiKeys([
new URL('../src/', import.meta.url),
new URL('../scripts/', import.meta.url),
new URL('../game-creator.config.json', import.meta.url),
new URL('../src-tauri/src/', import.meta.url),
new URL('../package.json', import.meta.url),
new URL('../vite.config.ts', import.meta.url),
@@ -97,19 +98,19 @@ if (packageConfig.name !== '@genarrative/ai-game-creator-shell') {
if (
packageConfig.scripts?.['llm-status'] !==
'node scripts/run-cli-with-env.mjs --llm-status'
'node scripts/run-cli-with-config.mjs --llm-status'
) {
throw new Error(
'AI game creator shell llm-status must load gitignored local env before checking LLM config',
'AI game creator shell llm-status must use client config before checking LLM config',
);
}
if (
packageConfig.scripts?.['agent-run'] !==
'node scripts/run-cli-with-env.mjs --agent-run'
'node scripts/run-cli-with-config.mjs --agent-run'
) {
throw new Error(
'AI game creator shell agent-run must load gitignored local env before running the provider path',
'AI game creator shell agent-run must use client config before running the provider path',
);
}
@@ -190,8 +191,8 @@ const devServerSource = fs.readFileSync(
new URL('../scripts/start-dev-server.mjs', import.meta.url),
'utf8',
);
const runCliWithEnvSource = fs.readFileSync(
new URL('../scripts/run-cli-with-env.mjs', import.meta.url),
const runCliWithConfigSource = fs.readFileSync(
new URL('../scripts/run-cli-with-config.mjs', import.meta.url),
'utf8',
);
@@ -211,15 +212,13 @@ for (const snippet of [
}
for (const snippet of [
"path.join(repoRoot, '.env.secrets.local')",
"path.join(appRoot, '.env.secrets.local')",
'dotenv.config({ path: envPath, override: false })',
"new URL('..', import.meta.url)",
"'--manifest-path'",
"'src-tauri/Cargo.toml'",
]) {
if (!runCliWithEnvSource.includes(snippet)) {
if (!runCliWithConfigSource.includes(snippet)) {
throw new Error(
`AI game creator shell local env CLI wrapper drifted: ${snippet}`,
`AI game creator shell config CLI wrapper drifted: ${snippet}`,
);
}
}
@@ -230,19 +229,32 @@ const tauriMainSource = fs.readFileSync(
);
for (const snippet of [
'fn load_game_creator_local_env()',
'fn load_game_creator_env_file(path: &Path)',
'directory.join(".env.secrets.local")',
'const GAME_CREATOR_CONFIG_FILE_NAME: &str = "game-creator.config.json"',
'const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.json"',
'const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json")',
'fn configure_game_creator_runtime_config_dir(',
'app.path().app_config_dir()?',
'fn load_game_creator_app_config()',
'fn read_game_creator_app_config()',
'fn write_game_creator_app_config(',
'fn writable_game_creator_config_path()',
'fn normalize_game_creator_app_config(',
'fn merge_game_creator_config_file(',
'.join("apps")',
'.join("ai-game-creator-shell")',
'load_game_creator_local_env()?;',
'let local_env_error = load_game_creator_local_env().err();',
'local.env.load.failed',
'configure_game_creator_runtime_config_dir(app.handle())?',
'read_game_creator_app_config,',
'write_game_creator_app_config,',
'build_game_creator_llm_client_from_config()?',
'let app_config = match load_game_creator_app_config()',
'#[cfg(debug_assertions)]\nfn developer_window_url()',
'tauri::WebviewUrl::App(PathBuf::from("index.html?dev"))',
'#[cfg(debug_assertions)]\nfn open_developer_window(app: &tauri::App)',
'tauri::WebviewWindowBuilder::new(app, "developer", developer_window_url())',
'open_developer_window(app)?;',
'fn append_local_permission_log_at(',
'"command.auto"',
'GameCreationAppPermission::Auto',
]) {
if (!tauriMainSource.includes(snippet)) {
throw new Error(
@@ -257,6 +269,12 @@ const appSource = fs.readFileSync(
);
for (const snippet of [
"'game.run_local'",
"'read_game_creator_app_config'",
"'write_game_creator_app_config'",
'aria-label="运行时配置"',
'LLM API Key',
'画板 API Key',
'runtime_config.save',
"'/run:运行自检,启动本地 HTTP 预览并交给外部浏览器'",
'async function openPreviewInExternalBrowser',
"'open_local_game_preview'",
@@ -275,6 +293,8 @@ for (const snippet of [
"'permission.pending'",
"'permission.confirm'",
"'permission.cancel'",
"'command.auto'",
"'agent.run_status'",
'function summarizeAgentRunTrace',
'工具调用:${agentRunTrace.toolCallCount}/${agentRunTrace.maxToolCalls}',
'agentRunTrace.error ?',
@@ -336,8 +356,10 @@ for (const snippet of [
'function writeStreamingChatCompletion',
'requestJson?.stream === true',
`requestBodies.every((body) => body.includes('"stream":true'))`,
"GENARRATIVE_GAME_CREATOR_LLM_API_KIND: 'openai_chat'",
"GENARRATIVE_GAME_CREATOR_LLM_STREAM: 'true'",
"const localConfigPath = path.join(appRoot, 'game-creator.config.local.json')",
"apiKind: 'openai_chat'",
'stream: true',
'await restoreOptionalFile(localConfigPath, previousLocalConfig)',
"method: 'HEAD'",
'previewAssetHead.contentLength === String(smokeAssetBytes.length)',
"previewAudioHead.contentType === 'audio/mpeg'",
@@ -1,21 +1,8 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import dotenv from 'dotenv';
const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
const repoRoot = path.resolve(appRoot, '../..');
for (const envPath of [
path.join(repoRoot, '.env.secrets.local'),
path.join(appRoot, '.env.secrets.local'),
]) {
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath, override: false });
}
}
const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo';
const child = spawn(
@@ -30,7 +17,6 @@ const child = spawn(
{
cwd: appRoot,
stdio: 'inherit',
env: process.env,
},
);
@@ -6,6 +6,7 @@ import os from 'node:os';
import path from 'node:path';
const appRoot = path.resolve(new URL('..', import.meta.url).pathname);
const localConfigPath = path.join(appRoot, 'game-creator.config.local.json');
const projectRoot = path.join(
os.tmpdir(),
`genarrative-ai-game-creator-smoke-${Date.now()}`,
@@ -254,9 +255,11 @@ function writeStreamingChatCompletion(response, content) {
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
const baseUrl = `http://127.0.0.1:${address.port}`;
const previousLocalConfig = await readOptionalFile(localConfigPath);
try {
await seedLocalAsset();
await writeSmokeLocalConfig(baseUrl);
const {
output,
previewHtml,
@@ -266,7 +269,7 @@ try {
previewAudio,
previewAudioHead,
previewDom,
} = await runAgent(baseUrl);
} = await runAgent();
const tracePath = path.join(projectRoot, '.agent/run.latest.json');
const trace = JSON.parse(await fs.readFile(tracePath, 'utf8'));
const pass2TaskGraph = JSON.parse(
@@ -542,10 +545,49 @@ try {
console.log(`projectPath=${projectRoot}`);
console.log(`tracePath=${tracePath}`);
} finally {
await restoreOptionalFile(localConfigPath, previousLocalConfig);
server.close();
}
function runAgent(baseUrl) {
async function readOptionalFile(filePath) {
try {
return await fs.readFile(filePath);
} catch (error) {
if (error?.code === 'ENOENT') {
return null;
}
throw error;
}
}
async function restoreOptionalFile(filePath, previous) {
if (previous === null) {
await fs.rm(filePath, { force: true });
return;
}
await fs.writeFile(filePath, previous);
}
async function writeSmokeLocalConfig(baseUrl) {
await fs.writeFile(
localConfigPath,
`${JSON.stringify(
{
llm: {
apiKey: 'local-provider-key',
baseUrl,
model: 'local-game-creator-smoke',
apiKind: 'openai_chat',
stream: true,
},
},
null,
2,
)}\n`,
);
}
function runAgent() {
return new Promise((resolve, reject) => {
let previewReadStarted = false;
let previewUrl = '';
@@ -568,14 +610,6 @@ function runAgent(baseUrl) {
],
{
cwd: appRoot,
env: {
...process.env,
GENARRATIVE_GAME_CREATOR_LLM_API_KEY: 'local-provider-key',
GENARRATIVE_GAME_CREATOR_LLM_BASE_URL: baseUrl,
GENARRATIVE_GAME_CREATOR_LLM_MODEL: 'local-game-creator-smoke',
GENARRATIVE_GAME_CREATOR_LLM_API_KIND: 'openai_chat',
GENARRATIVE_GAME_CREATOR_LLM_STREAM: 'true',
},
stdio: ['pipe', 'pipe', 'pipe'],
},
);
@@ -757,9 +791,6 @@ function readBrowserDom(url) {
}
function resolveChromeBin() {
if (process.env.GENARRATIVE_GAME_CREATOR_CHROME_BIN) {
return process.env.GENARRATIVE_GAME_CREATOR_CHROME_BIN;
}
for (const candidate of [
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
File diff suppressed because it is too large Load Diff
+318 -5
View File
@@ -10,6 +10,7 @@ import {
GAME_CREATION_APP_COMMANDS,
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
type GameCreationAppAgentGroup,
type GameCreationAppCommandDescriptor,
type GameCreationAgentRunTrace,
type GameCreationAppManifest,
type GameCreationAppPermission,
@@ -63,6 +64,30 @@ interface GameCreatorLlmConfigStatus {
error: string | null;
}
type GameCreatorLlmApiKind = 'openai_responses' | 'openai_chat' | 'anthropic';
interface GameCreatorAppConfig {
llm: {
apiKey: string;
baseUrl: string;
model: string;
apiKind: GameCreatorLlmApiKind;
stream: boolean;
requestTimeoutMs: number;
maxRetries: number;
retryBackoffMs: number;
};
editorApi: {
baseUrl: string;
apiKey: string;
};
}
interface GameCreatorAppConfigView {
path: string;
config: GameCreatorAppConfig;
}
interface UploadLocalAssetResult {
id: string;
localPath: string;
@@ -296,6 +321,23 @@ const chatCommandHelp = [
'/import-canvas-export /绝对/导出.zip 画板项目ID:导入画板素材导出包',
];
const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
llm: {
apiKey: '',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4.1',
apiKind: 'openai_responses',
stream: false,
requestTimeoutMs: 180000,
maxRetries: 0,
retryBackoffMs: 500,
},
editorApi: {
baseUrl: 'http://127.0.0.1:8082',
apiKey: '',
},
};
function taskRowsFromManifest(
manifest: GameCreationAppManifest,
): GameCreationAppTaskState[] {
@@ -1053,6 +1095,11 @@ export function App() {
LocalProjectFileEntry[]
>([]);
const [agentRunStatus, setAgentRunStatus] = useState('未运行');
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
const [runtimeConfigPath, setRuntimeConfigPath] = useState('');
const [runtimeConfigStatus, setRuntimeConfigStatus] = useState('未读取');
const [runtimeConfigDraft, setRuntimeConfigDraft] =
useState<GameCreatorAppConfig>(defaultRuntimeConfigDraft);
const [messages, setMessages] = useState<ChatMessage[]>([
{
role: 'assistant',
@@ -1099,8 +1146,12 @@ export function App() {
function appendLocalPermissionLog(
projectPath: string | null,
event: 'permission.pending' | 'permission.confirm' | 'permission.cancel',
commandId: PendingCommand['id'],
event:
| 'permission.pending'
| 'permission.confirm'
| 'permission.cancel'
| 'command.auto',
commandId: GameCreationAppCommandDescriptor['id'],
) {
const invoke = resolveTauriInvoke();
if (!invoke || !projectPath) {
@@ -1160,6 +1211,80 @@ export function App() {
return true;
}
function updateRuntimeLlmConfig<K extends keyof GameCreatorAppConfig['llm']>(
key: K,
value: GameCreatorAppConfig['llm'][K],
) {
setRuntimeConfigDraft((current) => ({
...current,
llm: {
...current.llm,
[key]: value,
},
}));
}
function updateRuntimeEditorConfig<
K extends keyof GameCreatorAppConfig['editorApi'],
>(key: K, value: GameCreatorAppConfig['editorApi'][K]) {
setRuntimeConfigDraft((current) => ({
...current,
editorApi: {
...current.editorApi,
[key]: value,
},
}));
}
function handleRuntimeConfigOpen() {
setRuntimeConfigOpen(true);
void readRuntimeConfig();
}
async function readRuntimeConfig() {
const invoke = resolveTauriInvoke();
if (!invoke) {
setRuntimeConfigStatus('需要在 Tauri App 内运行');
return;
}
setRuntimeConfigStatus('正在读取');
try {
const result = await invoke<GameCreatorAppConfigView>(
'read_game_creator_app_config',
);
setRuntimeConfigPath(result.path);
setRuntimeConfigDraft(result.config);
setRuntimeConfigStatus(`已读取:${result.path}`);
setCommandLog((current) => [...current, 'runtime_config.read']);
} catch (error) {
setRuntimeConfigStatus(error instanceof Error ? error.message : String(error));
}
}
async function handleRuntimeConfigSave(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const invoke = resolveTauriInvoke();
if (!invoke) {
setRuntimeConfigStatus('需要在 Tauri App 内运行');
return;
}
setRuntimeConfigStatus('正在保存');
try {
const result = await invoke<GameCreatorAppConfigView>(
'write_game_creator_app_config',
{ config: runtimeConfigDraft },
);
setRuntimeConfigPath(result.path);
setRuntimeConfigDraft(result.config);
setRuntimeConfigStatus(`已保存:${result.path}`);
setCommandLog((current) => [...current, 'runtime_config.save']);
} catch (error) {
setRuntimeConfigStatus(error instanceof Error ? error.message : String(error));
}
}
function requireChatProjectForUserAction() {
const nextProjectPath = resolveChatProjectPath(localProject);
if (nextProjectPath) {
@@ -2211,13 +2336,24 @@ export function App() {
setAgentRunStatus(
`${result.status} · ${result.lifecycleStatus} · ${result.nextStep}`,
);
const commandId =
action === 'status'
? 'agent.run_status'
: (`agent.${action}` as GameCreationAppCommandDescriptor['id']);
setCommandLog((current) => [
...current,
`agent.${action}`,
commandId,
'file.write .agent/activity.jsonl',
'file.write .agent/output.jsonl',
'file.write .agent/context.bundle.json',
]);
if (announceToChat && action === 'status') {
appendLocalPermissionLog(
nextProjectPath,
'command.auto',
'agent.run_status',
);
}
await refreshAgentRunTrace(nextProjectPath);
if (announceToChat) {
setMessages((current) => [
@@ -2501,6 +2637,13 @@ export function App() {
setPreviewStatus('未启动');
}
setCommandLog((current) => [...current, 'preview.status']);
if (announceToChat && nextProjectPath) {
appendLocalPermissionLog(
nextProjectPath,
'command.auto',
'preview.status',
);
}
if (announceToChat) {
setMessages((current) => [
...current,
@@ -3497,8 +3640,13 @@ export function App() {
>
<section className="chat-pane" aria-label="聊天">
<header className="chat-header">
<h1>AI </h1>
<span>{devMode ? '开发模式' : seedManifest.name}</span>
<div>
<h1>AI </h1>
<span>{devMode ? '开发模式' : seedManifest.name}</span>
</div>
<button type="button" onClick={handleRuntimeConfigOpen}>
</button>
</header>
<div className="message-list">
{messages.map((message, index) => (
@@ -3545,6 +3693,171 @@ export function App() {
</form>
</section>
{runtimeConfigOpen ? (
<div className="settings-overlay" role="presentation">
<form
className="settings-panel"
role="dialog"
aria-label="运行时配置"
aria-modal="true"
onSubmit={handleRuntimeConfigSave}
>
<header className="panel-header">
<h2></h2>
<div className="panel-actions">
<button type="button" onClick={readRuntimeConfig}>
</button>
<button
type="button"
onClick={() => setRuntimeConfigOpen(false)}
>
</button>
<button type="submit"></button>
</div>
</header>
{runtimeConfigPath ? (
<p className="manifest-path">{runtimeConfigPath}</p>
) : null}
<div className="settings-grid">
<label>
LLM API Key
<input
aria-label="LLM API Key"
type="password"
value={runtimeConfigDraft.llm.apiKey}
onChange={(event) =>
updateRuntimeLlmConfig('apiKey', event.currentTarget.value)
}
/>
</label>
<label>
LLM Base URL
<input
aria-label="LLM Base URL"
value={runtimeConfigDraft.llm.baseUrl}
onChange={(event) =>
updateRuntimeLlmConfig('baseUrl', event.currentTarget.value)
}
/>
</label>
<label>
LLM
<input
aria-label="LLM 模型"
value={runtimeConfigDraft.llm.model}
onChange={(event) =>
updateRuntimeLlmConfig('model', event.currentTarget.value)
}
/>
</label>
<label>
LLM API
<select
aria-label="LLM API 类型"
value={runtimeConfigDraft.llm.apiKind}
onChange={(event) =>
updateRuntimeLlmConfig(
'apiKind',
event.currentTarget.value as GameCreatorLlmApiKind,
)
}
>
<option value="openai_responses">openai_responses</option>
<option value="openai_chat">openai_chat</option>
<option value="anthropic">anthropic</option>
</select>
</label>
<label className="settings-checkbox">
<input
aria-label="LLM 流式请求"
type="checkbox"
checked={runtimeConfigDraft.llm.stream}
onChange={(event) =>
updateRuntimeLlmConfig('stream', event.currentTarget.checked)
}
/>
LLM
</label>
<label>
LLM ms
<input
aria-label="LLM 超时 ms"
type="number"
min="1"
value={runtimeConfigDraft.llm.requestTimeoutMs}
onChange={(event) =>
updateRuntimeLlmConfig(
'requestTimeoutMs',
Number(event.currentTarget.value),
)
}
/>
</label>
<label>
LLM
<input
aria-label="LLM 重试次数"
type="number"
min="0"
value={runtimeConfigDraft.llm.maxRetries}
onChange={(event) =>
updateRuntimeLlmConfig(
'maxRetries',
Number(event.currentTarget.value),
)
}
/>
</label>
<label>
LLM 退 ms
<input
aria-label="LLM 退避 ms"
type="number"
min="1"
value={runtimeConfigDraft.llm.retryBackoffMs}
onChange={(event) =>
updateRuntimeLlmConfig(
'retryBackoffMs',
Number(event.currentTarget.value),
)
}
/>
</label>
<label>
API Base URL
<input
aria-label="画板 API Base URL"
value={runtimeConfigDraft.editorApi.baseUrl}
onChange={(event) =>
updateRuntimeEditorConfig(
'baseUrl',
event.currentTarget.value,
)
}
/>
</label>
<label>
API Key
<input
aria-label="画板 API Key"
type="password"
value={runtimeConfigDraft.editorApi.apiKey}
onChange={(event) =>
updateRuntimeEditorConfig(
'apiKey',
event.currentTarget.value,
)
}
/>
</label>
</div>
<p className="status-line">{runtimeConfigStatus}</p>
</form>
</div>
) : null}
{devMode ? (
<section className="developer-pane" aria-label="开发环境">
<section className="developer-panel task-pane" aria-label="任务">
+70 -1
View File
@@ -59,11 +59,16 @@ textarea {
.chat-header,
.panel-header {
display: flex;
align-items: baseline;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.chat-header div {
display: grid;
gap: 4px;
}
.panel-header {
margin-bottom: 10px;
}
@@ -75,6 +80,7 @@ textarea {
}
.panel-header button,
.chat-header button,
.local-project-form button,
.panel-actions select {
height: 32px;
@@ -84,6 +90,7 @@ textarea {
}
.panel-header button,
.chat-header button,
.local-project-form button {
color: #fff;
background: #1f6feb;
@@ -114,6 +121,64 @@ h2 {
color: #647084;
}
.settings-overlay {
position: fixed;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
padding: 18px;
background: rgb(24 32 47 / 36%);
}
.settings-panel {
display: grid;
gap: 10px;
width: min(760px, 100%);
max-height: calc(100vh - 36px);
padding: 18px;
overflow: auto;
border: 1px solid #cfd7e6;
background: #fff;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.settings-grid label {
display: grid;
gap: 6px;
min-width: 0;
color: #647084;
font-size: 13px;
}
.settings-grid input,
.settings-grid select {
min-width: 0;
height: 36px;
padding: 0 10px;
border: 1px solid #cfd7e6;
border-radius: 6px;
color: #18202f;
background: #fff;
}
.settings-checkbox {
grid-template-columns: auto 1fr;
align-items: center;
align-self: end;
}
.settings-checkbox input {
width: 18px;
height: 18px;
padding: 0;
}
.message-list {
flex: 1;
border: 1px solid #dde3ee;
@@ -453,6 +518,10 @@ iframe.preview-frame {
grid-column: auto;
}
.settings-grid {
grid-template-columns: 1fr;
}
.composer {
grid-template-columns: 1fr auto;
}
@@ -33,18 +33,121 @@ afterEach(() => {
});
describe('AI 游戏创作 App 界面边界', () => {
it('keeps the user surface to chat, upload and command confirmation', () => {
it('keeps the user surface to chat, upload, config and command confirmation', () => {
renderAppAt('/');
expect(screen.getByLabelText('聊天')).not.toBeNull();
expect(screen.getByLabelText('创作想法')).not.toBeNull();
expect(screen.getByText('上传')).not.toBeNull();
expect(screen.getByRole('button', { name: '配置' })).not.toBeNull();
expect(screen.getByText('想做什么游戏?')).not.toBeNull();
expect(screen.queryByLabelText('开发环境')).toBeNull();
expect(screen.queryByLabelText('运行时配置')).toBeNull();
expect(screen.queryByText('Agent 能力')).toBeNull();
expect(screen.queryByText('编排 Trace')).toBeNull();
});
it('edits the published runtime config without leaking API keys into chat', async () => {
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_game_creator_app_config') {
return {
path: '/home/test/AppData/game-creator.config.json',
config: {
llm: {
apiKey: 'unit-loaded-secret-value',
baseUrl: 'https://llm.example.test/v1',
model: 'gpt-test',
apiKind: 'openai_responses',
stream: false,
requestTimeoutMs: 180000,
maxRetries: 0,
retryBackoffMs: 500,
},
editorApi: {
baseUrl: 'http://127.0.0.1:8082',
apiKey: 'editor-loaded-secret',
},
},
};
}
if (command === 'write_game_creator_app_config') {
return {
path: '/home/test/AppData/game-creator.config.json',
config: args?.config,
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
fireEvent.click(screen.getByRole('button', { name: '配置' }));
expect(await screen.findByDisplayValue('gpt-test')).not.toBeNull();
expect(
screen.getByText('/home/test/AppData/game-creator.config.json'),
).not.toBeNull();
expect(screen.getByLabelText('聊天').textContent).not.toContain(
'unit-loaded-secret-value',
);
fireEvent.change(screen.getByLabelText('LLM API Key'), {
target: { value: 'unit-new-secret-value' },
});
fireEvent.change(screen.getByLabelText('LLM Base URL'), {
target: { value: 'https://new-llm.example.test/v1' },
});
fireEvent.change(screen.getByLabelText('LLM 模型'), {
target: { value: 'gpt-next' },
});
fireEvent.change(screen.getByLabelText('LLM API 类型'), {
target: { value: 'openai_chat' },
});
fireEvent.click(screen.getByLabelText('LLM 流式请求'));
fireEvent.change(screen.getByLabelText('LLM 超时 ms'), {
target: { value: '90000' },
});
fireEvent.change(screen.getByLabelText('LLM 重试次数'), {
target: { value: '3' },
});
fireEvent.change(screen.getByLabelText('LLM 退避 ms'), {
target: { value: '800' },
});
fireEvent.change(screen.getByLabelText('画板 API Base URL'), {
target: { value: 'http://127.0.0.1:8099' },
});
fireEvent.change(screen.getByLabelText('画板 API Key'), {
target: { value: 'editor-new-secret' },
});
fireEvent.click(screen.getByRole('button', { name: '保存' }));
expect(await screen.findByText(/已保存:/)).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_game_creator_app_config');
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
config: {
llm: {
apiKey: 'unit-new-secret-value',
baseUrl: 'https://new-llm.example.test/v1',
model: 'gpt-next',
apiKind: 'openai_chat',
stream: true,
requestTimeoutMs: 90000,
maxRetries: 3,
retryBackoffMs: 800,
},
editorApi: {
baseUrl: 'http://127.0.0.1:8099',
apiKey: 'editor-new-secret',
},
},
});
expect(screen.getByLabelText('聊天').textContent).not.toContain(
'unit-new-secret-value',
);
});
it('keeps multiline chat evidence readable', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
@@ -1092,6 +1195,11 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(invoke).toHaveBeenCalledWith('get_local_game_preview_status', {
projectPath: '/tmp/authorized-game',
});
expect(invoke).toHaveBeenCalledWith('append_local_permission_log', {
projectPath: '/tmp/authorized-game',
event: 'command.auto',
commandId: 'preview.status',
});
});
it('runs static smoke and starts preview from chat through the authorized project path', async () => {
@@ -2193,16 +2301,40 @@ describe('AI 游戏创作 App 界面边界', () => {
};
}
if (command === 'control_agent_run') {
const action = String(args?.action ?? '');
const detail = String(args?.detail ?? '');
const resultByAction = {
status: {
status: 'pending',
lifecycleStatus: 'pending',
nextStep: 'runner-claim',
message: 'run run-control-chat 当前状态:pending / pending',
},
kill: {
status: 'killed',
lifecycleStatus: 'killed',
nextStep: 'resume-or-retry',
message: 'run run-control-chat 已标记为 killed',
},
retry: {
status: 'pending',
lifecycleStatus: 'pending',
nextStep: 'runner-claim',
message: 'run run-control-chat 已重试,等待下一次 claim',
},
resume: {
status: 'pending',
lifecycleStatus: 'pending',
nextStep: 'runner-claim',
message: `run run-control-chat 已恢复:${detail}`,
},
}[action];
if (!resultByAction) {
throw new Error(`unexpected agent run action ${action}`);
}
return {
runId: 'run-control-chat',
status: args?.action === 'kill' ? 'killed' : 'pending',
lifecycleStatus: args?.action === 'kill' ? 'killed' : 'pending',
nextStep:
args?.action === 'kill' ? 'resume-or-retry' : 'runner-claim',
message:
args?.action === 'kill'
? 'run run-control-chat 已标记为 killed'
: 'run run-control-chat 当前状态:pending / pending',
...resultByAction,
activityPath: '/tmp/authorized-game/.agent/activity.jsonl',
outputPath: '/tmp/authorized-game/.agent/output.jsonl',
contextBundlePath: '/tmp/authorized-game/.agent/context.bundle.json',
@@ -2236,6 +2368,11 @@ describe('AI 游戏创作 App 界面边界', () => {
action: 'status',
detail: undefined,
});
expect(invoke).toHaveBeenCalledWith('append_local_permission_log', {
projectPath: '/tmp/authorized-game',
event: 'command.auto',
commandId: 'agent.run_status',
});
submitChat('/agent-kill');
expect(screen.getByText('agent.kill')).not.toBeNull();
@@ -2253,6 +2390,40 @@ describe('AI 游戏创作 App 界面边界', () => {
action: 'kill',
detail: undefined,
});
submitChat('/agent-retry');
expect(screen.getByText('agent.retry')).not.toBeNull();
expect(
screen.getByText(
'标记 /tmp/authorized-game/.agent/run.latest.json 为 pending,等待 runner claim',
),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(/run run-control-chat 已重试,等待下一次 claim/),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'retry',
detail: undefined,
});
submitChat('/agent-resume 继续修复输入监听');
expect(screen.getByText('agent.resume')).not.toBeNull();
expect(
screen.getByText(
'附加用户说明并标记 /tmp/authorized-game/.agent/run.latest.json 为 pending',
),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(/run run-control-chat 已恢复:继续修复输入监听/),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'resume',
detail: '继续修复输入监听',
});
});
it('manages long memory from chat through the authorized local project path', async () => {
@@ -16,6 +16,14 @@
---
## 2026-06-30 AI 游戏创作 App 使用客户端配置文件
- 背景:`apps/ai-game-creator-shell` 是客户端 App,不应通过 `.env` 或进程环境变量承载 LLM / 画板同步配置;旧口径会让本地 secrets、CLI wrapper 和桌面 App 启动逻辑混在一起。
- 决策:仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板;发布 App 启动时在 Tauri 应用配置目录写入默认 `game-creator.config.json`,真实密钥和本机覆盖项都保存在该运行时配置文件中。主窗口提供“配置”面板读写该运行时 JSON;开发 CLI 无 AppHandle 时才回退读取仓库旁边的模板和 gitignored 本机覆盖文件。`llm.apiKey/baseUrl/model/protocol/stream/requestTimeoutMs/maxRetries/retryBackoffMs` 驱动真实 LLM 路径,`editorApi.baseUrl/apiKey` 驱动画板项目同步;`/llm-status` 只展示 baseUrl、model、protocol 和 API Key 是否存在,不显示密钥。
- 影响范围:AI 游戏创作 App 的 Tauri Rust 配置加载、主窗口配置面板、CLI wrapper、agent-run smoke、`check-config` 门禁、`.gitignore` 和实施计划文档。
- 验证方式:运行 `npm run ai-game-creator-shell:typecheck``cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml``npm run check:encoding``git diff --check`
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 2026-06-26 AI 游戏创作 App 生成过程必须在聊天可见
- 背景:普通用户窗口只保留聊天入口,但如果生成确认后只显示“已生成草案”和本地产物路径,真实 LLM / Agent loop 会被误解成固定模板落盘。
@@ -35,12 +43,12 @@
## 2026-06-25 AI 游戏创作 App 真实 LLM 联调用流式请求
- 背景:AI 游戏创作 App 的真实 OpenAI-compatible provider 验收中,小请求可返回,但 Planner 等稍长非流式请求会在上游响应前被网关空闲连接切断,表现为 TLS record 解密失败;本地无密钥 provider smoke 不能覆盖该真实网关行为。
- 决策:`platform-llm` 文本 client 使用系统 TLS backend,并保留底层错误链用于排障;AI 游戏创作 App 增加 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 开关,打开后 Planner、组内角色和 Generator 走流式请求。默认本地 smoke 继续使用非流式 OpenAI-compatible 测试 provider,避免把测试桩改重。
- 决策:`platform-llm` 文本 client 使用系统 TLS backend,并保留底层错误链用于排障;AI 游戏创作 App 通过客户端配置项 `llm.stream=true` 开关打开流式请求,打开后 Planner、组内角色和 Generator 走流式请求。
- 影响范围:`server-rs/crates/platform-llm``apps/ai-game-creator-shell/src-tauri/src/main.rs` 和 AI 游戏创作智能体 App 实施计划。
- 验证方式:运行 `cargo test -p platform-llm --manifest-path server-rs/Cargo.toml request_text_parses_non_stream_response`,并用真实 OpenAI-compatible 环境变量执行 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-real-loop-test-6 "做一个像素风反弹弹幕厨房小游戏..."`,确认 36 个 trace step、36 次 tool call、`game.static_smoke``preview.start``preview.stop` 完成。
- 验证方式:运行 `cargo test -p platform-llm --manifest-path server-rs/Cargo.toml request_text_parses_non_stream_response`,并用真实 OpenAI-compatible 本机配置执行 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-real-loop-test-6 "做一个像素风反弹弹幕厨房小游戏..."`,确认 36 个 trace step、36 次 tool call、`game.static_smoke``preview.start``preview.stop` 完成。
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
2026-06-29 追加:`platform-llm``LlmTextRequest` / `LlmTextResponse` 已直接替换为 provider-neutral 的 `LlmRunRequest` / `LlmRunResponse`API kind 先固定为 `openai_chat``openai_responses``anthropic` 三类。AI 游戏创作 App 默认 `openai_responses`,可 `GENARRATIVE_GAME_CREATOR_LLM_API_KIND=openai_chat` 接旧 Chat Completions 兼容网关,或 `GENARRATIVE_GAME_CREATOR_LLM_API_KIND=anthropic` 接 Anthropic Messages。当前 run 响应只保留通用文本、finish reason、response id 和 usage,高级能力后续按 capability 扩展,不把业务层绑死到 Responses 字段。
2026-06-27 追加2026-06-30 更新`platform-llm``LlmTextRequest` / `LlmTextResponse` 已直接替换为 provider-neutral 的 `LlmRunRequest` / `LlmRunResponse`API kind 先固定为 `openai_chat``openai_responses``anthropic` 三类。AI 游戏创作 App 改用客户端运行时配置(Tauri 应用配置目录的 `game-creator.config.json`),LLM 维度由 `llm.apiKind` 控制,默认 `openai_responses`,可设为 `openai_chat` 接旧 Chat Completions 兼容网关,或 `anthropic` 接 Anthropic Messages。当前 run 响应只保留通用文本、finish reason、response id 和 usage,高级能力后续按 capability 扩展,不把业务层绑死到 Responses 字段。
## 2026-06-24 AI 游戏创作 App 生成编排使用文件驱动 loop
@@ -3766,7 +3774,7 @@
- 决策:AI 游戏创作桌面入口新建 `apps/ai-game-creator-shell`。普通用户界面只保留聊天和上传入口;任务、能力、文件、记忆、预览和日志只放在开发模式或开发窗口。Agent 能力、manifest、内置命令和权限枚举写入 `packages/shared/src/contracts/gameCreationApp.ts``server-rs/crates/shared-contracts/src/game_creation_app.rs`;专业组和种子任务图写入 `server-rs/crates/platform-agent/src/game_creation.rs``canvas.project_open` 只允许打开本机 Genarrative 编辑器 `/editor/canvas?projectid=...`,默认本机端口为 `3000`,不得扩展成任意 URL 打开能力。
- 本地边界:生成代码、上传资产、短期记忆、长期记忆和预览入口必须保存到用户授权的本地项目目录;正式预览使用只读 `127.0.0.1:<port>` HTTP server,不使用 `file://``game.generate_draft``asset.upload` 这类 `confirm` 命令先在聊天区形成待确认命令,用户确认后才写本地产物;开发窗口中的 `confirm` 命令使用原生确认门,取消时只写日志不执行。`command.run_limited` 只执行白名单内置命令,当前最小真实命令是 `game.static_smoke`,用于检查 `game/index.html` 是否具备 canvas、canvas 渲染上下文、绘制调用、主循环、输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval``new Function``localStorage``fetch``WebSocket``ServiceWorker`,不得把任意 shell 执行暴露给普通用户界面。`.agent/manifest.json` 是本地最小状态源,记录专业组种子任务、资产、预览状态和受限命令运行结果;开发窗口专业组面板读取 manifest task state,不使用前端硬编码作为真相源。`file.list/read/write/delete` 只能访问本地项目目录内的相对路径,禁止绝对路径、`..`、反斜杠和符号链接逃逸。`asset.register` 只登记项目目录内已经存在的文件,并可记录 `uploaded``generated``canvas` 来源元数据。`canvas.asset_import` 是画板回流的本地落点,只导入项目内已有文件为 `canvas` 来源资产,并要求画板项目 ID 与 resourceId / assetObjectId 可追踪;`canvas.export_import` 复用现有画板素材导出 ZIP,把 `metadata.json` 引用的 `images/``media/``sequences/` 文件复制到本地项目 `assets/canvas-imports/` 并登记为 `canvas` 来源资产。
- 2026-06-24 调整:`game.generate_draft` 必须作为一次本地 agent 协作回合记录,用户确认后同时写入短期记忆、长期记忆、设计草案、数值配置、美术清单、音乐音效清单、发布包装草案、可运行 HTML、`.agent/logs/agent.log` 和 manifest `commandRuns`;manifest 任务状态必须反映策划、数值、美术、音乐、程序组首轮完成,预览试玩等待确认。
- 2026-06-24 调整:`game.generate_draft` 必须通过 OpenAI-compatible LLM 生成结构化 JSON 草案,读取 `GENARRATIVE_GAME_CREATOR_LLM_*` 或既有 `GENARRATIVE_LLM_*` / `LLM_*` / `OPENAI_*` 环境变量;LLM 配置缺失、上游失败、返回非 JSON、HTML 非自包含、缺少 `canvas` / `requestAnimationFrame` 或把危险用户输入原样写入 HTML 时直接失败,不得静默回退固定模板并声称 AI 生成。
- 2026-06-24 调整2026-06-30 更新`game.generate_draft` 必须通过 OpenAI-compatible LLM 生成结构化 JSON 草案;发布 App 读取 Tauri 应用配置目录中 `game-creator.config.json``llm.*` 配置项,开发 CLI 无 AppHandle 时才读仓库旁边的 fallback 配置。LLM 配置缺失、上游失败、返回非 JSON、HTML 非自包含、缺少 `canvas` / `requestAnimationFrame` 或把危险用户输入原样写入 HTML 时直接失败,不得静默回退固定模板并声称 AI 生成。
- 2026-06-24 调整:`game.generate_draft` 生成的 `game/index.html` 必须是可试玩原型,至少具备输入、主循环、目标、失败或胜利状态和重开路径;不得退回按钮计分、纯展示页或占位式游戏。
- 2026-06-24 调整:`game.generate_draft` 的设计草案、发布包装草案和 agent log 必须包含专业组 / 角色 / 产物交接摘要,作为 6 组 agent 协作的最小可追踪证据。
- 2026-06-25 调整:`game.generate_draft` 的 loop 不能只由 Generator 在提示词里模拟六组协作;每一轮必须在 Planner 之后分别调用策划、数值、美术、音乐、程序、运营 6 组下的角色 agent 产出 brief,写入 `.agent/passes/pass-N/groups/<group>/*.md`,再汇总为 `.agent/passes/pass-N/groups/*.md`,由 Generator 读取这些汇总 brief、spec、findings 和记忆整合成结构化草案。`.agent/run.latest.json` 必须记录 `llm.chat.group.<group>.<role>` toolCall 和 brief artifact,作为多智能体协作的最小真实证据。
@@ -3784,7 +3792,7 @@
- 2026-06-25 调整:新增 `npm run ai-game-creator-shell:agent-run:smoke` 作为无密钥开发验证入口。脚本在本机启动 OpenAI-compatible 测试 provider,预置一个本地上传图片和一个本地上传音频,并复用真实 `--agent-run`、本地落盘、`game.static_smoke` 和本地 HTTP 预览;脚本会断言 provider 请求体包含图片与音频资产上下文、生成 HTML 引用 `/assets/...`、预览服务能用 `GET` 读取这些资产、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、第二轮重跑 Evaluator 命中任务及其下游影响任务,未受影响组 carry-over,再自动给 CLI 发送回车停止预览。该脚本仅验证 runtime,不作为产品生成 fallback。
- 2026-06-25 调整:新增根级 `npm run ai-game-creator-shell:check` 作为 v1 开发验收入口,串起壳 typecheck、`platform-agent` 编排测试、`shared-contracts` 契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke,避免测试口径散落成多条手工命令。
- 2026-06-25 调整:`scripts/check-native-shells.mjs` 的 AI 游戏创作项从单独 typecheck 升级为 `npm run ai-game-creator-shell:check`,让原生壳总门禁覆盖 agent loop、本地落盘、静态自检和本地 HTTP 预览 smoke。
- 2026-06-25 调整:普通用户通过聊天输入 `/llm-status` 触发只读 `llm.config_check`,用于检查 LLM base_url、model 和 API Key 是否已从环境变量读取;状态消息不得显示或保存 API Key。终端可用 `npm run ai-game-creator-shell:llm-status` 做同类配置自检,缺配置时以非零状态退出。生成仍只从环境变量读取配置,不新增仓库文件、项目文件或普通用户界面里的 secret 持久化
- 2026-06-25 调整2026-06-30 更新:普通用户通过聊天输入 `/llm-status` 触发只读 `llm.config_check`,用于检查 LLM base_url、model 和 API Key 是否已从客户端配置读取;状态消息不得显示或保存 API Key。终端可用 `npm run ai-game-creator-shell:llm-status` 做同类配置自检,缺配置时以非零状态退出。发布 App 的真实密钥只放 Tauri 应用配置目录中的 `game-creator.config.json`;主窗口“配置”面板可读写该文件,但 API Key 不写入聊天、本地项目、trace 或 manifest
- 2026-06-25 调整:`npm run ai-game-creator-shell:dev` 固定加载 `http://127.0.0.1:3080/`Vite 继续 `strictPort` 与 Tauri `devUrl` 对齐。`beforeDevCommand` 改为先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,避免上次 Tauri 退出后遗留的同 app Vite 进程导致二次启动失败;如果 3080 是其它服务,仍直接失败并要求释放端口,不做端口漂移。
- 2026-06-25 调整:`preview.start` / `preview.stop` 必须追加 `.agent/logs/preview.log`,并把该日志列入 Preview trace step 的输出路径和 artifact 清单;这样 `preview-playtest` 任务声明的日志产物与实际本地 HTTP 预览行为一致。
- 2026-06-25 调整:AI 游戏创作 App v1 仍只维护一个全局本地 HTTP 预览实例;启动新项目预览替换旧预览时,必须 best-effort 把旧项目的 manifest preview 状态、`.agent/logs/preview.log` 和 run trace 记录为 stopped,避免旧项目状态残留 `running`。旧项目目录已删除时不阻断新预览启动。
@@ -3796,7 +3804,7 @@
- 2026-06-25 调整:普通用户通过聊天输入 `/run` 触发待确认 `game.run_local`,确认后只能复用白名单 `game.static_smoke` 自检当前 `game/index.html`,通过后启动 `127.0.0.1` 本地 HTTP 预览。独立执行 `game.static_smoke` 时如果已有 `.agent/run.latest.json`,必须追加 `Playtest / game.static_smoke` trace step,避免“运行了代码但编排 trace 不可见”。
- 2026-06-25 调整:普通用户通过聊天输入 `/trace` 触发只读 `agent.trace_read`,读取 `.agent/run.latest.json` 并在聊天里摘要 loop 轮次、stopReason、nextStep、active / carry-over 任务、repairRoutes、agent 建议命令和最近 step。trace 面板仍只在开发窗口展示,普通用户窗口不新增面板。
- 2026-06-25 调整:普通用户通过聊天输入 `/import-canvas-export /绝对/画板素材.zip 画板项目ID` 触发待确认 `canvas.export_import`,读取现有 `/editor/canvas` 素材导出 ZIP。导入命令只读取用户指定 ZIP,写入当前本地项目 `assets/canvas-imports/`,基础护栏限制路径逃逸、文件数量和解压体积;导出包没有真实 resourceId 时,用 `canvas-export:<file>` 作为可追踪 assetObjectId,不伪造后端画板资源行。
- 2026-06-25 调整:普通用户通过聊天输入 `/sync-canvas-project 画板项目ID` 触发待确认 `canvas.project_sync`,复用现有 `/api/external/v1/editor/projects/{projectId}` 读取画板项目快照,再用 `/api/external/v1/assets/read-url` 对 objectKey 或 legacy path 换签,下载资源到本地项目 `assets/canvas-sync/` 并登记为 `canvas` 来源资产;该命令从 `GENARRATIVE_GAME_CREATOR_EDITOR_API_KEY``GENARRATIVE_EXTERNAL_API_KEY` 读取平台 API Key,默认 base URL 为 `http://127.0.0.1:8082`,可用 `GENARRATIVE_GAME_CREATOR_EDITOR_API_BASE_URL` 覆盖。API Key 不写入 manifest、agent.db、trace 或日志。该路径不伪装浏览器登录态,也不绕过画板生成、钱包扣费或外部生成 worker;它只同步用户 API Key 已有权限读取的画板资源。
- 2026-06-25 调整2026-06-30 更新:普通用户通过聊天输入 `/sync-canvas-project 画板项目ID` 触发待确认 `canvas.project_sync`,复用现有 `/api/external/v1/editor/projects/{projectId}` 读取画板项目快照,再用 `/api/external/v1/assets/read-url` 对 objectKey 或 legacy path 换签,下载资源到本地项目 `assets/canvas-sync/` 并登记为 `canvas` 来源资产;该命令从客户端配置项 `editorApi.apiKey` 读取平台 API Key,默认 base URL 为 `http://127.0.0.1:8082`,可用 `editorApi.baseUrl` 覆盖。API Key 不写入 manifest、agent.db、trace 或日志。该路径不伪装浏览器登录态,也不绕过画板生成、钱包扣费或外部生成 worker;它只同步用户 API Key 已有权限读取的画板资源。
- 2026-06-25 调整:美术组 `Asset` 和音乐组 `SFX` 角色在 loop 中读取 `.agent/manifest.json`;当本地项目还没有对应类型的 `canvas` 来源资产时,角色 step 会追加 `agent.tool.suggest.canvas.project_sync` toolCall:美术组需要 `image/*``application/vnd.genarrative.image-sequence`,音乐组需要 `audio/*`。该 toolCall 只作为 trace 中的建议,不自动调用 External Editor API,也不绕过用户确认。
- 2026-06-24 调整:同一本地项目多次 `game.generate_draft` 必须追加 `memory/session.md``memory/project.md`,不得覆盖历史对话和创作目标记录。
- 2026-06-24 调整:普通用户通过聊天输入 `/help` 发现可用内置命令;命令发现必须留在聊天消息里,不得因此暴露开发面板。
@@ -3809,7 +3817,7 @@
- 2026-06-24 调整:普通用户只能通过聊天触发内置命令;当前 `/smoke` 映射到白名单 `command.run_limited game.static_smoke` 并走待确认卡片,不允许扩展成任意 shell 或自由命令解析。
- 2026-06-24 调整:普通用户通过聊天输入 `/project /绝对路径` 触发 `project.create` 待确认命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;不要把开发窗口项目路径输入框暴露到正式用户界面。
- 2026-06-25 调整:普通用户侧所有会写入、运行、查看 / 打开预览或导入本地产物的命令必须先完成 `/project` 初始化,包括 `game.generate_draft``asset.upload``game.run_local``command.run_limited``preview.start``preview.status``preview.open``preview.stop``memory.write``memory.delete``canvas.project_sync``canvas.asset_import``canvas.export_import`;没有已授权本地项目时只提示设置项目,不得落到默认 `/tmp` 草稿目录。
- 2026-06-24 调整:终端测试入口使用同一个 Tauri Rust 二进制的 `--agent-run <本地项目绝对路径> <创作需求>`,只复用现有 `game.generate_draft``game.static_smoke` 和本地 HTTP 预览链路,不另建第二套 agent runtimeLLM 配置只从环境变量读取,不写入仓库或项目文件。需要自动验证时可追加 `--no-wait`,生成预览 trace 后立即停止本地预览,避免命令卡在回车等待。
- 2026-06-24 调整2026-06-30 更新:终端测试入口使用同一个 Tauri Rust 二进制的 `--agent-run <本地项目绝对路径> <创作需求>`,只复用现有 `game.generate_draft``game.static_smoke` 和本地 HTTP 预览链路,不另建第二套 agent runtime发布 App 的 LLM 配置从 Tauri 应用配置目录读取,不写入仓库默认配置或项目文件。需要自动验证时可追加 `--no-wait`,生成预览 trace 后立即停止本地预览,避免命令卡在回车等待。
- 2026-06-24 调整:AI 游戏创作 App 的 release 配置只登记 `main` 聊天窗口,主窗口保持聊天尺寸;任务、文件、记忆、预览、日志和能力面板只能通过 debug/dev 下额外创建的 `developer` 窗口或 Vite dev `?dev/#dev` 查看,不进入普通用户窗口。
- 2026-06-24 调整:`check:native-shells` 必须静态守住 AI 游戏创作 App 的用户 / 开发边界:release 只保留 `main` 聊天窗口,`developer` 窗口只在 debug 创建,开发面板只能在 `devMode` 分支渲染。
- 2026-06-25 调整:`check:native-shells``ai-game-creator-shell:check` 之后必须追加 `ai-game-creator-shell:build -- --no-bundle`,让原生壳总门禁同时证明 AI 游戏创作独立 Tauri 壳能完成 release 编译,而不是只证明前端 / Rust 逻辑测试通过。
@@ -2,7 +2,7 @@
## 目标
在 Genarrative 内建设独立桌面 App:普通用户只看到聊天入口,通过聊天、上传文件和确认操作,让平台生成保存在本地的可运行 Web 游戏原型,并通过本地 HTTP server 预览。任务、文件、预览和日志等工程面板放到开发构建的独立开发窗口。v1 只做 Web 小游戏原型闭环,不扩展 Unity、Godot、云同步或插件市场。
在 Genarrative 内建设独立桌面 App:普通用户主要使用聊天入口,通过聊天、上传文件和确认操作,让平台生成保存在本地的可运行 Web 游戏原型,并通过本地 HTTP server 预览;主窗口提供运行时配置入口,用于保存发布版 AppData / Tauri 配置目录里的 LLM 与画板 API 配置。任务、文件、预览和日志等工程面板放到开发构建的独立开发窗口。v1 只做 Web 小游戏原型闭环,不扩展 Unity、Godot、云同步或插件市场。
## 技术选择
@@ -66,7 +66,7 @@ game-project/
- 默认后台生成,用户需要精修时打开画板继续编辑。
- `canvas.project_open` 只打开本机 Genarrative 编辑器的 `/editor/canvas?projectid=...`,默认地址为 `http://127.0.0.1:3000`,开发者可在开发窗口改成本机端口;不允许打开远程站点或任意 URL。
- 画板资源回流到本地项目 `assets/`,并在 manifest 中记录画板项目、资源 ID、assetObjectId、prompt、model、taskId 和 assetKind;当前最小落地提供 `asset.register` 登记项目内已有资产,并提供 `canvas.export_import` 读取现有画板素材导出 ZIP。
- `canvas.project_sync` 复用 Genarrative External Editor API,读取用户平台 API Key 可访问的画板项目快照,通过 `/api/external/v1/assets/read-url` 换签并把资源下载到本地项目 `assets/canvas-sync/`;默认 API base URL 为 `http://127.0.0.1:8082`,可用 `GENARRATIVE_GAME_CREATOR_EDITOR_API_BASE_URL` 覆盖,API Key 从 `GENARRATIVE_GAME_CREATOR_EDITOR_API_KEY``GENARRATIVE_EXTERNAL_API_KEY` 读取,不写入项目文件、trace、manifest 或日志。
- `canvas.project_sync` 复用 Genarrative External Editor API,读取用户平台 API Key 可访问的画板项目快照,通过 `/api/external/v1/assets/read-url` 换签并把资源下载到本地项目 `assets/canvas-sync/`;默认 API base URL 为 `http://127.0.0.1:8082`,可用 Tauri 应用配置目录中的 `game-creator.config.json``editorApi.baseUrl` 覆盖,API Key 从同一配置的 `editorApi.apiKey` 读取,不写入项目文件、trace、manifest 或日志。
- Agent loop 中美术组 `Asset` 和音乐组 `SFX` 会读取 `.agent/manifest.json`;当本地项目尚无对应类型的 `canvas` 来源资产时,会在 `.agent/run.latest.json` 的角色 step 中追加 `agent.tool.suggest.canvas.project_sync` 建议:美术组需要 `image/*``application/vnd.genarrative.image-sequence`,音乐组需要 `audio/*`。该建议提示用户通过聊天确认 `/sync-canvas-project <画板项目ID>` 回流画板资源,只进入 trace,不自动调用外部 API,也不绕过用户确认。
- `canvas.asset_import` 当前作为最小真实链路:导入项目目录内已有文件为 `canvas` 来源资产,并要求记录画板项目 ID 以及 resourceId 或 assetObjectId。
- `canvas.export_import` 复用 `/editor/canvas` 已有素材导出 ZIP 格式,读取根 `metadata.json`、复制 `images/` / `media/` / `sequences/` 到本地项目 `assets/canvas-imports/`,再按导出层登记为 `canvas` 来源资产;导出包不保存真实 resourceId 时,使用 `canvas-export:<file>` 作为可追踪 assetObjectId,不伪造后端资源行。
@@ -90,11 +90,11 @@ game-project/
## v1 验收证据矩阵
- `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、聊天侧 `/capabilities` 展示标准 Agent 能力清单且不打开开发面板、聊天侧 `/audit` 从 manifest / 本地文件 / `.agent/run.latest.json` 分别汇总用户面、6 组任务配置、6 组协作证据、任务编排、loop、记忆、本地产物、HTTP 预览、画板回流和权限日志证据且不打开开发面板;未生成 `.agent/run.latest.json` 前,`/audit` 只能标记任务配置通过,不能把 6 组协作证据误判为通过;trace 已存在但状态为 `failed``needs-revision``running``max-passes-exhausted` 或缺少 `Evaluator passed` 步骤时,`/audit` 不能把 loop 误判为通过。聊天侧 `/llm-status` 只显示 base_url / model / API Key 已读取状态且不泄露密钥本体、聊天侧长期记忆查看 / 追加 / 覆盖 / 删除的授权本地项目路径、上传资产写入后的 manifest 刷新和 `/assets` 聊天可见性、`/smoke` 聊天侧确认后只通过授权本地项目路径执行白名单 `game.static_smoke``/run` 聊天侧确认后通过授权本地项目路径执行 `game.static_smoke`、启动 `127.0.0.1` 本地预览并交给外部浏览器、`/preview` 聊天侧确认后通过授权本地项目路径启动 `127.0.0.1` 本地预览并交给外部浏览器、`/status` 聊天侧项目 / 任务 / 资产 / 预览 / 最近命令汇总、`/files` 聊天侧本地文件列表、`/read` 聊天侧文件读取的授权本地项目路径、`/tasks` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后实时展示 Planner / Orchestrator / 角色 brief / Generator / Evaluator / 写盘 / 自检进度,并自动读取 `.agent/run.latest.json` 在普通聊天消息里展示 Run、LLM 对话、loop 轮次、工具调用、active / carry-over 任务、返工焦点、编排轮次、最近步骤、画板同步建议命令和本地产物快照、`/trace` 聊天侧读取 `.agent/run.latest.json` 并展示 loop 轮次 / active 任务 / 返工路线 / dependency waves 的授权本地项目路径、`platform-agent` 编排测试、共享契约测试、Tauri 本地能力测试和无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环,并覆盖 loop 跑满 3 轮失败时不会写入最终游戏产物。
- `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、主窗口运行时配置面板读写 Tauri 配置目录中的 `game-creator.config.json` 且不把 API Key 写入聊天、聊天侧 `/capabilities` 展示标准 Agent 能力清单且不打开开发面板、聊天侧 `/audit` 从 manifest / 本地文件 / `.agent/run.latest.json` 分别汇总用户面、6 组任务配置、6 组协作证据、任务编排、loop、记忆、本地产物、HTTP 预览、画板回流和权限日志证据且不打开开发面板;未生成 `.agent/run.latest.json` 前,`/audit` 只能标记任务配置通过,不能把 6 组协作证据误判为通过;trace 已存在但状态为 `failed``needs-revision``running``max-passes-exhausted` 或缺少 `Evaluator passed` 步骤时,`/audit` 不能把 loop 误判为通过。聊天侧 `/llm-status` 只显示 base_url / model / API Key 已读取状态且不泄露密钥本体、聊天侧长期记忆查看 / 追加 / 覆盖 / 删除的授权本地项目路径、上传资产写入后的 manifest 刷新和 `/assets` 聊天可见性、`/smoke` 聊天侧确认后只通过授权本地项目路径执行白名单 `game.static_smoke``/run` 聊天侧确认后通过授权本地项目路径执行 `game.static_smoke`、启动 `127.0.0.1` 本地预览并交给外部浏览器、`/preview` 聊天侧确认后通过授权本地项目路径启动 `127.0.0.1` 本地预览并交给外部浏览器、`/status` 聊天侧项目 / 任务 / 资产 / 预览 / 最近命令汇总、`/files` 聊天侧本地文件列表、`/read` 聊天侧文件读取的授权本地项目路径、`/tasks` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后实时展示 Planner / Orchestrator / 角色 brief / Generator / Evaluator / 写盘 / 自检进度,并自动读取 `.agent/run.latest.json` 在普通聊天消息里展示 Run、LLM 对话、loop 轮次、工具调用、active / carry-over 任务、返工焦点、编排轮次、最近步骤、画板同步建议命令和本地产物快照、`/trace` 聊天侧读取 `.agent/run.latest.json` 并展示 loop 轮次 / active 任务 / 返工路线 / dependency waves 的授权本地项目路径、`platform-agent` 编排测试、共享契约测试、Tauri 本地能力测试和无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环,并覆盖 loop 跑满 3 轮失败时不会写入最终游戏产物。
- `npm run check:native-shells`:覆盖 AI 游戏创作壳的 release/dev 窗口边界、正式用户 App 不嵌入游戏预览 iframe、用户侧预览命令交给外部浏览器和 Tauri release `--no-bundle` 构建 smoke;用于证明正式用户窗口只登记 `main` 聊天窗口,开发面板只在 debug/dev 路径打开,独立壳能完成 release 编译。
- `npm run check:encoding``git diff --check`:覆盖中文文档、中文命令文案和补丁空白;用于避免乱码、尾随空白和无关格式漂移。
- `npm run ai-game-creator-shell:llm-status`:只检查 LLM 环境变量是否就绪,不请求上游、不显示 API Key;用于本机联调前确认配置。CLI 和桌面 App 内的 `/llm-status` / 生成入口都会先读取仓库根目录或 `apps/ai-game-creator-shell/` 下 gitignored 的 `.env.secrets.local`,再检查当前进程环境
- `npm run ai-game-creator-shell:agent-run -- --no-wait /绝对项目路径 "游戏创作需求"`:使用真实 LLM provider 配置跑一次本地生成、落盘、自检和预览;用于人工验收真实 provider 路径。真实 provider 放在 gitignored 的 `.env.secrets.local` 中,至少包含 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY``GENARRATIVE_GAME_CREATOR_LLM_BASE_URL``GENARRATIVE_GAME_CREATOR_LLM_MODEL`;默认 API kind 为 `openai_responses`,旧 Chat Completions 兼容网关需显式设置 `GENARRATIVE_GAME_CREATOR_LLM_API_KIND=openai_chat`Anthropic Messages 网关设置 `GENARRATIVE_GAME_CREATOR_LLM_API_KIND=anthropic`URL 会在 base URL 后拼 `/v1/messages`,例如 Minimax Anthropic base URL 可配置为 `https://api.minimaxi.com/anthropic`;真实网关长请求若在非流式响应前被 60 秒空闲连接切断,联调时设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true`
- `npm run ai-game-creator-shell:llm-status`:只检查 LLM 客户端配置是否就绪,不请求上游、不显示 API Key;用于本机联调前确认配置。发布版启动时会在 Tauri 应用配置目录生成默认 `game-creator.config.json`,仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板
- `npm run ai-game-creator-shell:agent-run -- --no-wait /绝对项目路径 "游戏创作需求"`:使用真实 OpenAI-compatible 配置跑一次本地生成、落盘、自检和预览;用于人工验收真实 provider 路径。真实 provider 配置放在 Tauri 应用配置目录的 `game-creator.config.json` 中,至少设置 `llm.apiKey`,需要覆盖默认服务时设置 `llm.baseUrl``llm.model`;默认 API kind 为 `openai_responses`,旧 Chat Completions 兼容网关设置 `llm.apiKind``openai_chat`Anthropic Messages 网关设置 `llm.apiKind``anthropic`URL 会在 base URL 后拼 `/v1/messages`,例如 Minimax Anthropic base URL 可配置为 `https://api.minimaxi.com/anthropic`;真实网关长请求若在非流式响应前被 60 秒空闲连接切断,联调时设置 `llm.stream``true`
## 当前最小落地
@@ -102,15 +102,16 @@ game-project/
- 本地项目初始化会创建 `game/``assets/``memory/``exports/``.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地产物索引 `.agent/agent.db`,并生成默认 `game/index.html`
- 普通用户可在聊天框输入 `/project /绝对路径` 生成待确认的 `project.create` 命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;开发窗口仍可直接编辑项目路径。
- 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。
- 终端可用 `npm run ai-game-creator-shell:llm-status` 检查 LLM 环境变量是否就绪;CLI 和桌面 App 内的 `/llm-status` / 生成入口都会先读取 gitignored 的 `.env.secrets.local`不请求上游、不显示 API Key,缺配置时以非零状态退出或在聊天里提示未就绪。
- 终端可用 `npm run ai-game-creator-shell:llm-status` 检查 LLM 客户端配置是否就绪;桌面 App 主窗口“配置”面板可读写 Tauri 应用配置目录中的 `game-creator.config.json``/llm-status` / 生成入口读取同一份配置,CLI 开发入口无 AppHandle 时才回退读取仓库旁边的配置模板和 gitignored 本机覆盖文件;不请求上游、不显示 API Key,缺配置时以非零状态退出或在聊天里提示未就绪。
- 终端可用 `npm run ai-game-creator-shell:check` 跑 v1 开发验收:壳 typecheck、`platform-agent` 编排测试、共享契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke。
- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;该入口读取当前环境和 gitignored 的 `.env.secrets.local`,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。默认 API kind 为 `openai_responses`;旧 Chat Completions 兼容网关设置 `GENARRATIVE_GAME_CREATOR_LLM_API_KIND=openai_chat`Anthropic Messages 网关设置 `GENARRATIVE_GAME_CREATOR_LLM_API_KIND=anthropic`。真实 OpenAI-compatible 网关建议设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 跑 Planner 和 Generator,避免长请求非流式空闲断连。
- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;发布 App 读取 Tauri 应用配置目录中的 `game-creator.config.json`,开发 CLI 无 AppHandle 时才读取仓库旁边的配置模板和 gitignored 本机覆盖文件,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。默认 API kind 为 `openai_responses`;旧 Chat Completions 兼容网关设置 `llm.apiKind``openai_chat`Anthropic Messages 网关设置 `llm.apiKind``anthropic`。真实 OpenAI-compatible 网关建议设置 `llm.stream``true` 跑 Planner 和 Generator,避免长请求非流式空闲断连。
- 终端可用 `npm run ai-game-creator-shell:agent-run:smoke` 跑一次无密钥本地端到端 smoke:脚本启动本机 OpenAI-compatible SSE 流式测试 provider,预置一个本地上传图片和一个本地上传音频,复用真实 `--agent-run`、Planner / Orchestrator / 角色 agent / Generator / Evaluator loop、本地落盘、`game.static_smoke` 和本地 HTTP 预览,并断言每次 provider 请求都使用 `stream: true`、provider prompt 收到图片与音频资产上下文、生成 HTML 引用这些资产、预览服务能用 `GET` 读取 `/assets/...`、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、`.agent/run.latest.json` 的 step group 覆盖 design / balance / art / audio / code / publishing 六组、第二轮会重跑 Evaluator 命中任务及其下游影响任务,未受影响角色 carry-over;随后脚本自动给 CLI 发送回车停止预览。该脚本只用于开发验证,不进入产品生成路径。
- `npm run ai-game-creator-shell:dev` 的 Tauri `devUrl` 固定为 `http://127.0.0.1:3080/`Vite 必须 `strictPort` 对齐;`beforeDevCommand` 先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,否则才启动新的 Vite,若端口被其它服务占用则直接失败并提示释放端口。
- `.agent/manifest.json` 会保存 6 个专业组下 16 个组内角色任务状态,当前覆盖 `Director``Gameplay``Difficulty``Asset``Polish``SFX``Code``Review``Preview``Playtest``Publish`;程序组内显式包含 `quality-review` 质量评审 gate,由 Evaluator trace 标记完成;开发窗口的专业组面板读取 manifest,而不是前端硬编码。
- 共享契约和 `platform-agent` 会按任务依赖与 `completed` 状态计算当前可执行任务,作为 v1 的最小编排选择器;每轮 `Orchestrator` 的 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 由 `platform-agent` 纯编排内核产出,`apps/ai-game-creator-shell` 只负责写入 `.agent/passes/pass-N/` 和执行本地工具;`Evaluator` 会在 `.agent/findings.md` 写出 `## Repair Routes` JSON,下一轮编排优先采用该结构化 taskIds,解析不到时才退回关键词路由;返工路由会按任务图自动扩展下游影响任务,例如美术资产变化会继续触发程序预览和运营包装重算。
- `game.generate_draft` 使用 LLM provider 配置生成结构化 JSON 草案,读取 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY` / `GENARRATIVE_LLM_API_KEY` / `LLM_API_KEY` / `OPENAI_API_KEY``GENARRATIVE_GAME_CREATOR_LLM_BASE_URL` / `GENARRATIVE_LLM_BASE_URL` / `LLM_BASE_URL` / `OPENAI_BASE_URL``GENARRATIVE_GAME_CREATOR_LLM_MODEL` / `GENARRATIVE_LLM_MODEL` / `LLM_MODEL` / `OPENAI_MODEL`;默认 API kind 为 `openai_responses`,可通过 `GENARRATIVE_GAME_CREATOR_LLM_API_KIND=openai_chat` 切回旧 Chat Completions 兼容网关,通过 `GENARRATIVE_GAME_CREATOR_LLM_API_KIND=anthropic` 走 Anthropic Messages`GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 时 Planner 和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。
- 聊天输入 `/llm-status` 会触发只读 `llm.config_check`确认 LLM base_url、model 和 API Key 是否已从环境变量读取;状态消息不会显示或保存 API Key
- `game.generate_draft` 使用 OpenAI-compatible LLM 配置生成结构化 JSON 草案,发布 App 的配置项来自 Tauri 应用配置目录中的 `game-creator.config.json``llm.apiKey``llm.baseUrl``llm.model``llm.apiKind``llm.stream``llm.requestTimeoutMs``llm.maxRetries``llm.retryBackoffMs`;默认 API kind 为 `openai_responses`,可 `llm.apiKind=openai_chat` 切回旧 Chat Completions 兼容网关, `llm.apiKind=anthropic` 走 Anthropic Messages`llm.stream=true` 时 Planner 和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。
- 主窗口“配置”面板读写 Tauri 应用配置目录中的 `game-creator.config.json`覆盖 LLM API Key、base URL、模型、API 类型、流式请求、超时、重试和画板 External API 配置;保存时只写运行时配置文件,不写仓库模板、本地项目、trace 或 manifest
- 聊天输入 `/llm-status` 会触发只读 `llm.config_check`,确认 LLM base_url、model 和 API Key 是否已从客户端配置读取;状态消息不会显示或保存 API Key。
- `game.generate_draft` 的 LLM JSON 必须包含 `handoffs` 数组,覆盖 `design``balance``art``audio``code``publishing` 6 个专业组;每组必须给出 role、summary、outputs 和 next,缺组或交接内容不完整会判定为模型输出无效并进入返工。
- `game.generate_draft` 的真实生成路径使用最小 Planner / Orchestrator / 组内角色 agent / Generator / Evaluator loopPlanner 写 `.agent/spec.md`;每轮 Orchestrator 先写 `.agent/passes/pass-N/agenda.md``.agent/passes/pass-N/task-graph.json`,首轮全量调度 16 个角色任务,返工轮按 `.agent/findings.md` 生成结构化 `repairRoutes`,重跑命中问题的角色任务及其下游依赖任务,其余角色 brief 从上一轮 carry-over`task-graph.json` 记录 activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和按依赖排序的 dependencyWaves;角色 brief 写入 `.agent/passes/pass-N/groups/<group>/*.md`,再汇总为 `.agent/passes/pass-N/groups/*.md`;Generator 必须读取用户需求、记忆、`.agent/spec.md`、本轮 `agenda.md``task-graph.json``.agent/findings.md` 和 6 组汇总 brief 后返回结构化 JSON;每轮会把 Generator 草案拆成 6 组交接快照,写入 `.agent/passes/pass-N/`Evaluator 做质量评审并写 `.agent/findings.md`,通过后才进入 `game.static_smoke` 静态自检和预览试玩。
- loop 最多执行 3 轮;Evaluator 发现 HTML 非自包含、缺少 `canvas`、缺少 `requestAnimationFrame`、缺少输入监听或用户输入未转义时,把问题写入 `.agent/findings.md` 并让下一轮 Generator 修复。3 轮仍失败则 `game.generate_draft` 失败,不写最终游戏产物。
@@ -455,7 +455,7 @@ OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs,但本地日
结构化创作 / RPG 的 Responses JSON 链路默认不打开 `web_search`;本地和生产如需联网增强,必须显式配置 `GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED=true``GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=true`。如果上游未开通工具,Responses 可能先吐自然语言再返回 `ToolNotOpen`,这类报错应按工具不可用排查,不要先当成 JSON 解析 bug。
`platform-llm` 文本请求默认使用 Responses 协议;需要接旧 OpenAI Chat Completions 兼容网关时,调用方必须显式选择 Chat Completions。AI 游戏创作独立 App 也默认使用 Responses,可在本地 `.env.secrets.local` 中设置 `GENARRATIVE_GAME_CREATOR_LLM_API_KIND=openai_chat` 接旧 Chat Completions 兼容网关,或 `GENARRATIVE_GAME_CREATOR_LLM_API_KIND=anthropic` 接 Anthropic Messages。
`platform-llm` 文本请求默认使用 Responses 协议;需要接旧 OpenAI Chat Completions 兼容网关时,调用方必须显式选择 Chat Completions。AI 游戏创作独立 App 是客户端,不读取 `.env`;发布 App 启动时会在 Tauri 应用配置目录生成 `game-creator.config.json`,主窗口“配置”面板读写该运行时文件,真实密钥和本机覆盖项写入该文件,仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板,开发 CLI 无 AppHandle 时才回退读取仓库旁边的 gitignored 覆盖文件。LLM 维度由 `llm.apiKind` 控制,默认 `openai_responses`,可设为 `openai_chat` 接旧 Chat Completions 兼容网关,或 `anthropic` 接 Anthropic Messages。
创意 Agent `gpt-5` 文本链路已从 APIMart 切到 VectorEngine`api-server` 读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 构造 OpenAI-compatible LLM client,并自动补齐 `/v1` 前缀用于 Responses 协议。排查或切换密钥后,可在本地运行: