From d653a5f5a266960acfcf20a372b1129ae90d22a3 Mon Sep 17 00:00:00 2001 From: suzmii Date: Thu, 3 Sep 2026 12:04:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BF=90=E8=A1=8C=E6=97=B6?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=AF=BB=E5=8F=96=E6=97=B6=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E5=8D=A1=E6=AD=BB=20=E9=85=8D=E7=BD=AE=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E4=B8=8ELLM=E8=AF=8A=E6=96=AD=E8=A7=A3=E8=80=A6=20=E5=B0=86LLM?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=E6=8E=A2=E6=B5=8B=E7=A7=BB=E5=85=A5=E9=98=BB?= =?UTF-8?q?=E5=A1=9E=E7=BA=BF=E7=A8=8B=E6=B1=A0=20=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=9D=A2=E6=9D=BF=E5=9B=9E=E5=BD=92=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=B8=8E=E6=8A=80=E6=9C=AF=E6=96=B9=E6=A1=88=E8=AF=B4?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/commands.rs | 6 +- .../runtime-config/RuntimeConfigDialog.tsx | 40 +++++---- .../appSurface/runtime-settings.suite.ts | 88 +++++++++++++++++++ ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 4 + 4 files changed, 118 insertions(+), 20 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index 3d6744e87..ebb2f1b1f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -1832,8 +1832,10 @@ pub(crate) fn schedule_game_creator_agent_ready_tasks( } #[tauri::command] -pub(crate) fn check_game_creator_llm_config() -> GameCreatorLlmConfigStatus { - check_game_creator_llm_config_from_config() +pub(crate) async fn check_game_creator_llm_config() -> Result { + tokio::task::spawn_blocking(check_game_creator_llm_config_from_config) + .await + .map_err(|error| format!("LLM 配置检查失败:{error}")) } #[tauri::command] diff --git a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx index 8cd344cea..101eb3e56 100644 --- a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx +++ b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx @@ -274,6 +274,7 @@ export function RuntimeConfigDialog({ const [appUpdateStatus, setAppUpdateStatus] = useState(''); const [appUpdateChecking, setAppUpdateChecking] = useState(false); const runtimeConfigBusyRef = useRef(false); + const runtimeConfigDiagnosticRequestRef = useRef(0); useEscapeToClose(onClose); @@ -478,6 +479,25 @@ export function RuntimeConfigDialog({ })); } + async function refreshEffectiveLlmConfigStatus() { + const invoke = resolveTauriInvoke(); + if (!invoke) { + return; + } + const requestId = ++runtimeConfigDiagnosticRequestRef.current; + try { + const status = await invoke( + 'check_game_creator_llm_config', + ); + if (requestId === runtimeConfigDiagnosticRequestRef.current) { + setEffectiveLlmConfigStatus(status); + } + } catch { + // The diagnostic is supplementary. Config read/save must remain usable + // when the command is unavailable or the Codex probe fails. + } + } + async function readRuntimeConfig() { if (runtimeConfigBusyRef.current) { return; @@ -502,15 +522,7 @@ export function RuntimeConfigDialog({ ); setRuntimeConfigDraft(config); setRuntimeConfigStatus(`已读取:${result.path}`); - try { - const status = await invoke( - 'check_game_creator_llm_config', - ); - setEffectiveLlmConfigStatus(status); - } catch { - // Older test fixtures and non-Tauri previews may not expose the - // diagnostic command; the config read itself remains usable. - } + void refreshEffectiveLlmConfigStatus(); onLog?.('runtime_config.read'); } catch (error) { setRuntimeConfigStatus( @@ -559,15 +571,7 @@ export function RuntimeConfigDialog({ ); setRuntimeConfigDraft(savedConfig); setRuntimeConfigStatus(`已保存:${result.path}`); - try { - const status = await invoke( - 'check_game_creator_llm_config', - ); - setEffectiveLlmConfigStatus(status); - } catch { - // Keep the last safe status when the optional diagnostic refresh is - // unavailable in a preview or test fixture. - } + void refreshEffectiveLlmConfigStatus(); setRuntimeConfigToast({ tone: 'success', message: '保存成功,新的运行时配置已生效', diff --git a/apps/ai-game-creator-shell/tests/appSurface/runtime-settings.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/runtime-settings.suite.ts index ece3e4022..a22045fc6 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/runtime-settings.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/runtime-settings.suite.ts @@ -757,6 +757,94 @@ export function registerRuntimeSettingsTests() { false, ); }); + + it('does not block runtime config actions while LLM diagnostics are pending', async () => { + let resolveRead: + | ((value: { + path: string; + config: { + agentMode: 'provider'; + llm: { + apiKey: string; + baseUrl: string; + model: string; + apiKind: string; + stream: boolean; + webSearchEnabled: boolean; + requestTimeoutMs: number; + maxRetries: number; + retryBackoffMs: number; + }; + editorApi: { baseUrl: string; apiKey: string }; + }; + }) => void) + | undefined; + let resolveDiagnostic: ((value: unknown) => void) | undefined; + const invoke = vi.fn((command: string) => { + if (command === 'read_game_creator_app_config') { + return new Promise((resolve) => { + resolveRead = resolve as typeof resolveRead; + }); + } + if (command === 'check_game_creator_llm_config') { + return new Promise((resolve) => { + resolveDiagnostic = resolve; + }); + } + throw new Error(`unexpected invoke ${command}`); + }); + window.__TAURI__ = { core: { invoke } }; + renderLauncherAt('/?launcher'); + + fireEvent.click(screen.getByRole('button', { name: '配置' })); + expect( + await screen.findByRole('dialog', { name: '运行时配置' }), + ).not.toBeNull(); + expect(await screen.findByText('正在读取')).not.toBeNull(); + + await act(async () => { + resolveRead?.({ + path: '/home/test/AppData/game-creator.config.json', + config: { + agentMode: 'provider', + llm: { + apiKey: '', + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-4.1', + apiKind: 'openai_responses', + stream: false, + webSearchEnabled: false, + requestTimeoutMs: 180000, + maxRetries: 0, + retryBackoffMs: 500, + }, + editorApi: { + baseUrl: 'http://127.0.0.1:8082', + apiKey: '', + }, + }, + }); + }); + + expect(await screen.findByText(/已读取:/)).not.toBeNull(); + expect(screen.getByRole('button', { name: '读取' })).toHaveProperty( + 'disabled', + false, + ); + expect(screen.getByRole('button', { name: '保存' })).toHaveProperty( + 'disabled', + false, + ); + expect(invoke).toHaveBeenCalledWith('check_game_creator_llm_config'); + + await act(async () => { + resolveDiagnostic?.({ + configured: true, + accountCredentialState: 'ready', + }); + }); + expect(await screen.findByText('账号权限已就绪')).not.toBeNull(); + }); } export function registerPublishedRuntimeSettingsTests() { diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 6e649a8c2..a822a17a4 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -220,6 +220,10 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创 - 模式升级:`agentMode` 扩为 `codex_app_server / codex_cli / provider`,新默认为 `codex_app_server`;V1.51 的一次性 `codex exec` 保留为显式兼容模式,HTTP Provider 保留为非 Responses 配置及故障回退模式。 - 进程与节点:External Runner 按“有效 Agent LLM 凭据/Responses 路由 + `projectId/agentId/sessionId/runId`”隔离长期 `codex app-server --stdio`,即每个权威节点 run 直接持有自己的 Codex CLI 子进程与 ephemeral thread,每次完整权威请求映射 turn。同一节点 turn 串行,节点之间进程级隔离;单节点连接失败不得使其它节点同时失去终态。Codex thread 不写 durable recovery;节点完成、重启、retry、handoff 和 finalization 仍只认 AGC 账本。 - LLM 配置:`apiKind` 始终只接受 `openai_responses`;非空 Key 转换为 app-server model provider,base URL 生效,Key 仅走专用环境变量;空 Key 只桥接用户 Codex `auth.json`,不继承环境 `CODEX_API_KEY`。设置面板在 app-server 模式继续显示并保存 model、effort、stream、全局/逐 Agent Key 与路由配置;`openai_chat / anthropic` 明确提示切 `provider`,不得悄悄忽略。`stream=true` 接入 app-server 文本 delta;`webSearchEnabled=true` 只允许 DirectProject 经客户端审核的 `agc_web_search` 使用,不得启用 Codex 原生 webSearch 或任意网络。 +- 配置面板交互:读取或保存配置只等待本地配置文件操作; + `check_game_creator_llm_config` 属于补充诊断,必须后台刷新并带有过期结果保护, + 不得让 Codex CLI/app-server 能力探测阻塞配置内容展示或保存反馈;Tauri 命令本身也必须在阻塞线程池执行, + 不得占用窗口事件线程。诊断失败、超时或账号未登录时,仅更新账号权限提示。 - 安全与取消:临时 cwd、隔离 `CODEX_HOME` 与 OS HOME、read-only、network off、never approval,并在启动前关闭 web/multi-agent/shell/browser/plugin/image 等原生能力;取消从 turn-start pending 阶段就跟踪且只 interrupt 当前 turn。已发送 turn 后连接断开或终态丢失进入 reconciliation,只关闭当前节点进程且不重放同一 request slot;明确 failed/interrupted 不按 transport 重试。 - remote-control 认证边界:没有 ChatGPT `auth.json` 的 API Key / provider-proxy app-server 在启动时设置 Codex 内部环境变量 `CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED=1`,让 remote-control 以 `desired_state=Disabled` 启动,避免上游进入 1Hz 认证重试;不再依赖需要 ChatGPT 登录态的 `remoteControl/disable` RPC。只有实际桥接 ChatGPT 登录态的 AuthBridge 保持 remote-control 可用。API Key 子进程同时使用 `RUST_LOG=warn` 收敛剩余预期噪音,不伪造 `auth.json` 或静默继续。 - 资源与退出:app-server pool 按实际凭据快照/base URL/API kind/CLI 版本和节点 run 身份隔离并做有界 LRU;空 AppData Key 必须读取同一份有界 `auth.json` 字节来生成池指纹并桥接隔离登录态,继承的 `CODEX_API_KEY` 始终移除,node thread 也只淘汰 inactive LRU。Runner 正常、强制和 watchdog 退出都显式关池,Linux child 绑定 parent-death signal,防止强杀 Runner 后遗留带凭据孤儿进程。stdout NDJSON 与 stderr 无换行记录均有硬上限;stderr 原文不写入诊断,只记录固定分类、总字节数、SHA-256 和可取得的退出状态。 -- 2.52.0