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
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
[ ] Step 1: Install @larksuiteoapi/node-sdk
cd D:\aura\AuraK\server
yarn add @larksuiteoapi/node-sdk
[ ] Step 2: Verify installation
yarn list @larksuiteoapi/node-sdk
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
Add after existing columns:
@Column({ default: false })
useWebSocket: boolean;
@Column({ nullable: true })
wsConnectionState: string;
[ ] Step 3: Run build to verify
cd D:\aura\AuraK\server
yarn build
Files:
Create: server/src/feishu/dto/ws-status.dto.ts
[ ] Step 1: Create DTO file
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[];
}
Files:
Create: server/src/feishu/feishu-ws.manager.ts
[ ] Step 1: Create the WebSocket manager
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
cd D:\aura\AuraK\server
yarn build
Expected: No errors
Files:
Modify: server/src/feishu/feishu.service.ts
[ ] Step 1: Read current service
File: D:\aura\AuraK\server\src\feishu\feishu.service.ts
Add at the end of the class (before the closing brace):
// ─── 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();
}
Add at top of file:
import { ConnectionStatus } from './dto/ws-status.dto';
import { FeishuWsManager } from './feishu-ws.manager';
[ ] Step 4: Run build to verify
cd D:\aura\AuraK\server
yarn build
Expected: No errors
Files:
Modify: server/src/feishu/feishu.controller.ts
[ ] Step 1: Read current controller
File: D:\aura\AuraK\server\src\feishu\feishu.controller.ts
Add after the existing bot management endpoints (after line ~79):
// ─── 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
cd D:\aura\AuraK\server
yarn build
Expected: No errors
Files:
Modify: server/src/feishu/feishu.module.ts
[ ] Step 1: Read current module
File: D:\aura\AuraK\server\src\feishu\feishu.module.ts
Add FeishuWsManager to providers and add FeishuService as constructor dependency:
import { FeishuWsManager } from './feishu-ws.manager';
@Module({
imports: [TypeOrmModule.forFeature([FeishuBot])],
controllers: [FeishuController],
providers: [FeishuService, FeishuWsManager],
exports: [FeishuService],
})
export class FeishuModule {}
In feishu.service.ts, add FeishuWsManager to constructor:
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
cd D:\aura\AuraK\server
yarn build
Expected: No errors
[ ] Step 1: Start the server
cd D:\aura\AuraK\server
yarn start:dev
Expected: Server starts without errors
[ ] Step 2: Verify endpoints exist
curl http://localhost:13000/api/feishu/ws/status
Expected: Returns JSON with connections array
Call connect API:
curl -X POST http://localhost:13000/api/feishu/bots/{botId}/ws/connect
Send a message in Feishu to the bot
Verify response is received
[ ] Step 4: Test disconnect
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.
Create or update documentation in docs/ explaining:
| 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