提示Agent聊天LLM配置缺口

开发 Agent 聊天入口主动检查并展示当前 Agent LLM 配置状态

单 Agent 对话在 LLM 未就绪时显示配置提示并阻止发送

补充开发入口和项目对话入口的 LLM 缺口回归测试
This commit is contained in:
AIGameCreator App
2026-07-09 15:00:35 +08:00
parent 9bcd6d928c
commit 57cb1fa391
3 changed files with 335 additions and 6 deletions
+139 -5
View File
@@ -2190,6 +2190,10 @@ export function WorkspaceLauncher({
const [agentChatInput, setAgentChatInput] = useState('');
const [agentChatStatus, setAgentChatStatus] = useState('请选择项目和 Agent');
const [agentChatBusy, setAgentChatBusy] = useState(false);
const [agentChatLlmConfigStatus, setAgentChatLlmConfigStatus] =
useState<GameCreatorLlmConfigStatus | null>(null);
const [agentChatLlmStatus, setAgentChatLlmStatus] =
useState('尚未检查 LLM 配置');
const agentChatLoadVersionRef = useRef(0);
useEffect(() => {
@@ -2270,6 +2274,13 @@ export function WorkspaceLauncher({
void loadWalletLedger();
}, [walletPanelOpen, walletLedgerStatus]);
useEffect(() => {
if (launcherView !== 'agent-chat') {
return;
}
void loadAgentChatLlmStatus();
}, [launcherView, agentChatSelectedAgentId]);
useEffect(() => {
function closeSidebarMenusIfOutside(target: EventTarget | null) {
if (!(target instanceof Node)) {
@@ -2799,6 +2810,51 @@ export function WorkspaceLauncher({
);
}
function getCurrentAgentChatLlmWarning(
agent = selectedLauncherAgentChatAgent(),
) {
if (!agent) {
return null;
}
return formatAgentLlmConfigWarning(agentChatLlmConfigStatus, agent);
}
async function loadAgentChatLlmStatus() {
const invoke = resolveTauriInvoke();
if (!invoke) {
setAgentChatLlmConfigStatus(null);
setAgentChatLlmStatus('需要在 Tauri App 内检查 LLM 配置');
return;
}
setAgentChatLlmStatus('正在检查 LLM 配置');
try {
const status = await invoke<GameCreatorLlmConfigStatus>(
'check_game_creator_llm_config',
);
setAgentChatLlmConfigStatus(status);
const agent = selectedLauncherAgentChatAgent();
const agentStatus = agent ? llmStatusForAgentCard(status, agent) : null;
if (!agentStatus) {
setAgentChatLlmStatus('未找到当前 Agent 的 LLM 路由');
return;
}
setAgentChatLlmStatus(
agentStatus.configured
? `当前 Agent LLM 已配置:${
agentStatus.model ?? '未命名模型'
}API Key ${agentStatus.apiKeyPresent ? '已读取' : '未读取'}`
: `当前 Agent LLM 未就绪:${
agentStatus.error ?? '缺少 API Key 或模型配置'
}`,
);
} catch (error) {
setAgentChatLlmConfigStatus(null);
setAgentChatLlmStatus(
`LLM 状态读取失败:${error instanceof Error ? error.message : String(error)}`,
);
}
}
async function handleAgentChatPickProjectDirectory() {
const invoke = resolveTauriInvoke();
if (!invoke) {
@@ -2874,6 +2930,11 @@ export function WorkspaceLauncher({
if (!projectPathForChat || !agent || !content || agentChatBusy) {
return;
}
const llmWarning = getCurrentAgentChatLlmWarning(agent);
if (llmWarning) {
setAgentChatStatus(llmWarning);
return;
}
const invoke = resolveTauriInvoke();
if (!invoke) {
setAgentChatStatus('需要在 Tauri App 内运行');
@@ -3019,6 +3080,9 @@ export function WorkspaceLauncher({
homeAgentModeItems[0]!;
const ActiveHomeModeIcon = activeHomeMode.icon;
const currentAgentChatAgent = selectedLauncherAgentChatAgent();
const currentAgentChatLlmWarning = getCurrentAgentChatLlmWarning(
currentAgentChatAgent,
);
const currentHelpTitle =
launcherView === 'guide'
? '使用指南'
@@ -3521,6 +3585,19 @@ export function WorkspaceLauncher({
<p>{agentChatStatus}</p>
</div>
<div className="launcher-project-list-actions">
<button
type="button"
disabled={agentChatBusy}
onClick={() => void loadAgentChatLlmStatus()}
>
LLM状态
</button>
<button
type="button"
onClick={() => setRuntimeConfigOpen(true)}
>
</button>
<button
type="button"
disabled={agentChatBusy}
@@ -3591,6 +3668,7 @@ export function WorkspaceLauncher({
{currentAgentChatAgent ? (
<p>{`${taskGroupLabels[currentAgentChatAgent.group]} / ${currentAgentChatAgent.role} · ${taskStatusLabels[currentAgentChatAgent.status]}`}</p>
) : null}
<p>{agentChatLlmStatus}</p>
</div>
<small>
{currentAgentChatAgent
@@ -3598,6 +3676,17 @@ export function WorkspaceLauncher({
: '请选择 Agent'}
</small>
</header>
{currentAgentChatLlmWarning ? (
<div className="agent-llm-warning" role="status">
<strong>{currentAgentChatLlmWarning}</strong>
<button
type="button"
onClick={() => setRuntimeConfigOpen(true)}
>
</button>
</div>
) : null}
<div className="launcher-agent-chat-messages" aria-label="Agent 聊天记录">
{agentChatMessages.length > 0 ? (
agentChatMessages.map((message, index) => (
@@ -3618,13 +3707,16 @@ export function WorkspaceLauncher({
>
<input
aria-label="Agent 聊天内容"
disabled={agentChatBusy}
disabled={agentChatBusy || currentAgentChatLlmWarning !== null}
value={agentChatInput}
onChange={(event) =>
setAgentChatInput(event.currentTarget.value)
}
/>
<button type="submit" disabled={agentChatBusy}>
<button
type="submit"
disabled={agentChatBusy || currentAgentChatLlmWarning !== null}
>
</button>
</form>
@@ -10240,7 +10332,7 @@ function summarizeAgentLlmRoutes(status: GameCreatorLlmConfigStatus) {
function llmStatusForAgentCard(
status: GameCreatorLlmConfigStatus | null,
agent: AgentStatusCard,
agent: Pick<AgentStatusCard, 'id' | 'taskId'>,
) {
return (
status?.agents?.find(
@@ -10250,6 +10342,19 @@ function llmStatusForAgentCard(
);
}
function formatAgentLlmConfigWarning(
status: GameCreatorLlmConfigStatus | null,
agent: Pick<AgentStatusCard, 'id' | 'taskId' | 'title'>,
) {
const agentStatus = llmStatusForAgentCard(status, agent);
if (!agentStatus || agentStatus.configured) {
return null;
}
return `当前 Agent LLM 未就绪:${
agentStatus.error ?? `${agentStatus.label} 缺少 API Key 或模型配置`
}`;
}
function formatAgentCardLlmStatus(
status: GameCreatorLlmConfigStatus | null,
agent: AgentStatusCard,
@@ -11921,6 +12026,16 @@ export function App() {
setAgentMemoryStatus('请先初始化本地项目');
return;
}
try {
const status = await invoke<GameCreatorLlmConfigStatus>(
'check_game_creator_llm_config',
);
if (agentConversationLoadVersionRef.current === loadVersion) {
setLlmConfigStatus(status);
}
} catch {
// Agent conversation history is still useful when config status is unavailable.
}
try {
if (
!skipConversationPolicyConfirm &&
@@ -12119,6 +12234,11 @@ export function App() {
setAgentConversationStatus('请先初始化本地项目');
return;
}
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
if (llmWarning) {
setAgentConversationStatus(llmWarning);
return;
}
const saveVersion = agentConversationLoadVersionRef.current;
try {
if (
@@ -18432,6 +18552,9 @@ export function App() {
const selectedAgentLlmStatus = selectedAgent
? formatAgentDialogLlmStatus(llmConfigStatus, selectedAgent)
: null;
const selectedAgentLlmWarning = selectedAgent
? formatAgentLlmConfigWarning(llmConfigStatus, selectedAgent)
: null;
function showEarlierConversationMessages() {
setConversationVisibleCount((current) =>
@@ -19309,6 +19432,14 @@ export function App() {
</button>
</div>
</header>
{selectedAgentLlmWarning ? (
<div className="agent-llm-warning" role="status">
<strong>{selectedAgentLlmWarning}</strong>
<button type="button" onClick={() => setRuntimeConfigOpen(true)}>
</button>
</div>
) : null}
<div
className="agent-conversation-list"
onScroll={handleAgentConversationScroll}
@@ -19428,13 +19559,16 @@ export function App() {
<form className="composer" onSubmit={handleAgentConversationSubmit}>
<input
aria-label="Agent 对话内容"
disabled={agentConversationSaving}
disabled={agentConversationSaving || selectedAgentLlmWarning !== null}
value={agentConversationInput}
onChange={(event) =>
setAgentConversationInput(event.currentTarget.value)
}
/>
<button type="submit" disabled={agentConversationSaving}>
<button
type="submit"
disabled={agentConversationSaving || selectedAgentLlmWarning !== null}
>
</button>
<button
+31 -1
View File
@@ -1129,7 +1129,7 @@ textarea {
.launcher-agent-chat-main {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
grid-template-rows: auto auto minmax(0, 1fr) auto;
overflow: hidden;
}
@@ -1201,6 +1201,36 @@ textarea {
color: #fff;
}
.agent-llm-warning {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin: 12px 14px 0;
padding: 10px 12px;
border: 1px solid #f4c7a1;
border-radius: 8px;
background: #fff7ed;
color: #8a3b12;
font-size: 13px;
}
.agent-llm-warning strong {
min-width: 0;
font-weight: 700;
overflow-wrap: anywhere;
}
.agent-llm-warning button {
flex: 0 0 auto;
height: 30px;
padding: 0 10px;
border: 1px solid #f4c7a1;
border-radius: 7px;
background: #fff;
color: #8a3b12;
}
.launcher-project-development {
padding-top: 92px;
}
@@ -994,6 +994,72 @@ describe('AI 游戏创作 App 界面边界', () => {
});
});
it('shows developer agent chat LLM configuration gaps before sending', async () => {
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'check_game_creator_llm_config') {
return {
configured: false,
apiKeyPresent: false,
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4.1',
apiKind: 'openai_responses',
stream: false,
error:
'LLM 未配置:请在 game-creator.config.json 的 llm.apiKey 中设置 API Key',
agents: [
{
agentId: 'design-director',
label: '拆解创作方向',
configured: false,
apiKeyPresent: false,
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4.1',
apiKind: 'openai_responses',
stream: false,
error:
'LLM 未配置:请在 agentLlm.design-director.apiKey 中设置 API Key',
},
],
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: [],
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderLauncherAgentChatAt('/?agent-chat');
const warning = await screen.findByRole('status');
expect(warning.textContent).toContain(
'当前 Agent LLM 未就绪:LLM 未配置:请在 agentLlm.design-director.apiKey 中设置 API Key',
);
expect(screen.getByLabelText('Agent 聊天内容')).toHaveProperty(
'disabled',
true,
);
expect(screen.getByRole('button', { name: '发送' })).toHaveProperty(
'disabled',
true,
);
fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), {
target: { value: '/tmp/authorized-game' },
});
fireEvent.click(screen.getByRole('button', { name: '读取历史' }));
expect(await screen.findByText(/已读取 0 条/)).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_role_agent',
expect.anything(),
);
});
it('loads selected developer agent history immediately after switching agents', async () => {
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
@@ -12043,6 +12109,105 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(screen.getByLabelText('Agent 对话内容')).toHaveProperty('value', '');
});
it('shows LLM configuration gaps in the project agent conversation before sending', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'check_game_creator_llm_config') {
return {
configured: false,
apiKeyPresent: false,
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4.1',
apiKind: 'openai_responses',
stream: false,
error:
'LLM 未配置:请在 game-creator.config.json 的 llm.apiKey 中设置 API Key',
agents: [
{
agentId: 'design-director',
label: '拆解创作方向',
configured: false,
apiKeyPresent: false,
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4.1',
apiKind: 'openai_responses',
stream: false,
error:
'LLM 未配置:请在 agentLlm.design-director.apiKey 中设置 API Key',
},
],
};
}
if (command === 'read_local_conversation') {
return {
path: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: [],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const agentDialog = await screen.findByLabelText('Agent 对话');
const warning = await within(agentDialog).findByRole('status');
expect(warning.textContent).toContain(
'当前 Agent LLM 未就绪:LLM 未配置:请在 agentLlm.design-director.apiKey 中设置 API Key',
);
expect(within(agentDialog).getByLabelText('Agent 对话内容')).toHaveProperty(
'disabled',
true,
);
expect(
within(agentDialog).getByRole('button', { name: '发送' }),
).toHaveProperty('disabled', true);
expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_role_agent',
expect.anything(),
);
});
it('reports Tauri availability when saving an agent conversation without invoke', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',