34 KiB
34 KiB
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 错误类型定义
/**
* 工具加载错误类型
*/
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 错误检测逻辑
class ToolGenerator {
private loadStates = new Map<string, ToolLoadState>();
/**
* 创建 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 错误提示组件
/**
* 工具错误提示覆盖层
*/
export const ToolErrorOverlay: React.FC<ToolErrorOverlayProps> = ({
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 (
<div className="tool-error-overlay">
<div className="tool-error-overlay__icon">{config.icon}</div>
<h4 className="tool-error-overlay__title">{config.title}</h4>
<p className="tool-error-overlay__description">{config.description}</p>
<div className="tool-error-overlay__details">
<span className="tool-error-overlay__tool-name">{toolName}</span>
<span className="tool-error-overlay__url" title={url}>
{truncateUrl(url)}
</span>
</div>
<div className="tool-error-overlay__actions">
<Button size="small" onClick={onRetry}>
重试
</Button>
<Button size="small" variant="outline" onClick={onRemove}>
移除
</Button>
</div>
</div>
);
};
🔌 模块 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 ── │ 插入图片请求
│ │
消息格式规范
/**
* 通用消息格式
*/
interface ToolMessage<T = any> {
// 协议版本
version: '1.0';
// 消息类型
type: ToolMessageType;
// 工具实例 ID
toolId: string;
// 消息 ID(用于追踪和去重)
messageId: string;
// 载荷数据
payload: T;
// 时间戳
timestamp: number;
// 可选:回复的消息 ID
replyTo?: string;
}
2.2 通信服务实现
核心服务类
export class ToolCommunicationService {
private board: PlaitBoard;
private messageHandlers: Map<ToolMessageType, MessageHandler[]>;
private pendingMessages: Map<string, PendingMessage>;
private processedMessageIds: Set<string>;
constructor(board: PlaitBoard) {
this.board = board;
this.messageHandlers = new Map();
this.pendingMessages = new Map();
this.processedMessageIds = new Set();
this.setupMessageListener();
}
/**
* 发送消息给工具(带超时和重试)
*/
async sendToTool<T>(
toolId: string,
type: ToolMessageType,
payload: T,
options?: {
timeout?: number;
expectReply?: boolean;
}
): Promise<ToolMessage | void> {
const iframe = this.getToolIframe(toolId);
if (!iframe?.contentWindow) {
throw new Error(`Tool iframe not found: ${toolId}`);
}
const message: ToolMessage<T> = {
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();
}
}
消息处理器注册
// 在 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 工具端示例代码
/**
* 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 数据模型
/**
* 自定义工具存储格式
*/
interface CustomToolsStorage {
version: string;
tools: ToolDefinition[];
updatedAt: number;
}
3.2 存储服务
class ToolboxService {
private static readonly STORAGE_KEY = 'aitu:custom-tools';
private static readonly MAX_CUSTOM_TOOLS = 50;
/**
* 初始化时加载自定义工具
*/
async initialize(): Promise<void> {
const storage = await localforage.getItem<CustomToolsStorage>(
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<ToolDefinition>): Promise<void> {
// 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<ToolDefinition>): 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<void> {
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<void> {
this.customTools = [];
await this.saveCustomTools();
}
}
3.3 自定义工具对话框
export const CustomToolDialog: React.FC<CustomToolDialogProps> = ({
visible,
onClose,
onAdd,
}) => {
const [formData, setFormData] = useState<Partial<ToolDefinition>>({
name: '',
url: '',
description: '',
icon: '🔧',
category: ToolCategory.CUSTOM,
defaultWidth: 800,
defaultHeight: 600,
permissions: ['allow-scripts', 'allow-same-origin'],
});
const [errors, setErrors] = useState<Record<string, string>>({});
/**
* 表单验证
*/
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
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 (
<Dialog visible={visible} onClose={onClose} header="添加自定义工具">
<Form>
<FormItem label="工具名称" required>
<Input
value={formData.name}
onChange={(value) => setFormData({ ...formData, name: value })}
placeholder="例如:香蕉提示词"
error={errors.name}
/>
</FormItem>
<FormItem label="工具 URL" required>
<Input
value={formData.url}
onChange={(value) => setFormData({ ...formData, url: value })}
placeholder="https://example.com"
error={errors.url}
/>
</FormItem>
<FormItem label="工具描述">
<Textarea
value={formData.description}
onChange={(value) => setFormData({ ...formData, description: value })}
placeholder="简要描述工具功能"
maxlength={200}
/>
</FormItem>
<FormItem label="工具图标">
<EmojiPicker
value={formData.icon}
onChange={(emoji) => setFormData({ ...formData, icon: emoji })}
/>
</FormItem>
<FormItem label="分类">
<Select
value={formData.category}
onChange={(value) => setFormData({ ...formData, category: value })}
>
<Option value={ToolCategory.AI_TOOLS}>AI 工具</Option>
<Option value={ToolCategory.CONTENT_TOOLS}>内容工具</Option>
<Option value={ToolCategory.UTILITIES}>实用工具</Option>
<Option value={ToolCategory.CUSTOM}>自定义</Option>
</Select>
</FormItem>
<FormItem label="iframe 权限">
<Checkbox.Group
value={formData.permissions}
onChange={(value) => setFormData({ ...formData, permissions: value })}
>
<Checkbox value="allow-scripts">允许脚本</Checkbox>
<Checkbox value="allow-same-origin">允许同源访问</Checkbox>
<Checkbox value="allow-forms">允许表单</Checkbox>
<Checkbox value="allow-popups">允许弹窗</Checkbox>
</Checkbox.Group>
</FormItem>
{errors.submit && (
<div className="custom-tool-dialog__error">{errors.submit}</div>
)}
</Form>
<div slot="footer">
<Button onClick={onClose}>取消</Button>
<Button theme="primary" onClick={handleSubmit}>
添加
</Button>
</div>
</Dialog>
);
};
🎨 模块 4: 样式优化架构
4.1 CSS 变量系统
// toolbox-theme.scss
:root {
// === 工具箱主题色 ===
--toolbox-bg: #ffffff;
--toolbox-border: #e5e5e5;
--toolbox-text: #262626;
--toolbox-text-secondary: #8c8c8c;
--toolbox-hover-bg: #f5f5f5;
--toolbox-shadow: rgba(0, 0, 0, 0.08);
// === 工具卡片 ===
--tool-card-bg: #ffffff;
--tool-card-border: #d9d9d9;
--tool-card-hover-border: var(--brand-color-primary, #f39c12);
--tool-card-shadow: rgba(0, 0, 0, 0.1);
// === 工具元素(画布上) ===
--tool-element-border: transparent;
--tool-element-border-selected: var(--brand-color-primary, #f39c12);
--tool-element-shadow: rgba(0, 0, 0, 0.15);
--tool-element-shadow-selected: rgba(243, 156, 18, 0.2);
}
// 深色模式
[data-theme='dark'] {
--toolbox-bg: #1f1f1f;
--toolbox-border: #3a3a3a;
--toolbox-text: #e5e5e5;
--toolbox-text-secondary: #a6a6a6;
--toolbox-hover-bg: #2a2a2a;
--toolbox-shadow: rgba(0, 0, 0, 0.3);
--tool-card-bg: #2a2a2a;
--tool-card-border: #3a3a3a;
--tool-card-hover-border: var(--brand-color-primary, #f39c12);
--tool-card-shadow: rgba(0, 0, 0, 0.3);
--tool-element-border: transparent;
--tool-element-border-selected: var(--brand-color-primary, #f39c12);
--tool-element-shadow: rgba(0, 0, 0, 0.5);
--tool-element-shadow-selected: rgba(243, 156, 18, 0.3);
}
4.2 响应式断点
// 响应式断点
$breakpoint-mobile: 480px;
$breakpoint-tablet: 768px;
$breakpoint-desktop: 1024px;
// Mixins
@mixin mobile {
@media (max-width: $breakpoint-mobile) {
@content;
}
}
@mixin tablet {
@media (max-width: $breakpoint-tablet) {
@content;
}
}
@mixin desktop {
@media (min-width: $breakpoint-desktop) {
@content;
}
}
📊 性能优化
1. postMessage 防抖
class ToolCommunicationService {
private messageQueue: ToolMessage[] = [];
private flushTimeout: NodeJS.Timeout | null = null;
/**
* 批量发送消息(防抖)
*/
private flushMessages(): void {
if (this.messageQueue.length === 0) return;
const messages = [...this.messageQueue];
this.messageQueue = [];
messages.forEach((message) => {
const iframe = this.getToolIframe(message.toolId);
iframe?.contentWindow?.postMessage(message, '*');
});
}
/**
* 添加消息到队列
*/
private queueMessage(message: ToolMessage): void {
this.messageQueue.push(message);
if (this.flushTimeout) {
clearTimeout(this.flushTimeout);
}
this.flushTimeout = setTimeout(() => {
this.flushMessages();
}, 16); // 一帧时间
}
}
2. 错误状态缓存
class ToolGenerator {
// 缓存错误状态,避免重复检测
private errorCache = new Map<string, { type: ToolErrorType; timestamp: number }>();
private getCachedError(elementId: string): ToolErrorType | null {
const cached = this.errorCache.get(elementId);
if (!cached) return null;
// 缓存 5 分钟
if (Date.now() - cached.timestamp > 5 * 60 * 1000) {
this.errorCache.delete(elementId);
return null;
}
return cached.type;
}
}
🔒 安全考虑
1. CSP 策略
<!-- 添加 CSP meta 标签 -->
<meta
http-equiv="Content-Security-Policy"
content="frame-src 'self' https:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
/>
2. URL 白名单
class ToolboxService {
// 可信域名白名单
private trustedDomains = [
'bananabanana.me',
'unsplash.com',
'batchgenerator.com',
];
/**
* 检查 URL 是否可信
*/
isTrustedUrl(url: string): boolean {
try {
const urlObj = new URL(url);
return this.trustedDomains.some(domain =>
urlObj.hostname.endsWith(domain)
);
} catch {
return false;
}
}
}
📝 总结
Phase 3 的架构设计遵循以下原则:
- 模块化 - 每个功能独立实现,易于测试和维护
- 类型安全 - 完整的 TypeScript 类型定义
- 性能优先 - 防抖、缓存、批量处理
- 安全第一 - 消息验证、URL 白名单、CSP
- 用户体验 - 友好的错误提示、流畅的动画、响应式设计
Created by: Claude Code Date: 2025-12-09 Status: ✅ 架构设计完成