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:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Feishu User│─────▶│ Feishu Server│─────▶│ AuraK API │
│ (User A) │◀─────│ Webhook │◀─────│ /feishu/* │
└─────────────┘ └──────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ FeishuModule│
│ - Bot Entity│
│ - Message │
│ - Events │
└─────────────┘
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ChatService │ │AssessmentSvc│ │ PluginsSvc │
│ (RAG Q&A) │ │ (评测对话) │ │ (插件状态管理)│
└─────────────┘ └─────────────┘ └─────────────┘
Plugin Isolation Strategy:
FeishuModule is a standalone NestJS module. It handles its own database entities and webhook logic./plugins. When the Feishu plugin is "enabled", its configuration UI is rendered.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; // 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
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;
}
Note: This module acts as an optional extension to the AuraK ecosystem.
Files:
server/src/feishu/feishu.module.tsModify: server/src/app.module.ts
[ ] Step 1: Create FeishuModule with isolated exports
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/Plugins/FeishuPlugin.tsxweb/src/components/views/PluginsView.tsxModify: 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
// 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}`),
};
web/src/components/views/PluginsView.tsx does not exist, create a generic plugin management layout that can host the Feishu configuration.Layout Requirements:
feishu.service.spec.ts - Test token refresh, message sendingfeishu.controller.spec.ts - Test webhook endpointsapp_secret and encrypt_key before storing in DB.Webhook Verification:
verify_token for simplicity.X-Lark-Signature using encrypt_key to ensure authenticity.Rate Limiting: Implement a message queue (e.g., BullMQ) for outbound messages to respect Feishu's global and per-bot rate limits.
User Privacy: Implement an opt-in flow for group chats to ensure the bot only processes messages when explicitly allowed or mentioned.
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.event_id in the webhook payload to ignore duplicate retries from Feishu.PATCH /im/v1/messages/:message_id API to update the message content every few seconds as chunks arrive.open_id and AuraK userId to persist chat history across different devices/platforms.root_id and parent_id to allow users to ask follow-up questions within a thread, keeping the UI clean.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.VisionService to handle images sent via 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