修复运行时配置读取时 GameAgent 窗口卡死 #263
@@ -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<GameCreatorLlmConfigStatus, String> {
|
||||
tokio::task::spawn_blocking(check_game_creator_llm_config_from_config)
|
||||
.await
|
||||
.map_err(|error| format!("LLM 配置检查失败:{error}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -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<GameCreatorLlmConfigStatus>(
|
||||
'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<GameCreatorLlmConfigStatus>(
|
||||
'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<GameCreatorLlmConfigStatus>(
|
||||
'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: '保存成功,新的运行时配置已生效',
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 和可取得的退出状态。
|
||||
|
||||
Reference in New Issue
Block a user