Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c36a5170f8 | |||
| 9663bbf911 | |||
| b3e9d0a906 | |||
| a60328623d | |||
| 9e63b76991 | |||
| a98ebcf68f | |||
| 576ff07a5e | |||
| 5aa616134c | |||
| 187b66c3b1 |
@@ -1 +1,4 @@
|
|||||||
|
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
|
||||||
|
# 子进程(npm、lint-staged、测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
|
||||||
|
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
||||||
npm run format:staged
|
npm run format:staged
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
|
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
|
||||||
|
# 钩子链(npm → check:repository-ci → 测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
|
||||||
|
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
||||||
npm run check:pre-push-master -- "$@"
|
npm run check:pre-push-master -- "$@"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
"schemaVersion": "game-creator-config.v2",
|
"schemaVersion": "game-creator-config.v2",
|
||||||
"agentMode": "codex_app_server",
|
"agentMode": "codex_app_server",
|
||||||
"llm": {
|
"llm": {
|
||||||
|
"customEnabled": false,
|
||||||
|
"visibleModels": [],
|
||||||
"apiKey": "",
|
"apiKey": "",
|
||||||
"baseUrl": "https://dev.genarrative.world/gpt/v1",
|
"baseUrl": "https://dev.genarrative.world/gpt/v1",
|
||||||
"model": "gpt-6-astra",
|
"model": "gpt-6-astra",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@genarrative/ai-game-creator-shell",
|
"name": "@genarrative/ai-game-creator-shell",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.29",
|
"version": "0.1.45",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/start-tauri-dev.mjs",
|
"dev": "node scripts/start-tauri-dev.mjs",
|
||||||
|
|||||||
+1
-1
@@ -1725,7 +1725,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "genarrative-ai-game-creator-shell"
|
name = "genarrative-ai-game-creator-shell"
|
||||||
version = "0.1.29"
|
version = "0.1.45"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-runtime-core",
|
"agent-runtime-core",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "genarrative-ai-game-creator-shell"
|
name = "genarrative-ai-game-creator-shell"
|
||||||
version = "0.1.29"
|
version = "0.1.45"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ impl CodexAppServerCredential {
|
|||||||
) -> Option<(&'a str, &'a str)> {
|
) -> Option<(&'a str, &'a str)> {
|
||||||
match self {
|
match self {
|
||||||
Self::PlatformSession { .. } => None,
|
Self::PlatformSession { .. } => None,
|
||||||
#[cfg(test)]
|
|
||||||
Self::AppDataKey { .. } => (!llm.api_key.trim().is_empty())
|
Self::AppDataKey { .. } => (!llm.api_key.trim().is_empty())
|
||||||
.then_some((llm.base_url.trim_end_matches('/'), llm.api_key.trim())),
|
.then_some((llm.base_url.trim_end_matches('/'), llm.api_key.trim())),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -139,7 +138,7 @@ impl CodexAppServerCredential {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|api_key| (GAME_CREATOR_CODEX_AUTH_BRIDGE_API_BASE_URL, api_key)),
|
.map(|api_key| (GAME_CREATOR_CODEX_AUTH_BRIDGE_API_BASE_URL, api_key)),
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
Self::AppDataKey { .. } | Self::AuthBridge { .. } => None,
|
Self::AuthBridge { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2018,7 +2017,13 @@ impl CodexAppServerConnection {
|
|||||||
let codex_cli_version = game_creator_codex_cli_version_identity()
|
let codex_cli_version = game_creator_codex_cli_version_identity()
|
||||||
.map_err(platform_llm::LlmError::InvalidConfig)?;
|
.map_err(platform_llm::LlmError::InvalidConfig)?;
|
||||||
let mut effective_llm = llm.clone();
|
let mut effective_llm = llm.clone();
|
||||||
let credential = if game_creator_official_llm_route_locked() {
|
let credential = if llm.custom_enabled {
|
||||||
|
crate::config::validate_custom_llm_connection(llm)
|
||||||
|
.map_err(platform_llm::LlmError::InvalidConfig)?;
|
||||||
|
CodexAppServerCredential::AppDataKey {
|
||||||
|
fingerprint: format!("custom-key:{:x}", Sha256::digest(llm.api_key.as_bytes())),
|
||||||
|
}
|
||||||
|
} else if game_creator_official_llm_route_locked() {
|
||||||
let session = current_platform_session().ok_or_else(|| {
|
let session = current_platform_session().ok_or_else(|| {
|
||||||
platform_llm::LlmError::InvalidConfig(
|
platform_llm::LlmError::InvalidConfig(
|
||||||
"authentication-required: 请先登录陶泥儿账号".to_string(),
|
"authentication-required: 请先登录陶泥儿账号".to_string(),
|
||||||
@@ -2186,7 +2191,8 @@ impl CodexAppServerConnection {
|
|||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
_ => (
|
_ => (
|
||||||
(workspace_mode == CodexAppServerWorkspaceMode::DirectProject)
|
(llm.custom_enabled
|
||||||
|
|| workspace_mode == CodexAppServerWorkspaceMode::DirectProject)
|
||||||
.then(|| credential.direct_provider_route(llm))
|
.then(|| credential.direct_provider_route(llm))
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string())),
|
.map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string())),
|
||||||
@@ -4743,6 +4749,8 @@ mod tests {
|
|||||||
|
|
||||||
fn test_llm() -> GameCreatorLlmConfig {
|
fn test_llm() -> GameCreatorLlmConfig {
|
||||||
GameCreatorLlmConfig {
|
GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "fixture-secret".to_string(),
|
api_key: "fixture-secret".to_string(),
|
||||||
base_url: "https://example.invalid/v1".to_string(),
|
base_url: "https://example.invalid/v1".to_string(),
|
||||||
model: "fixture-model".to_string(),
|
model: "fixture-model".to_string(),
|
||||||
@@ -5528,6 +5536,59 @@ mod tests {
|
|||||||
assert_ne!(command_token, provider_key);
|
assert_ne!(command_token, provider_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn custom_llm_broker_uses_configured_route_without_exposing_upstream_key() {
|
||||||
|
let mut llm = test_llm();
|
||||||
|
llm.custom_enabled = true;
|
||||||
|
llm.api_key = "custom-upstream-fixture-secret".into();
|
||||||
|
llm.base_url = "http://127.0.0.1:9/v1".into();
|
||||||
|
llm.model = "vendor/model.v1:latest".into();
|
||||||
|
llm.visible_models = vec![llm.model.clone()];
|
||||||
|
let credential = CodexAppServerCredential::AppDataKey {
|
||||||
|
fingerprint: "custom-fixture".into(),
|
||||||
|
};
|
||||||
|
let (base, key) = credential
|
||||||
|
.direct_provider_route(&llm)
|
||||||
|
.expect("custom route");
|
||||||
|
assert_eq!(base, llm.base_url);
|
||||||
|
assert_eq!(key, llm.api_key);
|
||||||
|
let proxy = start_codex_provider_proxy(base, key, false).await.unwrap();
|
||||||
|
for mode in [
|
||||||
|
CodexAppServerWorkspaceMode::DirectProject,
|
||||||
|
CodexAppServerWorkspaceMode::ToolHost,
|
||||||
|
] {
|
||||||
|
let mut command = tokio::process::Command::new("fixture");
|
||||||
|
configure_game_creator_codex_app_server_command_for_mode(
|
||||||
|
&mut command,
|
||||||
|
&llm,
|
||||||
|
mode,
|
||||||
|
Some(&proxy),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let arguments = command
|
||||||
|
.as_std()
|
||||||
|
.get_args()
|
||||||
|
.map(|arg| arg.to_string_lossy())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
let params = codex_app_server_thread_start_params(
|
||||||
|
&llm.model,
|
||||||
|
std::path::Path::new("fixture-workspace"),
|
||||||
|
mode,
|
||||||
|
String::new(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert_eq!(params["model"], "vendor/model.v1:latest");
|
||||||
|
assert!(!arguments.contains(&llm.api_key));
|
||||||
|
assert!(!arguments.contains("/api/llm"));
|
||||||
|
for (_, value) in command.as_std().get_envs() {
|
||||||
|
assert!(!value.is_some_and(|value| value.to_string_lossy().contains(&llm.api_key)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn direct_project_spawn_restores_broker_token_after_environment_isolation() {
|
async fn direct_project_spawn_restores_broker_token_after_environment_isolation() {
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ mod validation;
|
|||||||
mod wire;
|
mod wire;
|
||||||
|
|
||||||
pub(crate) use model::{
|
pub(crate) use model::{
|
||||||
DirectCodexUserAttachmentReferencePart, DirectCodexUserContentPart, DirectCodexUserItem,
|
DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageEnvelope,
|
||||||
DirectCodexUserMessageEnvelope, DirectCodexUserMessageItem, DirectCodexUserRole,
|
DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart,
|
||||||
DirectCodexUserRuntimeRegionPart,
|
|
||||||
};
|
};
|
||||||
pub(crate) use validation::validate_direct_codex_user_item;
|
pub(crate) use validation::validate_direct_codex_user_item;
|
||||||
pub(crate) use wire::{
|
pub(crate) use wire::{
|
||||||
|
|||||||
@@ -36,23 +36,6 @@ pub(crate) enum DirectCodexUserContentPart {
|
|||||||
AgcResourceReference { resource_id: String },
|
AgcResourceReference { resource_id: String },
|
||||||
#[serde(rename = "agc_runtime_region_reference")]
|
#[serde(rename = "agc_runtime_region_reference")]
|
||||||
AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart),
|
AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart),
|
||||||
/// Uploaded project attachment kept inline in canonical content.
|
|
||||||
#[serde(rename = "agc_attachment_reference")]
|
|
||||||
AgcAttachmentReference(DirectCodexUserAttachmentReferencePart),
|
|
||||||
#[serde(rename = "agc_image_reference")]
|
|
||||||
AgcImageReference(DirectCodexUserAttachmentReferencePart),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
|
||||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
||||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))]
|
|
||||||
pub(crate) struct DirectCodexUserAttachmentReferencePart {
|
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) media_type: String,
|
|
||||||
#[ts(type = "number")]
|
|
||||||
pub(crate) size: u64,
|
|
||||||
pub(crate) local_path: String,
|
|
||||||
pub(crate) status: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
|
|||||||
@@ -40,19 +40,6 @@ pub(crate) fn validate_direct_codex_user_item(
|
|||||||
reference_count = reference_count.saturating_add(1);
|
reference_count = reference_count.saturating_add(1);
|
||||||
validate_runtime_region_reference(&manifest, reference)?;
|
validate_runtime_region_reference(&manifest, reference)?;
|
||||||
}
|
}
|
||||||
DirectCodexUserContentPart::AgcAttachmentReference(reference)
|
|
||||||
| DirectCodexUserContentPart::AgcImageReference(reference) => {
|
|
||||||
if reference.name.trim().is_empty() {
|
|
||||||
return Err("附件缺少文件名".to_string());
|
|
||||||
}
|
|
||||||
if !reference.local_path.trim().is_empty() {
|
|
||||||
sanitize_attachment_local_path(&reference.local_path)
|
|
||||||
.ok_or_else(|| "附件项目路径无效".to_string())?;
|
|
||||||
}
|
|
||||||
if !matches!(reference.status.trim(), "imported" | "failed") {
|
|
||||||
return Err("附件状态无效".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if reference_count > MAX_DIRECT_CODEX_REFERENCES {
|
if reference_count > MAX_DIRECT_CODEX_REFERENCES {
|
||||||
|
|||||||
@@ -100,21 +100,6 @@ pub(crate) fn direct_codex_user_item_to_wire_input(
|
|||||||
summary.push(']');
|
summary.push(']');
|
||||||
summary
|
summary
|
||||||
}
|
}
|
||||||
DirectCodexUserContentPart::AgcAttachmentReference(reference)
|
|
||||||
| DirectCodexUserContentPart::AgcImageReference(reference) => {
|
|
||||||
let mut summary = format!(
|
|
||||||
"[附件:名称={};类型={};大小={} 字节",
|
|
||||||
reference.name.trim(),
|
|
||||||
reference.media_type.trim(),
|
|
||||||
reference.size
|
|
||||||
);
|
|
||||||
if !reference.local_path.trim().is_empty() {
|
|
||||||
summary.push_str(&format!(";项目路径={}", reference.local_path.trim()));
|
|
||||||
}
|
|
||||||
summary.push_str(&format!(";状态={}", reference.status.trim()));
|
|
||||||
summary.push(']');
|
|
||||||
summary
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
input.push(serde_json::json!({ "type": "text", "text": text }));
|
input.push(serde_json::json!({ "type": "text", "text": text }));
|
||||||
}
|
}
|
||||||
@@ -189,28 +174,4 @@ mod tests {
|
|||||||
.expect_err("history item without type must fail");
|
.expect_err("history item without type must fail");
|
||||||
assert!(error.contains("缺少 type"), "{error}");
|
assert!(error.contains("缺少 type"), "{error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn attachment_and_image_parts_remain_in_canonical_order_when_projected() {
|
|
||||||
let root = tempfile::tempdir().expect("temp project");
|
|
||||||
crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
|
|
||||||
.expect("init project");
|
|
||||||
let item = json!({
|
|
||||||
"type": "message",
|
|
||||||
"role": "user",
|
|
||||||
"id": "turn-1:user",
|
|
||||||
"content": [
|
|
||||||
{"type": "input_text", "text": "先看"},
|
|
||||||
{"type": "agc_image_reference", "name": "hero.png", "mediaType": "image/png", "size": 12, "localPath": "assets/hero.png", "status": "imported"},
|
|
||||||
{"type": "agc_attachment_reference", "name": "notes.txt", "mediaType": "text/plain", "size": 4, "localPath": "assets/notes.txt", "status": "imported"}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
let projected = direct_codex_user_item_to_response_item(root.path(), &item)
|
|
||||||
.expect("user response item should project");
|
|
||||||
let content = projected["content"].as_array().expect("content array");
|
|
||||||
assert_eq!(content.len(), 3);
|
|
||||||
assert!(content[0]["text"].as_str().unwrap().contains("先看"));
|
|
||||||
assert!(content[1]["text"].as_str().unwrap().contains("hero.png"));
|
|
||||||
assert!(content[2]["text"].as_str().unwrap().contains("notes.txt"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5329,6 +5329,8 @@ mod tests {
|
|||||||
|
|
||||||
fn direct_test_llm() -> GameCreatorLlmConfig {
|
fn direct_test_llm() -> GameCreatorLlmConfig {
|
||||||
GameCreatorLlmConfig {
|
GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "fixture-secret".to_string(),
|
api_key: "fixture-secret".to_string(),
|
||||||
base_url: "https://example.invalid/v1".to_string(),
|
base_url: "https://example.invalid/v1".to_string(),
|
||||||
model: "fixture-model".to_string(),
|
model: "fixture-model".to_string(),
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ pub(crate) fn normalize_direct_client_turn_id(
|
|||||||
pub(crate) async fn chat_with_game_creator_direct_codex(
|
pub(crate) async fn chat_with_game_creator_direct_codex(
|
||||||
project_path: String,
|
project_path: String,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
user_item: DirectCodexUserItem,
|
mut user_item: DirectCodexUserItem,
|
||||||
creation_type: Option<String>,
|
creation_type: Option<String>,
|
||||||
client_turn_id: Option<String>,
|
client_turn_id: Option<String>,
|
||||||
|
attachments: Option<Vec<DirectCodexTurnAttachment>>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let root = Path::new(project_path.trim());
|
let root = Path::new(project_path.trim());
|
||||||
let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?;
|
let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?;
|
||||||
@@ -42,7 +43,24 @@ pub(crate) async fn chat_with_game_creator_direct_codex(
|
|||||||
redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500)
|
redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500)
|
||||||
})?;
|
})?;
|
||||||
let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone());
|
let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone());
|
||||||
let mut audit = DirectCodexTurnAudit::start(root, &turn_id, &prompt, &[]);
|
let mut audit = DirectCodexTurnAudit::start(
|
||||||
|
root,
|
||||||
|
&turn_id,
|
||||||
|
&prompt,
|
||||||
|
attachments.as_deref().unwrap_or_default(),
|
||||||
|
);
|
||||||
|
let attachments = attachments.unwrap_or_default();
|
||||||
|
if !attachments.is_empty() {
|
||||||
|
let attachment_context =
|
||||||
|
render_direct_codex_user_prompt("", &attachments).map_err(|error| {
|
||||||
|
audit.finish(false);
|
||||||
|
error
|
||||||
|
})?;
|
||||||
|
let DirectCodexUserItem::Message(message) = &mut user_item;
|
||||||
|
message.content.push(DirectCodexUserContentPart::InputText {
|
||||||
|
text: attachment_context,
|
||||||
|
});
|
||||||
|
}
|
||||||
validate_direct_codex_user_item(root, &user_item).map_err(|error| {
|
validate_direct_codex_user_item(root, &user_item).map_err(|error| {
|
||||||
audit.finish(false);
|
audit.finish(false);
|
||||||
error
|
error
|
||||||
|
|||||||
+18
@@ -605,6 +605,8 @@ fn finalization_cleanup_closes_entire_tool_plan_repair_chain_before_removal() {
|
|||||||
response_stream_fixture("finalization-tool-plan-repair-chain-run");
|
response_stream_fixture("finalization-tool-plan-repair-chain-run");
|
||||||
let root = project.path();
|
let root = project.path();
|
||||||
let llm = GameCreatorLlmConfig {
|
let llm = GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "finalization-tool-plan-key".to_string(),
|
api_key: "finalization-tool-plan-key".to_string(),
|
||||||
base_url: "http://127.0.0.1:1/v1".to_string(),
|
base_url: "http://127.0.0.1:1/v1".to_string(),
|
||||||
model: "finalization-tool-plan-model".to_string(),
|
model: "finalization-tool-plan-model".to_string(),
|
||||||
@@ -968,6 +970,8 @@ async fn provider_handoff_identity_drift_closes_lifecycle_without_leaking_respon
|
|||||||
let root = project.path();
|
let root = project.path();
|
||||||
let request = LlmRunRequest::new(vec![LlmMessage::user("验证 handoff 身份漂移")]);
|
let request = LlmRunRequest::new(vec![LlmMessage::user("验证 handoff 身份漂移")]);
|
||||||
let old_llm = GameCreatorLlmConfig {
|
let old_llm = GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "old-provider-key".to_string(),
|
api_key: "old-provider-key".to_string(),
|
||||||
base_url: "http://127.0.0.1:1/v1".to_string(),
|
base_url: "http://127.0.0.1:1/v1".to_string(),
|
||||||
model: "old-provider-model".to_string(),
|
model: "old-provider-model".to_string(),
|
||||||
@@ -1073,6 +1077,8 @@ async fn tool_plan_handoff_identity_drift_closes_entire_repair_chain_before_remo
|
|||||||
LlmMessage::user("修复格式"),
|
LlmMessage::user("修复格式"),
|
||||||
]);
|
]);
|
||||||
let old_llm = GameCreatorLlmConfig {
|
let old_llm = GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "old-tool-plan-provider-key".to_string(),
|
api_key: "old-tool-plan-provider-key".to_string(),
|
||||||
base_url: "http://127.0.0.1:1/v1".to_string(),
|
base_url: "http://127.0.0.1:1/v1".to_string(),
|
||||||
model: "old-tool-plan-model".to_string(),
|
model: "old-tool-plan-model".to_string(),
|
||||||
@@ -1192,6 +1198,8 @@ async fn generic_retry_identity_drift_closes_tool_plan_repair_chain_before_remov
|
|||||||
response_stream_fixture("generic-retry-drift-tool-plan-chain-run");
|
response_stream_fixture("generic-retry-drift-tool-plan-chain-run");
|
||||||
let root = project.path();
|
let root = project.path();
|
||||||
let old_llm = GameCreatorLlmConfig {
|
let old_llm = GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "old-generic-retry-key".to_string(),
|
api_key: "old-generic-retry-key".to_string(),
|
||||||
base_url: "http://127.0.0.1:1/v1".to_string(),
|
base_url: "http://127.0.0.1:1/v1".to_string(),
|
||||||
model: "old-generic-retry-model".to_string(),
|
model: "old-generic-retry-model".to_string(),
|
||||||
@@ -1277,6 +1285,8 @@ async fn tool_plan_capacity_gate_runs_before_provider_lifecycle_and_network() {
|
|||||||
response_stream_fixture("tool-plan-capacity-preflight-run");
|
response_stream_fixture("tool-plan-capacity-preflight-run");
|
||||||
let root = project.path();
|
let root = project.path();
|
||||||
let llm = GameCreatorLlmConfig {
|
let llm = GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "tool-plan-capacity-key".to_string(),
|
api_key: "tool-plan-capacity-key".to_string(),
|
||||||
base_url: "http://127.0.0.1:1/v1".to_string(),
|
base_url: "http://127.0.0.1:1/v1".to_string(),
|
||||||
model: "tool-plan-capacity-model".to_string(),
|
model: "tool-plan-capacity-model".to_string(),
|
||||||
@@ -1400,6 +1410,8 @@ async fn tool_plan_handoff_durable_control_closes_entire_repair_chain_before_rem
|
|||||||
LlmMessage::user("修复格式"),
|
LlmMessage::user("修复格式"),
|
||||||
]);
|
]);
|
||||||
let llm = GameCreatorLlmConfig {
|
let llm = GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "durable-control-tool-plan-key".to_string(),
|
api_key: "durable-control-tool-plan-key".to_string(),
|
||||||
base_url: "http://127.0.0.1:1/v1".to_string(),
|
base_url: "http://127.0.0.1:1/v1".to_string(),
|
||||||
model: "durable-control-tool-plan-model".to_string(),
|
model: "durable-control-tool-plan-model".to_string(),
|
||||||
@@ -1525,6 +1537,8 @@ fn provider_recovery_cleanup_closes_tool_plan_lifecycle_before_removing_handoff(
|
|||||||
snapshot.request_slot = "loop-0-repair-0".to_string();
|
snapshot.request_slot = "loop-0-repair-0".to_string();
|
||||||
let request = LlmRunRequest::new(vec![LlmMessage::user("等待 steer 或 cancel")]);
|
let request = LlmRunRequest::new(vec![LlmMessage::user("等待 steer 或 cancel")]);
|
||||||
let llm = GameCreatorLlmConfig {
|
let llm = GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "tool-plan-cleanup-key".to_string(),
|
api_key: "tool-plan-cleanup-key".to_string(),
|
||||||
base_url: "http://127.0.0.1:1/v1".to_string(),
|
base_url: "http://127.0.0.1:1/v1".to_string(),
|
||||||
model: "tool-plan-cleanup-model".to_string(),
|
model: "tool-plan-cleanup-model".to_string(),
|
||||||
@@ -1597,6 +1611,8 @@ fn runtime_resume_scans_and_cleans_terminal_tool_plan_handoff() {
|
|||||||
snapshot.request_slot = "loop-0-repair-0".to_string();
|
snapshot.request_slot = "loop-0-repair-0".to_string();
|
||||||
let request = LlmRunRequest::new(vec![LlmMessage::user("终态遗留 handoff")]);
|
let request = LlmRunRequest::new(vec![LlmMessage::user("终态遗留 handoff")]);
|
||||||
let llm = GameCreatorLlmConfig {
|
let llm = GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "terminal-handoff-key".to_string(),
|
api_key: "terminal-handoff-key".to_string(),
|
||||||
base_url: "http://127.0.0.1:1/v1".to_string(),
|
base_url: "http://127.0.0.1:1/v1".to_string(),
|
||||||
model: "terminal-handoff-model".to_string(),
|
model: "terminal-handoff-model".to_string(),
|
||||||
@@ -1693,6 +1709,8 @@ async fn provider_handoff_retry_conflict_preserves_both_sidecars_for_reconciliat
|
|||||||
let root = project.path();
|
let root = project.path();
|
||||||
let request = LlmRunRequest::new(vec![LlmMessage::user("验证 handoff/retry 冲突")]);
|
let request = LlmRunRequest::new(vec![LlmMessage::user("验证 handoff/retry 冲突")]);
|
||||||
let llm = GameCreatorLlmConfig {
|
let llm = GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: "provider-key".to_string(),
|
api_key: "provider-key".to_string(),
|
||||||
base_url: "http://127.0.0.1:1/v1".to_string(),
|
base_url: "http://127.0.0.1:1/v1".to_string(),
|
||||||
model: "provider-model".to_string(),
|
model: "provider-model".to_string(),
|
||||||
|
|||||||
@@ -1991,6 +1991,8 @@ pub(crate) fn write_game_creator_app_config(
|
|||||||
.lock()
|
.lock()
|
||||||
.map_err(|_| "配置写入锁不可用")?;
|
.map_err(|_| "配置写入锁不可用")?;
|
||||||
let (current, overlays) = load_game_creator_app_config_for_write()?;
|
let (current, overlays) = load_game_creator_app_config_for_write()?;
|
||||||
|
// 自定义开关只能从本地配置文件开启,不能由渲染层越过配置门禁。
|
||||||
|
config.llm.custom_enabled = current.llm.custom_enabled;
|
||||||
config.selected_model_id = current.selected_model_id;
|
config.selected_model_id = current.selected_model_id;
|
||||||
config.selected_model_is_default = current.selected_model_is_default;
|
config.selected_model_is_default = current.selected_model_is_default;
|
||||||
persist_game_creator_app_config(config, overlays, false)
|
persist_game_creator_app_config(config, overlays, false)
|
||||||
@@ -2027,7 +2029,12 @@ pub(crate) fn select_game_creator_model(
|
|||||||
let _guard = GAME_CREATOR_CONFIG_WRITE_LOCK
|
let _guard = GAME_CREATOR_CONFIG_WRITE_LOCK
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|_| "配置写入锁不可用")?;
|
.map_err(|_| "配置写入锁不可用")?;
|
||||||
if model_id.is_empty()
|
let (mut config, overlays) = load_game_creator_app_config_for_write()?;
|
||||||
|
if config.llm.custom_enabled {
|
||||||
|
if !config.llm.visible_models.contains(&model_id) {
|
||||||
|
return Err("所选模型未勾选或已移除,请刷新模型列表".into());
|
||||||
|
}
|
||||||
|
} else if model_id.is_empty()
|
||||||
|| model_id.len() > 64
|
|| model_id.len() > 64
|
||||||
|| !model_id
|
|| !model_id
|
||||||
.bytes()
|
.bytes()
|
||||||
@@ -2035,12 +2042,21 @@ pub(crate) fn select_game_creator_model(
|
|||||||
{
|
{
|
||||||
return Err("模型标识无效".into());
|
return Err("模型标识无效".into());
|
||||||
}
|
}
|
||||||
let (mut config, overlays) = load_game_creator_app_config_for_write()?;
|
|
||||||
config.selected_model_id = model_id;
|
config.selected_model_id = model_id;
|
||||||
config.selected_model_is_default = is_default;
|
config.selected_model_is_default = is_default;
|
||||||
persist_game_creator_app_config(config, overlays, true)
|
persist_game_creator_app_config(config, overlays, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub(crate) async fn discover_game_creator_llm_models(
|
||||||
|
llm: GameCreatorLlmConfig,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
if !load_game_creator_app_config()?.llm.custom_enabled {
|
||||||
|
return Err("请先在本地配置中开启 llm.customEnabled".to_string());
|
||||||
|
}
|
||||||
|
fetch_custom_llm_models(&llm).await
|
||||||
|
}
|
||||||
|
|
||||||
fn persist_game_creator_app_config(
|
fn persist_game_creator_app_config(
|
||||||
config: GameCreatorAppConfig,
|
config: GameCreatorAppConfig,
|
||||||
overlays: Vec<(PathBuf, serde_json::Value)>,
|
overlays: Vec<(PathBuf, serde_json::Value)>,
|
||||||
@@ -2056,8 +2072,8 @@ fn persist_game_creator_app_config(
|
|||||||
let previous = overlay.clone();
|
let previous = overlay.clone();
|
||||||
if let Some(fields) = overlay.as_object_mut() {
|
if let Some(fields) = overlay.as_object_mut() {
|
||||||
for (key, value) in fields.iter_mut() {
|
for (key, value) in fields.iter_mut() {
|
||||||
if matches!(key.as_str(), "selectedModelId" | "selectedModelIsDefault")
|
if !model_only
|
||||||
== model_only
|
|| matches!(key.as_str(), "selectedModelId" | "selectedModelIsDefault")
|
||||||
{
|
{
|
||||||
if let Some(saved_value) = saved.get(key) {
|
if let Some(saved_value) = saved.get(key) {
|
||||||
// 仅同步已有覆盖项;其它字段继续保留原有覆盖语义。
|
// 仅同步已有覆盖项;其它字段继续保留原有覆盖语义。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1084,6 +1084,10 @@ struct GameCreatorAppConfigFile {
|
|||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct GameCreatorLlmConfigFile {
|
struct GameCreatorLlmConfigFile {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
custom_enabled: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
visible_models: Option<Vec<String>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -1139,6 +1143,10 @@ struct GameCreatorAppConfig {
|
|||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct GameCreatorLlmConfig {
|
struct GameCreatorLlmConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
custom_enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
visible_models: Vec<String>,
|
||||||
api_key: String,
|
api_key: String,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
model: String,
|
model: String,
|
||||||
@@ -1655,6 +1663,8 @@ impl Default for GameCreatorAppConfig {
|
|||||||
impl Default for GameCreatorLlmConfig {
|
impl Default for GameCreatorLlmConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: String::new(),
|
api_key: String::new(),
|
||||||
base_url: DEFAULT_GAME_CREATOR_LLM_BASE_URL.to_string(),
|
base_url: DEFAULT_GAME_CREATOR_LLM_BASE_URL.to_string(),
|
||||||
model: DEFAULT_GAME_CREATOR_LLM_MODEL.to_string(),
|
model: DEFAULT_GAME_CREATOR_LLM_MODEL.to_string(),
|
||||||
@@ -2721,6 +2731,7 @@ fn main() {
|
|||||||
read_game_creator_app_config,
|
read_game_creator_app_config,
|
||||||
write_game_creator_app_config,
|
write_game_creator_app_config,
|
||||||
select_game_creator_model,
|
select_game_creator_model,
|
||||||
|
discover_game_creator_llm_models,
|
||||||
upload_local_asset,
|
upload_local_asset,
|
||||||
register_local_asset,
|
register_local_asset,
|
||||||
create_ui_design_resource,
|
create_ui_design_resource,
|
||||||
|
|||||||
@@ -1,5 +1,73 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_official_config_gains_hand_editable_connection_keys_on_startup() {
|
||||||
|
// 用户现有配置文件(官方路由、连接字段已被清掉)在启动迁移路径上必须补齐
|
||||||
|
// 开关、模型列表与连接四要素,手写自定义连接时能看到完整字段。
|
||||||
|
let mut config: GameCreatorAppConfigFile = serde_json::from_str(
|
||||||
|
r#"{"schemaVersion":"game-creator-config.v2","agentMode":"codex_app_server","llm":{"reasoningEffort":"max","stream":true},"selectedModelId":"quality","selectedModelIsDefault":true}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(ensure_game_creator_custom_llm_file_fields(&mut config));
|
||||||
|
assert!(scrub_locked_game_creator_config_file(&mut config));
|
||||||
|
let migrated: serde_json::Value = serde_json::to_value(&config).unwrap();
|
||||||
|
assert_eq!(migrated["llm"]["customEnabled"], false);
|
||||||
|
assert_eq!(migrated["llm"]["visibleModels"], serde_json::json!([]));
|
||||||
|
assert_eq!(migrated["llm"]["apiKey"], "");
|
||||||
|
assert_eq!(migrated["llm"]["baseUrl"], OFFICIAL_LLM_ROUTER_BASE_URL);
|
||||||
|
assert_eq!(migrated["llm"]["model"], "quality");
|
||||||
|
assert_eq!(
|
||||||
|
migrated["llm"]["apiKind"],
|
||||||
|
DEFAULT_GAME_CREATOR_LLM_API_KIND
|
||||||
|
);
|
||||||
|
assert_eq!(migrated["llm"]["reasoningEffort"], "max");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_llm_config_save_reload_and_selection_preserve_overlay_and_credentials() {
|
||||||
|
let root = unique_project_path();
|
||||||
|
fs::create_dir_all(&root).unwrap();
|
||||||
|
let _guard = use_test_runtime_config_dir(root.clone());
|
||||||
|
let primary = root.join(GAME_CREATOR_CONFIG_FILE_NAME);
|
||||||
|
let overlay = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME);
|
||||||
|
write_game_creator_config_atomically(
|
||||||
|
&primary,
|
||||||
|
&serde_json::to_string(&GameCreatorAppConfig::default()).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
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();
|
||||||
|
assert_eq!(selected.config.llm.model, "b:v2");
|
||||||
|
assert!(select_game_creator_model("not-listed".into(), false).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();
|
||||||
|
let reloaded = read_game_creator_app_config().unwrap().config;
|
||||||
|
assert!(reloaded.llm.custom_enabled);
|
||||||
|
assert_eq!(reloaded.llm.visible_models, ["b:v2"]);
|
||||||
|
assert_eq!(reloaded.llm.model, "b:v2");
|
||||||
|
assert_eq!(reloaded.llm.api_key, "changed-fixture-key");
|
||||||
|
let persisted: serde_json::Value =
|
||||||
|
serde_json::from_str(&fs::read_to_string(&primary).unwrap()).unwrap();
|
||||||
|
for key in [
|
||||||
|
"customEnabled",
|
||||||
|
"visibleModels",
|
||||||
|
"apiKey",
|
||||||
|
"baseUrl",
|
||||||
|
"model",
|
||||||
|
"apiKind",
|
||||||
|
"reasoningEffort",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
persisted["llm"].get(key).is_some(),
|
||||||
|
"本地配置始终保留 {key},方便手写自定义连接:{persisted}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_file_overrides_defaults_without_env() {
|
fn config_file_overrides_defaults_without_env() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
@@ -313,10 +381,14 @@ fn locked_config_scrub_removes_all_legacy_provider_credentials() {
|
|||||||
.llm
|
.llm
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("global llm remains as non-sensitive tuning");
|
.expect("global llm remains as non-sensitive tuning");
|
||||||
assert!(llm.api_key.is_none());
|
// 连接字段保留在文件里(空 Key + 官方地址),便于手写自定义连接时对照。
|
||||||
assert!(llm.base_url.is_none());
|
assert_eq!(llm.api_key.as_deref(), Some(""));
|
||||||
assert!(llm.model.is_none());
|
assert_eq!(llm.base_url.as_deref(), Some(OFFICIAL_LLM_ROUTER_BASE_URL));
|
||||||
assert!(llm.api_kind.is_none());
|
assert_eq!(
|
||||||
|
llm.api_kind.as_deref(),
|
||||||
|
Some(DEFAULT_GAME_CREATOR_LLM_API_KIND)
|
||||||
|
);
|
||||||
|
assert!(llm.model.is_some());
|
||||||
let serialized = serde_json::to_string(&config).expect("serialize scrubbed config");
|
let serialized = serde_json::to_string(&config).expect("serialize scrubbed config");
|
||||||
assert!(!serialized.contains("legacy-global-key"));
|
assert!(!serialized.contains("legacy-global-key"));
|
||||||
assert!(!serialized.contains("legacy-agent-key"));
|
assert!(!serialized.contains("legacy-agent-key"));
|
||||||
@@ -688,6 +760,8 @@ fn app_config_commands_write_runtime_config_file() {
|
|||||||
agent_llm.insert(
|
agent_llm.insert(
|
||||||
" planner ".to_string(),
|
" planner ".to_string(),
|
||||||
GameCreatorLlmConfigFile {
|
GameCreatorLlmConfigFile {
|
||||||
|
custom_enabled: None,
|
||||||
|
visible_models: None,
|
||||||
api_key: Some(" planner-key ".to_string()),
|
api_key: Some(" planner-key ".to_string()),
|
||||||
base_url: Some(" https://planner.example.test/v1 ".to_string()),
|
base_url: Some(" https://planner.example.test/v1 ".to_string()),
|
||||||
model: Some(" planner-model ".to_string()),
|
model: Some(" planner-model ".to_string()),
|
||||||
@@ -709,6 +783,8 @@ fn app_config_commands_write_runtime_config_file() {
|
|||||||
schema_version: GAME_CREATOR_APP_CONFIG_SCHEMA_VERSION.to_string(),
|
schema_version: GAME_CREATOR_APP_CONFIG_SCHEMA_VERSION.to_string(),
|
||||||
agent_mode: GAME_CREATOR_AGENT_MODE_PROVIDER.to_string(),
|
agent_mode: GAME_CREATOR_AGENT_MODE_PROVIDER.to_string(),
|
||||||
llm: GameCreatorLlmConfig {
|
llm: GameCreatorLlmConfig {
|
||||||
|
custom_enabled: false,
|
||||||
|
visible_models: Vec::new(),
|
||||||
api_key: " unit-test-key ".to_string(),
|
api_key: " unit-test-key ".to_string(),
|
||||||
base_url: " https://runtime.example.test/v1 ".to_string(),
|
base_url: " https://runtime.example.test/v1 ".to_string(),
|
||||||
model: " runtime-model ".to_string(),
|
model: " runtime-model ".to_string(),
|
||||||
@@ -838,7 +914,7 @@ fn app_config_save_updates_conflicting_local_overlay() {
|
|||||||
fs::create_dir_all(&root).expect("config dir");
|
fs::create_dir_all(&root).expect("config dir");
|
||||||
let _guard = use_test_runtime_config_dir(root.clone());
|
let _guard = use_test_runtime_config_dir(root.clone());
|
||||||
let overlay_path = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME);
|
let overlay_path = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME);
|
||||||
fs::write(
|
write_game_creator_config_atomically(
|
||||||
&overlay_path,
|
&overlay_path,
|
||||||
r#"{"llm":{"reasoningEffort":"low"},"selectedModelId":"existing","selectedModelIsDefault":true,"custom":{"keep":true}}"#,
|
r#"{"llm":{"reasoningEffort":"low"},"selectedModelId":"existing","selectedModelIsDefault":true,"custom":{"keep":true}}"#,
|
||||||
)
|
)
|
||||||
@@ -871,7 +947,7 @@ fn app_config_model_selection_only_updates_model_overlay() {
|
|||||||
fs::create_dir_all(&root).expect("config dir");
|
fs::create_dir_all(&root).expect("config dir");
|
||||||
let _guard = use_test_runtime_config_dir(root.clone());
|
let _guard = use_test_runtime_config_dir(root.clone());
|
||||||
let overlay_path = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME);
|
let overlay_path = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME);
|
||||||
fs::write(
|
write_game_creator_config_atomically(
|
||||||
&overlay_path,
|
&overlay_path,
|
||||||
r#"{"selectedModelId":"old","selectedModelIsDefault":false,"llm":{"reasoningEffort":"low"}}"#,
|
r#"{"selectedModelId":"old","selectedModelIsDefault":false,"llm":{"reasoningEffort":"low"}}"#,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5885,6 +5885,8 @@ async fn agent_loop_uses_per_agent_llm_overrides() {
|
|||||||
agent_llm.insert(
|
agent_llm.insert(
|
||||||
"planner".to_string(),
|
"planner".to_string(),
|
||||||
GameCreatorLlmConfigFile {
|
GameCreatorLlmConfigFile {
|
||||||
|
custom_enabled: None,
|
||||||
|
visible_models: None,
|
||||||
api_key: Some("planner-key".to_string()),
|
api_key: Some("planner-key".to_string()),
|
||||||
base_url: Some(planner_base_url),
|
base_url: Some(planner_base_url),
|
||||||
model: Some("planner-model".to_string()),
|
model: Some("planner-model".to_string()),
|
||||||
@@ -5903,6 +5905,8 @@ async fn agent_loop_uses_per_agent_llm_overrides() {
|
|||||||
agent_llm.insert(
|
agent_llm.insert(
|
||||||
"generator".to_string(),
|
"generator".to_string(),
|
||||||
GameCreatorLlmConfigFile {
|
GameCreatorLlmConfigFile {
|
||||||
|
custom_enabled: None,
|
||||||
|
visible_models: None,
|
||||||
api_key: Some("generator-key".to_string()),
|
api_key: Some("generator-key".to_string()),
|
||||||
base_url: Some(generator_base_url),
|
base_url: Some(generator_base_url),
|
||||||
model: Some("generator-model".to_string()),
|
model: Some("generator-model".to_string()),
|
||||||
@@ -5921,6 +5925,8 @@ async fn agent_loop_uses_per_agent_llm_overrides() {
|
|||||||
agent_llm.insert(
|
agent_llm.insert(
|
||||||
"art-asset-plan".to_string(),
|
"art-asset-plan".to_string(),
|
||||||
GameCreatorLlmConfigFile {
|
GameCreatorLlmConfigFile {
|
||||||
|
custom_enabled: None,
|
||||||
|
visible_models: None,
|
||||||
api_key: Some("art-key".to_string()),
|
api_key: Some("art-key".to_string()),
|
||||||
base_url: Some(art_base_url),
|
base_url: Some(art_base_url),
|
||||||
model: Some("art-model".to_string()),
|
model: Some("art-model".to_string()),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "陶泥儿",
|
"productName": "陶泥儿",
|
||||||
"version": "0.1.29",
|
"version": "0.1.45",
|
||||||
"identifier": "world.genarrative.ai-game-creator",
|
"identifier": "world.genarrative.ai-game-creator",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
|
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
|
||||||
|
|||||||
@@ -891,9 +891,16 @@ export function App({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [chatInput, setChatInput] = useState(() =>
|
||||||
|
supervisorChatOnly && initialProjectPath
|
||||||
|
? readSupervisorChatDraft(initialProjectPath)
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
const [chatReferences, setChatReferences] = useState<ChatReference[]>([]);
|
||||||
/**
|
/**
|
||||||
* 输入盒待发送附件(direct-codex 回合附件):上传成功后先生成 chip,随下次提交一起
|
* 输入盒待发送附件(direct-codex 回合附件):上传成功后先生成 chip,随下次提交一起
|
||||||
* 转换为 canonical user item 的 `content[]`。附件只存在于前端待发状态,提交后即清空。
|
* 交给 `chat_with_game_creator_direct_codex` 的 `attachments`。附件只存在于前端状态,
|
||||||
|
* 提交后即清空——后端协议不变。
|
||||||
*/
|
*/
|
||||||
const [chatAttachments, setChatAttachments] = useState<
|
const [chatAttachments, setChatAttachments] = useState<
|
||||||
DirectCodexTurnAttachment[]
|
DirectCodexTurnAttachment[]
|
||||||
@@ -907,6 +914,9 @@ export function App({
|
|||||||
const [directCodexTurnCancelling, setDirectCodexTurnCancelling] =
|
const [directCodexTurnCancelling, setDirectCodexTurnCancelling] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const queuedChatTurnSequenceRef = useRef(0);
|
const queuedChatTurnSequenceRef = useRef(0);
|
||||||
|
const [chatContent, setChatContent] = useState<DirectCodexUserContentPart[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
const chatComposerRef = useRef<ResourceReferenceInputHandle | null>(null);
|
const chatComposerRef = useRef<ResourceReferenceInputHandle | null>(null);
|
||||||
/**
|
/**
|
||||||
* 切项目即清空只属于上一个项目的输入盒状态:待发附件的 `localPath` 是**项目相对**的,
|
* 切项目即清空只属于上一个项目的输入盒状态:待发附件的 `localPath` 是**项目相对**的,
|
||||||
@@ -914,6 +924,7 @@ export function App({
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChatAttachments([]);
|
setChatAttachments([]);
|
||||||
|
setChatContent([]);
|
||||||
setChatAttachmentNotice('');
|
setChatAttachmentNotice('');
|
||||||
setChatComposerNotice('');
|
setChatComposerNotice('');
|
||||||
setChatTurnQueue([]);
|
setChatTurnQueue([]);
|
||||||
@@ -3079,12 +3090,8 @@ export function App({
|
|||||||
if (!supervisorChatOnly) {
|
if (!supervisorChatOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const draft = chatComposerRef.current?.getDraft();
|
persistSupervisorChatDraft(initialProjectPath, chatInput);
|
||||||
if (!draft?.text.trim()) {
|
}, [chatInput, initialProjectPath, supervisorChatOnly]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
persistSupervisorChatDraft(initialProjectPath, draft.text);
|
|
||||||
}, [initialProjectPath, supervisorChatOnly]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
latestMessagesRef.current = messages;
|
latestMessagesRef.current = messages;
|
||||||
@@ -4374,12 +4381,10 @@ export function App({
|
|||||||
setWorkspaceProjectKind(projectKind);
|
setWorkspaceProjectKind(projectKind);
|
||||||
setLocalProject(openedProject);
|
setLocalProject(openedProject);
|
||||||
if (supervisorChatOnly) {
|
if (supervisorChatOnly) {
|
||||||
chatComposerRef.current?.replaceText(
|
setChatInput(readSupervisorChatDraft(openedProject.projectPath));
|
||||||
readSupervisorChatDraft(openedProject.projectPath),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
clearChatComposer();
|
|
||||||
}
|
}
|
||||||
|
setChatReferences([]);
|
||||||
|
setChatContent([]);
|
||||||
setManifest(openedProject.manifest);
|
setManifest(openedProject.manifest);
|
||||||
setProjectFiles([]);
|
setProjectFiles([]);
|
||||||
setProjectCheckpoints([]);
|
setProjectCheckpoints([]);
|
||||||
@@ -4618,29 +4623,16 @@ export function App({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function prepareChatCommandDraft(commandDraft: string) {
|
function prepareChatCommandDraft(commandDraft: string) {
|
||||||
chatComposerRef.current?.clear();
|
setChatInput(commandDraft);
|
||||||
chatComposerRef.current?.replaceText(commandDraft);
|
setChatReferences([]);
|
||||||
|
setChatContent([]);
|
||||||
window.setTimeout(() => chatInputRef.current?.focus(), 0);
|
window.setTimeout(() => chatInputRef.current?.focus(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readChatComposerDraft(): ChatComposerDraft {
|
|
||||||
return (
|
|
||||||
chatComposerRef.current?.getDraft() ?? {
|
|
||||||
text: '',
|
|
||||||
references: [],
|
|
||||||
content: [],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearChatComposer() {
|
|
||||||
chatComposerRef.current?.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChatComposerChange(draft: ChatComposerDraft) {
|
function handleChatComposerChange(draft: ChatComposerDraft) {
|
||||||
if (supervisorChatOnly) {
|
setChatInput(draft.text);
|
||||||
persistSupervisorChatDraft(initialProjectPath, draft.text);
|
setChatReferences(draft.references);
|
||||||
}
|
setChatContent(draft.content ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -5668,9 +5660,8 @@ export function App({
|
|||||||
|
|
||||||
async function handleChatSubmit(event: FormEvent<HTMLFormElement>) {
|
async function handleChatSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const draft = readChatComposerDraft();
|
const prompt = chatInput.trim();
|
||||||
const prompt = draft.text.trim();
|
const references = chatReferences;
|
||||||
const references = draft.references;
|
|
||||||
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
||||||
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
||||||
return;
|
return;
|
||||||
@@ -5679,7 +5670,9 @@ export function App({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearChatComposer();
|
setChatInput('');
|
||||||
|
setChatReferences([]);
|
||||||
|
setChatContent([]);
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
{
|
{
|
||||||
@@ -6769,7 +6762,7 @@ export function App({
|
|||||||
|
|
||||||
const clientTurnId = createDirectCodexConversationTurnId();
|
const clientTurnId = createDirectCodexConversationTurnId();
|
||||||
const userItem = chatComposerDraftToDirectCodexUserItem(
|
const userItem = chatComposerDraftToDirectCodexUserItem(
|
||||||
draft,
|
{ text: prompt, references, content: chatContent },
|
||||||
directCodexConversationMessageId(clientTurnId, 'user'),
|
directCodexConversationMessageId(clientTurnId, 'user'),
|
||||||
);
|
);
|
||||||
void executeChatAgentReply({ prompt, references, userItem, clientTurnId });
|
void executeChatAgentReply({ prompt, references, userItem, clientTurnId });
|
||||||
@@ -7039,32 +7032,12 @@ export function App({
|
|||||||
if (directProjectPath && directProjectId && directInvoke) {
|
if (directProjectPath && directProjectId && directInvoke) {
|
||||||
const clientTurnId =
|
const clientTurnId =
|
||||||
directConversationTurnId ?? createDirectCodexConversationTurnId();
|
directConversationTurnId ?? createDirectCodexConversationTurnId();
|
||||||
const baseUserItem =
|
const effectiveUserItem =
|
||||||
userItem ??
|
userItem ??
|
||||||
chatComposerDraftToDirectCodexUserItem(
|
chatComposerDraftToDirectCodexUserItem(
|
||||||
{ text: prompt, references: references ?? [], content: [] },
|
{ text: prompt, references: references ?? [], content: [] },
|
||||||
directCodexConversationMessageId(clientTurnId, 'user'),
|
directCodexConversationMessageId(clientTurnId, 'user'),
|
||||||
);
|
);
|
||||||
const effectiveUserItem = attachments?.length
|
|
||||||
? {
|
|
||||||
...baseUserItem,
|
|
||||||
content: [
|
|
||||||
...baseUserItem.content,
|
|
||||||
...attachments.map((attachment) => ({
|
|
||||||
type: attachment.mediaType.toLowerCase().startsWith('image/')
|
|
||||||
? ('agc_image_reference' as const)
|
|
||||||
: ('agc_attachment_reference' as const),
|
|
||||||
name: attachment.name,
|
|
||||||
mediaType: attachment.mediaType,
|
|
||||||
size: attachment.size ?? 0,
|
|
||||||
localPath: attachment.localPath ?? '',
|
|
||||||
status:
|
|
||||||
attachment.status ??
|
|
||||||
(attachment.localPath ? 'imported' : 'failed'),
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: baseUserItem;
|
|
||||||
if (
|
if (
|
||||||
!directPolicyChecked &&
|
!directPolicyChecked &&
|
||||||
projectConversationWriteConfirmedRef.current !== directProjectPath
|
projectConversationWriteConfirmedRef.current !== directProjectPath
|
||||||
@@ -7205,6 +7178,7 @@ export function App({
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
clientTurnId: string;
|
clientTurnId: string;
|
||||||
creationType?: HomeCreationType;
|
creationType?: HomeCreationType;
|
||||||
|
attachments?: DirectCodexTurnAttachment[];
|
||||||
userItem: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
|
userItem: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
|
||||||
} = {
|
} = {
|
||||||
projectPath: directProjectPath,
|
projectPath: directProjectPath,
|
||||||
@@ -7215,6 +7189,9 @@ export function App({
|
|||||||
if (creationType) {
|
if (creationType) {
|
||||||
directTurnInput.creationType = creationType;
|
directTurnInput.creationType = creationType;
|
||||||
}
|
}
|
||||||
|
if (attachments?.length) {
|
||||||
|
directTurnInput.attachments = attachments;
|
||||||
|
}
|
||||||
directTurnInput.userItem = effectiveUserItem;
|
directTurnInput.userItem = effectiveUserItem;
|
||||||
const reply = await withDirectCodexSessionRefresh(() => {
|
const reply = await withDirectCodexSessionRefresh(() => {
|
||||||
// 每次调用都会新建 Rust 事件流;续期重试需重新接收同一回合的进度。
|
// 每次调用都会新建 Rust 事件流;续期重试需重新接收同一回合的进度。
|
||||||
@@ -12615,20 +12592,6 @@ export function App({
|
|||||||
content?: DirectCodexUserContentPart[];
|
content?: DirectCodexUserContentPart[];
|
||||||
}) {
|
}) {
|
||||||
const clientTurnId = createDirectCodexConversationTurnId();
|
const clientTurnId = createDirectCodexConversationTurnId();
|
||||||
const attachmentContent: DirectCodexUserContentPart[] = (
|
|
||||||
input.attachments ?? []
|
|
||||||
).map((attachment) => ({
|
|
||||||
type: attachment.mediaType.toLowerCase().startsWith('image/')
|
|
||||||
? ('agc_image_reference' as const)
|
|
||||||
: ('agc_attachment_reference' as const),
|
|
||||||
name: attachment.name,
|
|
||||||
mediaType: attachment.mediaType,
|
|
||||||
size: attachment.size ?? 0,
|
|
||||||
localPath: attachment.localPath ?? '',
|
|
||||||
status:
|
|
||||||
attachment.status ?? (attachment.localPath ? 'imported' : 'failed'),
|
|
||||||
}));
|
|
||||||
const content = [...(input.content ?? []), ...attachmentContent];
|
|
||||||
supervisorChatShouldFollowLatestRef.current = true;
|
supervisorChatShouldFollowLatestRef.current = true;
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
@@ -12643,18 +12606,16 @@ export function App({
|
|||||||
void executeChatAgentReply({
|
void executeChatAgentReply({
|
||||||
prompt: input.prompt,
|
prompt: input.prompt,
|
||||||
clientTurnId,
|
clientTurnId,
|
||||||
|
attachments: input.attachments?.length ? input.attachments : undefined,
|
||||||
references: input.references,
|
references: input.references,
|
||||||
userItem: chatComposerDraftToDirectCodexUserItem(
|
userItem: chatComposerDraftToDirectCodexUserItem(
|
||||||
{
|
{
|
||||||
text: input.prompt,
|
text: input.prompt,
|
||||||
references: input.references ?? [],
|
references: input.references ?? [],
|
||||||
content,
|
content: input.content ?? [],
|
||||||
},
|
},
|
||||||
directCodexConversationMessageId(clientTurnId, 'user'),
|
directCodexConversationMessageId(clientTurnId, 'user'),
|
||||||
),
|
),
|
||||||
// DirectProject 的附件已经是 canonical content part;不能再作为 sidecar
|
|
||||||
// 传给 Rust,否则会重复追加。
|
|
||||||
attachments: undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12823,10 +12784,8 @@ export function App({
|
|||||||
event: FormEvent<HTMLFormElement>,
|
event: FormEvent<HTMLFormElement>,
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const draft = readChatComposerDraft();
|
const prompt = chatInput.trim();
|
||||||
const prompt = draft.text.trim();
|
const references = chatReferences;
|
||||||
const references = draft.references;
|
|
||||||
const content = draft.content ?? [];
|
|
||||||
const pendingAttachments = chatAttachments;
|
const pendingAttachments = chatAttachments;
|
||||||
if (
|
if (
|
||||||
!directCodexProductRuntime &&
|
!directCodexProductRuntime &&
|
||||||
@@ -12846,7 +12805,7 @@ export function App({
|
|||||||
if (
|
if (
|
||||||
!prompt &&
|
!prompt &&
|
||||||
references.length === 0 &&
|
references.length === 0 &&
|
||||||
content.length === 0 &&
|
chatContent.length === 0 &&
|
||||||
pendingAttachments.length === 0
|
pendingAttachments.length === 0
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -12859,10 +12818,12 @@ export function App({
|
|||||||
prompt,
|
prompt,
|
||||||
attachments: pendingAttachments,
|
attachments: pendingAttachments,
|
||||||
references,
|
references,
|
||||||
content,
|
content: chatContent,
|
||||||
});
|
});
|
||||||
if (enqueued) {
|
if (enqueued) {
|
||||||
clearChatComposer();
|
setChatInput('');
|
||||||
|
setChatContent([]);
|
||||||
|
setChatReferences([]);
|
||||||
setChatAttachments([]);
|
setChatAttachments([]);
|
||||||
setChatAttachmentNotice('');
|
setChatAttachmentNotice('');
|
||||||
}
|
}
|
||||||
@@ -12874,7 +12835,9 @@ export function App({
|
|||||||
if (!nextProjectPath) {
|
if (!nextProjectPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearChatComposer();
|
setChatInput('');
|
||||||
|
setChatReferences([]);
|
||||||
|
setChatContent([]);
|
||||||
void loadProjectConversation(nextProjectPath, false, 'replace');
|
void loadProjectConversation(nextProjectPath, false, 'replace');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -12884,7 +12847,7 @@ export function App({
|
|||||||
}
|
}
|
||||||
supervisorChatShouldFollowLatestRef.current = true;
|
supervisorChatShouldFollowLatestRef.current = true;
|
||||||
const clientTurnId = createAgentChatRunId('planning-v2-turn');
|
const clientTurnId = createAgentChatRunId('planning-v2-turn');
|
||||||
clearChatComposer();
|
setChatInput('');
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
{
|
{
|
||||||
@@ -12903,19 +12866,23 @@ export function App({
|
|||||||
}
|
}
|
||||||
if (directCodexProductRuntime) {
|
if (directCodexProductRuntime) {
|
||||||
// 待发附件随本轮提交一次性交给回合;提交后清空,避免同一批附件重复挂到下一轮。
|
// 待发附件随本轮提交一次性交给回合;提交后清空,避免同一批附件重复挂到下一轮。
|
||||||
clearChatComposer();
|
setChatInput('');
|
||||||
|
setChatReferences([]);
|
||||||
setChatAttachments([]);
|
setChatAttachments([]);
|
||||||
|
setChatContent([]);
|
||||||
setChatAttachmentNotice('');
|
setChatAttachmentNotice('');
|
||||||
setChatComposerNotice('');
|
setChatComposerNotice('');
|
||||||
startDirectCodexConversationTurn({
|
startDirectCodexConversationTurn({
|
||||||
prompt,
|
prompt,
|
||||||
attachments: pendingAttachments,
|
attachments: pendingAttachments,
|
||||||
references,
|
references,
|
||||||
content,
|
content: chatContent,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearChatComposer();
|
setChatInput('');
|
||||||
|
setChatReferences([]);
|
||||||
|
setChatContent([]);
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
{
|
{
|
||||||
@@ -12943,6 +12910,8 @@ export function App({
|
|||||||
<SupervisorChatOnlyView
|
<SupervisorChatOnlyView
|
||||||
activeVersionId={chatActiveVersionId}
|
activeVersionId={chatActiveVersionId}
|
||||||
chatAgentBusy={chatAgentBusy}
|
chatAgentBusy={chatAgentBusy}
|
||||||
|
chatInput={chatInput}
|
||||||
|
chatReferences={chatReferences}
|
||||||
composerRef={chatComposerRef}
|
composerRef={chatComposerRef}
|
||||||
chatProjectAssets={chatProjectAssets}
|
chatProjectAssets={chatProjectAssets}
|
||||||
messagesRef={supervisorChatMessagesRef}
|
messagesRef={supervisorChatMessagesRef}
|
||||||
@@ -12991,6 +12960,8 @@ export function App({
|
|||||||
activeVersionId={chatActiveVersionId}
|
activeVersionId={chatActiveVersionId}
|
||||||
attachments={chatAttachments}
|
attachments={chatAttachments}
|
||||||
attachmentNotice={chatAttachmentNotice}
|
attachmentNotice={chatAttachmentNotice}
|
||||||
|
chatInput={chatInput}
|
||||||
|
chatReferences={chatReferences}
|
||||||
composerNotice={chatComposerNotice}
|
composerNotice={chatComposerNotice}
|
||||||
onCancelQueuedTurn={cancelQueuedChatTurn}
|
onCancelQueuedTurn={cancelQueuedChatTurn}
|
||||||
onCancelTurn={() => void handleCancelDirectCodexTurn()}
|
onCancelTurn={() => void handleCancelDirectCodexTurn()}
|
||||||
@@ -13210,6 +13181,8 @@ export function App({
|
|||||||
}
|
}
|
||||||
cancelUiCommandConfirmation={cancelUiCommandConfirmation}
|
cancelUiCommandConfirmation={cancelUiCommandConfirmation}
|
||||||
chatAgentBusy={chatAgentBusy}
|
chatAgentBusy={chatAgentBusy}
|
||||||
|
chatInput={chatInput}
|
||||||
|
chatReferences={chatReferences}
|
||||||
composerRef={chatComposerRef}
|
composerRef={chatComposerRef}
|
||||||
chatProjectAssets={chatProjectAssets}
|
chatProjectAssets={chatProjectAssets}
|
||||||
chatInputRef={chatInputRef}
|
chatInputRef={chatInputRef}
|
||||||
|
|||||||
@@ -757,6 +757,8 @@ export type RuntimeAgentLlmProviderPresetId =
|
|||||||
| RuntimeLlmProviderPresetId;
|
| RuntimeLlmProviderPresetId;
|
||||||
|
|
||||||
export interface GameCreatorLlmConfig {
|
export interface GameCreatorLlmConfig {
|
||||||
|
customEnabled?: boolean;
|
||||||
|
visibleModels?: string[];
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
||||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
||||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
||||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
||||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
||||||
import {
|
|
||||||
COMMAND_PRIORITY_HIGH,
|
|
||||||
type EditorState,
|
|
||||||
KEY_ENTER_COMMAND,
|
|
||||||
type Klass,
|
|
||||||
type LexicalNode,
|
|
||||||
} from 'lexical';
|
|
||||||
import type { ReactElement, ReactNode, Ref } from 'react';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
type RichTextInputProps = {
|
|
||||||
namespace: string;
|
|
||||||
nodes: Klass<LexicalNode>[];
|
|
||||||
initialEditorState?: EditorState | null;
|
|
||||||
contentEditable?: ReactElement<typeof ContentEditable>;
|
|
||||||
placeholder?: ReactElement;
|
|
||||||
containerClassName?: string;
|
|
||||||
containerRef?: Ref<HTMLDivElement>;
|
|
||||||
disabled?: boolean;
|
|
||||||
onChange?: (editorState: EditorState) => void;
|
|
||||||
onEnter?: () => void;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
function SubmitOnEnter({ onEnter }: { onEnter?: () => void }) {
|
|
||||||
const [editor] = useLexicalComposerContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!onEnter) return undefined;
|
|
||||||
return editor.registerCommand(
|
|
||||||
KEY_ENTER_COMMAND,
|
|
||||||
(event) => {
|
|
||||||
if (!event || event.shiftKey || event.isComposing) return false;
|
|
||||||
event.preventDefault();
|
|
||||||
onEnter();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_HIGH,
|
|
||||||
);
|
|
||||||
}, [editor, onEnter]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetEditorEditable({ disabled }: { disabled: boolean }) {
|
|
||||||
const [editor] = useLexicalComposerContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
editor.setEditable(!disabled);
|
|
||||||
}, [disabled, editor]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RichTextInput({
|
|
||||||
namespace,
|
|
||||||
nodes,
|
|
||||||
initialEditorState,
|
|
||||||
contentEditable = <ContentEditable />,
|
|
||||||
placeholder,
|
|
||||||
containerClassName,
|
|
||||||
containerRef,
|
|
||||||
disabled = false,
|
|
||||||
onChange,
|
|
||||||
onEnter,
|
|
||||||
children,
|
|
||||||
}: RichTextInputProps) {
|
|
||||||
return (
|
|
||||||
<LexicalComposer
|
|
||||||
initialConfig={{
|
|
||||||
namespace,
|
|
||||||
nodes,
|
|
||||||
editorState: initialEditorState ?? undefined,
|
|
||||||
onError: (error) => {
|
|
||||||
throw error;
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className={containerClassName}
|
|
||||||
data-disabled={disabled ? 'true' : undefined}
|
|
||||||
>
|
|
||||||
<RichTextPlugin
|
|
||||||
contentEditable={contentEditable}
|
|
||||||
placeholder={placeholder}
|
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
<SetEditorEditable disabled={disabled} />
|
|
||||||
<SubmitOnEnter onEnter={onEnter} />
|
|
||||||
{onChange ? <OnChangePlugin onChange={onChange} /> : null}
|
|
||||||
</div>
|
|
||||||
</LexicalComposer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+29
-1
@@ -18,6 +18,8 @@ import { ClientAuthRequestError } from '../../services/clientApi';
|
|||||||
import { ClientHttpTimeoutError } from '../../services/clientHttp';
|
import { ClientHttpTimeoutError } from '../../services/clientHttp';
|
||||||
import {
|
import {
|
||||||
cachedLlmModelCatalog,
|
cachedLlmModelCatalog,
|
||||||
|
LLM_CONFIG_CHANGED_EVENT,
|
||||||
|
LlmModelCatalogConfigError,
|
||||||
refreshLlmModelCatalog,
|
refreshLlmModelCatalog,
|
||||||
} from '../../services/llmModelCatalog';
|
} from '../../services/llmModelCatalog';
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ export type ConversationModelSelectHandle = {
|
|||||||
class ModelSelectionConfigError extends Error {}
|
class ModelSelectionConfigError extends Error {}
|
||||||
|
|
||||||
function modelCatalogErrorMessage(error: unknown) {
|
function modelCatalogErrorMessage(error: unknown) {
|
||||||
|
if (error instanceof LlmModelCatalogConfigError) return error.message;
|
||||||
if (error instanceof ClientHttpTimeoutError)
|
if (error instanceof ClientHttpTimeoutError)
|
||||||
return '模型列表请求超时,请重试';
|
return '模型列表请求超时,请重试';
|
||||||
if (error instanceof ClientAuthRequestError && error.status)
|
if (error instanceof ClientAuthRequestError && error.status)
|
||||||
@@ -70,6 +73,7 @@ export function ConversationModelSelect({
|
|||||||
const selectedRef = useRef('');
|
const selectedRef = useRef('');
|
||||||
const selectionEpochRef = useRef(0);
|
const selectionEpochRef = useRef(0);
|
||||||
const busyTokenRef = useRef(0);
|
const busyTokenRef = useRef(0);
|
||||||
|
const syncEpochRef = useRef(0);
|
||||||
const configWriteChainRef = useRef<Promise<unknown>>(Promise.resolve());
|
const configWriteChainRef = useRef<Promise<unknown>>(Promise.resolve());
|
||||||
const onReadyRef = useRef(onReady);
|
const onReadyRef = useRef(onReady);
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
@@ -192,6 +196,7 @@ export function ConversationModelSelect({
|
|||||||
|
|
||||||
const syncCatalog = useCallback(
|
const syncCatalog = useCallback(
|
||||||
async (showBusy: boolean, manualRefresh = false) => {
|
async (showBusy: boolean, manualRefresh = false) => {
|
||||||
|
const syncEpoch = ++syncEpochRef.current;
|
||||||
if (manualRefresh && mountedRef.current) {
|
if (manualRefresh && mountedRef.current) {
|
||||||
setManualRefreshBusy(true);
|
setManualRefreshBusy(true);
|
||||||
setNotice('正在刷新模型列表');
|
setNotice('正在刷新模型列表');
|
||||||
@@ -214,10 +219,17 @@ export function ConversationModelSelect({
|
|||||||
try {
|
try {
|
||||||
catalog = await refreshLlmModelCatalog();
|
catalog = await refreshLlmModelCatalog();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (syncEpoch !== syncEpochRef.current)
|
||||||
|
return Boolean(selectedRef.current);
|
||||||
catalogError = error;
|
catalogError = error;
|
||||||
const cached = cachedLlmModelCatalog();
|
const cached = cachedLlmModelCatalog();
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
if (mountedRef.current) {
|
if (mountedRef.current) {
|
||||||
|
appliedRevisionRef.current = null;
|
||||||
|
selectedRef.current = '';
|
||||||
|
setModels([]);
|
||||||
|
setSelected('');
|
||||||
|
setDefaultModelId('');
|
||||||
setError(modelCatalogErrorMessage(error));
|
setError(modelCatalogErrorMessage(error));
|
||||||
setNotice('');
|
setNotice('');
|
||||||
}
|
}
|
||||||
@@ -227,6 +239,8 @@ export function ConversationModelSelect({
|
|||||||
catalog = cached;
|
catalog = cached;
|
||||||
usingCachedCatalog = true;
|
usingCachedCatalog = true;
|
||||||
}
|
}
|
||||||
|
if (syncEpoch !== syncEpochRef.current)
|
||||||
|
return Boolean(selectedRef.current);
|
||||||
const ready = await applyCatalog(catalog, showBusy, epochAtRequest);
|
const ready = await applyCatalog(catalog, showBusy, epochAtRequest);
|
||||||
if (usingCachedCatalog && mountedRef.current)
|
if (usingCachedCatalog && mountedRef.current)
|
||||||
setError(modelCatalogErrorMessage(catalogError));
|
setError(modelCatalogErrorMessage(catalogError));
|
||||||
@@ -235,6 +249,8 @@ export function ConversationModelSelect({
|
|||||||
}
|
}
|
||||||
return ready;
|
return ready;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (syncEpoch !== syncEpochRef.current)
|
||||||
|
return Boolean(selectedRef.current);
|
||||||
if (mountedRef.current) {
|
if (mountedRef.current) {
|
||||||
setNotice('');
|
setNotice('');
|
||||||
setError(
|
setError(
|
||||||
@@ -268,8 +284,20 @@ export function ConversationModelSelect({
|
|||||||
function handleWindowFocus() {
|
function handleWindowFocus() {
|
||||||
void syncCatalog(false);
|
void syncCatalog(false);
|
||||||
}
|
}
|
||||||
|
function handleConfigChanged() {
|
||||||
|
selectionEpochRef.current += 1;
|
||||||
|
appliedRevisionRef.current = null;
|
||||||
|
selectedRef.current = '';
|
||||||
|
setSelected('');
|
||||||
|
setModels([]);
|
||||||
|
void syncCatalog(true);
|
||||||
|
}
|
||||||
window.addEventListener('focus', handleWindowFocus);
|
window.addEventListener('focus', handleWindowFocus);
|
||||||
return () => window.removeEventListener('focus', handleWindowFocus);
|
window.addEventListener(LLM_CONFIG_CHANGED_EVENT, handleConfigChanged);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('focus', handleWindowFocus);
|
||||||
|
window.removeEventListener(LLM_CONFIG_CHANGED_EVENT, handleConfigChanged);
|
||||||
|
};
|
||||||
}, [syncCatalog]);
|
}, [syncCatalog]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ import {
|
|||||||
ResourceReferenceInput,
|
ResourceReferenceInput,
|
||||||
type ResourceReferenceInputHandle,
|
type ResourceReferenceInputHandle,
|
||||||
} from './ResourceReferenceInput';
|
} from './ResourceReferenceInput';
|
||||||
import type { ChatComposerDraft } from './resourceReferences';
|
import type { ChatComposerDraft, ChatReference } from './resourceReferences';
|
||||||
import { ToolCallGroup } from './ToolCallGroup';
|
import { ToolCallGroup } from './ToolCallGroup';
|
||||||
import {
|
import {
|
||||||
formatClockTime,
|
formatClockTime,
|
||||||
@@ -242,6 +242,8 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
|
|||||||
attachments?: DirectCodexTurnAttachment[];
|
attachments?: DirectCodexTurnAttachment[];
|
||||||
/** 上传/校验附件的提示文案(失败与成功都用它,空串不渲染)。 */
|
/** 上传/校验附件的提示文案(失败与成功都用它,空串不渲染)。 */
|
||||||
attachmentNotice?: string;
|
attachmentNotice?: string;
|
||||||
|
chatInput: string;
|
||||||
|
chatReferences: ChatReference[];
|
||||||
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
|
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
|
||||||
composerRef?: RefObject<ResourceReferenceInputHandle | null>;
|
composerRef?: RefObject<ResourceReferenceInputHandle | null>;
|
||||||
directCodex?: boolean;
|
directCodex?: boolean;
|
||||||
@@ -325,6 +327,8 @@ export function ProjectSupervisorView({
|
|||||||
activeVersionId = null,
|
activeVersionId = null,
|
||||||
attachments = [],
|
attachments = [],
|
||||||
attachmentNotice = '',
|
attachmentNotice = '',
|
||||||
|
chatInput,
|
||||||
|
chatReferences,
|
||||||
chatProjectAssets,
|
chatProjectAssets,
|
||||||
composerRef,
|
composerRef,
|
||||||
directCodex = false,
|
directCodex = false,
|
||||||
@@ -912,6 +916,8 @@ export function ProjectSupervisorView({
|
|||||||
Boolean(designView?.session.pendingClarification)
|
Boolean(designView?.session.pendingClarification)
|
||||||
}
|
}
|
||||||
rows={3}
|
rows={3}
|
||||||
|
value={chatInput}
|
||||||
|
references={chatReferences}
|
||||||
showTriggerButton={!directCodex}
|
showTriggerButton={!directCodex}
|
||||||
placeholder={
|
placeholder={
|
||||||
directCodex
|
directCodex
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user