Files
aurak/docs/feishu-assessment-integration-design.md
T
Developer 0a9588abb7 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
2026-04-23 17:19:11 +08:00

1342 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 飞书机器人与人才测评集成设计文档
> **文档版本**: 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 |
---
**文档结束**