feishu-assessment-integration-design.md 39 KB

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

文档版本: v1.0
创建日期: 2026-03-17
作者: AI Assistant
状态: Draft


目录

  1. 概述
  2. 现状分析
  3. 需求分析
  4. 详细设计方案
  5. 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:实时消息推送(推荐,性能更好)

知识库选择逻辑(关键代码)

// 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,
);

结论:飞书机器人当前使用默认知识库(用户的所有文件),因为 selectedFilesselectedGroups 都是 undefined

数据库实体

// 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. 流式支持:实时更新测评进度

关键接口

// 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                // 获取历史记录

集成点

  • 使用 KnowledgeBaseServiceKnowledgeGroupService 获取内容
  • 使用 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 实体
// 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 创建数据库迁移
// 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

// 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 更新创建机器人方法
// 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 修改消息处理逻辑
// 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 命令类型定义
// 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 命令解析器
// 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 数据库实体
// 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 迁移脚本
// 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 飞书测评服务
// 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 修改消息处理
// 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 模块初始化
// 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 创建/更新飞书机器人

POST /feishu/bots

请求体

{
    "appId": "cli_xxx",
    "appSecret": "xxx",
    "botName": "测评机器人",
    "knowledgeBaseId": "kb_xxx",      // 可选:特定知识库
    "knowledgeGroupId": "group_xxx"   // 可选:知识组
}

响应

{
    "id": "bot_xxx",
    "appId": "cli_xxx",
    "botName": "测评机器人",
    "webhookUrl": "/api/feishu/webhook/cli_xxx"
}

1.2 更新知识库配置

PATCH /feishu/bots/:id/knowledge

请求体

{
    "knowledgeBaseId": "kb_xxx",
    "knowledgeGroupId": null
}

1.3 获取机器人列表

GET /feishu/bots

响应

[
    {
        "id": "bot_xxx",
        "appId": "cli_xxx",
        "botName": "测评机器人",
        "enabled": true,
        "knowledgeBaseId": "kb_xxx",
        "knowledgeGroupName": "产品文档"
    }
]

2. 测评会话接口(可选)

2.1 通过飞书启动测评

POST /feishu/assessment/start

请求体

{
    "botId": "bot_xxx",
    "openId": "ou_xxx",
    "knowledgeBaseId": "kb_xxx",
    "templateId": "tmpl_xxx"
}

响应

{
    "sessionId": "sess_xxx",
    "question": {
        "id": "q_xxx",
        "text": "问题内容",
        "difficulty": "普通"
    }
}

2.2 提交测评答案

POST /feishu/assessment/answer

请求体

{
    "botId": "bot_xxx",
    "openId": "ou_xxx",
    "answer": "用户答案"
}

2.3 获取测评状态

GET /feishu/assessment/status/:botId/:openId

响应

{
    "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

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. 多租户隔离

  • 机制:所有查询必须包含 userIdtenantId 过滤
  • 实现:在 FeishuBot 实体中关联 User 实体,确保机器人只能访问所属用户的数据

2. 命令验证

  • 机制:白名单命令验证,防止恶意命令注入
  • 实现:命令解析器只识别预定义的命令格式

3. 会话超时

  • 机制:测评会话设置超时时间(如 24 小时)
  • 实现:定时清理过期会话

4. 数据隐私

  • 机制:测评结果仅对授权用户可见
  • 实现:所有接口使用 JWT 认证,验证用户权限

5. 敏感信息保护

  • 机制:不存储明文的 App Secret
  • 实现:加密存储 App Secret,使用时解密

附录

A. 参考资料

B. 术语表

  • RAG:检索增强生成 (Retrieval-Augmented Generation)
  • FeishuBot:飞书机器人实体
  • KnowledgeBase:知识库实体
  • AssessmentSession:测评会话实体

C. 变更记录

版本 日期 修改内容 作者
v1.0 2026-03-17 初始版本 AI Assistant

文档结束