diff --git a/apps/web-console/app.js b/apps/web-console/app.js index e13041c..9647eab 100755 --- a/apps/web-console/app.js +++ b/apps/web-console/app.js @@ -6417,22 +6417,34 @@ async function executeSavedModeProfile(mode) { } function applyModeCommand(command, modeProfileId = null) { - if (!command) return; - if ($("#runButton").disabled) { + const modeCommand = String(command || "").trim(); + if (!modeCommand) return false; + const assistant = window.CognitiveAgentAssistant; + if (assistant?.isPending?.()) { + showToast("AI助手正在处理上一条指令,请稍候"); + return false; + } + if ($("#runButton")?.disabled) { showToast("上一条指令仍在执行,请稍候"); - return; + return false; } const mode = modeProfiles().find((item) => item.id === modeProfileId); setModeRailSelection(mode); - if (mode?.command_flow?.source_request_id) { - void executeSavedModeProfile(mode); - return; - } closeModeGallery({ restoreFocus: false }); - state.pendingModeProfileId = modeProfileId; - if (modeProfileId) setModeExecutionState(modeProfileId, "running"); - $("#utterance").value = command; - $("#requestForm").requestSubmit(); + assistant?.open?.(); + const input = $("#utterance"); + const form = $("#requestForm"); + if (!input || !form) { + showToast("AI助手暂时不可用,请稍后重试"); + return false; + } + input.value = modeCommand; + delete input.dataset.speechStage; + delete input.dataset.speechUtteranceId; + delete input.dataset.speechValue; + input.dispatchEvent(new Event("input", { bubbles: true })); + form.requestSubmit(); + return true; } function applyRuntimeTargetPolicy(runtime) { diff --git a/apps/web-console/index.html b/apps/web-console/index.html index adbbfd9..afd0d27 100755 --- a/apps/web-console/index.html +++ b/apps/web-console/index.html @@ -20,7 +20,7 @@ - + diff --git a/apps/web-console/service-worker.js b/apps/web-console/service-worker.js index 0556b0d..d9980f2 100644 --- a/apps/web-console/service-worker.js +++ b/apps/web-console/service-worker.js @@ -1,4 +1,4 @@ -const CACHE_VERSION = "cognitive-agent-pad-v275"; +const CACHE_VERSION = "cognitive-agent-pad-v276"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const APP_SHELL = [ @@ -6,7 +6,7 @@ const APP_SHELL = [ "/index.html", "/styles.css?v=dual-seat-23", "/network.js?v=android-pad-2", - "/app.js?v=dual-seat-128", + "/app.js?v=dual-seat-129", "/gesture-control.js?v=android-pad-9", "/pwa.js?v=android-pad-3", "/console-v2.css?v=delivery-state-66", diff --git a/tests/e2e/test_seat_web_api.py b/tests/e2e/test_seat_web_api.py index 3e7c88d..cb75d2a 100755 --- a/tests/e2e/test_seat_web_api.py +++ b/tests/e2e/test_seat_web_api.py @@ -621,7 +621,7 @@ operations: def test_static_console_and_runtime_endpoint(self) -> None: with self.opener.open(f"{self.base_url}/", timeout=3) as response: html = response.read().decode("utf-8") - with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-128", timeout=3) as response: + with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-129", timeout=3) as response: app_script = response.read().decode("utf-8") with self.opener.open( f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3 @@ -697,7 +697,7 @@ operations: self.assertIn("CMD 0x431 · FB 0x41A · CH1", html) self.assertIn("CMD 0x431 · FB 0x388 · CH1", html) self.assertNotIn("透明车体 · 实体设备", html) - self.assertIn('src="/app.js?v=dual-seat-128"', html) + self.assertIn('src="/app.js?v=dual-seat-129"', html) self.assertIn('href="/styles.css?v=dual-seat-23"', html) self.assertIn('src="/gesture-control.js?v=android-pad-9"', html) self.assertNotIn('class="vehicle-system-hud"', html) @@ -992,14 +992,14 @@ operations: self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script) self.assertIn('canvas.addEventListener("webglcontextlost"', app_script) self.assertIn('"cognitive-agent:module-change"', app_script) - self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v275"', service_worker) + self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v276"', service_worker) self.assertIn('/console-v2.css?v=delivery-state-66', service_worker) self.assertIn('/console-v2.js?v=delivery-state-62', service_worker) self.assertIn('/assistant-chat.css?v=codex-assistant-26', service_worker) self.assertIn('/assistant-chat.js?v=codex-assistant-26', service_worker) self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker) self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker) - self.assertIn('"/app.js?v=dual-seat-128"', service_worker) + self.assertIn('"/app.js?v=dual-seat-129"', service_worker) self.assertIn('"/speech-input.js?v=sensevoice-13"', service_worker) self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker) self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker) @@ -1456,7 +1456,7 @@ operations: ) as response: extension_script = response.read().decode("utf-8") with self.opener.open( - f"{self.base_url}/app.js?v=dual-seat-128", timeout=3 + f"{self.base_url}/app.js?v=dual-seat-129", timeout=3 ) as response: app_script = response.read().decode("utf-8") diff --git a/tests/unit/test_assistant_frontend_contract.py b/tests/unit/test_assistant_frontend_contract.py index fd3051f..d866fb4 100644 --- a/tests/unit/test_assistant_frontend_contract.py +++ b/tests/unit/test_assistant_frontend_contract.py @@ -19,7 +19,7 @@ def test_console_contains_ai_assistant_surface(): def test_codex_assistant_assets_are_versioned_together_in_the_service_worker(): service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8") - assert 'const CACHE_VERSION = "cognitive-agent-pad-v275"' in service_worker + assert 'const CACHE_VERSION = "cognitive-agent-pad-v276"' in service_worker assert '"/assistant-chat.css?v=codex-assistant-26"' in service_worker assert '"/assistant-chat.js?v=codex-assistant-26"' in service_worker diff --git a/tests/unit/test_energy_monitor_frontend_contract.py b/tests/unit/test_energy_monitor_frontend_contract.py index e07e803..7e7dca7 100644 --- a/tests/unit/test_energy_monitor_frontend_contract.py +++ b/tests/unit/test_energy_monitor_frontend_contract.py @@ -38,7 +38,7 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None: assert html.index("/assistant-chat.js?v=codex-assistant-26") < html.index( "/energy-monitor.js?v=energy-monitor-4" ) - assert 'const CACHE_VERSION = "cognitive-agent-pad-v275"' in service_worker + assert 'const CACHE_VERSION = "cognitive-agent-pad-v276"' in service_worker def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None: diff --git a/tests/unit/test_frontend_theme_contract.py b/tests/unit/test_frontend_theme_contract.py index c17051f..fa8cee3 100644 --- a/tests/unit/test_frontend_theme_contract.py +++ b/tests/unit/test_frontend_theme_contract.py @@ -47,7 +47,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None: ): assert asset in html assert f'"{asset}"' in service_worker - assert 'const CACHE_VERSION = "cognitive-agent-pad-v275"' in service_worker + assert 'const CACHE_VERSION = "cognitive-agent-pad-v276"' in service_worker def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None: diff --git a/tests/unit/test_mode_gallery_grouping.py b/tests/unit/test_mode_gallery_grouping.py index dd85d82..fdb27cf 100644 --- a/tests/unit/test_mode_gallery_grouping.py +++ b/tests/unit/test_mode_gallery_grouping.py @@ -248,3 +248,114 @@ def test_top_mode_action_formatter_uses_device_target_action_and_parameter_value "action": "设置空调温度", "parameters": "目标温度:25 ℃", } + + +def test_mode_cards_open_assistant_and_submit_enter_mode_command() -> None: + app = (WEB / "app.js").read_text(encoding="utf-8") + start = app.index("function applyModeCommand") + end = app.index("\n\nfunction applyRuntimeTargetPolicy", start) + snippet = app[start:end] + script = "const snippet = " + json.dumps(snippet) + ";\n" + r""" +const vm = require("node:vm"); +const calls = []; +const input = { + value: "", + dataset: { speechStage: "final", speechUtteranceId: "old", speechValue: "old" }, + dispatchEvent(event) { calls.push(["input", event.type, event.bubbles, this.value]); }, +}; +const form = { requestSubmit() { calls.push(["submit", input.value]); } }; +const runButton = { disabled: false }; +const savedMode = { + id: "reading-user", + name: "阅读模式", + command_flow: { source_request_id: "saved-request" }, +}; +const context = { + Event: class Event { + constructor(type, options = {}) { this.type = type; this.bubbles = options.bubbles; } + }, + window: { + CognitiveAgentAssistant: { + isPending() { return false; }, + open() { calls.push(["open"]); }, + }, + }, + modeProfiles() { return [savedMode]; }, + setModeRailSelection(mode) { calls.push(["select", mode?.id]); }, + closeModeGallery(options) { calls.push(["close", options.restoreFocus]); }, + showToast(message) { calls.push(["toast", message]); }, + $(selector) { + return { "#utterance": input, "#requestForm": form, "#runButton": runButton }[selector]; + }, +}; +vm.createContext(context); +vm.runInContext(snippet + '\nthis.result = applyModeCommand(" 进入阅读模式 ", "reading-user");', context); +console.log(JSON.stringify({ + result: context.result, + calls, + value: input.value, + speechDataset: input.dataset, +})); +""" + completed = subprocess.run( + [node_runtime(), "-e", script], + check=True, + capture_output=True, + text=True, + encoding="utf-8", + ) + result = json.loads(completed.stdout) + + assert result["result"] is True + assert result["calls"] == [ + ["select", "reading-user"], + ["close", False], + ["open"], + ["input", "input", True, "进入阅读模式"], + ["submit", "进入阅读模式"], + ] + assert result["value"] == "进入阅读模式" + assert result["speechDataset"] == {} + assert "executeSavedModeProfile" not in snippet + + +def test_mode_cards_do_not_submit_while_assistant_is_busy() -> None: + app = (WEB / "app.js").read_text(encoding="utf-8") + start = app.index("function applyModeCommand") + end = app.index("\n\nfunction applyRuntimeTargetPolicy", start) + snippet = app[start:end] + script = "const snippet = " + json.dumps(snippet) + ";\n" + r""" +const vm = require("node:vm"); +const calls = []; +const context = { + window: { CognitiveAgentAssistant: { isPending() { return true; } } }, + showToast(message) { calls.push(message); }, +}; +vm.createContext(context); +vm.runInContext(snippet + '\nthis.result = applyModeCommand("进入办公模式", "office");', context); +console.log(JSON.stringify({ result: context.result, calls })); +""" + completed = subprocess.run( + [node_runtime(), "-e", script], + check=True, + capture_output=True, + text=True, + encoding="utf-8", + ) + result = json.loads(completed.stdout) + + assert result == { + "result": False, + "calls": ["AI助手正在处理上一条指令,请稍候"], + } + + +def test_top_and_management_mode_cards_share_the_assistant_submission_entrypoint() -> None: + app = (WEB / "app.js").read_text(encoding="utf-8") + install_start = app.index('document.addEventListener("DOMContentLoaded"') + handlers = app[install_start:] + + assert handlers.count("if (button) applyModeCommand(") == 2 + assert '$(".mode-chips").addEventListener("click"' in handlers + assert '$("#modeGalleryGroups").addEventListener("click"' in handlers + assert app.count('button.dataset.modeCommand = `进入${mode.name}`;') == 2