2026-03-16-feishu-bot-integration.md 25 KB

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 users to bind their Feishu (飞书) bots to their AuraK accounts and interact with the RAG system and talent assessment features through Feishu messages.

Architecture: Create a new feishu module that acts as a bridge between Feishu Open Platform and existing ChatService/AssessmentService. Users create Feishu custom apps, bind them in AuraK, and receive/push messages through Feishu's webhook-based event subscription.

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│        │  UserService│
                      │  (RAG Q&A)  │        │ (评测对话)   │        │ (绑定管理)  │
                      └─────────────┘        └─────────────┘        └─────────────┘

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

    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;
    
    @ManyToOne(() => User)
    @JoinColumn({ name: 'user_id' })
    user: User;
    
    @Column({ name: 'app_id', length: 64 })
    appId: string;  // 飞书应用 App ID (cli_xxx)
    
    @Column({ name: 'app_secret', length: 128 })
    appSecret: string;  // 飞书应用 App Secret (加密存储)
    
    @Column({ name: 'tenant_access_token', nullable: true })
    tenantAccessToken: string;
    
    @Column({ name: 'token_expires_at', nullable: true })
    tokenExpiresAt: Date;
    
    @Column({ name: 'verification_token', nullable: true })
    verificationToken: string;  // 用于验证回调请求
    
    @Column({ name: 'encrypt_key', nullable: true })
    encryptKey: string;  // 飞书加密 key (可选)
    
    @Column({ name: 'bot_name', nullable: true })
    botName: string;  // 机器人名称显示
    
    @Column({ default: true })
    enabled: boolean;
    
    @Column({ name: 'is_default', default: false })
    isDefault: boolean;  // 用户是否有多个机器人
    
    @CreateDateColumn({ name: 'created_at' })
    createdAt: Date;
    
    @UpdateDateColumn({ name: 'updated_at' })
    updatedAt: Date;
    }
    
  • [ ] Step 2: Add FeishuBot relation to User entity

Modify: server/src/user/user.entity.ts:73-74

Add after userSetting relation:

  @OneToMany(() => FeishuBot, (bot) => bot.user)
  feishuBots: FeishuBot[];
  • Step 3: Create DTOs

Create: server/src/feishu/dto/create-bot.dto.ts

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

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

    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<FeishuBot>,
    private httpService: HttpService,
    private configService: ConfigService,
    ) {}
    
    /**
    * Create or update a Feishu bot for a user
    */
    async createBot(userId: string, dto: CreateFeishuBotDto): Promise<FeishuBot> {
    // 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<FeishuBot[]> {
    return this.botRepository.find({ where: { userId } });
    }
    
    /**
    * Get bot by ID
    */
    async getBotById(botId: string): Promise<FeishuBot | null> {
    return this.botRepository.findOne({ where: { id: botId } });
    }
    
    /**
    * Get bot by appId
    */
    async getBotByAppId(appId: string): Promise<FeishuBot | null> {
    return this.botRepository.findOne({ where: { appId } });
    }
    
    /**
    * Get or refresh tenant_access_token
    */
    async getValidToken(bot: FeishuBot): Promise<string> {
    // 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<string> {
    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<string> {
    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<string> {
    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<string> {
    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<void> {
    await this.botRepository.delete(botId);
    }
    
    /**
    * Enable/disable bot
    */
    async setBotEnabled(botId: string, enabled: boolean): Promise<FeishuBot> {
    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

    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

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: Module Registration

Task 4.1: Register FeishuModule

Files:

  • Create: server/src/feishu/feishu.module.ts
  • Modify: server/src/app.module.ts

  • [ ] Step 1: Create FeishuModule

    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:

import { FeishuModule } from './feishu/feishu.module';

Add to imports array:

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:

  // 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<void> {
    // 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():

  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 settings UI

Files:

  • Create: web/src/pages/Settings/FeishuSettings.tsx (or similar)
  • Modify: web/src/services/api.ts

  • [ ] Step 1: Add API endpoints to frontend

Modify: web/src/services/api.ts

// 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 2: Create settings component

Create a simple settings page where users can:

  1. View their bound Feishu bots
  2. Add new bot (paste App ID, App Secret)
  3. Delete bots
  4. See setup instructions

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 before storing in DB
  2. Webhook Verification: Always verify verification_token from Feishu
  3. Rate Limiting: Implement queue for sending messages (Feishu limits)
  4. User Mapping: Consider secure mapping between Feishu open_id and AuraK user

Future Enhancements

  1. Assessment Integration: Bind assessment sessions to Feishu conversations
  2. Rich Responses: Use Feishu interactive cards for better UI
  3. Multi-bot Support: Users can have multiple bots for different purposes
  4. Group Chats: Support bot in group chats
  5. Voice Messages: Handle voice message transcription

Dependencies

  • @nestjs/axios - For HTTP requests to Feishu API
  • form-data - For file/image uploads
  • Optional: crypto (built-in) - For signature verification

Reference Links


Plan created: 2026-03-16 Based on: Feishu integration analysis