Merge branch 'master' into feat/msg-queue-to-rust
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m49s
Project CI / Frontend tests (pull_request) Successful in 4m14s
Project CI / Repository checks (pull_request) Successful in 4m34s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m13s
Project CI / Backend tests (pull_request) Successful in 7m10s
Project CI / Native shell tests (pull_request) Successful in 7m58s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m49s
Project CI / Frontend tests (pull_request) Successful in 4m14s
Project CI / Repository checks (pull_request) Successful in 4m34s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m13s
Project CI / Backend tests (pull_request) Successful in 7m10s
Project CI / Native shell tests (pull_request) Successful in 7m58s
This commit is contained in:
@@ -1075,11 +1075,14 @@ export interface AdminRechargeRefundActionResponse {
|
||||
export interface AdminWalletRestrictionResponse {
|
||||
wallet: AdminProfileWalletPayload;
|
||||
}
|
||||
export type AdminAgcAgentMode = 'codex' | 'cc';
|
||||
|
||||
export interface AdminAgcModel {
|
||||
id: string;
|
||||
alias: string;
|
||||
modelId: string;
|
||||
enabled: boolean;
|
||||
agentMode: AdminAgcAgentMode;
|
||||
}
|
||||
|
||||
export interface AdminAgcModelCatalog {
|
||||
|
||||
@@ -23,14 +23,23 @@ vi.mock('../components/useAdminWriteConfirm', () => ({
|
||||
confirmDialog: null,
|
||||
}),
|
||||
}));
|
||||
afterEach(cleanup);
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('edits alias and upstream model without changing the stable identifier or revision', async () => {
|
||||
const catalog = {
|
||||
revision: 3,
|
||||
defaultModelId: 'quality',
|
||||
models: [
|
||||
{ id: 'quality', alias: '高质量', modelId: 'gpt-6-astra', enabled: true },
|
||||
{
|
||||
id: 'quality',
|
||||
alias: '高质量',
|
||||
modelId: 'gpt-6-astra',
|
||||
enabled: true,
|
||||
agentMode: 'codex' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
|
||||
@@ -54,3 +63,44 @@ test('edits alias and upstream model without changing the stable identifier or r
|
||||
expect(screen.getAllByText('已保存').length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
test('new models default to Codex and save the selected Agent mode', async () => {
|
||||
const catalog = {
|
||||
revision: 3,
|
||||
defaultModelId: 'quality',
|
||||
models: [
|
||||
{
|
||||
id: 'quality',
|
||||
alias: '高质量',
|
||||
modelId: 'gpt-6-astra',
|
||||
enabled: true,
|
||||
agentMode: 'codex' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
|
||||
vi.mocked(saveAgcModelCatalog).mockImplementation(async (_, input) => ({
|
||||
...input,
|
||||
revision: 4,
|
||||
}));
|
||||
render(<AdminAgcModelsPage token="test" onUnauthorized={vi.fn()} />);
|
||||
await screen.findByDisplayValue('gpt-6-astra');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加模型' }));
|
||||
const agentMode = screen.getByLabelText('模型 2 Agent 模式');
|
||||
expect((agentMode as HTMLSelectElement).value).toBe('codex');
|
||||
fireEvent.change(agentMode, { target: { value: 'cc' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
||||
|
||||
await waitFor(() => {
|
||||
const calls = vi.mocked(saveAgcModelCatalog).mock.calls;
|
||||
const payload = calls[calls.length - 1]?.[1];
|
||||
expect(payload?.models).toHaveLength(2);
|
||||
expect(payload?.models[1]).toMatchObject({
|
||||
alias: '',
|
||||
modelId: '',
|
||||
enabled: true,
|
||||
agentMode: 'cc',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,6 +116,7 @@ export function AdminAgcModelsPage({
|
||||
columns={[
|
||||
{ key: 'alias', label: '别名' },
|
||||
{ key: 'modelId', label: '实际模型名' },
|
||||
{ key: 'agentMode', label: 'Agent 模式' },
|
||||
{ key: 'enabled', label: '启用' },
|
||||
{ key: 'default', label: '默认' },
|
||||
{ key: 'actions', label: '操作' },
|
||||
@@ -155,6 +156,7 @@ export function AdminAgcModelsPage({
|
||||
alias: '',
|
||||
modelId: '',
|
||||
enabled: true,
|
||||
agentMode: 'codex',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -211,6 +213,21 @@ export function AdminAgcModelsPage({
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
aria-label={`模型 ${index + 1} Agent 模式`}
|
||||
value={model.agentMode}
|
||||
disabled={busy}
|
||||
onChange={(e) =>
|
||||
update(model.id, {
|
||||
agentMode: e.target.value as AdminAgcModel['agentMode'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="codex">Codex</option>
|
||||
<option value="cc">Claude Code</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
aria-label={`模型 ${index + 1} 启用`}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@genarrative/ai-game-creator-agent-sidecar",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.285"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import readline from 'node:readline';
|
||||
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
const sessions = new Map();
|
||||
const active = new Map();
|
||||
let outputChain = Promise.resolve();
|
||||
|
||||
function write(value) {
|
||||
outputChain = outputChain
|
||||
.then(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
process.stdout.write(`${JSON.stringify(value)}\n`, (error) =>
|
||||
error ? reject(error) : resolve(),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.catch(() => undefined);
|
||||
return outputChain;
|
||||
}
|
||||
|
||||
function safeEnv(input) {
|
||||
const env = { ...process.env };
|
||||
if (input && typeof input === 'object') {
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (typeof value === 'string') env[key] = value;
|
||||
}
|
||||
}
|
||||
env.CLAUDE_AGENT_SDK_CLIENT_APP ??= 'genarrative-agc-agent-sidecar/0.1';
|
||||
return env;
|
||||
}
|
||||
|
||||
function buildOptions(request, controller) {
|
||||
const options = {
|
||||
abortController: controller,
|
||||
cwd: request.cwd,
|
||||
model: request.model,
|
||||
permissionMode: 'dontAsk',
|
||||
settingSources: [],
|
||||
tools: [],
|
||||
disallowedTools: ['Bash', 'Edit', 'Write', 'NotebookEdit'],
|
||||
verbatimPrompts: true,
|
||||
persistSession: true,
|
||||
includePartialMessages: true,
|
||||
env: safeEnv(request.env),
|
||||
};
|
||||
if (request.systemPrompt) {
|
||||
options.systemPrompt = {
|
||||
type: 'custom',
|
||||
prompt: request.systemPrompt,
|
||||
snapshot: true,
|
||||
};
|
||||
}
|
||||
if (request.resume) options.resume = request.resume;
|
||||
if (request.sessionId) options.sessionId = request.sessionId;
|
||||
if (request.mcpServers && typeof request.mcpServers === 'object') {
|
||||
options.mcpServers = request.mcpServers;
|
||||
}
|
||||
if (request.outputSchema) {
|
||||
options.outputFormat = {
|
||||
type: 'json_schema',
|
||||
schema: request.outputSchema,
|
||||
};
|
||||
}
|
||||
if (Number.isInteger(request.maxTurns) && request.maxTurns > 0) {
|
||||
options.maxTurns = request.maxTurns;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function runTurn(request) {
|
||||
const id = String(request.id ?? '');
|
||||
if (!id) return write({ type: 'error', message: 'sidecar turn 缺少 id' });
|
||||
if (active.has(id))
|
||||
return write({ id, type: 'error', message: 'sidecar turn 已存在' });
|
||||
const controller = new AbortController();
|
||||
active.set(id, { controller });
|
||||
try {
|
||||
const sessionKey =
|
||||
typeof request.sessionKey === 'string' ? request.sessionKey : '';
|
||||
const resume =
|
||||
request.resume || (sessionKey ? sessions.get(sessionKey) : undefined);
|
||||
const queryRequest = { ...request, resume };
|
||||
const stream = query({
|
||||
prompt: String(request.prompt ?? ''),
|
||||
options: buildOptions(queryRequest, controller),
|
||||
});
|
||||
for await (const message of stream) {
|
||||
if (
|
||||
message?.type === 'system' &&
|
||||
message?.subtype === 'init' &&
|
||||
message.session_id
|
||||
) {
|
||||
if (sessionKey) sessions.set(sessionKey, message.session_id);
|
||||
}
|
||||
if (message?.session_id && sessionKey)
|
||||
sessions.set(sessionKey, message.session_id);
|
||||
await write({ id, type: 'event', event: message });
|
||||
if (message?.type === 'result') {
|
||||
await write({
|
||||
id,
|
||||
type: 'done',
|
||||
sessionId:
|
||||
message.session_id ??
|
||||
(sessionKey ? sessions.get(sessionKey) : undefined),
|
||||
result: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await write({
|
||||
id,
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
active.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelTurn(request) {
|
||||
const id = String(request.id ?? '');
|
||||
const task = active.get(id);
|
||||
if (!task) return write({ id, type: 'cancelled', cancelled: false });
|
||||
task.controller.abort(new Error('AGC cancelled Claude Agent SDK query'));
|
||||
return write({ id, type: 'cancelled', cancelled: true });
|
||||
}
|
||||
|
||||
const input = readline.createInterface({
|
||||
input: process.stdin,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
for await (const line of input) {
|
||||
if (!line.trim()) continue;
|
||||
let request;
|
||||
try {
|
||||
request = JSON.parse(line);
|
||||
} catch {
|
||||
await write({ type: 'error', message: 'sidecar 输入不是有效 JSON' });
|
||||
continue;
|
||||
}
|
||||
if (request.type === 'cancel') {
|
||||
await cancelTurn(request);
|
||||
} else if (request.type === 'turn') {
|
||||
void runTurn(request);
|
||||
} else if (request.type === 'shutdown') {
|
||||
for (const task of active.values()) task.controller.abort();
|
||||
break;
|
||||
} else {
|
||||
await write({
|
||||
id: request.id,
|
||||
type: 'error',
|
||||
message: 'sidecar 不支持该请求类型',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && node scripts/check-config.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.285",
|
||||
"@cubone/react-file-manager": "^1.35.0",
|
||||
"@genarrative/image-canvas-core": "0.1.0",
|
||||
"@genarrative/image-canvas-react": "0.1.0",
|
||||
|
||||
@@ -1358,6 +1358,7 @@ for (const channel of ['release', 'beta-2']) {
|
||||
|
||||
const expectedBundledDesignAgentResources = {
|
||||
'design-agent': 'design-agent',
|
||||
'resources/claude-agent': 'claude-agent',
|
||||
...Object.fromEntries(
|
||||
[
|
||||
'codex-patch-parser',
|
||||
|
||||
@@ -32,6 +32,83 @@ fn sha256_file(path: &std::path::Path) -> Result<String, std::io::Error> {
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
fn claude_agent_platform(target: &str) -> Option<&'static str> {
|
||||
match target {
|
||||
"x86_64-pc-windows-msvc" => Some("win32-x64"),
|
||||
"aarch64-pc-windows-msvc" => Some("win32-arm64"),
|
||||
"x86_64-apple-darwin" => Some("darwin-x64"),
|
||||
"aarch64-apple-darwin" => Some("darwin-arm64"),
|
||||
"x86_64-unknown-linux-gnu" => Some("linux-x64"),
|
||||
"aarch64-unknown-linux-gnu" => Some("linux-arm64"),
|
||||
"x86_64-unknown-linux-musl" => Some("linux-x64-musl"),
|
||||
"aarch64-unknown-linux-musl" => Some("linux-arm64-musl"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_bundled_claude_agent(manifest_dir: &std::path::Path) {
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
let Some(platform) = claude_agent_platform(&target) else {
|
||||
println!("cargo:warning=Claude Agent SDK sidecar 暂不支持目标:{target}");
|
||||
return;
|
||||
};
|
||||
let app_root = manifest_dir.parent().expect("AGC Tauri manifest parent");
|
||||
let repo_root = app_root
|
||||
.parent()
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AGC 应用必须位于仓库 apps 目录下");
|
||||
let roots = [app_root, repo_root];
|
||||
let sdk_source = roots
|
||||
.iter()
|
||||
.map(|root| root.join("node_modules/@anthropic-ai/claude-agent-sdk"))
|
||||
.find(|path| path.join("sdk.mjs").is_file())
|
||||
.unwrap_or_else(|| panic!("Claude Agent SDK 缺失;请先执行 npm ci"));
|
||||
let binary_package = format!("claude-agent-sdk-{platform}");
|
||||
let binary_source = roots
|
||||
.iter()
|
||||
.map(|root| {
|
||||
root.join("node_modules/@anthropic-ai")
|
||||
.join(&binary_package)
|
||||
})
|
||||
.find(|path| path.is_dir())
|
||||
.unwrap_or_else(|| panic!("Claude Agent SDK 原生运行时缺失:{binary_package}"));
|
||||
let entry_source = app_root.join("agent-sidecar/src/index.mjs");
|
||||
assert!(entry_source.is_file(), "Claude Agent SDK sidecar 入口缺失");
|
||||
let target_root = manifest_dir.join("resources/claude-agent");
|
||||
if target_root.exists() {
|
||||
fs::remove_dir_all(&target_root).expect("清理 Claude Agent SDK staging 失败");
|
||||
}
|
||||
fs::create_dir_all(target_root.join("node_modules/@anthropic-ai"))
|
||||
.expect("创建 Claude Agent SDK staging 目录失败");
|
||||
stage_plugin_file(&entry_source, &target_root.join("index.mjs"));
|
||||
copy_plugin_tree(
|
||||
&sdk_source,
|
||||
&target_root.join("node_modules/@anthropic-ai/claude-agent-sdk"),
|
||||
);
|
||||
copy_plugin_tree(
|
||||
&binary_source,
|
||||
&target_root
|
||||
.join("node_modules/@anthropic-ai")
|
||||
.join(&binary_package),
|
||||
);
|
||||
let binary_name = if target.contains("windows") {
|
||||
"claude.exe"
|
||||
} else {
|
||||
"claude"
|
||||
};
|
||||
let staged_binary = target_root
|
||||
.join("node_modules/@anthropic-ai")
|
||||
.join(&binary_package)
|
||||
.join(binary_name);
|
||||
if let Ok(metadata) = fs::metadata(binary_source.join(binary_name)) {
|
||||
fs::set_permissions(&staged_binary, metadata.permissions())
|
||||
.expect("保留 Claude Agent SDK 原生运行时权限失败");
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", entry_source.display());
|
||||
println!("cargo:rerun-if-changed={}", sdk_source.display());
|
||||
println!("cargo:rerun-if-changed={}", binary_source.display());
|
||||
}
|
||||
|
||||
fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
println!("cargo:rustc-env=AGC_BUILD_TARGET={target}");
|
||||
@@ -262,6 +339,7 @@ fn main() {
|
||||
env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be available"),
|
||||
);
|
||||
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
||||
stage_bundled_claude_agent(&manifest_dir);
|
||||
stage_bundled_codex_cli(&manifest_dir);
|
||||
prepare_unity_editor_helper(&manifest_dir);
|
||||
prepare_godot_editor_extension(&manifest_dir);
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::collections::BTreeSet;
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
|
||||
mod claude_code_cli;
|
||||
mod codex_app_server;
|
||||
mod codex_cli;
|
||||
mod codex_provider_proxy;
|
||||
@@ -49,6 +50,12 @@ mod runtime_protocol;
|
||||
mod runtime_state;
|
||||
mod runtime_tools;
|
||||
mod skill_pack;
|
||||
use claude_code_cli::*;
|
||||
pub(crate) use claude_code_cli::{
|
||||
cancel_direct_claude_code_turn_at, direct_game_creator_claude_code_chat_at,
|
||||
direct_game_creator_claude_code_home_chat, game_creator_claude_code_cli_route_error,
|
||||
game_creator_claude_code_cli_version_identity,
|
||||
};
|
||||
use codex_app_server::*;
|
||||
pub(crate) use codex_app_server::{
|
||||
cancel_direct_codex_turn_at, direct_game_creator_codex_chat_at,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4525,6 +4525,9 @@ pub(crate) fn cancel_direct_codex_turn_at(
|
||||
root: &Path,
|
||||
client_turn_id: Option<&str>,
|
||||
) -> Result<DirectTurnCancelView, String> {
|
||||
if let Some(view) = cancel_direct_claude_code_turn_at(root, client_turn_id)? {
|
||||
return Ok(view);
|
||||
}
|
||||
let key = direct_codex_active_turn_key(root);
|
||||
let expected = client_turn_id
|
||||
.map(str::trim)
|
||||
@@ -5355,6 +5358,23 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
};
|
||||
let config = load_game_creator_app_config()
|
||||
.map_err(|detail| DirectTurnError::EnvironmentNotReady { detail })?;
|
||||
if config.agent_mode == GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI {
|
||||
if let Some(error) = game_creator_claude_code_cli_route_error(&config.llm) {
|
||||
return Err(DirectTurnError::EnvironmentNotReady { detail: error });
|
||||
}
|
||||
return direct_game_creator_claude_code_chat_at(
|
||||
&codex_root,
|
||||
&config.llm,
|
||||
system_prompt,
|
||||
user_prompt,
|
||||
client_turn_id,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
.map_err(|detail| {
|
||||
DirectTurnError::from_model_call(&platform_llm::LlmError::Transport(detail))
|
||||
});
|
||||
}
|
||||
game_creator_codex_app_server_validate_llm_config(&config.llm).map_err(|error| {
|
||||
DirectTurnError::EnvironmentNotReady {
|
||||
detail: error.to_string(),
|
||||
@@ -5451,6 +5471,13 @@ pub(crate) async fn direct_game_creator_home_codex_chat(
|
||||
user_prompt: String,
|
||||
) -> Result<String, String> {
|
||||
let config = load_game_creator_app_config()?;
|
||||
if config.agent_mode == GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI {
|
||||
if let Some(error) = game_creator_claude_code_cli_route_error(&config.llm) {
|
||||
return Err(error);
|
||||
}
|
||||
return direct_game_creator_claude_code_home_chat(&config.llm, system_prompt, user_prompt)
|
||||
.await;
|
||||
}
|
||||
game_creator_codex_app_server_validate_llm_config(&config.llm)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let snapshot = AgentRuntimeProviderRequestSnapshot {
|
||||
|
||||
@@ -373,10 +373,25 @@ fn game_creator_agent_runtime_provider_config_fingerprint_for_mode(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let claude_code_cli = if agent_mode == GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI {
|
||||
Some(serde_json::json!({
|
||||
"executable": "claude",
|
||||
"version": game_creator_claude_code_cli_version_identity()?,
|
||||
"protocol": "genarrative-claude-code-cli-agent.v1",
|
||||
"structuredOutput": true,
|
||||
"nativeTools": false,
|
||||
"mcp": "direct-loopback-only",
|
||||
"isolatedHome": true,
|
||||
"noSessionPersistence": true,
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let stable = serde_json::json!({
|
||||
"agentMode": agent_mode,
|
||||
"codexAppServer": codex_app_server,
|
||||
"codexCli": codex_cli,
|
||||
"claudeCodeCli": claude_code_cli,
|
||||
"apiKeySha256": format!("{:x}", Sha256::digest(llm.api_key.as_bytes())),
|
||||
"baseUrl": llm.base_url,
|
||||
"model": llm.model,
|
||||
@@ -419,6 +434,8 @@ pub(in crate::agent) fn game_creator_agent_runtime_provider_retry_identity_for_m
|
||||
GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER | GAME_CREATOR_AGENT_MODE_CODEX_CLI
|
||||
) {
|
||||
Some(game_creator_codex_cli_version_identity()?)
|
||||
} else if agent_mode == GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI {
|
||||
Some(game_creator_claude_code_cli_version_identity()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1448,6 +1465,9 @@ pub(in crate::agent) async fn request_game_creator_agent_runtime_llm_with_persis
|
||||
GAME_CREATOR_AGENT_MODE_CODEX_CLI => {
|
||||
request_game_creator_agent_codex_cli(request).await
|
||||
}
|
||||
GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI => {
|
||||
request_game_creator_agent_claude_code_cli(request).await
|
||||
}
|
||||
GAME_CREATOR_AGENT_MODE_PROVIDER => {
|
||||
let client = build_game_creator_agent_runtime_llm_client(
|
||||
&llm_for_request,
|
||||
@@ -1524,6 +1544,9 @@ pub(in crate::agent) async fn request_game_creator_agent_runtime_llm_with_transi
|
||||
GAME_CREATOR_AGENT_MODE_CODEX_CLI => {
|
||||
request_game_creator_agent_codex_cli(request.clone()).await
|
||||
}
|
||||
GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI => {
|
||||
request_game_creator_agent_claude_code_cli(request.clone()).await
|
||||
}
|
||||
GAME_CREATOR_AGENT_MODE_PROVIDER => {
|
||||
let client = client.expect("provider mode constructs an HTTP client");
|
||||
request_game_creator_agent_runtime_provider_llm(&client, llm, request.clone())
|
||||
|
||||
@@ -1927,10 +1927,10 @@ pub(crate) fn select_game_creator_reasoning_effort(
|
||||
persist_game_creator_app_config(config, overlays, false)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn select_game_creator_model(
|
||||
fn select_game_creator_model_inner(
|
||||
model_id: String,
|
||||
is_default: bool,
|
||||
catalog_agent_mode: Option<String>,
|
||||
) -> Result<GameCreatorAppConfigView, String> {
|
||||
let _guard = GAME_CREATOR_CONFIG_WRITE_LOCK
|
||||
.lock()
|
||||
@@ -1948,11 +1948,27 @@ pub(crate) fn select_game_creator_model(
|
||||
{
|
||||
return Err("模型标识无效".into());
|
||||
}
|
||||
if let Some(agent_mode) = catalog_agent_mode {
|
||||
config.agent_mode = match agent_mode.trim() {
|
||||
"codex" => GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER.to_string(),
|
||||
"cc" => GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI.to_string(),
|
||||
value => return Err(format!("模型 Agent 模式无效:{value}")),
|
||||
};
|
||||
}
|
||||
config.selected_model_id = model_id;
|
||||
config.selected_model_is_default = is_default;
|
||||
persist_game_creator_app_config(config, overlays, true)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn select_game_creator_model(
|
||||
model_id: String,
|
||||
is_default: bool,
|
||||
agent_mode: Option<String>,
|
||||
) -> Result<GameCreatorAppConfigView, String> {
|
||||
select_game_creator_model_inner(model_id, is_default, agent_mode)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn discover_game_creator_llm_models(
|
||||
llm: GameCreatorLlmConfig,
|
||||
|
||||
@@ -443,20 +443,30 @@ pub(crate) fn check_game_creator_llm_config_from_config() -> GameCreatorLlmConfi
|
||||
fn check_game_creator_codex_config(
|
||||
app_config: &GameCreatorAppConfig,
|
||||
) -> GameCreatorLlmConfigStatus {
|
||||
let cli_error = check_game_creator_codex_cli_available()
|
||||
.and_then(|()| {
|
||||
if app_config.agent_mode == GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER {
|
||||
check_game_creator_codex_app_server_available()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.err();
|
||||
let cli_error = match app_config.agent_mode.as_str() {
|
||||
GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI => {
|
||||
crate::agent::game_creator_claude_code_cli_version_identity().map(|_| ())
|
||||
}
|
||||
GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER | GAME_CREATOR_AGENT_MODE_CODEX_CLI => {
|
||||
check_game_creator_codex_cli_available().and_then(|()| {
|
||||
if app_config.agent_mode == GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER {
|
||||
check_game_creator_codex_app_server_available()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
.err();
|
||||
let global_route_error = game_creator_codex_app_server_llm_route_error(
|
||||
&app_config.agent_mode,
|
||||
&app_config.llm,
|
||||
"llm",
|
||||
);
|
||||
)
|
||||
.or_else(|| {
|
||||
game_creator_claude_code_cli_llm_route_error(&app_config.agent_mode, &app_config.llm)
|
||||
});
|
||||
let mut status = check_game_creator_llm_config_values(&app_config.llm, "llm");
|
||||
status.agent_mode = app_config.agent_mode.clone();
|
||||
if !app_config.llm.custom_enabled && game_creator_official_llm_route_locked() {
|
||||
@@ -550,6 +560,16 @@ pub(crate) fn game_creator_codex_app_server_llm_route_error(
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_claude_code_cli_llm_route_error(
|
||||
agent_mode: &str,
|
||||
llm: &GameCreatorLlmConfig,
|
||||
) -> Option<String> {
|
||||
if agent_mode != GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI {
|
||||
return None;
|
||||
}
|
||||
crate::agent::game_creator_claude_code_cli_route_error(llm)
|
||||
}
|
||||
|
||||
pub(crate) fn check_game_creator_codex_cli_available() -> Result<(), String> {
|
||||
crate::agent::game_creator_codex_cli_version_identity().map(|_| ())
|
||||
}
|
||||
@@ -3650,10 +3670,15 @@ pub(crate) fn scrub_locked_game_creator_config_file(config: &mut GameCreatorAppC
|
||||
if config.llm.as_ref().and_then(|llm| llm.custom_enabled) == Some(true) {
|
||||
return false;
|
||||
}
|
||||
let mut changed = config.agent_mode.as_deref()
|
||||
!= Some(GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER)
|
||||
|| config.agent_llm.is_some();
|
||||
config.agent_mode = Some(GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER.to_string());
|
||||
let locked_agent_mode =
|
||||
if config.agent_mode.as_deref() == Some(GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI) {
|
||||
GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI
|
||||
} else {
|
||||
GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER
|
||||
};
|
||||
let mut changed =
|
||||
config.agent_mode.as_deref() != Some(locked_agent_mode) || config.agent_llm.is_some();
|
||||
config.agent_mode = Some(locked_agent_mode.to_string());
|
||||
config.agent_llm = None;
|
||||
if let Some(llm) = config.llm.as_mut() {
|
||||
// 官方路由仍然清空凭据,但把连接字段留在文件里:手写自定义连接时
|
||||
@@ -3806,7 +3831,9 @@ pub(crate) fn lock_game_creator_app_config_to_official_route(config: &mut GameCr
|
||||
if !game_creator_official_llm_route_locked() {
|
||||
return;
|
||||
}
|
||||
config.agent_mode = GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER.to_string();
|
||||
if config.agent_mode != GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI {
|
||||
config.agent_mode = GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER.to_string();
|
||||
}
|
||||
config.agent_llm.clear();
|
||||
config.editor_api.api_key.clear();
|
||||
if config.llm.custom_enabled {
|
||||
@@ -4528,9 +4555,12 @@ pub(crate) fn normalize_game_creator_agent_mode(value: &str) -> Result<String, S
|
||||
Ok(GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER.to_string())
|
||||
}
|
||||
GAME_CREATOR_AGENT_MODE_CODEX_CLI => Ok(GAME_CREATOR_AGENT_MODE_CODEX_CLI.to_string()),
|
||||
GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI => {
|
||||
Ok(GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI.to_string())
|
||||
}
|
||||
GAME_CREATOR_AGENT_MODE_PROVIDER => Ok(GAME_CREATOR_AGENT_MODE_PROVIDER.to_string()),
|
||||
value => Err(format!(
|
||||
"配置项 agentMode 无效:{value},请使用 codex_app_server、codex_cli 或 provider"
|
||||
"配置项 agentMode 无效:{value},请使用 codex_app_server、codex_cli、claude_code_cli 或 provider"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,17 @@ use std::time::Duration;
|
||||
const API_RESPONSE_ENVELOPE_VERSION: &str = "v1";
|
||||
const MODEL_CATALOG_MAX_BYTES: usize = 1024 * 1024;
|
||||
|
||||
fn default_codex_agent_mode() -> String {
|
||||
"codex".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireModelSummary {
|
||||
id: String,
|
||||
display_name: String,
|
||||
#[serde(default = "default_codex_agent_mode")]
|
||||
agent_mode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -49,6 +55,7 @@ fn map_catalog(catalog: WireModelCatalog) -> LlmModelsResponse {
|
||||
.map(|model| LlmModelSummary {
|
||||
id: model.id,
|
||||
display_name: model.display_name,
|
||||
agent_mode: model.agent_mode,
|
||||
})
|
||||
.collect(),
|
||||
revision: catalog.revision,
|
||||
|
||||
@@ -1438,6 +1438,7 @@ 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 GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER: &str = "codex_app_server";
|
||||
const GAME_CREATOR_AGENT_MODE_CODEX_CLI: &str = "codex_cli";
|
||||
const GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI: &str = "claude_code_cli";
|
||||
const GAME_CREATOR_AGENT_MODE_PROVIDER: &str = "provider";
|
||||
const GAME_CREATOR_APP_CONFIG_SCHEMA_VERSION: &str = "game-creator-config.v2";
|
||||
const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://dev.genarrative.world/gpt/v1";
|
||||
|
||||
@@ -38,9 +38,9 @@ fn custom_llm_config_save_reload_and_selection_preserve_overlay_and_credentials(
|
||||
write_game_creator_config_atomically(&overlay, r#"{"llm":{"customEnabled":true,"apiKey":"fixture-key","baseUrl":"https://custom.example/v1","visibleModels":["a/v1","b:v2"]},"selectedModelId":"a/v1","selectedModelIsDefault":true}"#).unwrap();
|
||||
let mut config = read_game_creator_app_config().unwrap().config;
|
||||
assert_eq!(config.llm.model, "a/v1");
|
||||
let selected = select_game_creator_model("b:v2".into(), false).unwrap();
|
||||
let selected = select_game_creator_model("b:v2".into(), false, None).unwrap();
|
||||
assert_eq!(selected.config.llm.model, "b:v2");
|
||||
assert!(select_game_creator_model("not-listed".into(), false).is_err());
|
||||
assert!(select_game_creator_model("not-listed".into(), false, None).is_err());
|
||||
config.llm.visible_models = vec!["b:v2".into()];
|
||||
config.llm.api_key = "changed-fixture-key".into();
|
||||
write_game_creator_app_config(config).unwrap();
|
||||
@@ -228,6 +228,11 @@ fn agent_mode_defaults_to_codex_app_server_and_preserves_explicit_modes() {
|
||||
normalize_game_creator_agent_mode(GAME_CREATOR_AGENT_MODE_CODEX_CLI).expect("cli mode"),
|
||||
GAME_CREATOR_AGENT_MODE_CODEX_CLI
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_game_creator_agent_mode(GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI)
|
||||
.expect("cc mode"),
|
||||
GAME_CREATOR_AGENT_MODE_CLAUDE_CODE_CLI
|
||||
);
|
||||
assert!(normalize_game_creator_agent_mode("unknown")
|
||||
.expect_err("unknown mode")
|
||||
.contains("agentMode"));
|
||||
@@ -958,7 +963,8 @@ fn app_config_model_selection_only_updates_model_overlay() {
|
||||
r#"{"selectedModelId":"old","selectedModelIsDefault":false,"llm":{"reasoningEffort":"low"}}"#,
|
||||
)
|
||||
.expect("write overlay");
|
||||
let saved = select_game_creator_model("new-model".to_string(), true).expect("select model");
|
||||
let saved =
|
||||
select_game_creator_model("new-model".to_string(), true, None).expect("select model");
|
||||
let effective = load_game_creator_app_config().expect("effective config");
|
||||
assert_eq!(saved.config.selected_model_id, "new-model");
|
||||
assert_eq!(effective.selected_model_id, "new-model");
|
||||
@@ -981,12 +987,12 @@ fn model_selection_default_flag_round_trips() {
|
||||
let _guard = use_test_runtime_config_dir(root.clone());
|
||||
|
||||
let saved =
|
||||
select_game_creator_model("quality".to_string(), true).expect("select default model");
|
||||
select_game_creator_model("quality".to_string(), true, None).expect("select default model");
|
||||
assert_eq!(saved.config.selected_model_id, "quality");
|
||||
assert!(saved.config.selected_model_is_default);
|
||||
|
||||
let saved =
|
||||
select_game_creator_model("fast".to_string(), false).expect("select explicit model");
|
||||
select_game_creator_model("fast".to_string(), false, None).expect("select explicit model");
|
||||
assert_eq!(saved.config.selected_model_id, "fast");
|
||||
assert!(!saved.config.selected_model_is_default);
|
||||
|
||||
|
||||
@@ -39,14 +39,19 @@
|
||||
"vendor/codex-utils-path-uri/LICENSE": "licenses/codex-utils-path-uri/LICENSE",
|
||||
"vendor/codex-utils-path-uri/NOTICE": "licenses/codex-utils-path-uri/NOTICE",
|
||||
"vendor/codex-utils-absolute-path/LICENSE": "licenses/codex-utils-absolute-path/LICENSE",
|
||||
"vendor/codex-utils-absolute-path/NOTICE": "licenses/codex-utils-absolute-path/NOTICE"
|
||||
"vendor/codex-utils-absolute-path/NOTICE": "licenses/codex-utils-absolute-path/NOTICE",
|
||||
"resources/claude-agent": "claude-agent"
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["bubblewrap"]
|
||||
"depends": [
|
||||
"bubblewrap"
|
||||
]
|
||||
},
|
||||
"rpm": {
|
||||
"depends": ["bubblewrap"]
|
||||
"depends": [
|
||||
"bubblewrap"
|
||||
]
|
||||
}
|
||||
},
|
||||
"icon": [
|
||||
|
||||
@@ -575,6 +575,7 @@ export type GameCreatorLlmApiKind =
|
||||
export type GameCreatorAgentMode =
|
||||
| 'codex_app_server'
|
||||
| 'codex_cli'
|
||||
| 'claude_code_cli'
|
||||
| 'provider';
|
||||
export type RuntimeLlmProviderPresetId =
|
||||
| 'custom'
|
||||
|
||||
@@ -171,6 +171,9 @@ export function ConversationModelSelect({
|
||||
invoke<GameCreatorAppConfigView>('select_game_creator_model', {
|
||||
modelId: next,
|
||||
isDefault: nextIsDefault,
|
||||
agentMode:
|
||||
catalog.models.find((model) => model.id === next)?.agentMode ??
|
||||
'codex',
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
@@ -336,10 +339,13 @@ export function ConversationModelSelect({
|
||||
try {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) throw new Error('Native host unavailable');
|
||||
const agentMode =
|
||||
models.find((model) => model.id === id)?.agentMode ?? 'codex';
|
||||
const result = await queueConfigWrite(() =>
|
||||
invoke<GameCreatorAppConfigView>('select_game_creator_model', {
|
||||
modelId: id,
|
||||
isDefault: false,
|
||||
agentMode,
|
||||
}),
|
||||
);
|
||||
if (result.config.selectedModelId !== id)
|
||||
|
||||
@@ -166,7 +166,11 @@ function normalizeRuntimeConfigDraft(
|
||||
: defaultRuntimeConfigDraft.llm.reasoningEffort;
|
||||
return {
|
||||
...config,
|
||||
agentMode: 'codex_app_server',
|
||||
// Agent 模式由后台模型选择绑定;设置页保存不能把 cc 选择覆盖回 Codex。
|
||||
agentMode:
|
||||
config.agentMode === 'claude_code_cli'
|
||||
? 'claude_code_cli'
|
||||
: 'codex_app_server',
|
||||
llm: {
|
||||
...config.llm,
|
||||
apiKey: config.llm.customEnabled ? config.llm.apiKey : '',
|
||||
@@ -663,7 +667,11 @@ export function RuntimeConfigDialog({
|
||||
const config = normalizeRuntimeConfigDraft(
|
||||
{
|
||||
...runtimeConfigDraft,
|
||||
agentMode: 'codex_app_server',
|
||||
// 模型选择器已把后台绑定的执行模式写入配置,这里只保留它。
|
||||
agentMode:
|
||||
runtimeConfigDraft.agentMode === 'claude_code_cli'
|
||||
? 'claude_code_cli'
|
||||
: 'codex_app_server',
|
||||
editorApi: advancedExternalEditorConfigEnabled
|
||||
? runtimeConfigDraft.editorApi
|
||||
: defaultRuntimeConfigDraft.editorApi,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { resolveTauriInvoke } from '../app/tauri';
|
||||
|
||||
export type ClientLlmModel = {
|
||||
displayName: string;
|
||||
id: string;
|
||||
agentMode: 'codex' | 'cc';
|
||||
};
|
||||
|
||||
export type ClientLlmModelCatalog = {
|
||||
defaultModelId: string;
|
||||
models: ClientLlmModel[];
|
||||
revision: number;
|
||||
};
|
||||
|
||||
type ClientLlmModelWire = Omit<ClientLlmModel, 'agentMode'> & {
|
||||
agentMode?: ClientLlmModel['agentMode'] | null;
|
||||
};
|
||||
|
||||
type ClientLlmModelCatalogWire = Omit<ClientLlmModelCatalog, 'models'> & {
|
||||
models: ClientLlmModelWire[];
|
||||
};
|
||||
|
||||
export async function loadClientLlmModels(): Promise<ClientLlmModelCatalog> {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) throw new Error('本地模型服务不可用');
|
||||
const catalog = await invoke<ClientLlmModelCatalogWire>(
|
||||
'load_game_creator_llm_models',
|
||||
);
|
||||
return {
|
||||
...catalog,
|
||||
models: catalog.models.map((model) => ({
|
||||
...model,
|
||||
agentMode: model.agentMode ?? 'codex',
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
import { resolveTauriInvoke } from '../app/tauri';
|
||||
import type { GameCreatorAppConfigView } from '../app/types';
|
||||
import { type ClientLlmModelCatalog, loadClientLlmModels } from './clientApi';
|
||||
|
||||
export type ClientLlmModel = {
|
||||
displayName: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ClientLlmModelCatalog = {
|
||||
defaultModelId: string;
|
||||
models: ClientLlmModel[];
|
||||
revision: number;
|
||||
};
|
||||
export type { ClientLlmModel, ClientLlmModelCatalog } from './clientApi';
|
||||
|
||||
let cached: ClientLlmModelCatalog | null = null;
|
||||
let inFlight: Promise<ClientLlmModelCatalog> | null = null;
|
||||
@@ -56,12 +48,16 @@ async function loadEffectiveCatalog(epoch: number) {
|
||||
const ids = llm.visibleModels ?? [];
|
||||
return {
|
||||
defaultModelId: ids[0] ?? '',
|
||||
models: ids.map((id) => ({ id, displayName: id })),
|
||||
models: ids.map((id) => ({
|
||||
id,
|
||||
displayName: id,
|
||||
agentMode: 'codex' as const,
|
||||
})),
|
||||
revision: --localRevision,
|
||||
};
|
||||
}
|
||||
// 官方模型目录的凭据、请求和响应解析归 Rust;渲染层只接收目录快照。
|
||||
return invoke<ClientLlmModelCatalog>('load_game_creator_llm_models');
|
||||
return loadClientLlmModels();
|
||||
}
|
||||
|
||||
/** 最近一次成功读取的模型目录,用于首屏渲染与刷新失败时兜底。 */
|
||||
|
||||
@@ -281,6 +281,7 @@ export function registerDesignAgentSurfaceTests() {
|
||||
expect(harness.invoke).toHaveBeenCalledWith('select_game_creator_model', {
|
||||
modelId: 'fast',
|
||||
isDefault: false,
|
||||
agentMode: 'codex',
|
||||
});
|
||||
expect(
|
||||
screen.getByRole('button', { name: '对话模型' }).textContent,
|
||||
|
||||
@@ -148,6 +148,7 @@ export function registerClientHomeTests() {
|
||||
expect(invoke).toHaveBeenCalledWith('select_game_creator_model', {
|
||||
modelId: 'fast',
|
||||
isDefault: false,
|
||||
agentMode: 'codex',
|
||||
}),
|
||||
);
|
||||
expect(modelTrigger.textContent).toContain('快速');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user