From 78ab1e488761c31e87ce0bc1e8be75b9c2870956 Mon Sep 17 00:00:00 2001 From: Linghong Date: Sun, 20 Sep 2026 20:59:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BA=E7=AD=96=E5=88=92Agent=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E6=A8=A1=E5=9E=8B=E4=B8=8E=E6=8E=A8=E7=90=86=E6=A1=A3?= =?UTF-8?q?=E6=8E=A7=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复用现有控件和配置读写,调整策划输入区布局 保持策划提交流程和GameAgent既有行为不变 补充控件集成用例,记录master基线导致的验证限制 --- .../ProjectSupervisorView.tsx | 65 ++++----- apps/ai-game-creator-shell/src/styles.css | 25 +++- .../tests/appSurface/design-agent.suite.ts | 125 ++++++++++++++++++ .../tests/appSurface/harness.ts | 21 ++- ...】策划Agent模型与推理档控件接入-2026-09-20.md | 52 ++++++++ ...】策划Agent模型与推理档控件接入-2026-09-20.md | 12 +- ...】策划Agent生产迁移与工作区浏览-2026-09-10.md | 12 +- 7 files changed, 265 insertions(+), 47 deletions(-) create mode 100644 docs/project-memory/plans/【实施计划】策划Agent模型与推理档控件接入-2026-09-20.md diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx index b9a73f606..2bdfec00f 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx @@ -269,6 +269,7 @@ export function ProjectSupervisorView({ ...runtimePanelProps }: ProjectSupervisorViewProps) { const designAgentActive = Boolean(designView || onDesignApprove); + const showModelControls = directCodex || designAgentActive; const [settingsOpen, setSettingsOpen] = useState(false); // 语音输入的降级/失败提示:不支持时按钮本身就带提示,这里只承载启动失败与权限类错误。 const [voiceNotice, setVoiceNotice] = useState(''); @@ -774,44 +775,46 @@ export function ProjectSupervisorView({ } onChange={onChatInputChange} /> - {directCodex ? ( + {showModelControls ? (
-
- -
+ {directCodex ? ( +
+ +
+ ) : null}
- {/* 推理档放在模型选择器旁边(Codex 的「高」那个位置):写回的是客户端 - 配置,只影响后续回合;当前回合的行为不受影响。 */} + {/* 两种对话共用客户端配置控件;运行时何时采用选择由各自入口负责。 */} - - composerRef?.current?.insertText(text) - } - onNotice={setVoiceNotice} - /> - {submitting && onCancelTurn ? ( + {directCodex ? ( + + composerRef?.current?.insertText(text) + } + onNotice={setVoiceNotice} + /> + ) : null} + {directCodex && submitting && onCancelTurn ? ( , +) { + window.__TAURI__ = { + core: { invoke: harness.invoke }, + event: { listen: harness.listen }, + }; + window.history.pushState({}, '', '/'); + return render( + React.createElement(App, { + initialProjectPath: harness.projectPath, + orchestrationMode: 'single-supervisor', + planningStartMode: true, + projectSupervisorOnly: true, + }), + ); +} + +async function expectDesignModelReady() { + await waitFor(() => { + expect( + screen.getByRole('button', { name: '对话模型' }).textContent, + ).toContain('高质量'); + }); +} + function designApprovalView() { return { session: { @@ -97,6 +123,105 @@ function designHistoryView() { } export function registerDesignAgentSurfaceTests() { + it('persists Design Agent model and reasoning controls and reads them on reentry', async () => { + const harness = createProjectSupervisorRuntimeHarness({ + designAgentView: designConversationView(), + }); + const first = renderDesignAgent(harness); + await expectDesignModelReady(); + const reasoning = screen.getByRole('button', { name: '推理档' }); + expect( + reasoning.closest('.project-supervisor-composer-controls'), + ).not.toBeNull(); + fireEvent.click(reasoning); + fireEvent.click(screen.getByRole('option', { name: '低' })); + await waitFor(() => { + expect(harness.invoke).toHaveBeenCalledWith( + 'select_game_creator_reasoning_effort', + { effort: 'low' }, + ); + expect( + screen.getByRole('button', { name: '推理档' }).textContent, + ).toContain('低'); + }); + fireEvent.click(screen.getByRole('button', { name: '对话模型' })); + fireEvent.click(await screen.findByRole('option', { name: '快速' })); + await waitFor(() => { + expect(harness.invoke).toHaveBeenCalledWith('select_game_creator_model', { + modelId: 'fast', + isDefault: false, + }); + expect( + screen.getByRole('button', { name: '对话模型' }).textContent, + ).toContain('快速'); + }); + first.unmount(); + renderDesignAgent(harness); + await waitFor(() => { + expect( + screen.getByRole('button', { name: '推理档' }).textContent, + ).toContain('低'); + expect( + screen.getByRole('button', { name: '对话模型' }).textContent, + ).toContain('快速'); + }); + expect( + harness.invoke.mock.calls.some( + ([command]) => command === 'continue_design_agent_session', + ), + ).toBe(false); + }); + + it('allows Design Agent model changes while running without submitting a new turn', async () => { + const harness = createProjectSupervisorRuntimeHarness({ + designAgentView: { ...designConversationView(), running: true }, + }); + renderDesignAgent(harness); + await expectDesignModelReady(); + expect( + (screen.getByRole('button', { name: '推理档' }) as HTMLButtonElement) + .disabled, + ).toBe(false); + fireEvent.click(screen.getByRole('button', { name: '对话模型' })); + fireEvent.click(await screen.findByRole('option', { name: '快速' })); + await waitFor(() => { + expect( + screen.getByRole('button', { name: '对话模型' }).textContent, + ).toContain('快速'); + }); + const input = screen.getByLabelText('项目需求'); + expect(input.closest('[data-disabled="true"]')).not.toBeNull(); + const send = screen.getByRole('button', { name: '思考中' }); + expect(send).toHaveProperty('disabled', true); + fireEvent.click(send); + expect( + harness.invoke.mock.calls.some( + ([command]) => command === 'continue_design_agent_session', + ), + ).toBe(false); + }); + + it('shows Design Agent reasoning save failures and preserves the saved selection', async () => { + const harness = createProjectSupervisorRuntimeHarness({ + designAgentView: designConversationView(), + }); + const invoke = harness.invoke.getMockImplementation()!; + harness.invoke.mockImplementation(async (command, args) => { + if (command === 'select_game_creator_reasoning_effort') { + throw new Error('save failed'); + } + return invoke(command, args); + }); + renderDesignAgent(harness); + await expectDesignModelReady(); + const reasoning = screen.getByRole('button', { name: '推理档' }); + await waitFor(() => expect(reasoning.textContent).toContain('高')); + fireEvent.click(reasoning); + fireEvent.click(screen.getByRole('option', { name: '低' })); + await screen.findByText('推理档保存失败'); + expect(reasoning.textContent).toContain('高'); + }); + it('resumes following Design Agent messages when sending after scrolling up', async () => { const harness = createProjectSupervisorRuntimeHarness({ designAgentView: designHistoryView(), diff --git a/apps/ai-game-creator-shell/tests/appSurface/harness.ts b/apps/ai-game-creator-shell/tests/appSurface/harness.ts index 3b38bd2fe..25f28895e 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/harness.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/harness.ts @@ -534,6 +534,8 @@ function createProjectSupervisorRuntimeHarness({ ); let messageSequence = 0; let selectedModelId = 'quality'; + let selectedModelIsDefault = false; + let reasoningEffort = 'high'; let steerSequence = 0; let sessionExists = initialSessionExists; let currentProjectRevision = initialProjectRevision; @@ -605,9 +607,11 @@ function createProjectSupervisorRuntimeHarness({ }) => void) | null = null; let designAgentUpdateHandler: - ((event: { payload: Record }) => void) | null = null; + | ((event: { payload: Record }) => void) + | null = null; let directThreadNotifyHandler: - ((event: { payload: { subscriptionId: string } }) => void) | null = null; + | ((event: { payload: { subscriptionId: string } }) => void) + | null = null; let directThreadSubscriptionId: string | null = null; let directThreadSubscriptionSequence = 0; // 未消费的运行态事件队列:`subscribe` 的 bootstrap 与 `consume` 都从这里取, @@ -672,22 +676,29 @@ function createProjectSupervisorRuntimeHarness({ async (command: string, args?: Record) => { if ( command === 'read_game_creator_app_config' || - command === 'select_game_creator_model' + command === 'select_game_creator_model' || + command === 'select_game_creator_reasoning_effort' ) { - if (command === 'select_game_creator_model') + if (command === 'select_game_creator_model') { selectedModelId = String(args?.modelId); + selectedModelIsDefault = args?.isDefault === true; + } + if (command === 'select_game_creator_reasoning_effort') { + reasoningEffort = String(args?.effort); + } return { path: '/tmp/test-game-creator-config.json', config: { schemaVersion: 'game-creator-config.v2', agentMode: 'codex_app_server', selectedModelId, + selectedModelIsDefault, llm: { apiKey: '', baseUrl: '', model: 'quality', apiKind: 'openai_responses', - reasoningEffort: 'high', + reasoningEffort, stream: true, webSearchEnabled: true, contextWindowTokens: 128000, diff --git a/docs/project-memory/plans/【实施计划】策划Agent模型与推理档控件接入-2026-09-20.md b/docs/project-memory/plans/【实施计划】策划Agent模型与推理档控件接入-2026-09-20.md new file mode 100644 index 000000000..b2cb0b714 --- /dev/null +++ b/docs/project-memory/plans/【实施计划】策划Agent模型与推理档控件接入-2026-09-20.md @@ -0,0 +1,52 @@ +# 策划 Agent 模型与推理档控件接入实施计划 + +| 字段 | 值 | +| --- | --- | +| Milestone | [控件接入里程碑](./【里程碑】策划Agent模型与推理档控件接入-2026-09-20.md) | +| Status | accepted-with-baseline-limitations | +| Owner | 当前任务 Agent | + +## 修改边界 + +- 在 `ProjectSupervisorView.tsx` 扩展既有控件排的适用范围,直接复用 `ConversationModelSelect` 与 `ComposerReasoningEffortSelect`。素材引用、语音、消息队列和停止按钮仍按原 GameAgent 条件显示。 +- 策划发送、澄清、审批和重试回调保持原样,不新增模型校验和提交门禁;GameAgent 的原校验保持不变。 +- 必要时局部调整策划宿主 CSS,避免控件换行或弹层裁切。组件内部逻辑保持不变。 +- 在现有前端测试体系补齐策划集成场景,回归已有 GameAgent 控件用例。 +- 不修改 Rust、模型生效逻辑、配置默认值、Provider 或 GameAgent 组件算法。 + +## 实现顺序 + +1. 核对共享对话容器,接入同一组控件,保留原提交路径。 +2. 补充策划控件读写、保存失败和忙态测试,回归原策划和 GameAgent 行为。 +3. 运行定向测试和类型检查,检查宽窄布局,回写证据与未验证项。 + +## 验证命令 + +- `npm test -- apps/ai-game-creator-shell/tests/appSurface.test.ts`,先按用例名称过滤控件及相关策划场景。 +- 策划宿主集成用例使用现有 Vitest / Testing Library,按实际测试文件定向运行。 +- `npm run ai-game-creator-shell:typecheck` +- `npm run check:doc-index` +- `npm run check:encoding` +- `git diff --check` + +## 风险与回滚点 + +- 防止扩大 `directCodex` 条件时带入语音、队列或改变 GameAgent 行为:仅共享控件排,保留其它分支。 +- 模型目录失败通过原选择器反馈,不给策划宿主增加前置检查,也不改造控件已有逻辑。 +- 本次回滚仅撤回前端接入、相关用例和文档,不涉及持久化数据迁移。 +- 时间盒:一个工作时段内先完成接入与定向测试,随后检查类型和布局;仅影响里程碑验收的发现纳入修复。 +- 当前已知限制:策划旧会话仍可能使用创建时模型;本次不将控件读写成功视为运行时已切换模型。 + +## 验收证据 + +| 项目 | 结果 | +| --- | --- | +| 源码边界核对 | 两个控件文件、策划动作回调、GameAgent 提交校验及 Rust 均无修改;仅扩展控件排适用条件与策划布局 | +| 定向 ESLint | `ProjectSupervisorView.tsx`、`design-agent.suite.ts`、`harness.ts` 通过 | +| 定向界面测试 | 收集失败,执行 0 条;基线 `packages/shared/src/index.ts` 引用缺失的 `./contracts/gameDistribution` | +| AGC 类型检查 | 失败;另有 `App.tsx` 引用缺失的 `./components/game-distribution/GameDistributionPublishPanel`;未报告本次修改文件错误 | +| 桌面与窄面板 smoke、真实 Provider | 未运行;应用现有缺失导入尚未解决,不将静态检查视为实机证据 | + +定向测试命令:`npm test -- apps/ai-game-creator-shell/tests/appSurface.test.ts -t 'Design Agent|design session|clarification free text|reasoning effort control|selecting the model|model dropdown|chat composer an inset|current turn reasoning|historical reasoning'`。 + +缺失文件通过 Git HEAD 核对为已有问题。本次不补替身或顺带修复;用户接受此验证限制并授权提交、进入第二步。基线恢复后可重跑上述测试、类型检查及宽窄布局 smoke,未运行项不记为通过。 diff --git a/docs/project-memory/plans/【里程碑】策划Agent模型与推理档控件接入-2026-09-20.md b/docs/project-memory/plans/【里程碑】策划Agent模型与推理档控件接入-2026-09-20.md index da19d81aa..afe66e6ec 100644 --- a/docs/project-memory/plans/【里程碑】策划Agent模型与推理档控件接入-2026-09-20.md +++ b/docs/project-memory/plans/【里程碑】策划Agent模型与推理档控件接入-2026-09-20.md @@ -3,7 +3,7 @@ | 字段 | 值 | | --- | --- | | Version | 1.0 | -| Status | proposed | +| Status | accepted(用户接受基线验证限制并授权提交、进入第二步) | | Date | 2026-09-20 | | Parent Spec | [策划 Agent 生产迁移与工作区浏览方案 §4.1](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#41-策划对话模型与推理档选择提案2026-09-20) | @@ -15,7 +15,7 @@ - 输入区域中控件的显隐、位置、策划面板布局与必要的宿主接线。 - 复用原有目录、默认模型、保存和错误提示,不复制逻辑。 -- 策划发送、澄清、审批继续和主动重试中会调用 Provider 的入口,接入已有模型可用性检查;不改变原有审批和忙态含义。 +- 策划发送、澄清、审批继续和主动重试保持原流程;不新增模型可用性检查或提交门禁。 - 重新进入策划时显示保存值;执行中可以调整后续选择,但原输入与提交忙态维持不变。 ## 不在范围内 @@ -34,7 +34,7 @@ - [ ] 策划入口显示原模型及推理档控件,没有重复实现或第二份设置。 - [ ] 选择走原保存通道;重新进入显示已保存值;保存失败显示原有错误反馈。 -- [ ] 策划会调用 Provider 的用户入口执行已有模型可用性检查,失败时不提交。 +- [ ] 策划提交和报错流程保持原样,没有新增模型检查;原控件内部行为不改造。 - [ ] 策划忙态、待审批/澄清行为保持;新增布局在宽/窄面板可操作。 - [ ] GameAgent 原控件的显示、选择、发送校验、忙态和运行行为通过相关回归。 - [ ] 交付明确记录旧策划会话仍沿用旧模型,留待下一里程碑解决。 @@ -45,3 +45,9 @@ - 运行时:桌面与窄面板选择、保存、重新进入和一次策划发送 smoke;缺失环境如实标注。 - 边界:只验证接入所需的失败和忙态,不扩展为现有组件全量审计。 - 通过本步验收后,才准备并执行模型生效里程碑的实现计划。 + +## 当前交付 + +已接入原控件及策划布局,按用户最终要求保留策划提交、审批、澄清、重试与报错流程,没有新增模型检查。原控件与 GameAgent 的提交逻辑未修改;旧策划会话固定模型的限制仍由第二步解决。 + +新增三条控件集成用例(保存重进、运行中选择、保存失败反馈),但测试尚未执行:仓库基线缺少 `packages/shared/src/contracts/gameDistribution.ts`,界面测试在收集阶段失败;类型检查另报告缺少 `GameDistributionPublishPanel`。这些路径在当前 HEAD 中也不存在,并非本次删除。运行时和宽窄面板 smoke 尚未验证。用户已明确将这些问题视为 master 缺陷、不作为本次阻塞项,并授权提交第一步、进入第二步;未运行项仍不记为通过。完整验证记录见本里程碑的实施计划。 diff --git a/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md b/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md index 11ad233df..31ace5909 100644 --- a/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md +++ b/docs/technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md @@ -122,19 +122,19 @@ Runtime 不维护文档版本号,不解析文档版本,不提供版本回退 **交付结果**:策划 Agent 对话复用 GameAgent 的模型、推理档选择控件及配置通道,用户的新选择对后续策划回合实际生效,并保持 GameAgent 原有行为兼容。 -本节是本次变更的唯一主规范;状态为 `proposed`。现有生产迁移的“已完成”状态不代表本节已完成。拆分与验收见 [控件接入里程碑](../project-memory/plans/【里程碑】策划Agent模型与推理档控件接入-2026-09-20.md) 和 [模型生效里程碑](../project-memory/plans/【里程碑】策划Agent回合模型选择生效-2026-09-20.md)。实施前只为当前已评审里程碑编写实现计划,第一步验收后再进入第二步。 +本节是本次变更的唯一主规范;状态为 `in-progress`。现有生产迁移的“已完成”状态不代表本节已完成。控件接入代码已完成,验证受仓库基线缺失导入阻断;运行时模型生效逻辑尚未修改。拆分与验收见 [控件接入里程碑](../project-memory/plans/【里程碑】策划Agent模型与推理档控件接入-2026-09-20.md) 和 [模型生效里程碑](../project-memory/plans/【里程碑】策划Agent回合模型选择生效-2026-09-20.md)。实施前只为当前已评审里程碑编写实现计划,第一步验收后再进入第二步。 #### 范围与非目标 - 必须项:复用已有控件及客户端配置读写;补齐策划入口;修正旧策划会话固定使用创建时模型的行为;验证 GameAgent 兼容性。 -- 风险项:界面选择与实际请求不一致、执行中途切换配置、恢复旧会话、审批/澄清/重试绕过模型检查,以及策划窄面板新增控件的布局。 +- 风险项:界面选择与实际请求不一致、执行中途切换配置、恢复旧会话,以及策划窄面板新增控件的布局。 - 可选项:无。发现与上述验收无关的问题,只记录发现,不扩展本次实现。 - 明确不做:不评估或重构 GameAgent 已有控件的本征不足,不重做模型目录、缓存、配置同步、下拉交互、通用保存队列或错误处理;不调整 GameAgent Runtime、推理默认值、供应商适配、提示词或阶段审批规则;不新建 Agent 专属设置、模型管理页、配置框架或测试框架。 - 优先在现有共享对话容器中复用同一组组件,不复制控件源码,不因新增一个使用方迁移整套配置型组件。若需要局部接口扩展,默认调用必须保持 GameAgent 现有行为。 #### 控件和配置合同 -1. 策划对话输入框附近显示与 GameAgent 相同的模型和推理档控件,读取、选项、保存、错误反馈沿用已有能力;只调整策划宿主必要的显隐、布局和提交接线。 +1. 策划对话输入框附近显示与 GameAgent 相同的模型和推理档控件,读取、选项、保存、错误反馈沿用已有能力;只调整策划宿主必要的显隐和布局。按用户确认,控件保持原样,不给策划发送、审批、澄清或重试新增模型可用性检查,也不以模型目录状态新增提交门禁;GameAgent 已有提交逻辑保持原样。 2. 继续使用客户端全局模型选择与全局推理档,不新增第二份策划设置。它们是客户端偏好,可能影响其它后续对话;进入或重开策划界面时显示当前保存值。既有 GameAgent 配置解析保持不变,不承诺新增跨窗口实时同步。 3. 策划运行时最终以选择器保存的全局模型和推理档作为本次用户选择;策划专属底层配置不得在这两个字段上静默覆盖选择。连接、鉴权、超时等其余字段继续按既有生产配置解析,不清理或改写用户其它配置。 4. 模型的官方目录别名、自定义模型目录、默认项跟随及失效项处理复用已有能力;不新增供应商能力探测或模型自动降级。推理档的枚举与默认值维持现状。 @@ -144,7 +144,7 @@ Runtime 不维护文档版本号,不解析文档版本,不提供版本回退 | 触发 | 目标行为 | | --- | --- | -| 首次发送、后续普通发送 | 通过已有模型可用性检查,读取最新已保存的选择并开始新回合 | +| 首次发送、后续普通发送 | 读取最新已保存的选择并开始新回合,不新增提交前模型检查 | | 回答澄清、批准/拒绝阶段后实际继续调用 Provider | 同样使用本次继续前保存的选择;不更改审批结果和请求身份语义 | | 用户主动点击失败重试 | 使用最新选择开始本次执行;继续现有工具恢复规则,不重复已完成副作用 | | 同一执行内的工具循环、HTTP/流式瞬态自动重试 | 始终使用该次执行开始时确定的模型和推理档,不在每次 Provider 调用前重新采样这两个字段 | @@ -155,7 +155,7 @@ Runtime 不维护文档版本号,不解析文档版本,不提供版本回退 #### 失败、兼容与数据约束 -- 保存失败、目录不可用和模型校验失败复用现有提示和处理路径,不让策划新增提交入口绕过既有检查。只接入原有能力,不借机修复通用控件未承诺的行为。 +- 控件保存失败和目录不可用沿用控件现有提示;策划发送、审批、澄清、重试和 Provider 报错流程保持原样。原控件内部的目录同步与默认模型处理不在本次改造范围,也不在策划宿主另加同类逻辑。 - Provider 不接受所选模型或历史上下文时,沿用现有可见错误和重试;不静默换模型、清历史或另建跨模型上下文转换系统。 - 已有设计会话无需离线迁移:下次用户发起执行时采用新选择。自动恢复的旧活动回合优先保留已有模型;若尚无推理档快照,使用当次有效配置补齐一次并固定,不伪称还原了历史档位。 - 持久化仅增加恢复必需的模型选择标识和推理档快照,旧记录缺字段可读;不保存完整配置、端点凭据、Token 或 API Key。不增加平行会话账本,不改模型目录 ID 与真实型号的边界。 @@ -165,7 +165,7 @@ Runtime 不维护文档版本号,不解析文档版本,不提供版本回退 | 步骤 | 交付边界 | 完成证据 | | --- | --- | --- | -| 第一步:控件接入 | 策划宿主显示并使用原控件,选择写入现有全局配置;策划发送等入口接入已有模型检查;不改策划请求的模型解析与快照逻辑 | 策划控件读写、忙态与失败提示定向测试;GameAgent 相关现有用例;宽/窄面板 smoke | +| 第一步:控件接入 | 策划宿主显示并使用原控件,选择写入现有全局配置;保留策划原提交流程;不改策划请求的模型解析与快照逻辑 | 策划控件读写、忙态与失败提示定向测试;GameAgent 相关现有用例;宽/窄面板 smoke | | 第二步:模型生效 | 在策划执行边界采样并固定模型与推理档,旧会话下次执行采用新选择,自动恢复保留活动回合选择 | 本地 Provider fixture 验证真实请求字段、跨工具调用一致性、重试与恢复;界面到请求 smoke;GameAgent 兼容回归 | 第一步是内部可验收的接入结果,不能宣称“策划旧会话切模型已生效”,也不能作为完整功能单独发布。第二步验收前保持这一已知限制明确。