Merge branch 'feat/agc-generation-concurrency' into fix/agc-canvas-acceptance-batch

This commit is contained in:
2026-09-14 17:31:40 +08:00
2 changed files with 855 additions and 101 deletions
File diff suppressed because it is too large Load Diff
@@ -1074,6 +1074,102 @@ pub(in crate::agent) fn remove_platform_art_generation_runtime_state_at(
}
}
/// 一次性兼容:升级前 standalone 槽身份只由 `{outputPath, requireSlices}` 派生,
/// 同一项目所有图片类生成共用一个槽;升级后槽身份按精确动作派生,路径随之变化。
///
/// 若旧槽路径上的账本仍然属于本次精确动作(`agentId` 与 `actionFingerprint` 都与
/// 当前上下文一致),就在项目写锁内把它迁移到新身份路径:保留原 `idempotencyKey`
/// 与 `operationId`,避免同一精确动作在升级后二次 POST 计费。旧账本属于其他动作时
/// 原样保留(不迁移、不删除、不阻塞),由对应动作自己的请求迁移。
///
/// 返回 `Ok(false)` 表示没有需要迁移的旧账本。任何身份无法安全解释的情形都失败关闭。
pub(super) fn adopt_legacy_standalone_platform_art_generation_runtime_state_at(
root: &Path,
context: &PlatformArtGenerationRuntimeContext,
legacy_run_id: &str,
) -> Result<bool, String> {
if !is_standalone_platform_art_generation_runtime_context(context)
|| legacy_run_id == context.run_id
|| !is_lowercase_sha256(legacy_run_id.strip_prefix("slot-").unwrap_or_default())
{
return Ok(false);
}
// 与账本创建互斥:迁移必须在同一把项目写锁内完成,否则两个调用可能同时把同一份
// 旧账本迁移到新路径,或与新建账本互相覆盖。
let _claim_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"canvas.asset_generate.runtime.claim",
)?;
if game_creator_agent_runtime_external_generation_exists(
root,
&context.agent_id,
&context.run_id,
) {
// 新身份账本已经存在:旧账本不属于本次动作的权威状态,保持两边各自的身份。
return Ok(false);
}
let legacy_relative_path =
platform_art_generation_runtime_relative_path(&context.agent_id, legacy_run_id);
let Some(legacy_state) =
read_agent_runtime_json_sidecar_with_max_bytes::<PlatformArtGenerationRuntimeState>(
root,
&legacy_relative_path,
"External Editor 生成账本",
PLATFORM_ART_GENERATION_RUNTIME_MAX_BYTES,
)?
else {
return Ok(false);
};
if legacy_state.agent_id != context.agent_id
|| legacy_state.run_id != legacy_run_id
|| legacy_state.action_fingerprint != context.action_fingerprint
{
// 旧槽里是另一个精确动作的账本:它仍归那个动作所有,本次调用不得消费、改写或删除它。
return Ok(false);
}
let legacy_identity = format!("{}:{legacy_run_id}", context.agent_id);
let legacy_context = PlatformArtGenerationRuntimeContext {
task_id: legacy_identity.clone(),
session_id: legacy_identity.clone(),
run_id: legacy_run_id.to_string(),
action_id: legacy_identity,
..context.clone()
};
let Some(mut migrated) = read_platform_art_generation_runtime_state(root, &legacy_context)?
else {
return Ok(false);
};
migrated.run_id = context.run_id.clone();
migrated.task_id = context.task_id.clone();
migrated.session_id = context.session_id.clone();
migrated.action_id = context.action_id.clone();
migrated.updated_at = unix_timestamp();
write_platform_art_generation_runtime_state(root, &migrated)?;
remove_platform_art_generation_runtime_state_at(root, &context.agent_id, legacy_run_id)?;
Ok(true)
}
#[cfg(test)]
pub(super) fn platform_art_generation_runtime_operation_id_for_test(
state: &PlatformArtGenerationRuntimeState,
) -> Option<&str> {
state.operation_id.as_deref()
}
#[cfg(test)]
pub(super) fn platform_art_generation_runtime_run_id_for_test(
state: &PlatformArtGenerationRuntimeState,
) -> &str {
&state.run_id
}
#[cfg(test)]
pub(super) fn platform_art_generation_runtime_action_fingerprint_for_test(
state: &PlatformArtGenerationRuntimeState,
) -> &str {
&state.action_fingerprint
}
#[cfg(test)]
pub(crate) fn write_platform_art_generation_runtime_accepted_for_test(
root: &Path,