Compare commits

..

9 Commits

Author SHA1 Message Date
kdletters c36a5170f8 AGC 新增本地自定义 LLM 配置与展示模型勾选
Project CI / AI game creator shell Rust smoke (push) Waiting to run
Project CI / AI game creator shell Rust crates (push) Waiting to run
Project CI / Backend tests (push) Waiting to run
Project CI / Native shell tests (push) Waiting to run
Project CI / Frontend tests (push) Waiting to run
Project CI / Repository checks (push) Waiting to run
Project CI / AI game creator shell web tests (push) Waiting to run
Project CI / AI game creator shell Rust shard 1/4 (push) Has started running
Project CI / AI game creator shell Rust shard 2/4 (push) Has started running
Project CI / AI game creator shell Rust shard 3/4 (push) Has started running
Project CI / AI game creator shell Rust shard 4/4 (push) Has started running
- 本地配置新增 llm.customEnabled 与 llm.visibleModels,显式开启后保留自定义连接和勾选列表,官方路由下仍清空凭据
- 配置迁移始终写出 customEnabled、visibleModels、apiKey、baseUrl、model、apiKind、reasoningEffort,便于手写自定义连接
- 新增 discover_game_creator_llm_models 命令,直连自定义端点 GET /models,限制超时与响应大小并脱敏错误
- codex_app_server 自定义模式改用配置里的地址与 Key 走本地凭据代理,不再经过平台 /api/llm,也不回退官方路由
- 模型目录按官方与自定义来源隔离缓存,自定义模式只展示勾选模型,选项失效时回退第一项
- 设置页在自定义模式展示 API 地址、API Key、协议与推理档,并提供模型读取、搜索、勾选与已勾选预览
- 补齐固定项排版与读取按钮样式,同步 AGC 模型选择规范、实施计划与共享概览
2026-09-16 16:39:15 +08:00
kdletters 9663bbf911 修复 AGC 客户端登录失败原因提示
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 6m7s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 6m13s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m32s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m38s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m38s
Project CI / AI game creator shell Rust crates (push) Successful in 2m29s
Project CI / Frontend tests (push) Successful in 4m42s
Project CI / Repository checks (push) Successful in 4m2s
Project CI / Native shell tests (push) Successful in 7m45s
Project CI / Backend tests (push) Successful in 8m39s
Project CI / AI game creator shell web tests (push) Successful in 3m0s
- clientAuth 错误响应解析兼容未带 envelope 的旧形态错误体,密码错误时显示接口返回原因而不是固定“登录失败”
- 非 JSON 与结构无法识别的响应继续使用本地化兜底,不回显内部英文和原始 JSON
- appSurface 认证用例补充 envelope 与旧形态两条回归测试
2026-09-16 16:05:59 +08:00
kdletters b3e9d0a906 发布 AGC 标准版 0.1.45
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m11s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m39s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m54s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m35s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m26s
Project CI / AI game creator shell Rust crates (push) Successful in 3m17s
Project CI / Frontend tests (push) Successful in 4m41s
Project CI / Repository checks (push) Successful in 3m53s
Project CI / Native shell tests (push) Successful in 6m43s
Project CI / Backend tests (push) Successful in 7m26s
Project CI / AI game creator shell web tests (push) Successful in 3m23s
- apps/ai-game-creator-shell/package.json 版本提升到 0.1.45
- apps/ai-game-creator-shell/src-tauri/tauri.conf.json 版本提升到 0.1.45
- apps/ai-game-creator-shell/src-tauri/Cargo.toml 版本提升到 0.1.45
- apps/ai-game-creator-shell/src-tauri/Cargo.lock 同步 genarrative-ai-game-creator-shell 版本
- package-lock.json 同步 apps/ai-game-creator-shell 工作区版本
2026-09-16 11:52:03 +08:00
kdletters a60328623d 调整开发调度管线备份策略
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m12s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m36s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m2s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m52s
Project CI / AI game creator shell Rust crates (push) Successful in 2m7s
Project CI / Frontend tests (push) Successful in 4m45s
Project CI / Repository checks (push) Successful in 4m42s
Project CI / Backend tests (push) Successful in 6m58s
Project CI / Native shell tests (push) Successful in 8m22s
Project CI / AI game creator shell web tests (push) Successful in 4m5s
恢复 Full Job 备份参数原样透传
开发调度统一传入 skip,默认不上传 OSS
保持 Full 与 AGC 使用同一固定 revision 参数
2026-09-16 11:43:22 +08:00
kdletters 9e63b76991 修正开发环境备份默认映射
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m7s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m56s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m37s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m45s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m18s
Project CI / AI game creator shell Rust crates (push) Successful in 2m48s
Project CI / Repository checks (push) Failing after 1m54s
Project CI / Frontend tests (push) Successful in 5m15s
Project CI / Backend tests (push) Failing after 7m46s
Project CI / AI game creator shell web tests (push) Failing after 4m21s
Project CI / Native shell tests (push) Successful in 8m53s
保留备份参数原有枚举顺序以通过运维门禁
开发目标未显式选择时将 async 映射为 skip
开发环境默认不上传 OSS,release 仍保留显式策略
2026-09-16 11:40:22 +08:00
kdletters a98ebcf68f 调整开发环境默认备份策略
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m35s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m55s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m0s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m4s
Project CI / AI game creator shell Rust crates (push) Successful in 2m33s
Project CI / Repository checks (push) Failing after 1m16s
Project CI / Frontend tests (push) Successful in 4m59s
Project CI / AI game creator shell web tests (push) Successful in 3m28s
Project CI / Native shell tests (push) Successful in 7m12s
Project CI / Backend tests (push) Successful in 8m33s
开发环境默认跳过 OSS 备份上传
需要冷备时显式选择 async
保持 release 发布的备份策略可配置
2026-09-16 11:37:56 +08:00
kdletters 576ff07a5e 新增定时版本调度管线并收回两条下游管线的定时与去重
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m9s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 5m11s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m34s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m1s
Project CI / AI game creator shell Rust crates (push) Successful in 2m36s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m20s
Project CI / Frontend tests (push) Successful in 4m23s
Project CI / Repository checks (push) Successful in 4m13s
Project CI / Native shell tests (push) Successful in 6m44s
Project CI / Backend tests (push) Successful in 8m22s
Project CI / AI game creator shell web tests (push) Successful in 3m28s
- 新增 Genarrative-Scheduled-Revision-Trigger:每小时用 git ls-remote 解析分支版本,与上一次触发过的 revision 不同才继续
- 触发时把同一个固定 COMMIT_HASH 同时传给 Full Build 与 AGC Windows Build,保证两条管线构建同一个版本
- Full Build 与 AGC Windows Build 去掉自带定时触发器与管线内版本去重
- 新增 scheduled-revision-trigger Job 配置 XML,使用本机 Git 入口与既有 SSH 凭据,定时器留在 Jenkinsfile
- 生产运维门禁改为校验调度管线约束,并拦住两条下游管线重新加 cron 或自行去重
- 同步开发运维文档、共享开发工作流与踩坑记录
2026-09-16 10:55:16 +08:00
kdletters 5aa616134c 修复 Hook 链继承仓库定位变量导致真实仓库被夹具污染
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m30s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m47s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 5m53s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m8s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m48s
Project CI / AI game creator shell Rust crates (push) Successful in 2m46s
Project CI / Frontend tests (push) Failing after 4m6s
Project CI / Repository checks (push) Successful in 3m57s
Project CI / Native shell tests (push) Successful in 7m40s
Project CI / Backend tests (push) Successful in 8m27s
Project CI / AI game creator shell web tests (push) Successful in 12m53s
- .husky/pre-commit 与 .husky/pre-push 入口清除 GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE 等仓库定位变量

- scripts/check-repository-ci.sh 增加同样的清理,保证本地门禁与 CI 走同一套隔离

- scripts/git-hooks.test.mjs 夹具 Git 调用前自检仓库归属并禁用 Hook,命令落到外部仓库时直接失败

- 守卫用例的子进程必须真的继承 GIT_DIR,避免隔离断言空转

- 更新开发运维文档与 shared-memory/pitfalls.md,记录链接工作树注入形态与配置修复步骤
2026-09-16 10:35:07 +08:00
kdletters 187b66c3b1 Jenkins 定时构建增加版本未变化去重
- Full Build 改为每小时触发,并按已解析 commit 与全部构建参数判断是否重复
- AGC Windows Build 增加每小时触发与同一套去重逻辑,跳过时不再安装依赖、构建和上传
- 跳过记录为 NOT_BUILT,越过跳过记录后最近一次实际构建成功才允许跳过,失败或中断可重试
- 生产运维门禁补充两个 Job 的去重约束与各阶段跳过检查
- 同步开发运维文档与共享开发工作流说明
2026-09-16 10:32:03 +08:00
68 changed files with 2158 additions and 715 deletions
+3
View File
@@ -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
+3
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
@@ -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",
+60 -87
View File
@@ -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>
);
}
@@ -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