diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index dfefe961..4884c078 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -2189,7 +2189,9 @@ pub fn run() { webview_panel::preview_navigate, webview_panel::preview_set_bounds, webview_panel::preview_set_visible, - webview_panel::preview_close + webview_panel::preview_close, + webview_panel::preview_message, + webview_panel::preview_eval ]); // macOS: native menu bar (traffic-light overlay style) diff --git a/desktop/src-tauri/src/webview_panel.rs b/desktop/src-tauri/src/webview_panel.rs index b674e111..41661673 100644 --- a/desktop/src-tauri/src/webview_panel.rs +++ b/desktop/src-tauri/src/webview_panel.rs @@ -1,5 +1,5 @@ use std::sync::Mutex; -use tauri::{AppHandle, LogicalPosition, LogicalSize, Manager, Runtime, WebviewBuilder, WebviewUrl}; +use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, Runtime, WebviewBuilder, WebviewUrl}; const PREVIEW_LABEL: &str = "preview"; @@ -128,6 +128,21 @@ pub async fn preview_close( Ok(()) } +/// 页面 → 宿主:注入脚本经 IPC 调此命令;Rust 校验后转发给前端。 +#[tauri::command] +pub fn preview_message(app: AppHandle, raw: String) -> Result<(), String> { + // 仅转发 JSON 字符串;前端做强类型解析(schema 校验在 TS 侧 protocol) + serde_json::from_str::(&raw).map_err(|e| e.to_string())?; + app.emit("preview://event", raw).map_err(|e| e.to_string()) +} + +/// 宿主 → 页面:在子 webview 内 eval 一段 JS。 +#[tauri::command] +pub async fn preview_eval(app: AppHandle, js: String) -> Result<(), String> { + let webview = app.get_webview(PREVIEW_LABEL).ok_or_else(|| "preview not open".to_string())?; + webview.eval(&js).map_err(|e| e.to_string()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/desktop/src/preview-agent/protocol.test.ts b/desktop/src/preview-agent/protocol.test.ts new file mode 100644 index 00000000..6fa3a11b --- /dev/null +++ b/desktop/src/preview-agent/protocol.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest' +import { parseHostMessage, serializeAgentMessage } from './protocol' + +describe('preview-agent protocol', () => { + it('serializes agent→host messages to a stable envelope', () => { + expect(JSON.parse(serializeAgentMessage({ type: 'ready' }))).toEqual({ v: 1, type: 'ready' }) + expect(JSON.parse(serializeAgentMessage({ type: 'navigated', url: 'http://x/', title: 'T' }))) + .toEqual({ v: 1, type: 'navigated', url: 'http://x/', title: 'T' }) + }) + it('parses host→agent messages and rejects unknown/garbage', () => { + expect(parseHostMessage('{"v":1,"type":"enter-picker"}')).toEqual({ type: 'enter-picker' }) + expect(parseHostMessage('not json')).toBeNull() + expect(parseHostMessage('{"v":1,"type":"nope"}')).toBeNull() + }) +}) diff --git a/desktop/src/preview-agent/protocol.ts b/desktop/src/preview-agent/protocol.ts new file mode 100644 index 00000000..bd2ac191 --- /dev/null +++ b/desktop/src/preview-agent/protocol.ts @@ -0,0 +1,36 @@ +export type AgentMessage = + | { type: 'ready' } + | { type: 'navigated'; url: string; title: string } + | { type: 'error'; message: string } + | { type: 'selection'; payload: unknown } // M5 填充结构 + | { type: 'screenshot'; dataUrl: string; kind: 'full' | 'viewport' | 'element' } // M4 + +export type HostMessage = + | { type: 'enter-picker' } + | { type: 'exit-picker' } + | { type: 'capture'; kind: 'full' | 'viewport' | 'element' } + +const HOST_TYPES = new Set(['enter-picker', 'exit-picker', 'capture']) + +export function serializeAgentMessage(msg: AgentMessage): string { + return JSON.stringify({ v: 1, ...msg }) +} + +export function parseHostMessage(raw: string): HostMessage | null { + try { + const obj = JSON.parse(raw) as unknown + if ( + typeof obj !== 'object' || + obj === null || + (obj as Record)['v'] !== 1 || + typeof (obj as Record)['type'] !== 'string' || + !HOST_TYPES.has((obj as Record)['type'] as string) + ) { + return null + } + const { v: _v, ...rest } = obj as Record + return rest as unknown as HostMessage + } catch { + return null + } +}