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:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Feishu User│─────▶│ Feishu Server│─────▶│ AuraK API │
│ (User A) │◀─────│ Webhook │◀─────│ /feishu/* │
└─────────────┘ └──────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ FeishuModule│
│ - Bot Entity│
│ - Message │
│ - Events │
└─────────────┘
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ChatService │ │AssessmentSvc│ │ UserService│
│ (RAG Q&A) │ │ (评测对话) │ │ (绑定管理) │
└─────────────┘ └─────────────┘ └─────────────┘
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
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
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[];
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: 用于验证绑定关系
}
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);
}
}
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;
}
Files:
server/src/feishu/feishu.module.tsModify: 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,
Files:
server/src/feishu/feishu.controller.tsModify: 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);
}
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);
}
}
}
Files:
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}`),
};
Create a simple settings page where users can:
feishu.service.spec.ts - Test token refresh, message sendingfeishu.controller.spec.ts - Test webhook endpointsapp_secret before storing in DBverification_token from Feishu@nestjs/axios - For HTTP requests to Feishu APIform-data - For file/image uploadscrypto (built-in) - For signature verificationPlan created: 2026-03-16 Based on: Feishu integration analysis