Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b48293fb1f | |||
| e08171fd2e | |||
| 479120d368 | |||
| 4182979a19 | |||
| 262deaf9b7 | |||
| 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,21 +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),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,18 +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) => {
|
|
||||||
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,20 +100,6 @@ pub(crate) fn direct_codex_user_item_to_wire_input(
|
|||||||
summary.push(']');
|
summary.push(']');
|
||||||
summary
|
summary
|
||||||
}
|
}
|
||||||
DirectCodexUserContentPart::AgcAttachmentReference(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 }));
|
||||||
}
|
}
|
||||||
@@ -188,26 +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_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_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(), 2);
|
|
||||||
assert!(content[0]["text"].as_str().unwrap().contains("先看"));
|
|
||||||
assert!(content[1]["text"].as_str().unwrap().contains("notes.txt"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3053,14 +3053,7 @@ async fn recover_direct_taonier_spritesheet_read_only_at(
|
|||||||
)?;
|
)?;
|
||||||
let _platform_session_lease = access
|
let _platform_session_lease = access
|
||||||
.frozen_platform_session()
|
.frozen_platform_session()
|
||||||
.map(|session| {
|
.map(|session| acquire_platform_session_identity_lease(&session.identity()))
|
||||||
acquire_validated_platform_session_fingerprint(
|
|
||||||
&session.user_id,
|
|
||||||
&session.api_base_url,
|
|
||||||
session.generation,
|
|
||||||
&format!("{:x}", Sha256::digest(session.access_token.as_bytes())),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
// The network phase deliberately runs without the project write lock. Capture rollback state
|
// The network phase deliberately runs without the project write lock. Capture rollback state
|
||||||
// only after acquiring the lock and revalidating the source identity, otherwise a failure can
|
// only after acquiring the lock and revalidating the source identity, otherwise a failure can
|
||||||
@@ -5329,6 +5322,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
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ struct ExternalMcpHttpState {
|
|||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
token: String,
|
token: String,
|
||||||
session_user_id: String,
|
session_user_id: String,
|
||||||
session_generation: u64,
|
session_identity_generation: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn direct_tools_mcp_mode_requested(args: &[String]) -> bool {
|
pub(crate) fn direct_tools_mcp_mode_requested(args: &[String]) -> bool {
|
||||||
@@ -1241,7 +1241,8 @@ fn external_mcp_session_id(root: &Path) -> String {
|
|||||||
material.push('\0');
|
material.push('\0');
|
||||||
material.push_str(&session.user_id);
|
material.push_str(&session.user_id);
|
||||||
material.push('\0');
|
material.push('\0');
|
||||||
material.push_str(&session.generation.to_string());
|
// 用身份代次而不是 token:同一账号续期不得让 MCP 会话身份漂移。
|
||||||
|
material.push_str(&session.identity_generation.to_string());
|
||||||
}
|
}
|
||||||
format!("mcp-{:x}", Sha256::digest(material.as_bytes()))
|
format!("mcp-{:x}", Sha256::digest(material.as_bytes()))
|
||||||
}
|
}
|
||||||
@@ -1759,7 +1760,9 @@ async fn handle_external_mcp_http_request(
|
|||||||
let Some(session) = current_platform_session() else {
|
let Some(session) = current_platform_session() else {
|
||||||
return Err(StatusCode::UNAUTHORIZED);
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
};
|
};
|
||||||
if session.user_id != state.session_user_id || session.generation != state.session_generation {
|
if session.user_id != state.session_user_id
|
||||||
|
|| session.identity_generation != state.session_identity_generation
|
||||||
|
{
|
||||||
return Err(StatusCode::UNAUTHORIZED);
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
let response = EXTERNAL_MCP_BRIDGE_URL
|
let response = EXTERNAL_MCP_BRIDGE_URL
|
||||||
@@ -1794,7 +1797,7 @@ pub(crate) async fn start_external_mcp_loopback(
|
|||||||
root,
|
root,
|
||||||
token: token.clone(),
|
token: token.clone(),
|
||||||
session_user_id: session.user_id,
|
session_user_id: session.user_id,
|
||||||
session_generation: session.generation,
|
session_identity_generation: session.identity_generation,
|
||||||
};
|
};
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(&route, post(handle_external_mcp_http_request))
|
.route(&route, post(handle_external_mcp_http_request))
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ use super::external_generation_state::{
|
|||||||
retain_platform_art_generation_runtime_accepted_result, PlatformArtGenerationRuntimeState,
|
retain_platform_art_generation_runtime_accepted_result, PlatformArtGenerationRuntimeState,
|
||||||
};
|
};
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::platform_session::{
|
||||||
|
acquire_platform_session_identity_lease, validate_platform_session_identity,
|
||||||
|
PlatformSessionIdentity,
|
||||||
|
};
|
||||||
use reqwest::multipart::{Form, Part};
|
use reqwest::multipart::{Form, Part};
|
||||||
|
|
||||||
const EXTERNAL_GENERATION_POLL_TIMEOUT: Duration = Duration::from_secs(35 * 60);
|
const EXTERNAL_GENERATION_POLL_TIMEOUT: Duration = Duration::from_secs(35 * 60);
|
||||||
@@ -1510,10 +1514,7 @@ struct PreparedPlatformArtAssetSlice {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct PreparedPlatformSessionFence {
|
struct PreparedPlatformSessionFence {
|
||||||
user_id: String,
|
identity: PlatformSessionIdentity,
|
||||||
api_base_url: String,
|
|
||||||
generation: u64,
|
|
||||||
access_token_sha256: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreparedPlatformSessionFence {
|
impl PreparedPlatformSessionFence {
|
||||||
@@ -1521,41 +1522,17 @@ impl PreparedPlatformSessionFence {
|
|||||||
access
|
access
|
||||||
.frozen_platform_session()
|
.frozen_platform_session()
|
||||||
.map(|session| PreparedPlatformSessionFence {
|
.map(|session| PreparedPlatformSessionFence {
|
||||||
user_id: session.user_id.clone(),
|
identity: session.identity(),
|
||||||
api_base_url: session.api_base_url.clone(),
|
|
||||||
generation: session.generation,
|
|
||||||
access_token_sha256: format!(
|
|
||||||
"{:x}",
|
|
||||||
Sha256::digest(session.access_token.as_bytes())
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self) -> Result<(), String> {
|
fn validate(&self) -> Result<(), String> {
|
||||||
let matches = current_platform_session().is_some_and(|session| {
|
// 只比较身份:同一账号的 access token 轮换不得让在途生成 operation 失败。
|
||||||
session.user_id == self.user_id
|
validate_platform_session_identity(&self.identity)
|
||||||
&& session.api_base_url == self.api_base_url
|
|
||||||
&& session.generation == self.generation
|
|
||||||
&& format!("{:x}", Sha256::digest(session.access_token.as_bytes()))
|
|
||||||
== self.access_token_sha256
|
|
||||||
});
|
|
||||||
if matches {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(
|
|
||||||
"authentication-required: 陶泥儿登录态已变化,旧账号请求已停止,请使用当前账号重试"
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn acquire_lease(&self) -> Result<ValidatedPlatformSessionLease, String> {
|
fn acquire_lease(&self) -> Result<ValidatedPlatformSessionLease, String> {
|
||||||
acquire_validated_platform_session_fingerprint(
|
acquire_platform_session_identity_lease(&self.identity)
|
||||||
&self.user_id,
|
|
||||||
&self.api_base_url,
|
|
||||||
self.generation,
|
|
||||||
&self.access_token_sha256,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10636,7 +10613,7 @@ mod canvas_generation_tests {
|
|||||||
}
|
}
|
||||||
drop(owner_a_access);
|
drop(owner_a_access);
|
||||||
drop(frozen_owner_a);
|
drop(frozen_owner_a);
|
||||||
install_platform_session("durable-owner-b", "durable-owner-b-token", &base_url, 2)
|
install_platform_session("durable-owner-b", "durable-owner-b-token", &base_url, 2, 2)
|
||||||
.expect("switch to owner B");
|
.expect("switch to owner B");
|
||||||
|
|
||||||
let error = match request_platform_art_asset_with_runtime_options_at(
|
let error = match request_platform_art_asset_with_runtime_options_at(
|
||||||
@@ -10761,8 +10738,14 @@ mod canvas_generation_tests {
|
|||||||
.recv_timeout(Duration::from_secs(3))
|
.recv_timeout(Duration::from_secs(3))
|
||||||
.expect("wait for accepted response");
|
.expect("wait for accepted response");
|
||||||
std::thread::sleep(Duration::from_millis(50));
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
install_platform_session("post-202-user-b", "post-202-token-b", &switch_base_url, 2)
|
install_platform_session(
|
||||||
.expect("switch platform account after accepted response");
|
"post-202-user-b",
|
||||||
|
"post-202-token-b",
|
||||||
|
&switch_base_url,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
.expect("switch platform account after accepted response");
|
||||||
});
|
});
|
||||||
let runtime_context = PlatformArtGenerationRuntimeContext {
|
let runtime_context = PlatformArtGenerationRuntimeContext {
|
||||||
agent_id: "art-director".to_string(),
|
agent_id: "art-director".to_string(),
|
||||||
|
|||||||
+2
-1
@@ -1431,12 +1431,13 @@ mod external_generation_state_tests {
|
|||||||
base_url,
|
base_url,
|
||||||
);
|
);
|
||||||
let frozen_a = current_platform_session().expect("freeze owner A");
|
let frozen_a = current_platform_session().expect("freeze owner A");
|
||||||
validate_platform_session_snapshot(&frozen_a).expect("owner A is current before switch");
|
validate_frozen_platform_session(&frozen_a).expect("owner A is current before switch");
|
||||||
replace_platform_session_for_gui_owner(
|
replace_platform_session_for_gui_owner(
|
||||||
"fingerprint-owner-b",
|
"fingerprint-owner-b",
|
||||||
"fingerprint-token-b",
|
"fingerprint-token-b",
|
||||||
base_url,
|
base_url,
|
||||||
2,
|
2,
|
||||||
|
2,
|
||||||
)
|
)
|
||||||
.expect("switch global session to owner B");
|
.expect("switch global session to owner B");
|
||||||
let current_b = current_platform_session().expect("owner B is current after switch");
|
let current_b = current_platform_session().expect("owner B is current after switch");
|
||||||
|
|||||||
+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(),
|
||||||
|
|||||||
@@ -1939,8 +1939,9 @@ pub(crate) async fn polish_local_project_prompt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub(crate) fn read_platform_account_session_generation() -> u64 {
|
pub(crate) fn read_platform_account_session_state(
|
||||||
current_platform_session_generation()
|
) -> crate::platform_session::PlatformSessionWriteState {
|
||||||
|
crate::platform_session::current_platform_session_write_state()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1948,28 +1949,45 @@ pub(crate) async fn install_platform_account_session(
|
|||||||
user_id: String,
|
user_id: String,
|
||||||
access_token: String,
|
access_token: String,
|
||||||
api_base_url: String,
|
api_base_url: String,
|
||||||
generation: u64,
|
identity_generation: u64,
|
||||||
|
revision: u64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
validate_platform_session_input(&user_id, &access_token, &api_base_url, generation)?;
|
validate_platform_session_input(
|
||||||
|
&user_id,
|
||||||
|
&access_token,
|
||||||
|
&api_base_url,
|
||||||
|
identity_generation,
|
||||||
|
revision,
|
||||||
|
)?;
|
||||||
install_external_agent_runner_platform_session(
|
install_external_agent_runner_platform_session(
|
||||||
&user_id,
|
&user_id,
|
||||||
&access_token,
|
&access_token,
|
||||||
&api_base_url,
|
&api_base_url,
|
||||||
generation,
|
identity_generation,
|
||||||
|
revision,
|
||||||
)?;
|
)?;
|
||||||
install_platform_session(&user_id, &access_token, &api_base_url, generation)
|
install_platform_session(
|
||||||
|
&user_id,
|
||||||
|
&access_token,
|
||||||
|
&api_base_url,
|
||||||
|
identity_generation,
|
||||||
|
revision,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|error| format!("安装本地运行时会话任务意外终止:{error}"))?
|
.map_err(|error| format!("安装本地运行时会话任务意外终止:{error}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub(crate) async fn clear_platform_account_session(generation: u64) -> Result<(), String> {
|
pub(crate) async fn clear_platform_account_session(
|
||||||
|
identity_generation: u64,
|
||||||
|
revision: u64,
|
||||||
|
) -> Result<(), String> {
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
shutdown_game_creator_codex_app_servers()?;
|
shutdown_game_creator_codex_app_servers()?;
|
||||||
clear_external_agent_runner_platform_session(generation)?;
|
clear_external_agent_runner_platform_session(identity_generation, revision)?;
|
||||||
clear_platform_session(generation);
|
clear_platform_session(identity_generation, revision);
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -1991,6 +2009,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 +2047,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 +2060,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 +2090,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) {
|
||||||
// 仅同步已有覆盖项;其它字段继续保留原有覆盖语义。
|
// 仅同步已有覆盖项;其它字段继续保留原有覆盖语义。
|
||||||
@@ -4231,14 +4265,7 @@ pub(crate) async fn import_account_editor_assets_for_agent(
|
|||||||
access.validate_frozen_session()?;
|
access.validate_frozen_session()?;
|
||||||
let _platform_session_lease = frozen_session
|
let _platform_session_lease = frozen_session
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|session| {
|
.map(|session| acquire_platform_session_identity_lease(&session.identity()))
|
||||||
acquire_validated_platform_session_fingerprint(
|
|
||||||
&session.user_id,
|
|
||||||
&session.api_base_url,
|
|
||||||
session.generation,
|
|
||||||
&format!("{:x}", Sha256::digest(session.access_token.as_bytes())),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let _lock = acquire_project_write_lock(root, "canvas.asset_import")?;
|
let _lock = acquire_project_write_lock(root, "canvas.asset_import")?;
|
||||||
access.validate_frozen_session()?;
|
access.validate_frozen_session()?;
|
||||||
|
|||||||
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(),
|
||||||
@@ -2715,12 +2725,13 @@ fn main() {
|
|||||||
confirm_resume_game_creator_agent_runtime_tasks,
|
confirm_resume_game_creator_agent_runtime_tasks,
|
||||||
schedule_game_creator_agent_ready_tasks,
|
schedule_game_creator_agent_ready_tasks,
|
||||||
check_game_creator_llm_config,
|
check_game_creator_llm_config,
|
||||||
read_platform_account_session_generation,
|
read_platform_account_session_state,
|
||||||
install_platform_account_session,
|
install_platform_account_session,
|
||||||
clear_platform_account_session,
|
clear_platform_account_session,
|
||||||
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,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -110,11 +110,13 @@ impl<'a> ExternalEditorBindingAccess<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Call before and after every awaited remote action and immediately before installing a
|
/// Call before and after every awaited remote action and immediately before installing a
|
||||||
/// binding. Developer-key mode has no process-global account generation to compare.
|
/// binding. 只比较身份:同一账号的 access token 轮换(长回合保活、401 续期)不得让
|
||||||
|
/// 在途的生成、编辑、上传、确认或下载 operation 失效;换号、退出或 origin 变化仍然
|
||||||
|
/// 失败关闭。Developer-key 模式没有进程级身份代次可比对。
|
||||||
pub(crate) fn validate_frozen_session(&self) -> Result<(), String> {
|
pub(crate) fn validate_frozen_session(&self) -> Result<(), String> {
|
||||||
validate_external_editor_binding_access_shape(self)?;
|
validate_external_editor_binding_access_shape(self)?;
|
||||||
if let Some(session) = self.frozen_platform_session {
|
if let Some(session) = self.frozen_platform_session {
|
||||||
validate_platform_session_snapshot(session)?;
|
validate_frozen_platform_session(session)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1152,7 +1154,8 @@ mod tests {
|
|||||||
user_id: user_id.to_string(),
|
user_id: user_id.to_string(),
|
||||||
access_token: token.to_string(),
|
access_token: token.to_string(),
|
||||||
api_base_url: "https://dev.genarrative.world".to_string(),
|
api_base_url: "https://dev.genarrative.world".to_string(),
|
||||||
generation,
|
identity_generation: generation,
|
||||||
|
revision: generation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4389,14 +4389,7 @@ fn with_frozen_resource_edit_platform_session<T>(
|
|||||||
let Some(platform_session) = platform_session else {
|
let Some(platform_session) = platform_session else {
|
||||||
return action();
|
return action();
|
||||||
};
|
};
|
||||||
let access_token_sha256 = sha256_hex(platform_session.access_token.as_bytes());
|
with_validated_platform_session_identity(&platform_session.identity(), action)
|
||||||
with_validated_platform_session_fingerprint(
|
|
||||||
&platform_session.user_id,
|
|
||||||
&platform_session.api_base_url,
|
|
||||||
platform_session.generation,
|
|
||||||
&access_token_sha256,
|
|
||||||
action,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit_resource_edit_asset_with_frozen_platform_session(
|
fn commit_resource_edit_asset_with_frozen_platform_session(
|
||||||
@@ -4774,14 +4767,7 @@ pub(crate) fn list_pending_local_project_resource_edits_at(
|
|||||||
let current_platform_session = current_platform_session();
|
let current_platform_session = current_platform_session();
|
||||||
let _platform_session_lease = current_platform_session
|
let _platform_session_lease = current_platform_session
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|session| {
|
.map(|session| acquire_platform_session_identity_lease(&session.identity()))
|
||||||
acquire_validated_platform_session_fingerprint(
|
|
||||||
&session.user_id,
|
|
||||||
&session.api_base_url,
|
|
||||||
session.generation,
|
|
||||||
&sha256_hex(session.access_token.as_bytes()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let directory = resolve_local_project_path(root, &format!("{RESOURCE_EDIT_ROOT}/operations"))?;
|
let directory = resolve_local_project_path(root, &format!("{RESOURCE_EDIT_ROOT}/operations"))?;
|
||||||
let entries = match fs::read_dir(&directory) {
|
let entries = match fs::read_dir(&directory) {
|
||||||
@@ -5084,12 +5070,8 @@ pub(crate) async fn archive_failed_local_project_resource_edit_at(
|
|||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
if let Some(session) = platform_session {
|
if let Some(session) = platform_session {
|
||||||
let access_token_sha256 = sha256_hex(session.access_token.as_bytes());
|
crate::platform_session::with_validated_platform_session_identity(
|
||||||
crate::platform_session::with_validated_platform_session_fingerprint(
|
&session.identity(),
|
||||||
&session.user_id,
|
|
||||||
&session.api_base_url,
|
|
||||||
session.generation,
|
|
||||||
&access_token_sha256,
|
|
||||||
archive,
|
archive,
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
@@ -5500,7 +5482,8 @@ mod tests {
|
|||||||
user_id: "gui-owner".to_string(),
|
user_id: "gui-owner".to_string(),
|
||||||
access_token: "gui-token".to_string(),
|
access_token: "gui-token".to_string(),
|
||||||
api_base_url: "https://dev.genarrative.world".to_string(),
|
api_base_url: "https://dev.genarrative.world".to_string(),
|
||||||
generation: 7,
|
identity_generation: 7,
|
||||||
|
revision: 7,
|
||||||
};
|
};
|
||||||
let developer_credentials = (
|
let developer_credentials = (
|
||||||
"https://dev.genarrative.world".to_string(),
|
"https://dev.genarrative.world".to_string(),
|
||||||
@@ -5911,6 +5894,7 @@ mod tests {
|
|||||||
"source-binding-token-b",
|
"source-binding-token-b",
|
||||||
api_base_url,
|
api_base_url,
|
||||||
*generation,
|
*generation,
|
||||||
|
*generation,
|
||||||
)
|
)
|
||||||
.expect("switch account after source registration");
|
.expect("switch account after source registration");
|
||||||
}
|
}
|
||||||
@@ -6925,7 +6909,7 @@ mod tests {
|
|||||||
listener,
|
listener,
|
||||||
upload_url,
|
upload_url,
|
||||||
false,
|
false,
|
||||||
Some((base_url.clone(), frozen_session.generation + 1)),
|
Some((base_url.clone(), frozen_session.identity_generation + 1)),
|
||||||
done_receiver,
|
done_receiver,
|
||||||
);
|
);
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
@@ -6952,7 +6936,8 @@ mod tests {
|
|||||||
"source-binding-owner-a",
|
"source-binding-owner-a",
|
||||||
"source-binding-token-a",
|
"source-binding-token-a",
|
||||||
&base_url,
|
&base_url,
|
||||||
frozen_session.generation + 2,
|
frozen_session.identity_generation + 2,
|
||||||
|
frozen_session.identity_generation + 2,
|
||||||
)
|
)
|
||||||
.expect("switch back to source binding owner A");
|
.expect("switch back to source binding owner A");
|
||||||
let resumed_session = current_platform_session().expect("resumed source binding owner A");
|
let resumed_session = current_platform_session().expect("resumed source binding owner A");
|
||||||
@@ -7018,7 +7003,7 @@ mod tests {
|
|||||||
install_test_platform_session("submission-owner-a", "submission-token-a", &base_url);
|
install_test_platform_session("submission-owner-a", "submission-token-a", &base_url);
|
||||||
let frozen_session = current_platform_session().expect("frozen owner A session");
|
let frozen_session = current_platform_session().expect("frozen owner A session");
|
||||||
let switch_base_url = base_url.clone();
|
let switch_base_url = base_url.clone();
|
||||||
let switch_generation = frozen_session.generation + 1;
|
let switch_generation = frozen_session.identity_generation + 1;
|
||||||
let server = std::thread::spawn(move || {
|
let server = std::thread::spawn(move || {
|
||||||
let mut stream =
|
let mut stream =
|
||||||
accept_resource_editor_fixture_connection(&listener, "accepted switch fixture", 0);
|
accept_resource_editor_fixture_connection(&listener, "accepted switch fixture", 0);
|
||||||
@@ -7032,6 +7017,7 @@ mod tests {
|
|||||||
"submission-token-b",
|
"submission-token-b",
|
||||||
&switch_base_url,
|
&switch_base_url,
|
||||||
switch_generation,
|
switch_generation,
|
||||||
|
switch_generation,
|
||||||
)
|
)
|
||||||
.expect("switch to owner B before returning accepted response");
|
.expect("switch to owner B before returning accepted response");
|
||||||
write_json(
|
write_json(
|
||||||
@@ -7447,8 +7433,14 @@ mod tests {
|
|||||||
ledger.access_scheme = None;
|
ledger.access_scheme = None;
|
||||||
initialize_resource_edit_access_identity(root, &mut ledger, base_url, Some(&frozen_a))
|
initialize_resource_edit_access_identity(root, &mut ledger, base_url, Some(&frozen_a))
|
||||||
.expect("write resource ledger for owner A");
|
.expect("write resource ledger for owner A");
|
||||||
replace_platform_session_for_gui_owner("resource-owner-b", "resource-token-b", base_url, 2)
|
replace_platform_session_for_gui_owner(
|
||||||
.expect("switch global resource session to owner B");
|
"resource-owner-b",
|
||||||
|
"resource-token-b",
|
||||||
|
base_url,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
.expect("switch global resource session to owner B");
|
||||||
|
|
||||||
let error = prepare_resource_edit_service_identity(
|
let error = prepare_resource_edit_service_identity(
|
||||||
root,
|
root,
|
||||||
@@ -7509,7 +7501,8 @@ mod tests {
|
|||||||
user_id: "resource-identity-owner-b".to_string(),
|
user_id: "resource-identity-owner-b".to_string(),
|
||||||
access_token: "resource-identity-token-b".to_string(),
|
access_token: "resource-identity-token-b".to_string(),
|
||||||
api_base_url: owner_a.api_base_url.clone(),
|
api_base_url: owner_a.api_base_url.clone(),
|
||||||
generation: owner_a.generation + 1,
|
identity_generation: owner_a.identity_generation + 1,
|
||||||
|
revision: owner_a.revision + 1,
|
||||||
};
|
};
|
||||||
let mut ledger = ledger_for(&request, &source, ResourceEditLedgerPhase::Prepared);
|
let mut ledger = ledger_for(&request, &source, ResourceEditLedgerPhase::Prepared);
|
||||||
ledger.access_scheme = Some(RESOURCE_EDIT_PLATFORM_ACCESS_SCHEME.to_string());
|
ledger.access_scheme = Some(RESOURCE_EDIT_PLATFORM_ACCESS_SCHEME.to_string());
|
||||||
@@ -7535,7 +7528,8 @@ mod tests {
|
|||||||
&owner_b.user_id,
|
&owner_b.user_id,
|
||||||
&owner_b.access_token,
|
&owner_b.access_token,
|
||||||
&owner_b.api_base_url,
|
&owner_b.api_base_url,
|
||||||
owner_b.generation,
|
owner_b.identity_generation,
|
||||||
|
owner_b.revision,
|
||||||
)
|
)
|
||||||
.expect("switch to resource non-owner B");
|
.expect("switch to resource non-owner B");
|
||||||
|
|
||||||
@@ -7639,7 +7633,8 @@ mod tests {
|
|||||||
"resource-lease-owner-b",
|
"resource-lease-owner-b",
|
||||||
"resource-lease-token-b",
|
"resource-lease-token-b",
|
||||||
api_base_url,
|
api_base_url,
|
||||||
frozen_a.generation + 1,
|
frozen_a.identity_generation + 1,
|
||||||
|
frozen_a.revision + 1,
|
||||||
)
|
)
|
||||||
.expect("switch resource lease owner");
|
.expect("switch resource lease owner");
|
||||||
switched_sender.send(()).expect("signal resource switch");
|
switched_sender.send(()).expect("signal resource switch");
|
||||||
@@ -8259,7 +8254,8 @@ mod tests {
|
|||||||
"archive-owner-b",
|
"archive-owner-b",
|
||||||
"archive-token-b",
|
"archive-token-b",
|
||||||
api_base_url,
|
api_base_url,
|
||||||
owner_a.generation + 1,
|
owner_a.identity_generation + 1,
|
||||||
|
owner_a.revision + 1,
|
||||||
)
|
)
|
||||||
.expect("switch to owner B");
|
.expect("switch to owner B");
|
||||||
let error = archive_failed_local_project_resource_edit_at(
|
let error = archive_failed_local_project_resource_edit_at(
|
||||||
@@ -8388,7 +8384,8 @@ mod tests {
|
|||||||
"pending-owner-b",
|
"pending-owner-b",
|
||||||
"pending-token-b",
|
"pending-token-b",
|
||||||
api_base_url,
|
api_base_url,
|
||||||
owner_a.generation + 1,
|
owner_a.identity_generation + 1,
|
||||||
|
owner_a.revision + 1,
|
||||||
)
|
)
|
||||||
.expect("switch to pending owner B");
|
.expect("switch to pending owner B");
|
||||||
let owner_b = current_platform_session().expect("pending owner B session");
|
let owner_b = current_platform_session().expect("pending owner B session");
|
||||||
@@ -9347,7 +9344,7 @@ mod tests {
|
|||||||
let (attempted_sender, attempted_receiver) = mpsc::channel();
|
let (attempted_sender, attempted_receiver) = mpsc::channel();
|
||||||
let (completed_sender, completed_receiver) = mpsc::channel();
|
let (completed_sender, completed_receiver) = mpsc::channel();
|
||||||
let switch_api_base_url = api_base_url.to_string();
|
let switch_api_base_url = api_base_url.to_string();
|
||||||
let switch_generation = frozen_session.generation + 1;
|
let switch_generation = frozen_session.identity_generation + 1;
|
||||||
let switch_thread = std::thread::spawn(move || {
|
let switch_thread = std::thread::spawn(move || {
|
||||||
begin_switch_receiver
|
begin_switch_receiver
|
||||||
.recv()
|
.recv()
|
||||||
@@ -9360,6 +9357,7 @@ mod tests {
|
|||||||
"commit-token-b",
|
"commit-token-b",
|
||||||
&switch_api_base_url,
|
&switch_api_base_url,
|
||||||
switch_generation,
|
switch_generation,
|
||||||
|
switch_generation,
|
||||||
)
|
)
|
||||||
.expect("switch to commit owner B");
|
.expect("switch to commit owner B");
|
||||||
completed_sender
|
completed_sender
|
||||||
|
|||||||
@@ -987,7 +987,10 @@ pub(crate) fn attach_external_agent_runner_gui_owner(
|
|||||||
platform_api_base_url: platform_session
|
platform_api_base_url: platform_session
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|session| session.api_base_url.clone()),
|
.map(|session| session.api_base_url.clone()),
|
||||||
platform_auth_generation: platform_session.map(|session| session.generation),
|
platform_auth_generation: platform_session
|
||||||
|
.as_ref()
|
||||||
|
.map(|session| session.identity_generation),
|
||||||
|
platform_auth_revision: platform_session.map(|session| session.revision),
|
||||||
..ExternalAgentRunnerRequestParams::default()
|
..ExternalAgentRunnerRequestParams::default()
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
@@ -998,7 +1001,8 @@ pub(crate) fn install_external_agent_runner_platform_session(
|
|||||||
user_id: &str,
|
user_id: &str,
|
||||||
access_token: &str,
|
access_token: &str,
|
||||||
api_base_url: &str,
|
api_base_url: &str,
|
||||||
generation: u64,
|
identity_generation: u64,
|
||||||
|
revision: u64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let config_dir = external_agent_runner_config_dir()
|
let config_dir = external_agent_runner_config_dir()
|
||||||
.ok_or_else(|| "外部 Agent Runner 尚未配置 AppData".to_string())?;
|
.ok_or_else(|| "外部 Agent Runner 尚未配置 AppData".to_string())?;
|
||||||
@@ -1008,7 +1012,8 @@ pub(crate) fn install_external_agent_runner_platform_session(
|
|||||||
remember_external_agent_runner_platform_session(
|
remember_external_agent_runner_platform_session(
|
||||||
external_agent_runner_gui_owner_attachment_state(),
|
external_agent_runner_gui_owner_attachment_state(),
|
||||||
Some((user_id, access_token, api_base_url)),
|
Some((user_id, access_token, api_base_url)),
|
||||||
generation,
|
identity_generation,
|
||||||
|
revision,
|
||||||
)
|
)
|
||||||
.and_then(|_| ensure_external_agent_runner(&config_dir))
|
.and_then(|_| ensure_external_agent_runner(&config_dir))
|
||||||
.and_then(|endpoint| {
|
.and_then(|endpoint| {
|
||||||
@@ -1017,7 +1022,8 @@ pub(crate) fn install_external_agent_runner_platform_session(
|
|||||||
&config_dir,
|
&config_dir,
|
||||||
&endpoint,
|
&endpoint,
|
||||||
Some((user_id, access_token, api_base_url)),
|
Some((user_id, access_token, api_base_url)),
|
||||||
generation,
|
identity_generation,
|
||||||
|
revision,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -1025,7 +1031,10 @@ pub(crate) fn install_external_agent_runner_platform_session(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn clear_external_agent_runner_platform_session(generation: u64) -> Result<(), String> {
|
pub(crate) fn clear_external_agent_runner_platform_session(
|
||||||
|
identity_generation: u64,
|
||||||
|
revision: u64,
|
||||||
|
) -> Result<(), String> {
|
||||||
let Some(config_dir) = external_agent_runner_config_dir() else {
|
let Some(config_dir) = external_agent_runner_config_dir() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
@@ -1035,7 +1044,8 @@ pub(crate) fn clear_external_agent_runner_platform_session(generation: u64) -> R
|
|||||||
remember_external_agent_runner_platform_session(
|
remember_external_agent_runner_platform_session(
|
||||||
external_agent_runner_gui_owner_attachment_state(),
|
external_agent_runner_gui_owner_attachment_state(),
|
||||||
None,
|
None,
|
||||||
generation,
|
identity_generation,
|
||||||
|
revision,
|
||||||
)
|
)
|
||||||
.and_then(|_| ensure_external_agent_runner(&config_dir))
|
.and_then(|_| ensure_external_agent_runner(&config_dir))
|
||||||
.and_then(|endpoint| {
|
.and_then(|endpoint| {
|
||||||
@@ -1044,7 +1054,8 @@ pub(crate) fn clear_external_agent_runner_platform_session(generation: u64) -> R
|
|||||||
&config_dir,
|
&config_dir,
|
||||||
&endpoint,
|
&endpoint,
|
||||||
None,
|
None,
|
||||||
generation,
|
identity_generation,
|
||||||
|
revision,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -1057,7 +1068,8 @@ fn validate_external_agent_runner_platform_session_attachment(
|
|||||||
config_dir: &Path,
|
config_dir: &Path,
|
||||||
endpoint: &ExternalAgentRunnerEndpoint,
|
endpoint: &ExternalAgentRunnerEndpoint,
|
||||||
session: Option<(&str, &str, &str)>,
|
session: Option<(&str, &str, &str)>,
|
||||||
generation: u64,
|
identity_generation: u64,
|
||||||
|
revision: u64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let state = lock_unpoisoned(state);
|
let state = lock_unpoisoned(state);
|
||||||
let registration = state.registration.as_ref().ok_or_else(|| {
|
let registration = state.registration.as_ref().ok_or_else(|| {
|
||||||
@@ -1070,7 +1082,8 @@ fn validate_external_agent_runner_platform_session_attachment(
|
|||||||
|| registration.attached_boot_id.as_deref() != Some(endpoint.boot_id.as_str())
|
|| registration.attached_boot_id.as_deref() != Some(endpoint.boot_id.as_str())
|
||||||
|| registration.params.gui_owner_epoch.is_none()
|
|| registration.params.gui_owner_epoch.is_none()
|
||||||
|| registration.params.gui_owner_session_revision != Some(registration.generation)
|
|| registration.params.gui_owner_session_revision != Some(registration.generation)
|
||||||
|| registration.params.platform_auth_generation != Some(generation)
|
|| registration.params.platform_auth_generation != Some(identity_generation)
|
||||||
|
|| registration.params.platform_auth_revision != Some(revision)
|
||||||
|| registration.params.platform_user_id.as_deref() != expected_user_id
|
|| registration.params.platform_user_id.as_deref() != expected_user_id
|
||||||
|| registration.params.platform_access_token.as_deref() != expected_access_token
|
|| registration.params.platform_access_token.as_deref() != expected_access_token
|
||||||
|| registration.params.platform_api_base_url.as_deref() != expected_api_base_url
|
|| registration.params.platform_api_base_url.as_deref() != expected_api_base_url
|
||||||
@@ -1101,12 +1114,14 @@ pub(super) fn synchronize_external_agent_runner_platform_session_with(
|
|||||||
pub(super) fn remember_external_agent_runner_platform_session(
|
pub(super) fn remember_external_agent_runner_platform_session(
|
||||||
state: &Mutex<ExternalAgentRunnerGuiOwnerAttachmentState>,
|
state: &Mutex<ExternalAgentRunnerGuiOwnerAttachmentState>,
|
||||||
session: Option<(&str, &str, &str)>,
|
session: Option<(&str, &str, &str)>,
|
||||||
generation: u64,
|
identity_generation: u64,
|
||||||
|
revision: u64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
remember_external_agent_runner_platform_session_with(
|
remember_external_agent_runner_platform_session_with(
|
||||||
state,
|
state,
|
||||||
session,
|
session,
|
||||||
generation,
|
identity_generation,
|
||||||
|
revision,
|
||||||
write_external_agent_runner_gui_owner_claim_atomic,
|
write_external_agent_runner_gui_owner_claim_atomic,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1114,21 +1129,26 @@ pub(super) fn remember_external_agent_runner_platform_session(
|
|||||||
pub(super) fn remember_external_agent_runner_platform_session_with(
|
pub(super) fn remember_external_agent_runner_platform_session_with(
|
||||||
state: &Mutex<ExternalAgentRunnerGuiOwnerAttachmentState>,
|
state: &Mutex<ExternalAgentRunnerGuiOwnerAttachmentState>,
|
||||||
session: Option<(&str, &str, &str)>,
|
session: Option<(&str, &str, &str)>,
|
||||||
generation: u64,
|
identity_generation: u64,
|
||||||
|
revision: u64,
|
||||||
write_claim: impl FnOnce(&Path, &str, u64) -> Result<(), String>,
|
write_claim: impl FnOnce(&Path, &str, u64) -> Result<(), String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut state = lock_unpoisoned(state);
|
let mut state = lock_unpoisoned(state);
|
||||||
let Some(registration) = state.registration.as_ref() else {
|
let Some(registration) = state.registration.as_ref() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let current_generation = registration.params.platform_auth_generation.unwrap_or(0);
|
// 写入顺序只认 revision;身份代次只表达主体归属,同一账号续期会推进 revision
|
||||||
if generation < current_generation {
|
// 但保持 identity generation 不变。
|
||||||
|
let current_revision = registration.params.platform_auth_revision.unwrap_or(0);
|
||||||
|
if revision < current_revision {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if generation == current_generation {
|
if revision == current_revision {
|
||||||
match session {
|
match session {
|
||||||
Some((user_id, access_token, api_base_url))
|
Some((user_id, access_token, api_base_url))
|
||||||
if registration.params.platform_user_id.as_deref() == Some(user_id)
|
if registration.params.platform_user_id.as_deref() == Some(user_id)
|
||||||
|
&& registration.params.platform_auth_generation
|
||||||
|
== Some(identity_generation)
|
||||||
&& registration.params.platform_access_token.as_deref()
|
&& registration.params.platform_access_token.as_deref()
|
||||||
== Some(access_token)
|
== Some(access_token)
|
||||||
&& registration.params.platform_api_base_url.as_deref()
|
&& registration.params.platform_api_base_url.as_deref()
|
||||||
@@ -1168,7 +1188,8 @@ pub(super) fn remember_external_agent_runner_platform_session_with(
|
|||||||
session.map(|(_, access_token, _)| access_token.to_string());
|
session.map(|(_, access_token, _)| access_token.to_string());
|
||||||
registration.params.platform_api_base_url =
|
registration.params.platform_api_base_url =
|
||||||
session.map(|(_, _, api_base_url)| api_base_url.to_string());
|
session.map(|(_, _, api_base_url)| api_base_url.to_string());
|
||||||
registration.params.platform_auth_generation = Some(generation);
|
registration.params.platform_auth_generation = Some(identity_generation);
|
||||||
|
registration.params.platform_auth_revision = Some(revision);
|
||||||
registration.params.gui_owner_session_revision = Some(registration_generation);
|
registration.params.gui_owner_session_revision = Some(registration_generation);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1472,6 +1493,7 @@ pub(super) fn send_external_agent_runner_runtime_request_with_stable_identity(
|
|||||||
platform_access_token: None,
|
platform_access_token: None,
|
||||||
platform_api_base_url: None,
|
platform_api_base_url: None,
|
||||||
platform_auth_generation: None,
|
platform_auth_generation: None,
|
||||||
|
platform_auth_revision: None,
|
||||||
};
|
};
|
||||||
match stable_identity {
|
match stable_identity {
|
||||||
Some(stable_identity) => {
|
Some(stable_identity) => {
|
||||||
|
|||||||
@@ -133,37 +133,46 @@ fn apply_external_agent_runner_gui_owner_attachment(
|
|||||||
params.platform_access_token.as_deref(),
|
params.platform_access_token.as_deref(),
|
||||||
params.platform_api_base_url.as_deref(),
|
params.platform_api_base_url.as_deref(),
|
||||||
params.platform_auth_generation,
|
params.platform_auth_generation,
|
||||||
|
params.platform_auth_revision,
|
||||||
) {
|
) {
|
||||||
(Some(user_id), Some(access_token), Some(api_base_url), Some(generation)) => {
|
(
|
||||||
|
Some(user_id),
|
||||||
|
Some(access_token),
|
||||||
|
Some(api_base_url),
|
||||||
|
Some(identity_generation),
|
||||||
|
Some(revision),
|
||||||
|
) => {
|
||||||
if replace_claim {
|
if replace_claim {
|
||||||
crate::replace_platform_session_for_gui_owner(
|
crate::replace_platform_session_for_gui_owner(
|
||||||
user_id,
|
user_id,
|
||||||
access_token,
|
access_token,
|
||||||
api_base_url,
|
api_base_url,
|
||||||
generation,
|
identity_generation,
|
||||||
|
revision,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
crate::install_platform_session_checked(
|
crate::install_platform_session_checked(
|
||||||
user_id,
|
user_id,
|
||||||
access_token,
|
access_token,
|
||||||
api_base_url,
|
api_base_url,
|
||||||
generation,
|
identity_generation,
|
||||||
|
revision,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(None, None, None, Some(generation)) => {
|
(None, None, None, Some(identity_generation), Some(revision)) => {
|
||||||
if replace_claim {
|
if replace_claim {
|
||||||
crate::clear_platform_session_for_gui_owner(generation);
|
crate::clear_platform_session_for_gui_owner(identity_generation, revision);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
crate::clear_platform_session_checked(generation)
|
crate::clear_platform_session_checked(identity_generation, revision)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(None, None, None, None) if replace_claim => {
|
(None, None, None, None, None) if replace_claim => {
|
||||||
crate::clear_platform_session_for_gui_owner(0);
|
crate::clear_platform_session_for_gui_owner(0, 0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
(None, None, None, None) => Ok(()),
|
(None, None, None, None, None) => Ok(()),
|
||||||
_ => Err("Agent Runner GUI owner 的平台登录态同步参数不完整".to_string()),
|
_ => Err("Agent Runner GUI owner 的平台登录态同步参数不完整".to_string()),
|
||||||
};
|
};
|
||||||
result?;
|
result?;
|
||||||
@@ -171,7 +180,7 @@ fn apply_external_agent_runner_gui_owner_attachment(
|
|||||||
Ok(claim) => claim,
|
Ok(claim) => claim,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
*active_claim = None;
|
*active_claim = None;
|
||||||
crate::clear_platform_session_for_gui_owner(0);
|
crate::clear_platform_session_for_gui_owner(0, 0);
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Agent Runner GUI owner claim 在 attach 提交期间无法核验,平台登录态已隔离:{error}"
|
"Agent Runner GUI owner claim 在 attach 提交期间无法核验,平台登录态已隔离:{error}"
|
||||||
));
|
));
|
||||||
@@ -181,7 +190,7 @@ fn apply_external_agent_runner_gui_owner_attachment(
|
|||||||
|| committed_claim.session_revision != requested_revision
|
|| committed_claim.session_revision != requested_revision
|
||||||
{
|
{
|
||||||
*active_claim = None;
|
*active_claim = None;
|
||||||
crate::clear_platform_session_for_gui_owner(0);
|
crate::clear_platform_session_for_gui_owner(0, 0);
|
||||||
return Err("Agent Runner GUI owner claim 在 attach 提交期间已变化".to_string());
|
return Err("Agent Runner GUI owner claim 在 attach 提交期间已变化".to_string());
|
||||||
}
|
}
|
||||||
if let Some(event_sink) = event_sink {
|
if let Some(event_sink) = event_sink {
|
||||||
@@ -207,7 +216,7 @@ pub(super) fn validate_external_agent_runner_gui_owner_claim_current(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
*active_claim = None;
|
*active_claim = None;
|
||||||
crate::clear_platform_session_for_gui_owner(0);
|
crate::clear_platform_session_for_gui_owner(0, 0);
|
||||||
match durable_claim {
|
match durable_claim {
|
||||||
Ok(_) => Err(
|
Ok(_) => Err(
|
||||||
"authentication-required: Agent Runner GUI owner claim 已变化,平台登录态已隔离"
|
"authentication-required: Agent Runner GUI owner claim 已变化,平台登录态已隔离"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user