# Phase 3 架构设计 - 技术框架 > Feature: feat/08-multifunctional-toolbox > Created: 2025-12-09 > Status: 设计阶段 --- ## 📋 概述 本文档详细说明 Phase 3 的技术架构、设计模式、数据流和关键实现细节。 --- ## 🏗️ 整体架构图 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ Phase 3 增强架构 │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ UI 层 │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │ │ │ │ │ ToolErrorOverlay │ │ CustomToolDialog │ │ 样式优化组件 │ │ │ │ │ │ (错误提示) │ │ (自定义工具) │ │ (深色模式等) │ │ │ │ │ └──────────────────┘ └──────────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ ↕ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 服务层 │ │ │ │ ┌────────────────────────────┐ ┌──────────────────────────┐ │ │ │ │ │ ToolCommunicationService │ │ ToolboxService (增强) │ │ │ │ │ │ - postMessage 管理 │ │ - 自定义工具管理 │ │ │ │ │ │ - 消息路由 │ │ - 持久化存储 │ │ │ │ │ │ - 安全验证 │ │ - 工具验证 │ │ │ │ │ └────────────────────────────┘ └──────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ ↕ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 渲染层 (增强) │ │ │ │ ┌────────────────────────────┐ ┌──────────────────────────┐ │ │ │ │ │ ToolGenerator (更新) │ │ ToolComponent (更新) │ │ │ │ │ │ - 加载状态跟踪 │ │ - 样式增强 │ │ │ │ │ │ - 错误检测 │ │ - 通信集成 │ │ │ │ │ │ - 超时处理 │ │ - 错误显示 │ │ │ │ │ └────────────────────────────┘ └──────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ ↕ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 存储层 │ │ │ │ ┌────────────────────────────┐ ┌──────────────────────────┐ │ │ │ │ │ IndexedDB (localforage) │ │ 主题配置 (CSS Variables) │ │ │ │ │ │ - 自定义工具列表 │ │ - 亮色/暗色主题 │ │ │ │ │ └────────────────────────────┘ └──────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 工具 iframe (第三方) │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ │ │ 实现 Opentu Tool Protocol (可选) │ │ │ │ │ │ window.parent.postMessage({ │ │ │ │ │ │ type: 'tool:insert-text', │ │ │ │ │ │ payload: { text: '...' } │ │ │ │ │ │ }) │ │ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 🎨 模块 1: 错误处理架构 ### 1.1 错误状态机 ``` ┌──────────┐ │ LOADING │ ────┐ └──────────┘ │ │ │ timeout (10s) │ ↓ │ ┌──────────┐ │ │ ERROR │ │ │ (timeout)│ │ └──────────┘ │ │ load success ↓ ┌──────────┐ │ LOADED │ └──────────┘ ↑ │ retry │ ┌──────────┐ │ ERROR │ ← load failed / CORS blocked │ (other) │ └──────────┘ ``` ### 1.2 错误类型定义 ```typescript /** * 工具加载错误类型 */ export enum ToolErrorType { /** 加载失败 - iframe onerror */ LOAD_FAILED = 'load-failed', /** CORS 阻止 - X-Frame-Options */ CORS_BLOCKED = 'cors-blocked', /** 权限被拒绝 - iframe sandbox */ PERMISSION_DENIED = 'permission-denied', /** 加载超时 - 超过 10 秒 */ TIMEOUT = 'timeout', } /** * 工具加载状态 */ export interface ToolLoadState { status: 'loading' | 'loaded' | 'error'; errorType?: ToolErrorType; errorMessage?: string; loadStartTime: number; retryCount: number; } ``` ### 1.3 错误检测逻辑 ```typescript class ToolGenerator { private loadStates = new Map(); /** * 创建 iframe 并设置错误检测 */ private createIframe(element: PlaitTool): HTMLIFrameElement { const iframe = document.createElement('iframe'); const loadState: ToolLoadState = { status: 'loading', loadStartTime: Date.now(), retryCount: 0, }; this.loadStates.set(element.id, loadState); // 成功加载 iframe.onload = () => { // 检测 CORS 错误 if (this.detectCorsError(iframe)) { this.handleLoadError(element.id, ToolErrorType.CORS_BLOCKED); } else { this.handleLoadSuccess(element.id); } }; // 加载失败 iframe.onerror = () => { this.handleLoadError(element.id, ToolErrorType.LOAD_FAILED); }; // 超时检测 this.setupLoadTimeout(element.id, iframe); iframe.src = element.url; return iframe; } /** * 设置加载超时检测(10 秒) */ private setupLoadTimeout(elementId: string, iframe: HTMLIFrameElement): void { setTimeout(() => { const state = this.loadStates.get(elementId); if (state && state.status === 'loading') { this.handleLoadError(elementId, ToolErrorType.TIMEOUT); } }, 10000); // 10 秒超时 } /** * 检测 CORS 错误 * 尝试访问 iframe.contentWindow.location,如果抛出异常则可能是 CORS */ private detectCorsError(iframe: HTMLIFrameElement): boolean { try { // 如果可以访问 location,说明没有 CORS 限制 const _ = iframe.contentWindow?.location.href; return false; } catch (e) { // 访问被拒绝,可能是 X-Frame-Options 或 CSP return true; } } /** * 处理加载成功 */ private handleLoadSuccess(elementId: string): void { const state = this.loadStates.get(elementId); if (state) { state.status = 'loaded'; this.loadStates.set(elementId, state); } } /** * 处理加载错误 */ private handleLoadError(elementId: string, errorType: ToolErrorType): void { const state = this.loadStates.get(elementId); if (state) { state.status = 'error'; state.errorType = errorType; state.errorMessage = this.getErrorMessage(errorType); this.loadStates.set(elementId, state); // 触发错误事件,让组件显示错误提示 this.emitErrorEvent(elementId, errorType); } } /** * 获取错误提示文案 */ private getErrorMessage(errorType: ToolErrorType): string { const messages = { [ToolErrorType.LOAD_FAILED]: '工具加载失败,请检查网络连接', [ToolErrorType.CORS_BLOCKED]: '该网站禁止嵌入,无法显示', [ToolErrorType.PERMISSION_DENIED]: '权限不足,无法加载工具', [ToolErrorType.TIMEOUT]: '加载超时,请重试', }; return messages[errorType] || '未知错误'; } /** * 触发错误事件 */ private emitErrorEvent(elementId: string, errorType: ToolErrorType): void { const event = new CustomEvent('tool-load-error', { detail: { elementId, errorType }, }); window.dispatchEvent(event); } } ``` ### 1.4 错误提示组件 ```typescript /** * 工具错误提示覆盖层 */ export const ToolErrorOverlay: React.FC = ({ errorType, toolName, url, onRetry, onRemove, }) => { const errorConfig = { [ToolErrorType.LOAD_FAILED]: { icon: '⚠️', title: '加载失败', description: '工具无法加载,请检查网络连接', }, [ToolErrorType.CORS_BLOCKED]: { icon: '🚫', title: '无法显示', description: '该网站禁止嵌入到其他页面', }, [ToolErrorType.TIMEOUT]: { icon: '⏱️', title: '加载超时', description: '工具加载时间过长,请重试', }, [ToolErrorType.PERMISSION_DENIED]: { icon: '🔒', title: '权限不足', description: '缺少必要的权限,无法加载', }, }; const config = errorConfig[errorType]; return (
{config.icon}

