|
@@ -0,0 +1,727 @@
|
|
|
|
|
+# 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<string, { client: EventDispatcher; status: ConnectionStatus }> = new Map();
|
|
|
|
|
+ private reconnectAttempts: Map<string, number> = 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<void> {
|
|
|
|
|
+ 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<void> {
|
|
|
|
|
+ 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<void> {
|
|
|
|
|
+ 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<ConnectionStatus>): void {
|
|
|
|
|
+ const connection = this.connections.get(botId);
|
|
|
|
|
+ if (connection) {
|
|
|
|
|
+ connection.status = { ...connection.status, ...status };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Attempt to reconnect a bot
|
|
|
|
|
+ */
|
|
|
|
|
+ async attemptReconnect(bot: FeishuBot): Promise<void> {
|
|
|
|
|
+ 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<void> {
|
|
|
|
|
+ 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<void> {
|
|
|
|
|
+ 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<ConnectionStatus | null> {
|
|
|
|
|
+ return this.wsManager.getStatus(botId);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Get all WebSocket connection statuses
|
|
|
|
|
+ */
|
|
|
|
|
+async getAllWsStatuses(): Promise<ConnectionStatus[]> {
|
|
|
|
|
+ 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<FeishuBot>,
|
|
|
|
|
+ @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
|