mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
feat(desktop): add preview bridge protocol and host<->page commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d3e7a35e7a
commit
06461a93e9
@ -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)
|
||||
|
||||
@ -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<R: Runtime>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 页面 → 宿主:注入脚本经 IPC 调此命令;Rust 校验后转发给前端。
|
||||
#[tauri::command]
|
||||
pub fn preview_message<R: Runtime>(app: AppHandle<R>, raw: String) -> Result<(), String> {
|
||||
// 仅转发 JSON 字符串;前端做强类型解析(schema 校验在 TS 侧 protocol)
|
||||
serde_json::from_str::<serde_json::Value>(&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<R: Runtime>(app: AppHandle<R>, 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::*;
|
||||
|
||||
15
desktop/src/preview-agent/protocol.test.ts
Normal file
15
desktop/src/preview-agent/protocol.test.ts
Normal file
@ -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()
|
||||
})
|
||||
})
|
||||
36
desktop/src/preview-agent/protocol.ts
Normal file
36
desktop/src/preview-agent/protocol.ts
Normal file
@ -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<string, unknown>)['v'] !== 1 ||
|
||||
typeof (obj as Record<string, unknown>)['type'] !== 'string' ||
|
||||
!HOST_TYPES.has((obj as Record<string, unknown>)['type'] as string)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const { v: _v, ...rest } = obj as Record<string, unknown>
|
||||
return rest as unknown as HostMessage
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user