补充Agent LLM状态展示

回填LLM状态到Agent列表和单Agent对话
补充Agent级Provider状态的用户面测试
同步AI游戏创作App技术方案和共享决策记录
This commit is contained in:
AIGameCreator App
2026-07-03 02:36:08 +08:00
parent 875d9fd727
commit c503b15ab5
4 changed files with 192 additions and 28 deletions
+102 -26
View File
@@ -3095,6 +3095,58 @@ function formatLlmAgentStatusLine(agent: GameCreatorAgentLlmConfigStatus) {
return parts.join('');
}
function llmStatusForAgentCard(
status: GameCreatorLlmConfigStatus | null,
agent: AgentStatusCard,
) {
return (
status?.agents?.find(
(candidate) =>
candidate.agentId === agent.taskId || candidate.agentId === agent.id,
) ?? null
);
}
function formatAgentCardLlmStatus(
status: GameCreatorLlmConfigStatus | null,
agent: AgentStatusCard,
) {
const agentStatus = llmStatusForAgentCard(status, agent);
if (!agentStatus) {
return null;
}
return [
`LLM${agentStatus.configured ? '已配置' : '未就绪'}`,
agentStatus.model ?? '未命名模型',
agentStatus.apiKind,
`流式${agentStatus.stream ? '开' : '关'}`,
`Key${agentStatus.apiKeyPresent ? '已读' : '未读'}`,
].join(' · ');
}
function formatAgentDialogLlmStatus(
status: GameCreatorLlmConfigStatus | null,
agent: AgentStatusCard,
) {
const agentStatus = llmStatusForAgentCard(status, agent);
if (!agentStatus) {
return null;
}
const parts = [
`LLM${agentStatus.configured ? '已配置' : '未就绪'}`,
`${agentStatus.model ?? '未命名模型'} @ ${
agentStatus.baseUrl ?? '未设置 base_url'
}`,
agentStatus.apiKind,
`流式 ${agentStatus.stream ? '开启' : '关闭'}`,
`API Key ${agentStatus.apiKeyPresent ? '已读取' : '未读取'}`,
];
if (!agentStatus.configured && agentStatus.error) {
parts.push(`错误:${agentStatus.error}`);
}
return parts.join('');
}
function formatTraceTaskId(taskId: string, tasks: GameCreationAppTaskState[]) {
const task = tasks.find((candidate) => candidate.id === taskId);
if (!task) {
@@ -3546,6 +3598,8 @@ export function App() {
useState(false);
const [agentRunStatus, setAgentRunStatus] = useState('未运行');
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
const [llmConfigStatus, setLlmConfigStatus] =
useState<GameCreatorLlmConfigStatus | null>(null);
const [workspaceStatus, setWorkspaceStatus] = useState('请选择工作区');
const [selectedAgent, setSelectedAgent] = useState<AgentStatusCard | null>(
null,
@@ -5547,6 +5601,7 @@ export function App() {
async function executeLlmConfigStatus() {
const invoke = resolveTauriInvoke();
if (!invoke) {
setLlmConfigStatus(null);
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
@@ -5558,6 +5613,7 @@ export function App() {
const status = await invoke<GameCreatorLlmConfigStatus>(
'check_game_creator_llm_config',
);
setLlmConfigStatus(status);
setCommandLog((current) => [...current, 'llm.config_check']);
const agentLines = (status.agents ?? []).map(formatLlmAgentStatusLine);
const summary = status.configured
@@ -5579,6 +5635,7 @@ export function App() {
},
]);
} catch (error) {
setLlmConfigStatus(null);
setMessages((current) => [
...current,
{
@@ -9333,6 +9390,9 @@ export function App() {
0,
agentConversationMessages.length - visibleAgentConversationMessages.length,
);
const selectedAgentLlmStatus = selectedAgent
? formatAgentDialogLlmStatus(llmConfigStatus, selectedAgent)
: null;
function showEarlierConversationMessages() {
setConversationVisibleCount((current) =>
@@ -10017,32 +10077,39 @@ export function App() {
</div>
</header>
<div className="agent-status-list">
{agentStatusCards.map((agent) => (
<button
key={agent.id}
type="button"
disabled={!localProject}
onClick={() => openAgentConversation(agent)}
>
<strong>{agent.title}</strong>
<span>{`${taskGroupLabels[agent.group]} / ${agent.role} · ${taskStatusLabels[agent.status]}`}</span>
{agent.pass !== null || agent.phase ? (
<small>{`pass ${agent.pass ?? '-'} · ${agent.phase ?? '-'}`}</small>
) : null}
{agent.lifecycleStatus ? (
<small>{`run: ${agent.lifecycleStatus}`}</small>
) : null}
{agent.taskGraphState ? (
<small>{`编排:${
agentTaskGraphStateLabels[agent.taskGraphState]
}`}</small>
) : null}
{!agent.hasRecentEvidence ? (
<small></small>
) : null}
<small>{agent.summary}</small>
</button>
))}
{agentStatusCards.map((agent) => {
const agentLlmStatus = formatAgentCardLlmStatus(
llmConfigStatus,
agent,
);
return (
<button
key={agent.id}
type="button"
disabled={!localProject}
onClick={() => openAgentConversation(agent)}
>
<strong>{agent.title}</strong>
<span>{`${taskGroupLabels[agent.group]} / ${agent.role} · ${taskStatusLabels[agent.status]}`}</span>
{agentLlmStatus ? <small>{agentLlmStatus}</small> : null}
{agent.pass !== null || agent.phase ? (
<small>{`pass ${agent.pass ?? '-'} · ${agent.phase ?? '-'}`}</small>
) : null}
{agent.lifecycleStatus ? (
<small>{`run: ${agent.lifecycleStatus}`}</small>
) : null}
{agent.taskGraphState ? (
<small>{`编排:${
agentTaskGraphStateLabels[agent.taskGraphState]
}`}</small>
) : null}
{!agent.hasRecentEvidence ? (
<small></small>
) : null}
<small>{agent.summary}</small>
</button>
);
})}
</div>
</section>
</section>
@@ -10098,9 +10165,18 @@ export function App() {
}`}
</p>
) : null}
{selectedAgentLlmStatus ? (
<p className="status-line">{selectedAgentLlmStatus}</p>
) : null}
<p className="status-line">{selectedAgent.summary}</p>
</div>
<div className="panel-actions">
<button
type="button"
onClick={() => void executeLlmConfigStatus()}
>
LLM状态
</button>
<button
type="button"
onClick={() => void openAgentConversation(selectedAgent)}
@@ -1812,6 +1812,53 @@ describe('AI 游戏创作 App 界面边界', () => {
if (command === 'get_limited_local_commands') {
return [{ id: 'game.static_smoke', title: '静态入口自检' }];
}
if (command === 'check_game_creator_llm_config') {
return {
configured: true,
apiKeyPresent: true,
baseUrl: 'https://llm.example.test/v1',
model: 'gpt-main',
apiKind: 'openai_responses',
stream: false,
error: null,
agents: [
{
agentId: 'planner',
label: 'Planner',
configured: true,
apiKeyPresent: true,
baseUrl: 'https://planner.example.test/v1',
model: 'planner-model',
apiKind: 'anthropic',
stream: true,
error: null,
},
{
agentId: 'art-asset-plan',
label: '美术组 / Asset',
configured: true,
apiKeyPresent: true,
baseUrl: 'https://art.example.test/v1',
model: 'art-model',
apiKind: 'openai_chat',
stream: true,
error: null,
},
{
agentId: 'audio-asset-plan',
label: '音乐组 / SFX',
configured: false,
apiKeyPresent: false,
baseUrl: 'https://audio.example.test/v1',
model: 'audio-model',
apiKind: 'openai_chat',
stream: false,
error:
'LLM 未配置:请在 agentLlm.audio-asset-plan.apiKey 中设置 API Key',
},
],
};
}
if (command === 'list_local_project_files') {
return {
projectPath: String(args?.projectPath ?? ''),
@@ -1878,6 +1925,14 @@ describe('AI 游戏创作 App 界面边界', () => {
exists: true,
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: String(args?.taskId ?? ''),
path: 'memory/agents/art/asset.md',
content: '# 美术私有记忆\n- 使用厨房像素素材\n',
exists: true,
};
}
if (command === 'read_local_project_file') {
if (args?.relativePath === 'game/index.html') {
return {
@@ -1947,6 +2002,38 @@ describe('AI 游戏创作 App 界面边界', () => {
);
expect(screen.queryByLabelText('开发环境')).toBeNull();
expect(screen.queryByText('编排 Trace')).toBeNull();
expect(screen.queryByText(/LLM:已配置 · art-model/)).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'LLM状态' }));
expect(await screen.findByText(/LLM 已配置:gpt-main/)).not.toBeNull();
const agentStatusPane = screen.getByLabelText('Agent 状态');
expect(
within(agentStatusPane).getByText(
'LLM:已配置 · art-model · openai_chat · 流式开 · Key已读',
),
).not.toBeNull();
expect(
within(agentStatusPane).getByText(
'LLM:未就绪 · audio-model · openai_chat · 流式关 · Key未读',
),
).not.toBeNull();
expect(screen.queryByText(/planner-secret/)).toBeNull();
expect(screen.queryByText(/art-secret/)).toBeNull();
fireEvent.click(
within(agentStatusPane).getByRole('button', {
name: /规划美术资产/,
}),
);
const agentDialog = await screen.findByRole('dialog', {
name: 'Agent 对话',
});
expect(
within(agentDialog).getByText(
'LLM:已配置,art-model @ https://art.example.test/v1openai_chat,流式 开启,API Key 已读取',
),
).not.toBeNull();
fireEvent.click(within(agentDialog).getByRole('button', { name: '关闭' }));
fireEvent.click(screen.getByRole('button', { name: '能力' }));
expect(await screen.findByText(/Native Runtime 能力/)).not.toBeNull();
@@ -3856,6 +3856,7 @@
- 2026-06-26 调整,2026-07-03 更新:AI 游戏创作 App 借鉴 Harbour 的控制平面思想,但不搬 Harbour 后台。最近 run 在 `.agent/run.latest.json` 增加可选 `lifecycleStatus`,并通过 `/agent-status``/agent-kill``/agent-retry``/agent-resume [说明]` 控制本地生命周期,写入 `.agent/activity.jsonl``.agent/output.jsonl``.agent/context.bundle.json`;聊天里的状态 / 控制结果可填入 `/read .agent/output.jsonl` 草稿继续查看 run 输出,但不直接读取文件或绕过 `file.read` 策略。v1 的 kill/retry/resume 只更新本地状态和上下文包,不伪装成能中断已发出的上游 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。
- 2026-07-03 调整:主窗口 Agent 状态栏新增“继续说明”,只把 `/agent-resume ` 填入聊天输入框,让用户补充说明后再走原确认流;策略快捷入口新增 conversation.read / conversation.write 确认草稿,同样只填输入框,不直接写 `.agent/policy.json`
- 2026-07-03 调整:主窗口 header 常驻项目摘要只从当前已加载的 manifest / trace 派生任务完成数、ready 数、资产来源分布和最近命令结果;未选择工作区时不显示,不为了摘要额外触发 Tauri 读取或写入,也不把任务、文件、run history 或预览开发面板搬进普通用户窗口。
- 2026-07-03 调整:`/llm-status` 读取到的 agent 级 LLM 配置状态可回填到主窗口 Agent 状态列表和单 Agent 对话头部,显示 provider 类型、模型、流式开关和 API Key 是否已读取;密钥本体仍不能进入聊天、状态列表、manifest、trace 或本地项目文件。
- 2026-06-25 调整:本地 HTTP 预览静态 `HEAD` 必须返回与 `GET` 相同的真实 `Content-Length`,但不返回 body;浏览器、图片、音频和视频探测不能拿到 `Content-Length: 0` 的假响应。
- 2026-06-25 调整:普通用户通过聊天输入 `/run` 触发待确认 `game.run_local`,确认后只能复用白名单 `game.static_smoke` 自检当前 `game/index.html`,通过后启动 `127.0.0.1` 本地 HTTP 预览。独立执行 `game.static_smoke` 时如果已有 `.agent/run.latest.json`,必须追加 `Playtest / game.static_smoke` trace step,避免“运行了代码但编排 trace 不可见”。
- 2026-06-25 调整:普通用户通过聊天输入 `/trace` 触发只读 `agent.trace_read`,读取 `.agent/run.latest.json` 并在聊天里摘要 loop 轮次、stopReason、nextStep、active / carry-over 任务、repairRoutes、agent 建议命令和最近 step。trace 面板仍只在开发窗口展示,普通用户窗口不新增面板。
File diff suppressed because one or more lines are too long