feat: implement QuestionBank CRUD with pagination and template query

- Add pagination support to findAll (page, limit query params)
- Add findByTemplateId method to service
- Add GET /by-template/:templateId endpoint to controller
- Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
Developer
2026-04-23 17:19:11 +08:00
commit 0a9588abb7
492 changed files with 112453 additions and 0 deletions
@@ -0,0 +1,955 @@
# 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<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**
```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<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()`:
```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
@@ -0,0 +1,727 @@
# Feishu WebSocket 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:** 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
---
## File Structure
```
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
```
---
## Chunk 1: Dependencies & Entity Changes
### Task 1: Install Feishu SDK
- [ ] **Step 1: Install @larksuiteoapi/node-sdk**
```bash
cd D:\aura\AuraK\server
yarn add @larksuiteoapi/node-sdk
```
- [ ] **Step 2: Verify installation**
```bash
yarn list @larksuiteoapi/node-sdk
```
---
### Task 2: Update FeishuBot Entity
**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`
- [ ] **Step 2: Add WebSocket fields**
Add after existing columns:
```typescript
@Column({ default: false })
useWebSocket: boolean;
@Column({ nullable: true })
wsConnectionState: string;
```
- [ ] **Step 3: Run build to verify**
```bash
cd D:\aura\AuraK\server
yarn build
```
---
## Chunk 2: WebSocket Manager
### Task 3: Create WebSocket Status DTOs
**Files:**
- Create: `server/src/feishu/dto/ws-status.dto.ts`
- [ ] **Step 1: Create DTO file**
```typescript
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[];
}
```
---
### Task 4: Create FeishuWsManager
**Files:**
- Create: `server/src/feishu/feishu-ws.manager.ts`
- [ ] **Step 1: Create the WebSocket manager**
```typescript
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**
```bash
cd D:\aura\AuraK\server
yarn build
```
Expected: No errors
---
## Chunk 3: Service Integration
### Task 5: Update FeishuService
**Files:**
- Modify: `server/src/feishu/feishu.service.ts`
- [ ] **Step 1: Read current service**
File: `D:\aura\AuraK\server\src\feishu\feishu.service.ts`
- [ ] **Step 2: Add WS management methods**
Add at the end of the class (before the closing brace):
```typescript
// ─── 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();
}
```
- [ ] **Step 3: Add import for ConnectionStatus**
Add at top of file:
```typescript
import { ConnectionStatus } from './dto/ws-status.dto';
import { FeishuWsManager } from './feishu-ws.manager';
```
- [ ] **Step 4: Run build to verify**
```bash
cd D:\aura\AuraK\server
yarn build
```
Expected: No errors
---
## Chunk 4: Controller Endpoints
### Task 6: Update FeishuController
**Files:**
- Modify: `server/src/feishu/feishu.controller.ts`
- [ ] **Step 1: Read current controller**
File: `D:\aura\AuraK\server\src\feishu\feishu.controller.ts`
- [ ] **Step 2: Add WebSocket endpoints**
Add after the existing bot management endpoints (after line ~79):
```typescript
// ─── 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**
```bash
cd D:\aura\AuraK\server
yarn build
```
Expected: No errors
---
## Chunk 5: Module Registration
### Task 7: Update FeishuModule
**Files:**
- Modify: `server/src/feishu/feishu.module.ts`
- [ ] **Step 1: Read current module**
File: `D:\aura\AuraK\server\src\feishu\feishu.module.ts`
- [ ] **Step 2: Register FeishuWsManager**
Add FeishuWsManager to providers and add FeishuService as constructor dependency:
```typescript
import { FeishuWsManager } from './feishu-ws.manager';
@Module({
imports: [TypeOrmModule.forFeature([FeishuBot])],
controllers: [FeishuController],
providers: [FeishuService, FeishuWsManager],
exports: [FeishuService],
})
export class FeishuModule {}
```
- [ ] **Step 3: Update FeishuService constructor**
In `feishu.service.ts`, add FeishuWsManager to constructor:
```typescript
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**
```bash
cd D:\aura\AuraK\server
yarn build
```
Expected: No errors
---
## Chunk 6: Testing & Verification
### Task 8: Test WebSocket Integration
- [ ] **Step 1: Start the server**
```bash
cd D:\aura\AuraK\server
yarn start:dev
```
Expected: Server starts without errors
- [ ] **Step 2: Verify endpoints exist**
```bash
curl http://localhost:13000/api/feishu/ws/status
```
Expected: Returns JSON with connections array
- [ ] **Step 3: Manual test with Feishu bot**
1. Create a Feishu bot in the UI
2. Configure in Feishu developer console:
- Enable "Use long connection to receive events"
- Add event: im.message.receive_v1
3. Call connect API:
```bash
curl -X POST http://localhost:13000/api/feishu/bots/{botId}/ws/connect
```
4. Send a message in Feishu to the bot
5. Verify response is received
- [ ] **Step 4: Test disconnect**
```bash
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.
---
## Chunk 7: Documentation Update
### Task 9: Update User Documentation
- [ ] **Step 1: Add WebSocket configuration guide**
Create or update documentation in `docs/` explaining:
- How to configure WebSocket mode
- Differences from webhook mode
- Troubleshooting steps
---
## Summary
| 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
@@ -0,0 +1,345 @@
# i18n Default Language Configuration Fix Design
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development to implement this plan.
**Goal:** Add DEFAULT_LANGUAGE configuration in .env file, with English as the hardcoded fallback when not configured.
**Architecture:** Read DEFAULT_LANGUAGE from environment variables in constants.ts, default to 'en' if not set.
**Tech Stack:** NestJS (backend), React (frontend), TypeScript, dotenv
---
## Problem Statement
### Current Issues
1. **No .env Configuration** - Default language is hardcoded in constants.ts
- Cannot be changed without code modification
- Different environments cannot have different defaults
2. **Fallback Language Issue** (`server/src/i18n/i18n.service.ts:180`)
- When an unsupported language is passed, system defaults to Japanese
- This causes unexpected Japanese output for users with unsupported locale
3. **Japanese Comments** (`server/src/common/constants.ts:21`)
- Comment `// デフォルト言語` should be English per project standards
4. **Hardcoded System Prompts** (`server/src/i18n/i18n.service.ts`)
- `getPrompt()` method: Japanese fallback prompts hardcoded in else branch
- `getDocumentTitlePrompt()` method: Japanese fallback hardcoded
- `getChatTitlePrompt()` method: Japanese fallback hardcoded
### Current State
| Component | Default Language | Source |
|-----------|-----------------|--------|
| Backend constants | `'zh'` | Hardcoded |
| Backend i18n service | `'zh'` | From constants |
| Frontend context | `'en'` | Hardcoded |
---
## Fix Design
### Decision: Env-Based Configuration with English Default
**Configuration Flow:**
```
.env → DEFAULT_LANGUAGE → constants.ts → i18n.service.ts
↓ (if not set)
'en' (hardcoded fallback)
```
**Rationale:**
- English is the most universally understood international language
- Safer default for international users
- Matches frontend default
- Environment-based config allows per-deployment customization
---
## Files to Modify
### 1. `server/.env.sample`
**Add:** Default language configuration option
```bash
# Default language for the system (zh, en, ja)
# If not set, defaults to 'en'
# DEFAULT_LANGUAGE=en
```
### 2. `server/src/common/constants.ts`
**Change:** Read from environment with English fallback
```typescript
// Supported languages
const SUPPORTED_LANGUAGES = ['zh', 'en', 'ja'] as const;
// Read DEFAULT_LANGUAGE from environment
function getDefaultLanguage(): typeof SUPPORTED_LANGUAGES[number] {
const envValue = process.env.DEFAULT_LANGUAGE?.toLowerCase();
// Validate: must be one of supported languages
if (envValue && SUPPORTED_LANGUAGES.includes(envValue as typeof SUPPORTED_LANGUAGES[number])) {
return envValue as typeof SUPPORTED_LANGUAGES[number];
}
// Fallback to English if not set or invalid
return 'en';
}
// Default language - read from env, fallback to English
export const DEFAULT_LANGUAGE = getDefaultLanguage();
export const DEFAULT_LANGUAGE_FALLBACK = 'en';
```
### 3. `server/src/i18n/i18n.service.ts`
#### 3.1 Fix getPrompt() fallback (line ~180)
**Current:**
```typescript
} else { // 默认为日语,符合プロジェクト要求
return type === 'withContext' ? `
以下ナレッジベース...
```
**Proposed:**
```typescript
} else { // Fallback to English for unsupported languages
return type === 'withContext' ? `
Answer the user's question based on the following knowledge base content.
${hasKnowledgeGroup ? `
**IMPORTANT**: The user has selected a specific knowledge group. Please answer strictly based on the knowledge base content below. If the relevant information is not found in the knowledge base, explicitly tell the user: "${noMatchMsg}", before providing an answer.
` : ''}
Knowledge Base CONTENT:
{context}
Conversation history:
{history}
User question: {question}
Please answer in English and strictly follow these Markdown formatting guidelines:
1. **Paragraphs & Structure**:
- Use clear paragraph breaks with blank lines between key points
- Use headings (## or ###) to organize longer answers
2. **Text Formatting**:
- Use **bold** to emphasize important concepts and keywords
- Use lists (- or 1.) to organize multiple points
- Use \`code\` to mark technical terms, commands, file names
3. **Code Display**:
- Use code blocks with language specification:
\`\`\`python
def example():
return "example"
\`\`\`
- Supported languages: python, javascript, typescript, java, bash, sql, etc.
4. **Diagrams & Charts**:
- Use Mermaid syntax for flowcharts, sequence diagrams, etc.:
\`\`\`mermaid
graph LR
A[Start] --> B[Process]
B --> C[End]
\`\`\`
- Use cases: process flows, architecture diagrams, state diagrams, sequence diagrams
5. **Other Requirements**:
- Keep answers concise and clear
- Use numbered lists for multi-step processes
- Use tables for comparison information (if applicable)
` : `
As an intelligent assistant, please answer the user's question.
Conversation history:
{history}
User question: {question}
Please answer in English.
`;
}
```
#### 3.2 Fix getDocumentTitlePrompt() fallback (line ~253)
**Current:**
```typescript
} else {
return `あなたはドキュメントアナライザー...
```
**Proposed:**
```typescript
} else {
return `You are a document analyzer. Read the following text (start of a document) and generate a concise, professional title (max 50 chars).
Return ONLY the title text. No preamble like "The title is...".
Language: English
Text:
${contentSample}`;
}
```
#### 3.3 Fix getChatTitlePrompt() fallback (line ~278)
**Current:**
```typescript
} else {
return `以下の会話スニペットに基づい...
```
**Proposed:**
```typescript
} else {
return `Based on the following conversation snippet, generate a short, descriptive title (max 50 chars) that summarizes the topic.
Return ONLY the title. No preamble.
Language: English
Snippet:
User: ${userMessage}
Assistant: ${aiResponse}`;
}
```
---
## Task Breakdown
### Task 1: Update .env.sample
**Files:**
- Modify: `server/.env.sample`
- [ ] **Step 1: Add DEFAULT_LANGUAGE configuration**
```bash
# Default language for the system (zh, en, ja)
# If not set, defaults to 'en' (English)
DEFAULT_LANGUAGE=en
```
### Task 2: Update constants.ts with env-based config
**Files:**
- Modify: `server/src/common/constants.ts`
- [ ] **Step 1: Replace hardcoded DEFAULT_LANGUAGE with env-based logic**
```typescript
// Supported languages
const SUPPORTED_LANGUAGES = ['zh', 'en', 'ja'] as const;
// Read DEFAULT_LANGUAGE from environment
function getDefaultLanguage(): typeof SUPPORTED_LANGUAGES[number] {
const envValue = process.env.DEFAULT_LANGUAGE?.toLowerCase();
// Validate: must be one of supported languages
if (envValue && SUPPORTED_LANGUAGES.includes(envValue as typeof SUPPORTED_LANGUAGES[number])) {
return envValue as typeof SUPPORTED_LANGUAGES[number];
}
// Fallback to English if not set or invalid
return 'en';
}
// Default language - read from env, fallback to English
export const DEFAULT_LANGUAGE = getDefaultLanguage();
export const DEFAULT_LANGUAGE_FALLBACK = 'en';
```
**Note:** No new dependencies required - uses simple array validation.
### Task 3: Fix i18n.service.ts getPrompt() Fallback
**Files:**
- Modify: `server/src/i18n/i18n.service.ts:180-235`
- [ ] **Step 1: Replace Japanese fallback with English**
Replace the entire `else` block (lines ~180-235) with English fallback prompts.
### Task 4: Fix i18n.service.ts getDocumentTitlePrompt() Fallback
**Files:**
- Modify: `server/src/i18n/i18n.service.ts:253-259`
- [ ] **Step 1: Replace Japanese fallback with English**
Replace the `else` block with English prompt.
### Task 5: Fix i18n.service.ts getChatTitlePrompt() Fallback
**Files:**
- Modify: `server/src/i18n/i18n.service.ts:278-285`
- [ ] **Step 1: Replace Japanese fallback with English**
Replace the `else` block with English prompt.
### Task 6: Verification
- [ ] **Step 1: Run TypeScript check**
```bash
cd server && yarn build
```
Expected: Build succeeds without errors
- [ ] **Step 2: Test env configuration**
```bash
# Test with DEFAULT_LANGUAGE=zh
DEFAULT_LANGUAGE=zh yarn start:dev
# Test with DEFAULT_LANGUAGE=en
DEFAULT_LANGUAGE=en yarn start:dev
# Test with no DEFAULT_LANGUAGE (should default to 'en')
yarn start:dev
```
- [ ] **Step 3: Verify fallback behavior**
Test that unsupported language code returns English instead of Japanese.
---
## Expected Result
| Configuration | Before | After |
|---------------|--------|-------|
| DEFAULT_LANGUAGE source | Hardcoded `'zh'` | Read from `.env`, fallback to `'en'` |
| Fallback language | `'ja'` | `'en'` |
| constants.ts comment | 日本語 | English |
| getPrompt fallback | 日本語 | English |
| getDocumentTitlePrompt fallback | 日本語 | English |
| getChatTitlePrompt fallback | 日本語 | English |
### Configuration Examples
| .env Setting | Result |
|--------------|--------|
| `DEFAULT_LANGUAGE=zh` | 中文 |
| `DEFAULT_LANGUAGE=en` | English |
| `DEFAULT_LANGUAGE=ja` | 日本語 |
| (not set) | English (default) |
| `DEFAULT_LANGUAGE=invalid` | English (fallback) |
---
## Risk & Mitigation
**Low Risk:** Changes are configuration-based with safe defaults.
**Mitigation:**
- Hardcoded `'en'` fallback ensures safe behavior even if env is misconfigured
- Keep 'zh', 'en', 'ja' as supported languages
- Japanese ('ja') still fully supported when explicitly configured
@@ -0,0 +1,505 @@
# Talent Assessment Management System - Design Document
**Date**: 2026-03-16
**Author**: Sisyphus AI Agent
**Status**: Draft for Review
## Executive Summary
Enhance the existing talent assessment system with comprehensive management functionality allowing administrators to configure assessment generation through keywords, question counts, and style requirements. The system will extract relevant content from knowledge bases and generate customized assessments based on these configurations.
---
## 1. Architecture Overview
### Current System Flow
```
User selects knowledge base → System generates 3-5 questions → User answers → AI grades → Report generated
```
### Enhanced System Flow
```
User selects template/inline config → System filters content by keywords → Generates N questions with specified style → User answers → AI grades → Report generated
```
### Key Components
#### Backend (NestJS)
- **AssessmentConfig Entity**: Store reusable assessment templates
- **Enhanced Question Generator**: Accept config parameters for customization
- **Content Filter Service**: Filter knowledge base content by keywords
- **Config Management API**: CRUD operations for assessment templates
#### Frontend (React)
- **Template Management Page**: Create/edit assessment templates
- **Enhanced Assessment Start**: Support template selection + inline config
- **Configuration UI**: Keywords input, question count slider, style presets
---
## 2. Database Schema Design
### New Entity: AssessmentConfig
```typescript
@Entity('assessment_configs')
export class AssessmentConfig {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column({ nullable: true })
description: string;
@Column({ name: 'tenant_id', nullable: true })
tenantId: string;
@Column({ type: 'simple-json' })
configuration: AssessmentConfigSettings;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}
```
### Configuration Settings Interface
```typescript
interface AssessmentConfigSettings {
// Keywords for content filtering and AI focus
keywords: string[];
// Question generation parameters
questionCount: number; // 1-20
difficultyDistribution: {
standard: number; // 0-100%
advanced: number; // 0-100%
specialist: number; // 0-100%
};
// Style requirements
style: {
tone: 'formal' | 'conversational' | 'technical' | 'business';
questionTypes: ('multiple-choice' | 'open-ended' | 'scenario-based' | 'technical')[];
assessmentGoals: ('knowledge-check' | 'problem-solving' | 'critical-thinking' | 'application')[];
};
// Content filtering
contentFilter: {
minContentLength: number; // Minimum characters for content to be considered
maxContextChunks: number; // Max chunks to include in context
};
}
```
### Modified Session Entity
Add reference to assessment config:
```typescript
@Entity('assessment_sessions')
export class AssessmentSession {
// ... existing fields ...
@Column({ name: 'config_id', nullable: true })
configId: string | null;
@ManyToOne(() => AssessmentConfig, { nullable: true })
@JoinColumn({ name: 'config_id' })
config: AssessmentConfig;
// Inline configuration (when no template used)
@Column({ type: 'simple-json', nullable: true, name: 'inline_config' })
inlineConfig: AssessmentConfigSettings | null;
}
```
---
## 3. Backend Implementation
### 3.1 New Service: AssessmentConfigService
```typescript
@Injectable()
export class AssessmentConfigService {
async createConfig(tenantId: string, settings: AssessmentConfigSettings): Promise<AssessmentConfig>
async getConfigs(tenantId: string): Promise<AssessmentConfig[]>
async getConfig(id: string, tenantId: string): Promise<AssessmentConfig>
async updateConfig(id: string, tenantId: string, settings: Partial<AssessmentConfigSettings>): Promise<AssessmentConfig>
async deleteConfig(id: string, tenantId: string): Promise<void>
async getActiveConfigs(tenantId: string): Promise<AssessmentConfig[]>
}
```
### 3.2 Enhanced Content Filter Service
```typescript
@Injectable()
export class ContentFilterService {
async filterByKeywords(
content: string,
keywords: string[],
method: 'exact' | 'semantic' | 'hybrid' = 'hybrid'
): Promise<string> {
// Extract relevant sections based on keywords
// Use semantic similarity for broader matching
// Return filtered content for question generation
}
}
```
### 3.3 Enhanced Question Generator Node
Modify `generator.node.ts` to accept configuration:
```typescript
interface GeneratorConfig {
keywords?: string[];
questionCount?: number;
difficultyDistribution?: AssessmentConfigSettings['difficultyDistribution'];
style?: AssessmentConfigSettings['style'];
}
export const questionGeneratorNode = async (
state: EvaluationState,
config?: RunnableConfig & { generatorConfig?: GeneratorConfig }
): Promise<Partial<EvaluationState>> {
// 1. Filter knowledge base content by keywords if provided
// 2. Generate questions based on configuration
// 3. Enforce question count and difficulty distribution
// 4. Apply style requirements to question generation
}
```
### 3.4 API Endpoints
#### Config Management
```
POST /api/v1/assessment/configs - Create assessment config
GET /api/v1/assessment/configs - List configs
GET /api/v1/assessment/configs/:id - Get config details
PUT /api/v1/assessment/configs/:id - Update config
DELETE /api/v1/assessment/configs/:id - Delete config
```
#### Enhanced Session Start
```typescript
// Existing: POST /assessment/start
// New parameters supported:
interface StartSessionRequest {
knowledgeBaseId: string;
language?: string;
configId?: string; // Use template
inlineConfig?: AssessmentConfigSettings; // Or use inline config
}
```
---
## 4. Frontend Implementation
### 4.1 New Page: AssessmentTemplatePage
**Location**: `web/src/pages/workspace/AssessmentTemplatePage.tsx`
**Features**:
- List existing templates with quick actions (edit, clone, delete)
- Create new template wizard
- Template preview with configuration summary
- Status toggle (active/inactive)
### 4.2 Enhanced Assessment Start Component
**Location**: `web/components/views/AssessmentView.tsx`
**New UI Elements**:
- Template selection dropdown
- "Use custom settings" toggle for inline config
- Configuration panel with:
- Keywords input (tags)
- Question count slider (1-20)
- Difficulty distribution pie chart/editor
- Style preset buttons
- Advanced settings collapsible section
### 4.3 Configuration UI Components
```typescript
// KeywordsInput.tsx
- Tag-based input for keywords
- Suggestions from knowledge base content
- Visual feedback for keyword matching
// QuestionCountSlider.tsx
- Slider from 1-20 questions
- Smart defaults based on content size
// DifficultyDistribution.tsx
- Interactive pie chart
- Drag to adjust percentages
- Real-time validation (must sum to 100%)
// StylePresets.tsx
- Pre-defined style combinations
- Custom style builder
```
---
## 5. Data Flow & Processing
### 5.1 Content Filtering Pipeline
```
1. Load knowledge base content
2. Apply keyword filtering (semantic similarity)
3. Chunk content into manageable segments
4. Rank segments by relevance to keywords
5. Select top N segments for context
6. Pass filtered content to question generator
```
### 5.2 Question Generation Pipeline
```
1. Receive configuration + filtered content
2. Analyze content for key concepts
3. Generate questions matching:
- Specified count
- Difficulty distribution
- Question types
- Style requirements
4. Validate question quality
5. Return structured question set
```
---
## 6. Security & Validation
### 6.1 Input Validation
```typescript
// Keywords validation
@ValidateNested()
@ArrayMaxSize(50)
keywords: string[];
// Question count validation
@IsInt()
@Min(1)
@Max(20)
questionCount: number;
// Difficulty distribution validation
@ValidateNested()
@IsNotEmpty()
difficultyDistribution: {
@IsInt()
@Min(0)
@Max(100)
standard: number;
// ... similar for advanced, specialist
};
// Validate sum equals 100
@ValidatorConstraint({ name: 'difficultySum', async: false })
export class DifficultySumConstraint implements ValidatorConstraintInterface {
validate(value: any) {
return (value.standard + value.advanced + value.specialist) === 100;
}
}
```
### 6.2 Access Control
- **Tenant Isolation**: Configs scoped to tenant
- **Role-Based Access**: Only admins can manage templates
- **Ownership Tracking**: Track who created/modified each config
---
## 7. Error Handling
### 7.1 Common Error Scenarios
| Error | Cause | Resolution |
|-------|-------|------------|
| `INSUFFICIENT_CONTENT` | Filtered content too short | Relax keyword constraints or select broader keywords |
| `INVALID_CONFIG` | Config validation failed | Return validation errors with specific field issues |
| `CONFIG_NOT_FOUND` | Template ID doesn't exist | Return 404 with config ID |
| `KEYWORD_TOO_SPECIFIC` | No content matches keywords | Suggest broader keywords or use all content |
### 7.2 Error Response Format
```json
{
"error": {
"code": "INSUFFICIENT_CONTENT",
"message": "Filtered content is too short for question generation",
"details": {
"minLength": 1000,
"actualLength": 250,
"suggestedKeywords": ["technology", "software"]
}
}
}
```
---
## 8. Testing Strategy
### 8.1 Unit Tests
**Backend**:
- `AssessmentConfigService` - CRUD operations
- `ContentFilterService` - Keyword filtering logic
- `questionGeneratorNode` - Question generation with config
**Frontend**:
- Configuration UI components
- Template management page
- Integration with existing assessment flow
### 8.2 Integration Tests
- End-to-end assessment creation with templates
- Content filtering accuracy validation
- Question generation quality assessment
### 8.3 Performance Tests
- Large knowledge base filtering
- Multiple concurrent template usage
- API response times under load
---
## 9. Migration Plan
### Phase 1: Database Schema (1 day)
1. Create `assessment_configs` table
2. Add `config_id` and `inline_config` to `assessment_sessions`
3. Create indexes for performance
### Phase 2: Backend API (2 days)
1. Implement `AssessmentConfigService`
2. Create API endpoints
3. Enhance question generator node
4. Add content filtering service
### Phase 3: Frontend UI (2 days)
1. Create template management page
2. Enhance assessment start component
3. Add configuration UI components
4. Integrate with existing flows
### Phase 4: Testing & Polish (1 day)
1. Comprehensive testing
2. Bug fixes
3. Performance optimization
4. Documentation
---
## 10. Success Metrics
### 10.1 Functional Metrics
- ✅ Templates can be created/edited/deleted
- ✅ Question count configurable (1-20)
- ✅ Keywords filter content effectively
- ✅ Style requirements applied to questions
- ✅ Backward compatibility maintained
### 10.2 Quality Metrics
- Question relevance score > 80% (user feedback)
- Content filtering accuracy > 90%
- API response time < 500ms
- Frontend load time < 2s
### 10.3 User Experience
- Template creation time < 2 minutes
- Assessment start with config < 10 seconds
- Configuration UI intuitive (measured by user testing)
---
## 11. Risks & Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| Keyword filtering too restrictive | Low quality questions | Implement semantic matching + fallback |
| Config complexity overwhelms users | Poor adoption | Progressive disclosure + presets |
| Performance degradation with large KB | Slow generation | Content chunking + caching |
| Breaking existing assessments | System disruption | Feature flag + gradual rollout |
---
## 12. Future Enhancements
1. **AI-powered keyword suggestions** based on content analysis
2. **Template sharing** across tenants (with permissions)
3. **Assessment analytics** - track which configs produce best results
4. **Multi-language templates** - store configs in multiple languages
5. **Integration with HR systems** - export assessment results
---
## Appendix A: Configuration Examples
### Example 1: Technical Interview Template
```json
{
"name": "Senior Developer Interview",
"keywords": ["system design", "algorithms", "database", "API", "security"],
"questionCount": 8,
"difficultyDistribution": {
"standard": 25,
"advanced": 50,
"specialist": 25
},
"style": {
"tone": "technical",
"questionTypes": ["technical", "scenario-based"],
"assessmentGoals": ["problem-solving", "critical-thinking"]
}
}
```
### Example 2: Knowledge Check Template
```json
{
"name": "Weekly Training Quiz",
"keywords": [],
"questionCount": 5,
"difficultyDistribution": {
"standard": 70,
"advanced": 30,
"specialist": 0
},
"style": {
"tone": "formal",
"questionTypes": ["multiple-choice", "open-ended"],
"assessmentGoals": ["knowledge-check"]
}
}
```
---
**Document Status**: Ready for review
**Next Steps**:
1. Review this design document
2. Approve or request changes
3. Create implementation plan using writing-plans skill
@@ -0,0 +1,456 @@
# Feishu WebSocket Integration - Design Document
**Date**: 2026-03-17
**Author**: Sisyphus AI Agent
**Status**: Draft for Review
## Executive Summary
Add WebSocket long-connection support for Feishu bot integration, enabling internal network deployment without requiring public domain or internet-facing endpoints. The system will support both existing webhook mode and new WebSocket mode, allowing users to choose their preferred connection method.
---
## 1. Architecture Overview
### Current Architecture (Webhook Mode)
```
Feishu Cloud → Public Domain → NAT/Firewall → Internal Server
└→ POST /api/feishu/webhook/:appId
```
**Limitations:**
- Requires public domain with SSL certificate
- Requires NAT/firewall port forwarding or reverse proxy
- Not suitable for pure internal network deployment
### New Architecture (WebSocket Mode)
```
Feishu Cloud ←──────── WebSocket (wss://open.feishu.cn) ──────── Internal Server
Feishu Cloud → Webhook (optional backup) → Internal Server
```
**Advantages:**
- No public domain required
- No NAT/firewall configuration needed
- Direct connection from internal network to Feishu cloud
- Real-time message delivery (milliseconds vs minutes)
- Connection复用,资源效率更高
### Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ Feishu Open Platform │
│ (WebSocket Event Subscription) │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────┴─────────────┐
│ │
┌─────▼──────┐ ┌──────▼─────┐
│ Bot A │ │ Bot B │
│ ws://.../A │ │ ws://.../B │
└─────┬──────┘ └──────┬─────┘
│ │
┌─────▼──────────────────────────▼──────┐
│ AuraK Server │
│ ┌──────────────────────────────────┐ │
│ │ FeishuModule │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ FeishuWsManager │ │ │
│ │ │ - per-bot connections │ │ │
│ │ │ - auto-reconnect │ │ │
│ │ │ - message routing │ │ │
│ │ └────────────────────────────┘ │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ FeishuService │ │ │
│ │ │ - existing logic │ │ │
│ │ │ - new ws connect/disconnect│ │ │
│ │ └────────────────────────────┘ │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ FeishuController │ │ │
│ │ │ - webhook endpoints │ │ │
│ │ │ - ws management APIs │ │ │
│ │ └────────────────────────────┘ │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────┘
```
---
## 2. Implementation Plan
### 2.1 New Components
#### 2.1.1 FeishuWsManager
**Purpose**: Manage WebSocket connections for each bot
**Responsibilities:**
- Establish and maintain WebSocket connections
- Handle connection lifecycle (connect, disconnect, reconnect)
- Route incoming messages to appropriate bot handlers
- Manage connection state per bot
**Location**: `server/src/feishu/feishu-ws.manager.ts`
**Key Methods:**
```typescript
class FeishuWsManager {
// Start WebSocket connection for a bot
async connect(bot: FeishuBot): Promise<void>
// Stop WebSocket connection for a bot
async disconnect(botId: string): Promise<void>
// Get connection status
getStatus(botId: string): ConnectionStatus
// Get all active connections
getAllConnections(): Map<string, ConnectionStatus>
}
```
#### 2.1.2 ConnectionStatus Type
```typescript
enum ConnectionState {
DISCONNECTED = 'disconnected',
CONNECTING = 'connecting',
CONNECTED = 'connected',
ERROR = 'error'
}
interface ConnectionStatus {
botId: string
state: ConnectionState
connectedAt?: Date
lastHeartbeat?: Date
error?: string
}
```
### 2.2 Modified Components
#### 2.2.1 FeishuService
**New Methods:**
```typescript
class FeishuService {
// Start WebSocket connection for a bot
async startWsConnection(botId: string): Promise<void>
// Stop WebSocket connection for a bot
async stopWsConnection(botId: string): Promise<void>
// Get connection status
async getWsStatus(botId: string): Promise<ConnectionStatus>
// List all connection statuses
async getAllWsStatuses(): Promise<ConnectionStatus[]>
}
```
#### 2.2.2 FeishuController
**New Endpoints:**
```typescript
// POST /feishu/bots/:id/ws/connect - Start WebSocket connection
// POST /feishu/bots/:id/ws/disconnect - Stop WebSocket connection
// GET /feishu/bots/:id/ws/status - Get connection status
// GET /feishu/ws/status - Get all connection statuses
```
**Modified Endpoints:**
- Keep existing webhook endpoints unchanged
- Add WebSocket status indicator in bot list response
#### 2.2.3 FeishuBot Entity
**New Fields:**
```typescript
@Entity('feishu_bots')
export class FeishuBot {
// ... existing fields ...
@Column({ default: false })
useWebSocket: boolean
@Column({ nullable: true })
wsConnectionState: string
}
```
### 2.3 Feishu SDK Integration
**Package**: `@larksuiteoapi/node-sdk`
**Installation:**
```bash
cd server && yarn add @larksuiteoapi/node-sdk
```
**Configuration:**
```typescript
import { EventDispatcher, Conf } from '@larksuiteoapi/node-sdk'
const client = new EventDispatcher({
appId: bot.appId,
appSecret: bot.appSecret,
verificationToken: bot.verificationToken,
}, {
logger: console
})
```
### 2.4 Event Handling
**Flow for WebSocket Mode:**
```
Feishu Cloud ──WebSocket──> FeishuWsManager.on('message')
Parse event type
┌───────────────┼───────────────┐
│ │ │
im.message. im.message. other
receive_v1 p2p_msg_received
│ │
▼ ▼
FeishuService.processChatMessage()
Send response via
FeishuService.sendTextMessage()
```
### 2.5 Configuration Changes
**Feishu Open Platform:**
Users need to configure in Feishu developer console:
1. Go to "Event Subscription" (事件与回调)
2. Select "Use long connection to receive events" (使用长连接接收事件)
3. Add event: `im.message.receive_v1`
4. **Important**: Must start local WebSocket client first before saving
---
## 3. Data Flow
### WebSocket Message Flow
```
1. User triggers connect API
2. FeishuController.connect(botId)
3. FeishuService.startWsConnection(botId)
4. FeishuWsManager.connect(bot)
5. SDK establishes WebSocket to open.feishu.cn
6. Connection established, events flow:
├─> on('message') ──> _processEvent() ──> _handleMessage()
│ │
│ ▼
│ FeishuService.processChatMessage()
│ │
│ ▼
│ FeishuService.sendTextMessage() (via SDK)
├─> on('error') ──> log error ──> trigger reconnect
└─> on('close') ──> trigger auto-reconnect
```
---
## 4. Error Handling
### Connection Errors
| Error Type | Handling |
|------------|----------|
| Network timeout | Retry with exponential backoff (max 5 attempts) |
| Invalid credentials | Mark bot as error state, notify user |
| Token expired | Refresh token, reconnect |
| Feishu server error | Wait 30s, retry |
### Auto-Reconnect Strategy
```
Initial delay: 1 second
Max delay: 60 seconds
Backoff multiplier: 2x
Max attempts: 5
Reset on successful connection
```
---
## 5. API Design
### 5.1 Connect WebSocket
```
POST /api/feishu/bots/:id/ws/connect
Response 200:
{
"success": true,
"botId": "bot_xxx",
"status": "connecting"
}
Response 400:
{
"success": false,
"error": "Bot not found or disabled"
}
```
### 5.2 Disconnect WebSocket
```
POST /api/feishu/bots/:id/ws/disconnect
Response 200:
{
"success": true,
"botId": "bot_xxx",
"status": "disconnected"
}
```
### 5.3 Get Connection Status
```
GET /api/feishu/bots/:id/ws/status
Response 200:
{
"botId": "bot_xxx",
"state": "connected",
"connectedAt": "2026-03-17T10:00:00Z",
"lastHeartbeat": "2026-03-17T10:05:00Z"
}
```
### 5.4 Get All Statuses
```
GET /api/feishu/ws/status
Response 200:
{
"connections": [
{
"botId": "bot_xxx",
"state": "connected",
"connectedAt": "2026-03-17T10:00:00Z"
},
{
"botId": "bot_yyy",
"state": "disconnected"
}
]
}
```
---
## 6. Security Considerations
1. **Credential Storage**: App ID and App Secret stored encrypted in database
2. **Connection Validation**: Verify bot belongs to authenticated user before connect
3. **Rate Limiting**: Implement per-bot message rate limiting
4. **Connection Limits**: Max 10 concurrent WebSocket connections per server instance
---
## 7. Testing Strategy
### Unit Tests
- FeishuWsManager connection lifecycle
- Message routing logic
- Error handling and reconnection
### Integration Tests
- Full WebSocket connection flow
- Message send/receive cycle
- Reconnection after network failure
### Manual Testing
- Local development without ngrok
- Verify webhook still works alongside WebSocket
---
## 8. Backward Compatibility
- **Existing webhook endpoints**: Unchanged, continue to work
- **Bot configuration**: Existing bots keep webhook mode by default
- **Migration path**: Users can switch to WebSocket anytime via API
- **Dual mode**: Both modes can run simultaneously for different bots
---
## 9. Migration Guide
### For Existing Users
1. Update AuraK to new version (with WebSocket support)
2. Install Feishu SDK: `yarn add @larksuiteoapi/node-sdk`
3. In Feishu Developer Console:
- Start local WebSocket server
- Change event subscription to "Use long connection"
4. Call `POST /api/feishu/bots/:id/ws/connect` to activate
---
## 10. Limitations
1. **Outbound Network Required**: Server must be able to reach `open.feishu.cn` via WebSocket
2. **Single Connection Per Bot**: Each bot needs its own WebSocket connection
3. **Feishu SDK Required**: Must install official SDK, cannot use raw WebSocket
4. **Private Feishu**: Does not support Feishu private deployment (自建飞书)
---
## 11. File Changes Summary
### New Files
- `server/src/feishu/feishu-ws.manager.ts` - WebSocket connection manager
- `server/src/feishu/dto/ws-status.dto.ts` - WebSocket status DTOs
### Modified Files
- `server/src/feishu/feishu.service.ts` - Add WS methods
- `server/src/feishu/feishu.controller.ts` - Add WS endpoints
- `server/src/feishu/entities/feishu-bot.entity.ts` - Add WS fields
- `server/src/feishu/feishu.module.ts` - Register new manager
### Dependencies
- Add: `@larksuiteoapi/node-sdk`
---
## 12. Success Criteria
- [ ] Server can establish WebSocket connection to Feishu
- [ ] Messages received via WebSocket are processed correctly
- [ ] Responses sent back to Feishu via SDK
- [ ] Auto-reconnect works after network interruption
- [ ] Webhook mode continues to work unchanged
- [ ] Both modes can coexist for different bots
- [ ] Internal network deployment works without public domain