实现AGC直连运行时与陶泥儿美术闭环
新增直连 Codex 项目聊天、会话恢复与可读错误反馈 接入陶泥儿规范图、背景、固定四切片并校验实际渲染 同步游戏代码资源分类、项目版本 revision 与外部编辑器契约
This commit is contained in:
@@ -51,7 +51,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| --- | --- | --- | --- |
|
||||
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
|
||||
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
@@ -92,6 +92,8 @@ For image edit/redraw, confirming an upload is not sufficient: create a project
|
||||
|
||||
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
|
||||
|
||||
`sliceLayout: "grid-2x2"` is an opt-in contract for four fixed game-runtime assets. The provider prompt and server persistence both preserve the ordered slots left-top, right-top, left-bottom, right-bottom. Omit it to retain the default connected-component slicing behaviour for ordinary free-form icon sheets.
|
||||
|
||||
## Common Values
|
||||
|
||||
Use OpenAPI as the final authority; these common values are a routing aid:
|
||||
|
||||
@@ -78,9 +78,9 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
|
||||
|
||||
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
|
||||
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For the four-category game contract it must also send `sliceLayout: "grid-2x2"`; this is an explicit fixed-slot contract, not a client-side guessed crop.
|
||||
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require response `sliceLayout: "grid-2x2"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
|
||||
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"agentMode": "codex_app_server",
|
||||
"llm": {
|
||||
"apiKey": "",
|
||||
"baseUrl": "https://api.openai.com/v1",
|
||||
"model": "gpt-4.1",
|
||||
"baseUrl": "https://dev.genarrative.world/gpt/v1",
|
||||
"model": "gpt-5.6-sol",
|
||||
"apiKind": "openai_responses",
|
||||
"reasoningEffort": "high",
|
||||
"reasoningEffort": "max",
|
||||
"stream": false,
|
||||
"webSearchEnabled": false,
|
||||
"contextWindowTokens": 128000,
|
||||
|
||||
@@ -312,7 +312,7 @@ function assertContractRecordsMatch(label, leftRecords, rightRecords) {
|
||||
|
||||
function parseAppInvokeCommandNames(source) {
|
||||
return Array.from(
|
||||
source.matchAll(/invoke(?:<[^>]*>)?\(\s*['"]([a-z0-9_]+)['"]/g),
|
||||
source.matchAll(/(?:invoke|directInvoke)(?:<[^>]*>)?\(\s*['"]([a-z0-9_]+)['"]/g),
|
||||
([, command]) => command,
|
||||
);
|
||||
}
|
||||
@@ -1189,9 +1189,9 @@ const allowedLlmReasoningEfforts = new Set([
|
||||
'max',
|
||||
]);
|
||||
|
||||
if (defaultAppConfig.llm?.reasoningEffort !== 'high') {
|
||||
if (defaultAppConfig.llm?.reasoningEffort !== 'max') {
|
||||
throw new Error(
|
||||
'AI game creator shell default llm.reasoningEffort must stay high',
|
||||
'AI game creator shell default llm.reasoningEffort must stay max',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import fs from 'node:fs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import readline from 'node:readline';
|
||||
|
||||
const configPath = process.env.AGC_CONFIG_PATH ?? 'C:/Users/kdletters/AppData/Roaming/world.genarrative.ai-game-creator/game-creator.config.json';
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
const args = ['app-server', '--stdio', '-c', 'mcp_servers={}', '-c', 'web_search="disabled"', '-c', 'agents.enabled=false'];
|
||||
for (const feature of ['apps','browser_use','browser_use_external','browser_use_full_cdp_access','computer_use','goals','image_generation','in_app_browser','plugins','remote_plugin','shell_tool','tool_suggest','unified_exec','workspace_dependencies']) args.push('--disable', feature);
|
||||
args.push('-c', 'model_provider="genarrative_agc"', '-c', 'model_providers.genarrative_agc.name="Genarrative AGC"', '-c', `model_providers.genarrative_agc.base_url="${config.llm.baseUrl.replace(/\/$/, '')}"`, '-c', 'model_providers.genarrative_agc.env_key="GENARRATIVE_AGC_API_KEY"', '-c', 'model_providers.genarrative_agc.wire_api="responses"');
|
||||
const child = spawn('C:/Program Files/nodejs/node.exe', ['C:/Users/kdletters/AppData/Roaming/npm/node_modules/@openai/codex/bin/codex.js', ...args], { cwd: process.cwd(), env: { ...process.env, GENARRATIVE_AGC_API_KEY: config.llm.apiKey }, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
const send = (value) => child.stdin.write(`${JSON.stringify(value)}\n`);
|
||||
const timer = setTimeout(() => { console.log('smoke-timeout'); child.kill(); process.exit(1); }, Number(process.env.DIRECT_SMOKE_TIMEOUT_MS ?? 180000));
|
||||
readline.createInterface({ input: child.stdout }).on('line', (line) => {
|
||||
let message; try { message = JSON.parse(line); } catch { return; }
|
||||
if (message.id === 2) {
|
||||
console.log('thread-start-ok');
|
||||
const write = process.env.DIRECT_SMOKE_WRITE === '1';
|
||||
send({ id: 3, method: 'turn/start', params: { threadId: message.result.thread.id, input: [{ type: 'text', text: process.env.DIRECT_SMOKE_PROMPT ?? (write ? '在当前工作区创建 smoke-marker.txt,写入 DIRECT_CODEX_WRITE_OK,然后回复已完成。' : '只回复 DIRECT_CODEX_SMOKE_OK,不要修改文件。') }], model: config.llm.model, approvalPolicy: 'never', sandboxPolicy: write ? { type: 'workspaceWrite', writableRoots: [process.cwd()], networkAccess: true } : { type: 'readOnly', networkAccess: false } } });
|
||||
}
|
||||
if (message.id === 3 && message.error) { console.log(`turn-start-error:${message.error.message}`); clearTimeout(timer); child.kill(); process.exit(1); }
|
||||
if (message.method === 'turn/completed') { console.log(`turn-completed:${message.params?.turn?.status}`); clearTimeout(timer); child.kill(); process.exit(message.params?.turn?.status === 'completed' ? 0 : 1); }
|
||||
});
|
||||
send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'agc-direct-smoke', version: '0.1' }, capabilities: { experimentalApi: true } } });
|
||||
send({ method: 'initialized', params: {} });
|
||||
send({ id: 2, method: 'thread/start', params: { cwd: process.cwd(), model: config.llm.model, approvalPolicy: 'never', sandbox: process.env.DIRECT_SMOKE_WRITE === '1' ? 'workspace-write' : 'read-only', ephemeral: true } });
|
||||
@@ -11,6 +11,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
|
||||
mod codex_app_server;
|
||||
mod codex_cli;
|
||||
mod direct_runtime;
|
||||
mod generation;
|
||||
mod interaction;
|
||||
mod prompt;
|
||||
@@ -20,11 +21,13 @@ mod runtime_driver;
|
||||
mod runtime_protocol;
|
||||
mod runtime_state;
|
||||
mod runtime_tools;
|
||||
pub(crate) use codex_app_server::direct_game_creator_codex_chat_at;
|
||||
use codex_app_server::*;
|
||||
use codex_cli::*;
|
||||
pub(crate) use codex_cli::{
|
||||
game_creator_codex_cli_executable_path, game_creator_codex_cli_version_identity,
|
||||
};
|
||||
pub(crate) use direct_runtime::*;
|
||||
pub(crate) use generation::*;
|
||||
pub(crate) use interaction::*;
|
||||
pub(crate) use prompt::*;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -58,10 +58,12 @@ pub(crate) use canvas_generation::request_platform_art_asset_with_options_for_te
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use canvas_generation::{
|
||||
build_platform_art_asset_prompt, editor_api_key_is_configured, generate_platform_art_asset_at,
|
||||
maybe_generate_platform_art_asset_step, needs_platform_art_asset_generation,
|
||||
platform_art_asset_art_spec, platform_art_asset_output_extension_matches,
|
||||
prepare_platform_art_asset_output_path, project_canvas_asset_media_types,
|
||||
role_has_canvas_assets, suggested_canvas_tool_call, PlatformArtAssetGenerationOptions,
|
||||
generate_platform_art_asset_with_options_at,
|
||||
generate_platform_art_asset_with_required_slices_at, maybe_generate_platform_art_asset_step,
|
||||
needs_platform_art_asset_generation, platform_art_asset_art_spec,
|
||||
platform_art_asset_output_extension_matches, prepare_platform_art_asset_output_path,
|
||||
project_canvas_asset_media_types, role_has_canvas_assets, suggested_canvas_tool_call,
|
||||
PlatformArtAssetGenerationOptions,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use draft_validation::{
|
||||
|
||||
@@ -430,7 +430,8 @@ pub(crate) async fn external_editor_json_request(
|
||||
.map_err(|error| format!("{action}失败:{error}"))?;
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(format!("{action}失败:HTTP {}", status.as_u16()));
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(format_external_http_error(action, status, &body));
|
||||
}
|
||||
response
|
||||
.json::<serde_json::Value>()
|
||||
@@ -438,6 +439,92 @@ pub(crate) async fn external_editor_json_request(
|
||||
.map_err(|error| format!("解析{action}响应失败:{error}"))
|
||||
}
|
||||
|
||||
/// Keep provider validation details useful to the operator without copying an
|
||||
/// entire response body (which may contain URLs, ids, paths or credentials).
|
||||
fn format_external_http_error(action: &str, status: reqwest::StatusCode, body: &str) -> String {
|
||||
let detail = summarize_external_http_error_body(body);
|
||||
match detail {
|
||||
Some(detail) => format!("{action}失败:HTTP {}:{detail}", status.as_u16()),
|
||||
None => format!("{action}失败:HTTP {}", status.as_u16()),
|
||||
}
|
||||
}
|
||||
|
||||
fn summarize_external_http_error_body(body: &str) -> Option<String> {
|
||||
let payload = serde_json::from_str::<serde_json::Value>(body).ok()?;
|
||||
let mut fields = Vec::new();
|
||||
collect_external_http_error_fields(&payload, &mut fields);
|
||||
if fields.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut detail = fields.join(";");
|
||||
detail = redact_secret_tokens(&detail);
|
||||
detail = redact_absolute_path_tokens(&detail);
|
||||
for field in ["operationId", "operation_id", "api_key", "access_token"] {
|
||||
detail = redact_external_http_error_assignment(&detail, field);
|
||||
}
|
||||
detail.chars().take(600).collect::<String>().into()
|
||||
}
|
||||
|
||||
fn redact_external_http_error_assignment(value: &str, field: &str) -> String {
|
||||
let marker = format!("{field}=");
|
||||
let mut output = String::with_capacity(value.len());
|
||||
let mut cursor = 0usize;
|
||||
while let Some(offset) = value[cursor..].find(&marker) {
|
||||
let start = cursor + offset;
|
||||
output.push_str(&value[cursor..start]);
|
||||
output.push_str(&marker);
|
||||
output.push_str("[redacted]");
|
||||
let value_start = start + marker.len();
|
||||
let value_end = value[value_start..]
|
||||
.char_indices()
|
||||
.find_map(|(index, character)| {
|
||||
matches!(
|
||||
character,
|
||||
';' | ';' | ',' | ',' | '&' | '\n' | '\r' | '\t' | ' '
|
||||
)
|
||||
.then_some(value_start + index)
|
||||
})
|
||||
.unwrap_or(value.len());
|
||||
cursor = value_end;
|
||||
}
|
||||
output.push_str(&value[cursor..]);
|
||||
output
|
||||
}
|
||||
|
||||
fn collect_external_http_error_fields(value: &serde_json::Value, output: &mut Vec<String>) {
|
||||
const ALLOWED_KEYS: [&str; 7] = [
|
||||
"code", "field", "message", "reason", "detail", "error", "status",
|
||||
];
|
||||
match value {
|
||||
serde_json::Value::Object(map) => {
|
||||
for key in ALLOWED_KEYS {
|
||||
if let Some(value) = map.get(key) {
|
||||
match value {
|
||||
serde_json::Value::String(text) if !text.trim().is_empty() => {
|
||||
output.push(format!("{key}={}", text.trim()));
|
||||
}
|
||||
serde_json::Value::Number(number) => {
|
||||
output.push(format!("{key}={number}"));
|
||||
}
|
||||
_ => collect_external_http_error_fields(value, output),
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, child) in map {
|
||||
if !ALLOWED_KEYS.contains(&key.as_str()) {
|
||||
collect_external_http_error_fields(child, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::Value::Array(items) => {
|
||||
for item in items {
|
||||
collect_external_http_error_fields(item, output);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn external_generation_poll_after_ms(payload: &serde_json::Value) -> u64 {
|
||||
external_editor_response_data(payload)
|
||||
.get("pollAfterMs")
|
||||
@@ -791,6 +878,7 @@ pub(in crate::agent) struct PreparedPlatformArtAssetGeneration {
|
||||
warning: Option<String>,
|
||||
slice_warning: Option<String>,
|
||||
slices: Vec<PreparedPlatformArtAssetSlice>,
|
||||
spritesheet_slice_layout: Option<String>,
|
||||
generation_route: String,
|
||||
generation_kind: String,
|
||||
reference_resource_ids: Vec<String>,
|
||||
@@ -850,7 +938,11 @@ fn canonical_art_spec_reference_at(
|
||||
}
|
||||
|
||||
fn canonical_art_spritesheet_icon_descriptions(prompt: &str) -> Vec<String> {
|
||||
let project_context = truncate_inline(prompt.trim(), 320);
|
||||
// External Editor validates each description independently (currently at
|
||||
// 200 Unicode characters). Keep the gameplay context short enough that a
|
||||
// long creation request cannot reject the atlas before it is queued.
|
||||
const MAX_DESCRIPTION_CHARS: usize = 200;
|
||||
const CONTEXT_PREFIX: &str = ";遵循同一项目视觉规范:";
|
||||
[
|
||||
"第 1 类(左上):当前玩法的玩家主体或主要操作对象;只生成一个轮廓连贯、可独立使用的完整素材",
|
||||
"第 2 类(右上):当前玩法的方块、目标物、收集物、敌对实体或危险物;只生成一个完整素材",
|
||||
@@ -858,10 +950,36 @@ fn canonical_art_spritesheet_icon_descriptions(prompt: &str) -> Vec<String> {
|
||||
"第 4 类(右下):得分、受击、成长、失败、胜利或操作反馈特效;只生成一个完整素材",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|category| format!("{category};遵循同一项目视觉规范:{project_context}"))
|
||||
.map(|category| {
|
||||
let context_budget = MAX_DESCRIPTION_CHARS.saturating_sub(
|
||||
category
|
||||
.chars()
|
||||
.count()
|
||||
.saturating_add(CONTEXT_PREFIX.chars().count()),
|
||||
);
|
||||
let project_context = truncate_inline_bounded(prompt.trim(), context_budget);
|
||||
format!("{category}{CONTEXT_PREFIX}{project_context}")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn truncate_inline_bounded(value: &str, max_chars: usize) -> String {
|
||||
let normalized = value.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
let actual_chars = normalized.chars().count();
|
||||
if actual_chars <= max_chars {
|
||||
return normalized;
|
||||
}
|
||||
if max_chars <= 3 {
|
||||
return ".".repeat(max_chars);
|
||||
}
|
||||
let mut output = normalized
|
||||
.chars()
|
||||
.take(max_chars.saturating_sub(3))
|
||||
.collect::<String>();
|
||||
output.push_str("...");
|
||||
output
|
||||
}
|
||||
|
||||
fn decode_platform_art_image_with_limits(
|
||||
download: &CanvasResourceDownload,
|
||||
label: &str,
|
||||
@@ -1099,7 +1217,7 @@ async fn prepare_platform_art_spritesheet_slices(
|
||||
Ok(prepared)
|
||||
}
|
||||
|
||||
pub(in crate::agent) async fn generate_platform_art_asset_with_options_at(
|
||||
pub(crate) async fn generate_platform_art_asset_with_options_at(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
briefs: &[AgentGroupBrief],
|
||||
@@ -1117,6 +1235,32 @@ pub(in crate::agent) async fn generate_platform_art_asset_with_options_at(
|
||||
commit_prepared_platform_art_asset_at(root, prepared, options, |_| Ok(()))
|
||||
}
|
||||
|
||||
/// Generates the canonical game spritesheet together with the four durable
|
||||
/// core slices. Callers that promise a playable game must use this instead
|
||||
/// of the permissive asset path: a bare spritesheet is not enough evidence
|
||||
/// that player, target, obstacle, and feedback visuals are available.
|
||||
pub(crate) async fn generate_platform_art_asset_with_required_slices_at(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
briefs: &[AgentGroupBrief],
|
||||
options: &PlatformArtAssetGenerationOptions,
|
||||
) -> Result<GeneratedPlatformArtAsset, String> {
|
||||
if options.asset_kind != "art-spritesheet" {
|
||||
return Err("严格游戏切片生成只允许 art-spritesheet 资产类型".to_string());
|
||||
}
|
||||
{
|
||||
let recovery_lock =
|
||||
acquire_project_write_lock(root, "canvas.asset_generate.strict.recover")?;
|
||||
recover_interrupted_strict_platform_art_transaction_locked_at(root, &recovery_lock)?;
|
||||
}
|
||||
let prepared =
|
||||
request_platform_art_asset_with_options_at(root, prompt, briefs, options).await?;
|
||||
let lock = acquire_project_write_lock(root, "canvas.asset_generate.strict")?;
|
||||
recover_interrupted_strict_platform_art_transaction_locked_at(root, &lock)?;
|
||||
advance_agent_runtime_project_revision_locked(root)?;
|
||||
commit_prepared_platform_art_asset_strict_slices_at(root, prepared, options, |_| Ok(()))
|
||||
}
|
||||
|
||||
pub(in crate::agent) async fn request_platform_art_asset_with_options_at(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
@@ -1249,7 +1393,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
let is_canonical_art_spritesheet = options.asset_kind == "art-spritesheet";
|
||||
let canonical_reference = matches!(
|
||||
options.asset_kind.as_str(),
|
||||
"ui-prototype" | "art-spritesheet"
|
||||
"ui-prototype" | "game-background" | "art-spritesheet"
|
||||
)
|
||||
.then(|| canonical_art_spec_reference_at(root, &canvas_context.project_id))
|
||||
.transpose()?;
|
||||
@@ -1262,6 +1406,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
serde_json::json!({
|
||||
"referenceId": reference_id,
|
||||
"iconDescriptions": canonical_art_spritesheet_icon_descriptions(&generation_prompt),
|
||||
"sliceLayout": "grid-2x2",
|
||||
"screenColor": "auto",
|
||||
"aspectRatio": options.aspect_ratio,
|
||||
"imageSize": options.image_size,
|
||||
@@ -1346,6 +1491,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
.await?;
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
if external_generation_submit_rejection_is_definitive(status) {
|
||||
if let Some(context) = runtime_context {
|
||||
remove_platform_art_generation_runtime_state_at(
|
||||
@@ -1354,15 +1500,23 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
&context.run_id,
|
||||
)?;
|
||||
}
|
||||
return Err(format!("请求平台图片生成失败:HTTP {}", status.as_u16()));
|
||||
return Err(format_external_http_error(
|
||||
"请求平台图片生成",
|
||||
status,
|
||||
&body,
|
||||
));
|
||||
}
|
||||
if runtime_context.is_some() {
|
||||
return Err(format!(
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 平台图片生成提交返回 HTTP {},服务端是否已产生副作用未知",
|
||||
status.as_u16()
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} {},服务端是否已产生副作用未知",
|
||||
format_external_http_error("平台图片生成提交", status, &body)
|
||||
));
|
||||
}
|
||||
return Err(format!("请求平台图片生成失败:HTTP {}", status.as_u16()));
|
||||
return Err(format_external_http_error(
|
||||
"请求平台图片生成",
|
||||
status,
|
||||
&body,
|
||||
));
|
||||
}
|
||||
let submission_payload = response
|
||||
.json::<serde_json::Value>()
|
||||
@@ -1498,6 +1652,11 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
Vec::new()
|
||||
};
|
||||
let warning = platform_art_generation_warning(generated);
|
||||
let spritesheet_slice_layout = if is_canonical_art_spritesheet {
|
||||
json_string_field(generated, "sliceLayout")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let resource_id = json_string_field(resource, "resourceId");
|
||||
let task_id = if is_canonical_art_spritesheet {
|
||||
consistent_canvas_task_id("External Editor 图集主图", &[generated, resource, asset])?
|
||||
@@ -1553,6 +1712,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
warning,
|
||||
slice_warning,
|
||||
slices,
|
||||
spritesheet_slice_layout,
|
||||
generation_route,
|
||||
generation_kind,
|
||||
reference_resource_ids,
|
||||
@@ -4869,6 +5029,7 @@ fn validate_strict_platform_art_spritesheet_contract(
|
||||
task_id: Option<&str>,
|
||||
generation_route: &str,
|
||||
generation_kind: &str,
|
||||
spritesheet_slice_layout: Option<&str>,
|
||||
reference_resource_ids: &[String],
|
||||
has_transparent_pixels: bool,
|
||||
has_visible_pixels: bool,
|
||||
@@ -4905,6 +5066,11 @@ fn validate_strict_platform_art_spritesheet_contract(
|
||||
{
|
||||
return Err("game-chat 图集生成 route/kind 与严格图集合同不一致".to_string());
|
||||
}
|
||||
if spritesheet_slice_layout.map(str::trim) != Some("grid-2x2") {
|
||||
return Err(
|
||||
"game-chat 图集必须由 External Editor 以 grid-2x2 固定切片合同生成".to_string(),
|
||||
);
|
||||
}
|
||||
if reference_resource_ids.len() != 1
|
||||
|| reference_resource_ids[0].trim().is_empty()
|
||||
|| reference_resource_ids[0].trim() == resource_id
|
||||
@@ -5248,6 +5414,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
|
||||
warning,
|
||||
mut slice_warning,
|
||||
slices,
|
||||
spritesheet_slice_layout,
|
||||
generation_route,
|
||||
generation_kind,
|
||||
reference_resource_ids,
|
||||
@@ -5266,6 +5433,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
|
||||
task_id.as_deref(),
|
||||
&generation_route,
|
||||
&generation_kind,
|
||||
spritesheet_slice_layout.as_deref(),
|
||||
&reference_resource_ids,
|
||||
spritesheet_has_transparent_pixels,
|
||||
spritesheet_has_visible_pixels,
|
||||
@@ -5685,6 +5853,17 @@ pub(crate) fn platform_art_asset_art_spec(
|
||||
"references": [],
|
||||
});
|
||||
}
|
||||
if options.asset_kind == "game-background" {
|
||||
return serde_json::json!({
|
||||
"assetType": "background",
|
||||
"subject": format!("{};使用项目原创命名和原创场景设计", options.asset_label),
|
||||
"style": "原创横屏 Web 游戏场景背景;层次清晰,保留可玩区域的视觉留白,不包含 UI 或文字",
|
||||
"composition": "严格 16:9 单场景背景,前景、中景、远景有明确层次;不出现角色、可收集物、操作按钮、HUD、Logo 或文字",
|
||||
"format": format!("{} {}", options.aspect_ratio, options.image_size),
|
||||
"constraints": "必须是可见的真实图片产物,适合作为 Canvas 或 HTML 游戏场景底图;不得做成素材图集、完整游戏截图、海报或概念板;必须原创,不得复刻现有游戏场景、Logo、贴图、标志性布局或受保护视觉语言",
|
||||
"references": [],
|
||||
});
|
||||
}
|
||||
serde_json::json!({
|
||||
"assetType": "art",
|
||||
"subject": format!("{};使用项目原创命名和原创阵营设计", options.asset_label),
|
||||
@@ -5713,6 +5892,12 @@ pub(crate) fn build_platform_art_asset_prompt(
|
||||
truncate_prompt_context(prompt.trim())
|
||||
);
|
||||
}
|
||||
if options.asset_kind == "game-background" {
|
||||
return format!(
|
||||
"为 Web 小游戏生成一张可直接作为运行画面底图的原创 16:9 场景背景。严格从下方用户需求提炼自己的游戏主题、地点、季节、材质和氛围;画面要为真实可玩区域留出足够清楚的中部空间,并有前景、中景、远景层次。不得画玩家角色、道具、棋子、障碍、HUD、操作按钮、文字、Logo、完整游戏截图、海报或素材图集;这些元素会从独立透明核心图集中绘制。不得自行假设为塔防或加入玩法合同中不存在的实体;必须原创,不得复刻现有游戏场景、贴图、标志性布局或受保护视觉语言。\n\n用户需求:{}",
|
||||
truncate_prompt_context(prompt.trim())
|
||||
);
|
||||
}
|
||||
let art_asset_brief = briefs
|
||||
.iter()
|
||||
.flat_map(|brief| brief.role_briefs.iter())
|
||||
@@ -5737,6 +5922,28 @@ mod canvas_generation_tests {
|
||||
ColorType, ImageEncoder,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn external_http_error_summary_keeps_validation_details_without_sensitive_context() {
|
||||
let body = serde_json::json!({
|
||||
"error": {
|
||||
"code": "invalid-request",
|
||||
"field": "sliceLayout",
|
||||
"message": "只支持 grid-2x2;operationId=private-operation-id;api_key=private-key",
|
||||
},
|
||||
"details": {
|
||||
"path": "C:\\Users\\private\\secret.json",
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
let summary = summarize_external_http_error_body(&body).expect("summary");
|
||||
assert!(summary.contains("code=invalid-request"), "{summary}");
|
||||
assert!(summary.contains("field=sliceLayout"), "{summary}");
|
||||
assert!(summary.contains("只支持 grid-2x2"), "{summary}");
|
||||
assert!(!summary.contains("private-operation-id"), "{summary}");
|
||||
assert!(!summary.contains("private-key"), "{summary}");
|
||||
assert!(!summary.contains("C:\\Users\\private"), "{summary}");
|
||||
}
|
||||
|
||||
fn read_test_http_request(stream: &mut std::net::TcpStream) -> String {
|
||||
stream
|
||||
.set_nonblocking(false)
|
||||
@@ -5876,6 +6083,57 @@ mod canvas_generation_tests {
|
||||
assert!(slices.iter().all(|slice| slice.extension == "png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_spritesheet_contract_requires_the_grid_2x2_provider_receipt() {
|
||||
let canvas_context = ExternalCanvasGenerationContext {
|
||||
project_id: "canvas-project".to_string(),
|
||||
asset_folder_id: "asset-folder".to_string(),
|
||||
canvas_name: "contract-test".to_string(),
|
||||
};
|
||||
let slices = (0..4)
|
||||
.map(|index| {
|
||||
let download = rgba_test_png(100 + index);
|
||||
let validated = validate_platform_art_png_bytes_with_limits(
|
||||
&download.bytes,
|
||||
"strict layout receipt test slice",
|
||||
)
|
||||
.expect("fixture slice should validate");
|
||||
PreparedPlatformArtAssetSlice {
|
||||
name: format!("素材 {}", index + 1),
|
||||
width: validated.width,
|
||||
height: validated.height,
|
||||
download,
|
||||
resource_id: Some(format!("slice-resource-{index}")),
|
||||
asset_object_id: Some(format!("slice-object-{index}")),
|
||||
canvas_project_id: Some("canvas-project".to_string()),
|
||||
task_id: Some("spritesheet-task".to_string()),
|
||||
source_resource_id: Some("spritesheet-resource".to_string()),
|
||||
content_sha256: validated.content_sha256,
|
||||
pixel_sha256: validated.pixel_sha256,
|
||||
has_visible_pixels: true,
|
||||
extension: "png".to_string(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let error = validate_strict_platform_art_spritesheet_contract(
|
||||
&slices,
|
||||
&canvas_context,
|
||||
Some("canvas-project"),
|
||||
Some("spritesheet-resource"),
|
||||
Some("spritesheet-object"),
|
||||
Some("spritesheet-task"),
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
"icon-spritesheet",
|
||||
None,
|
||||
&["art-spec-resource".to_string()],
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.expect_err("missing fixed-layout proof must not be accepted");
|
||||
|
||||
assert!(error.contains("grid-2x2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_spritesheet_rejects_task_id_conflicts_across_all_identity_copies() {
|
||||
let generated = serde_json::json!({
|
||||
@@ -7078,6 +7336,16 @@ mod canvas_generation_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_art_spritesheet_descriptions_obey_external_editor_item_limit() {
|
||||
let descriptions = canonical_art_spritesheet_icon_descriptions(&"原创玩法需求".repeat(128));
|
||||
|
||||
assert_eq!(descriptions.len(), 4);
|
||||
assert!(descriptions
|
||||
.iter()
|
||||
.all(|description| description.chars().count() <= 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derived_visual_asset_waits_for_registered_art_spec() {
|
||||
let temporary = tempfile::tempdir().expect("create art dependency project");
|
||||
@@ -7214,6 +7482,7 @@ mod canvas_generation_tests {
|
||||
warning: None,
|
||||
slice_warning: None,
|
||||
slices: Vec::new(),
|
||||
spritesheet_slice_layout: Some("grid-2x2".to_string()),
|
||||
generation_route: "/api/external/v1/editor/icon-spritesheets/generations".to_string(),
|
||||
generation_kind: "icon-spritesheet".to_string(),
|
||||
reference_resource_ids: vec!["art-spec-resource".to_string()],
|
||||
@@ -7529,6 +7798,7 @@ mod canvas_generation_tests {
|
||||
warning: None,
|
||||
slice_warning: None,
|
||||
slices,
|
||||
spritesheet_slice_layout: Some("grid-2x2".to_string()),
|
||||
generation_route: "/api/external/v1/editor/icon-spritesheets/generations".to_string(),
|
||||
generation_kind: "icon-spritesheet".to_string(),
|
||||
reference_resource_ids: vec!["art-spec-resource".to_string()],
|
||||
|
||||
@@ -13,6 +13,10 @@ pub(crate) enum CliCommand {
|
||||
agent_id: String,
|
||||
prompt: String,
|
||||
},
|
||||
DirectCodexChat {
|
||||
project_path: PathBuf,
|
||||
prompt: String,
|
||||
},
|
||||
AgentTask {
|
||||
project_path: PathBuf,
|
||||
agent_id: String,
|
||||
@@ -186,6 +190,7 @@ impl CliCommand {
|
||||
..
|
||||
} => Some((project_path, *initialize)),
|
||||
Self::AgentChat { project_path, .. }
|
||||
| Self::DirectCodexChat { project_path, .. }
|
||||
| Self::AgentRuntimeStatus { project_path, .. }
|
||||
| Self::AgentContextCompact { project_path, .. }
|
||||
| Self::AgentGoalStatus { project_path, .. }
|
||||
@@ -687,6 +692,19 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
|
||||
prompt: prompt.to_string(),
|
||||
}));
|
||||
}
|
||||
if args.first().map(String::as_str) == Some("--direct-codex-chat") {
|
||||
if args.len() < 3 {
|
||||
return Err("用法:--direct-codex-chat <本地项目绝对路径> <聊天内容>".to_string());
|
||||
}
|
||||
let prompt = args[2..].join(" ");
|
||||
if prompt.trim().is_empty() {
|
||||
return Err("聊天内容不能为空".to_string());
|
||||
}
|
||||
return Ok(Some(CliCommand::DirectCodexChat {
|
||||
project_path: PathBuf::from(&args[1]),
|
||||
prompt: prompt.trim().to_string(),
|
||||
}));
|
||||
}
|
||||
if args.first().map(String::as_str) == Some("--swarm-chat") {
|
||||
const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] [--game-chat-smoke] <本地项目绝对路径> [parentAgentId]";
|
||||
let mut rest = args[1..].to_vec();
|
||||
@@ -929,6 +947,36 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
|
||||
println!("replyText={}", reply.reply_text);
|
||||
Ok(())
|
||||
}
|
||||
CliCommand::DirectCodexChat {
|
||||
project_path,
|
||||
prompt,
|
||||
} => {
|
||||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", true)?;
|
||||
if !project_path.join(".agent/manifest.json").is_file() {
|
||||
let project_name = project_path
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("Codex 直连项目");
|
||||
init_local_game_project_at(
|
||||
&project_path,
|
||||
&format!("direct-codex-{}", unix_millis()),
|
||||
project_name,
|
||||
)?;
|
||||
}
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
|
||||
let reply_result = runtime
|
||||
.block_on(async { run_direct_game_creator_turn_at(&project_path, &prompt).await });
|
||||
let shutdown_result = shutdown_game_creator_codex_app_servers();
|
||||
let reply = reply_result?;
|
||||
shutdown_result?;
|
||||
println!("direct-codex.chat.completed");
|
||||
println!("projectPath={}", project_path.display());
|
||||
println!("replyText={reply}");
|
||||
Ok(())
|
||||
}
|
||||
CliCommand::AgentTask {
|
||||
project_path,
|
||||
agent_id,
|
||||
|
||||
@@ -1215,10 +1215,10 @@ const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.jso
|
||||
const GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER: &str = "codex_app_server";
|
||||
const GAME_CREATOR_AGENT_MODE_CODEX_CLI: &str = "codex_cli";
|
||||
const GAME_CREATOR_AGENT_MODE_PROVIDER: &str = "provider";
|
||||
const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://api.openai.com/v1";
|
||||
const DEFAULT_GAME_CREATOR_LLM_MODEL: &str = "gpt-4.1";
|
||||
const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://dev.genarrative.world/gpt/v1";
|
||||
const DEFAULT_GAME_CREATOR_LLM_MODEL: &str = "gpt-5.6-sol";
|
||||
const DEFAULT_GAME_CREATOR_LLM_API_KIND: &str = "openai_responses";
|
||||
const DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT: &str = "high";
|
||||
const DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT: &str = "max";
|
||||
const DEFAULT_GAME_CREATOR_LLM_CONTEXT_WINDOW_TOKENS: u64 = 128_000;
|
||||
const DEFAULT_GAME_CREATOR_LLM_AUTO_COMPACT_TOKEN_LIMIT: u64 = 64_000;
|
||||
const DEFAULT_GAME_CREATOR_LLM_TOOL_OUTPUT_TOKEN_LIMIT: u64 = 12_000;
|
||||
@@ -1841,6 +1841,11 @@ where
|
||||
}
|
||||
|
||||
fn handle_game_creator_gui_run_event(event: &tauri::RunEvent) {
|
||||
if matches!(event, tauri::RunEvent::Exit) {
|
||||
if let Err(error) = agent::shutdown_game_creator_codex_app_servers() {
|
||||
eprintln!("agent.direct_codex.gui_exit.shutdown_failed: {error}");
|
||||
}
|
||||
}
|
||||
match resolve_game_creator_gui_runner_shutdown(event, shutdown_external_agent_runner) {
|
||||
GameCreatorGuiRunnerShutdownOutcome::NotRequested => {}
|
||||
GameCreatorGuiRunnerShutdownOutcome::Requested => {
|
||||
@@ -2192,6 +2197,7 @@ fn main() {
|
||||
chat_with_game_creator_agent,
|
||||
chat_with_game_creator_role_agent,
|
||||
chat_with_game_creator_role_agent_stream,
|
||||
chat_with_game_creator_direct_codex,
|
||||
start_game_creator_agent_runtime_task,
|
||||
start_game_creator_supervisor_runtime_task,
|
||||
compact_game_creator_agent_runtime_context,
|
||||
|
||||
@@ -590,10 +590,18 @@ pub(crate) fn resolve_preview_path(root: &Path, url_path: &str) -> Result<PathBu
|
||||
let mut file_path = root.to_path_buf();
|
||||
let mut parts = relative.split('/');
|
||||
let first = parts.next().ok_or_else(|| "预览路径非法".to_string())?;
|
||||
if first != "game" && first != "assets" {
|
||||
return Err("预览路径只能访问 game/ 或 assets/".to_string());
|
||||
if first.is_empty() || first == "." || first == ".." || first.contains('\\') {
|
||||
return Err("预览路径非法".to_string());
|
||||
}
|
||||
if first == "game" || first == "assets" {
|
||||
file_path.push(first);
|
||||
} else {
|
||||
// `/` serves `game/index.html`, so browser-relative resources such as
|
||||
// `style.css` and `scripts/game.js` must resolve from the same game root.
|
||||
// Explicit `/assets/...` URLs keep their project-level asset mapping.
|
||||
file_path.push("game");
|
||||
file_path.push(first);
|
||||
}
|
||||
file_path.push(first);
|
||||
for part in parts {
|
||||
if part.is_empty() || part == "." || part == ".." || part.contains('\\') {
|
||||
return Err("预览路径非法".to_string());
|
||||
|
||||
@@ -660,6 +660,74 @@ pub(crate) fn ensure_initial_game_iteration_version_at(
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends the formal version produced by a successful direct Agent turn.
|
||||
///
|
||||
/// The project revision is the cross-surface ordering boundary: the launcher
|
||||
/// rejects a same-revision manifest whose assets or versions differ from the
|
||||
/// snapshot it already rendered. Therefore callers must advance the durable
|
||||
/// project revision first and pass that new value here.
|
||||
pub(crate) fn append_agent_game_iteration_version_at(
|
||||
root: &Path,
|
||||
project_revision: u64,
|
||||
) -> Result<bool, String> {
|
||||
if project_revision == 0 {
|
||||
return Err("Agent 项目版本必须绑定大于 0 的项目 revision".to_string());
|
||||
}
|
||||
mutate_manifest_at(root, |manifest| {
|
||||
let resource_bindings = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.map(|asset| GameIterationVersionResourceBinding {
|
||||
slot_id: format!("asset:{}", asset.id),
|
||||
resource_id: asset.id.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(existing) = manifest
|
||||
.versions
|
||||
.iter()
|
||||
.find(|version| version.project_revision == project_revision)
|
||||
{
|
||||
if existing.resource_bindings == resource_bindings {
|
||||
return Ok(false);
|
||||
}
|
||||
return Err(format!(
|
||||
"项目 revision {project_revision} 已绑定不同的正式版本"
|
||||
));
|
||||
}
|
||||
|
||||
let (version_id, parent_version_id, created_reason) =
|
||||
if let Some(parent) = manifest.versions.last() {
|
||||
if project_revision <= parent.project_revision {
|
||||
return Err(format!(
|
||||
"Agent 项目版本 revision {project_revision} 必须大于父版本 revision {}",
|
||||
parent.project_revision
|
||||
));
|
||||
}
|
||||
(
|
||||
format!("agent-{project_revision}"),
|
||||
Some(parent.version_id.clone()),
|
||||
GameIterationVersionCreatedReason::AgentRevision,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("initial-{project_revision}"),
|
||||
None,
|
||||
GameIterationVersionCreatedReason::Initial,
|
||||
)
|
||||
};
|
||||
manifest.versions.push(GameIterationVersion {
|
||||
version_id,
|
||||
parent_version_id,
|
||||
project_revision,
|
||||
resource_bindings,
|
||||
created_reason,
|
||||
created_at: unix_timestamp(),
|
||||
edit_prompt: None,
|
||||
});
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_manifest_has_seed_tasks(
|
||||
root: &Path,
|
||||
goal: Option<&str>,
|
||||
|
||||
@@ -2839,6 +2839,7 @@ fn preview_path_rejects_traversal() {
|
||||
fs::create_dir_all(project.join("memory")).expect("memory dir");
|
||||
fs::create_dir_all(project.join(".agent")).expect("agent dir");
|
||||
fs::write(project.join("game/index.html"), "<!doctype html>").expect("index");
|
||||
fs::write(project.join("game/style.css"), "body {}").expect("game style");
|
||||
fs::write(project.join("assets/player.png"), b"png").expect("asset");
|
||||
fs::write(project.join("memory/project.md"), "secret memory").expect("memory");
|
||||
fs::write(project.join(".agent/run.latest.json"), "{}").expect("trace");
|
||||
@@ -2861,6 +2862,13 @@ fn preview_path_rejects_traversal() {
|
||||
.canonicalize()
|
||||
.expect("canonical asset")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_preview_path(&project, "/style.css?cache=1").unwrap(),
|
||||
project
|
||||
.join("game/style.css")
|
||||
.canonicalize()
|
||||
.expect("canonical game style")
|
||||
);
|
||||
|
||||
fs::remove_dir_all(project).ok();
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ import {
|
||||
normalizeAgentRuntimeState,
|
||||
projectNameFromPath,
|
||||
projectProfessionalAgentLabel,
|
||||
projectRuntimeVisibleError,
|
||||
projectSupervisorPendingRepairMatchesProfessional,
|
||||
projectSupervisorResponseStreamIdentity,
|
||||
readProjectSupervisorActiveSessionId,
|
||||
@@ -252,6 +253,7 @@ const LEGACY_GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY =
|
||||
'genarrative.game-chat.auto-preview-authorization.v1';
|
||||
const GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY =
|
||||
'genarrative.game-chat.auto-preview-authorization.v2';
|
||||
const DIRECT_CODEX_PRODUCT_RUNTIME = true;
|
||||
|
||||
type GameChatAutoPreviewAuthorization = {
|
||||
afterRevision: number;
|
||||
@@ -599,6 +601,11 @@ export function App({
|
||||
onAgentRuntimeSummariesChange,
|
||||
onAgentResultsChange,
|
||||
}: AppProps = {}) {
|
||||
const directCodexProductRuntime =
|
||||
DIRECT_CODEX_PRODUCT_RUNTIME &&
|
||||
projectSupervisorOnly &&
|
||||
!supervisorChatOnly &&
|
||||
!gameChatOnly;
|
||||
const [devMode] = useState(() =>
|
||||
projectSupervisorOnly ? false : isDeveloperMode(),
|
||||
);
|
||||
@@ -1314,6 +1321,26 @@ export function App({
|
||||
}
|
||||
initialProjectOpenedRef.current = true;
|
||||
if (projectSupervisorOnly) {
|
||||
if (directCodexProductRuntime) {
|
||||
if (!isAbsoluteProjectPath(initialProjectPath)) {
|
||||
setWorkspaceStatus('请提供工作区绝对路径');
|
||||
return;
|
||||
}
|
||||
if (projectPathHasControlCharacter(initialProjectPath)) {
|
||||
setWorkspaceStatus('工作区路径不能包含控制字符');
|
||||
return;
|
||||
}
|
||||
// This component is mounted inside the already-created project
|
||||
// workbench. Hydrate localProject before the first direct turn so the
|
||||
// direct runtime cannot silently create a second workspace.
|
||||
void openWorkspace(
|
||||
initialProjectPath,
|
||||
false,
|
||||
'open',
|
||||
initialProjectKind,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (gameChatOnly) {
|
||||
void openGameChatProjectPath(initialProjectPath);
|
||||
return;
|
||||
@@ -2937,14 +2964,46 @@ export function App({
|
||||
const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1;
|
||||
projectSupervisorHistoryLoadVersionRef.current = loadVersion;
|
||||
try {
|
||||
const resumeError = await resumeProjectSupervisorRuntimeTasksIfNeeded(
|
||||
invoke,
|
||||
nextProjectPath,
|
||||
);
|
||||
const sessionId = await readProjectSupervisorActiveSession(
|
||||
const resumeError = directCodexProductRuntime
|
||||
? ''
|
||||
: await resumeProjectSupervisorRuntimeTasksIfNeeded(
|
||||
invoke,
|
||||
nextProjectPath,
|
||||
);
|
||||
if (directCodexProductRuntime) {
|
||||
projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||||
}
|
||||
let sessionId = await readProjectSupervisorActiveSession(
|
||||
invoke,
|
||||
nextProjectPath,
|
||||
);
|
||||
let persistedSupervisorRuntimeResult: AgentRuntimeResult | null = null;
|
||||
let runtimeError = '';
|
||||
if (!sessionId && directCodexProductRuntime) {
|
||||
try {
|
||||
const runtimeResults = await invoke<AgentRuntimeResult[]>(
|
||||
'read_game_creator_agent_runtimes',
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
persistedSupervisorRuntimeResult =
|
||||
runtimeResults
|
||||
.filter(
|
||||
(runtimeResult) =>
|
||||
runtimeResult.state.agentId ===
|
||||
PROJECT_SUPERVISOR_AGENT_ID,
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
right.state.updatedAt - left.state.updatedAt,
|
||||
)[0] ?? null;
|
||||
sessionId =
|
||||
persistedSupervisorRuntimeResult?.state.sessionId || null;
|
||||
} catch (error) {
|
||||
runtimeError =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
const projectConversation = await invoke<LocalConversationResult>(
|
||||
'read_local_conversation',
|
||||
{
|
||||
@@ -2955,7 +3014,6 @@ export function App({
|
||||
let supervisorConversation: LocalConversationResult | null = null;
|
||||
let runtime: AgentRuntimeState | null = null;
|
||||
let runtimeResponseStream: AgentRuntimeResponseStream | null = null;
|
||||
let runtimeError = '';
|
||||
if (sessionId) {
|
||||
supervisorConversation = await invoke<LocalConversationResult>(
|
||||
'read_local_conversation',
|
||||
@@ -2965,6 +3023,14 @@ export function App({
|
||||
sessionId,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (persistedSupervisorRuntimeResult) {
|
||||
runtime = agentRuntimeStateFromResult(
|
||||
persistedSupervisorRuntimeResult,
|
||||
);
|
||||
runtimeResponseStream =
|
||||
persistedSupervisorRuntimeResult.responseStream ?? null;
|
||||
} else if (sessionId) {
|
||||
try {
|
||||
const runtimeResult = await invoke<AgentRuntimeResult>(
|
||||
'read_game_creator_agent_runtime',
|
||||
@@ -3209,6 +3275,9 @@ export function App({
|
||||
},
|
||||
]);
|
||||
}
|
||||
// Direct Codex must not resume legacy Supervisor work, but existing
|
||||
// projects still need their conversation and run history hydrated as
|
||||
// read-only evidence.
|
||||
void loadProjectConversation(openedProject.projectPath);
|
||||
void refreshAgentRunTrace(openedProject.projectPath);
|
||||
} catch (error) {
|
||||
@@ -5755,7 +5824,144 @@ export function App({
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
async function refreshDirectProjectSurface(
|
||||
invoke: TauriInvoke,
|
||||
nextProjectPath: string,
|
||||
) {
|
||||
try {
|
||||
await refreshManifest(nextProjectPath);
|
||||
} catch {
|
||||
// The direct turn result is still authoritative for the chat. A missing
|
||||
// or legacy manifest must not hide the Codex reply.
|
||||
}
|
||||
try {
|
||||
const current = await invoke<LocalPreviewStatus>(
|
||||
'get_local_game_preview_status',
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
if (current.status === 'running' && current.url && current.port) {
|
||||
updateClientPreview({
|
||||
url: current.url,
|
||||
port: current.port,
|
||||
root: current.root ?? nextProjectPath,
|
||||
});
|
||||
setPreviewStatus(`运行中:127.0.0.1:${current.port}`);
|
||||
return;
|
||||
}
|
||||
const started = await invoke<LocalPreviewResult>(
|
||||
'start_local_game_preview',
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
updateClientPreview(started);
|
||||
setPreviewStatus(`运行中:127.0.0.1:${started.port}`);
|
||||
setCommandLog((currentLog) => [...currentLog, 'preview.start']);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setPreviewStatus(`预览未启动:${message}`);
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: `智能创作已返回,但客户端预览启动失败:${message}`,
|
||||
runtimeOwned: true,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async function executeChatAgentReply(prompt: string) {
|
||||
// Product default: send the conversation directly to Codex app-server.
|
||||
// The legacy Supervisor/harness path remains below for rollback and tests.
|
||||
if (directCodexProductRuntime) {
|
||||
const directInvoke = resolveTauriInvoke();
|
||||
let directProjectPath = resolveChatProjectPath(localProject);
|
||||
if (!directProjectPath && directInvoke) {
|
||||
try {
|
||||
const created = await directInvoke<InitLocalProjectResult>(
|
||||
'create_automatic_local_game_project',
|
||||
);
|
||||
await openWorkspace(created.projectPath, false, 'open');
|
||||
directProjectPath = created.projectPath;
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: `自动创建 AGC 项目失败:${message}`,
|
||||
runtimeOwned: true,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
]);
|
||||
setProjectSupervisorRuntimeError(`智能创作执行失败:${message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (directProjectPath && directInvoke) {
|
||||
setChatAgentBusy(true);
|
||||
setProjectSupervisorRuntimeError('');
|
||||
try {
|
||||
const reply = await directInvoke<string>(
|
||||
'chat_with_game_creator_direct_codex',
|
||||
{
|
||||
projectPath: directProjectPath,
|
||||
prompt,
|
||||
},
|
||||
);
|
||||
if (localProjectPathRef.current === directProjectPath) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: reply,
|
||||
runtimeOwned: true,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
]);
|
||||
await refreshDirectProjectSurface(directInvoke, directProjectPath);
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
const visibleMessage = projectRuntimeVisibleError(
|
||||
message,
|
||||
'陶泥儿智能创作',
|
||||
true,
|
||||
);
|
||||
if (localProjectPathRef.current === directProjectPath) {
|
||||
setProjectSupervisorRuntimeError(visibleMessage);
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: visibleMessage,
|
||||
runtimeOwned: true,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
} finally {
|
||||
setChatAgentBusy(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const message = directInvoke
|
||||
? '当前项目尚未准备好,无法启动智能创作。'
|
||||
: '需要在 Tauri App 内运行,无法启动智能创作。';
|
||||
setProjectSupervisorRuntimeError(message);
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: message,
|
||||
runtimeOwned: true,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
||||
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
||||
return;
|
||||
@@ -5951,7 +6157,11 @@ export function App({
|
||||
|
||||
useEffect(() => {
|
||||
const latch = initialSupervisorMessageLatchRef.current;
|
||||
if (!gameChatOnly || !latch.prompt || !localProject) {
|
||||
if (
|
||||
(!directCodexProductRuntime && !gameChatOnly) ||
|
||||
!latch.prompt ||
|
||||
!localProject
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (localProject.projectPath !== latch.projectPath) {
|
||||
@@ -10334,6 +10544,9 @@ export function App({
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (directCodexProductRuntime) {
|
||||
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
|
||||
}
|
||||
if (agentRuntimeResumeProjectPathRef.current !== nextProjectPath) {
|
||||
try {
|
||||
const resumedRuntimes = await invoke<AgentRuntimeResult[]>(
|
||||
@@ -11059,15 +11272,20 @@ export function App({
|
||||
return (
|
||||
<ProjectSupervisorView
|
||||
chatInput={chatInput}
|
||||
directCodex={directCodexProductRuntime}
|
||||
hiddenConversationCount={hiddenConversationCount}
|
||||
needsUserInput={projectSupervisorNeedsUserInput}
|
||||
needsUserInput={
|
||||
directCodexProductRuntime ? false : projectSupervisorNeedsUserInput
|
||||
}
|
||||
onCancelConfirmation={cancelUiCommandConfirmation}
|
||||
onChatInputChange={setChatInput}
|
||||
onConfirmConfirmation={confirmUiCommand}
|
||||
onScroll={handleConversationScroll}
|
||||
onShowEarlierMessages={showEarlierConversationMessages}
|
||||
onSubmit={handleProjectSupervisorOnlySubmit}
|
||||
pendingConfirmation={pendingUiConfirmation}
|
||||
pendingConfirmation={
|
||||
directCodexProductRuntime ? null : pendingUiConfirmation
|
||||
}
|
||||
transientReply={projectSupervisorTransientReply}
|
||||
visibleMessages={visibleMessages}
|
||||
visibleProfessionalAgentCards={visibleProfessionalAgentCards}
|
||||
@@ -11077,6 +11295,7 @@ export function App({
|
||||
error={projectSupervisorRuntimeError}
|
||||
runtimeByAgentId={agentRuntimeById}
|
||||
controlBusy={chatAgentBusy}
|
||||
readOnly={directCodexProductRuntime}
|
||||
professionalResultsByAgentId={professionalAgentResultsById}
|
||||
onToolAction={handleProjectSupervisorToolAction}
|
||||
onSupervisorRetry={handleProjectSupervisorRetry}
|
||||
|
||||
@@ -1729,6 +1729,90 @@ export function isMudPointInsufficientRuntimeError(message: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function directPlatformFailureDetail(message: string) {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (
|
||||
!(
|
||||
lower.includes('陶泥儿美术包生成失败') ||
|
||||
lower.includes('平台图片生成任务失败') ||
|
||||
lower.includes('external editor') ||
|
||||
lower.includes('game-chat 图集') ||
|
||||
lower.includes('透明美术图集') ||
|
||||
lower.includes('图集切片')
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
/(?:authorization|bearer|api[_ -]?key|token|secret|cookie|set-cookie|https?:\/\/|(?:^|[\\s::])\/?(?:users|home|var|tmp|private)\/|[A-Z]:\\Users\\)/i.test(
|
||||
trimmed,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const safe = trimmed
|
||||
.replace(/(?:;|;|\s)operationId\s*=\s*[^;;\s]+/gi, '')
|
||||
.replace(/(?:;|;)\s*externalGenerationJobId\s*=\s*[^;;\s]+/gi, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 280);
|
||||
return safe || null;
|
||||
}
|
||||
|
||||
function directCodexFailureDetail(message: string) {
|
||||
const trimmed = message.trim();
|
||||
const marker = 'direct-codex-error:';
|
||||
const markerIndex = trimmed.indexOf(marker);
|
||||
if (markerIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
const detail = trimmed.slice(markerIndex + marker.length).trim();
|
||||
if (!detail) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
/(?:authorization|bearer|api[_ -]?key|token|secret|cookie|set-cookie)/i.test(
|
||||
detail,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const safe = detail
|
||||
.replace(/https?:\/\/[^\s]+/gi, '[已隐藏链接]')
|
||||
.replace(/(?:[A-Z]:\\|\\\\|\/(?:Users|home|var|tmp|private)\/)[^\s]+/gi, '[已隐藏路径]')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 280);
|
||||
return safe || null;
|
||||
}
|
||||
|
||||
function directRuntimeFailureDetail(message: string) {
|
||||
const trimmed = message.trim();
|
||||
if (
|
||||
!(
|
||||
trimmed.startsWith('Codex 已返回,但客户端登记生成产物失败:') ||
|
||||
trimmed.includes('陶泥儿美术包不完整,已终止代码生成') ||
|
||||
trimmed.includes('陶泥儿规范图生成后未形成可用平台合同') ||
|
||||
trimmed.includes('陶泥儿美术包生成返回后未形成可用的已登记平台素材合同')
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
/(?:authorization|bearer|api[_ -]?key|token|secret|cookie|set-cookie|https?:\/\/|(?:^|[\\s::])\/?(?:users|home|var|tmp|private)\/|[A-Z]:\\Users\\)/i.test(
|
||||
trimmed,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const safe = trimmed.replace(/\s+/g, ' ').trim().slice(0, 320);
|
||||
return safe || null;
|
||||
}
|
||||
|
||||
export function projectRuntimeVisibleError(
|
||||
message: string,
|
||||
subject: string,
|
||||
@@ -1742,6 +1826,10 @@ export function projectRuntimeVisibleError(
|
||||
if (isMudPointInsufficientRuntimeError(message)) {
|
||||
return MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE;
|
||||
}
|
||||
const directFailureDetail = directPlatformFailureDetail(message);
|
||||
if (directFailureDetail) {
|
||||
return `${subject}:${directFailureDetail}`;
|
||||
}
|
||||
const codexAppServerKind = visibleMessage.match(
|
||||
/(?:^|[\s::])kind=codex-app-server-([a-z-]+)(?=\s|$)/,
|
||||
)?.[1];
|
||||
@@ -1749,20 +1837,49 @@ export function projectRuntimeVisibleError(
|
||||
const detail = {
|
||||
'context-window-exceeded': '模型上下文已超限,请缩小任务范围后重试',
|
||||
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
||||
'usage-limit-exceeded': 'Codex 用量已达上限,请检查账户额度后重试',
|
||||
'usage-limit-exceeded': '智能创作用量已达上限,请检查账户额度后重试',
|
||||
unauthorized: 'Codex 鉴权失败,请重新登录或检查 API Key',
|
||||
'bad-request': 'Codex 请求无效,请检查模型与运行时配置',
|
||||
'cyber-policy': 'Codex 安全策略拒绝了本次请求,请调整任务内容',
|
||||
'sandbox-error': 'Codex 隔离环境启动失败,请重试或检查本机环境',
|
||||
'thread-rollback-failed': 'Codex 会话恢复失败,请新建任务后重试',
|
||||
'bad-request': '智能创作请求无效,请检查模型与运行时配置',
|
||||
'cyber-policy': '智能创作安全策略拒绝了本次请求,请调整任务内容',
|
||||
'sandbox-error': '智能创作隔离环境启动失败,请重试或检查本机环境',
|
||||
'thread-rollback-failed': '智能创作会话恢复失败,请新建任务后重试',
|
||||
'active-turn-not-steerable':
|
||||
'当前 Codex 任务无法追加指令,请等待结束后重试',
|
||||
other: 'Codex 执行失败,请查看运行详情后重试',
|
||||
'当前智能创作任务无法追加指令,请等待结束后重试',
|
||||
other: '智能创作执行失败,请查看运行详情后重试',
|
||||
}[codexAppServerKind];
|
||||
if (detail) {
|
||||
return `${subject} ${detail}`;
|
||||
}
|
||||
}
|
||||
const directCodexAppServerKind = visibleMessage.match(
|
||||
/codex-app-server-error:([a-z-]+)/,
|
||||
)?.[1];
|
||||
if (directCodexAppServerKind) {
|
||||
const detail = {
|
||||
'context-window-exceeded': '模型上下文已超限,请缩小任务范围后重试',
|
||||
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
||||
'usage-limit-exceeded': '用量已达上限,请检查账户额度后重试',
|
||||
unauthorized: '鉴权失败,请检查 API Key 或登录态',
|
||||
'bad-request': '请求无效,请检查模型与运行时配置',
|
||||
'cyber-policy': '安全策略拒绝了本次请求,请调整任务内容',
|
||||
'sandbox-error': '工作区隔离启动失败,请检查项目目录后重试',
|
||||
other: '未完成本次执行,请查看项目文件是否已修改后再重试',
|
||||
}[directCodexAppServerKind];
|
||||
if (detail) {
|
||||
return `${subject}${detail}`;
|
||||
}
|
||||
}
|
||||
if (visibleMessage.includes('codex-app-server-terminal-unknown:')) {
|
||||
return `${subject}服务或网络在执行中断开,最终状态未知;请先检查项目文件是否已修改,再决定是否重试`;
|
||||
}
|
||||
const directCodexDetail = directCodexFailureDetail(visibleMessage);
|
||||
if (directCodexDetail) {
|
||||
return `${subject}:Codex 执行失败:${directCodexDetail}`;
|
||||
}
|
||||
const directRuntimeDetail = directRuntimeFailureDetail(visibleMessage);
|
||||
if (directRuntimeDetail) {
|
||||
return `${subject}:${directRuntimeDetail}`;
|
||||
}
|
||||
const exhaustedUpstreamRetry = visibleMessage.match(
|
||||
/(?:^|[\s::])kind=upstream-(\d{3}) httpStatus=(\d{3}) fingerprint=[0-9a-f]{64} chars=\d+ retryAttempt=(\d+) maxRetries=(\d+) retryState=exhausted\s*$/,
|
||||
);
|
||||
|
||||
@@ -663,6 +663,7 @@ export function ProjectSupervisorRuntimePanel({
|
||||
error,
|
||||
runtimeByAgentId,
|
||||
controlBusy,
|
||||
readOnly = false,
|
||||
professionalResultsByAgentId,
|
||||
onToolAction,
|
||||
onSupervisorRetry,
|
||||
@@ -674,6 +675,7 @@ export function ProjectSupervisorRuntimePanel({
|
||||
error: string;
|
||||
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>;
|
||||
controlBusy: boolean;
|
||||
readOnly?: boolean;
|
||||
professionalResultsByAgentId: Record<
|
||||
string,
|
||||
ProjectAgentResultSummary | undefined
|
||||
@@ -776,6 +778,7 @@ export function ProjectSupervisorRuntimePanel({
|
||||
? (runtime.taskQueue?.pending ?? 0)
|
||||
: 0;
|
||||
const canRetrySupervisor = Boolean(
|
||||
!readOnly &&
|
||||
runtime &&
|
||||
!pendingToolAction &&
|
||||
(runtime.status === 'failed' ||
|
||||
@@ -843,7 +846,14 @@ export function ProjectSupervisorRuntimePanel({
|
||||
{compactProgress ? (
|
||||
<small aria-label="项目总控 Agent 进度">{compactProgress}</small>
|
||||
) : null}
|
||||
{(needsSupervisorReconciliation || canRetrySupervisor) && runtime ? (
|
||||
{readOnly && runtime ? (
|
||||
<small className="project-runtime-retry-feedback" role="status">
|
||||
历史运行记录仅供查看;新版智能创作不会恢复、确认或重试旧任务。
|
||||
</small>
|
||||
) : null}
|
||||
{!readOnly &&
|
||||
(needsSupervisorReconciliation || canRetrySupervisor) &&
|
||||
runtime ? (
|
||||
<div
|
||||
className="project-runtime-recovery"
|
||||
aria-label={
|
||||
@@ -933,6 +943,7 @@ export function ProjectSupervisorRuntimePanel({
|
||||
) : null}
|
||||
{pendingToolAction &&
|
||||
pendingActionPresentation &&
|
||||
!readOnly &&
|
||||
!needsSupervisorReconciliation ? (
|
||||
<div
|
||||
className="pending-command project-runtime-pending-command"
|
||||
@@ -976,6 +987,7 @@ export function ProjectSupervisorRuntimePanel({
|
||||
const professionalResult =
|
||||
professionalResultsByAgentId[professionalRuntime.agentId];
|
||||
const canRetryProfessional =
|
||||
!readOnly &&
|
||||
!supervisorIsTerminal &&
|
||||
!professionalPendingAction &&
|
||||
(professionalRuntime.status === 'failed' ||
|
||||
@@ -1027,7 +1039,7 @@ export function ProjectSupervisorRuntimePanel({
|
||||
professionalRuntime.updatedAt,
|
||||
)}
|
||||
</small>
|
||||
{professionalPendingAction ? (
|
||||
{professionalPendingAction && !readOnly ? (
|
||||
<div
|
||||
className="project-runtime-professional-actions"
|
||||
aria-label={`${projectProfessionalAgentLabel(
|
||||
@@ -1185,14 +1197,14 @@ export function ProjectSupervisorRuntimePanel({
|
||||
{projectRuntimeVisibleError(professionalActionError, '专业 Agent')}
|
||||
</small>
|
||||
) : null}
|
||||
{userInputRequest ? (
|
||||
{userInputRequest && !readOnly ? (
|
||||
<AgentRuntimeUserInputCard
|
||||
key={`${userInputRequest.requestId}:${userInputRequest.responseId ?? 'pending'}`}
|
||||
request={userInputRequest}
|
||||
controlBusy={controlBusy}
|
||||
onSubmit={onUserInput}
|
||||
/>
|
||||
) : needsUserInput ? (
|
||||
) : needsUserInput && !readOnly ? (
|
||||
<p className="agent-runtime-user-input-missing" role="status">
|
||||
待回答问题未能读取,请稍后重试。
|
||||
</p>
|
||||
|
||||
@@ -62,7 +62,6 @@ export function WorkspaceLauncherShell({
|
||||
activeProjectAgentResults,
|
||||
setAgentResults: setActiveProjectAgentResults,
|
||||
resetLauncherHomeDraft,
|
||||
createHomeDraft,
|
||||
createHomeDraftAutomatically,
|
||||
openProject,
|
||||
} = homeProject;
|
||||
@@ -259,7 +258,6 @@ export function WorkspaceLauncherShell({
|
||||
onStatusChange={setStatus}
|
||||
homeAgentModeItems={homeAgentModeItems}
|
||||
recentProjectRows={recentProjectRows}
|
||||
onCreateDraft={createHomeDraft}
|
||||
onCreateDraftAutomatically={createHomeDraftAutomatically}
|
||||
onProjectsOpen={() => setLauncherView('projects')}
|
||||
onProjectOpen={(path) => {
|
||||
@@ -300,6 +298,7 @@ export function WorkspaceLauncherShell({
|
||||
initialProjectPath={currentProjectContext.projectPath}
|
||||
initialProjectManifest={currentProjectContext.manifest}
|
||||
initialProjectKind={currentProjectContext.projectKind}
|
||||
initialSupervisorMessage={currentProjectContext.initialPrompt}
|
||||
orchestrationMode="single-supervisor"
|
||||
projectSupervisorOnly
|
||||
onManifestChange={syncActiveProjectManifest}
|
||||
|
||||
@@ -35,6 +35,7 @@ export type ProjectSupervisorComponentProps = {
|
||||
initialProjectPath?: string;
|
||||
initialProjectManifest?: GameCreationAppManifest;
|
||||
initialProjectKind?: 'web' | 'godot';
|
||||
initialSupervisorMessage?: string;
|
||||
orchestrationMode?: 'single-supervisor' | 'professional-dag';
|
||||
projectSupervisorOnly?: boolean;
|
||||
onManifestChange?: (
|
||||
|
||||
@@ -34,11 +34,7 @@ import type {
|
||||
ProjectAgentResultSummary,
|
||||
ProjectAgentRuntimeSummary,
|
||||
} from '../../view/project-development';
|
||||
import {
|
||||
ensureProjectSupervisorActiveSessionId,
|
||||
projectNameFromPath,
|
||||
submitProjectSupervisorRuntimeTask,
|
||||
} from '../agent-runtime';
|
||||
import { projectNameFromPath } from '../agent-runtime';
|
||||
import {
|
||||
isAbsoluteProjectPath,
|
||||
projectPathHasControlCharacter,
|
||||
@@ -174,22 +170,11 @@ export function useHomeProjectCreation({
|
||||
result.projectPath,
|
||||
attachments,
|
||||
);
|
||||
const sessionId = await ensureProjectSupervisorActiveSessionId(
|
||||
invoke,
|
||||
result.projectPath,
|
||||
const conversationContent = buildHomeConversationContent(
|
||||
mode,
|
||||
prompt,
|
||||
attachments,
|
||||
);
|
||||
if (!sessionId) {
|
||||
throw new Error('项目总控 Agent active Session 不可用');
|
||||
}
|
||||
await submitProjectSupervisorRuntimeTask({
|
||||
invoke,
|
||||
projectPath: result.projectPath,
|
||||
sessionId,
|
||||
prompt: buildHomeConversationContent(mode, prompt, attachments),
|
||||
runtime: null,
|
||||
runProfile: 'autonomous-game-build',
|
||||
source: 'project-supervisor-game-chat',
|
||||
});
|
||||
enterProjectDevelopment({
|
||||
projectPath: result.projectPath,
|
||||
projectName:
|
||||
@@ -202,7 +187,7 @@ export function useHomeProjectCreation({
|
||||
),
|
||||
mode,
|
||||
initialPrompt:
|
||||
prompt.trim() ||
|
||||
conversationContent.trim() ||
|
||||
(attachments.length > 0
|
||||
? '用户上传了参考附件,等待后续补充需求。'
|
||||
: ''),
|
||||
@@ -265,7 +250,7 @@ export function useHomeProjectCreation({
|
||||
prompt,
|
||||
attachments,
|
||||
);
|
||||
setStatus('已创建项目并交给项目总控 Agent');
|
||||
setStatus('已创建项目,正在开始智能创作');
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`项目已创建;首条需求投递失败:${
|
||||
@@ -291,11 +276,11 @@ export function useHomeProjectCreation({
|
||||
if (!invoke) {
|
||||
throw new Error('需要在 Tauri App 内运行');
|
||||
}
|
||||
setStatus('正在启动项目总控 Agent');
|
||||
setStatus('正在启动智能创作');
|
||||
try {
|
||||
await enterCreatedHomeProject(invoke, result, mode, prompt, attachments);
|
||||
setStatus('已自动创建工作区并开始工作');
|
||||
return '已自动创建工作区并开始工作';
|
||||
setStatus('已自动创建工作区并开始智能创作');
|
||||
return '已自动创建工作区并开始智能创作';
|
||||
} catch (error) {
|
||||
const message = `工作区已创建;首条需求投递失败:${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
|
||||
+10
-2
@@ -26,6 +26,7 @@ type RuntimePanelProps = ComponentProps<typeof ProjectSupervisorRuntimePanel>;
|
||||
|
||||
type ProjectSupervisorViewProps = RuntimePanelProps & {
|
||||
chatInput: string;
|
||||
directCodex?: boolean;
|
||||
hiddenConversationCount: number;
|
||||
needsUserInput: boolean;
|
||||
onCancelConfirmation: () => void;
|
||||
@@ -44,6 +45,7 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
|
||||
|
||||
export function ProjectSupervisorView({
|
||||
chatInput,
|
||||
directCodex = false,
|
||||
hiddenConversationCount,
|
||||
needsUserInput,
|
||||
onCancelConfirmation,
|
||||
@@ -99,7 +101,9 @@ export function ProjectSupervisorView({
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<ProjectSupervisorRuntimePanel {...runtimePanelProps} />
|
||||
{directCodex ? null : (
|
||||
<ProjectSupervisorRuntimePanel {...runtimePanelProps} />
|
||||
)}
|
||||
{pendingConfirmation ? (
|
||||
<div className="pending-command">
|
||||
<span>
|
||||
@@ -120,7 +124,11 @@ export function ProjectSupervisorView({
|
||||
disabled={runtimePanelProps.controlBusy || needsUserInput}
|
||||
rows={3}
|
||||
value={chatInput}
|
||||
placeholder="告诉项目总控接下来要做什么"
|
||||
placeholder={
|
||||
directCodex
|
||||
? '告诉陶泥儿接下来要做什么'
|
||||
: '告诉项目总控接下来要做什么'
|
||||
}
|
||||
onChange={(event) => onChatInputChange(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import { resolveTauriInvoke } from '../../app/tauri';
|
||||
import {
|
||||
type GameCreatorAgentLlmConfig,
|
||||
type GameCreatorAgentMode,
|
||||
type GameCreatorAppConfig,
|
||||
type GameCreatorAppConfigView,
|
||||
type GameCreatorLlmApiKind,
|
||||
@@ -69,10 +68,10 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
|
||||
agentMode: 'codex_app_server',
|
||||
llm: {
|
||||
apiKey: '',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4.1',
|
||||
baseUrl: 'https://dev.genarrative.world/gpt/v1',
|
||||
model: 'gpt-5.6-sol',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'high',
|
||||
reasoningEffort: 'max',
|
||||
stream: false,
|
||||
webSearchEnabled: false,
|
||||
contextWindowTokens: 128000,
|
||||
@@ -90,14 +89,13 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
|
||||
mcpServers: {},
|
||||
};
|
||||
|
||||
const runtimeAgentModes = new Set<GameCreatorAgentMode>([
|
||||
'codex_app_server',
|
||||
'codex_cli',
|
||||
'provider',
|
||||
]);
|
||||
|
||||
type RuntimeSettingsSection = 'general' | 'agents' | 'connections' | 'advanced';
|
||||
|
||||
type RuntimeConfigToast = {
|
||||
tone: 'success' | 'error';
|
||||
message: string;
|
||||
};
|
||||
|
||||
const runtimeSettingsSections = [
|
||||
{
|
||||
id: 'general',
|
||||
@@ -579,6 +577,8 @@ export function RuntimeConfigDialog({
|
||||
}) {
|
||||
const [runtimeConfigPath, setRuntimeConfigPath] = useState('');
|
||||
const [runtimeConfigStatus, setRuntimeConfigStatus] = useState('未读取');
|
||||
const [runtimeConfigToast, setRuntimeConfigToast] =
|
||||
useState<RuntimeConfigToast | null>(null);
|
||||
const [runtimeConfigDraft, setRuntimeConfigDraft] =
|
||||
useState<GameCreatorAppConfig>(defaultRuntimeConfigDraft);
|
||||
const [runtimeConfigBusy, setRuntimeConfigBusy] = useState(false);
|
||||
@@ -615,10 +615,6 @@ export function RuntimeConfigDialog({
|
||||
}));
|
||||
}
|
||||
|
||||
function updateRuntimeAgentMode(agentMode: GameCreatorAgentMode) {
|
||||
setRuntimeConfigDraft((current) => ({ ...current, agentMode }));
|
||||
}
|
||||
|
||||
function updateRuntimeLlmProviderPreset(
|
||||
presetId: RuntimeLlmProviderPresetId,
|
||||
) {
|
||||
@@ -830,10 +826,12 @@ export function RuntimeConfigDialog({
|
||||
|
||||
runtimeConfigBusyRef.current = true;
|
||||
setRuntimeConfigBusy(true);
|
||||
setRuntimeConfigToast(null);
|
||||
setRuntimeConfigStatus('正在保存');
|
||||
try {
|
||||
const config = normalizeRuntimeConfigDraft({
|
||||
...runtimeConfigDraft,
|
||||
agentMode: 'codex_app_server',
|
||||
mcpServers: materializeRuntimeMcpServers(
|
||||
runtimeConfigDraft.mcpServers,
|
||||
mcpStructuredDrafts,
|
||||
@@ -851,11 +849,20 @@ export function RuntimeConfigDialog({
|
||||
);
|
||||
setMcpCatalog(null);
|
||||
setRuntimeConfigStatus(`已保存:${result.path}`);
|
||||
setRuntimeConfigToast({
|
||||
tone: 'success',
|
||||
message: '保存成功,新的运行时配置已生效',
|
||||
});
|
||||
onLog?.('runtime_config.save');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setRuntimeConfigStatus(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
message,
|
||||
);
|
||||
setRuntimeConfigToast({
|
||||
tone: 'error',
|
||||
message: `保存失败:${message}`,
|
||||
});
|
||||
} finally {
|
||||
runtimeConfigBusyRef.current = false;
|
||||
setRuntimeConfigBusy(false);
|
||||
@@ -933,6 +940,24 @@ export function RuntimeConfigDialog({
|
||||
role="presentation"
|
||||
onMouseDown={(event) => closeDialogOnBackdropMouseDown(event, onClose)}
|
||||
>
|
||||
{runtimeConfigToast ? (
|
||||
<div
|
||||
className="runtime-settings-toast"
|
||||
data-tone={runtimeConfigToast.tone}
|
||||
role={runtimeConfigToast.tone === 'error' ? 'alert' : 'status'}
|
||||
aria-label="运行时配置提示"
|
||||
aria-live={
|
||||
runtimeConfigToast.tone === 'error' ? 'assertive' : 'polite'
|
||||
}
|
||||
>
|
||||
{runtimeConfigToast.tone === 'error' ? (
|
||||
<CircleAlert size={16} aria-hidden="true" />
|
||||
) : (
|
||||
<CheckCircle2 size={16} aria-hidden="true" />
|
||||
)}
|
||||
<span>{runtimeConfigToast.message}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<form
|
||||
className="settings-panel runtime-settings-panel"
|
||||
role="dialog"
|
||||
@@ -1004,27 +1029,11 @@ export function RuntimeConfigDialog({
|
||||
<div className="settings-grid runtime-settings-fields">
|
||||
{activeSection === 'general' ? (
|
||||
<>
|
||||
<label>
|
||||
Agent 模式
|
||||
<select
|
||||
aria-label="Agent 模式"
|
||||
value={runtimeConfigDraft.agentMode}
|
||||
onChange={(event) =>
|
||||
updateRuntimeAgentMode(
|
||||
event.currentTarget.value as GameCreatorAgentMode,
|
||||
)
|
||||
}
|
||||
>
|
||||
{!runtimeAgentModes.has(runtimeConfigDraft.agentMode) ? (
|
||||
<option value={runtimeConfigDraft.agentMode}>
|
||||
{runtimeConfigDraft.agentMode}(当前配置)
|
||||
</option>
|
||||
) : null}
|
||||
<option value="codex_app_server">Codex App Server</option>
|
||||
<option value="codex_cli">Codex CLI</option>
|
||||
<option value="provider">HTTP Provider</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="runtime-settings-readonly-field">
|
||||
<span>Agent 模式</span>
|
||||
<strong>陶泥儿智能创作(固定)</strong>
|
||||
<small>需求将由陶泥儿智能创作服务执行</small>
|
||||
</div>
|
||||
{runtimeConfigDraft.agentMode !== 'codex_cli' ? (
|
||||
<>
|
||||
<label>
|
||||
|
||||
@@ -3369,6 +3369,36 @@ h2 {
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.runtime-settings-toast {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: min(420px, calc(100vw - 48px));
|
||||
padding: 11px 14px;
|
||||
border: 1px solid var(--platform-success-border);
|
||||
border-radius: 12px;
|
||||
background: var(--platform-success-fill);
|
||||
box-shadow: var(--platform-panel-shadow);
|
||||
color: var(--platform-success-text);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.runtime-settings-toast[data-tone='error'] {
|
||||
border-color: var(--platform-warm-border);
|
||||
background: var(--platform-warm-bg);
|
||||
color: var(--platform-warm-text);
|
||||
}
|
||||
|
||||
.runtime-settings-toast span {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -4614,6 +4644,13 @@ iframe.preview-frame {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.runtime-settings-toast {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.runtime-settings-panel {
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
@@ -4685,7 +4722,7 @@ iframe.preview-frame {
|
||||
max-width: none;
|
||||
min-height: 0;
|
||||
gap: 10px;
|
||||
padding: 40px 8px 8px;
|
||||
padding: 72px 8px 8px;
|
||||
overflow: hidden;
|
||||
color: var(--platform-text-base);
|
||||
}
|
||||
@@ -5660,6 +5697,23 @@ iframe.preview-frame {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.game-resource-code-body {
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
border: 1px solid #eadbd4;
|
||||
border-radius: 12px;
|
||||
background: #2c2021;
|
||||
color: #ffe8d8;
|
||||
}
|
||||
|
||||
.game-resource-code-body pre {
|
||||
min-width: max-content;
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
font: 12px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.game-resource-document-body > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -5765,8 +5819,10 @@ iframe.preview-frame {
|
||||
|
||||
.game-run-surface {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(300px, 1fr) auto auto;
|
||||
grid-template-rows: minmax(300px, 1fr) auto;
|
||||
grid-row: 2 / -1;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 12px;
|
||||
background: #fffdfa;
|
||||
@@ -5774,10 +5830,7 @@ iframe.preview-frame {
|
||||
|
||||
.game-run-preview {
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
gap: 9px;
|
||||
display: block;
|
||||
min-height: 300px;
|
||||
border: 1px dashed #dfb59f;
|
||||
border-radius: 14px;
|
||||
@@ -5788,9 +5841,11 @@ iframe.preview-frame {
|
||||
}
|
||||
|
||||
.game-run-preview iframe {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
min-height: 0;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
@@ -5817,33 +5872,6 @@ iframe.preview-frame {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.game-run-slice-controls {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.game-run-slice-controls button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e7d3c9;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #855c4c;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.game-run-slice-controls span {
|
||||
color: #9a7d70;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.game-run-panels {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -6612,15 +6640,6 @@ iframe.preview-frame {
|
||||
min-width: 460px;
|
||||
}
|
||||
|
||||
.game-run-slice-controls {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.game-run-slice-controls span {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.game-run-panels {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ type HomeViewProps = {
|
||||
onStatusChange: (status: string) => void;
|
||||
homeAgentModeItems: readonly HomeAgentModeItem[];
|
||||
recentProjectRows: readonly HomeProjectRow[];
|
||||
onCreateDraft: (draft: HomeDraft) => Promise<string>;
|
||||
onCreateDraftAutomatically: (draft: HomeDraft) => Promise<string>;
|
||||
onProjectsOpen: () => void;
|
||||
onProjectOpen: (path: string) => void;
|
||||
@@ -59,7 +58,6 @@ export default function HomeView({
|
||||
onStatusChange,
|
||||
homeAgentModeItems,
|
||||
recentProjectRows,
|
||||
onCreateDraft,
|
||||
onCreateDraftAutomatically,
|
||||
onProjectsOpen,
|
||||
onProjectOpen,
|
||||
@@ -118,7 +116,7 @@ export default function HomeView({
|
||||
|
||||
function handleHomeSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
void createFromHome(onCreateDraft, '请选择项目目录');
|
||||
void createFromHome(onCreateDraftAutomatically, '正在创建工作区');
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user