补充 Agent LLM 路由清单
新增 /llm-routes 只读聊天入口 展示全局默认路由、单独 agent 路由和配置缺口 补充 /help 与 /next 的路由清单覆盖 同步 AI 游戏创作 App 技术方案和决策记录
This commit is contained in:
@@ -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/v1,openai_responses,流式 关闭,API Key 已读取',
|
||||
);
|
||||
expect(chatText).toContain('Agent:2/3 就绪 · 1 个单独路由 · 1 个缺口');
|
||||
expect(chatText).toContain(
|
||||
'Planner:已配置 · 解析后与全局一致 · gpt-main @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 已读取',
|
||||
);
|
||||
expect(chatText).toContain(
|
||||
'Generator:已配置 · 单独路由 · generator-model @ https://generator.example.test/v1,openai_chat,流式 开启,API Key 已读取',
|
||||
);
|
||||
expect(chatText).toContain(
|
||||
'音效规划:未就绪 · 解析后与全局一致 · gpt-main @ https://llm.example.test/v1,openai_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
Reference in New Issue
Block a user