"添加前端模板和运行代码模块"

This commit is contained in:
Codex
2026-06-01 13:43:09 +08:00
parent 24045274ad
commit 8092c4b1f8
587 changed files with 99761 additions and 0 deletions

View File

@@ -0,0 +1,651 @@
import {
DesignModeMessage,
IframeToParentMessage,
ParentToIframeMessage,
RequestResponseMessage,
MessageValidator,
BridgeInterface,
BridgeConfig,
MessageUtilsInterface,
MessageValidationResult,
AcknowledgementMessage,
HealthCheckMessage,
HealthCheckResponseMessage,
HeartbeatMessage
} from '@xagi/design-mode-shared/messages';
/**
* postMessage bridge: optional request/response, validation hooks, heartbeats.
*/
export class EnhancedBridge implements BridgeInterface {
private listeners: Map<string, Set<Function>> = new Map();
private pendingRequests: Map<string, {
resolve: Function;
reject: Function;
timeout: NodeJS.Timeout;
responseType: string;
}> = new Map();
private config: BridgeConfig;
private _isConnected = false;
private lastHeartbeat = 0;
private heartbeatTimer?: NodeJS.Timeout;
private connectionCheckTimer?: NodeJS.Timeout;
constructor(config: Partial<BridgeConfig> = {}) {
this.config = {
timeout: 10000,
retryAttempts: 3,
retryDelay: 1000,
heartbeatInterval: 30000,
debug: false,
...config
};
this.initializeMessageHandling();
this.initializeHeartbeat();
this.initializeConnectionCheck();
}
/** Listen for `message` and mark connected after a short delay (iframe vs top). */
private initializeMessageHandling() {
if (typeof window === 'undefined') return;
window.addEventListener('message', this.handleMessage.bind(this));
if (this.isIframeEnvironment()) {
setTimeout(() => {
this._isConnected = true;
this.log('Bridge initialized and connected (iframe mode)');
this.sendReadyMessage();
}, 200);
} else {
setTimeout(() => {
this._isConnected = true;
this.log('Bridge initialized and connected (main window mode)');
}, 100);
}
}
/** Notify parent that the child iframe bridge is ready. */
private sendReadyMessage() {
try {
const readyMessage = {
type: 'BRIDGE_READY',
payload: {
timestamp: this.createTimestamp(),
environment: 'iframe'
},
timestamp: this.createTimestamp()
};
this.getTargetWindow().postMessage(readyMessage, '*');
this.log('Sent ready message to parent');
} catch (error) {
this.log('Failed to send ready message:', error);
}
}
/**
* Periodic HEARTBEAT posts when running inside an iframe.
*/
private initializeHeartbeat() {
if (typeof window === 'undefined') return;
if (this.isIframeEnvironment()) {
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.config.heartbeatInterval);
}
}
/** Periodically infer disconnect from stale heartbeats. */
private initializeConnectionCheck() {
if (typeof window === 'undefined') return;
this.connectionCheckTimer = setInterval(() => {
this.checkConnection();
}, this.config.heartbeatInterval * 2);
}
/** True when `window.self !== window.top`. */
private isIframeEnvironment(): boolean {
return typeof window !== 'undefined' && window.self !== window.top;
}
/** Snapshot for diagnostics / health. */
public getEnvironmentInfo(): {
isIframe: boolean;
isConnected: boolean;
origin: string;
userAgent: string;
location: string;
} {
if (typeof window === 'undefined') {
return {
isIframe: false,
isConnected: false,
origin: '',
userAgent: '',
location: ''
};
}
return {
isIframe: this.isIframeEnvironment(),
isConnected: this._isConnected,
origin: window.location.origin,
userAgent: navigator.userAgent.substring(0, 100),
location: window.location.href
};
}
/** Log bridge state to the console (dev aid). */
public diagnose(): void {
// Diagnose output is explicitly gated by debug mode to avoid noisy runtime logs.
if (!this.config.debug) return;
const env = this.getEnvironmentInfo();
console.group('[EnhancedBridge] Bridge Diagnosis');
console.log('Environment:', env);
console.log('Connection Status:', this._isConnected);
console.log('Pending Requests:', this.pendingRequests.size);
console.log('Message Listeners:', Array.from(this.listeners.keys()));
console.log('Config:', this.config);
console.groupEnd();
}
/** Parent when iframe; `window` when top-level. */
private getTargetWindow(): Window {
return this.isIframeEnvironment() ? window.parent : window;
}
/** Route incoming postMessage payloads. */
private handleMessage(event: MessageEvent) {
// Optional origin filter (disabled by default):
// if (event.origin !== window.location.origin && window.location.origin !== 'null') {
// if (!event.origin.startsWith('http') && !event.origin.startsWith('https')) {
// this.log('Received message from different origin, allowing:', event.origin);
// } else {
// this.log('Skipping message from different origin:', event.origin);
// return;
// }
// }
const message = event.data;
// Drop malformed payloads
if (!this.isValidMessage(message)) {
this.log('Invalid message received:', message);
return;
}
// Child handshake
if (message.type === 'BRIDGE_READY') {
this.log('Received ready message from child');
this._isConnected = true;
return;
}
// 父窗口 -> iframe 的控制类消息:必须优先走业务分发,不能误判为 RPC response。
// 否则像 TOGGLE_DESIGN_MODE 这类同样带 requestId/timestamp 的消息会被 isResponseMessage
// 吞掉,导致 bridge.on('TOGGLE_DESIGN_MODE') 永远不触发,父页面一直等不到 DESIGN_MODE_CHANGED。
if (this.isIframeEnvironment() && this.isParentToIframeCommand(message)) {
this.dispatchMessage(message);
return;
}
// Completes a pending sendWithResponse仅处理真正的响应类型避免与父到子命令冲突
if (this.isResponseMessage(message)) {
this.handleResponseMessage(message);
return;
}
// Fan-out to on(type) handlers
this.dispatchMessage(message);
}
/** Resolve or reject pendingRequests entry. */
private handleResponseMessage(message: RequestResponseMessage) {
const { requestId } = message;
if (this.pendingRequests.has(requestId)) {
const request = this.pendingRequests.get(requestId)!;
clearTimeout(request.timeout);
this.pendingRequests.delete(requestId);
if (message.type === 'ACKNOWLEDGEMENT') {
request.resolve({ success: true, acknowledged: true });
} else {
request.resolve(message);
}
}
}
/** Shape check before dispatch. */
private isValidMessage(message: any): message is DesignModeMessage {
return (
message &&
typeof message.type === 'string' &&
this.isSupportedMessageType(message.type)
);
}
/** Known DesignModeMessage union tags. */
private isSupportedMessageType(type: string): boolean {
const supportedTypes = [
// Iframe to Parent
'ELEMENT_SELECTED', 'ELEMENT_DESELECTED', 'CONTENT_UPDATED', 'STYLE_UPDATED',
'DESIGN_MODE_CHANGED', 'ELEMENT_STATE_RESPONSE', 'ERROR', 'ACKNOWLEDGEMENT',
'HEARTBEAT', 'HEALTH_CHECK_RESPONSE', 'BRIDGE_READY', 'ADD_TO_CHAT',
// Parent to Iframe
'TOGGLE_DESIGN_MODE', 'UPDATE_STYLE', 'UPDATE_CONTENT', 'BATCH_UPDATE',
'HEALTH_CHECK'
];
return supportedTypes.includes(type);
}
/**
* 是否为 sendWithResponse / ACK 通道的响应消息。
* 注意:不能仅凭 requestId + timestamp 判断,否则父页面发来的 TOGGLE_DESIGN_MODE
*(同样带 requestId会被误判为 response从而跳过 dispatchMessage。
*/
private isResponseMessage(message: any): message is RequestResponseMessage {
if (!message || typeof message.type !== 'string') {
return false;
}
if (message.requestId === undefined || message.timestamp === undefined) {
return false;
}
return (
message.type === 'ACKNOWLEDGEMENT' ||
message.type === 'HEALTH_CHECK_RESPONSE'
);
}
/** 父窗口发往 iframe 的控制命令(在 iframe 内必须由业务 handler 处理) */
private isParentToIframeCommand(message: DesignModeMessage): boolean {
const t = (message as { type?: string }).type;
return (
t === 'TOGGLE_DESIGN_MODE' ||
t === 'UPDATE_STYLE' ||
t === 'UPDATE_CONTENT' ||
t === 'BATCH_UPDATE' ||
t === 'HEALTH_CHECK'
);
}
/**
* Fire-and-forget postMessage (may no-op if not connected).
*/
public async send<T extends DesignModeMessage>(message: T): Promise<void> {
if (!this._isConnected && this.isIframeEnvironment()) {
this.log('Bridge not connected, attempting to reconnect...');
await new Promise(resolve => setTimeout(resolve, 100));
if (!this._isConnected) return;
}
if (!this._isConnected && !this.isIframeEnvironment()) {
this.log('Running in main window, bridge connection not applicable');
return;
}
const enhancedMessage = this.enhanceMessage(message);
try {
this.log('Sending message:', enhancedMessage);
this.getTargetWindow().postMessage(enhancedMessage, '*');
if (this.isIframeEnvironment() && enhancedMessage.requestId) {
this.sendAcknowledgement(enhancedMessage.requestId);
}
} catch (error) {
this.log('Error sending message:', error);
// Dev: surface send failures
if (process.env.NODE_ENV === 'development') {
throw error;
}
}
}
/** RPC-style: wait until matching response or timeout. */
public async sendWithResponse<T extends DesignModeMessage, R extends DesignModeMessage>(
message: T,
responseType: R['type']
): Promise<R> {
if (!this._isConnected) {
throw new Error('Bridge is not connected');
}
const enhancedMessage = this.enhanceMessage(message);
const requestId = enhancedMessage.requestId!;
return new Promise((resolve, reject) => {
// Timeout
const timeout = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new Error(`Request timeout after ${this.config.timeout}ms`));
}, this.config.timeout);
// Pending request map
this.pendingRequests.set(requestId, {
resolve,
reject,
timeout,
responseType
});
try {
this.log('Sending request with response:', enhancedMessage);
this.getTargetWindow().postMessage(enhancedMessage, '*');
} catch (error) {
this.pendingRequests.delete(requestId);
clearTimeout(timeout);
reject(error);
}
});
}
/** Ensure requestId + timestamp exist. */
private enhanceMessage<T extends DesignModeMessage>(message: T): T & { requestId?: string; timestamp: number } {
const requestId = (message as any).requestId || this.generateRequestId();
const timestamp = (message as any).timestamp || this.createTimestamp();
return {
...message,
requestId,
timestamp
};
}
/** Unique id for correlating responses. */
private generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/** ms since epoch */
private createTimestamp(): number {
return Date.now();
}
/** Best-effort ACK (messageType not wired through yet). */
private sendAcknowledgement(requestId: string) {
const acknowledgement: AcknowledgementMessage = {
type: 'ACKNOWLEDGEMENT',
payload: {
messageType: 'UNKNOWN', // TODO: propagate originating type
},
requestId,
timestamp: this.createTimestamp()
};
this.getTargetWindow().postMessage(acknowledgement, '*');
}
/** HEARTBEAT post to parent. */
private sendHeartbeat() {
const heartbeat: HeartbeatMessage = {
type: 'HEARTBEAT',
payload: {
timestamp: this.createTimestamp()
},
timestamp: this.createTimestamp()
};
this.getTargetWindow().postMessage(heartbeat, '*');
this.lastHeartbeat = Date.now();
}
/** Mark disconnected if no heartbeats for 3× interval. */
private checkConnection() {
const now = Date.now();
const timeSinceLastHeartbeat = now - this.lastHeartbeat;
if (timeSinceLastHeartbeat > this.config.heartbeatInterval * 3) {
this.log('Connection appears to be lost');
this._isConnected = false;
// Optimistic reconnect
setTimeout(() => {
this._isConnected = true;
this.log('Connection restored');
}, 1000);
}
}
/** Subscribe; returns unsubscribe. */
public on<T extends DesignModeMessage>(type: T['type'], handler: (message: T) => void): () => void {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type)!.add(handler);
// Unsubscribe
return () => {
this.listeners.get(type)?.delete(handler);
};
}
/** Remove one handler for `type`. */
public off(type: string, handler: Function): void {
this.listeners.get(type)?.delete(handler);
}
/** Invoke all listeners for message.type */
private dispatchMessage(message: DesignModeMessage) {
const handlers = this.listeners.get(message.type);
if (handlers) {
handlers.forEach(handler => {
try {
handler(message);
} catch (error) {
this.log('Error in message handler:', error);
}
});
}
}
/** `_isConnected` flag */
public isConnected(): boolean {
return this._isConnected;
}
/** Same as `isConnected` (legacy name). */
public isConnectedToTarget(): boolean {
return this._isConnected;
}
/** Clear timers, reject pendings, remove listener. */
public disconnect(): void {
this._isConnected = false;
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
if (this.connectionCheckTimer) {
clearInterval(this.connectionCheckTimer);
}
// Reject pending RPCs
this.pendingRequests.forEach(request => {
clearTimeout(request.timeout);
request.reject(new Error('Connection disconnected'));
});
this.pendingRequests.clear();
// Remove global listener
if (typeof window !== 'undefined') {
window.removeEventListener('message', this.handleMessage.bind(this));
}
this.listeners.clear();
this.log('Bridge disconnected');
}
/** Summarize connectivity; may issue HEALTH_CHECK RPC in iframe. */
public async healthCheck(): Promise<{ status: string; details: any }> {
try {
const env = this.getEnvironmentInfo();
// Top-level window
if (!env.isIframe && this._isConnected) {
return {
status: 'healthy',
details: {
...env,
message: 'Bridge is healthy and running in main window'
}
};
}
if (env.isIframe && this._isConnected) {
// Iframe: ping parent
try {
const response = await this.sendWithResponse<HealthCheckMessage, HealthCheckResponseMessage>(
{
type: 'HEALTH_CHECK',
requestId: '',
timestamp: this.createTimestamp()
},
'HEALTH_CHECK_RESPONSE'
);
return {
status: 'healthy',
details: {
...env,
response: response.payload,
message: 'Bridge is healthy and responsive'
}
};
} catch (error) {
return {
status: 'degraded',
details: {
...env,
error: error instanceof Error ? error.message : 'Unknown error',
message: 'Bridge is connected but not responding to health checks'
}
};
}
}
return {
status: env.isIframe ? 'connecting' : 'unnecessary',
details: {
...env,
message: env.isIframe ? 'Bridge is initializing' : 'Bridge not needed in main window'
}
};
} catch (error) {
return {
status: 'unhealthy',
details: {
error: error instanceof Error ? error.message : 'Unknown error',
environment: this.getEnvironmentInfo()
}
};
}
}
/** Guarded by config.debug */
private log(...args: any[]) {
// Keep debug logging capability for explicit troubleshooting sessions.
if (this.config.debug) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [EnhancedBridge]`, ...args);
}
}
/** Alias for disconnect(). */
public destroy(): void {
this.disconnect();
}
}
/** Small helpers for request ids / timestamps. */
export const MessageUtils: MessageUtilsInterface = {
generateRequestId: (): string => {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
},
createTimestamp: (): number => {
return Date.now();
},
isValidMessage: (message: any): boolean => {
return message &&
typeof message.type === 'string' &&
typeof message.timestamp === 'number';
},
createResponse: function<T extends IframeToParentMessage>(
originalMessage: ParentToIframeMessage,
payload: T extends { payload: infer P } ? P : never,
success: boolean = true,
error?: string
): T {
return {
payload,
type: (payload as any)?.type,
requestId: (originalMessage as any).requestId || '',
timestamp: this.createTimestamp()
} as unknown as T;
}
};
/** Singleton used by the design-mode client bundle. */
export const bridge = new EnhancedBridge();
/** Structural validation for a subset of message types. */
export class MessageValidatorImpl implements MessageValidator {
validate(message: any): MessageValidationResult {
const errors: string[] = [];
if (!message) {
errors.push('Message is null or undefined');
return { isValid: false, errors };
}
if (typeof message.type !== 'string') {
errors.push('Message type must be a string');
}
if (typeof message.timestamp !== 'number') {
errors.push('Message timestamp must be a number');
}
// Per-type payload checks
switch (message.type) {
case 'ELEMENT_SELECTED':
if (!message.payload?.elementInfo) {
errors.push('ELEMENT_SELECTED must have elementInfo in payload');
}
break;
case 'UPDATE_STYLE':
case 'UPDATE_CONTENT':
if (!message.payload?.sourceInfo) {
errors.push(`${message.type} must have sourceInfo in payload`);
}
break;
case 'BATCH_UPDATE':
if (!Array.isArray(message.payload?.updates)) {
errors.push('BATCH_UPDATE must have updates array in payload');
}
break;
}
return {
isValid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined
};
}
}
export const messageValidator = new MessageValidatorImpl();