| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- import {
- Controller,
- Post,
- Get,
- Delete,
- Body,
- Param,
- Headers,
- UseGuards,
- Request,
- Logger,
- Patch,
- Res,
- } from '@nestjs/common';
- import { Response } from 'express';
- import { FeishuService } from './feishu.service';
- import { FeishuAssessmentService } from './services/feishu-assessment.service';
- import { CreateFeishuBotDto } from './dto/create-bot.dto';
- import { CombinedAuthGuard } from '../auth/combined-auth.guard';
- import { Public } from '../auth/public.decorator';
- import * as fs from 'fs';
- import * as path from 'path';
- @Controller('feishu')
- export class FeishuController {
- private readonly logger = new Logger(FeishuController.name);
- constructor(
- private readonly feishuService: FeishuService,
- private readonly feishuAssessmentService: FeishuAssessmentService,
- ) {}
- // ─── Bot Management Endpoints (JWT-protected) ─────────────────────────────
- /** GET /feishu/bots - List user's bots, masking sensitive fields */
- @Get('bots')
- @UseGuards(CombinedAuthGuard)
- async listBots(@Request() req) {
- const bots = await this.feishuService.getUserBots(
- req.user.id,
- req.user.tenantId,
- );
- return bots.map((bot) => ({
- id: bot.id,
- appId: bot.appId,
- botName: bot.botName,
- enabled: bot.enabled,
- isDefault: bot.isDefault,
- webhookUrl: `/api/feishu/webhook/${bot.appId}`,
- createdAt: bot.createdAt,
- }));
- }
- /** POST /feishu/bots - Create or update a bot */
- @Post('bots')
- @UseGuards(CombinedAuthGuard)
- async createBot(@Request() req, @Body() dto: CreateFeishuBotDto) {
- const bot = await this.feishuService.createBot(
- req.user.id,
- req.user.tenantId,
- dto,
- );
- return {
- id: bot.id,
- appId: bot.appId,
- botName: bot.botName,
- enabled: bot.enabled,
- webhookUrl: `/api/feishu/webhook/${bot.appId}`,
- };
- }
- /** PATCH /feishu/bots/:id/toggle - Enable or disable a bot */
- @Patch('bots/:id/toggle')
- @UseGuards(CombinedAuthGuard)
- async toggleBot(
- @Request() req,
- @Param('id') botId: string,
- @Body() body: { enabled: boolean },
- ) {
- const bot = await this.feishuService.setBotEnabled(botId, body.enabled);
- return { id: bot.id, enabled: bot.enabled };
- }
- /** DELETE /feishu/bots/:id - Delete a bot */
- @Delete('bots/:id')
- @UseGuards(CombinedAuthGuard)
- async deleteBot(@Request() req, @Param('id') botId: string) {
- await this.feishuService.deleteBot(req.user.id, botId);
- return { success: true };
- }
- // ─── WebSocket Management Endpoints ────────────────────────────────────
- /** POST /feishu/bots/:id/ws/connect - Start WebSocket connection */
- @Post('bots/:id/ws/connect')
- @UseGuards(CombinedAuthGuard)
- async connectWs(@Request() req, @Param('id') botId: string) {
- const bot = await this.feishuService.getBotById(botId);
- if (!bot || bot.userId !== req.user.id) {
- return { success: false, error: 'Bot not found' };
- }
- try {
- await this.feishuService.startWsConnection(botId);
- return { success: true, botId, status: 'connecting' };
- } catch (error: any) {
- return {
- success: false,
- botId,
- error: error?.message || 'Failed to connect',
- };
- }
- }
- /** POST /feishu/bots/:id/ws/disconnect - Stop WebSocket connection */
- @Post('bots/:id/ws/disconnect')
- @UseGuards(CombinedAuthGuard)
- async disconnectWs(@Request() req, @Param('id') botId: string) {
- const bot = await this.feishuService.getBotById(botId);
- if (!bot || bot.userId !== req.user.id) {
- return { success: false, error: 'Bot not found' };
- }
- try {
- await this.feishuService.stopWsConnection(botId);
- return { success: true, botId, status: 'disconnected' };
- } catch (error: any) {
- return {
- success: false,
- botId,
- error: error?.message || 'Failed to disconnect',
- };
- }
- }
- /** GET /feishu/bots/:id/ws/status - Get connection status */
- @Get('bots/:id/ws/status')
- @UseGuards(CombinedAuthGuard)
- async getWsStatus(@Request() req, @Param('id') botId: string) {
- const bot = await this.feishuService.getBotById(botId);
- if (!bot || bot.userId !== req.user.id) {
- return { success: false, error: 'Bot not found' };
- }
- const status = await this.feishuService.getWsStatus(botId);
- if (!status) {
- return { botId, state: 'disconnected' };
- }
- return {
- botId: status.botId,
- state: status.state,
- connectedAt: status.connectedAt?.toISOString(),
- lastHeartbeat: status.lastHeartbeat?.toISOString(),
- error: status.error,
- };
- }
- /** GET /feishu/ws/status - Get all active WS connection statuses */
- @Get('ws/status')
- @UseGuards(CombinedAuthGuard)
- async getAllWsStatus() {
- const statuses = await this.feishuService.getAllWsStatuses();
- return {
- connections: statuses.map((s) => ({
- botId: s.botId,
- state: s.state,
- connectedAt: s.connectedAt?.toISOString(),
- lastHeartbeat: s.lastHeartbeat?.toISOString(),
- error: s.error,
- })),
- };
- }
- // ─── Feishu Webhook Endpoint (Public) ────────────────────────────────────
- @Get('webhook/:appId')
- @Post('webhook/:appId')
- @Public()
- async handleWebhook(
- @Param('appId') appId: string,
- @Body() body: any,
- @Headers() headers: any,
- @Request() req: any,
- @Res() res: Response,
- ) {
- const logEntry = `\n[${new Date().toISOString()}] ${req.method} /api/feishu/webhook/${appId}\nHeaders: ${JSON.stringify(headers)}\nBody: ${JSON.stringify(body)}\n`;
- fs.appendFileSync('feishu_webhook.log', logEntry);
- this.logger.log(
- `Incoming Feishu webhook [${req.method}] for appId: ${appId}`,
- );
- // GET request for simple connection test
- if (req.method === 'GET') {
- return res.status(200).json({
- status: 'ok',
- message: 'AuraK Feishu Webhook is active.',
- appId,
- timestamp: new Date().toISOString(),
- });
- }
- // Step 1: URL verification handshake
- const challenge = body?.challenge || body?.event?.challenge;
- if (body?.type === 'url_verification' || challenge) {
- this.logger.log(
- `URL verification active for appId: ${appId}, challenge: ${challenge}`,
- );
- return res.status(200).json({ challenge });
- }
- // Step 2: Return 200 immediately for all other events
- res.status(200).json({ success: true });
- // Step 3: Process the event asynchronously
- if (body?.type === 'event_callback' || body?.header?.event_type) {
- setImmediate(() =>
- this._processEvent(appId, body).catch((e) =>
- this.logger.error('Failed to process Feishu event async', e),
- ),
- );
- }
- }
- // ─── Private Event Processor ─────────────────────────────────────────────
- private async _processEvent(appId: string, body: any): Promise<void> {
- const { type, event, header } = body;
- if (type !== 'event_callback') return;
- const eventType = header?.event_type || body.event_type;
- const eventId = header?.event_id;
- this.logger.log(
- `Processing Feishu event [${eventId}]: ${eventType} for appId: ${appId}`,
- );
- const bot = await this.feishuService.getBotByAppId(appId);
- if (!bot || !bot.enabled) {
- this.logger.warn(`Bot not found or disabled for appId: ${appId}`);
- return;
- }
- switch (eventType) {
- case 'im.message.receive_v1':
- case 'im.message.p2p_msg_received':
- case 'im.message.group_at_msg_received':
- await this._handleMessage(bot, event);
- break;
- default:
- this.logger.log(`Unhandled event type: ${eventType}`);
- }
- }
- /**
- * Parse incoming IM message and route to chatService via FeishuService.
- * Implements Chunk 5 integration.
- */
- 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;
- }
- // Parse text content
- 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 {
- // Centralized routing via FeishuService
- await this.feishuService.handleIncomingMessage(
- bot,
- openId,
- messageId,
- userText,
- );
- } catch (error) {
- this.logger.error('Message handling failed', error);
- }
- }
- }
|