/** * 插件页(iframe)侧桥接 client。 * 用法(插件页入口):`import { getHarnessClient } from '@aso/plugin-sdk/client'` * 通信:postMessage —— 页 → window.parent;宿主 → iframe.contentWindow。 * 宿主严格校验消息来源 origin(plugin://);invoke 仅暴露白名单方法。 */ import type { OpenViewAction, StatusItem } from './status' /** 桥协议版本(宿主与插件页握手比对;不匹配时宿主拒绝 invoke) */ export const HARNESS_BRIDGE_VERSION = 1 /** 宿主 → 插件页 */ export type HostToPluginMessage = | { v: number; type: 'harness:hello'; contract: string } | { v: number; type: 'harness:theme'; dark: boolean; accent?: string } | { v: number; type: 'harness:deep-link'; params?: unknown } | { v: number; type: 'harness:agent'; event: HarnessAgentEvent } | { v: number; type: 'harness:invoke-result'; seq: number; ok: true; value: unknown } | { v: number; type: 'harness:invoke-result'; seq: number; ok: false; error: string } /** 插件页 → 宿主 */ export type PluginToHostMessage = | { v: number; type: 'plugin:hello'; pluginId: string } | { v: number; type: 'plugin:open-view'; view: string; params?: unknown } | { v: number; type: 'plugin:set-status'; id: string; item: StatusItem | null } | { v: number; type: 'plugin:invoke'; seq: number; method: string; args: unknown[] } /** 宿主中继的 agent 事件(宽松类型:具体事件形状由宿主版本决定,插件页防御性读取) */ export interface HarnessAgentEvent { type: string [key: string]: unknown } /** invoke 白名单(宿主桥逐方法放行;后续按需扩充) */ export interface HarnessInvokeMap { getState(): unknown send(text: string, images?: { mimeType: string; data: string }[]): void getSettings(): unknown } export interface HarnessTheme { dark: boolean accent?: string } export interface HarnessClient { /** 握手完成(收到 harness:hello)后 resolve */ ready: Promise /** 宿主契约版本(ready 后可用) */ contractVersion(): string /** 深链打开视图 */ openView(view: string, params?: unknown): void /** 写/清一条状态胶囊(item=null 移除) */ setStatus(id: string, item: StatusItem | null): void /** 深链参数:页面被 openView 打开/更新时回调(含挂起期间的一次重放) */ onDeepLink(cb: (params: unknown) => void): () => void /** 主题变化(深浅色/accent) */ onTheme(cb: (theme: HarnessTheme) => void): () => void /** agent 事件中继(宿主挑选的子集:state/agent_start/agent_end/stats…) */ onAgentEvent(cb: (event: HarnessAgentEvent) => void): () => void /** 调用宿主白名单 API */ invoke( method: M, ...args: Parameters ): Promise>> } type DeepLinkCb = (params: unknown) => void type ThemeCb = (theme: HarnessTheme) => void type AgentCb = (event: HarnessAgentEvent) => void let cached: HarnessClient | null = null /** 获取桥接 client(单例;首个调用即开始握手) */ export function getHarnessClient(pluginId: string): HarnessClient { if (cached) return cached let contract = '' let resolveReady: (() => void) | null = null const ready = new Promise((resolve) => { resolveReady = resolve }) const deepLinkCbs = new Set() const themeCbs = new Set() const agentCbs = new Set() const pending = new Map void; reject: (e: Error) => void }>() let seq = 0 window.addEventListener('message', (event: MessageEvent) => { const msg = event.data as HostToPluginMessage | undefined if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string' || !msg.type.startsWith('harness:')) return switch (msg.type) { case 'harness:hello': contract = msg.contract resolveReady?.() resolveReady = null break case 'harness:theme': for (const cb of themeCbs) cb({ dark: msg.dark, accent: msg.accent }) break case 'harness:deep-link': for (const cb of deepLinkCbs) cb(msg.params) break case 'harness:agent': for (const cb of agentCbs) cb(msg.event) break case 'harness:invoke-result': { const waiter = pending.get(msg.seq) if (!waiter) break pending.delete(msg.seq) if (msg.ok) waiter.resolve(msg.value) else waiter.reject(new Error(msg.error)) break } default: break } }) const post = (msg: PluginToHostMessage): void => { // 宿主 origin 随运行模式不同(file:// 或 dev server),用 '*' 发送; // 方向安全由宿主侧的 origin 白名单校验保证 window.parent?.postMessage({ ...msg, v: HARNESS_BRIDGE_VERSION }, '*') } const invokeRaw = (method: string, ...args: unknown[]): Promise => new Promise((resolve, reject) => { const id = ++seq pending.set(id, { resolve, reject }) post({ v: HARNESS_BRIDGE_VERSION, type: 'plugin:invoke', seq: id, method, args }) }) post({ v: HARNESS_BRIDGE_VERSION, type: 'plugin:hello', pluginId }) cached = { ready, contractVersion: () => contract, openView: (view, params) => post({ v: HARNESS_BRIDGE_VERSION, type: 'plugin:open-view', view, params }), setStatus: (id, item) => post({ v: HARNESS_BRIDGE_VERSION, type: 'plugin:set-status', id, item }), onDeepLink(cb) { deepLinkCbs.add(cb) return () => void deepLinkCbs.delete(cb) }, onTheme(cb) { themeCbs.add(cb) return () => void themeCbs.delete(cb) }, onAgentEvent(cb) { agentCbs.add(cb) return () => void agentCbs.delete(cb) }, invoke: invokeRaw as HarnessClient['invoke'] } return cached } export type { OpenViewAction, StatusItem }