Merge remote-tracking branch 'origin/master' into feat/smart-paste
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
This commit is contained in:
@@ -839,7 +839,7 @@ fn direct_thread_visible_item(
|
||||
/// 与 `direct_project_history::is_direct_project_codex_user_item` 的判据同一份口径(前缀 +
|
||||
/// `:user` 后缀)。回合生命周期事件的 `userItemId` 只能来自这里或已落盘条目自身的 id;
|
||||
/// clientTurnId 缺失时不猜身份,返回 `None` 让前端按"未知归属"处理。
|
||||
fn direct_codex_user_item_id_for_client_turn_id(client_turn_id: &str) -> Option<String> {
|
||||
pub(super) fn direct_codex_user_item_id_for_client_turn_id(client_turn_id: &str) -> Option<String> {
|
||||
let client_turn_id = client_turn_id.trim();
|
||||
(!client_turn_id.is_empty()).then(|| format!("direct-codex:{client_turn_id}:user"))
|
||||
}
|
||||
@@ -1619,6 +1619,36 @@ async fn stage_codex_app_server_image(
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum DirectCodexTurnKind {
|
||||
User,
|
||||
HostFeedback,
|
||||
}
|
||||
|
||||
async fn direct_project_turn_input(
|
||||
request: &LlmRunRequest,
|
||||
prompt: &str,
|
||||
workspace_path: &std::path::Path,
|
||||
turn_kind: DirectCodexTurnKind,
|
||||
original_user_item: Option<&serde_json::Value>,
|
||||
skill_roots: &[std::path::PathBuf],
|
||||
) -> Result<serde_json::Value, platform_llm::LlmError> {
|
||||
// 原始用户条目一直用于历史和事件关联,但只有首次请求将它作为模型输入。
|
||||
if turn_kind == DirectCodexTurnKind::User {
|
||||
if let Some(item) = original_user_item {
|
||||
let canonical: DirectCodexUserItem = serde_json::from_value(item.clone())
|
||||
.map_err(|error| platform_llm::LlmError::InvalidRequest(error.to_string()))?;
|
||||
return direct_codex_user_item_to_codex_turn_input(
|
||||
workspace_path,
|
||||
&canonical,
|
||||
skill_roots,
|
||||
)
|
||||
.map_err(platform_llm::LlmError::InvalidRequest);
|
||||
}
|
||||
}
|
||||
codex_app_server_turn_input(request, prompt, workspace_path).await
|
||||
}
|
||||
|
||||
async fn codex_app_server_turn_input(
|
||||
request: &LlmRunRequest,
|
||||
prompt: &str,
|
||||
@@ -3217,6 +3247,7 @@ impl CodexAppServerConnection {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
DirectCodexTurnKind::User,
|
||||
on_agent_message_delta,
|
||||
direct_observer,
|
||||
audit,
|
||||
@@ -3233,6 +3264,7 @@ impl CodexAppServerConnection {
|
||||
direct_history_root: Option<&std::path::Path>,
|
||||
direct_client_turn_id: Option<&str>,
|
||||
direct_user_item: Option<&serde_json::Value>,
|
||||
turn_kind: DirectCodexTurnKind,
|
||||
mut on_agent_message_delta: Option<&mut (dyn FnMut(&platform_llm::LlmStreamDelta) + Send)>,
|
||||
mut direct_observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>,
|
||||
mut audit: Option<&mut DirectCodexTurnAudit>,
|
||||
@@ -3358,18 +3390,15 @@ impl CodexAppServerConnection {
|
||||
.map_err(platform_llm::LlmError::InvalidRequest)?
|
||||
};
|
||||
let mut input = if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
|
||||
if let Some(item) = direct_user_item {
|
||||
let canonical: DirectCodexUserItem = serde_json::from_value(item.clone())
|
||||
.map_err(|error| platform_llm::LlmError::InvalidRequest(error.to_string()))?;
|
||||
direct_codex_user_item_to_codex_turn_input(
|
||||
&self.inner.workspace_path,
|
||||
&canonical,
|
||||
self.inner._skill_roots.as_deref().unwrap_or_default(),
|
||||
)
|
||||
.map_err(platform_llm::LlmError::InvalidRequest)?
|
||||
} else {
|
||||
codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await?
|
||||
}
|
||||
direct_project_turn_input(
|
||||
&request,
|
||||
&prompt,
|
||||
&self.inner.workspace_path,
|
||||
turn_kind,
|
||||
direct_user_item,
|
||||
self.inner._skill_roots.as_deref().unwrap_or_default(),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await?
|
||||
};
|
||||
@@ -5042,6 +5071,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at(
|
||||
root,
|
||||
system_prompt,
|
||||
user_prompt,
|
||||
DirectCodexTurnKind::User,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -5060,6 +5090,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_observer(
|
||||
root,
|
||||
system_prompt,
|
||||
user_prompt,
|
||||
DirectCodexTurnKind::User,
|
||||
None,
|
||||
Some(observer),
|
||||
None,
|
||||
@@ -5072,6 +5103,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
root: &std::path::Path,
|
||||
system_prompt: String,
|
||||
user_prompt: String,
|
||||
turn_kind: DirectCodexTurnKind,
|
||||
client_turn_id: Option<&str>,
|
||||
observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>,
|
||||
audit: Option<&mut DirectCodexTurnAudit>,
|
||||
@@ -5171,6 +5203,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
Some(&codex_root),
|
||||
effective_client_turn_id,
|
||||
direct_user_item.as_ref(),
|
||||
turn_kind,
|
||||
None,
|
||||
observer,
|
||||
audit,
|
||||
@@ -5348,6 +5381,7 @@ mod tests {
|
||||
Path::new("not-read"),
|
||||
String::new(),
|
||||
String::new(),
|
||||
DirectCodexTurnKind::User,
|
||||
Some("not-executed"),
|
||||
None,
|
||||
None,
|
||||
@@ -6018,6 +6052,108 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn direct_project_turn_input_preserves_user_input_and_sends_host_feedback() {
|
||||
let temp = tempfile::tempdir().expect("temp project");
|
||||
crate::init_local_game_project_at(temp.path(), "feedback-input", "反馈输入")
|
||||
.expect("init project");
|
||||
let skill_root = temp.path().join("skills");
|
||||
let skill_path = skill_root.join("test-skill").join("SKILL.md");
|
||||
std::fs::create_dir_all(skill_path.parent().unwrap()).expect("create skill directory");
|
||||
std::fs::write(&skill_path, "# 测试 Skill").expect("write skill");
|
||||
let user_item = serde_json::json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"id": "direct-codex:feedback-input:user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "请创建菜单"},
|
||||
{"type": "agc_skill_reference", "name": "test-skill"}
|
||||
]
|
||||
});
|
||||
let skill_roots = vec![skill_root];
|
||||
let request = LlmRunRequest::single_turn("系统", "请创建菜单");
|
||||
let input = direct_project_turn_input(
|
||||
&request,
|
||||
&direct_codex_user_prompt(&request),
|
||||
temp.path(),
|
||||
DirectCodexTurnKind::User,
|
||||
Some(&user_item),
|
||||
&skill_roots,
|
||||
)
|
||||
.await
|
||||
.expect("initial input");
|
||||
assert_eq!(
|
||||
input,
|
||||
serde_json::json!([
|
||||
{"type": "text", "text": "请创建菜单"},
|
||||
{"type": "skill", "name": "test-skill", "path": skill_path}
|
||||
])
|
||||
);
|
||||
|
||||
// 连续验收反馈和执行错误都携带同一个原始条目,但 wire input 必须是当次反馈。
|
||||
for feedback in [
|
||||
"宿主验收未通过:缺少移动端玩法证据,请补齐。",
|
||||
"npm run build 失败:入口不存在,请修复后继续。",
|
||||
] {
|
||||
let request = LlmRunRequest::single_turn("系统", feedback);
|
||||
let input = direct_project_turn_input(
|
||||
&request,
|
||||
&direct_codex_user_prompt(&request),
|
||||
temp.path(),
|
||||
DirectCodexTurnKind::HostFeedback,
|
||||
Some(&user_item),
|
||||
&skill_roots,
|
||||
)
|
||||
.await
|
||||
.expect("host feedback input");
|
||||
assert_eq!(
|
||||
input,
|
||||
serde_json::json!([{"type": "text", "text": feedback}])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn direct_project_turn_input_cli_feedback_keeps_one_original_history_item() {
|
||||
let temp = tempfile::tempdir().expect("temp project");
|
||||
crate::init_local_game_project_at(temp.path(), "cli-feedback", "CLI 反馈")
|
||||
.expect("init project");
|
||||
let user_item = direct_project_local_message_item(
|
||||
"user",
|
||||
"请创建菜单",
|
||||
direct_codex_user_item_id_for_client_turn_id("direct-cli-feedback").as_deref(),
|
||||
)
|
||||
.expect("freeze original CLI input");
|
||||
for (kind, prompt) in [
|
||||
(DirectCodexTurnKind::User, "请创建菜单"),
|
||||
(
|
||||
DirectCodexTurnKind::HostFeedback,
|
||||
"宿主验收未通过:缺少玩法证据",
|
||||
),
|
||||
(DirectCodexTurnKind::HostFeedback, "构建失败:请修复入口"),
|
||||
] {
|
||||
// 与发送入口一样,每次先幂等落盘原始用户条目,再构造本次模型输入。
|
||||
append_direct_project_user_message_at(temp.path(), &user_item)
|
||||
.expect("persist original user item without an ID conflict");
|
||||
let request = LlmRunRequest::single_turn("系统", prompt);
|
||||
let input = direct_project_turn_input(
|
||||
&request,
|
||||
&direct_codex_user_prompt(&request),
|
||||
temp.path(),
|
||||
kind,
|
||||
Some(&user_item),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.expect("CLI turn input");
|
||||
assert_eq!(input, serde_json::json!([{"type": "text", "text": prompt}]));
|
||||
}
|
||||
assert_eq!(
|
||||
read_direct_project_history_items_at(temp.path()).expect("read history"),
|
||||
vec![user_item]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_server_turn_input_stages_data_url_as_isolated_local_image() {
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
@@ -6035,9 +6171,16 @@ mod tests {
|
||||
let prompt = codex_app_server_text_prompt(&request).expect("sanitized prompt");
|
||||
assert!(!prompt.contains("data:image"));
|
||||
assert!(prompt.contains("原生视觉输入"));
|
||||
let input = codex_app_server_turn_input(&request, &prompt, temp.path())
|
||||
.await
|
||||
.expect("turn input");
|
||||
let input = direct_project_turn_input(
|
||||
&request,
|
||||
&prompt,
|
||||
temp.path(),
|
||||
DirectCodexTurnKind::User,
|
||||
None,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.expect("turn input");
|
||||
assert_eq!(input[0]["type"], "text");
|
||||
assert_eq!(input[1]["type"], "localImage");
|
||||
let staged_path = input[1]["path"].as_str().expect("staged path");
|
||||
@@ -7717,6 +7860,7 @@ done
|
||||
Some(&project),
|
||||
Some("turn-0001"),
|
||||
Some(&user_item),
|
||||
DirectCodexTurnKind::User,
|
||||
None,
|
||||
Some(&mut observer),
|
||||
None,
|
||||
|
||||
@@ -5115,6 +5115,20 @@ async fn run_direct_game_creator_turn_inner(
|
||||
if let Some(report) = super::direct_delivery::terminal_report(&execution_session) {
|
||||
return Ok(report);
|
||||
}
|
||||
// CLI 没有结构化条目;在进入反馈循环前固定原始消息,避免后续反馈以同一 ID
|
||||
// 写成内容不同的用户消息。GUI 则保留原有的 Skill、资源及附件引用。
|
||||
let direct_user_item = match direct_user_item {
|
||||
Some(item) => item,
|
||||
None => execution_session.snapshot().and_then(|state| {
|
||||
direct_project_local_message_item(
|
||||
"user",
|
||||
prompt,
|
||||
direct_codex_user_item_id_for_client_turn_id(&state.client_turn_id).as_deref(),
|
||||
)
|
||||
}).map_err(|error| {
|
||||
DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error)
|
||||
})?,
|
||||
};
|
||||
if execution_session.newly_accepted {
|
||||
if let Ok(ledger) = execution_session.snapshot() {
|
||||
crate::analytics::goal::accepted(
|
||||
@@ -5268,20 +5282,23 @@ async fn run_direct_game_creator_turn_inner(
|
||||
}
|
||||
};
|
||||
let mut feedback_prompt = prompt.to_string();
|
||||
let mut turn_kind = DirectCodexTurnKind::User;
|
||||
let mut audit = audit;
|
||||
let mut attempt = 1;
|
||||
let reply_result = loop {
|
||||
match direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
let result = direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
root,
|
||||
system_prompt.clone(),
|
||||
feedback_prompt.clone(),
|
||||
turn_kind,
|
||||
Some(&client_turn_id),
|
||||
Some(&mut observer),
|
||||
audit.as_deref_mut(),
|
||||
direct_user_item.clone(),
|
||||
Some(direct_user_item.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
turn_kind = DirectCodexTurnKind::HostFeedback;
|
||||
match result {
|
||||
Ok(value) => match super::direct_delivery::review_reply(root,&execution_session).await {
|
||||
Ok(Some(report)) => break Ok(report),
|
||||
Ok(None) => break Ok(value),
|
||||
@@ -5324,21 +5341,24 @@ async fn run_direct_game_creator_turn_inner(
|
||||
reply_result
|
||||
} else {
|
||||
let mut feedback_prompt = prompt.to_string();
|
||||
let mut turn_kind = DirectCodexTurnKind::User;
|
||||
let mut audit = audit;
|
||||
let mut response = None;
|
||||
let mut attempt = 1;
|
||||
loop {
|
||||
match direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
let result = direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
root,
|
||||
system_prompt.clone(),
|
||||
feedback_prompt.clone(),
|
||||
turn_kind,
|
||||
None,
|
||||
None,
|
||||
audit.as_deref_mut(),
|
||||
direct_user_item.clone(),
|
||||
Some(direct_user_item.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
turn_kind = DirectCodexTurnKind::HostFeedback;
|
||||
match result {
|
||||
Ok(value) => {
|
||||
match super::direct_delivery::review_reply(root,&execution_session).await {
|
||||
Ok(Some(report)) => { response = Some(report); break; }
|
||||
|
||||
@@ -1716,8 +1716,7 @@ export function registerHomeProjectCreationTests() {
|
||||
'卡住的自动建项',
|
||||
);
|
||||
let resolveAutomaticProject:
|
||||
| ((result: Record<string, unknown>) => void)
|
||||
| null = null;
|
||||
((result: Record<string, unknown>) => void) | null = null;
|
||||
const invoke = vi.fn(async (command: string) => {
|
||||
if (command === 'preflight_web_game_creation') return { status: 'ready' };
|
||||
if (command === 'create_automatic_local_game_project') {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 踩坑与排障记录
|
||||
|
||||
## Direct 宿主继续请求不能重发原始用户条目
|
||||
|
||||
原始 `direct_user_item` 同时参与历史持久化和模型输入转换;验收或错误反馈更新了 prompt 后,如果发送层仍优先转换原始条目,模型会收到重复的用户输入,而本地历史按 itemId 去重后只显示一次。首次请求与宿主继续必须显式区分:首次保留结构化输入,继续发送当次反馈,原始条目只保留历史与事件关联职责。GUI、CLI 的两条循环都要覆盖;只改反馈文本或清空原始条目不完整。见 [Direct 宿主继续请求输入修复](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#2026-09-23-direct-宿主继续请求输入修复)。
|
||||
|
||||
## 最近项目一次失败会被钉成终态
|
||||
|
||||
- **现象**:AGC 卡住一次后,项目列表每一行都显示「检查失败 + 待识别」,首页「最近项目」变成「暂无最近项目」;现场在后端恢复后逐条复跑 `inspect_local_project_directory`(8 个项目)全部 0ms 成功,界面仍然全红(issue #490)。
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# AI 游戏创作智能体 App 实施计划
|
||||
|
||||
## 2026-09-23 Direct 宿主继续请求输入修复
|
||||
|
||||
- 首次模型请求使用原始结构化用户输入,保留 Skill 提及及其它引用;未提供结构化输入时沿用请求正文与图片转换。
|
||||
- 验收反馈和可修复错误触发的后续模型请求,显式标记为宿主继续,只发送本次反馈正文,不再用原始用户条目覆盖。GUI 与 CLI 共用这一规则,验收条件、重试上限和执行预算保持不变。
|
||||
- 原始用户条目仍用于历史持久化和回合事件关联,沿用同一 clientTurnId/itemId;反馈不是新的用户输入。不要通过丢弃原始条目来修复发送内容。
|
||||
- 定向回归检查首次结构化输入、验收继续和错误继续的实际 wire input,以及同一用户条目的历史去重。
|
||||
|
||||
## 2026-09-21 Godot 工作区发现放宽与内置插件行去掉手动启动
|
||||
|
||||
本节覆盖下文“打开项目自动识别 Godot”中的旧口径:判定从「唯一命中」放宽为「确定性命中」,`project.godot` 从「必须是普通文件」放宽为「按链接目标判定」。
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type PlatformSegmentedTabsColumns =
|
||||
| 'one'
|
||||
| 'two'
|
||||
| 'three'
|
||||
| 'four'
|
||||
| 'threeToSix';
|
||||
'one' | 'two' | 'three' | 'four' | 'threeToSix';
|
||||
export type PlatformSegmentedTabsGap = 'sm' | 'md';
|
||||
export type PlatformSegmentedTabsRadius = 'md' | 'lg' | 'xl';
|
||||
export type PlatformSegmentedTabsSize =
|
||||
| 'sm'
|
||||
| 'md'
|
||||
| 'compact'
|
||||
| 'choice'
|
||||
| 'tab';
|
||||
'sm' | 'md' | 'compact' | 'choice' | 'tab';
|
||||
export type PlatformSegmentedTabsSurface = 'default' | 'soft' | 'transparent';
|
||||
export type PlatformSegmentedTabsTone =
|
||||
| 'neutral'
|
||||
| 'warm'
|
||||
| 'rose'
|
||||
| 'accent'
|
||||
| 'underline';
|
||||
'neutral' | 'warm' | 'rose' | 'accent' | 'underline';
|
||||
export type PlatformSegmentedTabsFrame = 'panel' | 'bare';
|
||||
export type PlatformSegmentedTabsSemantics = 'segment' | 'tabs';
|
||||
export type PlatformSegmentedTabsLayout = 'grid' | 'scroll';
|
||||
|
||||
@@ -31,9 +31,7 @@ export type GameDistributionDeviceSupport = {
|
||||
|
||||
export type GameDistributionInputMode = 'keyboard' | 'mouse' | 'touch';
|
||||
export type GameDistributionOrientation =
|
||||
| 'landscape'
|
||||
| 'portrait'
|
||||
| 'responsive';
|
||||
'landscape' | 'portrait' | 'responsive';
|
||||
|
||||
export type GameDistributionAuthor = {
|
||||
id: string;
|
||||
@@ -54,9 +52,7 @@ export type GameDistributionVersionStatus =
|
||||
| 'revoked';
|
||||
|
||||
export type GameDistributionGameVisibility =
|
||||
| 'unpublished'
|
||||
| 'published'
|
||||
| 'suspended';
|
||||
'unpublished' | 'published' | 'suspended';
|
||||
|
||||
export type GameDistributionVersionSummary = {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user