0a9588abb7
- 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
1342 lines
39 KiB
Markdown
1342 lines
39 KiB
Markdown
# 飞书机器人与人才测评集成设计文档
|
||
|
||
> **文档版本**: 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 |
|
||
|
||
---
|
||
|
||
**文档结束**
|