# Feishu Bot 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:** Enable Feishu (飞书) bot integration as a standalone **Plugin** that users can manage via the system's "Plugins" menu. This allows users to bind bots, interact with RAG, and access assessment features in a modular, plug-and-play fashion. **Architecture:** Implement the integration as a pluggable `FeishuModule`. It acts as a bridge between Feishu Open Platform and AuraK's core services. The plugin is managed through the new `/plugins` workspace, isolating its UI and backend logic from the core system. **Tech Stack:** - NestJS (existing) - Feishu Open Platform APIs (im/v1/messages, auth/v3/tenant_access_token) - Event subscription (Webhooks) - Existing: ChatService, AssessmentService, JWT Auth --- ## Architecture Overview ``` ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Feishu User│─────▶│ Feishu Server│─────▶│ AuraK API │ │ (User A) │◀─────│ Webhook │◀─────│ /feishu/* │ └─────────────┘ └──────────────┘ └─────────────┘ │ ▼ ┌─────────────┐ │ FeishuModule│ │ - Bot Entity│ │ - Message │ │ - Events │ └─────────────┘ │ ┌───────────────────────┼───────────────────────┐ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ ChatService │ │AssessmentSvc│ │ PluginsSvc │ │ (RAG Q&A) │ │ (评测对话) │ │ (插件状态管理)│ └─────────────┘ └─────────────┘ └─────────────┘ ``` **Plugin Isolation Strategy:** - **Backend**: `FeishuModule` is a standalone NestJS module. It handles its own database entities and webhook logic. - **Frontend**: Integrated as a sub-view within `/plugins`. When the Feishu plugin is "enabled", its configuration UI is rendered. - **Decoupling**: Communicates with core services via standard service calls or an event-subscriber pattern. --- ## File Structure ### New Files to Create ``` server/src/ ├── feishu/ │ ├── feishu.module.ts # Module definition │ ├── feishu.controller.ts # Webhook endpoints │ ├── feishu.service.ts # Business logic │ ├── feishu.gateway.ts # Event handling (optional) │ ├── entities/ │ │ └── feishu-bot.entity.ts # Bot configuration entity │ ├── dto/ │ │ ├── create-bot.dto.ts # Create bot DTO │ │ ├── bind-bot.dto.ts # Bind bot DTO │ │ └── feishu-webhook.dto.ts # Webhook event DTO │ └── interfaces/ │ └── feishu.interface.ts # TypeScript interfaces ``` ### Existing Files to Modify ``` server/src/ ├── app.module.ts # Import FeishuModule ├── user/ │ └── user.entity.ts # Add one-to-many relation to FeishuBot ├── user/user.service.ts # Add methods to get/set Feishu binding ``` --- ## Chunk 1: Entity and DTO Definitions ### Task 1.1: Create FeishuBot Entity **Files:** - Create: `server/src/feishu/entities/feishu-bot.entity.ts` - [ ] **Step 1: Create the FeishuBot entity file** ```typescript import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, } from 'typeorm'; import { User } from '../../user/user.entity'; @Entity('feishu_bots') export class FeishuBot { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'user_id' }) userId: string; // Plugin manages its own relationship to the core User @ManyToOne(() => User) @JoinColumn({ name: 'user_id' }) user: User; @Column({ name: 'app_id', length: 64 }) appId: string; // ... (rest of the fields as defined previously) } ``` - [ ] **Step 2: Decoupled Relation** Instead of modifying the core `User` entity directly, the `FeishuBot` entity maintains its own reference to `User`. This keeps the core system clean and allows the plugin to be purely optional. - [ ] **Step 3: Create DTOs** Create: `server/src/feishu/dto/create-bot.dto.ts` ```typescript import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator'; export class CreateFeishuBotDto { @IsString() @IsNotEmpty() appId: string; @IsString() @IsNotEmpty() appSecret: string; @IsString() @IsOptional() verificationToken?: string; @IsString() @IsOptional() encryptKey?: string; @IsString() @IsOptional() botName?: string; @IsBoolean() @IsOptional() enabled?: boolean; } ``` Create: `server/src/feishu/dto/bind-bot.dto.ts` ```typescript import { IsString, IsNotEmpty, IsUUID } from 'class-validator'; export class BindFeishuBotDto { @IsUUID() @IsNotEmpty() botId: string; @IsString() @IsNotEmpty() verificationCode?: string; // Optional: 用于验证绑定关系 } ``` --- ## Chunk 2: Core Service Implementation ### Task 2.1: Create Feishu Service **Files:** - Create: `server/src/feishu/feishu.service.ts` - [ ] **Step 1: Implement FeishuService with token management and message sending** ```typescript import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ConfigService } from '@nestjs/config'; import { HttpService } from '@nestjs/axios'; import { FeishuBot } from './entities/feishu-bot.entity'; import { CreateFeishuBotDto } from './dto/create-bot.dto'; @Injectable() export class FeishuService { private readonly logger = new Logger(FeishuService.name); private readonly feishuApiBase = 'https://open.feishu.cn/open-apis'; constructor( @InjectRepository(FeishuBot) private botRepository: Repository, private httpService: HttpService, private configService: ConfigService, ) {} /** * Create or update a Feishu bot for a user */ async createBot(userId: string, dto: CreateFeishuBotDto): Promise { // Check if bot already exists for this user with same appId const existing = await this.botRepository.findOne({ where: { userId, appId: dto.appId }, }); if (existing) { // Update existing bot Object.assign(existing, dto); return this.botRepository.save(existing); } // Create new bot const bot = this.botRepository.create({ userId, ...dto, }); return this.botRepository.save(bot); } /** * Get all bots for a user */ async getUserBots(userId: string): Promise { return this.botRepository.find({ where: { userId } }); } /** * Get bot by ID */ async getBotById(botId: string): Promise { return this.botRepository.findOne({ where: { id: botId } }); } /** * Get bot by appId */ async getBotByAppId(appId: string): Promise { return this.botRepository.findOne({ where: { appId } }); } /** * Get or refresh tenant_access_token */ async getValidToken(bot: FeishuBot): Promise { // Check if token is still valid (expire in 2 hours, refresh at 1.5 hours) if ( bot.tokenExpiresAt && bot.tenantAccessToken && new Date(bot.tokenExpiresAt) > new Date(Date.now() + 30 * 60 * 1000) ) { return bot.tenantAccessToken; } // Refresh token const response = await this.httpService .post(`${this.feishuApiBase}/auth/v3/tenant_access_token/internal`, { app_id: bot.appId, app_secret: bot.appSecret, }) .toPromise(); if (response.data.code !== 0) { throw new Error(`Failed to get token: ${response.data.msg}`); } const { tenant_access_token, expire } = response.data; // Update bot with new token bot.tenantAccessToken = tenant_access_token; bot.tokenExpiresAt = new Date(Date.now() + expire * 1000); await this.botRepository.save(bot); return tenant_access_token; } /** * Send message to Feishu user */ async sendMessage( bot: FeishuBot, receiveIdType: 'open_id' | 'user_id' | 'union_id' | 'chat_id', receiveId: string, messageType: 'text' | 'interactive' | 'post', content: any, ): Promise { const token = await this.getValidToken(bot); const response = await this.httpService .post( `${this.feishuApiBase}/im/v1/messages?receive_id_type=${receiveIdType}`, { receive_id: receiveId, msg_type: messageType, content: JSON.stringify(content), }, { headers: { Authorization: `Bearer ${token}`, }, }, ) .toPromise(); if (response.data.code !== 0) { throw new Error(`Failed to send message: ${response.data.msg}`); } return response.data.data.message_id; } /** * Send text message (convenience method) */ async sendTextMessage( bot: FeishuBot, receiveIdType: 'open_id' | 'user_id' | 'chat_id', receiveId: string, text: string, ): Promise { return this.sendMessage(bot, receiveIdType, receiveId, 'text', { text }); } /** * Reply to a message */ async replyMessage( bot: FeishuBot, messageId: string, messageType: 'text' | 'interactive' | 'post', content: any, ): Promise { const token = await this.getValidToken(bot); const response = await this.httpService .post( `${this.feishuApiBase}/im/v1/messages/${messageId}/reply`, { msg_type: messageType, content: JSON.stringify(content), }, { headers: { Authorization: `Bearer ${token}`, }, }, ) .toPromise(); if (response.data.code !== 0) { throw new Error(`Failed to reply message: ${response.data.msg}`); } return response.data.data.message_id; } /** * Upload image for sending */ async uploadImage( bot: FeishuBot, imageType: 'message' | 'avatar', image: Buffer, imageName: string, ): Promise { const token = await this.getValidToken(bot); const FormData = require('form-data'); const form = new FormData(); form.append('image_type', imageType); form.append('image', image, { filename: imageName }); const response = await this.httpService .post(`${this.feishuApiBase}/im/v1/images`, form, { headers: { Authorization: `Bearer ${token}`, ...form.getHeaders(), }, }) .toPromise(); if (response.data.code !== 0) { throw new Error(`Failed to upload image: ${response.data.msg}`); } return response.data.data.image_key; } /** * Delete bot */ async deleteBot(botId: string): Promise { await this.botRepository.delete(botId); } /** * Enable/disable bot */ async setBotEnabled(botId: string, enabled: boolean): Promise { const bot = await this.botRepository.findOne({ where: { id: botId } }); if (!bot) { throw new Error('Bot not found'); } bot.enabled = enabled; return this.botRepository.save(bot); } } ``` --- ## Chunk 3: Controller and Webhook Endpoints ### Task 3.1: Create Feishu Controller **Files:** - Create: `server/src/feishu/feishu.controller.ts` - [ ] **Step 1: Implement webhook endpoints** ```typescript import { Controller, Post, Get, Delete, Body, Param, Query, Headers, UseGuards, Request, RawBodyRequest, Req, } from '@nestjs/common'; import { Request as ExpressRequest } from 'express'; import { FeishuService } from './feishu.service'; import { CreateFeishuBotDto } from './dto/create-bot.dto'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { CreateSignatureDto, VerifyWebhookDto } from './dto/webhook.dto'; @Controller('feishu') export class FeishuController { constructor(private readonly feishuService: FeishuService) {} /** * GET /feishu/bots - List user's bots * Requires JWT auth */ @Get('bots') @UseGuards(JwtAuthGuard) async listBots(@Request() req) { const bots = await this.feishuService.getUserBots(req.user.id); // Mask sensitive data return bots.map((bot) => ({ id: bot.id, appId: bot.appId, botName: bot.botName, enabled: bot.enabled, isDefault: bot.isDefault, createdAt: bot.createdAt, })); } /** * POST /feishu/bots - Create a new bot * Requires JWT auth */ @Post('bots') @UseGuards(JwtAuthGuard) async createBot(@Request() req, @Body() dto: CreateFeishuBotDto) { const bot = await this.feishuService.createBot(req.user.id, dto); return { id: bot.id, appId: bot.appId, botName: bot.botName, enabled: bot.enabled, }; } /** * DELETE /feishu/bots/:id - Delete a bot * Requires JWT auth */ @Delete('bots/:id') @UseGuards(JwtAuthGuard) async deleteBot(@Request() req, @Param('id') botId: string) { await this.feishuService.deleteBot(botId); return { success: true }; } /** * POST /feishu/webhook - Feishu webhook endpoint * Public endpoint - no auth required (uses verification token) */ @Post('webhook') async handleWebhook( @Body() body: any, @Headers('x-xsign') xSign?: string, @Headers('x-timESTAMP') xTimestamp?: string, ) { this.logger.log(`Received webhook: ${JSON.stringify(body)}`); const { type, schema, event } = body; // Handle URL verification (飞书首次配置时验证) if (type === 'url_verification') { return { challenge: body.challenge, }; } // Handle event callback if (type === 'event_callback') { const { event_type, token } = body; // Verify token if (token !== body?.verify_token) { this.logger.warn('Webhook token verification failed'); return { success: false }; } // Handle different event types switch (event_type) { case 'im.message.p2p_msg_received': // Handle private message await this.handleP2PMsg(event); break; case 'im.message.group_at_msg_received': // Handle group @message await this.handleGroupMsg(event); break; default: this.logger.log(`Unhandled event type: ${event_type}`); } } return { success: true }; } /** * Handle private message */ private async handleP2PMsg(event: any) { const { message } = event; const { message_id, chat_id, sender, message_type, body } = message; // Get message content const content = JSON.parse(message.content || '{}'); const text = content.text || ''; // Find bot by app_id (from chat or event) const openId = sender?.id?.open_id; if (!openId) { this.logger.warn('No sender open_id found'); return; } // Find user's bot const bot = await this.feishuService.getBotByAppId(sender?.sender_id?.app_id); if (!bot || !bot.enabled) { this.logger.warn('Bot not found or disabled'); return; } // Process message through RAG/Chat service await this.processMessage(bot, openId, message_id, text); } /** * Handle group message (@bot) */ private async handleGroupMsg(event: any) { const { message } = event; const { message_id, chat_id, sender, message_type, content } = message; // Check if bot was mentioned const msgContent = JSON.parse(content || '{}'); // Group messages often require specific handling // Similar to P2P but with chat_id context await this.handleP2PMsg(event); } /** * Process message through ChatService */ private async processMessage( bot: any, openId: string, messageId: string, text: string, ) { // TODO: Integrate with ChatService // This will be implemented in Chunk 5 // For now, echo back (placeholder) try { await this.feishuService.sendTextMessage( bot, 'open_id', openId, `Received: ${text}`, ); } catch (error) { this.logger.error('Failed to send response', error); } } } ``` - [ ] **Step 2: Create webhook DTOs** Create: `server/src/feishu/dto/webhook.dto.ts` ```typescript import { IsString, IsOptional } from 'class-validator'; export class CreateSignatureDto { @IsString() @IsOptional() timestamp?: string; @IsString() @IsOptional() nonce?: string; } export class VerifyWebhookDto { @IsString() token: string; @IsString() @IsOptional() challenge?: string; } ``` --- ## Chunk 4: Plugin Registration ### Task 4.1: Register Feishu Plugin **Note:** This module acts as an optional extension to the AuraK ecosystem. **Files:** - Create: `server/src/feishu/feishu.module.ts` - Modify: `server/src/app.module.ts` - [ ] **Step 1: Create FeishuModule with isolated exports** ```typescript import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { HttpModule } from '@nestjs/axios'; import { FeishuController } from './feishu.controller'; import { FeishuService } from './feishu.service'; import { FeishuBot } from './entities/feishu-bot.entity'; import { ChatModule } from '../chat/chat.module'; import { UserModule } from '../user/user.module'; @Module({ imports: [ TypeOrmModule.forFeature([FeishuBot]), HttpModule, forwardRef(() => ChatModule), forwardRef(() => UserModule), ], controllers: [FeishuController], providers: [FeishuService], exports: [FeishuService, TypeOrmModule], }) export class FeishuModule {} ``` - [ ] **Step 2: Add FeishuModule to AppModule** Modify: `server/src/app.module.ts` Add import: ```typescript import { FeishuModule } from './feishu/feishu.module'; ``` Add to imports array: ```typescript FeishuModule, ``` --- ## Chunk 5: Integration with ChatService ### Task 5.1: Connect Feishu messages to ChatService **Files:** - Modify: `server/src/feishu/feishu.controller.ts` - Modify: `server/src/feishu/feishu.service.ts` - [ ] **Step 1: Extend FeishuService to handle chat integration** Add to FeishuService: ```typescript // Import these import { ChatService } from '../chat/chat.service'; import { ModelConfigService } from '../model-config/model-config.service'; import { TenantService } from '../tenant/tenant.service'; import { UserService } from '../user/user.service'; import { ModelType } from '../types'; // Add to constructor constructor( // ... existing private chatService: ChatService, private modelConfigService: ModelConfigService, private tenantService: TenantService, private userService: UserService, ) {} /** * Process chat message through RAG */ async processChatMessage( bot: FeishuBot, openId: string, messageId: string, userMessage: string, ): Promise { // Get user by Feishu open_id mapping (future: map table) // For now, use userId from bot const userId = bot.userId; // Get user's tenant const user = await this.userService.findById(userId); const tenantId = user?.tenantId || 'default'; // Get user's LLM config const llmModel = await this.modelConfigService.findDefaultByType( tenantId, ModelType.LLM, ); if (!llmModel) { await this.sendTextMessage(bot, 'open_id', openId, '请先配置 LLM 模型'); return; } // Send "thinking" message await this.sendTextMessage(bot, 'open_id', openId, '正在思考...'); // Stream chat response const stream = this.chatService.streamChat( userMessage, [], // No history for now (future: persist per openId) userId, llmModel as any, user?.userSetting?.language || 'zh', undefined, // selectedEmbeddingId undefined, // selectedGroups undefined, // selectedFiles undefined, // historyId false, // enableRerank undefined, // selectedRerankId undefined, // temperature undefined, // maxTokens 10, // topK 0.7, // similarityThreshold undefined, // rerankSimilarityThreshold undefined, // enableQueryExpansion undefined, // enableHyDE tenantId, ); let fullResponse = ''; for await (const chunk of stream) { if (chunk.type === 'content') { fullResponse += chunk.data; // Could send incrementally, but Feishu prefers complete messages } } // Send final response // Truncate if too long (Feishu has limits) const maxLength = 5000; const finalMessage = fullResponse.length > maxLength ? fullResponse.substring(0, maxLength) + '...(内容过长)' : fullResponse; await this.sendTextMessage(bot, 'open_id', openId, finalMessage); } ``` - [ ] **Step 2: Update controller to use chat integration** Modify the `processMessage` method in FeishuController to call `feishuService.processChatMessage()`: ```typescript private async processMessage( bot: any, openId: string, messageId: string, text: string, ) { try { await this.feishuService.processChatMessage(bot, openId, messageId, text); } catch (error) { this.logger.error('Failed to process message', error); try { await this.feishuService.sendTextMessage( bot, 'open_id', openId, '抱歉,处理消息时发生错误,请稍后重试。', ); } catch (sendError) { this.logger.error('Failed to send error message', sendError); } } } ``` --- ## Chunk 6: Frontend Integration (Optional) ### Task 6.1: Add Feishu sub-view to Plugins **Files:** - Create: `web/src/pages/Plugins/FeishuPlugin.tsx` - Modify: `web/src/components/views/PluginsView.tsx` - Modify: `web/src/services/api.ts` - [ ] **Step 1: Create the Plugin Configuration UI** This view should match the design of other plugin cards in the /plugins page but provide a detailed setup guide and form for: - App ID / App Secret - Webhook URL (Read-only generated URL) - Verification Token & Encrypt Key - [ ] **Step 2: Register the Plugin in PluginsView** Modify the main plugins listing to include "Feishu Bot" as an available (or installed) plugin. Modify: `web/src/services/api.ts` ```typescript // Add Feishu API calls export const feishuApi = { listBots: () => api.get('/feishu/bots'), createBot: (data: CreateBotDto) => api.post('/feishu/bots', data), deleteBot: (botId: string) => api.delete(`/feishu/bots/${botId}`), }; ``` - [ ] **Step 3: Implementation of PluginsView.tsx** If `web/src/components/views/PluginsView.tsx` does not exist, create a generic plugin management layout that can host the Feishu configuration. **Layout Requirements:** - Grid of available plugins. - Status toggle (Enabled/Disabled). - Detail/Configuration view for active plugins. ## Testing Strategy ### Unit Tests - `feishu.service.spec.ts` - Test token refresh, message sending - `feishu.controller.spec.ts` - Test webhook endpoints ### Integration Tests - Test full flow: Feishu message → Webhook → ChatService → Response ### Manual Testing 1. Create Feishu app in 开放平台 2. Configure webhook URL (use ngrok for local) 3. Subscribe to message events 4. Send message to bot 5. Verify RAG response --- ## Security Considerations 1. **Token Storage**: Encrypt `app_secret` and `encrypt_key` before storing in DB. 2. **Webhook Verification**: - Verify `verify_token` for simplicity. - **Recommended**: Validate `X-Lark-Signature` using `encrypt_key` to ensure authenticity. 3. **Rate Limiting**: Implement a message queue (e.g., BullMQ) for outbound messages to respect Feishu's global and per-bot rate limits. 4. **User Privacy**: Implement an opt-in flow for group chats to ensure the bot only processes messages when explicitly allowed or mentioned. --- ## Advanced Optimizations (Recommended) ### 1. Webhook Performance & Reliability - **Immediate Response**: Feishu requires a response within 3 seconds. The RAG process can take 10s+. - **Optimization**: The `handleWebhook` should only validate the request and push the event to an internal queue, returning `200 OK` immediately. A background worker then processes the RAG logic and sends the response. - **Deduplication**: Use the `event_id` in the webhook payload to ignore duplicate retries from Feishu. ### 2. UX: Managed "Thinking" State - **Simulated Streaming**: Since Feishu doesn't support SSE, send an initial "Thinking..." message and use the `PATCH /im/v1/messages/:message_id` API to update the message content every few seconds as chunks arrive. - **Interactive Cards**: Use [Message Cards](https://open.feishu.cn/document/common-capabilities/message-card/message-card-overview) instead of plain text for: - Showing search citations with clickable links. - Providing "Regenerate" or "Clear Context" buttons. - Displaying assessment results with formatting (tables/charts). ### 3. Context & History Management - **OpenID Mapping**: Maintain a mapping between Feishu `open_id` and AuraK `userId` to persist chat history across different devices/platforms. - **Thread Support**: Use Feishu's `root_id` and `parent_id` to allow users to ask follow-up questions within a thread, keeping the UI clean. ### 4. Multi-modal Support - **File Ingestion**: Support `im.message.file_received` events. If a user sends a PDF/Docx to the bot, automatically import it into a "Feishu Uploads" group for immediate RAG context. - **Image Analysis**: Use the `VisionService` to handle images sent via Feishu. ## Future Enhancements 1. **Assessment Integration**: Bind assessment sessions to Feishu conversations using interactive card forms. 2. **Rich Responses**: Use Feishu interactive cards for better visual presentation. 3. **Multi-bot Support**: Users can have multiple bots for different specialized tasks. 4. **Group Chats**: Support bot in group chats with specific @mention logic and moderation. 5. **Voice Messages**: Handle voice message transcription via Feishu's audio-to-text API for accessibility. --- ## Dependencies - `@nestjs/axios` - For HTTP requests to Feishu API - `form-data` - For file/image uploads - Optional: `crypto` (built-in) - For signature verification --- ## Reference Links - [飞书开放平台 - 机器人文档](https://open.feishu.cn/document/faq/bot) - [飞书事件订阅](https://open.feishu.cn/document/server-docs/event-subscription-guide/overview) - [消息发送 API](https://open.feishu.cn/document/server-docs/im-v1/message-content-description) - [获取 tenant_access_token](https://open.feishu.cn/document/server-docs/authentication-management/access-token) --- > **Plan created:** 2026-03-16 > **Based on:** Feishu integration analysis