# 飞书机器人与人才测评集成设计文档

> **文档版本**: 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 |

---

**文档结束**
