提示Agent聊天LLM配置缺口
开发 Agent 聊天入口主动检查并展示当前 Agent LLM 配置状态 单 Agent 对话在 LLM 未就绪时显示配置提示并阻止发送 补充开发入口和项目对话入口的 LLM 缺口回归测试
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user