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:
@@ -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
|
||||
Reference in New Issue
Block a user