AGC 客户端 MCP 能力暴露 #274
@@ -113,6 +113,8 @@ const allowedUncalledTauriCommands = [
|
||||
'open_game_creator_launcher_window',
|
||||
'open_game_creator_workspace_window',
|
||||
'stop_local_game_preview_if_matches',
|
||||
'start_game_creator_external_mcp',
|
||||
'stop_game_creator_external_mcp',
|
||||
];
|
||||
const sourceExtensions = new Set([
|
||||
'.json',
|
||||
|
||||
@@ -127,6 +127,39 @@ function readBackendTargets({ requireAgcBackend = false } = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function readBackendServiceFailure(
|
||||
state,
|
||||
{
|
||||
expectedDatabase = backendDatabase,
|
||||
expectedSpacetimeDataDir = backendSpacetimeDataDir,
|
||||
} = {},
|
||||
) {
|
||||
const targets = resolveBackendTargetsFromState(state, {
|
||||
requireAgcBackend: true,
|
||||
expectedDatabase,
|
||||
expectedSpacetimeDataDir,
|
||||
});
|
||||
if (!targets.hasMatchingBackend) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const serviceName of ['spacetime', 'api-server', 'bgfilter-worker']) {
|
||||
const service = state?.services?.[serviceName];
|
||||
if (service?.status !== 'failed') {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
serviceName,
|
||||
failure: service.signal
|
||||
? `signal=${service.signal}`
|
||||
: `code=${service.exitCode ?? 1}`,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function isBackendReady({
|
||||
state = readJson(devStackStatePath),
|
||||
isReady = isHttpReady,
|
||||
@@ -505,11 +538,29 @@ async function terminateChildTree(
|
||||
return { stopped, forced: true };
|
||||
}
|
||||
|
||||
async function waitForBackendReady(backendChild, timeoutMs = 600_000) {
|
||||
async function waitForBackendReady(
|
||||
backendChild,
|
||||
timeoutMs = 600_000,
|
||||
{
|
||||
checkBackendReady = isBackendReady,
|
||||
readState = () => readJson(devStackStatePath),
|
||||
resolveTargets = readBackendTargets,
|
||||
} = {},
|
||||
) {
|
||||
const initialStateUpdatedAt = readState()?.updatedAt ?? '';
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
if (await isBackendReady()) {
|
||||
return readBackendTargets();
|
||||
if (await checkBackendReady()) {
|
||||
return resolveTargets();
|
||||
}
|
||||
const state = readState();
|
||||
if ((state?.updatedAt ?? '') !== initialStateUpdatedAt) {
|
||||
const serviceFailure = readBackendServiceFailure(state);
|
||||
if (serviceFailure) {
|
||||
throw new Error(
|
||||
`配套后端启动失败: ${serviceFailure.serviceName} ${serviceFailure.failure}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const failure = readChildFailure(backendChild);
|
||||
if (failure) {
|
||||
@@ -686,6 +737,7 @@ export {
|
||||
isDirectModuleExecution,
|
||||
isProcessGroupAlive,
|
||||
preflightExistingVite,
|
||||
readBackendServiceFailure,
|
||||
readChildFailure,
|
||||
readExistingViteServer,
|
||||
readLinuxProcessGroupAlive,
|
||||
|
||||
@@ -2598,7 +2598,7 @@ impl CodexAppServerConnection {
|
||||
"AGC 直连项目缺少客户端受控工具桥".to_string(),
|
||||
)
|
||||
})?
|
||||
.begin_user_turn(direct_codex_current_user_prompt(&request))
|
||||
.begin_user_turn()
|
||||
.map_err(platform_llm::LlmError::InvalidRequest)?,
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -2147,7 +2147,7 @@ fn direct_registered_taonier_slice_paths(root: &Path) -> Vec<String> {
|
||||
let Ok(validated_slices) = validated_art_slices(root) else {
|
||||
return Vec::new();
|
||||
};
|
||||
if validated_slices.len() != 4 {
|
||||
if validated_slices.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut resource_ids = std::collections::HashSet::with_capacity(validated_slices.len());
|
||||
@@ -2718,6 +2718,7 @@ async fn generate_direct_taonier_art_asset_at(
|
||||
asset_kind: asset_kind.to_string(),
|
||||
asset_label: asset_label.to_string(),
|
||||
replace_existing: root.join(output_path).is_file(),
|
||||
slice_count: None,
|
||||
};
|
||||
let runtime_context =
|
||||
direct_taonier_art_generation_runtime_context(root, output_path, asset_kind)?;
|
||||
@@ -2821,9 +2822,9 @@ fn direct_taonier_art_package_result(
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if includes_spritesheet && slice_paths.len() != 4 {
|
||||
if includes_spritesheet && slice_paths.is_empty() {
|
||||
slice_warnings.push(
|
||||
"当前核心图集没有可验证的独立切片;只能使用完整图集,不得猜测切片或伪造衍生素材"
|
||||
"当前图集没有可验证的独立切片;只能使用完整图集,不得猜测切片或伪造衍生素材"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ struct DirectToolBridgeTurnAuthorization {
|
||||
|
||||
struct DirectToolBridgeActiveTurnAuthorization {
|
||||
turn_id: String,
|
||||
allows_regeneration: bool,
|
||||
brief_sha256: Option<String>,
|
||||
completed_result: Option<Value>,
|
||||
resource_request_ids: BTreeMap<String, (String, String)>,
|
||||
@@ -181,30 +180,23 @@ impl DirectToolBridge {
|
||||
&self.url
|
||||
}
|
||||
|
||||
/// Arm exactly one client-owned Direct turn. The raw user message is used
|
||||
/// only for this synchronous decision and is never retained by the bridge.
|
||||
pub(crate) fn begin_user_turn(
|
||||
&self,
|
||||
user_prompt: &str,
|
||||
) -> Result<DirectToolBridgeTurnGuard, String> {
|
||||
self.state.begin_user_turn(user_prompt)
|
||||
/// Arm exactly one client-owned Direct turn. Codex chooses the business
|
||||
/// operation through the reviewed MCP tool and arguments; the bridge only
|
||||
/// binds that call to the active client turn.
|
||||
pub(crate) fn begin_user_turn(&self) -> Result<DirectToolBridgeTurnGuard, String> {
|
||||
self.state.begin_user_turn()
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectToolBridgeState {
|
||||
fn begin_user_turn(
|
||||
self: &Arc<Self>,
|
||||
user_prompt: &str,
|
||||
) -> Result<DirectToolBridgeTurnGuard, String> {
|
||||
fn begin_user_turn(self: &Arc<Self>) -> Result<DirectToolBridgeTurnGuard, String> {
|
||||
let turn_id = direct_taonier_active_invocation_id_at(&self.root)?;
|
||||
let allows_regeneration = direct_user_explicitly_authorizes_art_regeneration(user_prompt);
|
||||
let mut authorization = self
|
||||
.turn_authorization
|
||||
.lock()
|
||||
.map_err(|_| "AGC 工具桥回合授权状态不可用".to_string())?;
|
||||
authorization.active = Some(DirectToolBridgeActiveTurnAuthorization {
|
||||
turn_id: turn_id.clone(),
|
||||
allows_regeneration,
|
||||
brief_sha256: None,
|
||||
completed_result: None,
|
||||
resource_request_ids: BTreeMap::new(),
|
||||
@@ -594,12 +586,9 @@ impl DirectToolBridgeState {
|
||||
.active
|
||||
.as_mut()
|
||||
.ok_or_else(|| "当前没有客户端签发的美术重生成回合授权".to_string())?;
|
||||
if !active.allows_regeneration {
|
||||
return Err("当前用户消息未显式授权重新生成或替换美术".to_string());
|
||||
}
|
||||
match active.brief_sha256.as_deref() {
|
||||
Some(expected) if expected != brief_sha256 => {
|
||||
return Err("当前用户授权已绑定另一项稳定美术重生成请求".to_string())
|
||||
return Err("当前客户端回合已绑定另一项稳定美术重生成请求".to_string())
|
||||
}
|
||||
None => active.brief_sha256 = Some(brief_sha256.clone()),
|
||||
Some(_) => {}
|
||||
@@ -2116,6 +2105,7 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
|
||||
asset_kind: kind.clone(),
|
||||
asset_label: asset_name.clone(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
};
|
||||
let _generation_guard = state.image_generation_gate.lock().await;
|
||||
let generated = with_direct_editor_api_credentials(
|
||||
@@ -2736,106 +2726,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regenerate_requires_current_explicit_user_authorization_and_one_stable_brief() {
|
||||
for prompt in [
|
||||
"继续修复布局",
|
||||
"解释一下重新生成美术是什么意思",
|
||||
"不要重新生成美术,只调整代码",
|
||||
"别换一套美术,继续用现在这套",
|
||||
"解释一下换一套美术按钮",
|
||||
"是否要改变视觉风格?",
|
||||
"Do not regenerate the art; keep the current package.",
|
||||
"I don't want to change the visual style.",
|
||||
"What does use a new art set mean?",
|
||||
"文案写着“换一套美术”",
|
||||
"Yesterday I said regenerate art, but today keep it.",
|
||||
"Please explain how to regenerate art.",
|
||||
"重新生成美术以后再说,现在只修代码",
|
||||
"重做美术先不做,先改玩法",
|
||||
"Regenerate the art maybe later; for now just fix the code.",
|
||||
"把按钮文案改成“请重新生成美术”,不要执行生成工具",
|
||||
"把按钮文案改成‘请重新生成美术’,不要执行生成工具",
|
||||
"Change the button label to 'please regenerate the art'; do not execute it.",
|
||||
"用户之前说请重新生成美术,我只是在复述",
|
||||
"Yesterday the user said please regenerate the art; I am just quoting it.",
|
||||
"以后请重新生成美术,现在先改代码",
|
||||
"你能不能帮我重新生成美术,顺便解释一下价格",
|
||||
"请重新生成美术吗",
|
||||
"请重新生成美术吗,还是只改代码",
|
||||
"请重新生成美术或者只改代码",
|
||||
"请重新生成美术以外的内容",
|
||||
"请重新生成美术,但不要执行生成工具",
|
||||
"不需要重新生成美术",
|
||||
"界面上显示:请重新生成美术",
|
||||
"界面标题是请重新生成美术",
|
||||
"产品经理让我写请重新生成美术",
|
||||
"下周请重新生成美术",
|
||||
"他说«请重新生成美术»",
|
||||
"Could you please regenerate the art",
|
||||
"Please regenerate the art? Or only fix code.",
|
||||
"Please regenerate the art except for the paid generation.",
|
||||
"Please regenerate the art, but do not execute the tool.",
|
||||
"Please regenerate the art, but don’t execute the tool.",
|
||||
"Please regenerate the art, but I don't authorize this paid generation.",
|
||||
"Please regenerate the art, but I don‘t authorize this paid generation.",
|
||||
"Please regenerate the art, but do not execute the paid tool.",
|
||||
"Please regenerate the art, but never execute the paid tool.",
|
||||
"Please regenerate the art, but avoid executing the paid tool.",
|
||||
"Please regenerate the art, but 'do not execute the tool",
|
||||
"Please regenerate the art only if it is free.",
|
||||
"Please regenerate the art only after I confirm the charge.",
|
||||
"Please regenerate the art, but do “not” execute the paid tool.",
|
||||
"请重新生成美术,三天后再执行。",
|
||||
"请重新生成美术,得到我的许可再做。",
|
||||
"请重新生成美术,地面需要无缝循环。",
|
||||
"Please regenerate the art, but skip the paid generation.",
|
||||
"请重新生成美术【生成操作跳过】",
|
||||
"请重新生成美术【仅在零元时执行】",
|
||||
"Please regenerate the art “but skip the paid generation”",
|
||||
"Please regenerate the art; alternatively, just fix the code.",
|
||||
"Please regenerate the art, but do n\u{200B}ot execute the paid tool.",
|
||||
"Please regenerate the art with a clay style.",
|
||||
"I don't need you to regenerate the art",
|
||||
"The UI shows: please regenerate the art",
|
||||
"Please regenerate the art next week",
|
||||
"He said «please regenerate the art»",
|
||||
] {
|
||||
assert!(
|
||||
!direct_user_explicitly_authorizes_art_regeneration(prompt),
|
||||
"prompt must fail closed: {prompt}"
|
||||
);
|
||||
}
|
||||
for prompt in [
|
||||
"请重新生成美术。",
|
||||
"那就请重新生成美术!",
|
||||
"换一套美术",
|
||||
"Please regenerate the art!",
|
||||
] {
|
||||
assert!(
|
||||
direct_user_explicitly_authorizes_art_regeneration(prompt),
|
||||
"prompt must explicitly authorize: {prompt}"
|
||||
);
|
||||
}
|
||||
|
||||
fn regenerate_uses_current_client_turn_and_one_stable_brief() {
|
||||
let root = tempfile::tempdir().expect("stable client turn root");
|
||||
let state = direct_tool_bridge_state(root.path().to_path_buf());
|
||||
assert!(state.begin_user_turn("请重新生成美术").is_err());
|
||||
assert!(state.begin_user_turn().is_err());
|
||||
let client_turn_id = "client-turn-stable-0001";
|
||||
let _active_invocation =
|
||||
DirectTaonierActiveInvocationGuard::enter(root.path(), client_turn_id)
|
||||
.expect("client-owned stable invocation");
|
||||
let ordinary_turn = state
|
||||
.begin_user_turn("继续优化交互")
|
||||
.expect("ordinary turn authorization state");
|
||||
assert!(state.authorize_regeneration_call("陶泥风格").is_err());
|
||||
drop(ordinary_turn);
|
||||
|
||||
let authorized_turn = state
|
||||
.begin_user_turn("请重新生成美术")
|
||||
.expect("authorized regeneration turn");
|
||||
let active_turn = state
|
||||
.begin_user_turn()
|
||||
.expect("client turn authorization state");
|
||||
let (turn_id, brief_sha256) = match state
|
||||
.authorize_regeneration_call("陶泥风格")
|
||||
.expect("first stable regeneration call")
|
||||
.expect("MCP mode selects regeneration explicitly")
|
||||
{
|
||||
DirectToolBridgeRegenerationCall::Execute {
|
||||
turn_id,
|
||||
@@ -2864,7 +2768,7 @@ mod tests {
|
||||
panic!("completed stable retry must not execute a second paid call")
|
||||
}
|
||||
}
|
||||
drop(authorized_turn);
|
||||
drop(active_turn);
|
||||
assert!(state.authorize_regeneration_call("陶泥风格").is_err());
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -412,6 +412,7 @@ pub(crate) struct PlatformArtAssetGenerationOptions {
|
||||
pub(crate) asset_kind: String,
|
||||
pub(crate) asset_label: String,
|
||||
pub(crate) replace_existing: bool,
|
||||
pub(crate) slice_count: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for PlatformArtAssetGenerationOptions {
|
||||
@@ -423,6 +424,7 @@ impl Default for PlatformArtAssetGenerationOptions {
|
||||
asset_kind: "game-art".to_string(),
|
||||
asset_label: "AI 游戏首版美术素材".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -681,6 +683,47 @@ pub(in crate::agent) fn platform_art_generation_error_result_unknown(error: &str
|
||||
error.starts_with(EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX)
|
||||
}
|
||||
|
||||
/// Observe an accepted operation once without waiting. A single GET that
|
||||
/// reports `failed` is authoritative and allows a changed retry to release
|
||||
/// the old local slot; queued/running/unknown outcomes remain protected.
|
||||
async fn accepted_generation_is_authoritatively_failed_once(
|
||||
client: &reqwest::Client,
|
||||
access: &ExternalEditorBindingAccess<'_>,
|
||||
submission_payload: &serde_json::Value,
|
||||
) -> Result<bool, String> {
|
||||
let submission = external_editor_response_data(submission_payload);
|
||||
let operation_id = json_string_field(submission, "operationId")
|
||||
.ok_or_else(|| "External Editor accepted 账本缺少 operationId".to_string())?;
|
||||
access.validate_frozen_session()?;
|
||||
let payload = tokio::time::timeout(
|
||||
Duration::from_secs(3),
|
||||
external_editor_json_request(
|
||||
client
|
||||
.get(format!(
|
||||
"{}{}",
|
||||
access.api_base_url(),
|
||||
access.generation_status_route(&operation_id)
|
||||
))
|
||||
.bearer_auth(access.bearer_token()),
|
||||
"查询平台图片生成任务",
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "查询平台图片生成任务超时".to_string())??;
|
||||
access.validate_frozen_session()?;
|
||||
let generation = platform_generation_status_data(&payload);
|
||||
match json_string_field(generation, "status").as_deref() {
|
||||
Some("failed") => Ok(true),
|
||||
Some("queued" | "running" | "completed") => Ok(false),
|
||||
Some(status) => Err(format!(
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 平台图片生成任务返回未知状态 {status};operationId={operation_id}"
|
||||
)),
|
||||
None => Err(format!(
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 平台图片生成任务状态响应缺少 status;operationId={operation_id}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn external_editor_json_request(
|
||||
request: reqwest::RequestBuilder,
|
||||
action: &str,
|
||||
@@ -1714,24 +1757,16 @@ fn canonical_art_spritesheet_icon_descriptions(prompt: &str) -> Vec<String> {
|
||||
// long creation request cannot reject the atlas before it is queued.
|
||||
const MAX_DESCRIPTION_CHARS: usize = 200;
|
||||
const CONTEXT_PREFIX: &str = ";遵循同一项目视觉规范:";
|
||||
[
|
||||
"第 1 类(左上):当前玩法的玩家主体或主要操作对象;只生成一个轮廓连贯、可独立使用的完整素材",
|
||||
"第 2 类(右上):当前玩法的方块、目标物、收集物、敌对实体或危险物;只生成一个完整素材",
|
||||
"第 3 类(左下):当前玩法需要的地块、障碍、资源物件或场景装饰;只生成一个完整素材",
|
||||
"第 4 类(右下):得分、受击、成长、失败、胜利或操作反馈特效;只生成一个完整素材",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|category| {
|
||||
let context_budget = MAX_DESCRIPTION_CHARS.saturating_sub(
|
||||
category
|
||||
.chars()
|
||||
.count()
|
||||
.saturating_add(CONTEXT_PREFIX.chars().count()),
|
||||
);
|
||||
let project_context = truncate_inline_bounded(prompt.trim(), context_budget);
|
||||
format!("{category}{CONTEXT_PREFIX}{project_context}")
|
||||
})
|
||||
.collect()
|
||||
let category =
|
||||
"按当前项目需求生成一组可独立使用的透明素材;数量、类别、排列和切片方式由本次需求决定";
|
||||
let context_budget = MAX_DESCRIPTION_CHARS.saturating_sub(
|
||||
category
|
||||
.chars()
|
||||
.count()
|
||||
.saturating_add(CONTEXT_PREFIX.chars().count()),
|
||||
);
|
||||
let project_context = truncate_inline_bounded(prompt.trim(), context_budget);
|
||||
vec![format!("{category}{CONTEXT_PREFIX}{project_context}")]
|
||||
}
|
||||
|
||||
fn truncate_inline_bounded(value: &str, max_chars: usize) -> String {
|
||||
@@ -2043,13 +2078,13 @@ pub(crate) async fn generate_platform_art_asset_with_required_slices_at(
|
||||
}
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, briefs, options);
|
||||
let runtime_context =
|
||||
standalone_platform_art_generation_runtime_context(&generation_prompt, options, true)?;
|
||||
standalone_platform_art_generation_runtime_context(&generation_prompt, options, false)?;
|
||||
generate_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
prompt,
|
||||
briefs,
|
||||
options,
|
||||
true,
|
||||
false,
|
||||
&runtime_context,
|
||||
)
|
||||
.await
|
||||
@@ -2427,6 +2462,34 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
)
|
||||
})?;
|
||||
if snapshot.generation_prompt != generation_prompt {
|
||||
if platform_art_generation_runtime_status(&state) == "accepted" {
|
||||
if let Ok(submission) = platform_art_generation_runtime_submission_payload(&state) {
|
||||
if accepted_generation_is_authoritatively_failed_once(
|
||||
&client,
|
||||
&binding_access,
|
||||
&submission,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Some(context) = runtime_context {
|
||||
remove_platform_art_generation_runtime_state_at(
|
||||
root,
|
||||
&context.agent_id,
|
||||
&context.run_id,
|
||||
)?;
|
||||
}
|
||||
return Box::pin(request_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
prompt,
|
||||
briefs,
|
||||
options,
|
||||
runtime_context,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(format!(
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 当前生成意图与已持久化请求快照不一致,已拒绝将旧操作当作本次请求恢复;原生成账本已保留,需要先完成或对账旧操作"
|
||||
));
|
||||
@@ -2448,6 +2511,36 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
)
|
||||
})?;
|
||||
if snapshot.reference_resource_ids != [current_reference] {
|
||||
if platform_art_generation_runtime_status(&state) == "accepted" {
|
||||
if let Ok(submission) =
|
||||
platform_art_generation_runtime_submission_payload(&state)
|
||||
{
|
||||
if accepted_generation_is_authoritatively_failed_once(
|
||||
&client,
|
||||
&binding_access,
|
||||
&submission,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Some(context) = runtime_context {
|
||||
remove_platform_art_generation_runtime_state_at(
|
||||
root,
|
||||
&context.agent_id,
|
||||
&context.run_id,
|
||||
)?;
|
||||
}
|
||||
return Box::pin(request_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
prompt,
|
||||
briefs,
|
||||
options,
|
||||
runtime_context,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(format!(
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 当前规范图身份与已持久化派生请求不一致,已拒绝恢复旧操作;原生成账本已保留,需要先完成或对账旧操作"
|
||||
));
|
||||
@@ -2556,7 +2649,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
serde_json::json!({
|
||||
"referenceId": reference_id,
|
||||
"iconDescriptions": canonical_art_spritesheet_icon_descriptions(&generation_prompt),
|
||||
"sliceLayout": "grid-2x2",
|
||||
"sliceCount": options.slice_count,
|
||||
"screenColor": "auto",
|
||||
"aspectRatio": options.aspect_ratio,
|
||||
"imageSize": options.image_size,
|
||||
@@ -6259,11 +6352,8 @@ fn validate_strict_platform_art_spritesheet_contract(
|
||||
has_transparent_pixels: bool,
|
||||
has_visible_pixels: bool,
|
||||
) -> Result<(), String> {
|
||||
if slices.len() != 4 {
|
||||
return Err(format!(
|
||||
"strict spritesheet 图集必须恰好包含 4 个独立切片,实际为 {} 个",
|
||||
slices.len()
|
||||
));
|
||||
if slices.is_empty() {
|
||||
return Err("spritesheet 图集至少需要一个独立切片".to_string());
|
||||
}
|
||||
let resource_id = resource_id
|
||||
.map(str::trim)
|
||||
@@ -6294,12 +6384,7 @@ fn validate_strict_platform_art_spritesheet_contract(
|
||||
{
|
||||
return Err("strict spritesheet 图集生成 route/kind 与严格图集合同不一致".to_string());
|
||||
}
|
||||
if spritesheet_slice_layout.map(str::trim) != Some("grid-2x2") {
|
||||
return Err(
|
||||
"strict spritesheet 图集必须由 External Editor 以 grid-2x2 固定切片合同生成"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let _requested_slice_layout = spritesheet_slice_layout;
|
||||
if reference_resource_ids.len() != 1
|
||||
|| reference_resource_ids[0].trim().is_empty()
|
||||
|| reference_resource_ids[0].trim() == resource_id
|
||||
@@ -6634,7 +6719,7 @@ fn existing_platform_art_slice_registrations_are_complete(
|
||||
manifest: &GameCreationAppManifest,
|
||||
registrations: &[PlatformArtSliceManifestRegistration],
|
||||
) -> Result<bool, String> {
|
||||
if registrations.len() != 4 {
|
||||
if registrations.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut resource_ids = std::collections::HashSet::with_capacity(registrations.len());
|
||||
@@ -7594,6 +7679,7 @@ mod canvas_generation_tests {
|
||||
asset_kind: "game-background".to_string(),
|
||||
asset_label: "手工背景".to_string(),
|
||||
replace_existing: true,
|
||||
slice_count: None,
|
||||
};
|
||||
let ordinary =
|
||||
standalone_platform_art_generation_runtime_context("完整生成提示词", &options, false)
|
||||
@@ -9402,6 +9488,7 @@ mod canvas_generation_tests {
|
||||
asset_kind: "icon-spec".to_string(),
|
||||
asset_label: "整包规范图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
};
|
||||
let prompt = "生成同一套整包美术";
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options);
|
||||
@@ -10239,6 +10326,7 @@ mod canvas_generation_tests {
|
||||
asset_kind: "game-background".to_string(),
|
||||
asset_label: "整包背景图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
};
|
||||
let prompt = "保持同一个生成提示词";
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options);
|
||||
@@ -10690,6 +10778,7 @@ mod canvas_generation_tests {
|
||||
asset_kind: "icon-spec".to_string(),
|
||||
asset_label: "游戏统一视觉规范图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
};
|
||||
let prompt = "恢复已受理视觉规范图";
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options);
|
||||
@@ -11264,6 +11353,7 @@ mod canvas_generation_tests {
|
||||
asset_kind: "art-spritesheet".to_string(),
|
||||
asset_label: "游戏首版核心美术素材".to_string(),
|
||||
replace_existing: true,
|
||||
slice_count: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+3
-10
@@ -453,16 +453,9 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_initial_collaboration_
|
||||
"首批 art-director 必须是非只读规范图生成任务",
|
||||
));
|
||||
}
|
||||
let art_artifacts =
|
||||
// 图片产物由 Codex 按项目需求决定;不再要求固定 art-spec.png。
|
||||
let _art_artifacts =
|
||||
autonomous_initial_delegate_expected_artifacts(art_director, "art-director")?;
|
||||
if !art_artifacts
|
||||
.iter()
|
||||
.any(|path| path == "assets/art-spec.png")
|
||||
{
|
||||
return Err(autonomous_initial_collaboration_contract_error(
|
||||
"首批 art-director 的 expectedArtifacts 必须包含 assets/art-spec.png",
|
||||
));
|
||||
}
|
||||
|
||||
let code_director = code_director.ok_or_else(|| {
|
||||
autonomous_initial_collaboration_contract_error("首批缺少 code-director 委派")
|
||||
@@ -1976,7 +1969,7 @@ mod tests {
|
||||
plan: Vec::new(),
|
||||
actions: vec![
|
||||
autonomous_initial_delegate("design-director", &[]),
|
||||
autonomous_initial_delegate("art-director", &["assets/art-spec.png"]),
|
||||
autonomous_initial_delegate("art-director", &[]),
|
||||
autonomous_initial_delegate("code-director", &[]),
|
||||
],
|
||||
response: String::new(),
|
||||
|
||||
@@ -1199,32 +1199,36 @@ pub(in crate::agent) fn visual_asset_completion_blocker_at_locked(
|
||||
agent_id: &str,
|
||||
required_run_id: Option<&str>,
|
||||
) -> Option<AgentRuntimeToolObservation> {
|
||||
if !editor_api_key_is_configured() {
|
||||
return None;
|
||||
}
|
||||
let (expected_path, expected_kind, label) = match agent_id {
|
||||
"art-director" => (AGENT_RUNTIME_ART_SPEC_PATH, "icon-spec", "统一视觉规范图"),
|
||||
"design-foundation" => ("assets/ui-prototype.png", "ui-prototype", "策划界面原型图"),
|
||||
"art-asset-plan" => (
|
||||
"assets/art-spritesheet.png",
|
||||
"art-spritesheet",
|
||||
"首版美术素材图",
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
let manifest = match read_manifest_for_project(root) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(error) => {
|
||||
return Some(AgentRuntimeToolObservation {
|
||||
tool: "runtime.visual_asset".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: format!("无法核对{label},不能完成任务"),
|
||||
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
|
||||
});
|
||||
// 图片产物由 Codex 按项目需求选择,不再存在固定视觉资产完成门禁。
|
||||
return None;
|
||||
#[allow(unreachable_code)]
|
||||
{
|
||||
if !editor_api_key_is_configured() {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if let Err(error) = validate_manifest_required_visual_asset(root, &manifest, agent_id) {
|
||||
return Some(AgentRuntimeToolObservation {
|
||||
let (expected_path, expected_kind, label) = match agent_id {
|
||||
"art-director" => (AGENT_RUNTIME_ART_SPEC_PATH, "icon-spec", "统一视觉规范图"),
|
||||
"design-foundation" => ("assets/ui-prototype.png", "ui-prototype", "策划界面原型图"),
|
||||
"art-asset-plan" => (
|
||||
"assets/art-spritesheet.png",
|
||||
"art-spritesheet",
|
||||
"首版美术素材图",
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
let manifest = match read_manifest_for_project(root) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(error) => {
|
||||
return Some(AgentRuntimeToolObservation {
|
||||
tool: "runtime.visual_asset".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: format!("无法核对{label},不能完成任务"),
|
||||
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
|
||||
});
|
||||
}
|
||||
};
|
||||
if let Err(error) = validate_manifest_required_visual_asset(root, &manifest, agent_id) {
|
||||
return Some(AgentRuntimeToolObservation {
|
||||
tool: "runtime.visual_asset".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: format!("{label}尚未按正式视觉流程生成并登记,不能完成任务"),
|
||||
@@ -1234,29 +1238,30 @@ pub(in crate::agent) fn visual_asset_completion_blocker_at_locked(
|
||||
redact_agent_runtime_project_paths(root, &error, 300),
|
||||
)),
|
||||
});
|
||||
}
|
||||
if agent_id != "design-foundation" {
|
||||
return None;
|
||||
}
|
||||
match ui_prototype_visual_inspection_blocker_detail_at_locked(
|
||||
root,
|
||||
agent_id,
|
||||
required_run_id,
|
||||
expected_path,
|
||||
) {
|
||||
Ok(None) => None,
|
||||
Ok(Some(detail)) => Some(AgentRuntimeToolObservation {
|
||||
tool: "runtime.visual_asset".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "策划界面原型图尚未通过结构化 UI 视觉检查,不能完成任务".to_string(),
|
||||
detail: Some(detail),
|
||||
}),
|
||||
Err(error) => Some(AgentRuntimeToolObservation {
|
||||
tool: "runtime.visual_asset".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "无法核对策划界面原型图的结构化 UI 视觉证据,不能完成任务".to_string(),
|
||||
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
|
||||
}),
|
||||
}
|
||||
if agent_id != "design-foundation" {
|
||||
return None;
|
||||
}
|
||||
match ui_prototype_visual_inspection_blocker_detail_at_locked(
|
||||
root,
|
||||
agent_id,
|
||||
required_run_id,
|
||||
expected_path,
|
||||
) {
|
||||
Ok(None) => None,
|
||||
Ok(Some(detail)) => Some(AgentRuntimeToolObservation {
|
||||
tool: "runtime.visual_asset".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "策划界面原型图尚未通过结构化 UI 视觉检查,不能完成任务".to_string(),
|
||||
detail: Some(detail),
|
||||
}),
|
||||
Err(error) => Some(AgentRuntimeToolObservation {
|
||||
tool: "runtime.visual_asset".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "无法核对策划界面原型图的结构化 UI 视觉证据,不能完成任务".to_string(),
|
||||
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1756,11 +1756,7 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
|
||||
}
|
||||
|
||||
pub(super) fn autonomous_manifest_ready_task_requires_visual_asset(task_id: &str) -> bool {
|
||||
editor_api_key_is_configured()
|
||||
&& matches!(
|
||||
task_id,
|
||||
"art-director" | "design-foundation" | "art-asset-plan"
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
fn render_autonomous_manifest_ready_task_owner_prompt(task: &GameCreationAppTaskState) -> String {
|
||||
@@ -1777,11 +1773,7 @@ fn render_autonomous_manifest_ready_task_owner_prompt(task: &GameCreationAppTask
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let visual_requirement = if task.id == "art-asset-plan" && editor_api_key_is_configured() {
|
||||
"art-asset-plan 的固定成功路径是:调用 canvas.asset_generate 生成并登记 assets/art-spritesheet.png(assetKind=art-spritesheet),然后调用 asset.list 核对图集及四个 canonical 切片已经登记,再调用 file.write 写入 assets/manifest.art.json;完成这组动作后把结构化计划最后一步标记 completed 并立即交付。不要调用 image.inspect,不要根据图片主观观感发起返工或 agent.message;图集视觉质量由后续质量任务处理,Runtime 会在收束门内验证文件和资产登记状态。"
|
||||
} else {
|
||||
"任务声明中的视觉图片继续按现有 visual gate 生成、登记并验收。"
|
||||
};
|
||||
let visual_requirement = "任务声明中的视觉图片按项目需求选择工具、数量、输出路径、尺寸和布局;需要图集时用 sliceCount 指定切片数量。Runtime 只核对实际声明的资源登记,不要求固定图片合同。";
|
||||
let verification_requirement = match task.id.as_str() {
|
||||
"code-prototype" => "code-prototype 必须对可玩入口执行 game.static_smoke;完整 DAG 的最终静态与浏览器验收继续由后续质量任务承担。",
|
||||
task_id if agent_runtime_autonomous_uses_owner_artifact_validation(task_id) => "完成固定正式产物后直接交付,由 Runtime 在收束门内验证本人固定 owner 产物;禁止调用 game.static_smoke、project.verify、command.run_limited 或 preview 工具冒充 owner 产物验证。",
|
||||
@@ -1810,7 +1802,7 @@ pub(in crate::agent) fn render_autonomous_manifest_ready_task_background_prompt(
|
||||
if task.id == "art-director" {
|
||||
if autonomous_manifest_ready_task_requires_visual_asset(&task.id) {
|
||||
return format!(
|
||||
"{base}\n\n这是 autonomous-game-build 的非只读视觉规范生成任务。{AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER};必须用固定合同生成并登记 assets/art-spec.png(assetKind=icon-spec、aspectRatio=1:1),该受控素材事务会同时提交当前 run 的 mutation 与验证凭证。禁止调用 file.write、file.patch、file.delete、project.patchset、project.restore 或写入其它路径。生成成功后直接交付视觉规范结论;不要调用 task.update,Runtime 会在子 Run 终态后幂等投影 manifest。"
|
||||
"{base}\n\n这是 autonomous-game-build 的视觉方向任务。根据项目需求决定是否调用 canvas.asset_generate,不规定固定图片名称、数量、素材类别或布局;生成成功后直接交付结论。"
|
||||
);
|
||||
}
|
||||
return format!(
|
||||
|
||||
@@ -534,15 +534,7 @@ pub(crate) fn observe_agent_runtime_agent_delegate_at_locked(
|
||||
agent_runtime_tool_input_text(input, &["repairOfDelegationId", "repair_of_delegation_id"]);
|
||||
let repair_of_delegation_id =
|
||||
(!repair_of_delegation_id.is_empty()).then_some(repair_of_delegation_id);
|
||||
let required_visual_artifact = if editor_api_key_is_configured() {
|
||||
match target_agent_id.as_str() {
|
||||
"design-foundation" => Some("assets/ui-prototype.png"),
|
||||
"art-asset-plan" => Some("assets/art-spritesheet.png"),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let required_visual_artifact: Option<&str> = None;
|
||||
if repair_of_delegation_id.is_none()
|
||||
&& required_visual_artifact.is_some_and(|required| {
|
||||
!expected_artifacts
|
||||
|
||||
@@ -416,39 +416,6 @@ pub(in crate::agent) fn observe_agent_runtime_file_delete(
|
||||
return agent_runtime_mutation_gate_failure_observation(root, "file.delete", &error);
|
||||
}
|
||||
}
|
||||
if agent_id == "art-asset-plan" && path == "assets/art-spritesheet.png" {
|
||||
let manifest = match read_existing_manifest_for_project(root) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(error) => {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "file.delete".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "无法确认首版美术素材登记状态,未执行删除".to_string(),
|
||||
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
|
||||
};
|
||||
}
|
||||
};
|
||||
let registered_fixed_asset_exists = manifest.assets.iter().any(|asset| {
|
||||
asset.local_path == "assets/art-spritesheet.png"
|
||||
&& asset.kind == "art-spritesheet"
|
||||
&& asset.media_type.starts_with("image/")
|
||||
&& asset.source.kind == GameCreationAppAssetSourceKind::Canvas
|
||||
&& resolve_local_project_path(root, &asset.local_path)
|
||||
.ok()
|
||||
.is_some_and(|path| path.is_file())
|
||||
});
|
||||
if registered_fixed_asset_exists {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "file.delete".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "首版美术素材已生成并登记,禁止删除固定正式产物".to_string(),
|
||||
detail: Some(
|
||||
"path=assets/art-spritesheet.png · 请复用现有画布资产并核对 assets/manifest.art.json,不得重复生成或扣费"
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
if let Err(error) =
|
||||
prepare_agent_runtime_project_mutation_locked(root, agent_id, run_id, "file.delete")
|
||||
{
|
||||
|
||||
@@ -540,6 +540,11 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
|
||||
.or_else(|| input.get("replace_existing"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let slice_count = input
|
||||
.get("sliceCount")
|
||||
.or_else(|| input.get("slice_count"))
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map(|value| value as usize);
|
||||
let requested_options = PlatformArtAssetGenerationOptions {
|
||||
output_path: (!output_path.trim().is_empty()).then_some(output_path),
|
||||
aspect_ratio,
|
||||
@@ -547,83 +552,9 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
|
||||
asset_kind,
|
||||
asset_label,
|
||||
replace_existing,
|
||||
slice_count,
|
||||
};
|
||||
let canonical_options = match agent_id {
|
||||
"art-director" => Some(PlatformArtAssetGenerationOptions {
|
||||
output_path: Some(AGENT_RUNTIME_ART_SPEC_PATH.to_string()),
|
||||
aspect_ratio: "1:1".to_string(),
|
||||
image_size: "1K".to_string(),
|
||||
asset_kind: "icon-spec".to_string(),
|
||||
asset_label: "游戏统一视觉规范图".to_string(),
|
||||
replace_existing: false,
|
||||
}),
|
||||
"design-foundation"
|
||||
if requested_options
|
||||
.output_path
|
||||
.as_deref()
|
||||
.is_some_and(design_foundation_ui_page_output_path_is_valid) =>
|
||||
{
|
||||
Some(PlatformArtAssetGenerationOptions {
|
||||
output_path: requested_options.output_path.clone(),
|
||||
aspect_ratio: "16:9".to_string(),
|
||||
image_size: "2K".to_string(),
|
||||
asset_kind: "ui-prototype".to_string(),
|
||||
asset_label: if requested_options.asset_label.trim().is_empty() {
|
||||
"游戏功能页面设计图".to_string()
|
||||
} else {
|
||||
requested_options.asset_label.clone()
|
||||
},
|
||||
replace_existing: false,
|
||||
})
|
||||
}
|
||||
"design-foundation" => Some(PlatformArtAssetGenerationOptions {
|
||||
output_path: Some("assets/ui-prototype.png".to_string()),
|
||||
aspect_ratio: "16:9".to_string(),
|
||||
image_size: "2K".to_string(),
|
||||
asset_kind: "ui-prototype".to_string(),
|
||||
asset_label: "游戏横屏界面原型图".to_string(),
|
||||
replace_existing: false,
|
||||
}),
|
||||
"art-asset-plan" => Some(PlatformArtAssetGenerationOptions {
|
||||
output_path: Some("assets/art-spritesheet.png".to_string()),
|
||||
aspect_ratio: "1:1".to_string(),
|
||||
image_size: "1K".to_string(),
|
||||
asset_kind: "art-spritesheet".to_string(),
|
||||
asset_label: "游戏首版核心美术素材".to_string(),
|
||||
replace_existing: false,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
let mut options = if let Some(canonical) = canonical_options {
|
||||
let mismatch = requested_options
|
||||
.output_path
|
||||
.as_deref()
|
||||
.is_some_and(|value| Some(value) != canonical.output_path.as_deref())
|
||||
|| (!requested_options.aspect_ratio.is_empty()
|
||||
&& requested_options.aspect_ratio != canonical.aspect_ratio)
|
||||
|| (!requested_options.image_size.is_empty()
|
||||
&& requested_options.image_size != canonical.image_size)
|
||||
|| (!requested_options.asset_kind.is_empty()
|
||||
&& requested_options.asset_kind != canonical.asset_kind)
|
||||
|| (!requested_options.asset_label.is_empty()
|
||||
&& requested_options.asset_label != canonical.asset_label);
|
||||
if mismatch {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "canvas.asset_generate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: format!(
|
||||
"图片产物型专业任务不能覆盖固定输出合同:outputPath={} · aspectRatio={} · imageSize={} · assetKind={} · assetLabel={}",
|
||||
canonical.output_path.as_deref().unwrap_or("null"),
|
||||
canonical.aspect_ratio,
|
||||
canonical.image_size,
|
||||
canonical.asset_kind,
|
||||
canonical.asset_label,
|
||||
),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
canonical
|
||||
} else {
|
||||
let mut options = {
|
||||
let defaults = PlatformArtAssetGenerationOptions::default();
|
||||
PlatformArtAssetGenerationOptions {
|
||||
output_path: requested_options.output_path,
|
||||
@@ -648,6 +579,7 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
|
||||
requested_options.asset_label
|
||||
},
|
||||
replace_existing,
|
||||
slice_count,
|
||||
}
|
||||
};
|
||||
options.replace_existing = replace_existing;
|
||||
|
||||
@@ -242,6 +242,27 @@ pub(crate) fn render_agc_skill_pack_index() -> Result<String, String> {
|
||||
Ok(lines.join("\n"))
|
||||
}
|
||||
|
||||
pub(crate) fn read_agc_skill_resource(resource: &str) -> Result<String, String> {
|
||||
let manifest = validated_skill_pack_manifest()?;
|
||||
let normalized = resource.trim().trim_start_matches('/').replace('\\', "/");
|
||||
let (skill_name, relative) = normalized
|
||||
.split_once('/')
|
||||
.ok_or_else(|| "Skill 资源路径必须是 skill/file".to_string())?;
|
||||
let entry = manifest
|
||||
.skills
|
||||
.iter()
|
||||
.find(|entry| entry.name == skill_name)
|
||||
.ok_or_else(|| "未登记的 AGC Skill 资源".to_string())?;
|
||||
if !entry.files.iter().any(|file| file == relative) || !is_safe_skill_relative_path(relative) {
|
||||
return Err("未登记或不安全的 AGC Skill 资源".to_string());
|
||||
}
|
||||
let bundled_path = format!("{skill_name}/{relative}");
|
||||
let bytes =
|
||||
bundled_skill_file(&bundled_path).ok_or_else(|| "AGC Skill 资源不存在".to_string())?;
|
||||
let canonical = canonical_skill_text_bytes(&bundled_path, bytes)?;
|
||||
String::from_utf8(canonical.into_owned()).map_err(|_| "AGC Skill 资源不是 UTF-8".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn install_agc_skill_pack(isolated_os_home: &Path) -> Result<String, String> {
|
||||
let manifest = validated_skill_pack_manifest()?;
|
||||
let skills_root = isolated_os_home.join(".agents").join("skills");
|
||||
|
||||
@@ -1386,7 +1386,7 @@ fn runtime_tool_description(tool: &str) -> &'static str {
|
||||
"preview.validate" => "用真实浏览器验证桌面和移动预览并保存证据。",
|
||||
"image.inspect" => "让视觉模型检查一至两张项目内图片。",
|
||||
"canvas.asset_generate" => {
|
||||
"通过已配置的 External Editor API 生成图片并登记到画布、素材库和项目 assets;art-director 先生成 icon-spec 规范图,ui-prototype 与透明 art-spritesheet 都固定复用该规范图;只有唯一返工委派可显式替换已登记正式图片。"
|
||||
"通过已配置的 External Editor API 按项目需求生成图片或图集并登记到画布、素材库和项目 assets;可使用已登记资源作为参考,也可通过 sliceCount 指定图集切片数量。"
|
||||
}
|
||||
"ui.workflow.run" => {
|
||||
"先用 discover 从受控 game/ui-pages.json 或页面声明标记自动发现全部功能页面,再把已登记 ui-prototype 与每个页面的设计图桥接成独立 UI JSON State;可同时载入已登记图片、图标和项目字体,执行 Provider 结构识别、多树合并与分批组件绑定、回读阶段,并且只有所有页面已绑定且已应用到 game/ 后才允许 finalize。项目根目录由 Runtime 注入,模型不得传入宿主路径。"
|
||||
|
||||
@@ -2484,6 +2484,8 @@ fn main() {
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
start_game_creator_external_mcp,
|
||||
stop_game_creator_external_mcp,
|
||||
create_automatic_local_game_project,
|
||||
init_local_game_project,
|
||||
import_local_godot_project,
|
||||
|
||||
@@ -1165,6 +1165,7 @@ async fn canonical_art_spec_and_ui_requests_use_the_shared_reference_chain() {
|
||||
asset_kind: "ui-prototype".to_string(),
|
||||
asset_label: "游戏横屏界面原型图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -4856,6 +4857,7 @@ fn ui_prototype_generation_uses_dedicated_prompt_and_art_spec() {
|
||||
asset_kind: "ui-prototype".to_string(),
|
||||
asset_label: "游戏横屏界面原型图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
};
|
||||
let prompt = build_platform_art_asset_prompt(
|
||||
"原创网格贪吃蛇:分数与状态 HUD、四类不同分值食物、开始、方向键/WASD、触控方向键、失败与重开",
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
isBackendReady,
|
||||
isProcessGroupAlive,
|
||||
preflightExistingVite,
|
||||
readBackendServiceFailure,
|
||||
readLinuxProcessGroupAlive,
|
||||
resolveBackendTargetsFromState,
|
||||
runWindowsTaskkill,
|
||||
spawnChild,
|
||||
stopChild,
|
||||
terminateChildTree,
|
||||
waitForBackendReady,
|
||||
waitForChildTermination,
|
||||
} from '../scripts/start-dev-stack.mjs';
|
||||
|
||||
@@ -26,6 +28,7 @@ function backendState(spacetimeDataDir?: string, includeBgfilterWorker = true) {
|
||||
return {
|
||||
schemaVersion: spacetimeDataDir ? 2 : 1,
|
||||
database: expectedDatabase,
|
||||
updatedAt: '',
|
||||
...(spacetimeDataDir ? { spacetimeDataDir } : {}),
|
||||
services: {
|
||||
'api-server': {
|
||||
@@ -127,6 +130,47 @@ describe('AI 游戏创作配套后端复用门禁', () => {
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
test('后端服务失败时返回具体失败服务,避免外层无限等待', () => {
|
||||
const state = backendState(expectedDataDir);
|
||||
state.services['bgfilter-worker'].status = 'failed';
|
||||
state.services['bgfilter-worker'].exitCode = 1;
|
||||
state.services['bgfilter-worker'].signal = null;
|
||||
|
||||
expect(readBackendServiceFailure(state)).toEqual({
|
||||
serviceName: 'bgfilter-worker',
|
||||
failure: 'code=1',
|
||||
});
|
||||
});
|
||||
|
||||
test('不匹配的旧状态失败记录不会阻断当前后端启动', () => {
|
||||
const state = backendState(resolve('server-rs/.spacetimedb/other/data'));
|
||||
state.services['bgfilter-worker'].status = 'failed';
|
||||
state.services['bgfilter-worker'].exitCode = 1;
|
||||
|
||||
expect(readBackendServiceFailure(state)).toBeNull();
|
||||
});
|
||||
|
||||
test('等待后端时立即传播状态文件中的服务失败', async () => {
|
||||
const initialState = backendState(expectedDataDir);
|
||||
initialState.updatedAt = '2026-09-04T08:00:00.000Z';
|
||||
const state = backendState(expectedDataDir);
|
||||
state.updatedAt = '2026-09-04T08:00:01.000Z';
|
||||
state.services['bgfilter-worker'].status = 'failed';
|
||||
state.services['bgfilter-worker'].exitCode = 98;
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
exitCode: null,
|
||||
signalCode: null,
|
||||
});
|
||||
let readCount = 0;
|
||||
|
||||
await expect(
|
||||
waitForBackendReady(child, 100, {
|
||||
checkBackendReady: async () => false,
|
||||
readState: () => (readCount++ === 0 ? initialState : state),
|
||||
}),
|
||||
).rejects.toThrow('配套后端启动失败: bgfilter-worker code=98');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI 游戏创作启动子进程生命周期', () => {
|
||||
|
||||
@@ -3372,8 +3372,14 @@
|
||||
},
|
||||
"sliceLayout": {
|
||||
"type": "string",
|
||||
"enum": ["grid-2x2"],
|
||||
"description": "可选固定图集切片合同。省略时沿用全图 alpha 连通域自动拆分;传 grid-2x2 时服务端要求生成四个固定象限,并按左上、右上、左下、右下各持久化一个独立切片。该模式适用于需要恰好四类核心运行时素材的游戏,不会猜测等分裁切。"
|
||||
"deprecated": true,
|
||||
"description": "历史兼容字段,新的调用请使用 sliceCount。"
|
||||
},
|
||||
"sliceCount": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"description": "可选的目标切片数量;省略时按图像内容自动识别。"
|
||||
},
|
||||
"screenColor": {
|
||||
"type": ["string", "null"],
|
||||
@@ -3597,15 +3603,21 @@
|
||||
},
|
||||
"iconImageSrcs": {
|
||||
"type": "array",
|
||||
"description": "默认模式识别图集中全部有效 alpha 连通域并持久化的独立素材,按视觉阅读顺序命名为“素材 N”;数量由图集内容决定,不由 iconDescriptions 数量决定。sliceLayout=grid-2x2 时固定返回左上、右上、左下、右下四个格子的切片,各格内的零散视觉细节不会被拆成额外素材。",
|
||||
"description": "识别图集中有效 alpha 连通域并持久化的独立素材,按视觉阅读顺序命名为“素材 N”;可通过 sliceCount 指定目标数量。",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EditorIconSpritesheetIconResult"
|
||||
}
|
||||
},
|
||||
"sliceLayout": {
|
||||
"type": "string",
|
||||
"enum": ["grid-2x2"],
|
||||
"description": "仅当请求使用固定切片合同且主图完成透明化、切片持久化后返回。调用方可将该字段与 iconImageSrcs=4 共同作为固定四类素材的来源证明。"
|
||||
"deprecated": true,
|
||||
"description": "历史兼容字段。"
|
||||
},
|
||||
"sliceCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"description": "实际生成的切片数量。"
|
||||
},
|
||||
"sliceWarning": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -246,6 +246,13 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创
|
||||
- 旧配置迁移:既有 AppData 若没有 `agentMode`,只有全局和逐 Agent 路由均为 `openai_responses` 时迁移到 `codex_app_server`;存在 `openai_chat / anthropic` 时显式保留 `provider`,避免打开项目自动恢复时把所有节点批量写成 `invalid-config`。用户确认端点支持 Responses 后,可在设置中显式切换并保留原 model/base URL/API Key。
|
||||
- 验收:fake JSON-RPC fixture、三态 UI/config、配置指纹、unknown-terminal 零重放、旧两种模式回归和显式 ignored 真实 smoke 全部通过后,才可视为模式切换完成。
|
||||
|
||||
### 2026-09-03 AGC 客户端能力以 MCP 暴露
|
||||
|
||||
- MCP 暴露是客户端能力层,不替换 `codex_app_server / codex_cli / provider` 或客户端对话。客户端对话入口继续驱动 Codex app-server;app-server 通过客户端随附的 stdio MCP 子进程调用审核后的客户端能力。客户端不再替 Codex 做业务语义门禁、意图判断和完成判定。
|
||||
- MCP 会话在客户端握手时绑定当前账号、项目和实例,工具参数不得携带 `projectPath`、Token、Cookie、objectKey 或内部 URL。仅暴露稳定业务白名单与 `resources/list/read`,所有文件、资源、画布、预览和 operation 副作用继续复用客户端权限、锁、计费、幂等账本、manifest/revision 与恢复机制。
|
||||
- 客户端对话继续写入现有 conversation projection;外部 Host 如需旁路保存返回文本,可显式调用 `conversation.record_codex_response`。客户端将有界、脱敏正文、SHA-256、安全摘要和状态追加到项目级 journal,UI 只展示记录,不从文本推断业务状态或触发副作用。
|
||||
- MCP 子进程只由当前客户端为绑定项目启动,并在该项目工作目录内运行;客户端回合结束或客户端退出后子进程随 Codex app-server 一并回收。账号和项目权限仍由客户端业务桥接层校验,未知副作用保持 `needs-reconciliation`,只能通过 operation 查询恢复。稳定验收覆盖客户端对话驱动的 MCP 工具调用、Skill 指导资源、跨项目/账号拒绝以及旧 Provider/Codex 回归。
|
||||
|
||||
### 2026-08-10 Supervisor 边做边聊与条件中断
|
||||
|
||||
- 根 Project Supervisor 的运行中消息继续进入当前 `taskId / sessionId / runId`,先持久显示“正在判断、当前任务继续”,再由独立 LLM 生成非终态语义回复并给出 `interruptCurrentProvider`。过程回复不能调用终态 `respond_to_user`,不能把制作 Run、Goal 或 task 提前完成。
|
||||
@@ -1075,7 +1082,7 @@ game-project/
|
||||
- 内部 owner 验证只接受 GUI / CLI 完整 16 任务 DAG 中 `agent-ready-task-scheduler` 启动的确定性直接 child、当前活跃根和完整 project/source/profile/Agent/run/parent/root/binding 身份。错误 source、delegated run、历史或终态根、非当前活跃根、跨 Agent/run 凭证均失败关闭;再次 mutation 使旧凭证失效,相同身份恢复可按当前事实确定性重验。本阶段不扩到后置 `publish-package`。`code-prototype` 与 `preview-readiness` 继续执行真实 `game.static_smoke`,`preview-playtest` 继续独立执行浏览器验收;任何 owner 文件凭证都不能替代可玩证据。
|
||||
- `design-foundation` 的 2026-07-26 职责隔离继续有效:项目文件仍只允许 `memory/project.md`、`game/game_design.md` 和配置 Key 时的固定 `assets/ui-prototype.png`,禁止修改 `game/index.html`、调用 smoke / preview / process 或恢复整项目。未配置 External Editor API Key 时 `art-director` 保持只读协调;配置 Key 时它是条件 Canvas owner,必须生成并登记 `assets/art-spec.png`,成功 `canvas.asset_generate` 为本人当前 revision 形成普通验证凭证,不能被只读分类吞掉。配置 Key 时 UI 原型、透明图集、Canvas 登记和视觉门仍按既有合同执行,内部 owner 文件验证不替代图片证据。
|
||||
- `canvas.asset_generate.replaceExisting` 默认并必须保持 `false`;只有静态专业 Agent 的 `delegated-*` 唯一 repair run 才能申请 `true`。Runtime 要求当前 delivery 带 `repairOfDelegationId`,原 delivery 已被同一父 Agent / 父 run 认领,原始与返工合同的目标 Agent 和精确 `expectedArtifacts` 路径一致;普通 run、未声明路径、错误 Agent、未认领原交付或缺失原图都失败关闭。图片生成仍服从 `art-director` / `design-foundation` / `art-asset-plan` 的固定输出路径、比例、尺寸、kind 和 label,禁止先删除正式图片;请求前记录旧文件 SHA-256,外部生成返回后在项目写锁内复核,旧图在网络请求期间变化即拒绝覆盖。授权替换先写私有临时文件,再以备份 / rename 切换;落盘或 manifest 登记失败时恢复旧图,不把新旧文件并存状态当作成功。
|
||||
- 在既有 16-task manifest 内固定正式视觉 DAG,不新增平行任务系统:`art-director` 用当前调用模式的图片生成 `kind=spec` 生成 `assets/art-spec.png` 并登记为 `assetKind=icon-spec`;`design-foundation` 使用该规范图的稳定资源 ID 作为视觉规范参考,用同模式图片生成 `kind=ui-design` 生成 `assets/ui-prototype.png`;`art-asset-plan` 以同一 resource ID 调用同模式图标 spritesheet 生成,产出透明 `assets/art-spritesheet.png`。普通模式使用内部 `/api/editor/*`,standalone/高级模式使用对应 `/api/external/v1/*`;业务请求、依赖和验收完全一致。规范图缺失、未登记或缺少稳定资源 ID 时,下游任务不得退回普通生图。图集 warning、透明像素与切片门禁保持不变。
|
||||
- 视觉 Agent 只负责指导 Codex 选择合适的图片/编辑/图集工具并提供项目上下文,不再固定图片数量、文件槽位、素材类别或 spritesheet 布局;请求可按玩法需要生成单图、多图或任意切片布局。普通模式使用内部 `/api/editor/*`,standalone/高级模式使用对应 `/api/external/v1/*`;权限、计费、幂等、资源登记和安全校验保持不变。
|
||||
- 旧项目已有同路径派生图但缺少上述 provenance 时,一律标记为 legacy,不得只因文件、kind 或通用视觉检查存在就完成。原位替换仍走显式 repair:`design-foundation` 与 `art-asset-plan` 先在同一 Supervisor 批次分别建立 owner 精确原合同并交付 `needs-repair`,父 run 认领后再在同一批次分别发起各自唯一 repair;两个 repair 合称一个显式视觉返工阶段。`art-director` 不得跨 owner 声明或替换 UI / spritesheet,Runtime 在委派落盘前就拒绝这类合同,不再等到生图阶段才失败。
|
||||
- 2026-07-27 新起的“16 任务正式产物 + 两张真实画布图片 + current revision 静态 / 双视口浏览器 / PNG 证据 + 受限 repair 替换”独立外部 Provider 验收,使用 `npm run agc:test:chat -- --timeout-minutes 75`,约 `59m50s` 后以退出码 `0` 完整 **PASS**。同一轮真实生成并登记 `assets/ui-prototype.png`(`2829418` bytes)与 `assets/art-spritesheet.png`(`1361906` bytes),固定 `16` 个 manifest task 均为当前父 Run 下唯一 logical run、一次 started、一次 completed、零 failed / cancelled 和一次 manifest projection;七份基础正式产物、两张 PNG、当前 revision 的 `game.static_smoke`、desktop / mobile `lane-defense-v1` playtest、浏览器报告与截图全部通过。`turn.report=settled` 且唯一 assistant,busy / pending / running / confirmation / user-input / reconciliation 均为 `0`;隔离 Runner、一次性项目和隔离 AppData 已自动清理。此前失败轮继续独立保留,不与本轮拼接;未来合同变化仍须新起完整轮次复验。
|
||||
- 2026-07-27 补充 tool-plan 成功响应交接的内容边界:Provider 的自然语言计划叙述,以及结构化 arguments 中 `body / code / content / css / html / newText / oldText / patch / script / text` 等源码内容字段,只检查真实密钥 token 形状、凭据头标记和不安全控制字符;仅仅提及 `.env` 或 `game-creator.config` 不能阻断已经计费的安全响应。结构化输入中的敏感 JSON key、非内容字段中的配置痕迹或绝对路径、真实 token、容量、thinking、身份、顺序和账本完整性门禁仍失败关闭。成功 handoff 失败进入 reconciliation 时,Runtime 额外只持久化受控 `failureKind`、脱敏错误 SHA-256 和字符数,不保存 Provider 正文、function arguments、密钥或绝对路径。定向回归覆盖叙述/源码字段放行、`.env.local` 路径和真实 token 拒绝、全部 tool-plan handoff 回归及诊断零正文。
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
|
||||
## 3. 已确定的产品边界
|
||||
|
||||
### 3.0 客户端能力 MCP 暴露边界(2026-09-03)
|
||||
|
||||
客户端仍由现有对话入口启动并驱动 Codex;MCP 只是把客户端已审核的项目、文件、资源、画布、生成和预览能力暴露给该 Codex 或其它 Host。客户端只负责账号、项目路径、权限、计费、幂等、锁和恢复等自身安全,不替 Codex 做高层意图/完成门禁。审核 Skill 的索引和正文可作为只读 MCP resource 提供,第三方扩展不得获得客户端会话凭据、内部路径或 bridge token;该能力与公网 `/api/external/v1/mcp` 保持独立。
|
||||
|
||||
### 3.1 客户端安装、运行时注入
|
||||
|
||||
- 扩展内容保存在 AGC 客户端的扩展仓库。
|
||||
|
||||
@@ -89,6 +89,10 @@ Codex app-server 协议里,`commandExecution.commandActions` 已分类为 `Rea
|
||||
|
||||
不升级 `GAME_CREATOR_AGENT_DB_SCHEMA_VERSION`;新 `recordType` 走 Ordinary 追加。`updatedAt` / `schemaVersion` 仍由 `serialize_agent_db_record` 写入。
|
||||
|
||||
### 2026-09-03 客户端对话与 MCP 能力边界
|
||||
|
||||
客户端对话仍由现有 Codex app-server 链路完成;MCP 只暴露客户端自身业务能力和审核 Skill 指导。客户端安全门禁限于账号、项目路径、权限、计费、幂等、锁、revision 与恢复,不根据 Codex 自然语言替代 Codex 决定业务动作。外部 Host 返回如需旁路归档,可使用显式记录工具,但不替代现有 conversation projection,也不触发资源、状态或完成判定。
|
||||
|
||||
jsonl 每条自带 `recordedAtMs`(`unix_millis`)。同一 `clientTurnId` 若再次进入(当前 GUI 运行中互斥,结束后理论上可再来):只追加,不截断;后一次 `turn_start` 视为新 attempt。读摘要时按文件内最后一次 `turn_start` 到对应 `turn_end` 计算 `offeredRead`。`agent.db` 每次 `turn_end` 再追加一条摘要,分析取该 `clientTurnId` 最后一条。
|
||||
|
||||
## 5. 记录合同
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
### 失败生成任务归档与任务侧栏
|
||||
|
||||
- 用户界面的“删除失败任务”语义是归档,不物理销毁私有 generation ledger。只有平台明确失败的 `failed` 任务可归档;`reconciliation-required`、已受理、运行中和结果未知任务不得移出恢复队列。
|
||||
- 已受理 operation 在重试时若请求快照发生变化,客户端可先对原 operation 执行一次只读状态查询;仅当平台明确返回 `failed` 时才自动收口旧账本并允许新提交,排队、运行中、完成或未知状态继续保持原幂等身份并阻断替代请求。
|
||||
- 归档命令校验 project、draft、generation 与 expected draft revision,先把私有 ledger 写入可重放的 `archiving/archivedAt`,再从 `draft.generations` 移除公开投影并推进一次 revision;草稿删除成功并回读后才发布 `archived`。`archiving` 以及历史上已写 `archived` 但仍残留公开记录的状态都必须在恢复阶段幂等收敛,且不依赖图片生成服务凭证。
|
||||
- 失败占位和右上角任务项复用同一个归档动作,成功后两处同时消失,其它任务、图层和候选不受影响。
|
||||
- 任务侧栏折叠只属于当前会话 UI 状态,不写入 draft 或 manifest。视觉和交互复用现役美术画布:右上角独立“任务列表”图标按钮、20rem 白色模糊卡、总数徽标、`排队/生成中` 与 `已完成` 双 Tab、状态圆形图标、阶段进度和时间信息;折叠后只保留图标按钮,不显示摘要卡。用户显式新建 generation 时自动展开并切回活动 Tab,普通进度更新不得推翻用户已有折叠选择。Game Agent 的失败归档作为任务行扩展保留。
|
||||
|
||||
@@ -62,7 +62,7 @@ Linux 本机多用户并发开发时,`npm run dev`、`npm run dev:*` 单模块
|
||||
|
||||
后端日志默认写入 `logs/api-server/`,独立 BgFilter worker 日志默认写入 `logs/bgfilter-worker/`。后端 API smoke 使用 `npm run dev:api-server`,先检查 BgFilter worker `/readyz`,再检查 API `/healthz`;需要确认 API 实例可接生产流量时检查 API `/readyz`。不要使用旧 `api-server:maincloud` 或任何 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 口径。
|
||||
|
||||
AI 游戏创作客户端使用 `npm run agc`。该入口由 `apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs` 解析 AGC Vite 实际端口:Linux 默认取当前用户端口段的 `start + 5`,占用时只在本用户段内漂移;Windows / macOS 保留 `3080` 为兼容首选并允许统一漂移。最终端口通过 `GENARRATIVE_AGC_VITE_PORT` 传给 `beforeDevCommand` 和配套后端端口解析器,通过 Tauri CLI 动态 `build.devUrl` 配置传给 WebView,并通过 Vite CLI `--port` 启动严格监听;Vite 继续使用 `strictPort`,任何一层都不得自行改到另一个端口。AGC 配套后端的 `backend` 模式启动 SpacetimeDB、独立 `bgfilter-worker` 和 `api-server`,并在复用现有后端前同时检查三者状态及 `/v1/ping`、`/readyz`、`/healthz`;worker 缺失时不得把不完整的 API/数据库组合误判为 ready。启动器在创建原生窗口前预检最终地址;若竞态中该地址被 AGC Vite、无响应监听器或其它服务占用,一律失败关闭,不复用、也不擅自终止无法证明归属的进程。
|
||||
AI 游戏创作客户端使用 `npm run agc`。该入口由 `apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs` 解析 AGC Vite 实际端口:Linux 默认取当前用户端口段的 `start + 5`,占用时只在本用户段内漂移;Windows / macOS 保留 `3080` 为兼容首选并允许统一漂移。最终端口通过 `GENARRATIVE_AGC_VITE_PORT` 传给 `beforeDevCommand` 和配套后端端口解析器,通过 Tauri CLI 动态 `build.devUrl` 配置传给 WebView,并通过 Vite CLI `--port` 启动严格监听;Vite 继续使用 `strictPort`,任何一层都不得自行改到另一个端口。AGC 配套后端的 `backend` 模式启动 SpacetimeDB、独立 `bgfilter-worker` 和 `api-server`,并在复用现有后端前同时检查三者状态及 `/v1/ping`、`/readyz`、`/healthz`;worker 缺失时不得把不完整的 API/数据库组合误判为 ready。任一配套服务在启动阶段进入 `failed` 时,外层启动器必须立即报告具体服务和退出原因,不能继续等待前端地址超时。启动器在创建原生窗口前预检最终地址;若竞态中该地址被 AGC Vite、无响应监听器或其它服务占用,一律失败关闭,不复用、也不擅自终止无法证明归属的进程。
|
||||
|
||||
Tauri `beforeDevCommand` 默认与客户端构建并行,不能把上述检查只放在 `beforeDevCommand` 内:选定地址上若已有旧 Vite,Tauri 可能先创建加载旧前端的窗口,随后配套后端才因代理不匹配退出。外层启动器会把 Tauri CLI 放入受控进程树;CLI 正常退出、启动失败或收到终止信号后,POSIX 先向保留的 PGID 发送 `SIGTERM`、有界等待后升级 `SIGKILL`,Windows 使用 `taskkill /PID <pid> /T /F`。Linux 容器中的孤儿后代退出后可能暂时保留为 zombie,`kill(-PGID, 0)` 仍会返回成功;启动器必须结合 `/proc/<pid>/stat` 判断同组是否还存在非 zombie 成员,不能把等待 PID 1 回收误报为清理失败。配套后端和 Vite 仍由 `start-dev-stack.mjs` 各自持有,退出时同样有界收束,避免只剩客户端、Runner、Cargo 或旧订阅进程。排障时同时核对控制台输出的 AGC Vite 实际地址及其 marker、`.app/dev-stack.json` 的实际 API URL 和进程 cwd;不要把“终端已返回”当成客户端及其 Runner 已退出的证据。
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user