补齐Agent聊天失败回执

Agent 聊天在用户消息已保存后将回复失败原因落盘为 assistant 消息

新增 --agent-chat 开发诊断入口验证单 Agent LLM 调用链

补充开发 Agent 聊天失败回执和 CLI 解析测试
This commit is contained in:
AIGameCreator App
2026-07-09 15:41:12 +08:00
parent 57cb1fa391
commit a2bbeb79ee
4 changed files with 223 additions and 20 deletions
@@ -3,6 +3,11 @@ use super::*;
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum CliCommand {
LlmStatus,
AgentChat {
project_path: PathBuf,
agent_id: String,
prompt: String,
},
AgentRun {
project_path: PathBuf,
prompt: String,
@@ -63,6 +68,27 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
if args.first().map(String::as_str) == Some("--llm-status") {
return Ok(Some(CliCommand::LlmStatus));
}
if args.first().map(String::as_str) == Some("--agent-chat") {
let project_path = args.get(1).map(String::as_str).ok_or_else(|| {
"用法:--agent-chat <本地项目绝对路径> <agentId> <聊天内容>".to_string()
})?;
let agent_id = args.get(2).map(String::as_str).ok_or_else(|| {
"用法:--agent-chat <本地项目绝对路径> <agentId> <聊天内容>".to_string()
})?;
if args.len() < 4 {
return Err("用法:--agent-chat <本地项目绝对路径> <agentId> <聊天内容>".to_string());
}
let prompt = args[3..].join(" ");
let prompt = prompt.trim();
if prompt.is_empty() {
return Err("聊天内容不能为空".to_string());
}
return Ok(Some(CliCommand::AgentChat {
project_path: PathBuf::from(project_path),
agent_id: agent_id.trim().to_string(),
prompt: prompt.to_string(),
}));
}
if args.first().map(String::as_str) != Some("--agent-run") {
return Ok(None);
}
@@ -105,6 +131,26 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
Err("LLM 配置未就绪".to_string())
}
}
CliCommand::AgentChat {
project_path,
agent_id,
prompt,
} => {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
let reply = runtime.block_on(chat_with_game_creator_role_agent_at(
&project_path,
&agent_id,
&prompt,
))?;
println!("agent.chat.completed");
println!("projectPath={}", project_path.display());
println!("agentId={agent_id}");
println!("replyText={}", reply.reply_text);
Ok(())
}
CliCommand::AgentRun {
project_path,
prompt,
@@ -5091,6 +5091,22 @@ fn cli_agent_run_requires_project_and_prompt() {
wait_for_enter: true,
}
);
let agent_chat = parse_cli_command(&[
"--agent-chat".to_string(),
"/tmp/genarrative-cli-game".to_string(),
"art-director".to_string(),
"我要生成一个开罗风格的 dota".to_string(),
])
.expect("parse agent chat")
.expect("agent chat command");
assert_eq!(
agent_chat,
CliCommand::AgentChat {
project_path: PathBuf::from("/tmp/genarrative-cli-game"),
agent_id: "art-director".to_string(),
prompt: "我要生成一个开罗风格的 dota".to_string(),
}
);
let no_wait = parse_cli_command(&[
"--agent-run".to_string(),
"--no-wait".to_string(),
@@ -5109,6 +5125,7 @@ fn cli_agent_run_requires_project_and_prompt() {
);
assert!(parse_cli_command(&[]).expect("parse no cli").is_none());
assert!(parse_cli_command(&["--agent-run".to_string()]).is_err());
assert!(parse_cli_command(&["--agent-chat".to_string()]).is_err());
}
#[test]
+58 -20
View File
@@ -3002,16 +3002,35 @@ export function WorkspaceLauncher({
const message = `已保存用户消息;Agent 回复失败:${
error instanceof Error ? error.message : String(error)
}`;
setAgentChatMessages([
...savedUserResult.messages,
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: message,
agentId: null,
updatedAt: Date.now(),
},
]);
try {
const errorResult = await invoke<LocalConversationResult>(
'append_local_conversation_message',
{
projectPath: projectPathForChat,
agentId: agent.id,
message: {
role: 'assistant',
content: message,
agentId: null,
},
},
);
if (agentChatLoadVersionRef.current !== saveVersion) {
return;
}
setAgentChatMessages(errorResult.messages);
} catch {
setAgentChatMessages([
...savedUserResult.messages,
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: message,
agentId: null,
updatedAt: Date.now(),
},
]);
}
setAgentChatStatus(message);
} else {
setAgentChatInput(content);
@@ -12323,16 +12342,35 @@ export function App() {
const message = `已保存用户消息;Agent 回复失败:${
error instanceof Error ? error.message : String(error)
}`;
setAgentConversationMessages([
...savedUserResult.messages,
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: message,
agentId: null,
updatedAt: Date.now(),
},
]);
try {
const errorResult = await invoke<LocalConversationResult>(
'append_local_conversation_message',
{
projectPath: nextProjectPath,
agentId: agent.id,
message: {
role: 'assistant',
content: message,
agentId: null,
},
},
);
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
setAgentConversationMessages(errorResult.messages);
} catch {
setAgentConversationMessages([
...savedUserResult.messages,
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: message,
agentId: null,
updatedAt: Date.now(),
},
]);
}
setAgentConversationStatus(message);
} else {
setAgentConversationInput(content);
@@ -1060,6 +1060,108 @@ describe('AI 游戏创作 App 界面边界', () => {
);
});
it('persists developer agent chat reply failures after saving the user message', async () => {
const persistedMessages: Array<{
role: 'user' | 'assistant';
content: string;
agentId: string | null;
}> = [];
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'check_game_creator_llm_config') {
return {
configured: true,
apiKeyPresent: true,
baseUrl: 'https://llm.example.test/v1',
model: 'gpt-5.5',
apiKind: 'openai_chat',
stream: false,
error: null,
agents: [
{
agentId: 'design-director',
label: '拆解创作方向',
configured: true,
apiKeyPresent: true,
baseUrl: 'https://llm.example.test/v1',
model: 'gpt-5.5',
apiKind: 'openai_chat',
stream: false,
error: null,
},
],
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: persistedMessages.map((message, index) => ({
schemaVersion: '1',
...message,
updatedAt: 1000 + index,
})),
};
}
if (command === 'chat_with_game_creator_role_agent') {
throw new Error('上游 LLM 连接失败');
}
if (command === 'append_local_conversation_message') {
const message = args?.message as {
role: 'user' | 'assistant';
content: string;
agentId: string | null;
};
persistedMessages.push(message);
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: persistedMessages.map((record, index) => ({
schemaVersion: '1',
...record,
updatedAt: 2000 + index,
})),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderLauncherAgentChatAt('/?agent-chat');
fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), {
target: { value: '/tmp/authorized-game' },
});
fireEvent.click(screen.getByRole('button', { name: '读取历史' }));
expect(await screen.findByText(/已读取 0 条/)).not.toBeNull();
fireEvent.change(screen.getByLabelText('Agent 聊天内容'), {
target: { value: '请真实回复,不要只记录' },
});
fireEvent.click(screen.getByRole('button', { name: '发送' }));
expect(await screen.findByText('请真实回复,不要只记录')).not.toBeNull();
expect(
(
await screen.findAllByText(
/已保存用户消息;Agent 回复失败:上游 LLM 连接失败/,
)
).length,
).toBeGreaterThan(0);
expect(persistedMessages).toEqual([
{
role: 'user',
content: '请真实回复,不要只记录',
agentId: null,
},
{
role: 'assistant',
content: '已保存用户消息;Agent 回复失败:上游 LLM 连接失败',
agentId: null,
},
]);
});
it('loads selected developer agent history immediately after switching agents', async () => {
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {