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
+1341
View File
@@ -0,0 +1,1341 @@
# 飞书机器人与人才测评集成设计文档
> **文档版本**: v1.0
> **创建日期**: 2026-03-17
> **作者**: AI Assistant
> **状态**: Draft
---
## 目录
1. [概述](#概述)
2. [现状分析](#现状分析)
3. [需求分析](#需求分析)
4. [详细设计方案](#详细设计方案)
5. [API 接口设计](#api-接口设计)
6. [数据库设计](#数据库设计)
7. [实施计划](#实施计划)
8. [安全考虑](#安全考虑)
9. [附录](#附录)
---
## 概述
### 背景
本项目是一个基于 RAG(检索增强生成)的问答系统,支持多知识库管理。飞书机器人作为外部接入点,目前与聊天系统集成,但知识库选择机制不明确。用户希望:
1. 明确飞书机器人当前对接的知识库
2. 将飞书机器人与人才测评模块集成
### 设计目标
- 明确飞书机器人的知识库选择机制
- 实现飞书机器人与人才测评的完整集成
- 保持多租户隔离和系统安全性
- 提供友好的用户交互体验
---
## 现状分析
### 1. 飞书机器人知识库对接现状
#### 当前实现位置
- **主服务**: `D:\aura\AuraK\server\src\feishu\feishu.service.ts`
- **控制器**: `D:\aura\AuraK\server\src\feishu\feishu.controller.ts`
- **WebSocket 管理**: `D:\aura\AuraK\server\src\feishu\feishu-ws.manager.ts`
#### 集成方式
飞书机器人通过以下两种方式接收消息:
1. **Webhook**:飞书开放平台推送事件
2. **WebSocket**:实时消息推送(推荐,性能更好)
#### 知识库选择逻辑(关键代码)
```typescript
// feishu.service.ts (line 311-331)
const stream = this.chatService.streamChat(
userMessage,
[],
userId,
llmModel as any,
language,
undefined, // selectedEmbeddingId - 未指定
undefined, // selectedGroups - 未指定
undefined, // selectedFiles - 未指定 ← 关键点
undefined, // historyId
false, // enableRerank
// ... 其他参数
tenantId,
);
```
**结论**:飞书机器人当前使用**默认知识库**(用户的所有文件),因为 `selectedFiles``selectedGroups` 都是 `undefined`
#### 数据库实体
```typescript
// feishu-bot.entity.ts
@Entity('feishu_bots')
export class FeishuBot {
id: string;
userId: string;
appId: string;
appSecret: string;
botName?: string;
enabled: boolean;
isDefault: boolean;
useWebSocket: boolean;
// ❌ 缺少知识库配置字段
}
```
### 2. 人才测评模块现状
#### 模块位置
- **主服务**: `D:\aura\AuraK\server\src\assessment\assessment.service.ts`
- **控制器**: `D:\aura\AuraK\server\src\assessment\assessment.controller.ts`
- **实体**: `D:\aura\AuraK\server\src\assessment\entities\`
#### 核心功能
1. **会话管理**:创建、查询、删除测评会话
2. **问题生成**:基于知识库内容生成测评问题
3. **问答交互**:用户回答问题,系统评估并生成下一个问题
4. **报告生成**:测评完成后生成详细报告和评分
5. **流式支持**:实时更新测评进度
#### 关键接口
```typescript
// assessment.controller.ts
POST /assessment/start // 开始测评会话
POST /assessment/:id/answer // 提交答案
SSE /assessment/:id/start-stream // 流式获取初始问题
SSE /assessment/:id/answer-stream // 流式获取评估结果
GET /assessment/:id/state // 获取会话状态
GET /assessment // 获取历史记录
```
#### 集成点
- 使用 `KnowledgeBaseService``KnowledgeGroupService` 获取内容
- 使用 `RagService` 进行混合搜索
- 使用 `ChatService` 进行 LLM 交互
- 使用 LangGraph 构建评估图算法
---
## 需求分析
### 用户需求
1. **明确知识库选择机制**
- 飞书机器人当前对接哪个知识库?
- 如何配置飞书机器人使用特定知识库?
2. **飞书机器人与人才测评集成**
- 通过飞书机器人启动测评
- 通过飞书机器人回答测评问题
- 通过飞书机器人获取测评结果
### 功能需求
1. **知识库配置功能**
- 支持为每个飞书机器人配置特定知识库或知识组
- 支持动态切换知识库
2. **测评命令支持**
- `/assessment start [kbId|templateId]` - 开始测评
- `/assessment answer [answer]` - 回答问题
- `/assessment status` - 查看状态
- `/assessment result` - 获取结果
3. **交互体验优化**
- 使用飞书卡片展示问题
- 实时更新测评进度
- 友好的错误提示
### 非功能需求
1. **安全性**:多租户隔离,防止越权访问
2. **性能**:WebSocket 实时推送,避免超时
3. **可扩展性**:支持未来新增测评类型
4. **兼容性**:不影响现有聊天功能
---
## 详细设计方案
### 方案 1:飞书机器人知识库选择机制
#### 设计思路
`FeishuBot` 实体中增加知识库配置字段,支持以下模式:
1. **默认模式**:使用用户所有文件(当前行为)
2. **特定知识库**:只搜索指定知识库的文件
3. **知识组**:搜索知识组下的所有文件
#### 数据库变更
##### 1.1 新增字段到 FeishuBot 实体
```typescript
// D:\aura\AuraK\server\src\feishu\entities\feishu-bot.entity.ts
@Entity('feishu_bots')
export class FeishuBot {
// ... 现有字段保持不变
@Column({ name: 'knowledge_base_id', nullable: true, length: 36 })
knowledgeBaseId: string;
@Column({ name: 'knowledge_group_id', nullable: true, length: 36 })
knowledgeGroupId: string;
@ManyToOne(() => KnowledgeBase, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'knowledge_base_id' })
knowledgeBase?: KnowledgeBase;
@ManyToOne(() => KnowledgeGroup, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'knowledge_group_id' })
knowledgeGroup?: KnowledgeGroup;
}
```
##### 1.2 创建数据库迁移
```typescript
// D:\aura\AuraK\server\src\migrations\XXXXXX-AddFeishuBotKnowledgeFields.ts
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddFeishuBotKnowledgeFieldsXXXXXX implements MigrationInterface {
name = 'AddFeishuBotKnowledgeFields';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE feishu_bots
ADD COLUMN knowledge_base_id VARCHAR(36) NULL,
ADD COLUMN knowledge_group_id VARCHAR(36) NULL,
ADD CONSTRAINT fk_feishu_bot_knowledge_base
FOREIGN KEY (knowledge_base_id)
REFERENCES knowledge_bases(id)
ON DELETE SET NULL,
ADD CONSTRAINT fk_feishu_bot_knowledge_group
FOREIGN KEY (knowledge_group_id)
REFERENCES knowledge_groups(id)
ON DELETE SET NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE feishu_bots
DROP FOREIGN KEY fk_feishu_bot_knowledge_base,
DROP FOREIGN KEY fk_feishu_bot_knowledge_group,
DROP COLUMN knowledge_base_id,
DROP COLUMN knowledge_group_id;
`);
}
}
```
#### 1.3 更新 DTO
```typescript
// D:\aura\AuraK\server\src\feishu\dto\create-bot.dto.ts
export class CreateFeishuBotDto {
@IsString()
@IsNotEmpty()
appId: string;
@IsString()
@IsNotEmpty()
appSecret: string;
@IsString()
@IsOptional()
botName?: string;
// 新增知识库配置字段
@IsString()
@IsOptional()
knowledgeBaseId?: string;
@IsString()
@IsOptional()
knowledgeGroupId?: string;
}
```
#### 1.4 修改 FeishuService
##### 1.4.1 更新创建机器人方法
```typescript
// feishu.service.ts
async createBot(userId: string, dto: CreateFeishuBotDto): Promise<FeishuBot> {
const existing = await this.botRepository.findOne({
where: { userId, appId: dto.appId },
});
if (existing) {
Object.assign(existing, dto);
return this.botRepository.save(existing);
}
const bot = this.botRepository.create({ userId, ...dto });
return this.botRepository.save(bot);
}
```
##### 1.4.2 修改消息处理逻辑
```typescript
// feishu.service.ts
async processChatMessage(
bot: FeishuBot,
openId: string,
messageId: string,
userMessage: string,
): Promise<void> {
// ... 前面的代码保持不变
// 确定搜索范围
let selectedFiles: string[] | undefined;
let selectedGroups: string[] | undefined;
// 如果配置了特定知识库,获取该知识库的文件ID
if (bot.knowledgeBaseId) {
selectedFiles = await this.getFilesByKnowledgeBase(
bot.knowledgeBaseId,
userId,
tenantId
);
}
// 如果配置了知识组,使用知识组
else if (bot.knowledgeGroupId) {
selectedGroups = [bot.knowledgeGroupId];
}
// 否则使用默认(所有文件)
const stream = this.chatService.streamChat(
userMessage,
[],
userId,
llmModel as any,
language,
undefined, // selectedEmbeddingId
selectedGroups, // 改为使用配置的知识组
selectedFiles, // 改为使用配置的知识库文件
undefined, // historyId
false, // enableRerank
undefined, // selectedRerankId
undefined, // temperature
undefined, // maxTokens
10, // topK
0.7, // similarityThreshold
undefined, // rerankSimilarityThreshold
undefined, // enableQueryExpansion
undefined, // enableHyDE
tenantId,
);
// ... 后续处理保持不变
}
/**
* 获取知识库下的所有文件ID
*/
private async getFilesByKnowledgeBase(
knowledgeBaseId: string,
userId: string,
tenantId: string
): Promise<string[]> {
try {
// 调用 KnowledgeBaseService 获取文件列表
const kb = await this.knowledgeBaseService.findOne(knowledgeBaseId, userId, tenantId);
if (!kb) {
this.logger.warn(`Knowledge base not found: ${knowledgeBaseId}`);
return [];
}
// 假设 KnowledgeBase 有 files 字段或通过关联表获取
// 这里需要根据实际的 KnowledgeBase 实体结构调整
return kb.files?.map(f => f.id) || [];
} catch (error) {
this.logger.error(`Failed to get files from knowledge base: ${knowledgeBaseId}`, error);
return [];
}
}
```
### 方案 2:飞书机器人与人才测评集成
#### 设计思路
通过自然语言命令触发测评功能,支持以下场景:
1. 用户发送 `/assessment start` 启动测评
2. 系统发送问题卡片
3. 用户回复答案
4. 系统评估并发送下一个问题
5. 测评完成发送结果报告
#### 2.1 命令解析机制
##### 2.1.1 命令类型定义
```typescript
// D:\aura\AuraK\server\src\feishu\dto\assessment-command.dto.ts
export enum AssessmentCommandType {
START = 'start',
ANSWER = 'answer',
STATUS = 'status',
RESULT = 'result',
HELP = 'help',
}
export interface AssessmentCommand {
type: AssessmentCommandType;
parameters: string[];
rawMessage: string;
}
```
##### 2.1.2 命令解析器
```typescript
// D:\aura\AuraK\server\src\feishu\services\assessment-command.parser.ts
@Injectable()
export class AssessmentCommandParser {
private readonly commandPrefixes = ['/assessment', '/测评', '/eval'];
parse(message: string): AssessmentCommand | null {
const trimmed = message.trim();
// 检查是否是测评命令
const isCommand = this.commandPrefixes.some(prefix =>
trimmed.toLowerCase().startsWith(prefix)
);
if (!isCommand) {
return null;
}
// 解析命令
const parts = trimmed.split(/\s+/);
const commandType = parts[1]?.toLowerCase();
switch (commandType) {
case 'start':
return {
type: AssessmentCommandType.START,
parameters: parts.slice(2),
rawMessage: message,
};
case 'answer':
return {
type: AssessmentCommandType.ANSWER,
parameters: [parts.slice(2).join(' ')],
rawMessage: message,
};
case 'status':
return {
type: AssessmentCommandType.STATUS,
parameters: [],
rawMessage: message,
};
case 'result':
return {
type: AssessmentCommandType.RESULT,
parameters: [],
rawMessage: message,
};
case 'help':
case '?':
return {
type: AssessmentCommandType.HELP,
parameters: [],
rawMessage: message,
};
default:
return {
type: AssessmentCommandType.HELP,
parameters: [],
rawMessage: message,
};
}
}
}
```
#### 2.2 测评会话管理
##### 2.2.1 数据库实体
```typescript
// D:\aura\AuraK\server\src\feishu\entities\feishu-assessment-session.entity.ts
@Entity('feishu_assessment_sessions')
export class FeishuAssessmentSession {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'bot_id' })
botId: string;
@Column({ name: 'open_id' })
openId: string;
@Column({ name: 'assessment_session_id' })
assessmentSessionId: string;
@Column({
type: 'enum',
enum: ['active', 'completed', 'cancelled'],
default: 'active'
})
status: 'active' | 'completed' | 'cancelled';
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
// 关联关系
@ManyToOne(() => FeishuBot, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'bot_id' })
bot: FeishuBot;
}
```
##### 2.2.2 迁移脚本
```typescript
// D:\aura\AuraK\server\src\migrations\XXXXXX-CreateFeishuAssessmentSessionTable.ts
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateFeishuAssessmentSessionTableXXXXXX implements MigrationInterface {
name = 'CreateFeishuAssessmentSessionTable';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE feishu_assessment_sessions (
id VARCHAR(36) PRIMARY KEY,
bot_id VARCHAR(36) NOT NULL,
open_id VARCHAR(255) NOT NULL,
assessment_session_id VARCHAR(36) NOT NULL,
status ENUM('active', 'completed', 'cancelled') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_bot_open (bot_id, open_id),
INDEX idx_assessment_session (assessment_session_id),
CONSTRAINT fk_feishu_assessment_bot
FOREIGN KEY (bot_id)
REFERENCES feishu_bots(id)
ON DELETE CASCADE
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP TABLE feishu_assessment_sessions;
`);
}
}
```
#### 2.3 服务层实现
##### 2.3.1 飞书测评服务
```typescript
// D:\aura\AuraK\server\src\feishu\services\feishu-assessment.service.ts
@Injectable()
export class FeishuAssessmentService {
private readonly logger = new Logger(FeishuAssessmentService.name);
constructor(
@InjectRepository(FeishuAssessmentSession)
private sessionRepository: Repository<FeishuAssessmentSession>,
private assessmentService: AssessmentService,
private feishuService: FeishuService,
private commandParser: AssessmentCommandParser,
) {}
/**
* 处理测评命令
*/
async handleCommand(
bot: FeishuBot,
openId: string,
message: string,
): Promise<void> {
const command = this.commandParser.parse(message);
if (!command) {
// 不是测评命令,使用默认聊天处理
await this.feishuService.processChatMessage(bot, openId, '', message);
return;
}
try {
switch (command.type) {
case AssessmentCommandType.START:
await this.startAssessment(bot, openId, command.parameters);
break;
case AssessmentCommandType.ANSWER:
await this.submitAnswer(bot, openId, command.parameters[0]);
break;
case AssessmentCommandType.STATUS:
await this.getStatus(bot, openId);
break;
case AssessmentCommandType.RESULT:
await this.getResult(bot, openId);
break;
case AssessmentCommandType.HELP:
await this.sendHelp(bot, openId);
break;
}
} catch (error) {
this.logger.error(`Failed to handle assessment command: ${error.message}`, error);
await this.feishuService.sendTextMessage(
bot,
'open_id',
openId,
`处理测评命令时出错: ${error.message}`
);
}
}
/**
* 开始测评
*/
async startAssessment(
bot: FeishuBot,
openId: string,
parameters: string[],
): Promise<void> {
// 检查是否已有进行中的测评
const existingSession = await this.getActiveSession(bot.id, openId);
if (existingSession) {
await this.feishuService.sendTextMessage(
bot,
'open_id',
openId,
'您已有进行中的测评会话,请先完成当前测评。'
);
return;
}
// 解析参数
const [kbIdOrTemplateId, secondParam] = parameters;
let knowledgeBaseId: string | undefined;
let templateId: string | undefined;
// 判断是知识库ID还是模板ID
if (kbIdOrTemplateId) {
// 这里可以根据实际需求判断参数类型
// 简单实现:如果参数是UUID格式,假设是模板ID
if (kbIdOrTemplateId.length === 36) {
templateId = kbIdOrTemplateId;
} else {
// 否则尝试作为知识库ID
knowledgeBaseId = kbIdOrTemplateId;
}
}
// 使用机器人配置的知识库(如果未指定)
if (!knowledgeBaseId && !templateId && bot.knowledgeBaseId) {
knowledgeBaseId = bot.knowledgeBaseId;
}
this.logger.log(`Starting assessment: bot=${bot.id}, openId=${openId}, kb=${knowledgeBaseId}, template=${templateId}`);
// 创建测评会话
const session = await this.assessmentService.startSession(
bot.userId,
knowledgeBaseId,
bot.user?.tenantId || 'default',
'zh',
templateId,
);
// 存储飞书会话关联
const feishuSession = this.sessionRepository.create({
botId: bot.id,
openId,
assessmentSessionId: session.id,
status: 'active',
});
await this.sessionRepository.save(feishuSession);
// 发送第一个问题
if (session.questions_json && session.questions_json.length > 0) {
const firstQuestion = session.questions_json[0];
const card = this.buildQuestionCard(firstQuestion, session.id, 1, session.questions_json.length);
await this.feishuService.sendCardMessage(bot, 'open_id', openId, card);
} else {
await this.feishuService.sendTextMessage(
bot,
'open_id',
openId,
'测评会话已创建,但未能生成问题。'
);
}
}
/**
* 提交答案
*/
async submitAnswer(
bot: FeishuBot,
openId: string,
answer: string,
): Promise<void> {
const session = await this.getActiveSession(bot.id, openId);
if (!session) {
await this.feishuService.sendTextMessage(
bot,
'open_id',
openId,
'没有进行中的测评会话。请发送 /assessment start 开始测评。'
);
return;
}
this.logger.log(`Submitting answer for session ${session.assessmentSessionId}`);
// 提交答案到测评服务
const result = await this.assessmentService.submitAnswer(
session.assessmentSessionId,
bot.userId,
answer,
'zh',
);
// 更新飞书会话状态
if (result.report) {
session.status = 'completed';
await this.sessionRepository.save(session);
// 发送测评结果
await this.sendAssessmentResult(bot, openId, result);
} else if (result.questions && result.questions.length > 0) {
// 发送下一个问题
const currentQuestionIndex = result.currentQuestionIndex || 0;
const nextQuestion = result.questions[currentQuestionIndex];
const totalQuestions = result.questions.length;
const card = this.buildQuestionCard(
nextQuestion,
session.assessmentSessionId,
currentQuestionIndex + 1,
totalQuestions
);
await this.feishuService.sendCardMessage(bot, 'open_id', openId, card);
}
}
/**
* 获取测评状态
*/
async getStatus(bot: FeishuBot, openId: string): Promise<void> {
const session = await this.getActiveSession(bot.id, openId);
if (!session) {
await this.feishuService.sendTextMessage(
bot,
'open_id',
openId,
'没有进行中的测评会话。'
);
return;
}
const assessmentState = await this.assessmentService.getSessionState(
session.assessmentSessionId,
bot.userId
);
const currentQuestionIndex = assessmentState.currentQuestionIndex || 0;
const totalQuestions = assessmentState.questions?.length || 0;
const message = `测评状态:\n` +
`- 进度: ${currentQuestionIndex + 1}/${totalQuestions}\n` +
`- 状态: ${session.status}\n` +
`- 开始时间: ${session.createdAt.toLocaleString('zh-CN')}`;
await this.feishuService.sendTextMessage(bot, 'open_id', openId, message);
}
/**
* 获取测评结果
*/
async getResult(bot: FeishuBot, openId: string): Promise<void> {
const session = await this.getActiveSession(bot.id, openId);
if (!session) {
await this.feishuService.sendTextMessage(
bot,
'open_id',
openId,
'没有进行中的测评会话。'
);
return;
}
if (session.status !== 'completed') {
await this.feishuService.sendTextMessage(
bot,
'open_id',
openId,
'测评尚未完成,请先完成所有问题。'
);
return;
}
const assessmentState = await this.assessmentService.getSessionState(
session.assessmentSessionId,
bot.userId
);
await this.sendAssessmentResult(bot, openId, assessmentState);
}
/**
* 发送帮助信息
*/
async sendHelp(bot: FeishuBot, openId: string): Promise<void> {
const helpText = `
**人才测评机器人帮助**
命令格式:
- `/assessment start [kbId|templateId]` - 开始测评
- `/assessment answer [answer]` - 提交答案
- `/assessment status` - 查看测评状态
- `/assessment result` - 获取测评结果
- `/assessment help` - 显示帮助
说明:
- 如果未指定知识库/模板,将使用机器人配置的默认知识库
- 也可直接回复答案,无需命令前缀
`.trim();
await this.feishuService.sendTextMessage(bot, 'open_id', openId, helpText);
}
/**
* 获取活跃会话
*/
private async getActiveSession(
botId: string,
openId: string,
): Promise<FeishuAssessmentSession | null> {
return this.sessionRepository.findOne({
where: {
botId,
openId,
status: 'active',
},
order: { createdAt: 'DESC' },
});
}
/**
* 构建问题卡片
*/
private buildQuestionCard(
question: any,
sessionId: string,
currentIndex: number,
totalQuestions: number,
): any {
return {
config: { wide_screen_mode: true },
header: {
template: 'blue',
title: {
content: `人才测评 (${currentIndex}/${totalQuestions})`,
tag: 'plain_text',
},
},
elements: [
{
tag: 'div',
text: {
content: `**问题 ${currentIndex}:** ${question.text || question.content}`,
tag: 'lark_md',
},
},
...(question.options ? [
{
tag: 'div',
text: {
content: `选项:\n${question.options.map((opt: string, i: number) =>
`${String.fromCharCode(65 + i)}. ${opt}`
).join('\n')}`,
tag: 'lark_md',
},
}
] : []),
{
tag: 'div',
text: {
content: `难度: ${question.difficulty || '普通'} | 分值: ${question.score || 1}`,
tag: 'lark_md',
},
},
{
tag: 'hr',
},
{
tag: 'note',
elements: [
{
content: `直接回复答案或使用 /assessment answer [你的答案]`,
tag: 'plain_text',
},
],
},
],
};
}
/**
* 发送测评结果
*/
private async sendAssessmentResult(
bot: FeishuBot,
openId: string,
result: any,
): Promise<void> {
const report = result.report || result.finalReport;
const score = result.finalScore || result.score;
const resultCard = {
config: { wide_screen_mode: true },
header: {
template: 'green',
title: {
content: '测评完成',
tag: 'plain_text',
},
},
elements: [
{
tag: 'div',
text: {
content: `**测评结果**`,
tag: 'lark_md',
},
},
...(score !== undefined ? [
{
tag: 'div',
text: {
content: `**总分**: ${score.toFixed(1)}`,
tag: 'lark_md',
},
}
] : []),
...(report ? [
{
tag: 'div',
text: {
content: `**报告**:\n${report}`,
tag: 'lark_md',
},
}
] : []),
{
tag: 'hr',
},
{
tag: 'note',
elements: [
{
content: `发送 /assessment start 开始新的测评`,
tag: 'plain_text',
},
],
},
],
};
await this.feishuService.sendCardMessage(bot, 'open_id', openId, resultCard);
}
}
```
#### 2.4 集成到 FeishuService
##### 2.4.1 修改消息处理
```typescript
// feishu.service.ts
// 新增字段
private feishuAssessmentService: FeishuAssessmentService;
// 在构造函数后初始化
setFeishuAssessmentService(service: FeishuAssessmentService): void {
this.feishuAssessmentService = service;
}
// 修改 _handleMessage 方法
private async _handleMessage(bot: any, event: any): Promise<void> {
const message = event?.message;
if (!message) return;
const messageId = message.message_id;
const openId = event?.sender?.sender_id?.open_id;
if (!openId) {
this.logger.warn('No sender open_id found in Feishu event');
return;
}
// 解析文本内容
let userText = '';
try {
const content = JSON.parse(message.content || '{}');
userText = content.text || '';
} catch {
this.logger.warn('Failed to parse Feishu message content');
return;
}
if (!userText.trim()) return;
try {
// 检查是否是测评命令
if (this.isAssessmentCommand(userText)) {
// 委托给测评服务处理
await this.feishuAssessmentService.handleCommand(bot, openId, userText);
} else {
// 默认使用知识库问答
await this.processChatMessage(bot, openId, messageId, userText);
}
} catch (error) {
this.logger.error('Message handling failed', error);
try {
await this.sendTextMessage(
bot,
'open_id',
openId,
'抱歉,处理您的消息时遇到了错误,请稍后重试。',
);
} catch (sendError) {
this.logger.error('Failed to send error message to Feishu', sendError);
}
}
}
private isAssessmentCommand(message: string): boolean {
const trimmed = message.trim().toLowerCase();
return trimmed.startsWith('/assessment') ||
trimmed.startsWith('/测评') ||
trimmed.startsWith('/eval');
}
```
##### 2.4.2 模块初始化
```typescript
// D:\aura\AuraK\server\src\feishu\feishu.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([
FeishuBot,
FeishuAssessmentSession,
]),
forwardRef(() => ChatModule),
forwardRef(() => AssessmentModule),
forwardRef(() => KnowledgeBaseModule),
],
controllers: [FeishuController],
providers: [
FeishuService,
FeishuWsManager,
FeishuAssessmentService,
AssessmentCommandParser,
],
exports: [FeishuService, FeishuAssessmentService],
})
export class FeishuModule {}
```
---
## API 接口设计
### 1. 飞书机器人管理接口
#### 1.1 创建/更新飞书机器人
```http
POST /feishu/bots
```
**请求体**
```json
{
"appId": "cli_xxx",
"appSecret": "xxx",
"botName": "测评机器人",
"knowledgeBaseId": "kb_xxx", // 可选:特定知识库
"knowledgeGroupId": "group_xxx" // 可选:知识组
}
```
**响应**
```json
{
"id": "bot_xxx",
"appId": "cli_xxx",
"botName": "测评机器人",
"webhookUrl": "/api/feishu/webhook/cli_xxx"
}
```
#### 1.2 更新知识库配置
```http
PATCH /feishu/bots/:id/knowledge
```
**请求体**
```json
{
"knowledgeBaseId": "kb_xxx",
"knowledgeGroupId": null
}
```
#### 1.3 获取机器人列表
```http
GET /feishu/bots
```
**响应**
```json
[
{
"id": "bot_xxx",
"appId": "cli_xxx",
"botName": "测评机器人",
"enabled": true,
"knowledgeBaseId": "kb_xxx",
"knowledgeGroupName": "产品文档"
}
]
```
### 2. 测评会话接口(可选)
#### 2.1 通过飞书启动测评
```http
POST /feishu/assessment/start
```
**请求体**
```json
{
"botId": "bot_xxx",
"openId": "ou_xxx",
"knowledgeBaseId": "kb_xxx",
"templateId": "tmpl_xxx"
}
```
**响应**
```json
{
"sessionId": "sess_xxx",
"question": {
"id": "q_xxx",
"text": "问题内容",
"difficulty": "普通"
}
}
```
#### 2.2 提交测评答案
```http
POST /feishu/assessment/answer
```
**请求体**
```json
{
"botId": "bot_xxx",
"openId": "ou_xxx",
"answer": "用户答案"
}
```
#### 2.3 获取测评状态
```http
GET /feishu/assessment/status/:botId/:openId
```
**响应**
```json
{
"sessionId": "sess_xxx",
"status": "active",
"currentQuestion": 3,
"totalQuestions": 10,
"startTime": "2026-03-17T10:00:00Z"
}
```
---
## 数据库设计
### 实体关系图
```
┌─────────────────┐
│ FeishuBot │
│─────────────────│
│ id │◄──────┐
│ userId │ │
│ appId │ │ 1..*
│ knowledgeBaseId │───────┼──────┐
│ knowledgeGroupId│ │ │
└─────────────────┘ │ │
│ │
│ │
┌─────────────────────────┼──────┼─────────────────────┐
│ │ │ │
│ ┌─────────────────────┐ │ │ ┌─────────────────┐ │
│ │ FeishuAssessment │ │ │ │ KnowledgeBase │ │
│ │ Session │ │ │ └─────────────────┘ │
│ │─────────────────────│ │ │ │
│ │ id │ │ │ ┌─────────────────┐ │
│ │ botId │─┼──────┼─┤ KnowledgeGroup │ │
│ │ openId │ │ │ └─────────────────┘ │
│ │ assessmentSessionId │ │ │ │
│ │ status │ │ │ │
│ └─────────────────────┘ │ │ │
└─────────────────────────┴──────┴─────────────────────┘
│ 1..*
┌─────────────────────────┼─────────────────────────┐
│ │ │
│ ┌─────────────────────┐ │ ┌─────────────────────┐ │
│ │ AssessmentSession │ │ │ AssessmentResult │ │
│ │─────────────────────│ │ │─────────────────────│ │
│ │ id │ │ │ id │ │
│ │ userId │ │ │ sessionId │ │
│ │ knowledgeBaseId │ │ │ report │ │
│ │ questions_json │ │ │ score │ │
│ │ finalScore │ │ │ ... │ │
│ └─────────────────────┘ │ └─────────────────────┘ │
└─────────────────────────┴─────────────────────────┘
```
### 数据表结构
#### feishu_assessment_sessions
```sql
CREATE TABLE feishu_assessment_sessions (
id VARCHAR(36) PRIMARY KEY,
bot_id VARCHAR(36) NOT NULL,
open_id VARCHAR(255) NOT NULL,
assessment_session_id VARCHAR(36) NOT NULL,
status ENUM('active', 'completed', 'cancelled') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_bot_open (bot_id, open_id),
INDEX idx_assessment_session (assessment_session_id),
CONSTRAINT fk_feishu_assessment_bot
FOREIGN KEY (bot_id)
REFERENCES feishu_bots(id)
ON DELETE CASCADE
);
```
---
## 实施计划
### 阶段 1:基础架构(1-2 天)
#### 任务清单
- [ ] 1.1 创建数据库迁移脚本
- [ ] 1.2 更新 FeishuBot 实体和 DTO
- [ ] 1.3 修改 FeishuService 支持知识库选择
- [ ] 1.4 更新飞书机器人创建/更新接口
**交付物**
- 数据库迁移脚本
- 更新后的实体和 DTO
- 修改后的 FeishuService
### 阶段 2:测评集成(2-3 天)
#### 任务清单
- [ ] 2.1 创建 FeishuAssessmentSession 实体和迁移
- [ ] 2.2 实现命令解析器
- [ ] 2.3 实现 FeishuAssessmentService
- [ ] 2.4 集成到 FeishuService
- [ ] 2.5 设计并实现飞书卡片模板
**交付物**
- 测评会话实体和迁移
- 命令解析器
- 测评服务实现
- 飞书卡片设计
### 阶段 3:测试优化(1-2 天)
#### 任务清单
- [ ] 3.1 单元测试
- [ ] 3.2 集成测试
- [ ] 3.3 性能测试
- [ ] 3.4 文档编写
**交付物**
- 测试用例和测试报告
- 性能测试结果
- 用户使用文档
---
## 安全考虑
### 1. 多租户隔离
- **机制**:所有查询必须包含 `userId``tenantId` 过滤
- **实现**:在 `FeishuBot` 实体中关联 `User` 实体,确保机器人只能访问所属用户的数据
### 2. 命令验证
- **机制**:白名单命令验证,防止恶意命令注入
- **实现**:命令解析器只识别预定义的命令格式
### 3. 会话超时
- **机制**:测评会话设置超时时间(如 24 小时)
- **实现**:定时清理过期会话
### 4. 数据隐私
- **机制**:测评结果仅对授权用户可见
- **实现**:所有接口使用 JWT 认证,验证用户权限
### 5. 敏感信息保护
- **机制**:不存储明文的 App Secret
- **实现**:加密存储 App Secret,使用时解密
---
## 附录
### A. 参考资料
- [飞书开放平台文档](https://open.feishu.cn/document)
- [RAG 系统架构设计](./rag-architecture.md)
- [人才测评模块文档](./assessment-module.md)
### B. 术语表
- **RAG**:检索增强生成 (Retrieval-Augmented Generation)
- **FeishuBot**:飞书机器人实体
- **KnowledgeBase**:知识库实体
- **AssessmentSession**:测评会话实体
### C. 变更记录
| 版本 | 日期 | 修改内容 | 作者 |
|------|------|----------|------|
| v1.0 | 2026-03-17 | 初始版本 | AI Assistant |
---
**文档结束**