重整AGC官方路由与安全文件链路
记录当前 AGC 客户端、Tauri、API Server、后台查询和契约改动 保留后续按新 Router 后端方案重构的检查点
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+7
-13
@@ -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) {
|
||||
|
||||
+8
-8
@@ -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(
|
||||
|
||||
+36
-12
@@ -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 [
|
||||
|
||||
+13
-15
@@ -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
Reference in New Issue
Block a user