# Feishu WebSocket Integration Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add WebSocket long-connection support for Feishu bot integration, enabling internal network deployment without requiring public domain. **Architecture:** Each bot maintains its own WebSocket connection to Feishu cloud via official SDK. Connection management handled by dedicated FeishuWsManager service. Existing webhook mode preserved for backward compatibility. **Tech Stack:** NestJS, @larksuiteoapi/node-sdk, TypeScript --- ## File Structure ``` server/src/feishu/ ├── feishu.module.ts # Register new manager ├── feishu.service.ts # Add WS control methods ├── feishu.controller.ts # Add WS API endpoints ├── feishu-ws.manager.ts # NEW: WebSocket connection manager ├── dto/ │ └── ws-status.dto.ts # NEW: WebSocket status DTOs └── entities/ └── feishu-bot.entity.ts # Add WS fields ``` --- ## Chunk 1: Dependencies & Entity Changes ### Task 1: Install Feishu SDK - [ ] **Step 1: Install @larksuiteoapi/node-sdk** ```bash cd D:\aura\AuraK\server yarn add @larksuiteoapi/node-sdk ``` - [ ] **Step 2: Verify installation** ```bash yarn list @larksuiteoapi/node-sdk ``` --- ### Task 2: Update FeishuBot Entity **Files:** - Modify: `server/src/feishu/entities/feishu-bot.entity.ts` - [ ] **Step 1: Read current entity** File: `D:\aura\AuraK\server\src\feishu\entities\feishu-bot.entity.ts` - [ ] **Step 2: Add WebSocket fields** Add after existing columns: ```typescript @Column({ default: false }) useWebSocket: boolean; @Column({ nullable: true }) wsConnectionState: string; ``` - [ ] **Step 3: Run build to verify** ```bash cd D:\aura\AuraK\server yarn build ``` --- ## Chunk 2: WebSocket Manager ### Task 3: Create WebSocket Status DTOs **Files:** - Create: `server/src/feishu/dto/ws-status.dto.ts` - [ ] **Step 1: Create DTO file** ```typescript export enum ConnectionState { DISCONNECTED = 'disconnected', CONNECTING = 'connecting', CONNECTED = 'connected', ERROR = 'error', } export interface ConnectionStatus { botId: string; state: ConnectionState; connectedAt?: Date; lastHeartbeat?: Date; error?: string; } export class ConnectWsDto { // No params needed - uses bot ID from route } export class WsStatusResponseDto { botId: string; state: ConnectionState; connectedAt?: string; lastHeartbeat?: string; error?: string; } export class WsConnectResponseDto { success: boolean; botId: string; status: ConnectionState; error?: string; } export class WsDisconnectResponseDto { success: boolean; botId: string; status: ConnectionState; } export class AllWsStatusResponseDto { connections: WsStatusResponseDto[]; } ``` --- ### Task 4: Create FeishuWsManager **Files:** - Create: `server/src/feishu/feishu-ws.manager.ts` - [ ] **Step 1: Create the WebSocket manager** ```typescript import { Injectable, Logger } from '@nestjs/common'; import { EventDispatcher, Conf } from '@larksuiteoapi/node-sdk'; import { FeishuBot } from './entities/feishu-bot.entity'; import { ConnectionState, ConnectionStatus } from './dto/ws-status.dto'; import { FeishuService } from './feishu.service'; @Injectable() export class FeishuWsManager { private readonly logger = new Logger(FeishuWsManager.name); private connections: Map = new Map(); private reconnectAttempts: Map = new Map(); private readonly MAX_RECONNECT_ATTEMPTS = 5; private readonly RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000]; // Exponential backoff constructor(private readonly feishuService: FeishuService) {} /** * Start WebSocket connection for a bot */ async connect(bot: FeishuBot): Promise { const botId = bot.id; // Check if already connected const existing = this.connections.get(botId); if (existing && existing.status.state === ConnectionState.CONNECTED) { this.logger.warn(`Bot ${botId} already connected`); return; } // Set connecting state this.updateStatus(botId, { botId, state: ConnectionState.CONNECTING, }); try { // Create event dispatcher (WebSocket client) const client = new EventDispatcher( { appId: bot.appId, appSecret: bot.appSecret, verificationToken: bot.verificationToken, } as any, { logger: { debug: (msg: any) => this.logger.debug(msg), info: (msg: any) => this.logger.log(msg), warn: (msg: any) => this.logger.warn(msg), error: (msg: any) => this.logger.error(msg), }, } as any, ); // Register event handlers client.on('im.message.receive_v1', async (data: any) => { await this.handleMessage(bot, data); }); // Store connection this.connections.set(botId, { client: client as any, status: { botId, state: ConnectionState.CONNECTED, connectedAt: new Date(), }, }); this.reconnectAttempts.set(botId, 0); this.logger.log(`WebSocket connected for bot ${botId}`); // Update bot state in DB await this.feishuService.getBotById(botId); } catch (error) { this.logger.error(`Failed to connect WebSocket for bot ${botId}`, error); this.updateStatus(botId, { botId, state: ConnectionState.ERROR, error: error.message || 'Connection failed', }); throw error; } } /** * Disconnect WebSocket for a bot */ async disconnect(botId: string): Promise { const connection = this.connections.get(botId); if (!connection) { this.logger.warn(`No connection found for bot ${botId}`); return; } try { // SDK doesn't have explicit disconnect, just remove references this.connections.delete(botId); this.reconnectAttempts.delete(botId); this.logger.log(`WebSocket disconnected for bot ${botId}`); } catch (error) { this.logger.error(`Error disconnecting bot ${botId}`, error); throw error; } } /** * Get connection status for a bot */ getStatus(botId: string): ConnectionStatus | null { const connection = this.connections.get(botId); return connection?.status || null; } /** * Get all connection statuses */ getAllStatuses(): ConnectionStatus[] { const statuses: ConnectionStatus[] = []; for (const [botId, connection] of this.connections.entries()) { statuses.push(connection.status); } return statuses; } /** * Check if a bot is connected */ isConnected(botId: string): boolean { const connection = this.connections.get(botId); return connection?.status.state === ConnectionState.CONNECTED; } /** * Handle incoming message from Feishu */ private async handleMessage(bot: FeishuBot, data: any): Promise { this.logger.log(`Received message for bot ${bot.id}: ${JSON.stringify(data)}`); try { const event = data.event || data; const message = event?.message; if (!message) { this.logger.warn('No message in event data'); return; } const messageId = message.message_id; const openId = event?.sender?.sender_id?.open_id; if (!openId) { this.logger.warn('No sender open_id found'); return; } // Parse text content let userText = ''; try { const content = JSON.parse(message.content || '{}'); userText = content.text || ''; } catch { this.logger.warn('Failed to parse message content'); return; } if (!userText.trim()) return; // Process via FeishuService await this.feishuService.processChatMessage(bot, openId, messageId, userText); } catch (error) { this.logger.error('Error handling message', error); } } /** * Update connection status */ private updateStatus(botId: string, status: Partial): void { const connection = this.connections.get(botId); if (connection) { connection.status = { ...connection.status, ...status }; } } /** * Attempt to reconnect a bot */ async attemptReconnect(bot: FeishuBot): Promise { const botId = bot.id; const attempts = this.reconnectAttempts.get(botId) || 0; if (attempts >= this.MAX_RECONNECT_ATTEMPTS) { this.logger.error(`Max reconnect attempts reached for bot ${botId}`); this.updateStatus(botId, { botId, state: ConnectionState.ERROR, error: 'Max reconnect attempts reached', }); return; } const delay = this.RECONNECT_DELAYS[attempts] || this.RECONNECT_DELAYS[this.RECONNECT_DELAYS.length - 1]; this.logger.log(`Reconnecting bot ${botId} in ${delay}ms (attempt ${attempts + 1})`); this.reconnectAttempts.set(botId, attempts + 1); setTimeout(async () => { try { await this.connect(bot); } catch (error) { this.logger.error(`Reconnect failed for bot ${botId}`, error); } }, delay); } } ``` - [ ] **Step 2: Run build to verify** ```bash cd D:\aura\AuraK\server yarn build ``` Expected: No errors --- ## Chunk 3: Service Integration ### Task 5: Update FeishuService **Files:** - Modify: `server/src/feishu/feishu.service.ts` - [ ] **Step 1: Read current service** File: `D:\aura\AuraK\server\src\feishu\feishu.service.ts` - [ ] **Step 2: Add WS management methods** Add at the end of the class (before the closing brace): ```typescript // ─── WebSocket Connection Management ───────────────────────────────────────── @Inject(forwardRef(() => FeishuWsManager)) private wsManager: FeishuWsManager; /** * Start WebSocket connection for a bot */ async startWsConnection(botId: string): Promise { const bot = await this.getBotById(botId); if (!bot) { throw new Error('Bot not found'); } if (!bot.enabled) { throw new Error('Bot is disabled'); } bot.useWebSocket = true; await this.botRepository.save(bot); await this.wsManager.connect(bot); } /** * Stop WebSocket connection for a bot */ async stopWsConnection(botId: string): Promise { const bot = await this.getBotById(botId); if (!bot) { throw new Error('Bot not found'); } bot.useWebSocket = false; await this.botRepository.save(bot); await this.wsManager.disconnect(botId); } /** * Get WebSocket connection status */ async getWsStatus(botId: string): Promise { return this.wsManager.getStatus(botId); } /** * Get all WebSocket connection statuses */ async getAllWsStatuses(): Promise { return this.wsManager.getAllStatuses(); } ``` - [ ] **Step 3: Add import for ConnectionStatus** Add at top of file: ```typescript import { ConnectionStatus } from './dto/ws-status.dto'; import { FeishuWsManager } from './feishu-ws.manager'; ``` - [ ] **Step 4: Run build to verify** ```bash cd D:\aura\AuraK\server yarn build ``` Expected: No errors --- ## Chunk 4: Controller Endpoints ### Task 6: Update FeishuController **Files:** - Modify: `server/src/feishu/feishu.controller.ts` - [ ] **Step 1: Read current controller** File: `D:\aura\AuraK\server\src\feishu\feishu.controller.ts` - [ ] **Step 2: Add WebSocket endpoints** Add after the existing bot management endpoints (after line ~79): ```typescript // ─── WebSocket Management Endpoints ──────────────────────────────────────── /** * POST /feishu/bots/:id/ws/connect - Start WebSocket connection */ @Post('bots/:id/ws/connect') @UseGuards(CombinedAuthGuard) async connectWs(@Request() req, @Param('id') botId: string) { // Verify bot belongs to user const bot = await this.feishuService.getBotById(botId); if (!bot || bot.userId !== req.user.id) { return { success: false, error: 'Bot not found' }; } try { await this.feishuService.startWsConnection(botId); return { success: true, botId, status: 'connecting', }; } catch (error) { return { success: false, botId, error: error.message || 'Failed to connect', }; } } /** * POST /feishu/bots/:id/ws/disconnect - Stop WebSocket connection */ @Post('bots/:id/ws/disconnect') @UseGuards(CombinedAuthGuard) async disconnectWs(@Request() req, @Param('id') botId: string) { // Verify bot belongs to user const bot = await this.feishuService.getBotById(botId); if (!bot || bot.userId !== req.user.id) { return { success: false, error: 'Bot not found' }; } try { await this.feishuService.stopWsConnection(botId); return { success: true, botId, status: 'disconnected', }; } catch (error) { return { success: false, botId, error: error.message || 'Failed to disconnect', }; } } /** * GET /feishu/bots/:id/ws/status - Get connection status */ @Get('bots/:id/ws/status') @UseGuards(CombinedAuthGuard) async getWsStatus(@Request() req, @Param('id') botId: string) { // Verify bot belongs to user const bot = await this.feishuService.getBotById(botId); if (!bot || bot.userId !== req.user.id) { return { success: false, error: 'Bot not found' }; } const status = await this.feishuService.getWsStatus(botId); if (!status) { return { botId, state: 'disconnected', }; } return { botId: status.botId, state: status.state, connectedAt: status.connectedAt?.toISOString(), lastHeartbeat: status.lastHeartbeat?.toISOString(), error: status.error, }; } /** * GET /feishu/ws/status - Get all connection statuses */ @Get('ws/status') @UseGuards(CombinedAuthGuard) async getAllWsStatus(@Request() req) { const statuses = await this.feishuService.getAllWsStatuses(); return { connections: statuses.map(s => ({ botId: s.botId, state: s.state, connectedAt: s.connectedAt?.toISOString(), lastHeartbeat: s.lastHeartbeat?.toISOString(), error: s.error, })), }; } ``` - [ ] **Step 3: Run build to verify** ```bash cd D:\aura\AuraK\server yarn build ``` Expected: No errors --- ## Chunk 5: Module Registration ### Task 7: Update FeishuModule **Files:** - Modify: `server/src/feishu/feishu.module.ts` - [ ] **Step 1: Read current module** File: `D:\aura\AuraK\server\src\feishu\feishu.module.ts` - [ ] **Step 2: Register FeishuWsManager** Add FeishuWsManager to providers and add FeishuService as constructor dependency: ```typescript import { FeishuWsManager } from './feishu-ws.manager'; @Module({ imports: [TypeOrmModule.forFeature([FeishuBot])], controllers: [FeishuController], providers: [FeishuService, FeishuWsManager], exports: [FeishuService], }) export class FeishuModule {} ``` - [ ] **Step 3: Update FeishuService constructor** In `feishu.service.ts`, add FeishuWsManager to constructor: ```typescript constructor( @InjectRepository(FeishuBot) private botRepository: Repository, @Inject(forwardRef(() => ChatService)) private chatService: ChatService, @Inject(forwardRef(() => ModelConfigService)) private modelConfigService: ModelConfigService, @Inject(forwardRef(() => UserService)) private userService: UserService, @Inject(forwardRef(() => FeishuWsManager)) private wsManager: FeishuWsManager, ) {} ``` - [ ] **Step 4: Run build to verify** ```bash cd D:\aura\AuraK\server yarn build ``` Expected: No errors --- ## Chunk 6: Testing & Verification ### Task 8: Test WebSocket Integration - [ ] **Step 1: Start the server** ```bash cd D:\aura\AuraK\server yarn start:dev ``` Expected: Server starts without errors - [ ] **Step 2: Verify endpoints exist** ```bash curl http://localhost:13000/api/feishu/ws/status ``` Expected: Returns JSON with connections array - [ ] **Step 3: Manual test with Feishu bot** 1. Create a Feishu bot in the UI 2. Configure in Feishu developer console: - Enable "Use long connection to receive events" - Add event: im.message.receive_v1 3. Call connect API: ```bash curl -X POST http://localhost:13000/api/feishu/bots/{botId}/ws/connect ``` 4. Send a message in Feishu to the bot 5. Verify response is received - [ ] **Step 4: Test disconnect** ```bash curl -X POST http://localhost:13000/api/feishu/bots/{botId}/ws/disconnect ``` - [ ] **Step 5: Verify webhook still works** Test existing webhook endpoint still works as before. --- ## Chunk 7: Documentation Update ### Task 9: Update User Documentation - [ ] **Step 1: Add WebSocket configuration guide** Create or update documentation in `docs/` explaining: - How to configure WebSocket mode - Differences from webhook mode - Troubleshooting steps --- ## Summary | Task | Description | Estimated Time | |------|-------------|----------------| | 1 | Install Feishu SDK | 2 min | | 2 | Update FeishuBot entity | 5 min | | 3 | Create WS Status DTOs | 5 min | | 4 | Create FeishuWsManager | 15 min | | 5 | Update FeishuService | 10 min | | 6 | Update FeishuController | 10 min | | 7 | Update FeishuModule | 5 min | | 8 | Testing & Verification | 20 min | | 9 | Documentation | 10 min | **Total estimated time:** ~80 minutes