补充 Agent LLM 路由清单

新增 /llm-routes 只读聊天入口

展示全局默认路由、单独 agent 路由和配置缺口

补充 /help 与 /next 的路由清单覆盖

同步 AI 游戏创作 App 技术方案和决策记录
This commit is contained in:
AIGameCreator App
2026-07-04 14:16:24 +08:00
parent 608764d974
commit 7f210c5028
4 changed files with 203 additions and 3 deletions
+111
View File
@@ -1864,6 +1864,7 @@ const chatCommandHelp = [
'/project /绝对路径:设置本地项目目录',
'/config:打开运行时配置',
'/llm-status:检查 LLM 配置',
'/llm-routes:查看 Agent LLM 路由清单',
'/capabilities:查看 Agent 能力清单',
'/audit:审计当前项目的 Agent 能力证据',
'/status:查看项目状态',
@@ -6283,6 +6284,7 @@ function summarizeNextProjectActions(
addSuggestion('查看数值与难度口径', '/balance');
addSuggestion('查看当前阻塞项', '/blockers');
addSuggestion('查看试玩就绪度', '/ready');
addSuggestion('查看 Agent LLM 路由', '/llm-routes');
addSuggestion('查看任务依赖链', '/deps');
addSuggestion('准备下一轮改版说明', '/revise');
addSuggestion('查看隐私与导出边界', '/privacy');
@@ -7788,6 +7790,71 @@ function formatLlmAgentStatusLine(agent: GameCreatorAgentLlmConfigStatus) {
return parts.join('');
}
function formatLlmRouteEndpoint(
status: Pick<
GameCreatorLlmConfigStatus,
'baseUrl' | 'model' | 'apiKind' | 'stream' | 'apiKeyPresent'
>,
) {
return `${status.model ?? '未命名模型'} @ ${
status.baseUrl ?? '未设置 base_url'
}${status.apiKind} ${
status.stream ? '开启' : '关闭'
}API Key ${status.apiKeyPresent ? '已读取' : '未读取'}`;
}
function isSameResolvedLlmRouteAsGlobal(
globalStatus: GameCreatorLlmConfigStatus,
agentStatus: GameCreatorAgentLlmConfigStatus,
) {
return (
agentStatus.baseUrl === globalStatus.baseUrl &&
agentStatus.model === globalStatus.model &&
agentStatus.apiKind === globalStatus.apiKind &&
agentStatus.stream === globalStatus.stream
);
}
function summarizeAgentLlmRoutes(status: GameCreatorLlmConfigStatus) {
const agents = status.agents ?? [];
const readyCount = agents.filter((agent) => agent.configured).length;
const gapAgents = agents.filter((agent) => !agent.configured);
const separateRouteAgents = agents.filter(
(agent) => !isSameResolvedLlmRouteAsGlobal(status, agent),
);
const routeLines =
agents.length > 0
? agents.map((agent) => {
const routeMode = isSameResolvedLlmRouteAsGlobal(status, agent)
? '解析后与全局一致'
: '单独路由';
const parts = [
`- ${agent.label}${agent.configured ? '已配置' : '未就绪'}`,
routeMode,
formatLlmRouteEndpoint(agent),
];
if (!agent.configured && agent.error) {
parts.push(`错误:${agent.error}`);
}
return parts.join(' · ');
})
: ['- 暂无 Agent 路由'];
const draftCommand = gapAgents.length > 0 ? '/config' : '/llm-status';
return {
text: [
'Agent LLM 路由:',
`- 默认路由:${formatLlmRouteEndpoint(status)}`,
`- Agent${readyCount}/${agents.length} 就绪 · ${separateRouteAgents.length} 个单独路由 · ${gapAgents.length} 个缺口`,
`- 路由清单:\n${routeLines.join('\n')}`,
'- 边界:只读取运行时配置解析结果;不请求上游;不显示 API Key;不写项目',
`- 建议:${draftCommand}`,
].join('\n'),
draftCommand,
draftCommandLabel: gapAgents.length > 0 ? '打开配置' : '查看状态',
};
}
function llmStatusForAgentCard(
status: GameCreatorLlmConfigStatus | null,
agent: AgentStatusCard,
@@ -11038,6 +11105,11 @@ export function App() {
return;
}
if (prompt === '/llm-routes') {
void executeLlmRouteSummary();
return;
}
if (prompt === '/open-project' || prompt === '/show-project') {
if (!requireChatProjectForUserAction()) {
return;
@@ -11857,6 +11929,45 @@ export function App() {
}
}
async function executeLlmRouteSummary() {
const invoke = resolveTauriInvoke();
if (!invoke) {
setLlmConfigStatus(null);
setMessages((current) => [
...current,
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
]);
return;
}
try {
const status = await invoke<GameCreatorLlmConfigStatus>(
'check_game_creator_llm_config',
);
setLlmConfigStatus(status);
setCommandLog((current) => [...current, 'llm.route_summary']);
const summary = summarizeAgentLlmRoutes(status);
setMessages((current) => [
...current,
{
role: 'assistant',
text: summary.text,
draftCommand: summary.draftCommand,
draftCommandLabel: summary.draftCommandLabel,
},
]);
} catch (error) {
setLlmConfigStatus(null);
setMessages((current) => [
...current,
{
role: 'assistant',
text: error instanceof Error ? error.message : String(error),
},
]);
}
}
async function executeAgentCapabilitiesChat() {
const invoke = resolveTauriInvoke();
let capabilities: readonly GameCreationAgentCapabilityDescriptor[] =
@@ -4996,6 +4996,7 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(screen.getByText(/查看下一轮小步清单:\/todo/)).not.toBeNull();
expect(screen.getByText(/查看当前阻塞项:\/blockers/)).not.toBeNull();
expect(screen.getByText(/查看试玩就绪度:\/ready/)).not.toBeNull();
expect(screen.getByText(/查看 Agent LLM 路由:\/llm-routes/)).not.toBeNull();
expect(screen.getByText(/查看任务依赖链:\/deps/)).not.toBeNull();
expect(screen.getByText(/准备下一轮改版说明:\/revise/)).not.toBeNull();
expect(screen.getByText(/查看隐私与导出边界:\/privacy/)).not.toBeNull();
@@ -12785,6 +12786,9 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
screen.getByText(/\/audit:审计当前项目的 Agent 能力证据/),
).not.toBeNull();
expect(
screen.getByText(/\/llm-routes:查看 Agent LLM 路由清单/),
).not.toBeNull();
expect(screen.getByText(/\/checkpoint:保存本地项目快照/)).not.toBeNull();
expect(screen.getByText(/\/export:导出本地试玩包/)).not.toBeNull();
expect(screen.getByText(/\/exports:列出本地试玩包/)).not.toBeNull();
@@ -17837,6 +17841,90 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(invoke).toHaveBeenCalledWith('check_game_creator_llm_config');
});
it('summarizes Agent LLM routes from chat without leaking API key values', async () => {
const invoke = vi.fn(async (command: string) => {
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://llm.example.test/v1',
model: 'gpt-main',
apiKind: 'openai_responses',
stream: false,
error: null,
},
{
agentId: 'generator',
label: 'Generator',
configured: true,
apiKeyPresent: true,
baseUrl: 'https://generator.example.test/v1',
model: 'generator-model',
apiKind: 'openai_chat',
stream: true,
error: null,
},
{
agentId: 'audio-sfx',
label: '音效规划',
configured: false,
apiKeyPresent: false,
baseUrl: 'https://llm.example.test/v1',
model: 'gpt-main',
apiKind: 'openai_responses',
stream: false,
error:
'LLM 未配置:请在 agentLlm.audio-sfx.apiKey 中设置 API Key',
},
],
};
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
submitChat('/llm-routes');
expect(await screen.findByText(/Agent LLM 路由:/)).not.toBeNull();
const chatText = screen.getByLabelText('聊天').textContent ?? '';
expect(chatText).toContain(
'默认路由:gpt-main @ https://llm.example.test/v1openai_responses,流式 关闭,API Key 已读取',
);
expect(chatText).toContain('Agent2/3 就绪 · 1 个单独路由 · 1 个缺口');
expect(chatText).toContain(
'Planner:已配置 · 解析后与全局一致 · gpt-main @ https://llm.example.test/v1openai_responses,流式 关闭,API Key 已读取',
);
expect(chatText).toContain(
'Generator:已配置 · 单独路由 · generator-model @ https://generator.example.test/v1openai_chat,流式 开启,API Key 已读取',
);
expect(chatText).toContain(
'音效规划:未就绪 · 解析后与全局一致 · gpt-main @ https://llm.example.test/v1openai_responses,流式 关闭,API Key 未读取 · 错误:LLM 未配置:请在 agentLlm.audio-sfx.apiKey 中设置 API Key',
);
expect(chatText).toContain(
'边界:只读取运行时配置解析结果;不请求上游;不显示 API Key;不写项目',
);
expect(screen.getByRole('button', { name: '打开配置' })).not.toBeNull();
expect(screen.queryByText(/sk-test-secret/)).toBeNull();
expect(screen.queryByText(/generator-secret/)).toBeNull();
expect(invoke).toHaveBeenCalledWith('check_game_creator_llm_config');
expect(invoke).not.toHaveBeenCalledWith(
'generate_local_game_draft',
expect.anything(),
);
});
it('checks LLM config from the main window shortcut without leaking the API key value', async () => {
const invoke = vi.fn(async (command: string) => {
if (command === 'check_game_creator_llm_config') {
@@ -40,7 +40,7 @@
## 2026-06-30 AI 游戏创作 App 使用客户端配置文件
- 背景:`apps/ai-game-creator-shell` 是客户端 App,不应通过 `.env` 或进程环境变量承载 LLM / 画板同步配置;旧口径会让本地 secrets、CLI wrapper 和桌面 App 启动逻辑混在一起。
- 决策:仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板;发布 App 启动时在 Tauri 应用配置目录写入默认 `game-creator.config.json`,真实密钥和本机覆盖项都保存在该运行时配置文件中。主窗口提供“配置”面板读写该运行时 JSON;开发 CLI 无 AppHandle 时才回退读取仓库旁边的模板和 gitignored 本机覆盖文件。`llm.apiKey/baseUrl/model/apiKind/stream/requestTimeoutMs/maxRetries/retryBackoffMs` 驱动全局 LLM 路径,`agentLlm.<agentId>` 可为 Planner、Generator 和角色 agent 单独覆盖 API Key、base URL、模型、API 类型和流式请求,空项继承全局配置;`editorApi.baseUrl/apiKey` 驱动画板项目同步;`/llm-status` 只展示全局和各 agent resolved 后的 baseUrl、model、apiKind、stream 和 API Key 是否存在,不显示密钥。生成游戏或平台美术遇到 LLM / editorApi 缺配置错误时,主窗口自动打开运行时配置弹窗,但错误消息仍只显示缺失项,不回显密钥值。
- 决策:仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板;发布 App 启动时在 Tauri 应用配置目录写入默认 `game-creator.config.json`,真实密钥和本机覆盖项都保存在该运行时配置文件中。主窗口提供“配置”面板读写该运行时 JSON;开发 CLI 无 AppHandle 时才回退读取仓库旁边的模板和 gitignored 本机覆盖文件。`llm.apiKey/baseUrl/model/apiKind/stream/requestTimeoutMs/maxRetries/retryBackoffMs` 驱动全局 LLM 路径,`agentLlm.<agentId>` 可为 Planner、Generator 和角色 agent 单独覆盖 API Key、base URL、模型、API 类型和流式请求,空项继承全局配置;`editorApi.baseUrl/apiKey` 驱动画板项目同步;`/llm-status` 只展示全局和各 agent resolved 后的 baseUrl、model、apiKind、stream 和 API Key 是否存在,不显示密钥`/llm-routes` 复用同一只读检查结果,按 agent 展示 resolved provider 路由、单独路由数量和缺口数量,不请求上游、不显示密钥、不写项目。生成游戏或平台美术遇到 LLM / editorApi 缺配置错误时,主窗口自动打开运行时配置弹窗,但错误消息仍只显示缺失项,不回显密钥值。
- 影响范围:AI 游戏创作 App 的 Tauri Rust 配置加载、主窗口配置面板、CLI wrapper、agent-run smoke、`check-config` 门禁、`.gitignore` 和实施计划文档。
- 验证方式:运行 `npm run ai-game-creator-shell:typecheck``cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml``npm run check:encoding``git diff --check`
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
File diff suppressed because one or more lines are too long