# 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