{config.title}

{config.description}

{toolName} {truncateUrl(url)}
); }; ``` --- ## 🔌 模块 2: postMessage 通信架构 ### 2.1 通信协议设计 #### 消息流向 ``` 画布 (Opentu) 工具 (iframe) │ │ │ ──────── BOARD_TO_TOOL_INIT ────> │ 初始化 │ │ │ <─────── TOOL_TO_BOARD_READY ───── │ 就绪通知 │ │ │ ──────── BOARD_TO_TOOL_DATA ────> │ 发送数据 │ │ │ <─── TOOL_TO_BOARD_INSERT_TEXT ─── │ 插入文本请求 │ │ │ <─── TOOL_TO_BOARD_INSERT_IMAGE ── │ 插入图片请求 │ │ ``` #### 消息格式规范 ```typescript /** * 通用消息格式 */ interface ToolMessage { // 协议版本 version: '1.0'; // 消息类型 type: ToolMessageType; // 工具实例 ID toolId: string; // 消息 ID(用于追踪和去重) messageId: string; // 载荷数据 payload: T; // 时间戳 timestamp: number; // 可选:回复的消息 ID replyTo?: string; } ``` ### 2.2 通信服务实现 #### 核心服务类 ```typescript export class ToolCommunicationService { private board: PlaitBoard; private messageHandlers: Map; private pendingMessages: Map; private processedMessageIds: Set; constructor(board: PlaitBoard) { this.board = board; this.messageHandlers = new Map(); this.pendingMessages = new Map(); this.processedMessageIds = new Set(); this.setupMessageListener(); } /** * 发送消息给工具(带超时和重试) */ async sendToTool( toolId: string, type: ToolMessageType, payload: T, options?: { timeout?: number; expectReply?: boolean; } ): Promise { const iframe = this.getToolIframe(toolId); if (!iframe?.contentWindow) { throw new Error(`Tool iframe not found: ${toolId}`); } const message: ToolMessage = { version: '1.0', type, toolId, messageId: this.generateMessageId(), payload, timestamp: Date.now(), }; // 如果期待回复,注册 pending 消息 if (options?.expectReply) { return new Promise((resolve, reject) => { const timeoutMs = options.timeout || 5000; const timeoutId = setTimeout(() => { this.pendingMessages.delete(message.messageId); reject(new Error('Message timeout')); }, timeoutMs); this.pendingMessages.set(message.messageId, { message, resolve, reject, timeoutId, }); iframe.contentWindow.postMessage(message, '*'); }); } else { // 不期待回复,直接发送 iframe.contentWindow.postMessage(message, '*'); } } /** * 注册消息处理器 */ on(type: ToolMessageType, handler: MessageHandler): void { if (!this.messageHandlers.has(type)) { this.messageHandlers.set(type, []); } this.messageHandlers.get(type)!.push(handler); } /** * 取消注册处理器 */ off(type: ToolMessageType, handler: MessageHandler): void { const handlers = this.messageHandlers.get(type); if (handlers) { const index = handlers.indexOf(handler); if (index > -1) { handlers.splice(index, 1); } } } /** * 处理接收到的消息 */ private handleMessage = (event: MessageEvent): void => { // 1. 验证消息格式 if (!this.isValidToolMessage(event.data)) { return; } const message: ToolMessage = event.data; // 2. 防止重复处理 if (this.processedMessageIds.has(message.messageId)) { console.warn('Duplicate message:', message.messageId); return; } this.processedMessageIds.add(message.messageId); // 3. 如果是回复消息,解析 pending promise if (message.replyTo) { this.resolvePendingMessage(message); return; } // 4. 调用注册的处理器 const handlers = this.messageHandlers.get(message.type); if (handlers) { handlers.forEach(handler => handler(message)); } }; /** * 解析等待中的消息 */ private resolvePendingMessage(message: ToolMessage): void { const pending = this.pendingMessages.get(message.replyTo!); if (pending) { clearTimeout(pending.timeoutId); pending.resolve(message); this.pendingMessages.delete(message.replyTo!); } } /** * 验证消息格式和来源 */ private isValidToolMessage(data: any): boolean { if (!data || typeof data !== 'object') { return false; } // 检查必需字段 const requiredFields = ['version', 'type', 'toolId', 'messageId', 'timestamp']; const hasAllFields = requiredFields.every(field => field in data); if (!hasAllFields) { return false; } // 检查版本 if (data.version !== '1.0') { console.warn('Unsupported message version:', data.version); return false; } // 检查工具是否存在 const iframe = this.getToolIframe(data.toolId); if (!iframe) { console.warn('Message from unknown tool:', data.toolId); return false; } return true; } /** * 生成唯一消息 ID */ private generateMessageId(): string { return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * 获取工具 iframe */ private getToolIframe(toolId: string): HTMLIFrameElement | null { // 从 DOM 中查找对应的工具元素 const toolElement = document.querySelector( `[data-tool-id="${toolId}"] iframe` ) as HTMLIFrameElement; return toolElement; } /** * 清理过期的消息 ID(防止内存泄漏) */ private cleanupProcessedMessages(): void { // 保留最近 1000 条消息 ID if (this.processedMessageIds.size > 1000) { const idsArray = Array.from(this.processedMessageIds); this.processedMessageIds = new Set(idsArray.slice(-1000)); } } /** * 设置全局监听器 */ private setupMessageListener(): void { window.addEventListener('message', this.handleMessage); // 定期清理 setInterval(() => this.cleanupProcessedMessages(), 60000); // 每分钟 } /** * 销毁服务 */ destroy(): void { window.removeEventListener('message', this.handleMessage); this.pendingMessages.forEach(pending => clearTimeout(pending.timeoutId)); this.pendingMessages.clear(); this.messageHandlers.clear(); this.processedMessageIds.clear(); } } ``` #### 消息处理器注册 ```typescript // 在 withTool 插件中注册处理器 export const withTool: PlaitPlugin = (board: PlaitBoard) => { const communicationService = new ToolCommunicationService(board); // 注册处理器 communicationService.on( ToolMessageType.TOOL_TO_BOARD_INSERT_TEXT, (message) => { const { text, position } = message.payload; // 调用 Plait API 插入文本 // TODO: 实现文本插入 } ); communicationService.on( ToolMessageType.TOOL_TO_BOARD_INSERT_IMAGE, (message) => { const { url, position, width, height } = message.payload; // 调用 Plait API 插入图片 // TODO: 实现图片插入 } ); // 保存到 board (board as any).__toolCommunicationService = communicationService; return board; }; ``` ### 2.3 工具端示例代码 ```typescript /** * Opentu Tool SDK(供第三方工具使用) */ class AituToolSDK { private parentWindow: Window; private toolId: string; constructor() { this.parentWindow = window.parent; this.toolId = this.getToolIdFromUrl(); this.setupMessageListener(); this.notifyReady(); } /** * 通知画布工具已准备就绪 */ private notifyReady(): void { this.sendMessage(ToolMessageType.TOOL_TO_BOARD_READY, { version: '1.0', }); } /** * 插入文本到画布 */ insertText(text: string, position?: [number, number]): void { this.sendMessage(ToolMessageType.TOOL_TO_BOARD_INSERT_TEXT, { text, position, }); } /** * 插入图片到画布 */ insertImage( url: string, options?: { position?: [number, number]; width?: number; height?: number } ): void { this.sendMessage(ToolMessageType.TOOL_TO_BOARD_INSERT_IMAGE, { url, ...options, }); } /** * 发送消息给画布 */ private sendMessage(type: ToolMessageType, payload: any): void { const message: ToolMessage = { version: '1.0', type, toolId: this.toolId, messageId: `msg_${Date.now()}_${Math.random()}`, payload, timestamp: Date.now(), }; this.parentWindow.postMessage(message, '*'); } /** * 从 URL 参数获取 tool ID */ private getToolIdFromUrl(): string { const params = new URLSearchParams(window.location.search); return params.get('aituToolId') || 'unknown'; } /** * 监听来自画布的消息 */ private setupMessageListener(): void { window.addEventListener('message', (event) => { const message = event.data; if (message.type === ToolMessageType.BOARD_TO_TOOL_INIT) { this.handleInit(message.payload); } }); } /** * 处理初始化消息 */ private handleInit(payload: any): void { console.log('Tool initialized:', payload); } } // 使用示例 const sdk = new AituToolSDK(); // 插入文本 document.getElementById('copyBtn')?.addEventListener('click', () => { sdk.insertText('Hello from tool!'); }); // 插入图片 document.getElementById('insertImageBtn')?.addEventListener('click', () => { sdk.insertImage('https://example.com/image.png', { width: 400, height: 300, }); }); ``` --- ## ⚙️ 模块 3: 自定义工具架构 ### 3.1 数据模型 ```typescript /** * 自定义工具存储格式 */ interface CustomToolsStorage { version: string; tools: ToolDefinition[]; updatedAt: number; } ``` ### 3.2 存储服务 ```typescript class ToolboxService { private static readonly STORAGE_KEY = 'aitu:custom-tools'; private static readonly MAX_CUSTOM_TOOLS = 50; /** * 初始化时加载自定义工具 */ async initialize(): Promise { const storage = await localforage.getItem( ToolboxService.STORAGE_KEY ); if (storage) { // 验证版本兼容性 if (this.isCompatibleVersion(storage.version)) { this.customTools = storage.tools; } else { console.warn('Incompatible custom tools version, resetting'); await this.resetCustomTools(); } } } /** * 添加自定义工具(带验证) */ async addCustomTool(tool: Partial): Promise { // 1. 验证工具数量限制 if (this.customTools.length >= ToolboxService.MAX_CUSTOM_TOOLS) { throw new Error(`Maximum ${ToolboxService.MAX_CUSTOM_TOOLS} custom tools allowed`); } // 2. 验证必填字段 this.validateToolDefinition(tool); // 3. 生成唯一 ID const toolWithId: ToolDefinition = { ...tool, id: tool.id || `custom-${Date.now()}`, category: tool.category || ToolCategory.CUSTOM, defaultWidth: tool.defaultWidth || 800, defaultHeight: tool.defaultHeight || 600, permissions: tool.permissions || ['allow-scripts', 'allow-same-origin'], } as ToolDefinition; // 4. 检查 ID 唯一性 if (this.getToolById(toolWithId.id)) { throw new Error('Tool ID already exists'); } // 5. 添加到列表 this.customTools.push(toolWithId); // 6. 持久化 await this.saveCustomTools(); } /** * 验证工具定义 */ private validateToolDefinition(tool: Partial): void { // 必填字段 if (!tool.name || !tool.url) { throw new Error('Tool name and URL are required'); } // URL 格式验证 try { const url = new URL(tool.url); // 只允许 https 和 http if (!['https:', 'http:'].includes(url.protocol)) { throw new Error('Only HTTP/HTTPS URLs are allowed'); } } catch (e) { throw new Error('Invalid URL format'); } // 名称长度限制 if (tool.name.length > 50) { throw new Error('Tool name too long (max 50 characters)'); } // 描述长度限制 if (tool.description && tool.description.length > 200) { throw new Error('Tool description too long (max 200 characters)'); } } /** * 保存自定义工具 */ private async saveCustomTools(): Promise { const storage: CustomToolsStorage = { version: '1.0', tools: this.customTools, updatedAt: Date.now(), }; await localforage.setItem(ToolboxService.STORAGE_KEY, storage); } /** * 版本兼容性检查 */ private isCompatibleVersion(version: string): boolean { return version === '1.0'; // 目前只有 1.0 版本 } /** * 重置自定义工具 */ private async resetCustomTools(): Promise { this.customTools = []; await this.saveCustomTools(); } } ``` ### 3.3 自定义工具对话框 ```typescript export const CustomToolDialog: React.FC = ({ visible, onClose, onAdd, }) => { const [formData, setFormData] = useState>({ name: '', url: '', description: '', icon: '🔧', category: ToolCategory.CUSTOM, defaultWidth: 800, defaultHeight: 600, permissions: ['allow-scripts', 'allow-same-origin'], }); const [errors, setErrors] = useState>({}); /** * 表单验证 */ const validateForm = (): boolean => { const newErrors: Record = {}; if (!formData.name) { newErrors.name = '请输入工具名称'; } if (!formData.url) { newErrors.url = '请输入工具 URL'; } else { try { new URL(formData.url); } catch { newErrors.url = 'URL 格式不正确'; } } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; /** * 提交表单 */ const handleSubmit = async () => { if (!validateForm()) { return; } try { await onAdd(formData as ToolDefinition); onClose(); } catch (error) { setErrors({ submit: error.message }); } }; return (
setFormData({ ...formData, name: value })} placeholder="例如:香蕉提示词" error={errors.name} /> setFormData({ ...formData, url: value })} placeholder="https://example.com" error={errors.url} />