diff --git a/examples/smart-cockpit/README.md b/examples/smart-cockpit/README.md index 43da7bf7..749fcf97 100644 --- a/examples/smart-cockpit/README.md +++ b/examples/smart-cockpit/README.md @@ -96,8 +96,9 @@ A preflight validates the Realtime configuration and all four ports before any c backend Agent loads these workflows and composes existing MCP tools; they are not dynamic MCP plugins, A2A Agent Card entries, or globally installed Agent Skills. - The foreground Agent owns realtime conversation and directly calls weather, - vehicle-state, window, and headlight tools through standard MCP. Explicit - window and headlight commands execute inline without a redundant second confirmation. + vehicle-state, window, sunroof, headlight, and climate tools through standard + MCP. Explicit single vehicle-control commands execute inline without a redundant + second confirmation. - Other cockpit work goes through the fixed `spawn_thinking` bridge. The example backend attaches over A2A, and Qwen3.8-Flash discovers the complete backend MCP surface for vehicle control, navigation, music, flash-buy, and custom workflows, diff --git a/examples/smart-cockpit/README_ZH.md b/examples/smart-cockpit/README_ZH.md index 89368e91..d0b10536 100644 --- a/examples/smart-cockpit/README_ZH.md +++ b/examples/smart-cockpit/README_ZH.md @@ -80,8 +80,8 @@ npm run example:smart-cockpit - UI 通过场景自己的 HTTP/SSE 通道展示车辆、路线、音乐、天气和订单状态,以及细粒度场景进度;Gateway 不解析这些对象。 - 用户可以通过语音创建和运行持久化的座舱自定义技能;技能是按座舱隔离的用户工作流, 由后台 Agent 加载后编排现有 MCP 工具。它不是动态 MCP 插件、A2A Agent Card 或全局 Agent Skill。 -- 前台 Agent 负责实时聊天,通过标准 MCP 直接调用天气、车况、车窗和大灯工具; - 用户明确说出的车窗和大灯指令直接执行,不再增加重复确认。 +- 前台 Agent 负责实时聊天,通过标准 MCP 直接调用天气、车况以及车窗、天窗、 + 大灯和空调工具;用户明确说出的单次车辆控制指令直接执行,不再增加重复确认。 - 其他座舱任务通过固定的 `spawn_thinking` 桥梁提交给后台。示例后台通过 A2A 接入 Gateway,Qwen3.8-Flash 会发现并调用独立的后台 MCP 工具面,完成车控、 导航、音乐、闪购和自定义技能任务,包括有序的多途经点导航。 diff --git a/examples/smart-cockpit/agent/executor.mjs b/examples/smart-cockpit/agent/executor.mjs index 2cb7554a..abc38267 100644 --- a/examples/smart-cockpit/agent/executor.mjs +++ b/examples/smart-cockpit/agent/executor.mjs @@ -12,8 +12,8 @@ const CUSTOM_SKILL_LIST_TOOL = 'custom_skill_list' export const COCKPIT_AGENT_PROMPT = `你是智能座舱的后台 Agent,负责理解并执行座舱任务。 规则: -- 单次车况查询、车窗和大灯操作通常由前台低延迟处理;当它们属于后台收到的组合任务或自定义技能时,仍须使用提供的工具真实执行。 -- 天窗、空调、导航、音乐、闪购和自定义技能操作必须使用提供的工具,不得假装已经执行。 +- 单次车况查询及车窗、天窗、大灯、空调操作通常由前台低延迟处理;当车辆操作属于后台收到的组合任务或自定义技能时,仍须使用提供的工具真实执行。 +- 导航、音乐、闪购、自定义技能及后台收到的车辆操作必须使用提供的工具,不得假装已经执行。 - 复杂请求可以连续调用多个工具;严格按照用户表达的先后顺序执行。 - 导航请求可以包含多个有序途经点。将中间地点放入 waypoints,最后一个地点作为 destination。 - 用户明确说“导航到”“带我去”“去某地”或“开始导航”时直接调用 navigation_start,成功后不要再次询问是否开始。 diff --git a/examples/smart-cockpit/docs/architecture.md b/examples/smart-cockpit/docs/architecture.md index a4bc6d7c..f2e95b79 100644 --- a/examples/smart-cockpit/docs/architecture.md +++ b/examples/smart-cockpit/docs/architecture.md @@ -51,8 +51,8 @@ cockpit-client ── GCP 6.0 ──► cockpit-gateway ── A2A ──► coc - `cockpit-service` 是车辆、导航、音乐、天气和闪购状态的唯一来源。 - UI 通过 HTTP 获取快照、执行面板操作,通过 SSE 接收状态变化。 -- Gateway 的前台 Agent 通过 `/mcp/frontend` 直接使用天气、车况、车窗和大灯工具; - 明确的车窗和大灯口头指令直接执行,不增加重复确认。 +- Gateway 的前台 Agent 通过 `/mcp/frontend` 直接使用天气、车况以及车窗、天窗、 + 大灯和空调工具;明确的单次车辆控制口头指令直接执行,不增加重复确认。 - 后台 Agent 通过 `/mcp/backend` 使用完整工具面,支持组合任务以及自定义技能的 发现、创建、加载和执行。 - 两个工具面由 `service/tools/registry.mjs` 显式组合,但共用同一份执行器和座舱状态; diff --git a/examples/smart-cockpit/gateway/frontend-mcp.json b/examples/smart-cockpit/gateway/frontend-mcp.json index 334763d1..abe162ac 100644 --- a/examples/smart-cockpit/gateway/frontend-mcp.json +++ b/examples/smart-cockpit/gateway/frontend-mcp.json @@ -17,9 +17,17 @@ "enabled": true, "description": "立即打开或关闭指定车窗;只在用户明确要求控制时调用,该指令即为本次操作授权。不要再通过 spawn_thinking 重复执行同一操作。" }, + "vehicle_sunroof_control": { + "enabled": true, + "description": "立即打开或关闭车辆天窗;只在用户明确要求控制时调用,该指令即为本次操作授权。不要再通过 spawn_thinking 重复执行同一操作。" + }, "vehicle_headlights_control": { "enabled": true, "description": "立即打开或关闭车辆大灯;只在用户明确要求控制时调用,该指令即为本次操作授权。不要再通过 spawn_thinking 重复执行同一操作。" + }, + "vehicle_climate_control": { + "enabled": true, + "description": "立即打开或关闭空调,或设置用户明确指定的温度、制冷制热模式和风量;不要把状态查询当成控制指令,也不要再通过 spawn_thinking 重复执行同一操作。" } } } diff --git a/examples/smart-cockpit/gateway/spawn-thinking-tool.mjs b/examples/smart-cockpit/gateway/spawn-thinking-tool.mjs index 1eb5948e..0eae1494 100644 --- a/examples/smart-cockpit/gateway/spawn-thinking-tool.mjs +++ b/examples/smart-cockpit/gateway/spawn-thinking-tool.mjs @@ -1,6 +1,6 @@ export const COCKPIT_SPAWN_THINKING_DESCRIPTION = [ '将需要座舱 Agent 执行的任务异步提交到后台。', - '用于车辆天窗和空调控制,单点或多途经点导航与当前路线查询,音乐播放,以及示例闪购流程。车况查询、车窗和大灯控制由前台工具直接执行,不要提交到后台。', + '用于单点或多途经点导航与当前路线查询、音乐播放,以及示例闪购流程。天气、车况和单次车辆控制由前台工具直接执行,不要提交到后台。', '复杂、多步骤或包含多个地点的座舱指令也应提交给后台,不要由前台自行判断不支持。', '用户要求创建、更新或运行自定义座舱技能时也应提交给后台;objective 必须保留技能名称、触发语义和完整步骤。', '闪购的搜索、加购预览和确认下单是不同任务;必须在 objective 中忠实保留用户选定的商品和当前动作,不要把加购改写为搜索。', diff --git a/examples/smart-cockpit/gateway/test/frontend-tools.test.mjs b/examples/smart-cockpit/gateway/test/frontend-tools.test.mjs index 8ae2806f..629f521b 100644 --- a/examples/smart-cockpit/gateway/test/frontend-tools.test.mjs +++ b/examples/smart-cockpit/gateway/test/frontend-tools.test.mjs @@ -29,7 +29,9 @@ test('calls selected cockpit tools inline through the frontend MCP client', asyn weather: { enabled: true }, vehicle_state_query: { enabled: true }, vehicle_window_control: { enabled: true }, + vehicle_sunroof_control: { enabled: true }, vehicle_headlights_control: { enabled: true }, + vehicle_climate_control: { enabled: true }, }, }, }, @@ -42,7 +44,9 @@ test('calls selected cockpit tools inline through the frontend MCP client', asyn 'mcp__cockpit__weather', 'mcp__cockpit__vehicle_state_query', 'mcp__cockpit__vehicle_window_control', + 'mcp__cockpit__vehicle_sunroof_control', 'mcp__cockpit__vehicle_headlights_control', + 'mcp__cockpit__vehicle_climate_control', ]) const output = await client.execute('mcp__cockpit__weather', { city: '杭州' }) assert.match(output.text, /杭州,晴,27°/u) @@ -53,6 +57,13 @@ test('calls selected cockpit tools inline through the frontend MCP client', asyn await client.execute('mcp__cockpit__vehicle_headlights_control', { action: 'open', }) + await client.execute('mcp__cockpit__vehicle_sunroof_control', { + action: 'open', + }) + await client.execute('mcp__cockpit__vehicle_climate_control', { + action: 'set_temp', + temperature: 22, + }) const state = await client.execute('mcp__cockpit__vehicle_state_query', { part: 'all', }) @@ -60,5 +71,7 @@ test('calls selected cockpit tools inline through the frontend MCP client', asyn assert.match(state.text, /大灯: 开启/u) assert.equal(service.snapshot().weather.daytemp, '27') assert.equal(service.snapshot().vehicle.windowFL, 1) + assert.equal(service.snapshot().vehicle.sunroof, 1) assert.equal(service.snapshot().vehicle.headlights, 1) + assert.equal(service.snapshot().vehicle.acTemp, 22) }) diff --git a/examples/smart-cockpit/service/README.md b/examples/smart-cockpit/service/README.md index 58a657bd..bf8c60f4 100644 --- a/examples/smart-cockpit/service/README.md +++ b/examples/smart-cockpit/service/README.md @@ -15,7 +15,7 @@ npm run example:smart-cockpit:service Endpoints: - `POST /mcp/frontend` — foreground MCP surface; weather, vehicle-state queries, - window control, and headlight control. + and window, sunroof, headlight, and climate control. - `POST /mcp/backend` — complete backend Agent MCP surface for composed work, including custom skill discovery, creation, and loading. - `GET /api/cockpit/state?cockpitId=default` — current snapshot. diff --git a/examples/smart-cockpit/service/test/server.test.mjs b/examples/smart-cockpit/service/test/server.test.mjs index a606be48..3717963f 100644 --- a/examples/smart-cockpit/service/test/server.test.mjs +++ b/examples/smart-cockpit/service/test/server.test.mjs @@ -186,7 +186,9 @@ test('scopes frontend tools while retaining a complete backend MCP surface', asy 'weather', 'vehicle_state_query', 'vehicle_window_control', + 'vehicle_sunroof_control', 'vehicle_headlights_control', + 'vehicle_climate_control', ]) const output = await backend.callTool({ @@ -210,9 +212,23 @@ test('scopes frontend tools while retaining a complete backend MCP surface', asy assert.equal(window.isError, undefined) assert.equal(window.structuredContent.vehicle.windowFL, 1) - const unavailable = await frontend.callTool({ + const sunroof = await frontend.callTool({ + name: 'vehicle_sunroof_control', + arguments: { action: 'open' }, + }) + assert.equal(sunroof.isError, undefined) + assert.equal(sunroof.structuredContent.vehicle.sunroof, 1) + + const climate = await frontend.callTool({ name: 'vehicle_climate_control', - arguments: { action: 'close' }, + arguments: { action: 'set_fan', fan: 4 }, + }) + assert.equal(climate.isError, undefined) + assert.equal(climate.structuredContent.vehicle.acFan, 4) + + const unavailable = await frontend.callTool({ + name: 'music_pause', + arguments: {}, }) assert.equal(unavailable.isError, true) assert.match(unavailable.content[0].text, /not available on this MCP surface/u) @@ -220,6 +236,8 @@ test('scopes frontend tools while retaining a complete backend MCP surface', asy const state = await fetch(`${server.origin}/api/cockpit/state?cockpitId=mcp-car`) .then(response => response.json()) assert.equal(state.vehicle.acTemp, 22) + assert.equal(state.vehicle.acFan, 4) + assert.equal(state.vehicle.sunroof, 1) assert.equal(state.vehicle.windowFL, 1) assert.equal(state.weather.dayweather, '晴') }) diff --git a/examples/smart-cockpit/service/tools/registry.mjs b/examples/smart-cockpit/service/tools/registry.mjs index dff5fe88..7d540bb9 100644 --- a/examples/smart-cockpit/service/tools/registry.mjs +++ b/examples/smart-cockpit/service/tools/registry.mjs @@ -89,7 +89,9 @@ export const FRONTEND_TOOL_NAMES = Object.freeze([ 'weather', 'vehicle_state_query', 'vehicle_window_control', + 'vehicle_sunroof_control', 'vehicle_headlights_control', + 'vehicle_climate_control', ]) const toolDefinitionsByName = new Map( diff --git a/examples/smart-cockpit/test/tool-surfaces.test.mjs b/examples/smart-cockpit/test/tool-surfaces.test.mjs index de5ede0d..1744f49a 100644 --- a/examples/smart-cockpit/test/tool-surfaces.test.mjs +++ b/examples/smart-cockpit/test/tool-surfaces.test.mjs @@ -12,7 +12,9 @@ test('adds a foreground fast path while retaining a complete backend orchestrati 'weather', 'vehicle_state_query', 'vehicle_window_control', + 'vehicle_sunroof_control', 'vehicle_headlights_control', + 'vehicle_climate_control', ]) assert.equal(BACKEND_TOOL_DEFINITIONS.length, 18) const backendNames = BACKEND_TOOL_DEFINITIONS.map(tool => tool.name) @@ -42,10 +44,14 @@ test('binds the cockpit frontend profile to the scoped MCP configuration', () => 'weather', 'vehicle_state_query', 'vehicle_window_control', + 'vehicle_sunroof_control', 'vehicle_headlights_control', + 'vehicle_climate_control', ]) assert.equal(config.servers.cockpit.tools.vehicle_window_control.enabled, true) assert.ok(!('approval' in config.servers.cockpit.tools.vehicle_window_control)) + assert.equal(config.servers.cockpit.tools.vehicle_climate_control.enabled, true) + assert.ok(!('approval' in config.servers.cockpit.tools.vehicle_climate_control)) }) test('keeps asynchronous cockpit acknowledgements natural and action-specific', () => {