重整AGC官方路由与安全文件链路

记录当前 AGC 客户端、Tauri、API Server、后台查询和契约改动

保留后续按新 Router 后端方案重构的检查点
This commit is contained in:
2026-08-30 14:21:18 +08:00
parent 63abea0b3e
commit 93477f2b1b
104 changed files with 7715 additions and 2283 deletions
+35
View File
@@ -23,6 +23,8 @@ import type {
AdminEditorShowcaseListQuery,
AdminEditorShowcaseListResponse,
AdminEditorShowcaseReviewRequest,
AdminExternalApiKeyListQuery,
AdminExternalApiKeyListResponse,
AdminFeatureGateConfigResponse,
AdminLoginResponse,
AdminMeResponse,
@@ -246,6 +248,16 @@ export function getAdminDatabaseTableRows(
);
}
export function getAdminExternalApiKeys(
token: string,
query: AdminExternalApiKeyListQuery = {},
) {
return request<AdminExternalApiKeyListResponse>(
`/admin/api/external-api-keys${buildExternalApiKeyQuery(query)}`,
{ token },
);
}
export function debugAdminHttp(token: string, payload: AdminDebugHttpRequest) {
return request<AdminDebugHttpResponse>('/admin/api/debug/http', {
method: 'POST',
@@ -861,6 +873,29 @@ function buildDatabaseTableRowsQuery(query: AdminDatabaseTableRowsQuery) {
return queryString ? `?${queryString}` : '';
}
function buildExternalApiKeyQuery(query: AdminExternalApiKeyListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'ownerUserId', query.ownerUserId);
appendQueryParam(params, 'publicUserCode', query.publicUserCode);
appendQueryParam(params, 'keyId', query.keyId);
appendQueryParam(params, 'name', query.name);
appendQueryParam(params, 'keyPrefix', query.keyPrefix);
appendQueryParam(params, 'createdAfter', query.createdAfter);
appendQueryParam(params, 'createdBefore', query.createdBefore);
appendQueryParam(params, 'status', query.status);
appendQueryParam(params, 'purpose', query.purpose);
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
params.set('limit', String(Math.floor(query.limit)));
}
if (typeof query.offset === 'number' && Number.isFinite(query.offset)) {
params.set('offset', String(Math.floor(query.offset)));
}
appendQueryParam(params, 'sortColumn', query.sortColumn);
appendQueryParam(params, 'sortDirection', query.sortDirection);
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function buildEditorAssetListQuery(query: AdminEditorAssetListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'cursor', query.cursor);
+48
View File
@@ -243,6 +243,54 @@ export interface AdminDatabaseTableStatPayload {
errorMessage: string | null;
}
export interface AdminExternalApiKeyListQuery {
ownerUserId?: string;
publicUserCode?: string;
keyId?: string;
name?: string;
keyPrefix?: string;
createdAfter?: string;
createdBefore?: string;
status?: 'active' | 'revoked';
purpose?: 'external-editor' | 'agc-llm';
limit?: number;
offset?: number;
sortColumn?:
| 'keyId'
| 'ownerUserId'
| 'name'
| 'keyPrefix'
| 'purpose'
| 'createdAt'
| 'lastUsedAt'
| 'updatedAt';
sortDirection?: 'asc' | 'desc';
}
export interface AdminExternalApiKeyPayload {
keyId: string;
ownerUserId: string;
name: string;
keyPrefix: string;
purpose: string;
scopes: string[];
createdAt: string;
lastUsedAt: string | null;
revokedAt: string | null;
updatedAt: string;
status: 'active' | 'revoked';
}
export interface AdminExternalApiKeyListResponse {
keys: AdminExternalApiKeyPayload[];
total: number;
limit: number;
offset: number;
scannedCount: number;
scanLimit: number;
scanLimitReached: boolean;
}
export interface AdminDebugHeaderInput {
name: string;
value: string;
@@ -7,6 +7,7 @@ import { beforeEach, expect, test, vi } from 'vitest';
import {
getAdminDatabaseTableRows,
getAdminDatabaseTables,
getAdminExternalApiKeys,
} from '../api/adminApiClient';
import {
AdminDatabaseTablesPage,
@@ -19,11 +20,15 @@ vi.mock('../api/adminApiClient', () => ({
),
getAdminDatabaseTableRows: vi.fn(),
getAdminDatabaseTables: vi.fn(),
getAdminExternalApiKeys: vi.fn(),
isAdminApiError: vi.fn(() => false),
}));
vi.mock('../components/AdminUserReferenceButton', () => ({
AdminUserReferenceButton: ({ userId, publicUserCode }: {
AdminUserReferenceButton: ({
userId,
publicUserCode,
}: {
userId?: string;
publicUserCode?: string;
}) => (
@@ -69,6 +74,7 @@ const referralRows = [
beforeEach(() => {
window.location.hash = '#tables?table=profile_referral_relation';
vi.mocked(getAdminExternalApiKeys).mockReset();
vi.mocked(getAdminDatabaseTables).mockResolvedValue({
fetchErrors: [],
tables: ['profile_referral_relation'],
@@ -87,6 +93,56 @@ beforeEach(() => {
});
});
test('external_api_key 使用专用安全查询且详情不展示原始 JSON', async () => {
const user = userEvent.setup();
window.location.hash = '#tables?table=external_api_key';
vi.mocked(getAdminDatabaseTables).mockResolvedValue({
fetchErrors: [],
tables: ['external_api_key'],
});
vi.mocked(getAdminExternalApiKeys).mockResolvedValue({
keys: [
{
keyId: 'external-api-key-1',
ownerUserId: 'user-1',
name: '陶泥儿 AGC 官方 LLM(本机)',
keyPrefix: 'tnr_sk_fixture',
purpose: 'agc-llm',
scopes: ['llm:responses'],
createdAt: '2026-08-29T00:00:00Z',
lastUsedAt: null,
revokedAt: null,
updatedAt: '2026-08-29T00:00:00Z',
status: 'active',
},
],
total: 1,
limit: 100,
offset: 0,
scannedCount: 1,
scanLimit: 5000,
scanLimitReached: false,
});
render(
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await user.type(screen.getByPlaceholderText('精确 ownerUserId'), 'user-1');
await user.click(screen.getByRole('button', { name: '安全查询' }));
await waitFor(() => {
expect(getAdminExternalApiKeys).toHaveBeenLastCalledWith(
'admin-token',
expect.objectContaining({ ownerUserId: 'user-1', purpose: undefined }),
);
});
expect(await screen.findByText('tnr_sk_fixture')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '详情' }));
expect(screen.getByRole('dialog')).toBeTruthy();
expect(screen.queryByText('复制 JSON')).toBeNull();
expect(screen.queryByText('key_hash')).toBeNull();
});
test('后台表查询页通过页面级固定栏翻页并提示扫描结果可能不完整', async () => {
const user = userEvent.setup();
vi.mocked(getAdminDatabaseTableRows).mockResolvedValue({
@@ -124,7 +180,10 @@ test('后台表查询页通过页面级固定栏翻页并提示扫描结果可
),
).toBeTruthy();
await user.type(screen.getByRole('textbox', { name: '关键词' }), '未执行条件');
await user.type(
screen.getByRole('textbox', { name: '关键词' }),
'未执行条件',
);
await user.click(screen.getByRole('button', { name: '下一页' }));
await waitFor(() => {
@@ -227,7 +286,9 @@ test('数据库用户字段显示查看按钮且点击不会打开行详情', as
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
);
const userButton = await screen.findByRole('button', { name: '查看用户 u-b' });
const userButton = await screen.findByRole('button', {
name: '查看用户 u-b',
});
await user.click(userButton);
expect(screen.queryByRole('dialog')).toBeNull();
});
@@ -250,7 +311,11 @@ test('数据库用户字段识别会排除后台操作者与合成邀请码字
resolveAdminDatabaseUserReference('audit_log', 'admin_user_id', 'u-1'),
).toBeNull();
expect(
resolveAdminDatabaseUserReference('profile_wallet', 'user_id', 'admin:root'),
resolveAdminDatabaseUserReference(
'profile_wallet',
'user_id',
'admin:root',
),
).toBeNull();
expect(
resolveAdminDatabaseUserReference('profile_invite_code', 'user_id', 'u-1'),
File diff suppressed because it is too large Load Diff
@@ -50,6 +50,18 @@ struct CodexPendingRpc {
}
enum CodexAppServerCredential {
PlatformSession {
api_base_url: String,
access_token: String,
fingerprint: String,
},
AccountKey {
route_origin: String,
api_key: String,
key_id: String,
storage_path: std::path::PathBuf,
fingerprint: String,
},
AppDataKey {
fingerprint: String,
},
@@ -63,7 +75,10 @@ enum CodexAppServerCredential {
impl CodexAppServerCredential {
fn fingerprint(&self) -> &str {
match self {
Self::AppDataKey { fingerprint } | Self::AuthBridge { fingerprint, .. } => fingerprint,
Self::PlatformSession { fingerprint, .. }
| Self::AccountKey { fingerprint, .. }
| Self::AppDataKey { fingerprint }
| Self::AuthBridge { fingerprint, .. } => fingerprint,
}
}
@@ -89,6 +104,12 @@ impl CodexAppServerCredential {
llm: &'a GameCreatorLlmConfig,
) -> Option<(&'a str, &'a str)> {
match self {
Self::PlatformSession { .. } => None,
Self::AccountKey {
route_origin,
api_key,
..
} => Some((route_origin.as_str(), api_key.as_str())),
Self::AppDataKey { .. } => (!llm.api_key.trim().is_empty())
.then_some((llm.base_url.trim_end_matches('/'), llm.api_key.trim())),
Self::AuthBridge { api_key, .. } => api_key
@@ -437,7 +458,9 @@ pub(super) fn resolve_direct_codex_project_authority(
if !project_root.is_absolute() {
return Err("AGC 直连项目根目录必须是绝对路径".to_string());
}
let project_metadata = std::fs::metadata(project_root)
crate::prepare_game_creator_private_path_for_read(project_root, true, "AGC 直连项目根目录")
.map_err(|error| format!("AGC 直连项目根目录无法安全访问:{error}"))?;
let project_metadata = std::fs::symlink_metadata(project_root)
.map_err(|_| "AGC 直连项目根目录不存在或无法读取".to_string())?;
if !project_metadata.is_dir() {
return Err("AGC 直连项目根目录不是目录".to_string());
@@ -778,17 +801,52 @@ async fn stage_codex_app_server_image(
));
}
let image_dir = workspace_path.join("input-images");
tokio::fs::create_dir_all(&image_dir)
.await
.map_err(|error| {
platform_llm::LlmError::Transport(format!("创建 app-server 图片暂存目录失败:{error}"))
})?;
crate::ensure_game_creator_private_directory_tree(&image_dir, "app-server 图片暂存目录")
.map_err(platform_llm::LlmError::Transport)?;
crate::prepare_game_creator_private_path_for_read(&image_dir, true, "app-server 图片暂存目录")
.map_err(platform_llm::LlmError::Transport)?;
let digest = Sha256::digest(&bytes);
let path = image_dir.join(format!("{:x}-{image_index}.{extension}", digest));
if !path.exists() {
tokio::fs::write(&path, bytes).await.map_err(|error| {
platform_llm::LlmError::Transport(format!("写入 app-server 图片暂存文件失败:{error}"))
})?;
let mut image_options = tokio::fs::OpenOptions::new();
image_options.write(true).create_new(true);
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
image_options.custom_flags(crate::PROJECT_FILE_FLAG_OPEN_REPARSE_POINT);
}
match image_options.open(&path).await {
Ok(mut file) => {
if let Err(error) =
crate::harden_new_game_creator_private_path(&path, false, "app-server 图片暂存文件")
{
drop(file);
let _ = std::fs::remove_file(&path);
return Err(platform_llm::LlmError::Transport(error));
}
file.write_all(&bytes).await.map_err(|error| {
platform_llm::LlmError::Transport(format!(
"写入 app-server 图片暂存文件失败:{error}"
))
})?;
file.sync_all().await.map_err(|error| {
platform_llm::LlmError::Transport(format!(
"同步 app-server 图片暂存文件失败:{error}"
))
})?;
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
crate::prepare_game_creator_private_path_for_read(
&path,
false,
"app-server 图片暂存文件",
)
.map_err(platform_llm::LlmError::Transport)?;
}
Err(error) => {
return Err(platform_llm::LlmError::Transport(format!(
"写入 app-server 图片暂存文件失败:{error}"
)));
}
}
Ok(path)
}
@@ -975,6 +1033,8 @@ fn find_game_creator_codex_auth_path() -> Option<std::path::PathBuf> {
fn read_game_creator_codex_auth_bridge(
source_auth: &std::path::Path,
) -> Result<CodexAppServerCredential, platform_llm::LlmError> {
crate::prepare_game_creator_private_path_for_read(source_auth, false, "Codex CLI 登录态")
.map_err(platform_llm::LlmError::InvalidConfig)?;
let mut auth_json = Vec::new();
{
use std::io::Read;
@@ -1279,6 +1339,8 @@ fn prepare_isolated_game_creator_codex_home(
std::fs::create_dir(&isolated_home).map_err(|error| {
platform_llm::LlmError::Transport(format!("创建隔离 Codex app-server HOME 失败:{error}"))
})?;
crate::harden_new_game_creator_private_path(&isolated_home, true, "隔离 Codex app-server HOME")
.map_err(platform_llm::LlmError::Transport)?;
let CodexAppServerCredential::AuthBridge { auth_json, .. } = credential else {
return Ok(isolated_home);
};
@@ -1286,20 +1348,8 @@ fn prepare_isolated_game_creator_codex_home(
return Ok(isolated_home);
}
let target_auth = isolated_home.join("auth.json");
std::fs::write(&target_auth, auth_json).map_err(|error| {
platform_llm::LlmError::Transport(format!(
"桥接 Codex 登录态到隔离 app-server 失败:{error}"
))
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&target_auth, std::fs::Permissions::from_mode(0o600)).map_err(
|error| {
platform_llm::LlmError::Transport(format!("收紧隔离 Codex 登录态权限失败:{error}"))
},
)?;
}
crate::write_game_creator_private_file(&target_auth, auth_json.as_slice(), "隔离 Codex 登录态")
.map_err(platform_llm::LlmError::Transport)?;
Ok(isolated_home)
}
@@ -1310,11 +1360,13 @@ fn trust_isolated_game_creator_codex_workspace(
let workspace = workspace.to_string_lossy();
let quoted_workspace = quoted_toml_string(&workspace)?;
let config = format!("[projects.{quoted_workspace}]\ntrust_level = \"trusted\"\n");
std::fs::write(codex_home.join("config.toml"), config).map_err(|error| {
platform_llm::LlmError::Transport(format!(
"写入隔离 Codex app-server 项目信任配置失败:{error}"
))
})
let config_path = codex_home.join("config.toml");
crate::write_game_creator_private_file(
&config_path,
config.as_bytes(),
"隔离 Codex 项目信任配置",
)
.map_err(platform_llm::LlmError::Transport)
}
impl CodexAppServerConnection {
@@ -1337,9 +1389,35 @@ impl CodexAppServerConnection {
}
let codex_cli_version = game_creator_codex_cli_version_identity()
.map_err(platform_llm::LlmError::InvalidConfig)?;
let credential = resolve_game_creator_codex_app_server_credential(llm)?;
let mut effective_llm = llm.clone();
let credential = if game_creator_official_llm_route_locked() {
let session = current_platform_session().ok_or_else(|| {
platform_llm::LlmError::InvalidConfig(
"authentication-required: 请先登录陶泥儿账号".to_string(),
)
})?;
effective_llm.base_url =
format!("{}/api/llm", session.api_base_url.trim_end_matches('/'));
effective_llm.api_key.clear();
effective_llm.model = OFFICIAL_AGC_LLM_MODEL.to_string();
CodexAppServerCredential::PlatformSession {
fingerprint: format!(
"platform-session:{}:{}:{}",
session.user_id,
session.api_base_url,
Sha256::digest(session.access_token.as_bytes())
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
),
api_base_url: session.api_base_url,
access_token: session.access_token,
}
} else {
resolve_game_creator_codex_app_server_credential(llm)?
};
let key = game_creator_codex_app_server_pool_key(
llm,
&effective_llm,
&codex_cli_version,
snapshot,
credential.fingerprint(),
@@ -1383,7 +1461,7 @@ impl CodexAppServerConnection {
let executable = game_creator_codex_cli_executable_path()
.map_err(platform_llm::LlmError::InvalidConfig)?;
Self::spawn_with_executable_and_credential_at_workspace(
llm,
&effective_llm,
&credential,
executable.as_os_str(),
Some(workspace),
@@ -1395,7 +1473,7 @@ impl CodexAppServerConnection {
let executable = game_creator_codex_cli_executable_path()
.map_err(platform_llm::LlmError::InvalidConfig)?;
Self::spawn_with_executable_and_credential_at_workspace(
llm,
&effective_llm,
&credential,
executable.as_os_str(),
None,
@@ -1455,10 +1533,29 @@ impl CodexAppServerConnection {
"创建 Codex app-server 临时目录失败:{error}"
))
})?;
let direct_provider_route = (workspace_mode == CodexAppServerWorkspaceMode::DirectProject)
.then(|| credential.direct_provider_route(llm))
.flatten()
.map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string()));
crate::harden_new_game_creator_private_path(
working_dir.path(),
true,
"Codex app-server 临时目录",
)
.map_err(platform_llm::LlmError::Transport)?;
let direct_provider_route = match credential {
CodexAppServerCredential::PlatformSession {
api_base_url,
access_token,
..
} => Some((
format!("{}/api/llm", api_base_url.trim_end_matches('/')),
access_token.clone(),
)),
CodexAppServerCredential::AccountKey { .. } => credential
.direct_provider_route(llm)
.map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string())),
_ => (workspace_mode == CodexAppServerWorkspaceMode::DirectProject)
.then(|| credential.direct_provider_route(llm))
.flatten()
.map(|(base_url, api_key)| (base_url.to_string(), api_key.to_string())),
};
let remote_control_disable_reason =
credential.remote_control_disable_reason(direct_provider_route.is_some());
let isolated_codex_home = prepare_isolated_game_creator_codex_home(
@@ -1473,11 +1570,24 @@ impl CodexAppServerConnection {
"创建 Codex app-server 隔离工作目录失败:{error}"
))
})?;
std::fs::create_dir(isolated_workspace.join(".git")).map_err(|error| {
crate::harden_new_game_creator_private_path(
&isolated_workspace,
true,
"Codex app-server 隔离工作目录",
)
.map_err(platform_llm::LlmError::Transport)?;
let isolated_git = isolated_workspace.join(".git");
std::fs::create_dir(&isolated_git).map_err(|error| {
platform_llm::LlmError::Transport(format!(
"创建 Codex app-server 隔离仓库边界失败:{error}"
))
})?;
crate::harden_new_game_creator_private_path(
&isolated_git,
true,
"Codex app-server 隔离仓库边界",
)
.map_err(platform_llm::LlmError::Transport)?;
}
let (tool_bridge_root, workspace_path) =
if workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
@@ -1502,16 +1612,21 @@ impl CodexAppServerConnection {
let isolated_os_home = working_dir.path().join("home");
let isolated_app_data = isolated_os_home.join("appdata");
let isolated_local_app_data = isolated_os_home.join("local-appdata");
for path in [
&isolated_os_home,
&isolated_app_data,
&isolated_local_app_data,
for (path, label) in [
(&isolated_os_home, "Codex app-server 隔离用户目录"),
(&isolated_app_data, "Codex app-server 隔离 AppData 目录"),
(
&isolated_local_app_data,
"Codex app-server 隔离 LocalAppData 目录",
),
] {
std::fs::create_dir_all(path).map_err(|error| {
std::fs::create_dir(path).map_err(|error| {
platform_llm::LlmError::Transport(format!(
"创建 Codex app-server 隔离用户目录失败:{error}"
))
})?;
crate::harden_new_game_creator_private_path(path, true, label)
.map_err(platform_llm::LlmError::Transport)?;
}
let skill_root = if workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
install_agc_skill_pack(&isolated_os_home)
@@ -1520,9 +1635,7 @@ impl CodexAppServerConnection {
} else {
None
};
let provider_proxy = if workspace_mode != CodexAppServerWorkspaceMode::DirectProject {
None
} else if let Some((base_url, api_key)) = direct_provider_route.as_ref() {
let provider_proxy = if let Some((base_url, api_key)) = direct_provider_route.as_ref() {
Some(
start_codex_provider_proxy(base_url, api_key)
.await
@@ -608,6 +608,8 @@ async fn request_game_creator_agent_codex_cli_with_executable(
.map_err(|error| {
platform_llm::LlmError::Transport(format!("创建 Codex CLI Agent 临时目录失败:{error}"))
})?;
crate::harden_new_game_creator_private_path(temp_dir.path(), true, "Codex CLI Agent 临时目录")
.map_err(platform_llm::LlmError::Transport)?;
let schema_path = if let Some(schema) = game_creator_codex_cli_tool_output_schema(&request) {
let path = temp_dir.path().join("tool-output.schema.json");
let content = serde_json::to_vec(&schema).map_err(|error| {
@@ -615,11 +617,8 @@ async fn request_game_creator_agent_codex_cli_with_executable(
"序列化 Codex CLI Agent output schema 失败:{error}"
))
})?;
std::fs::write(&path, content).map_err(|error| {
platform_llm::LlmError::Transport(format!(
"写入 Codex CLI Agent output schema 失败:{error}"
))
})?;
crate::write_game_creator_private_file(&path, &content, "Codex CLI Agent output schema")
.map_err(platform_llm::LlmError::Transport)?;
Some(path)
} else {
None
@@ -1136,6 +1136,7 @@ fn direct_taonier_manifest_entry_at(
fn direct_taonier_asset_sha256_at(root: &Path, local_path: &str) -> Result<String, String> {
let path = resolve_local_project_path(root, local_path)?;
prepare_game_creator_private_path_for_read(&path, false, "整包重生成素材")?;
let bytes = std::fs::read(&path)
.map_err(|error| format!("读取整包重生成素材失败:{}: {error}", path.display()))?;
Ok(format!("{:x}", Sha256::digest(bytes)))
@@ -1146,6 +1147,11 @@ fn direct_taonier_optional_contract_file_sha256_at(
local_path: &str,
) -> Result<Option<(String, usize)>, String> {
let path = resolve_local_project_path(root, local_path)?;
let prepared =
prepare_game_creator_private_path_for_read(&path, false, "整包重生成严格合同文件")?;
if !prepared {
return Ok(None);
}
let metadata = match std::fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
@@ -1198,6 +1204,7 @@ impl DirectTaonierRegenerationRollback {
.into_iter()
.map(|local_path| {
let path = resolve_local_project_path(root, local_path)?;
prepare_game_creator_private_path_for_read(&path, false, "整包重生成旧素材")?;
let previous_bytes = std::fs::read(&path).map_err(|error| {
format!("读取整包重生成旧素材失败:{}: {error}", path.display())
})?;
@@ -1909,7 +1916,8 @@ fn direct_taonier_art_asset_identity(
return None;
}
let asset_path = resolve_local_project_path(root, &asset.local_path).ok()?;
if !asset_path.is_file() {
if !prepare_game_creator_private_path_for_read(&asset_path, false, "陶泥儿平台素材").ok()?
{
return None;
}
let bytes = std::fs::read(asset_path).ok()?;
@@ -2061,13 +2069,16 @@ fn direct_taonier_strict_art_package_is_valid(root: &Path) -> bool {
return false;
}
let expected_art_manifest = art_manifest_content();
if std::fs::read(root.join("assets/manifest.art.json"))
let art_manifest_path = root.join("assets/manifest.art.json");
if !prepare_game_creator_private_path_for_read(&art_manifest_path, false, "美术 manifest")
.ok()
.as_deref()
!= Some(expected_art_manifest.as_bytes())
.unwrap_or(false)
{
return false;
}
if std::fs::read(&art_manifest_path).ok().as_deref() != Some(expected_art_manifest.as_bytes()) {
return false;
}
let Ok(manifest) = read_manifest_for_project(root) else {
return false;
};
@@ -2177,9 +2188,7 @@ fn direct_registered_taonier_slice_paths(root: &Path) -> Vec<String> {
fn direct_game_sources_referenced_taonier_assets(root: &Path) -> Vec<String> {
let sources = direct_codex_game_outputs(root)
.into_iter()
.filter_map(|(relative_path, _, _)| {
std::fs::read_to_string(root.join(relative_path)).ok()
})
.filter_map(|(relative_path, _, _)| std::fs::read_to_string(root.join(relative_path)).ok())
.collect::<Vec<_>>();
let mut available_paths = Vec::new();
if direct_taonier_art_base_is_valid(root) {
@@ -2267,9 +2276,7 @@ fn direct_browser_evidence_needs_art_repair(
fn direct_game_output_completion_error(root: &Path) -> Option<String> {
let entry = agent_runtime_game_entry_relative_path(root);
if !root.join(entry).is_file() {
return Some(format!(
"Codex 返回后未找到 {entry},项目未进入可运行状态"
));
return Some(format!("Codex 返回后未找到 {entry},项目未进入可运行状态"));
}
if !direct_game_sources_reference_taonier_art_package(root) {
return Some(
@@ -2450,6 +2457,7 @@ async fn recover_direct_taonier_spritesheet_read_only_at(
.find(|asset| asset.local_path == DIRECT_CODEX_ART_SPEC_ASSET_PATH)
.ok_or_else(|| "陶泥儿规范图 manifest 记录缺失".to_string())?;
let source_path = resolve_local_project_path(root, &source_asset.local_path)?;
prepare_game_creator_private_path_for_read(&source_path, false, "陶泥儿规范图")?;
let source_bytes =
fs::read(source_path).map_err(|error| format!("读取陶泥儿规范图失败:{error}"))?;
let source_identity = new_external_editor_source_identity(
@@ -2572,14 +2580,25 @@ async fn recover_direct_taonier_spritesheet_read_only_at(
return Err("本地图集文件已存在但合同不完整,已拒绝覆盖并保持失败状态".to_string());
}
if let Some(parent) = output.parent() {
std::fs::create_dir_all(parent)
.map_err(|error| format!("创建陶泥儿图集目录失败:{error}"))?;
ensure_game_creator_private_directory_tree(parent, "陶泥儿图集目录")?;
prepare_game_creator_private_path_for_read(parent, true, "陶泥儿图集目录")?;
}
let mut output_file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
let mut output_options = std::fs::OpenOptions::new();
output_options.write(true).create_new(true);
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
output_options.custom_flags(crate::PROJECT_FILE_FLAG_OPEN_REPARSE_POINT);
}
let mut output_file = output_options
.open(&output)
.map_err(|error| format!("创建恢复的陶泥儿图集文件失败:{error}"))?;
if let Err(error) = harden_new_game_creator_private_path(&output, false, "恢复的陶泥儿图集")
{
drop(output_file);
let _ = std::fs::remove_file(&output);
return Err(error);
}
output_file
.write_all(&download.bytes)
.and_then(|_| output_file.sync_all())
@@ -3169,7 +3188,9 @@ fn direct_codex_output_fingerprint(root: &Path) -> String {
for (local_path, _, _) in direct_codex_game_outputs(root) {
hasher.update(local_path.as_bytes());
hasher.update([0]);
match std::fs::read(root.join(local_path)) {
let path = root.join(local_path);
let _ = prepare_game_creator_private_path_for_read(&path, false, "游戏输出文件");
match std::fs::read(path) {
Ok(bytes) => {
hasher.update([1]);
hasher.update((bytes.len() as u64).to_le_bytes());
@@ -6,6 +6,7 @@ use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex as StdMutex};
use unicode_normalization::UnicodeNormalization;
@@ -1073,6 +1074,10 @@ fn bridge_art_preparation_mode(
}
fn bridge_png_content(root: &Path, path: &Path) -> Result<String, String> {
crate::validate_game_creator_private_path_ancestors(root, "工具桥项目根")?;
crate::prepare_game_creator_private_path_for_read(root, true, "工具桥项目根")?;
crate::validate_game_creator_private_path_ancestors(path, "工具桥图片")?;
crate::prepare_game_creator_private_path_for_read(path, false, "工具桥图片")?;
let root = root
.canonicalize()
.map_err(|_| "工具桥项目根无法安全解析".to_string())?;
@@ -1082,6 +1087,7 @@ fn bridge_png_content(root: &Path, path: &Path) -> Result<String, String> {
if !path.starts_with(&root) {
return Err("工具桥图片越出当前项目边界".to_string());
}
crate::prepare_game_creator_private_path_for_read(&path, false, "工具桥图片")?;
let metadata =
std::fs::symlink_metadata(&path).map_err(|_| "读取工具桥图片失败".to_string())?;
if metadata.file_type().is_symlink()
@@ -1090,7 +1096,10 @@ fn bridge_png_content(root: &Path, path: &Path) -> Result<String, String> {
{
return Err("工具桥图片不满足普通文件或大小边界".to_string());
}
let bytes = std::fs::read(&path).map_err(|_| "读取工具桥图片失败".to_string())?;
let (mut file, _) = open_project_private_regular_file(&path, "工具桥图片")?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|_| "读取工具桥图片失败".to_string())?;
if !bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
return Err("工具桥图片不是有效 PNG".to_string());
}
@@ -1178,8 +1178,7 @@ mod tests {
#[test]
fn tool_catalog_preserves_reviewed_resource_contracts() {
assert!(
DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES
> DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES + 1024,
DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES > DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES + 1024,
"MCP request envelope must fit the advertised file-write payload"
);
let specs = direct_tools_mcp_specs();
@@ -24,8 +24,7 @@ pub(crate) use canvas_generation::{
ExternalGenerationInitialResponse,
};
pub(in crate::agent) use canvas_generation::{
commit_prepared_platform_art_asset_at,
commit_prepared_platform_art_asset_strict_slices_at,
commit_prepared_platform_art_asset_at, commit_prepared_platform_art_asset_strict_slices_at,
generate_platform_art_asset_with_retained_runtime_options_at,
generate_platform_art_asset_with_runtime_options_at,
platform_art_generation_error_result_unknown, register_existing_platform_art_slices_at,
@@ -475,7 +475,11 @@ fn prepare_platform_art_asset_output_path_for_mode(
return Err("图片生成 outputPath 只允许 png、jpg、jpeg 或 webp 文件".to_string());
}
let absolute = resolve_local_project_path(root, &normalized)?;
let existing = if absolute.exists() {
let existing = if crate::prepare_game_creator_private_path_for_read(
&absolute,
false,
"图片生成 outputPath",
)? {
if !replace_existing {
return Err(format!(
"图片生成 outputPath 已存在,禁止静默覆盖:{normalized}"
@@ -3118,6 +3122,10 @@ fn cleanup_interrupted_platform_art_contract_files_at(root: &Path) -> Result<(),
}
fn write_new_platform_art_slice(path: &Path, bytes: &[u8]) -> Result<bool, String> {
if let Some(parent) = path.parent() {
crate::ensure_game_creator_private_directory_tree(parent, "平台图集切片目录")?;
crate::prepare_game_creator_private_path_for_read(parent, true, "平台图集切片目录")?;
}
let mut output = fs::OpenOptions::new();
output.write(true).create_new(true);
#[cfg(unix)]
@@ -3126,9 +3134,15 @@ fn write_new_platform_art_slice(path: &Path, bytes: &[u8]) -> Result<bool, Strin
output.custom_flags(libc::O_NOFOLLOW);
output.mode(0o600);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
output.custom_flags(crate::PROJECT_FILE_FLAG_OPEN_REPARSE_POINT);
}
let mut output = match output.open(path) {
Ok(output) => output,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
crate::prepare_game_creator_private_path_for_read(path, false, "平台图集切片")?;
let existing = fs::read(path)
.map_err(|read_error| format!("回读既有平台图集切片失败:{read_error}"))?;
if existing == bytes {
@@ -3143,6 +3157,12 @@ fn write_new_platform_art_slice(path: &Path, bytes: &[u8]) -> Result<bool, Strin
return Err(format!("创建平台图集切片失败:{}: {error}", path.display()));
}
};
if let Err(error) = crate::harden_new_game_creator_private_path(path, false, "平台图集切片")
{
drop(output);
let _ = fs::remove_file(path);
return Err(error);
}
output.write_all(bytes).map_err(|error| {
let _ = fs::remove_file(path);
format!("写入平台图集切片失败:{}: {error}", path.display())
@@ -3151,6 +3171,10 @@ fn write_new_platform_art_slice(path: &Path, bytes: &[u8]) -> Result<bool, Strin
}
fn replace_platform_art_slice_file(path: &Path, bytes: &[u8], suffix: &str) -> Result<(), String> {
if let Some(parent) = path.parent() {
crate::ensure_game_creator_private_directory_tree(parent, "平台图集切片目录")?;
crate::prepare_game_creator_private_path_for_read(parent, true, "平台图集切片目录")?;
}
let file_name = path
.file_name()
.and_then(|value| value.to_str())
@@ -3158,8 +3182,10 @@ fn replace_platform_art_slice_file(path: &Path, bytes: &[u8], suffix: &str) -> R
let temporary = path.with_file_name(format!(".{file_name}.replacement.{suffix}"));
let backup = path.with_file_name(format!(".{file_name}.previous.{suffix}"));
write_new_platform_art_slice(&temporary, bytes)?;
let had_previous = path.is_file();
let had_previous =
crate::prepare_game_creator_private_path_for_read(path, false, "平台图集切片")?;
if had_previous {
crate::prepare_game_creator_private_path_for_read(path, false, "平台图集切片")?;
if let Err(error) = fs::rename(path, &backup) {
let _ = fs::remove_file(&temporary);
return Err(format!("准备替换平台图集切片文件失败:{error}"));
@@ -3239,6 +3265,10 @@ fn write_durable_platform_art_transaction_file(
bytes: &[u8],
label: &str,
) -> Result<(), String> {
if let Some(parent) = path.parent() {
crate::ensure_game_creator_private_directory_tree(parent, "平台图集事务目录")?;
crate::prepare_game_creator_private_path_for_read(parent, true, "平台图集事务目录")?;
}
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
@@ -3247,12 +3277,23 @@ fn write_durable_platform_art_transaction_file(
options.custom_flags(libc::O_NOFOLLOW);
options.mode(0o600);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
options.custom_flags(crate::PROJECT_FILE_FLAG_OPEN_REPARSE_POINT);
}
let mut file = options
.open(path)
.map_err(|error| format!("创建{label}失败:{}: {error}", path.display()))?;
if let Err(error) = crate::harden_new_game_creator_private_path(path, false, label) {
drop(file);
let _ = fs::remove_file(path);
return Err(error);
}
file.write_all(bytes)
.and_then(|_| file.sync_all())
.map_err(|error| format!("持久化{label}失败:{}: {error}", path.display()))
.map_err(|error| format!("持久化{label}失败:{}: {error}", path.display()))?;
Ok(())
}
#[cfg(test)]
@@ -6857,12 +6898,12 @@ fn commit_strict_platform_art_slices_at(
)?);
let absolute_path = resolve_local_project_path(root, &local_path)?;
if let Some(parent) = absolute_path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"创建正式平台图集切片目录失败:{}: {error}",
parent.display()
)
})?;
crate::ensure_game_creator_private_directory_tree(parent, "正式平台图集切片目录")?;
crate::prepare_game_creator_private_path_for_read(
parent,
true,
"正式平台图集切片目录",
)?;
}
replace_platform_art_slice_file(&absolute_path, &slice.download.bytes, suffix)?;
content_sha256s.push(slice.content_sha256.clone());
@@ -6931,12 +6972,8 @@ fn commit_prepared_platform_art_slices_at(
sanitize_file_name(generation_key)
);
let directory_path = resolve_local_project_path(root, &directory)?;
fs::create_dir_all(&directory_path).map_err(|error| {
format!(
"创建平台图集切片目录失败:{}: {error}",
directory_path.display()
)
})?;
crate::ensure_game_creator_private_directory_tree(&directory_path, "平台图集切片目录")?;
crate::prepare_game_creator_private_path_for_read(&directory_path, true, "平台图集切片目录")?;
let mut generated = Vec::with_capacity(slices.len());
let mut created_paths = Vec::with_capacity(slices.len());
let mut slice_paths = Vec::with_capacity(slices.len());
@@ -7131,8 +7168,8 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
}
};
if let Some(parent) = absolute_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("创建平台生成素材目录失败:{}: {error}", parent.display()))?;
crate::ensure_game_creator_private_directory_tree(parent, "平台生成素材目录")?;
crate::prepare_game_creator_private_path_for_read(parent, true, "平台生成素材目录")?;
}
absolute_path = resolve_local_project_path(root, &local_path)?;
let replacement_suffix = format!(
@@ -7185,13 +7222,29 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
output.custom_flags(libc::O_NOFOLLOW);
output.mode(0o600);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
output.custom_flags(crate::PROJECT_FILE_FLAG_OPEN_REPARSE_POINT);
}
let mut output = output.open(&output_path).map_err(|error| {
format!("创建平台生成素材失败:{}: {error}", output_path.display())
})?;
if let Err(error) =
crate::harden_new_game_creator_private_path(&output_path, false, "平台生成素材")
{
drop(output);
let _ = fs::remove_file(&output_path);
return Err(error);
}
output.write_all(&download.bytes).map_err(|error| {
let _ = fs::remove_file(&output_path);
format!("写入平台生成素材失败:{}: {error}", output_path.display())
})?;
output.sync_all().map_err(|error| {
let _ = fs::remove_file(&output_path);
format!("同步平台生成素材失败:{}: {error}", output_path.display())
})?;
drop(output);
}
let replacement_backup_path =
@@ -7306,12 +7359,15 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
let receipt_path =
resolve_local_project_path(root, ".agent/runtime/art-spritesheet-contract.json")?;
if let Some(parent) = receipt_path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"创建平台图集私有合同回执目录失败:{}: {error}",
parent.display()
)
})?;
crate::ensure_game_creator_private_directory_tree(
parent,
"平台图集私有合同回执目录",
)?;
crate::prepare_game_creator_private_path_for_read(
parent,
true,
"平台图集私有合同回执目录",
)?;
}
let receipt_bytes = strict_game_art_contract_receipt_bytes(
resource_id
@@ -37,63 +37,56 @@ pub(crate) fn write_local_game_draft_at(
&format!("- {timestamp}: {prompt}\n"),
"写入长期记忆失败",
)?;
fs::write(
write_game_creator_private_file(
&design_path,
format!(
"# 游戏设计草案\n\n## 原始想法\n\n{prompt}\n\n## Agent 协作交接\n\n{handoff_summary}\n\n## LLM 生成草案\n\n{}\n",
draft.design_markdown.trim()
),
)
.map_err(|error| format!("写入游戏设计失败:{}: {error}", design_path.display()))?;
fs::write(
)
.as_bytes(),
"游戏设计",
)?;
write_game_creator_private_file(
&balance_path,
serde_json::to_string_pretty(&draft.balance)
.map_err(|error| format!("生成数值配置失败:{error}"))?,
)
.map_err(|error| format!("写入数值配置失败:{}: {error}", balance_path.display()))?;
fs::write(
.map_err(|error| format!("生成数值配置失败:{error}"))?
.as_bytes(),
"数值配置",
)?;
write_game_creator_private_file(
&art_manifest_path,
serde_json::to_string_pretty(&draft.art_manifest)
.map_err(|error| format!("生成美术清单失败:{error}"))?,
)
.map_err(|error| format!("写入美术清单失败:{}: {error}", art_manifest_path.display()))?;
fs::write(
.map_err(|error| format!("生成美术清单失败:{error}"))?
.as_bytes(),
"美术清单",
)?;
write_game_creator_private_file(
&audio_manifest_path,
serde_json::to_string_pretty(&draft.audio_manifest)
.map_err(|error| format!("生成音乐音效清单失败:{error}"))?,
)
.map_err(|error| {
format!(
"写入音乐音效清单失败:{}: {error}",
audio_manifest_path.display()
)
})?;
fs::write(
.map_err(|error| format!("生成音乐音效清单失败:{error}"))?
.as_bytes(),
"音乐音效清单",
)?;
write_game_creator_private_file(
&publish_readme_path,
format!(
"# 发布包装草案\n\n## 标题\n\n{title}\n\n## 简介\n\n{prompt}\n\n## Agent 协作交接\n\n{handoff_summary}\n\n{}\n",
draft.publish_readme.trim()
),
)
.map_err(|error| {
format!(
"写入发布包装草案失败:{}: {error}",
publish_readme_path.display()
)
})?;
.as_bytes(),
"发布包装草案",
)?;
fs::write(&game_index_path, draft.game_html.trim())
.map_err(|error| format!("写入游戏入口失败:{}: {error}", game_index_path.display()))?;
fs::OpenOptions::new()
.create(true)
.append(true)
.open(&agent_log_path)
.and_then(|mut file| {
file.write_all(
format!("{timestamp} game.generate_draft llm\n{handoff_summary}\n").as_bytes(),
)
})
.map_err(|error| format!("写入 Agent 日志失败:{}: {error}", agent_log_path.display()))?;
write_game_creator_private_file(
&game_index_path,
draft.game_html.trim().as_bytes(),
"游戏入口",
)?;
append_game_creator_private_file(
&agent_log_path,
format!("{timestamp} game.generate_draft llm\n{handoff_summary}\n").as_bytes(),
"Agent 日志",
)?;
append_agent_db_record(
root,
serde_json::json!({
@@ -61,8 +61,7 @@ pub(crate) async fn run_game_creator_agent_loop_at(
)
.await
.map(|spec| render_planner_spec(prompt, &spec))?;
fs::write(&spec_path, &planner_spec)
.map_err(|error| format!("写入 Planner 规格失败:{}: {error}", spec_path.display()))?;
write_game_creator_private_file(&spec_path, planner_spec.as_bytes(), "Planner 规格")?;
emit_agent_progress(progress, "llm.planner.done", "Planner 规格已生成");
steps.push(with_task_context(
agent_trace_step(
@@ -89,12 +88,7 @@ pub(crate) async fn run_game_creator_agent_loop_at(
let mut latest_findings =
render_evaluator_findings(0, &["暂无上一轮问题,Generator 可开始首轮实现。"]);
fs::write(&findings_path, &latest_findings).map_err(|error| {
format!(
"写入 Evaluator 结果失败:{}: {error}",
findings_path.display()
)
})?;
write_game_creator_private_file(&findings_path, latest_findings.as_bytes(), "Evaluator 结果")?;
steps.push(agent_trace_step(
0,
"Evaluator",
@@ -169,12 +163,11 @@ pub(crate) async fn run_game_creator_agent_loop_at(
"agent.role.brief",
));
latest_findings = render_evaluator_findings(pass, &issues);
fs::write(&findings_path, &latest_findings).map_err(|write_error| {
format!(
"写入 Evaluator 结果失败:{}: {write_error}",
findings_path.display()
)
})?;
write_game_creator_private_file(
&findings_path,
latest_findings.as_bytes(),
"Evaluator 结果",
)?;
steps.push(with_task_context(
agent_trace_step(
pass,
@@ -271,12 +264,11 @@ pub(crate) async fn run_game_creator_agent_loop_at(
append_collaboration_steps(pass, &draft, &pass_artifacts, &mut steps);
let issues = evaluate_game_draft(prompt, &draft);
latest_findings = render_evaluator_findings(pass, &issues);
fs::write(&findings_path, &latest_findings).map_err(|error| {
format!(
"写入 Evaluator 结果失败:{}: {error}",
findings_path.display()
)
})?;
write_game_creator_private_file(
&findings_path,
latest_findings.as_bytes(),
"Evaluator 结果",
)?;
steps.push(with_task_context(
agent_trace_step_owned(
pass,
@@ -344,12 +336,11 @@ pub(crate) async fn run_game_creator_agent_loop_at(
"llm.chat.generator",
));
latest_findings = render_evaluator_findings(pass, &issues);
fs::write(&findings_path, &latest_findings).map_err(|write_error| {
format!(
"写入 Evaluator 结果失败:{}: {write_error}",
findings_path.display()
)
})?;
write_game_creator_private_file(
&findings_path,
latest_findings.as_bytes(),
"Evaluator 结果",
)?;
steps.push(with_task_context(
agent_trace_step(
pass,
@@ -186,8 +186,8 @@ pub(crate) fn write_agent_pass_artifacts(
) -> Result<AgentPassArtifactPaths, String> {
let relative_dir = format!(".agent/passes/pass-{pass}");
let pass_dir = root.join(&relative_dir);
fs::create_dir_all(&pass_dir)
.map_err(|error| format!("创建 Agent pass 目录失败:{}: {error}", pass_dir.display()))?;
ensure_game_creator_private_directory_tree(&pass_dir, "Agent pass 目录")?;
prepare_game_creator_private_path_for_read(&pass_dir, true, "Agent pass 目录")?;
let paths = AgentPassArtifactPaths {
draft_json: format!("{relative_dir}/draft.json"),
@@ -262,16 +262,7 @@ pub(crate) fn write_agent_pass_file(
content: &str,
) -> Result<(), String> {
let path = root.join(relative_path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"创建 Agent pass 文件目录失败:{}: {error}",
parent.display()
)
})?;
}
fs::write(&path, content)
.map_err(|error| format!("写入 Agent pass 文件失败:{}: {error}", path.display()))
write_game_creator_private_file(&path, content.as_bytes(), "Agent pass 文件")
}
pub(crate) fn write_agent_group_brief(
@@ -714,22 +714,17 @@ pub(crate) fn append_agent_loop_log(
) -> Result<(), String> {
let agent_log_path = root.join(".agent/logs/agent.log");
let timestamp = unix_timestamp();
fs::OpenOptions::new()
.create(true)
.append(true)
.open(&agent_log_path)
.and_then(|mut file| {
file.write_all(
format!(
"{timestamp} agent.loop passes={}\nPlanner -> .agent/spec.md\n组内角色 briefs -> .agent/passes/pass-*/groups/<group>/*.md\n专业组汇总 -> .agent/passes/pass-*/groups/*.md\nGenerator -> .agent/passes/pass-*/draft.json\n专业组 handoffs -> .agent/passes/pass-*/handoff.md\nEvaluator -> .agent/findings.md\n{}\n{}\n",
loop_result.passes,
loop_result.spec_markdown.trim(),
loop_result.findings_markdown.trim()
)
.as_bytes(),
)
})
.map_err(|error| format!("写入 Agent loop 日志失败:{}: {error}", agent_log_path.display()))?;
append_game_creator_private_file(
&agent_log_path,
format!(
"{timestamp} agent.loop passes={}\nPlanner -> .agent/spec.md\n组内角色 briefs -> .agent/passes/pass-*/groups/<group>/*.md\n专业组汇总 -> .agent/passes/pass-*/groups/*.md\nGenerator -> .agent/passes/pass-*/draft.json\n专业组 handoffs -> .agent/passes/pass-*/handoff.md\nEvaluator -> .agent/findings.md\n{}\n{}\n",
loop_result.passes,
loop_result.spec_markdown.trim(),
loop_result.findings_markdown.trim()
)
.as_bytes(),
"Agent loop 日志",
)?;
append_agent_loop_memory(root, loop_result)
}
@@ -191,26 +191,12 @@ pub(crate) fn write_agent_run_trace_payload(
let payload = serde_json::to_string_pretty(&trace)
.map_err(|error| format!("生成 Agent run trace 失败:{error}"))?;
let latest_path = root.join(".agent/run.latest.json");
fs::write(&latest_path, &payload).map_err(|error| {
format!(
"写入 Agent run trace 失败:{}: {error}",
latest_path.display()
)
})?;
write_game_creator_private_file(&latest_path, payload.as_bytes(), "Agent run trace")?;
let run_dir = root.join(".agent/runs");
fs::create_dir_all(&run_dir).map_err(|error| {
format!(
"创建 Agent run history 目录失败:{}: {error}",
run_dir.display()
)
})?;
ensure_game_creator_private_directory_tree(&run_dir, "Agent run history 目录")?;
prepare_game_creator_private_path_for_read(&run_dir, true, "Agent run history 目录")?;
let run_path = run_dir.join(format!("{}.json", trace.run_id));
fs::write(&run_path, payload).map_err(|error| {
format!(
"写入 Agent run history 失败:{}: {error}",
run_path.display()
)
})?;
write_game_creator_private_file(&run_path, payload.as_bytes(), "Agent run history")?;
prune_agent_run_history(&run_dir)
}
@@ -309,20 +309,11 @@ pub(crate) fn append_preview_log(
url: Option<&str>,
) -> Result<(), String> {
let log_path = root.join(".agent/logs/preview.log");
if let Some(parent) = log_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("创建预览日志目录失败:{}: {error}", parent.display()))?;
}
let line = match url {
Some(url) => format!("{} preview.{status} {url}\n", unix_timestamp()),
None => format!("{} preview.{status}\n", unix_timestamp()),
};
fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.and_then(|mut file| file.write_all(line.as_bytes()))
.map_err(|error| format!("写入预览日志失败:{}: {error}", log_path.display()))
append_game_creator_private_file(&log_path, line.as_bytes(), "预览日志")
}
pub(crate) fn record_replaced_preview_stop(preview: &LocalPreviewResult) {
@@ -340,6 +331,7 @@ pub(crate) fn append_agent_run_trace_step(
error: Option<&str>,
) -> Result<(), String> {
let trace_path = root.join(".agent/run.latest.json");
prepare_game_creator_private_path_for_read(&trace_path, false, "Agent run trace")?;
let content = match fs::read_to_string(&trace_path) {
Ok(content) => content,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
@@ -781,8 +773,8 @@ pub(crate) fn append_agent_run_jsonl(
) -> Result<(), String> {
let path = root.join(relative_path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("创建 Agent 事件目录失败:{}: {error}", parent.display()))?;
ensure_game_creator_private_directory_tree(parent, "Agent 事件目录")?;
prepare_game_creator_private_path_for_read(parent, true, "Agent 事件目录")?;
}
let line =
serde_json::to_string(value).map_err(|error| format!("序列化 Agent 事件失败:{error}"))?;
@@ -795,12 +787,8 @@ pub(crate) fn write_agent_run_context_bundle(
) -> Result<(), String> {
let bundle_path = root.join(".agent/context.bundle.json");
if let Some(parent) = bundle_path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"创建 Agent context bundle 目录失败:{}: {error}",
parent.display()
)
})?;
ensure_game_creator_private_directory_tree(parent, "Agent context bundle 目录")?;
prepare_game_creator_private_path_for_read(parent, true, "Agent context bundle 目录")?;
}
let manifest = read_manifest_for_project(root).ok();
let payload = serde_json::json!({
@@ -825,10 +813,5 @@ pub(crate) fn write_agent_run_context_bundle(
});
let content = serde_json::to_string_pretty(&payload)
.map_err(|error| format!("生成 Agent context bundle 失败:{error}"))?;
fs::write(&bundle_path, content).map_err(|error| {
format!(
"写入 Agent context bundle 失败:{}: {error}",
bundle_path.display()
)
})
write_game_creator_private_file(&bundle_path, content.as_bytes(), "Agent context bundle")
}
@@ -48,7 +48,9 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_complete_game_index_wr
) -> Result<(), String> {
if path
.and_then(serde_json::Value::as_str)
.map_or(true, |path| !is_agent_runtime_game_entry_relative_path(path))
.map_or(true, |path| {
!is_agent_runtime_game_entry_relative_path(path)
})
{
return Ok(());
}
@@ -793,7 +795,7 @@ fn autonomous_manifest_parent_has_active_ready_task_at(
let records =
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(
&game_creator_agent_runtime_task_path(root, task_id),
)?);
)?);
for record in records {
if record.source != "agent-ready-task-scheduler"
|| game_creator_agent_runtime_terminal_status(&record).is_some()
@@ -806,10 +808,9 @@ fn autonomous_manifest_parent_has_active_ready_task_at(
// binding's root id solely to associate an in-flight child with
// this root; never reject or block the child for a mismatch.
if record.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
let parent_hint_matches =
record.parent_agent_id.as_deref()
== Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
&& record.parent_run_id.as_deref() == Some(parent_run_id);
let parent_hint_matches = record.parent_agent_id.as_deref()
== Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
&& record.parent_run_id.as_deref() == Some(parent_run_id);
let binding_root_matches = read_game_creator_agent_runtime_run_profile_binding(
root,
&record.agent_id,
@@ -821,8 +822,7 @@ fn autonomous_manifest_parent_has_active_ready_task_at(
}
continue;
}
if record.parent_agent_id.as_deref()
!= Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
if record.parent_agent_id.as_deref() != Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|| record.parent_run_id.as_deref() != Some(parent_run_id)
{
continue;
@@ -338,7 +338,8 @@ pub(in crate::agent) fn validate_agent_runtime_pending_tool_action_record(
validate_agent_runtime_pending_goal_binding(pending)?;
match pending.planning_session_binding.as_ref() {
Some(binding) => {
validate_plan_provider_session_binding(binding).map_err(|error| error.to_string())?;
validate_plan_provider_session_binding(binding)
.map_err(|error| error.to_string())?;
if pending.action.tool.trim() != PLAN_SUBMIT_GDD_TOOL
|| binding.agent_id != pending.agent_id
|| binding.task_id != pending.task_id
@@ -652,12 +653,8 @@ pub(crate) fn write_game_creator_agent_runtime_tool_confirmation(
let path =
game_creator_agent_runtime_tool_confirmation_path(root, agent_id, run_id, command_id);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"创建 Agent Runtime 工具确认目录失败:{}: {error}",
parent.display()
)
})?;
ensure_game_creator_private_directory_tree(parent, "Agent Runtime 工具确认目录")?;
prepare_game_creator_private_path_for_read(parent, true, "Agent Runtime 工具确认目录")?;
}
let payload = serde_json::json!({
"schemaVersion": AGENT_RUNTIME_SCHEMA_VERSION,
@@ -670,12 +667,8 @@ pub(crate) fn write_game_creator_agent_runtime_tool_confirmation(
});
let content = serde_json::to_string_pretty(&payload)
.map_err(|error| format!("序列化 Agent Runtime 工具确认失败:{error}"))?;
fs::write(&path, content).map_err(|error| {
format!(
"写入 Agent Runtime 工具确认失败:{}: {error}",
path.display()
)
})
crate::write_game_creator_private_file(&path, content.as_bytes(), "Agent Runtime 工具确认")?;
Ok(())
}
pub(in crate::agent) fn consume_game_creator_agent_runtime_tool_confirmation(
@@ -690,6 +683,7 @@ pub(in crate::agent) fn consume_game_creator_agent_runtime_tool_confirmation(
}
let path =
game_creator_agent_runtime_tool_confirmation_path(root, agent_id, run_id, command_id);
prepare_game_creator_private_path_for_read(&path, false, "Agent Runtime 工具确认")?;
let content = match fs::read_to_string(&path) {
Ok(content) => content,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
@@ -195,9 +195,9 @@ pub(crate) fn finish_agent_runtime_project_verification_locked(
};
if passed && !html.contains("<html") && !html.contains("<!doctype html") {
passed = false;
static_smoke_credential_error = Some(
format!("game.static_smoke 通过后 {entry_path} 不再是 HTML 文档"),
);
static_smoke_credential_error = Some(format!(
"game.static_smoke 通过后 {entry_path} 不再是 HTML 文档"
));
}
if passed {
match validate_game_html_smoke(html) {
@@ -541,14 +541,14 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
}
if !relaxed_autonomous {
if let Some(violation) = collaboration_preflight.violation {
return Ok(AgentRuntimeProviderActionBatchPreparation::Blocked(
AgentRuntimeToolObservation {
tool: "runtime.collaboration_policy".to_string(),
status: "blocked".to_string(),
summary: violation.summary,
detail: Some(violation.detail),
},
));
return Ok(AgentRuntimeProviderActionBatchPreparation::Blocked(
AgentRuntimeToolObservation {
tool: "runtime.collaboration_policy".to_string(),
status: "blocked".to_string(),
summary: violation.summary,
detail: Some(violation.detail),
},
));
}
}
if provider_action_batch_is_not_needed(
@@ -288,7 +288,8 @@ fn build_game_creator_agent_background_tool_plan_request_at(
.with_response_text_verbosity(platform_llm::LlmResponseTextVerbosity::Low)
.with_function_tools(function_tools)
.with_tool_choice(platform_llm::LlmToolChoice::Required);
let request = apply_game_creator_llm_reasoning_effort(request, &llm)?.with_web_search(false);
let request =
apply_game_creator_llm_reasoning_effort(request, &llm)?.with_web_search(false);
return Ok((
llm,
config_path,
@@ -979,7 +980,8 @@ mod tests {
game_creator_agent_runtime_run_profile_binding_path,
game_creator_project_supervisor_chat_system_prompt, init_local_game_project_at,
new_game_creation_app_seed_tasks, provider_command_exec_contract,
provider_command_start_contract, render_relaxed_autonomous_manifest_ready_task_background_prompt,
provider_command_start_contract,
render_relaxed_autonomous_manifest_ready_task_background_prompt,
required_runtime_prompt_section, start_game_creator_agent_runtime_task_at,
AgentRuntimeGoalContractAcceptanceNodeDraft, AgentRuntimeGoalContractDraft,
AgentRuntimeTaskLink, AgentRuntimeToolObservation, AgentRuntimeToolPlan,
@@ -1322,7 +1324,8 @@ mod tests {
let system_prompt = &request.messages[0].content;
let user_prompt = &request.messages[1].content;
assert!(system_prompt.contains("自主执行 Agent"));
assert!(system_prompt.contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件"));
assert!(system_prompt
.contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件"));
assert!(user_prompt.contains("依赖只作为参考"));
assert!(user_prompt.contains("不要等待或索要平台资产/验收回执"));
assert!(!user_prompt.contains("固定 owner 收束协议"));
@@ -1375,31 +1378,50 @@ mod tests {
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
"code-prototype",
);
for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] {
for tool in [
"project.verify",
"command.run_limited",
"preview.start",
"preview.validate",
] {
assert!(!request_advertises_native_tool(&code, tool));
}
assert!(request_advertises_native_tool(&code, "file.write"));
assert!(code.messages[0].content.contains("自主执行 Agent"));
assert!(code.messages[1].content.contains("不要等待或索要平台资产/验收回执"));
assert!(code.messages[1]
.content
.contains("不要等待或索要平台资产/验收回执"));
let readiness = build_autonomous_ready_child_request(
"preview-readiness",
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
"preview-readiness",
);
for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] {
for tool in [
"project.verify",
"command.run_limited",
"preview.start",
"preview.validate",
] {
assert!(!request_advertises_native_tool(&readiness, tool));
}
assert!(request_advertises_native_tool(&readiness, "file.read"));
assert!(readiness.messages[0].content.contains("自主执行 Agent"));
assert!(readiness.messages[1].content.contains("不要等待或索要平台资产/验收回执"));
assert!(readiness.messages[1]
.content
.contains("不要等待或索要平台资产/验收回执"));
let publish = build_autonomous_ready_child_request(
"publish-package",
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
"publish-package",
);
for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] {
for tool in [
"project.verify",
"command.run_limited",
"preview.start",
"preview.validate",
] {
assert!(!request_advertises_native_tool(&publish, tool));
}
assert!(request_advertises_native_tool(&publish, "file.write"));
@@ -1474,9 +1496,8 @@ mod tests {
assert!(prompts.contains("自主执行 Agent"));
assert!(prompts.contains("依赖只作为参考"));
assert!(!prompts.contains("非只读视觉规范生成任务"));
assert!(!prompts.contains(
crate::agent::AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER
));
assert!(!prompts
.contains(crate::agent::AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER));
assert!(!prompts.contains("会同时提交当前 run 的 mutation 与验证凭证"));
assert!(!prompts.contains("无生图凭据只读协调任务"));
}
@@ -1519,7 +1540,10 @@ mod tests {
// The autonomous execution marker lives in the system message; the
// user message carries only the task-specific runtime context.
let prompt = &request.messages[0].content;
assert!(prompt.contains("自主执行 Agent"), "unexpected relaxed root prompt: {prompt}");
assert!(
prompt.contains("自主执行 Agent"),
"unexpected relaxed root prompt: {prompt}"
);
assert!(prompt.contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件"));
assert!(request.function_tools.len() > 1);
for tool in [
@@ -439,19 +439,18 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
let code_prototype_requires_static_smoke = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& agent_id == "code-prototype";
let verified_delivery = if !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
{
let verification_gate =
read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?;
runtime_owner_artifact_validation_available
|| agent_runtime_autonomous_verified_delivery_allows_plan_completion(
agent_id,
&verification_gate,
)
} else {
false
};
let verified_delivery =
if !relaxed_autonomous && run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
let verification_gate =
read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?;
runtime_owner_artifact_validation_available
|| agent_runtime_autonomous_verified_delivery_allows_plan_completion(
agent_id,
&verification_gate,
)
} else {
false
};
let allow_runtime_plan_completion = read_only_delivery || verified_delivery;
let autonomous_project_verify_available = relaxed_autonomous
|| run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
@@ -758,8 +757,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
};
let parsed = match parsed {
Ok((parsed, source_payload))
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& !relaxed_autonomous =>
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID && !relaxed_autonomous =>
{
let collaboration_policy =
resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id)?
@@ -245,36 +245,39 @@ pub(in crate::agent) fn resume_game_creator_agent_finalization_at(
let assistant_exists =
game_creator_agent_runtime_finalization_assistant_exists(root, &journal)?;
if !relaxed_autonomous {
match classify_game_creator_agent_runtime_finalization_goal_snapshot_at(root, &journal, &state)?
{
AgentRuntimeFinalizationGoalSnapshotRelation::Matches => {}
AgentRuntimeFinalizationGoalSnapshotRelation::StaleRevision {
journal_revision,
current_revision,
} if journal.status == AGENT_RUNTIME_FINALIZATION_STATUS_PREPARED && !assistant_exists => {
remove_game_creator_agent_runtime_finalization_recovery_sidecars(
root,
&journal.agent_id,
&journal.run_id,
)?;
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.finalization_stale_recovered",
"agentId": journal.agent_id,
"taskId": journal.task_id,
"sessionId": journal.session_id,
"runId": journal.run_id,
"source": journal.source,
"summary": "Goal revision 已更新,旧最终回复已丢弃",
"journalGoalRevision": journal_revision,
"currentGoalRevision": current_revision,
}),
);
return Ok(AgentRuntimeFinalizationResume::NotFound(runtime_lock));
}
relation => {
let detail = match relation {
match classify_game_creator_agent_runtime_finalization_goal_snapshot_at(
root, &journal, &state,
)? {
AgentRuntimeFinalizationGoalSnapshotRelation::Matches => {}
AgentRuntimeFinalizationGoalSnapshotRelation::StaleRevision {
journal_revision,
current_revision,
} if journal.status == AGENT_RUNTIME_FINALIZATION_STATUS_PREPARED
&& !assistant_exists =>
{
remove_game_creator_agent_runtime_finalization_recovery_sidecars(
root,
&journal.agent_id,
&journal.run_id,
)?;
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.finalization_stale_recovered",
"agentId": journal.agent_id,
"taskId": journal.task_id,
"sessionId": journal.session_id,
"runId": journal.run_id,
"source": journal.source,
"summary": "Goal revision 已更新,旧最终回复已丢弃",
"journalGoalRevision": journal_revision,
"currentGoalRevision": current_revision,
}),
);
return Ok(AgentRuntimeFinalizationResume::NotFound(runtime_lock));
}
relation => {
let detail = match relation {
AgentRuntimeFinalizationGoalSnapshotRelation::StaleRevision {
journal_revision,
current_revision,
@@ -284,14 +287,16 @@ pub(in crate::agent) fn resume_game_creator_agent_finalization_at(
AgentRuntimeFinalizationGoalSnapshotRelation::Conflict(detail) => detail,
AgentRuntimeFinalizationGoalSnapshotRelation::Matches => unreachable!(),
};
let error = format!("Agent Runtime finalization 恢复已阻断:Goal 快照冲突:{detail}");
record_game_creator_agent_runtime_finalization_pending(root, &state, &error);
return read_game_creator_agent_runtime_at(root, agent_id)
.map(AgentRuntimeFinalizationResume::Blocked);
}
let error =
format!("Agent Runtime finalization 恢复已阻断:Goal 快照冲突:{detail}");
record_game_creator_agent_runtime_finalization_pending(root, &state, &error);
return read_game_creator_agent_runtime_at(root, agent_id)
.map(AgentRuntimeFinalizationResume::Blocked);
}
}
}
if !relaxed_autonomous && assistant_exists
if !relaxed_autonomous
&& assistant_exists
&& !state_reconstructed_from_task
&& !game_creator_agent_runtime_finalization_plan_matches_state(&journal, &state)
{
@@ -415,11 +420,13 @@ pub(in crate::agent) fn resume_game_creator_agent_finalization_at(
goal_contract_acceptance_completion_blocker_at_locked(&root, &state)
{
Some(blocker)
} else if let Some(blocker) = agent_runtime_non_verification_completion_blocker_at_locked(
&root,
&journal.agent_id,
&journal.run_id,
) {
} else if let Some(blocker) =
agent_runtime_non_verification_completion_blocker_at_locked(
&root,
&journal.agent_id,
&journal.run_id,
)
{
Some(blocker)
} else if let Some(blocker) =
autonomous_game_build_completion_blocker_at_locked(&root, &state)
@@ -810,101 +810,15 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
// impossible for a relaxed run to read the DAG and accidentally
// re-enter `waiting-for-manifest-tasks`.
if !relaxed_autonomous {
let autonomous_root_goal_contract_persisted =
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
{
match autonomous_root_goal_contract_persisted_at(
&root,
&agent_id,
&runtime.run_id,
) {
Ok(value) => value,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("读取自主构建根 Goal Contract 门失败:{error}"),
);
}
}
} else {
false
};
let autonomous_manifest_parent_can_wait =
autonomous_root_goal_contract_persisted
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& !game_creator_agent_runtime_provider_action_batch_exists(
&root,
&agent_id,
&runtime.run_id,
)
&& supervisor_collaboration_policy_completion_blocker_at_locked(
&root,
&agent_id,
&runtime.run_id,
)
.is_none()
&& isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id)
.is_none()
&& static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
.is_none();
if autonomous_manifest_parent_can_wait {
let manifest_state_before_schedule = match autonomous_manifest_dag_state_at(&root) {
Ok(value) => value,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("读取自主构建 manifest 等待屏障失败:{error}"),
);
}
};
// 已登记但损坏的派生视觉需要先由父 Run 规划修复,因此此时不再调度新的
// manifest child;但已经持久化并运行的 child 仍是当前 DAG 的活跃工作,
// 父 Run 必须继续等待,不能提前结束。
// 同理,已有失败任务时只能等待已在途 child 收束或立即安全失败,不能再
// 启动新的 pending sibling 并用它遮蔽原始失败。
let manifest_scheduler_blocked = matches!(
&manifest_state_before_schedule,
AutonomousManifestDagState::Completed
| AutonomousManifestDagState::Failed { .. }
) || autonomous_registered_derived_visuals_block_manifest_scheduler_at(
&root, &runtime,
);
let scheduled_ready_tasks = if manifest_scheduler_blocked {
Vec::new()
} else {
match schedule_autonomous_game_build_ready_tasks_at(
let autonomous_root_goal_contract_persisted = if agent_id
== GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
{
match autonomous_root_goal_contract_persisted_at(
&root,
&agent_id,
&runtime.run_id,
3,
) {
Ok(tasks) => tasks,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("调度自主构建 manifest 任务失败:{error}"),
);
}
}
};
let manifest_state = if matches!(
&manifest_state_before_schedule,
AutonomousManifestDagState::Failed { .. }
) {
manifest_state_before_schedule
} else if scheduled_ready_tasks.is_empty() {
match autonomous_manifest_dag_state_at(&root) {
Ok(value) => value,
Err(error) => {
return fail_game_creator_agent_background_context_at(
@@ -912,68 +826,155 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
&agent_id,
&session_id,
runtime,
&format!("读取自主构建 manifest 等待屏障失败:{error}"),
&format!("读取自主构建根 Goal Contract 门失败:{error}"),
);
}
}
} else {
AutonomousManifestDagState::InProgress
false
};
let manifest_parent_must_wait = matches!(
&manifest_state,
AutonomousManifestDagState::InProgress
| AutonomousManifestDagState::Failed {
has_active_children: true,
..
}
);
if manifest_parent_must_wait {
let blocker =
autonomous_game_build_completion_blocker_at_locked(&root, &runtime)
.unwrap_or_else(|| AgentRuntimeToolObservation {
tool: "runtime.autonomous_completion".to_string(),
status: "blocked".to_string(),
summary: "自主构建 manifest DAG 尚未完成".to_string(),
detail: Some("manifest 子任务仍在运行".to_string()),
});
if let Err(error) = persist_waiting_autonomous_manifest_parent_context_at(
let autonomous_manifest_parent_can_wait = autonomous_root_goal_contract_persisted
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& !game_creator_agent_runtime_provider_action_batch_exists(
&root,
&mut runtime,
&task,
&plan,
&mut observations,
loop_index,
&mut context_tracker,
blocker,
&agent_id,
&runtime.run_id,
)
&& supervisor_collaboration_policy_completion_blocker_at_locked(
&root,
&agent_id,
&runtime.run_id,
)
.is_none()
&& isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id)
.is_none()
&& static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
.is_none();
if autonomous_manifest_parent_can_wait {
let manifest_state_before_schedule =
match autonomous_manifest_dag_state_at(&root) {
Ok(value) => value,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("读取自主构建 manifest 等待屏障失败:{error}"),
);
}
};
// 已登记但损坏的派生视觉需要先由父 Run 规划修复,因此此时不再调度新的
// manifest child;但已经持久化并运行的 child 仍是当前 DAG 的活跃工作,
// 父 Run 必须继续等待,不能提前结束。
// 同理,已有失败任务时只能等待已在途 child 收束或立即安全失败,不能再
// 启动新的 pending sibling 并用它遮蔽原始失败。
let manifest_scheduler_blocked = matches!(
&manifest_state_before_schedule,
AutonomousManifestDagState::Completed
| AutonomousManifestDagState::Failed { .. }
)
|| autonomous_registered_derived_visuals_block_manifest_scheduler_at(
&root, &runtime,
);
let scheduled_ready_tasks = if manifest_scheduler_blocked {
Vec::new()
} else {
match schedule_autonomous_game_build_ready_tasks_at(
&root,
&agent_id,
&runtime.run_id,
3,
) {
Ok(tasks) => tasks,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("调度自主构建 manifest 任务失败:{error}"),
);
}
}
};
let manifest_state = if matches!(
&manifest_state_before_schedule,
AutonomousManifestDagState::Failed { .. }
) {
manifest_state_before_schedule
} else if scheduled_ready_tasks.is_empty() {
match autonomous_manifest_dag_state_at(&root) {
Ok(value) => value,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("读取自主构建 manifest 等待屏障失败:{error}"),
);
}
}
} else {
AutonomousManifestDagState::InProgress
};
let manifest_parent_must_wait = matches!(
&manifest_state,
AutonomousManifestDagState::InProgress
| AutonomousManifestDagState::Failed {
has_active_children: true,
..
}
);
if manifest_parent_must_wait {
let blocker =
autonomous_game_build_completion_blocker_at_locked(&root, &runtime)
.unwrap_or_else(|| AgentRuntimeToolObservation {
tool: "runtime.autonomous_completion".to_string(),
status: "blocked".to_string(),
summary: "自主构建 manifest DAG 尚未完成".to_string(),
detail: Some("manifest 子任务仍在运行".to_string()),
});
if let Err(error) = persist_waiting_autonomous_manifest_parent_context_at(
&root,
&mut runtime,
&task,
&plan,
&mut observations,
loop_index,
&mut context_tracker,
blocker,
) {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("持久化 manifest 父 run 等待状态失败:{error}"),
);
}
return AgentBackgroundTaskOutcome::WaitingForManifestTasks;
}
if let AutonomousManifestDagState::Failed {
failed_task_ids, ..
} = manifest_state
{
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("持久化 manifest 父 run 等待状态失败:{error}"),
&format!(
"自主构建 manifest 专业任务失败;failedTaskIds={}",
failed_task_ids.join(",")
),
);
}
return AgentBackgroundTaskOutcome::WaitingForManifestTasks;
}
if let AutonomousManifestDagState::Failed {
failed_task_ids, ..
} = manifest_state
{
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!(
"自主构建 manifest 专业任务失败;failedTaskIds={}",
failed_task_ids.join(",")
),
);
}
}
}
}
let mut resumed_provider_batch = if game_creator_agent_runtime_provider_action_batch_exists(
&root,
&agent_id,
@@ -1706,7 +1707,9 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
&agent_id,
&runtime.run_id,
)
.or_else(|| process_session_completion_blocker_at(&root, &agent_id, &runtime.run_id))
.or_else(|| {
process_session_completion_blocker_at(&root, &agent_id, &runtime.run_id)
})
.or_else(|| autonomous_game_build_completion_blocker_at_locked(&root, &runtime))
} else {
structured_plan_completion_blocker(&runtime)
@@ -1731,7 +1734,9 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
.or_else(|| {
game_creator_agent_goal_completion_blocker_at_locked(&root, &runtime)
})
.or_else(|| goal_contract_acceptance_completion_blocker_at_locked(&root, &runtime))
.or_else(|| {
goal_contract_acceptance_completion_blocker_at_locked(&root, &runtime)
})
.or_else(|| {
supervisor_collaboration_policy_completion_blocker_at_locked(
&root,
@@ -1790,26 +1795,22 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
match art_wait_state {
AutonomousCodePrototypeArtAssetWaitState::Waiting => {
let next_loop_index = loop_index.saturating_add(1);
if let Err(error) =
persist_waiting_autonomous_manifest_child_context_at(
&root,
&mut runtime,
&task,
&plan,
&mut observations,
next_loop_index,
&mut context_tracker,
blocker,
)
{
if let Err(error) = persist_waiting_autonomous_manifest_child_context_at(
&root,
&mut runtime,
&task,
&plan,
&mut observations,
next_loop_index,
&mut context_tracker,
blocker,
) {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!(
"持久化 code-prototype 美术依赖等待状态失败:{error}"
),
&format!("持久化 code-prototype 美术依赖等待状态失败:{error}"),
);
}
return AgentBackgroundTaskOutcome::WaitingForManifestTasks;

Some files were not shown because too many files have changed in this diff Show More