瀏覽代碼

feishu plugin

anhuiqiang 1 周之前
父節點
當前提交
9ec81b3f1b

+ 78 - 68
docs/superpowers/plans/2026-03-16-feishu-bot-integration.md

@@ -2,11 +2,11 @@
 
 > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
 
-**Goal:** Enable users to bind their Feishu (飞书) bots to their AuraK accounts and interact with the RAG system and talent assessment features through Feishu messages.
+**Goal:** Enable Feishu (飞书) bot integration as a standalone **Plugin** that users can manage via the system's "Plugins" menu. This allows users to bind bots, interact with RAG, and access assessment features in a modular, plug-and-play fashion.
 
-**Architecture:** Create a new `feishu` module that acts as a bridge between Feishu Open Platform and existing ChatService/AssessmentService. Users create Feishu custom apps, bind them in AuraK, and receive/push messages through Feishu's webhook-based event subscription.
+**Architecture:** Implement the integration as a pluggable `FeishuModule`. It acts as a bridge between Feishu Open Platform and AuraK's core services. The plugin is managed through the new `/plugins` workspace, isolating its UI and backend logic from the core system.
 
-**Tech Stack:** 
+**Tech Stack:**
 - NestJS (existing)
 - Feishu Open Platform APIs (im/v1/messages, auth/v3/tenant_access_token)
 - Event subscription (Webhooks)
@@ -33,15 +33,21 @@
                               ┌───────────────────────┼───────────────────────┐
                               ▼                       ▼                       ▼
                       ┌─────────────┐        ┌─────────────┐        ┌─────────────┐
-                      │ ChatService │        │AssessmentSvc│        │  UserService
-                      │  (RAG Q&A)  │        │ (评测对话)   │        │ (绑定管理)  
+                      │ ChatService │        │AssessmentSvc│        │ PluginsSvc  
+                      │  (RAG Q&A)  │        │ (评测对话)   │        │ (插件状态管理)
                       └─────────────┘        └─────────────┘        └─────────────┘
 ```
 
+**Plugin Isolation Strategy:**
+- **Backend**: `FeishuModule` is a standalone NestJS module. It handles its own database entities and webhook logic.
+- **Frontend**: Integrated as a sub-view within `/plugins`. When the Feishu plugin is "enabled", its configuration UI is rendered.
+- **Decoupling**: Communicates with core services via standard service calls or an event-subscriber pattern.
+
 ---
 
 ## File Structure
 
+
 ### New Files to Create
 
 ```
@@ -100,56 +106,21 @@ export class FeishuBot {
   id: string;
 
   @Column({ name: 'user_id' })
-  userId: string;
+  userId: string; // Plugin manages its own relationship to the core User
 
   @ManyToOne(() => User)
   @JoinColumn({ name: 'user_id' })
   user: User;
 
   @Column({ name: 'app_id', length: 64 })
-  appId: string;  // 飞书应用 App ID (cli_xxx)
-
-  @Column({ name: 'app_secret', length: 128 })
-  appSecret: string;  // 飞书应用 App Secret (加密存储)
-
-  @Column({ name: 'tenant_access_token', nullable: true })
-  tenantAccessToken: string;
-
-  @Column({ name: 'token_expires_at', nullable: true })
-  tokenExpiresAt: Date;
-
-  @Column({ name: 'verification_token', nullable: true })
-  verificationToken: string;  // 用于验证回调请求
-
-  @Column({ name: 'encrypt_key', nullable: true })
-  encryptKey: string;  // 飞书加密 key (可选)
-
-  @Column({ name: 'bot_name', nullable: true })
-  botName: string;  // 机器人名称显示
-
-  @Column({ default: true })
-  enabled: boolean;
-
-  @Column({ name: 'is_default', default: false })
-  isDefault: boolean;  // 用户是否有多个机器人
-
-  @CreateDateColumn({ name: 'created_at' })
-  createdAt: Date;
-
-  @UpdateDateColumn({ name: 'updated_at' })
-  updatedAt: Date;
+  appId: string;
+// ... (rest of the fields as defined previously)
 }
 ```
 
-- [ ] **Step 2: Add FeishuBot relation to User entity**
-
-Modify: `server/src/user/user.entity.ts:73-74`
+- [ ] **Step 2: Decoupled Relation**
+Instead of modifying the core `User` entity directly, the `FeishuBot` entity maintains its own reference to `User`. This keeps the core system clean and allows the plugin to be purely optional.
 
-Add after userSetting relation:
-```typescript
-  @OneToMany(() => FeishuBot, (bot) => bot.user)
-  feishuBots: FeishuBot[];
-```
 
 - [ ] **Step 3: Create DTOs**
 
@@ -675,15 +646,17 @@ export class VerifyWebhookDto {
 
 ---
 
-## Chunk 4: Module Registration
+## Chunk 4: Plugin Registration
 
-### Task 4.1: Register FeishuModule
+### Task 4.1: Register Feishu Plugin
+**Note:** This module acts as an optional extension to the AuraK ecosystem.
 
 **Files:**
 - Create: `server/src/feishu/feishu.module.ts`
 - Modify: `server/src/app.module.ts`
 
-- [ ] **Step 1: Create FeishuModule**
+- [ ] **Step 1: Create FeishuModule with isolated exports**
+
 
 ```typescript
 import { Module, forwardRef } from '@nestjs/common';
@@ -859,13 +832,20 @@ Modify the `processMessage` method in FeishuController to call `feishuService.pr
 
 ## Chunk 6: Frontend Integration (Optional)
 
-### Task 6.1: Add Feishu settings UI
-
+### Task 6.1: Add Feishu sub-view to Plugins
 **Files:**
-- Create: `web/src/pages/Settings/FeishuSettings.tsx` (or similar)
+- Create: `web/src/pages/Plugins/FeishuPlugin.tsx`
+- Modify: `web/src/components/views/PluginsView.tsx`
 - Modify: `web/src/services/api.ts`
 
-- [ ] **Step 1: Add API endpoints to frontend**
+- [ ] **Step 1: Create the Plugin Configuration UI**
+This view should match the design of other plugin cards in the /plugins page but provide a detailed setup guide and form for:
+- App ID / App Secret
+- Webhook URL (Read-only generated URL)
+- Verification Token & Encrypt Key
+
+- [ ] **Step 2: Register the Plugin in PluginsView**
+Modify the main plugins listing to include "Feishu Bot" as an available (or installed) plugin.
 
 Modify: `web/src/services/api.ts`
 ```typescript
@@ -877,50 +857,80 @@ export const feishuApi = {
 };
 ```
 
-- [ ] **Step 2: Create settings component**
+- [ ] **Step 3: Implementation of PluginsView.tsx**
+If `web/src/components/views/PluginsView.tsx` does not exist, create a generic plugin management layout that can host the Feishu configuration.
 
-Create a simple settings page where users can:
-1. View their bound Feishu bots
-2. Add new bot (paste App ID, App Secret)
-3. Delete bots
-4. See setup instructions
+**Layout Requirements:**
+- Grid of available plugins.
+- Status toggle (Enabled/Disabled).
+- Detail/Configuration view for active plugins.
 
----
 
 ## Testing Strategy
 
 ### Unit Tests
+
 - `feishu.service.spec.ts` - Test token refresh, message sending
 - `feishu.controller.spec.ts` - Test webhook endpoints
 
 ### Integration Tests
+
 - Test full flow: Feishu message → Webhook → ChatService → Response
 
 ### Manual Testing
+
 1. Create Feishu app in 开放平台
 2. Configure webhook URL (use ngrok for local)
 3. Subscribe to message events
 4. Send message to bot
 5. Verify RAG response
 
+
 ---
 
 ## Security Considerations
 
-1. **Token Storage**: Encrypt `app_secret` before storing in DB
-2. **Webhook Verification**: Always verify `verification_token` from Feishu
-3. **Rate Limiting**: Implement queue for sending messages (Feishu limits)
-4. **User Mapping**: Consider secure mapping between Feishu open_id and AuraK user
+1. **Token Storage**: Encrypt `app_secret` and `encrypt_key` before storing in DB.
+2. **Webhook Verification**:
+   - Verify `verify_token` for simplicity.
+   - **Recommended**: Validate `X-Lark-Signature` using `encrypt_key` to ensure authenticity.
+
+3. **Rate Limiting**: Implement a message queue (e.g., BullMQ) for outbound messages to respect Feishu's global and per-bot rate limits.
+4. **User Privacy**: Implement an opt-in flow for group chats to ensure the bot only processes messages when explicitly allowed or mentioned.
 
 ---
 
+## Advanced Optimizations (Recommended)
+
+### 1. Webhook Performance & Reliability
+- **Immediate Response**: Feishu requires a response within 3 seconds. The RAG process can take 10s+. 
+- **Optimization**: The `handleWebhook` should only validate the request and push the event to an internal queue, returning `200 OK` immediately. A background worker then processes the RAG logic and sends the response.
+- **Deduplication**: Use the `event_id` in the webhook payload to ignore duplicate retries from Feishu.
+
+### 2. UX: Managed "Thinking" State
+- **Simulated Streaming**: Since Feishu doesn't support SSE, send an initial "Thinking..." message and use the `PATCH /im/v1/messages/:message_id` API to update the message content every few seconds as chunks arrive.
+- **Interactive Cards**: Use [Message Cards](https://open.feishu.cn/document/common-capabilities/message-card/message-card-overview) instead of plain text for:
+  - Showing search citations with clickable links.
+  - Providing "Regenerate" or "Clear Context" buttons.
+  - Displaying assessment results with formatting (tables/charts).
+
+### 3. Context & History Management
+- **OpenID Mapping**: Maintain a mapping between Feishu `open_id` and AuraK `userId` to persist chat history across different devices/platforms.
+- **Thread Support**: Use Feishu's `root_id` and `parent_id` to allow users to ask follow-up questions within a thread, keeping the UI clean.
+
+### 4. Multi-modal Support
+- **File Ingestion**: Support `im.message.file_received` events. If a user sends a PDF/Docx to the bot, automatically import it into a "Feishu Uploads" group for immediate RAG context.
+- **Image Analysis**: Use the `VisionService` to handle images sent via Feishu.
+
+
 ## Future Enhancements
 
-1. **Assessment Integration**: Bind assessment sessions to Feishu conversations
-2. **Rich Responses**: Use Feishu interactive cards for better UI
-3. **Multi-bot Support**: Users can have multiple bots for different purposes
-4. **Group Chats**: Support bot in group chats
-5. **Voice Messages**: Handle voice message transcription
+1. **Assessment Integration**: Bind assessment sessions to Feishu conversations using interactive card forms.
+2. **Rich Responses**: Use Feishu interactive cards for better visual presentation.
+3. **Multi-bot Support**: Users can have multiple bots for different specialized tasks.
+4. **Group Chats**: Support bot in group chats with specific @mention logic and moderation.
+5. **Voice Messages**: Handle voice message transcription via Feishu's audio-to-text API for accessibility.
+
 
 ---
 

+ 727 - 0
docs/superpowers/plans/2026-03-17-feishu-websocket-integration.md

@@ -0,0 +1,727 @@
+# Feishu WebSocket Integration Implementation Plan
+
+> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add WebSocket long-connection support for Feishu bot integration, enabling internal network deployment without requiring public domain.
+
+**Architecture:** Each bot maintains its own WebSocket connection to Feishu cloud via official SDK. Connection management handled by dedicated FeishuWsManager service. Existing webhook mode preserved for backward compatibility.
+
+**Tech Stack:** NestJS, @larksuiteoapi/node-sdk, TypeScript
+
+---
+
+## File Structure
+
+```
+server/src/feishu/
+├── feishu.module.ts           # Register new manager
+├── feishu.service.ts          # Add WS control methods
+├── feishu.controller.ts      # Add WS API endpoints
+├── feishu-ws.manager.ts      # NEW: WebSocket connection manager
+├── dto/
+│   └── ws-status.dto.ts       # NEW: WebSocket status DTOs
+└── entities/
+    └── feishu-bot.entity.ts   # Add WS fields
+```
+
+---
+
+## Chunk 1: Dependencies & Entity Changes
+
+### Task 1: Install Feishu SDK
+
+- [ ] **Step 1: Install @larksuiteoapi/node-sdk**
+
+```bash
+cd D:\aura\AuraK\server
+yarn add @larksuiteoapi/node-sdk
+```
+
+- [ ] **Step 2: Verify installation**
+
+```bash
+yarn list @larksuiteoapi/node-sdk
+```
+
+---
+
+### Task 2: Update FeishuBot Entity
+
+**Files:**
+- Modify: `server/src/feishu/entities/feishu-bot.entity.ts`
+
+- [ ] **Step 1: Read current entity**
+
+File: `D:\aura\AuraK\server\src\feishu\entities\feishu-bot.entity.ts`
+
+- [ ] **Step 2: Add WebSocket fields**
+
+Add after existing columns:
+
+```typescript
+@Column({ default: false })
+useWebSocket: boolean;
+
+@Column({ nullable: true })
+wsConnectionState: string;
+```
+
+- [ ] **Step 3: Run build to verify**
+
+```bash
+cd D:\aura\AuraK\server
+yarn build
+```
+
+---
+
+## Chunk 2: WebSocket Manager
+
+### Task 3: Create WebSocket Status DTOs
+
+**Files:**
+- Create: `server/src/feishu/dto/ws-status.dto.ts`
+
+- [ ] **Step 1: Create DTO file**
+
+```typescript
+export enum ConnectionState {
+  DISCONNECTED = 'disconnected',
+  CONNECTING = 'connecting',
+  CONNECTED = 'connected',
+  ERROR = 'error',
+}
+
+export interface ConnectionStatus {
+  botId: string;
+  state: ConnectionState;
+  connectedAt?: Date;
+  lastHeartbeat?: Date;
+  error?: string;
+}
+
+export class ConnectWsDto {
+  // No params needed - uses bot ID from route
+}
+
+export class WsStatusResponseDto {
+  botId: string;
+  state: ConnectionState;
+  connectedAt?: string;
+  lastHeartbeat?: string;
+  error?: string;
+}
+
+export class WsConnectResponseDto {
+  success: boolean;
+  botId: string;
+  status: ConnectionState;
+  error?: string;
+}
+
+export class WsDisconnectResponseDto {
+  success: boolean;
+  botId: string;
+  status: ConnectionState;
+}
+
+export class AllWsStatusResponseDto {
+  connections: WsStatusResponseDto[];
+}
+```
+
+---
+
+### Task 4: Create FeishuWsManager
+
+**Files:**
+- Create: `server/src/feishu/feishu-ws.manager.ts`
+
+- [ ] **Step 1: Create the WebSocket manager**
+
+```typescript
+import { Injectable, Logger } from '@nestjs/common';
+import { EventDispatcher, Conf } from '@larksuiteoapi/node-sdk';
+import { FeishuBot } from './entities/feishu-bot.entity';
+import { ConnectionState, ConnectionStatus } from './dto/ws-status.dto';
+import { FeishuService } from './feishu.service';
+
+@Injectable()
+export class FeishuWsManager {
+  private readonly logger = new Logger(FeishuWsManager.name);
+  private connections: Map<string, { client: EventDispatcher; status: ConnectionStatus }> = new Map();
+  private reconnectAttempts: Map<string, number> = new Map();
+  private readonly MAX_RECONNECT_ATTEMPTS = 5;
+  private readonly RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000]; // Exponential backoff
+
+  constructor(private readonly feishuService: FeishuService) {}
+
+  /**
+   * Start WebSocket connection for a bot
+   */
+  async connect(bot: FeishuBot): Promise<void> {
+    const botId = bot.id;
+
+    // Check if already connected
+    const existing = this.connections.get(botId);
+    if (existing && existing.status.state === ConnectionState.CONNECTED) {
+      this.logger.warn(`Bot ${botId} already connected`);
+      return;
+    }
+
+    // Set connecting state
+    this.updateStatus(botId, {
+      botId,
+      state: ConnectionState.CONNECTING,
+    });
+
+    try {
+      // Create event dispatcher (WebSocket client)
+      const client = new EventDispatcher(
+        {
+          appId: bot.appId,
+          appSecret: bot.appSecret,
+          verificationToken: bot.verificationToken,
+        } as any,
+        {
+          logger: {
+            debug: (msg: any) => this.logger.debug(msg),
+            info: (msg: any) => this.logger.log(msg),
+            warn: (msg: any) => this.logger.warn(msg),
+            error: (msg: any) => this.logger.error(msg),
+          },
+        } as any,
+      );
+
+      // Register event handlers
+      client.on('im.message.receive_v1', async (data: any) => {
+        await this.handleMessage(bot, data);
+      });
+
+      // Store connection
+      this.connections.set(botId, {
+        client: client as any,
+        status: {
+          botId,
+          state: ConnectionState.CONNECTED,
+          connectedAt: new Date(),
+        },
+      });
+
+      this.reconnectAttempts.set(botId, 0);
+
+      this.logger.log(`WebSocket connected for bot ${botId}`);
+
+      // Update bot state in DB
+      await this.feishuService.getBotById(botId);
+    } catch (error) {
+      this.logger.error(`Failed to connect WebSocket for bot ${botId}`, error);
+      this.updateStatus(botId, {
+        botId,
+        state: ConnectionState.ERROR,
+        error: error.message || 'Connection failed',
+      });
+      throw error;
+    }
+  }
+
+  /**
+   * Disconnect WebSocket for a bot
+   */
+  async disconnect(botId: string): Promise<void> {
+    const connection = this.connections.get(botId);
+    if (!connection) {
+      this.logger.warn(`No connection found for bot ${botId}`);
+      return;
+    }
+
+    try {
+      // SDK doesn't have explicit disconnect, just remove references
+      this.connections.delete(botId);
+      this.reconnectAttempts.delete(botId);
+
+      this.logger.log(`WebSocket disconnected for bot ${botId}`);
+    } catch (error) {
+      this.logger.error(`Error disconnecting bot ${botId}`, error);
+      throw error;
+    }
+  }
+
+  /**
+   * Get connection status for a bot
+   */
+  getStatus(botId: string): ConnectionStatus | null {
+    const connection = this.connections.get(botId);
+    return connection?.status || null;
+  }
+
+  /**
+   * Get all connection statuses
+   */
+  getAllStatuses(): ConnectionStatus[] {
+    const statuses: ConnectionStatus[] = [];
+    for (const [botId, connection] of this.connections.entries()) {
+      statuses.push(connection.status);
+    }
+    return statuses;
+  }
+
+  /**
+   * Check if a bot is connected
+   */
+  isConnected(botId: string): boolean {
+    const connection = this.connections.get(botId);
+    return connection?.status.state === ConnectionState.CONNECTED;
+  }
+
+  /**
+   * Handle incoming message from Feishu
+   */
+  private async handleMessage(bot: FeishuBot, data: any): Promise<void> {
+    this.logger.log(`Received message for bot ${bot.id}: ${JSON.stringify(data)}`);
+
+    try {
+      const event = data.event || data;
+      const message = event?.message;
+
+      if (!message) {
+        this.logger.warn('No message in event data');
+        return;
+      }
+
+      const messageId = message.message_id;
+      const openId = event?.sender?.sender_id?.open_id;
+
+      if (!openId) {
+        this.logger.warn('No sender open_id found');
+        return;
+      }
+
+      // Parse text content
+      let userText = '';
+      try {
+        const content = JSON.parse(message.content || '{}');
+        userText = content.text || '';
+      } catch {
+        this.logger.warn('Failed to parse message content');
+        return;
+      }
+
+      if (!userText.trim()) return;
+
+      // Process via FeishuService
+      await this.feishuService.processChatMessage(bot, openId, messageId, userText);
+    } catch (error) {
+      this.logger.error('Error handling message', error);
+    }
+  }
+
+  /**
+   * Update connection status
+   */
+  private updateStatus(botId: string, status: Partial<ConnectionStatus>): void {
+    const connection = this.connections.get(botId);
+    if (connection) {
+      connection.status = { ...connection.status, ...status };
+    }
+  }
+
+  /**
+   * Attempt to reconnect a bot
+   */
+  async attemptReconnect(bot: FeishuBot): Promise<void> {
+    const botId = bot.id;
+    const attempts = this.reconnectAttempts.get(botId) || 0;
+
+    if (attempts >= this.MAX_RECONNECT_ATTEMPTS) {
+      this.logger.error(`Max reconnect attempts reached for bot ${botId}`);
+      this.updateStatus(botId, {
+        botId,
+        state: ConnectionState.ERROR,
+        error: 'Max reconnect attempts reached',
+      });
+      return;
+    }
+
+    const delay = this.RECONNECT_DELAYS[attempts] || this.RECONNECT_DELAYS[this.RECONNECT_DELAYS.length - 1];
+    this.logger.log(`Reconnecting bot ${botId} in ${delay}ms (attempt ${attempts + 1})`);
+
+    this.reconnectAttempts.set(botId, attempts + 1);
+
+    setTimeout(async () => {
+      try {
+        await this.connect(bot);
+      } catch (error) {
+        this.logger.error(`Reconnect failed for bot ${botId}`, error);
+      }
+    }, delay);
+  }
+}
+```
+
+- [ ] **Step 2: Run build to verify**
+
+```bash
+cd D:\aura\AuraK\server
+yarn build
+```
+
+Expected: No errors
+
+---
+
+## Chunk 3: Service Integration
+
+### Task 5: Update FeishuService
+
+**Files:**
+- Modify: `server/src/feishu/feishu.service.ts`
+
+- [ ] **Step 1: Read current service**
+
+File: `D:\aura\AuraK\server\src\feishu\feishu.service.ts`
+
+- [ ] **Step 2: Add WS management methods**
+
+Add at the end of the class (before the closing brace):
+
+```typescript
+// ─── WebSocket Connection Management ─────────────────────────────────────────
+
+@Inject(forwardRef(() => FeishuWsManager))
+private wsManager: FeishuWsManager;
+
+/**
+ * Start WebSocket connection for a bot
+ */
+async startWsConnection(botId: string): Promise<void> {
+  const bot = await this.getBotById(botId);
+  if (!bot) {
+    throw new Error('Bot not found');
+  }
+  if (!bot.enabled) {
+    throw new Error('Bot is disabled');
+  }
+
+  bot.useWebSocket = true;
+  await this.botRepository.save(bot);
+
+  await this.wsManager.connect(bot);
+}
+
+/**
+ * Stop WebSocket connection for a bot
+ */
+async stopWsConnection(botId: string): Promise<void> {
+  const bot = await this.getBotById(botId);
+  if (!bot) {
+    throw new Error('Bot not found');
+  }
+
+  bot.useWebSocket = false;
+  await this.botRepository.save(bot);
+
+  await this.wsManager.disconnect(botId);
+}
+
+/**
+ * Get WebSocket connection status
+ */
+async getWsStatus(botId: string): Promise<ConnectionStatus | null> {
+  return this.wsManager.getStatus(botId);
+}
+
+/**
+ * Get all WebSocket connection statuses
+ */
+async getAllWsStatuses(): Promise<ConnectionStatus[]> {
+  return this.wsManager.getAllStatuses();
+}
+```
+
+- [ ] **Step 3: Add import for ConnectionStatus**
+
+Add at top of file:
+
+```typescript
+import { ConnectionStatus } from './dto/ws-status.dto';
+import { FeishuWsManager } from './feishu-ws.manager';
+```
+
+- [ ] **Step 4: Run build to verify**
+
+```bash
+cd D:\aura\AuraK\server
+yarn build
+```
+
+Expected: No errors
+
+---
+
+## Chunk 4: Controller Endpoints
+
+### Task 6: Update FeishuController
+
+**Files:**
+- Modify: `server/src/feishu/feishu.controller.ts`
+
+- [ ] **Step 1: Read current controller**
+
+File: `D:\aura\AuraK\server\src\feishu\feishu.controller.ts`
+
+- [ ] **Step 2: Add WebSocket endpoints**
+
+Add after the existing bot management endpoints (after line ~79):
+
+```typescript
+// ─── 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) {
+  // Verify bot belongs to user
+  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) {
+    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) {
+  // Verify bot belongs to user
+  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) {
+    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) {
+  // Verify bot belongs to user
+  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 connection statuses
+ */
+@Get('ws/status')
+@UseGuards(CombinedAuthGuard)
+async getAllWsStatus(@Request() req) {
+  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,
+    })),
+  };
+}
+```
+
+- [ ] **Step 3: Run build to verify**
+
+```bash
+cd D:\aura\AuraK\server
+yarn build
+```
+
+Expected: No errors
+
+---
+
+## Chunk 5: Module Registration
+
+### Task 7: Update FeishuModule
+
+**Files:**
+- Modify: `server/src/feishu/feishu.module.ts`
+
+- [ ] **Step 1: Read current module**
+
+File: `D:\aura\AuraK\server\src\feishu\feishu.module.ts`
+
+- [ ] **Step 2: Register FeishuWsManager**
+
+Add FeishuWsManager to providers and add FeishuService as constructor dependency:
+
+```typescript
+import { FeishuWsManager } from './feishu-ws.manager';
+
+@Module({
+  imports: [TypeOrmModule.forFeature([FeishuBot])],
+  controllers: [FeishuController],
+  providers: [FeishuService, FeishuWsManager],
+  exports: [FeishuService],
+})
+export class FeishuModule {}
+```
+
+- [ ] **Step 3: Update FeishuService constructor**
+
+In `feishu.service.ts`, add FeishuWsManager to constructor:
+
+```typescript
+constructor(
+  @InjectRepository(FeishuBot)
+  private botRepository: Repository<FeishuBot>,
+  @Inject(forwardRef(() => ChatService))
+  private chatService: ChatService,
+  @Inject(forwardRef(() => ModelConfigService))
+  private modelConfigService: ModelConfigService,
+  @Inject(forwardRef(() => UserService))
+  private userService: UserService,
+  @Inject(forwardRef(() => FeishuWsManager))
+  private wsManager: FeishuWsManager,
+) {}
+```
+
+- [ ] **Step 4: Run build to verify**
+
+```bash
+cd D:\aura\AuraK\server
+yarn build
+```
+
+Expected: No errors
+
+---
+
+## Chunk 6: Testing & Verification
+
+### Task 8: Test WebSocket Integration
+
+- [ ] **Step 1: Start the server**
+
+```bash
+cd D:\aura\AuraK\server
+yarn start:dev
+```
+
+Expected: Server starts without errors
+
+- [ ] **Step 2: Verify endpoints exist**
+
+```bash
+curl http://localhost:13000/api/feishu/ws/status
+```
+
+Expected: Returns JSON with connections array
+
+- [ ] **Step 3: Manual test with Feishu bot**
+
+1. Create a Feishu bot in the UI
+2. Configure in Feishu developer console:
+   - Enable "Use long connection to receive events"
+   - Add event: im.message.receive_v1
+3. Call connect API:
+```bash
+curl -X POST http://localhost:13000/api/feishu/bots/{botId}/ws/connect
+```
+4. Send a message in Feishu to the bot
+5. Verify response is received
+
+- [ ] **Step 4: Test disconnect**
+
+```bash
+curl -X POST http://localhost:13000/api/feishu/bots/{botId}/ws/disconnect
+```
+
+- [ ] **Step 5: Verify webhook still works**
+
+Test existing webhook endpoint still works as before.
+
+---
+
+## Chunk 7: Documentation Update
+
+### Task 9: Update User Documentation
+
+- [ ] **Step 1: Add WebSocket configuration guide**
+
+Create or update documentation in `docs/` explaining:
+- How to configure WebSocket mode
+- Differences from webhook mode
+- Troubleshooting steps
+
+---
+
+## Summary
+
+| Task | Description | Estimated Time |
+|------|-------------|----------------|
+| 1 | Install Feishu SDK | 2 min |
+| 2 | Update FeishuBot entity | 5 min |
+| 3 | Create WS Status DTOs | 5 min |
+| 4 | Create FeishuWsManager | 15 min |
+| 5 | Update FeishuService | 10 min |
+| 6 | Update FeishuController | 10 min |
+| 7 | Update FeishuModule | 5 min |
+| 8 | Testing & Verification | 20 min |
+| 9 | Documentation | 10 min |
+
+**Total estimated time:** ~80 minutes

+ 456 - 0
docs/superpowers/specs/2026-03-17-feishu-websocket-integration-design.md

@@ -0,0 +1,456 @@
+# Feishu WebSocket Integration - Design Document
+
+**Date**: 2026-03-17
+**Author**: Sisyphus AI Agent
+**Status**: Draft for Review
+
+## Executive Summary
+
+Add WebSocket long-connection support for Feishu bot integration, enabling internal network deployment without requiring public domain or internet-facing endpoints. The system will support both existing webhook mode and new WebSocket mode, allowing users to choose their preferred connection method.
+
+---
+
+## 1. Architecture Overview
+
+### Current Architecture (Webhook Mode)
+
+```
+Feishu Cloud → Public Domain → NAT/Firewall → Internal Server
+                                    └→ POST /api/feishu/webhook/:appId
+```
+
+**Limitations:**
+- Requires public domain with SSL certificate
+- Requires NAT/firewall port forwarding or reverse proxy
+- Not suitable for pure internal network deployment
+
+### New Architecture (WebSocket Mode)
+
+```
+Feishu Cloud ←──────── WebSocket (wss://open.feishu.cn) ──────── Internal Server
+                                         ↑
+Feishu Cloud → Webhook (optional backup) → Internal Server
+```
+
+**Advantages:**
+- No public domain required
+- No NAT/firewall configuration needed
+- Direct connection from internal network to Feishu cloud
+- Real-time message delivery (milliseconds vs minutes)
+- Connection复用,资源效率更高
+
+### Architecture Diagram
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│                     Feishu Open Platform                    │
+│                  (WebSocket Event Subscription)             │
+└───────────────────────┬─────────────────────────────────────┘
+                        │
+          ┌─────────────┴─────────────┐
+          │                           │
+    ┌─────▼──────┐            ┌──────▼─────┐
+    │   Bot A    │            │   Bot B    │
+    │ ws://.../A │            │ ws://.../B │
+    └─────┬──────┘            └──────┬─────┘
+          │                          │
+    ┌─────▼──────────────────────────▼──────┐
+    │            AuraK Server                │
+    │  ┌──────────────────────────────────┐  │
+    │  │      FeishuModule               │  │
+    │  │  ┌────────────────────────────┐ │  │
+    │  │  │  FeishuWsManager           │ │  │
+    │  │  │  - per-bot connections     │ │  │
+    │  │  │  - auto-reconnect         │ │  │
+    │  │  │  - message routing        │ │  │
+    │  │  └────────────────────────────┘ │  │
+    │  │  ┌────────────────────────────┐ │  │
+    │  │  │  FeishuService            │ │  │
+    │  │  │  - existing logic        │ │  │
+    │  │  │  - new ws connect/disconnect│ │  │
+    │  │  └────────────────────────────┘ │  │
+    │  │  ┌────────────────────────────┐ │  │
+    │  │  │  FeishuController         │ │  │
+    │  │  │  - webhook endpoints     │ │  │
+    │  │  │  - ws management APIs    │ │  │
+    │  │  └────────────────────────────┘ │  │
+    │  └──────────────────────────────────┘  │
+    └──────────────────────────────────────────┘
+```
+
+---
+
+## 2. Implementation Plan
+
+### 2.1 New Components
+
+#### 2.1.1 FeishuWsManager
+
+**Purpose**: Manage WebSocket connections for each bot
+
+**Responsibilities:**
+- Establish and maintain WebSocket connections
+- Handle connection lifecycle (connect, disconnect, reconnect)
+- Route incoming messages to appropriate bot handlers
+- Manage connection state per bot
+
+**Location**: `server/src/feishu/feishu-ws.manager.ts`
+
+**Key Methods:**
+```typescript
+class FeishuWsManager {
+  // Start WebSocket connection for a bot
+  async connect(bot: FeishuBot): Promise<void>
+  
+  // Stop WebSocket connection for a bot
+  async disconnect(botId: string): Promise<void>
+  
+  // Get connection status
+  getStatus(botId: string): ConnectionStatus
+  
+  // Get all active connections
+  getAllConnections(): Map<string, ConnectionStatus>
+}
+```
+
+#### 2.1.2 ConnectionStatus Type
+
+```typescript
+enum ConnectionState {
+  DISCONNECTED = 'disconnected',
+  CONNECTING = 'connecting',
+  CONNECTED = 'connected',
+  ERROR = 'error'
+}
+
+interface ConnectionStatus {
+  botId: string
+  state: ConnectionState
+  connectedAt?: Date
+  lastHeartbeat?: Date
+  error?: string
+}
+```
+
+### 2.2 Modified Components
+
+#### 2.2.1 FeishuService
+
+**New Methods:**
+```typescript
+class FeishuService {
+  // Start WebSocket connection for a bot
+  async startWsConnection(botId: string): Promise<void>
+  
+  // Stop WebSocket connection for a bot
+  async stopWsConnection(botId: string): Promise<void>
+  
+  // Get connection status
+  async getWsStatus(botId: string): Promise<ConnectionStatus>
+  
+  // List all connection statuses
+  async getAllWsStatuses(): Promise<ConnectionStatus[]>
+}
+```
+
+#### 2.2.2 FeishuController
+
+**New Endpoints:**
+```typescript
+// POST /feishu/bots/:id/ws/connect - Start WebSocket connection
+// POST /feishu/bots/:id/ws/disconnect - Stop WebSocket connection  
+// GET  /feishu/bots/:id/ws/status - Get connection status
+// GET  /feishu/ws/status - Get all connection statuses
+```
+
+**Modified Endpoints:**
+- Keep existing webhook endpoints unchanged
+- Add WebSocket status indicator in bot list response
+
+#### 2.2.3 FeishuBot Entity
+
+**New Fields:**
+```typescript
+@Entity('feishu_bots')
+export class FeishuBot {
+  // ... existing fields ...
+  
+  @Column({ default: false })
+  useWebSocket: boolean
+  
+  @Column({ nullable: true })
+  wsConnectionState: string
+}
+```
+
+### 2.3 Feishu SDK Integration
+
+**Package**: `@larksuiteoapi/node-sdk`
+
+**Installation:**
+```bash
+cd server && yarn add @larksuiteoapi/node-sdk
+```
+
+**Configuration:**
+```typescript
+import { EventDispatcher, Conf } from '@larksuiteoapi/node-sdk'
+
+const client = new EventDispatcher({
+  appId: bot.appId,
+  appSecret: bot.appSecret,
+  verificationToken: bot.verificationToken,
+}, {
+  logger: console
+})
+```
+
+### 2.4 Event Handling
+
+**Flow for WebSocket Mode:**
+```
+Feishu Cloud ──WebSocket──> FeishuWsManager.on('message')
+                                    │
+                                    ▼
+                            Parse event type
+                                    │
+                    ┌───────────────┼───────────────┐
+                    │               │               │
+              im.message.   im.message.      other
+              receive_v1    p2p_msg_received
+                    │               │
+                    ▼               ▼
+            FeishuService.processChatMessage()
+                                    │
+                                    ▼
+                            Send response via
+                            FeishuService.sendTextMessage()
+```
+
+### 2.5 Configuration Changes
+
+**Feishu Open Platform:**
+
+Users need to configure in Feishu developer console:
+1. Go to "Event Subscription" (事件与回调)
+2. Select "Use long connection to receive events" (使用长连接接收事件)
+3. Add event: `im.message.receive_v1`
+4. **Important**: Must start local WebSocket client first before saving
+
+---
+
+## 3. Data Flow
+
+### WebSocket Message Flow
+
+```
+1. User triggers connect API
+       │
+       ▼
+2. FeishuController.connect(botId)
+       │
+       ▼
+3. FeishuService.startWsConnection(botId)
+       │
+       ▼
+4. FeishuWsManager.connect(bot)
+       │
+       ▼
+5. SDK establishes WebSocket to open.feishu.cn
+       │
+       ▼
+6. Connection established, events flow:
+       │
+       ├─> on('message') ──> _processEvent() ──> _handleMessage()
+       │                                              │
+       │                                              ▼
+       │                                    FeishuService.processChatMessage()
+       │                                              │
+       │                                              ▼
+       │                                    FeishuService.sendTextMessage() (via SDK)
+       │
+       ├─> on('error') ──> log error ──> trigger reconnect
+       │
+       └─> on('close') ──> trigger auto-reconnect
+```
+
+---
+
+## 4. Error Handling
+
+### Connection Errors
+
+| Error Type | Handling |
+|------------|----------|
+| Network timeout | Retry with exponential backoff (max 5 attempts) |
+| Invalid credentials | Mark bot as error state, notify user |
+| Token expired | Refresh token, reconnect |
+| Feishu server error | Wait 30s, retry |
+
+### Auto-Reconnect Strategy
+
+```
+Initial delay: 1 second
+Max delay: 60 seconds
+Backoff multiplier: 2x
+Max attempts: 5
+Reset on successful connection
+```
+
+---
+
+## 5. API Design
+
+### 5.1 Connect WebSocket
+
+```
+POST /api/feishu/bots/:id/ws/connect
+
+Response 200:
+{
+  "success": true,
+  "botId": "bot_xxx",
+  "status": "connecting"
+}
+
+Response 400:
+{
+  "success": false,
+  "error": "Bot not found or disabled"
+}
+```
+
+### 5.2 Disconnect WebSocket
+
+```
+POST /api/feishu/bots/:id/ws/disconnect
+
+Response 200:
+{
+  "success": true,
+  "botId": "bot_xxx",
+  "status": "disconnected"
+}
+```
+
+### 5.3 Get Connection Status
+
+```
+GET /api/feishu/bots/:id/ws/status
+
+Response 200:
+{
+  "botId": "bot_xxx",
+  "state": "connected",
+  "connectedAt": "2026-03-17T10:00:00Z",
+  "lastHeartbeat": "2026-03-17T10:05:00Z"
+}
+```
+
+### 5.4 Get All Statuses
+
+```
+GET /api/feishu/ws/status
+
+Response 200:
+{
+  "connections": [
+    {
+      "botId": "bot_xxx",
+      "state": "connected",
+      "connectedAt": "2026-03-17T10:00:00Z"
+    },
+    {
+      "botId": "bot_yyy",
+      "state": "disconnected"
+    }
+  ]
+}
+```
+
+---
+
+## 6. Security Considerations
+
+1. **Credential Storage**: App ID and App Secret stored encrypted in database
+2. **Connection Validation**: Verify bot belongs to authenticated user before connect
+3. **Rate Limiting**: Implement per-bot message rate limiting
+4. **Connection Limits**: Max 10 concurrent WebSocket connections per server instance
+
+---
+
+## 7. Testing Strategy
+
+### Unit Tests
+- FeishuWsManager connection lifecycle
+- Message routing logic
+- Error handling and reconnection
+
+### Integration Tests
+- Full WebSocket connection flow
+- Message send/receive cycle
+- Reconnection after network failure
+
+### Manual Testing
+- Local development without ngrok
+- Verify webhook still works alongside WebSocket
+
+---
+
+## 8. Backward Compatibility
+
+- **Existing webhook endpoints**: Unchanged, continue to work
+- **Bot configuration**: Existing bots keep webhook mode by default
+- **Migration path**: Users can switch to WebSocket anytime via API
+- **Dual mode**: Both modes can run simultaneously for different bots
+
+---
+
+## 9. Migration Guide
+
+### For Existing Users
+
+1. Update AuraK to new version (with WebSocket support)
+2. Install Feishu SDK: `yarn add @larksuiteoapi/node-sdk`
+3. In Feishu Developer Console:
+   - Start local WebSocket server
+   - Change event subscription to "Use long connection"
+4. Call `POST /api/feishu/bots/:id/ws/connect` to activate
+
+---
+
+## 10. Limitations
+
+1. **Outbound Network Required**: Server must be able to reach `open.feishu.cn` via WebSocket
+2. **Single Connection Per Bot**: Each bot needs its own WebSocket connection
+3. **Feishu SDK Required**: Must install official SDK, cannot use raw WebSocket
+4. **Private Feishu**: Does not support Feishu private deployment (自建飞书)
+
+---
+
+## 11. File Changes Summary
+
+### New Files
+- `server/src/feishu/feishu-ws.manager.ts` - WebSocket connection manager
+- `server/src/feishu/dto/ws-status.dto.ts` - WebSocket status DTOs
+
+### Modified Files
+- `server/src/feishu/feishu.service.ts` - Add WS methods
+- `server/src/feishu/feishu.controller.ts` - Add WS endpoints
+- `server/src/feishu/entities/feishu-bot.entity.ts` - Add WS fields
+- `server/src/feishu/feishu.module.ts` - Register new manager
+
+### Dependencies
+- Add: `@larksuiteoapi/node-sdk`
+
+---
+
+## 12. Success Criteria
+
+- [ ] Server can establish WebSocket connection to Feishu
+- [ ] Messages received via WebSocket are processed correctly
+- [ ] Responses sent back to Feishu via SDK
+- [ ] Auto-reconnect works after network interruption
+- [ ] Webhook mode continues to work unchanged
+- [ ] Both modes can coexist for different bots
+- [ ] Internal network deployment works without public domain

+ 1 - 0
server/package.json

@@ -27,6 +27,7 @@
     "@langchain/langgraph": "^1.0.4",
     "@langchain/openai": "^1.1.3",
     "@langchain/textsplitters": "^1.0.1",
+    "@larksuiteoapi/node-sdk": "^1.59.0",
     "@nestjs/common": "^11.0.1",
     "@nestjs/config": "^4.0.2",
     "@nestjs/core": "^11.0.1",

+ 7 - 0
server/src/app.module.ts

@@ -53,6 +53,9 @@ import { TenantMember } from './tenant/tenant-member.entity';
 import { TenantModule } from './tenant/tenant.module';
 import { SuperAdminModule } from './super-admin/super-admin.module';
 import { AdminModule } from './admin/admin.module';
+import { FeishuModule } from './feishu/feishu.module';
+import { FeishuBot } from './feishu/entities/feishu-bot.entity';
+
 
 @Module({
   imports: [
@@ -91,6 +94,8 @@ import { AdminModule } from './admin/admin.module';
           TenantSetting,
           TenantMember,
           ApiKey,
+          FeishuBot,
+
         ],
         synchronize: true, // Auto-create database schema. Disable in production.
       }),
@@ -118,6 +123,8 @@ import { AdminModule } from './admin/admin.module';
     AssessmentModule,
     SuperAdminModule,
     AdminModule,
+    FeishuModule,
+
   ],
   controllers: [AppController],
   providers: [

+ 11 - 2
server/src/auth/combined-auth.guard.ts

@@ -7,6 +7,8 @@ import { lastValueFrom, Observable } from 'rxjs';
 import { IS_PUBLIC_KEY } from './public.decorator';
 import { tenantStore } from '../tenant/tenant.store';
 import { UserRole } from '../user/user-role.enum';
+import * as fs from 'fs';
+import * as path from 'path';
 
 /**
  * A combined authentication guard that accepts either:
@@ -35,9 +37,16 @@ export class CombinedAuthGuard implements CanActivate {
             context.getHandler(),
             context.getClass(),
         ]);
-        if (isPublic) return true;
-
+        
         const request = context.switchToHttp().getRequest<Request & { user?: any; tenantId?: string }>();
+        const logMsg = `\n[${new Date().toISOString()}] AuthGuard: ${request.method} ${request.url} (isPublic: ${isPublic})\n`;
+        fs.appendFileSync('auth_debug.log', logMsg);
+
+        if (isPublic) {
+            return true;
+        }
+
+        console.log(`[CombinedAuthGuard] Checking auth for route: ${request.method} ${request.url}`);
 
         // --- Try API Key first ---
         const apiKey = this.extractApiKey(request);

+ 11 - 0
server/src/feishu/dto/bind-bot.dto.ts

@@ -0,0 +1,11 @@
+import { IsString, IsNotEmpty, IsUUID } from 'class-validator';
+
+export class BindFeishuBotDto {
+    @IsUUID()
+    @IsNotEmpty()
+    botId: string;
+
+    @IsString()
+    @IsNotEmpty()
+    verificationCode?: string; // Optional: used to validate the binding relationship
+}

+ 27 - 0
server/src/feishu/dto/create-bot.dto.ts

@@ -0,0 +1,27 @@
+import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
+
+export class CreateFeishuBotDto {
+    @IsString()
+    @IsNotEmpty()
+    appId: string;
+
+    @IsString()
+    @IsNotEmpty()
+    appSecret: string;
+
+    @IsString()
+    @IsOptional()
+    verificationToken?: string;
+
+    @IsString()
+    @IsOptional()
+    encryptKey?: string;
+
+    @IsString()
+    @IsOptional()
+    botName?: string;
+
+    @IsBoolean()
+    @IsOptional()
+    enabled?: boolean;
+}

+ 20 - 0
server/src/feishu/dto/webhook.dto.ts

@@ -0,0 +1,20 @@
+import { IsString, IsOptional } from 'class-validator';
+
+export class CreateSignatureDto {
+    @IsString()
+    @IsOptional()
+    timestamp?: string;
+
+    @IsString()
+    @IsOptional()
+    nonce?: string;
+}
+
+export class VerifyWebhookDto {
+    @IsString()
+    token: string;
+
+    @IsString()
+    @IsOptional()
+    challenge?: string;
+}

+ 39 - 0
server/src/feishu/dto/ws-status.dto.ts

@@ -0,0 +1,39 @@
+export enum ConnectionState {
+  DISCONNECTED = 'disconnected',
+  CONNECTING = 'connecting',
+  CONNECTED = 'connected',
+  ERROR = 'error',
+}
+
+export interface ConnectionStatus {
+  botId: string;
+  state: ConnectionState;
+  connectedAt?: Date;
+  lastHeartbeat?: Date;
+  error?: string;
+}
+
+export class WsStatusResponseDto {
+  botId: string;
+  state: ConnectionState;
+  connectedAt?: string;
+  lastHeartbeat?: string;
+  error?: string;
+}
+
+export class WsConnectResponseDto {
+  success: boolean;
+  botId: string;
+  status: ConnectionState | string;
+  error?: string;
+}
+
+export class WsDisconnectResponseDto {
+  success: boolean;
+  botId: string;
+  status: ConnectionState | string;
+}
+
+export class AllWsStatusResponseDto {
+  connections: WsStatusResponseDto[];
+}

+ 65 - 0
server/src/feishu/entities/feishu-bot.entity.ts

@@ -0,0 +1,65 @@
+import {
+    Entity,
+    PrimaryGeneratedColumn,
+    Column,
+    CreateDateColumn,
+    UpdateDateColumn,
+    ManyToOne,
+    JoinColumn,
+} from 'typeorm';
+import { User } from '../../user/user.entity';
+
+@Entity('feishu_bots')
+export class FeishuBot {
+    @PrimaryGeneratedColumn('uuid')
+    id: string;
+
+    @Column({ name: 'user_id' })
+    userId: string;
+
+    @ManyToOne(() => User, { onDelete: 'CASCADE' })
+    @JoinColumn({ name: 'user_id' })
+    user: User;
+
+    @Column({ name: 'app_id', length: 64 })
+    appId: string;
+
+    @Column({ name: 'app_secret', length: 256 })
+    appSecret: string;
+
+    @Column({ name: 'tenant_access_token', nullable: true, type: 'text' })
+    tenantAccessToken: string;
+
+    @Column({ name: 'token_expires_at', nullable: true, type: 'datetime' })
+    tokenExpiresAt: Date;
+
+    @Column({ name: 'verification_token', nullable: true, length: 128 })
+    verificationToken: string;
+
+    @Column({ name: 'encrypt_key', nullable: true, length: 256 })
+    encryptKey: string;
+
+    @Column({ name: 'bot_name', nullable: true, length: 128 })
+    botName: string;
+
+    @Column({ default: true })
+    enabled: boolean;
+
+    @Column({ name: 'is_default', default: false })
+    isDefault: boolean;
+
+    @Column({ name: 'webhook_url', nullable: true, type: 'text' })
+    webhookUrl: string;
+
+    @Column({ name: 'use_web_socket', default: false })
+    useWebSocket: boolean;
+
+    @Column({ name: 'ws_connection_state', nullable: true, length: 32 })
+    wsConnectionState: string;
+
+    @CreateDateColumn({ name: 'created_at' })
+    createdAt: Date;
+
+    @UpdateDateColumn({ name: 'updated_at' })
+    updatedAt: Date;
+}

+ 218 - 0
server/src/feishu/feishu-ws.manager.ts

@@ -0,0 +1,218 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { WSClient, EventDispatcher, LoggerLevel } from '@larksuiteoapi/node-sdk';
+import { FeishuBot } from './entities/feishu-bot.entity';
+import { ConnectionState, ConnectionStatus } from './dto/ws-status.dto';
+
+interface BotConnection {
+  client: WSClient;
+  status: ConnectionStatus;
+}
+
+@Injectable()
+export class FeishuWsManager {
+  private readonly logger = new Logger(FeishuWsManager.name);
+  private connections: Map<string, BotConnection> = new Map();
+  private reconnectAttempts: Map<string, number> = new Map();
+  private readonly MAX_RECONNECT_ATTEMPTS = 5;
+  private readonly RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000];
+
+  // Injected after construction to avoid circular dep
+  private _feishuService: any;
+
+  setFeishuService(service: any): void {
+    this._feishuService = service;
+  }
+
+  /**
+   * Start WebSocket connection for a bot
+   */
+  async connect(bot: FeishuBot): Promise<void> {
+    const botId = bot.id;
+
+    // Check if already connected or connecting
+    const existing = this.connections.get(botId);
+    if (existing && (existing.status.state === ConnectionState.CONNECTED || existing.status.state === ConnectionState.CONNECTING)) {
+      this.logger.warn(`Bot ${botId} is already connecting or connected`);
+      return;
+    }
+
+    // Mark as connecting immediately to prevent race conditions
+    this.connections.set(botId, {
+      client: null as any,
+      status: { botId, state: ConnectionState.CONNECTING }
+    });
+
+    try {
+      const wsClient = new WSClient({
+        appId: bot.appId,
+        appSecret: bot.appSecret,
+        loggerLevel: LoggerLevel.info,
+        logger: {
+          debug: (msg: string) => this.logger.debug(msg),
+          info: (msg: string) => this.logger.log(msg),
+          warn: (msg: string) => this.logger.warn(msg),
+          error: (msg: string) => this.logger.error(msg),
+          trace: (msg: string) => this.logger.verbose(msg),
+        },
+      });
+
+      // Register the main message event handler
+      wsClient.start({
+        eventDispatcher: new EventDispatcher({}).register({
+          'im.message.receive_v1': async (data: any) => {
+            await this._handleMessage(bot, data);
+          },
+        }),
+      });
+
+      const conn = this.connections.get(botId);
+      if (conn) {
+        conn.client = wsClient;
+        conn.status = {
+          botId,
+          state: ConnectionState.CONNECTED,
+          connectedAt: new Date(),
+        };
+      }
+
+      this.reconnectAttempts.set(botId, 0);
+      this.logger.log(`WebSocket connected for bot ${botId}`);
+    } catch (error: any) {
+      this.logger.error(`Failed to connect WebSocket for bot ${botId}`, error);
+      this._setStatus(botId, {
+        botId,
+        state: ConnectionState.ERROR,
+        error: error?.message || 'Connection failed',
+      });
+      throw error;
+    }
+  }
+
+  /**
+   * Disconnect WebSocket for a bot
+   */
+  async disconnect(botId: string): Promise<void> {
+    const connection = this.connections.get(botId);
+    if (!connection) {
+      this.logger.warn(`No connection found for bot ${botId}`);
+      return;
+    }
+
+    try {
+      // Lark.WSClient does not expose a public stop() in all versions;
+      // we simply remove the reference and let GC clean up.
+      this.connections.delete(botId);
+      this.reconnectAttempts.delete(botId);
+      this.logger.log(`WebSocket disconnected for bot ${botId}`);
+    } catch (error) {
+      this.logger.error(`Error disconnecting bot ${botId}`, error);
+      throw error;
+    }
+  }
+
+  /**
+   * Get connection status for a specific bot
+   */
+  getStatus(botId: string): ConnectionStatus | null {
+    return this.connections.get(botId)?.status ?? null;
+  }
+
+  /**
+   * Get all active connection statuses
+   */
+  getAllStatuses(): ConnectionStatus[] {
+    return Array.from(this.connections.values()).map((c) => c.status);
+  }
+
+  /**
+   * Returns true if the bot has an active connection
+   */
+  isConnected(botId: string): boolean {
+    return this.connections.get(botId)?.status.state === ConnectionState.CONNECTED;
+  }
+
+  // ─── Private Helpers ──────────────────────────────────────────────────────
+
+  private _setStatus(botId: string, status: ConnectionStatus): void {
+    const connection = this.connections.get(botId);
+    if (connection) {
+      connection.status = status;
+    }
+    // If connection not stored yet (during initial connect), we just skip —
+    // the status will be set when the connection map entry is created.
+  }
+
+  /**
+   * Handle an incoming im.message.receive_v1 event from Feishu WebSocket.
+   */
+  private async _handleMessage(bot: FeishuBot, data: any): Promise<void> {
+    this.logger.log(`Received WS message for bot ${bot.id}`);
+
+    try {
+      const event = data?.event ?? data;
+      const message = event?.message;
+
+      if (!message) {
+        this.logger.warn('No message field in WS event');
+        return;
+      }
+
+      const messageId: string = message.message_id;
+      const openId: string | undefined = event?.sender?.sender_id?.open_id;
+
+      if (!openId) {
+        this.logger.warn('No sender open_id in WS event');
+        return;
+      }
+
+      let userText = '';
+      try {
+        const content = JSON.parse(message.content || '{}');
+        userText = content.text || '';
+      } catch {
+        this.logger.warn('Failed to parse WS message content');
+        return;
+      }
+
+      if (!userText.trim()) return;
+
+      if (this._feishuService) {
+        await this._feishuService.processChatMessage(bot, openId, messageId, userText);
+      } else {
+        this.logger.error('FeishuService not injected into FeishuWsManager');
+      }
+    } catch (error) {
+      this.logger.error('Error in _handleMessage', error);
+    }
+  }
+
+  /**
+   * Schedule an auto-reconnect with exponential backoff.
+   */
+  async attemptReconnect(bot: FeishuBot): Promise<void> {
+    const botId = bot.id;
+    const attempts = this.reconnectAttempts.get(botId) ?? 0;
+
+    if (attempts >= this.MAX_RECONNECT_ATTEMPTS) {
+      this.logger.error(`Max reconnect attempts reached for bot ${botId}`);
+      this._setStatus(botId, {
+        botId,
+        state: ConnectionState.ERROR,
+        error: 'Max reconnect attempts reached',
+      });
+      return;
+    }
+
+    const delay = this.RECONNECT_DELAYS[attempts] ?? this.RECONNECT_DELAYS[this.RECONNECT_DELAYS.length - 1];
+    this.logger.log(`Reconnecting bot ${botId} in ${delay}ms (attempt ${attempts + 1})`);
+    this.reconnectAttempts.set(botId, attempts + 1);
+
+    setTimeout(async () => {
+      try {
+        await this.connect(bot);
+      } catch (error) {
+        this.logger.error(`Reconnect failed for bot ${botId}`, error);
+      }
+    }, delay);
+  }
+}

+ 271 - 0
server/src/feishu/feishu.controller.ts

@@ -0,0 +1,271 @@
+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 { 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) {}
+
+    // ─── 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);
+        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, 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 {
+            // Delegate to FeishuService which calls ChatService RAG pipeline (Chunk 5)
+            await this.feishuService.processChatMessage(bot, openId, messageId, userText);
+        } catch (error) {
+            this.logger.error('processChatMessage failed', error);
+            try {
+                await this.feishuService.sendTextMessage(
+                    bot,
+                    'open_id',
+                    openId,
+                    '抱歉,处理您的消息时遇到了错误,请稍后重试。',
+                );
+            } catch (sendError) {
+                this.logger.error('Failed to send error message to Feishu', sendError);
+            }
+        }
+    }
+}

+ 23 - 0
server/src/feishu/feishu.module.ts

@@ -0,0 +1,23 @@
+import { Module, forwardRef } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { FeishuController } from './feishu.controller';
+import { FeishuService } from './feishu.service';
+import { FeishuBot } from './entities/feishu-bot.entity';
+import { FeishuWsManager } from './feishu-ws.manager';
+import { ChatModule } from '../chat/chat.module';
+import { UserModule } from '../user/user.module';
+import { ModelConfigModule } from '../model-config/model-config.module';
+
+@Module({
+    imports: [
+        TypeOrmModule.forFeature([FeishuBot]),
+        forwardRef(() => ChatModule),
+        forwardRef(() => UserModule),
+        forwardRef(() => ModelConfigModule),
+    ],
+    controllers: [FeishuController],
+    providers: [FeishuService, FeishuWsManager],
+    exports: [FeishuService, TypeOrmModule],
+})
+export class FeishuModule {}
+

+ 401 - 0
server/src/feishu/feishu.service.ts

@@ -0,0 +1,401 @@
+import { Injectable, Logger, forwardRef, Inject, OnModuleInit } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import axios from 'axios';
+import { FeishuBot } from './entities/feishu-bot.entity';
+import { CreateFeishuBotDto } from './dto/create-bot.dto';
+import { ChatService } from '../chat/chat.service';
+import { ModelConfigService } from '../model-config/model-config.service';
+import { UserService } from '../user/user.service';
+import { ModelType } from '../types';
+import { FeishuWsManager } from './feishu-ws.manager';
+import { ConnectionStatus } from './dto/ws-status.dto';
+
+@Injectable()
+export class FeishuService implements OnModuleInit {
+    private readonly logger = new Logger(FeishuService.name);
+    private readonly feishuApiBase = 'https://open.feishu.cn/open-apis';
+
+    constructor(
+        @InjectRepository(FeishuBot)
+        private botRepository: Repository<FeishuBot>,
+        @Inject(forwardRef(() => ChatService))
+        private chatService: ChatService,
+        @Inject(forwardRef(() => ModelConfigService))
+        private modelConfigService: ModelConfigService,
+        @Inject(forwardRef(() => UserService))
+        private userService: UserService,
+        private wsManager: FeishuWsManager,
+    ) {}
+
+    onModuleInit(): void {
+        // Break circular dep: inject self into manager after module is ready
+        this.wsManager.setFeishuService(this);
+    }
+
+    // ─── Bot CRUD ────────────────────────────────────────────────────────────────
+
+    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);
+    }
+
+    async getUserBots(userId: string): Promise<FeishuBot[]> {
+        return this.botRepository.find({ where: { userId } });
+    }
+
+    async getBotById(botId: string): Promise<FeishuBot | null> {
+        return this.botRepository.findOne({ where: { id: botId } });
+    }
+
+    async getBotByAppId(appId: string): Promise<FeishuBot | null> {
+        return this.botRepository.findOne({ where: { appId } });
+    }
+
+    async setBotEnabled(botId: string, enabled: boolean): Promise<FeishuBot> {
+        const bot = await this.botRepository.findOne({ where: { id: botId } });
+        if (!bot) throw new Error('Bot not found');
+        bot.enabled = enabled;
+        return this.botRepository.save(bot);
+    }
+
+    async deleteBot(userId: string, botId: string): Promise<void> {
+        await this.botRepository.delete({ id: botId, userId });
+    }
+
+    // ─── Feishu API Calls ─────────────────────────────────────────────────────────
+
+    /**
+     * Get or refresh tenant_access_token, cached per bot in DB
+     */
+    async getValidToken(bot: FeishuBot): Promise<string> {
+        if (
+            bot.tokenExpiresAt &&
+            bot.tenantAccessToken &&
+            new Date(bot.tokenExpiresAt) > new Date(Date.now() + 30 * 60 * 1000)
+        ) {
+            return bot.tenantAccessToken;
+        }
+
+        this.logger.log(`Refreshing access token for bot: ${bot.appId}`);
+        const { data } = await axios.post<{
+            code: number;
+            msg: string;
+            tenant_access_token: string;
+            expire: number;
+        }>(`${this.feishuApiBase}/auth/v3/tenant_access_token/internal`, {
+            app_id: bot.appId,
+            app_secret: bot.appSecret,
+        });
+
+        if (data.code !== 0) {
+            throw new Error(`Failed to get Feishu token: ${data.msg}`);
+        }
+
+        bot.tenantAccessToken = data.tenant_access_token;
+        bot.tokenExpiresAt = new Date(Date.now() + data.expire * 1000);
+        await this.botRepository.save(bot);
+
+        return data.tenant_access_token;
+    }
+
+    /**
+     * Send a card message to a Feishu user
+     */
+    async sendCardMessage(
+        bot: FeishuBot,
+        receiveIdType: 'open_id' | 'user_id' | 'chat_id',
+        receiveId: string,
+        card: any,
+    ): Promise<string> {
+        const token = await this.getValidToken(bot);
+
+        const { data } = await axios.post<{
+            code: number;
+            msg: string;
+            data: { message_id: string };
+        }>(
+            `${this.feishuApiBase}/im/v1/messages?receive_id_type=${receiveIdType}`,
+            {
+                receive_id: receiveId,
+                msg_type: 'interactive',
+                content: JSON.stringify(card),
+            },
+            { headers: { Authorization: `Bearer ${token}` } },
+        );
+
+        if (data.code !== 0) {
+            throw new Error(`Failed to send Feishu card: ${data.msg}`);
+        }
+
+        return data.data.message_id;
+    }
+
+    /**
+     * Send a simple text message to a Feishu user
+     */
+    async sendTextMessage(
+        bot: FeishuBot,
+        receiveIdType: 'open_id' | 'user_id' | 'chat_id',
+        receiveId: string,
+        text: string,
+    ): Promise<string> {
+        const token = await this.getValidToken(bot);
+
+        const { data } = await axios.post<{
+            code: number;
+            msg: string;
+            data: { message_id: string };
+        }>(
+            `${this.feishuApiBase}/im/v1/messages?receive_id_type=${receiveIdType}`,
+            {
+                receive_id: receiveId,
+                msg_type: 'text',
+                content: JSON.stringify({ text }),
+            },
+            { headers: { Authorization: `Bearer ${token}` } },
+        );
+
+        if (data.code !== 0) {
+            throw new Error(`Failed to send Feishu message: ${data.msg}`);
+        }
+
+        return data.data.message_id;
+    }
+
+    /**
+     * Update an already-sent message (supports interactive cards)
+     */
+    async updateMessage(bot: FeishuBot, messageId: string, content: any, msgType: 'text' | 'interactive' = 'interactive'): Promise<void> {
+        const token = await this.getValidToken(bot);
+
+        const { data } = await axios.patch<{ code: number; msg: string }>(
+            `${this.feishuApiBase}/im/v1/messages/${messageId}`,
+            { msg_type: msgType, content: JSON.stringify(content) },
+            { headers: { Authorization: `Bearer ${token}` } },
+        );
+
+        if (data.code !== 0) {
+            this.logger.warn(`Failed to update Feishu message: ${data.msg}`);
+        }
+    }
+
+    /**
+     * Build a professional Feishu card
+     */
+    private buildFeishuCard(content: string, title = 'AuraK AI 助手', isFinal = false) {
+        return {
+            config: {
+                wide_screen_mode: true,
+            },
+            header: {
+                template: isFinal ? 'blue' : 'orange',
+                title: {
+                    content: title + (isFinal ? '' : ' (正在生成...)'),
+                    tag: 'plain_text',
+                },
+            },
+            elements: [
+                {
+                    tag: 'div',
+                    text: {
+                        content: content || '...',
+                        tag: 'lark_md',
+                    },
+                },
+                {
+                    tag: 'hr',
+                },
+                {
+                    tag: 'note',
+                    elements: [
+                        {
+                            content: `由 AuraK 知识库驱动 · ${new Date().toLocaleTimeString()}`,
+                            tag: 'plain_text',
+                        },
+                    ],
+                },
+            ],
+        };
+    }
+
+    // ─── Chunk 5: ChatService RAG Integration ─────────────────────────────────────
+
+    private processedMessages = new Map<string, { time: number; responseId?: string }>();
+
+    /**
+     * Process a user message via the AuraK RAG pipeline and send the result back.
+     * This is the core of the Feishu integration.
+     */
+    async processChatMessage(
+        bot: FeishuBot,
+        openId: string,
+        messageId: string,
+        userMessage: string,
+    ): Promise<void> {
+        this.logger.log(`Processing Feishu message [${messageId}] for bot ${bot.appId}`);
+
+        // 1. Deduplication: check if we are already processing this message
+        const now = Date.now();
+        const existing = this.processedMessages.get(messageId);
+        if (existing && now - existing.time < 1000 * 60 * 10) {
+            this.logger.warn(`Ignoring duplicate Feishu message: ${messageId}`);
+            return;
+        }
+
+        // Mark as being processed
+        this.processedMessages.set(messageId, { time: now });
+
+        // Cleanup old cache (simple)
+        if (this.processedMessages.size > 1000) {
+            for (const [key, val] of this.processedMessages) {
+                if (now - val.time > 1000 * 60 * 30) this.processedMessages.delete(key);
+            }
+        }
+
+        // Get user from bot owner
+        const userId = bot.userId;
+        const user = await this.userService.findOneById(userId);
+        const tenantId = user?.tenantId || 'default';
+        const language = user?.userSetting?.language || 'zh';
+
+        // Get the user's default LLM model
+        const llmModel = await this.modelConfigService.findDefaultByType(tenantId, ModelType.LLM);
+
+        if (!llmModel) {
+            await this.sendTextMessage(bot, 'open_id', openId, '❌ 请先在 AuraK 中配置 LLM 模型才能使用机器人。');
+            return;
+        }
+
+        // Send initial "thinking" card
+        const cardTitle = 'AuraK 知识检索';
+        const initialCard = this.buildFeishuCard('⏳ 正在检索知识库,请稍候...', cardTitle, false);
+        const msgId = await this.sendCardMessage(bot, 'open_id', openId, initialCard);
+        
+        // Save the response message ID for potential future deduplication debugging
+        this.processedMessages.set(messageId, { time: now, responseId: msgId });
+
+        // Run the RAG pipeline in the background so we don't block the Feishu event handler
+        // This prevents Feishu from retrying the event if it takes > 3s.
+        this._runRagBackground(bot, msgId, userMessage, userId, llmModel, language, tenantId, cardTitle);
+    }
+
+    /**
+     * Internal background task for RAG processing
+     */
+    private async _runRagBackground(
+        bot: FeishuBot,
+        msgId: string,
+        userMessage: string,
+        userId: string,
+        llmModel: any,
+        language: string,
+        tenantId: string,
+        cardTitle: string,
+    ) {
+        let fullResponse = '';
+        let lastUpdateTime = Date.now();
+        const UPDATE_INTERVAL = 1500;
+
+        try {
+            // Stream from ChatService RAG pipeline
+            const stream = this.chatService.streamChat(
+                userMessage,
+                [],
+                userId,
+                llmModel as any,
+                language,
+                undefined,
+                undefined,
+                undefined,
+                undefined,
+                false,
+                undefined,
+                undefined,
+                undefined,
+                10,
+                0.7,
+                undefined,
+                undefined,
+                undefined,
+                tenantId,
+            );
+
+            for await (const chunk of stream) {
+                if (chunk.type === 'content') {
+                    fullResponse += chunk.data;
+
+                    const now = Date.now();
+                    if (now - lastUpdateTime > UPDATE_INTERVAL && fullResponse.length > 50) {
+                        const loadingCard = this.buildFeishuCard(fullResponse, cardTitle, false);
+                        await this.updateMessage(bot, msgId, loadingCard);
+                        lastUpdateTime = now;
+                    }
+                }
+            }
+        } catch (err) {
+            this.logger.error('RAG stream error for Feishu message', err);
+            fullResponse = `抱歉,处理您的问题时遇到了错误:${err?.message || '未知错误'}。`;
+        }
+
+        const MAX_LENGTH = 4500;
+        const finalContent = fullResponse.length > MAX_LENGTH
+            ? fullResponse.substring(0, MAX_LENGTH) + '\n\n...(内容过长,已截断)'
+            : fullResponse || '抱歉,未能生成有效回复,请稍后再试。';
+
+        const finalCard = this.buildFeishuCard(finalContent, cardTitle, true);
+        await this.updateMessage(bot, msgId, finalCard);
+    }
+
+    // ─── WebSocket Connection Management ─────────────────────────────────────────
+
+    /**
+     * Start WebSocket connection for a bot
+     */
+    async startWsConnection(botId: string): Promise<void> {
+        const bot = await this.getBotById(botId);
+        if (!bot) throw new Error('Bot not found');
+        if (!bot.enabled) throw new Error('Bot is disabled');
+
+        bot.useWebSocket = true;
+        await this.botRepository.save(bot);
+
+        await this.wsManager.connect(bot);
+    }
+
+    /**
+     * Stop WebSocket connection for a bot
+     */
+    async stopWsConnection(botId: string): Promise<void> {
+        const bot = await this.getBotById(botId);
+        if (!bot) throw new Error('Bot not found');
+
+        bot.useWebSocket = false;
+        await this.botRepository.save(bot);
+
+        await this.wsManager.disconnect(botId);
+    }
+
+    /**
+     * Get WebSocket connection status for a specific bot
+     */
+    async getWsStatus(botId: string): Promise<ConnectionStatus | null> {
+        return this.wsManager.getStatus(botId);
+    }
+
+    /**
+     * Get all WebSocket connection statuses
+     */
+    async getAllWsStatuses(): Promise<ConnectionStatus[]> {
+        return this.wsManager.getAllStatuses();
+    }
+}

文件差異過大導致無法顯示
+ 0 - 0
server/tsconfig.build.tsbuildinfo


+ 2 - 2
web/src/components/layouts/WorkspaceLayout.tsx

@@ -155,13 +155,13 @@ const WorkspaceLayout: React.FC = () => {
                             isActive={location.pathname.startsWith('/agents')}
                             onClick={handleNavClick}
                         />
-                        {/* <SidebarItem
+                        <SidebarItem
                             icon={Blocks}
                             label={t('navPlugin')}
                             path="/plugins"
                             isActive={location.pathname.startsWith('/plugins')}
                             onClick={handleNavClick}
-                        /> */}
+                        />
 
                         <SidebarItem
                             icon={Database}

+ 509 - 0
web/src/components/plugins/FeishuPluginConfig.tsx

@@ -0,0 +1,509 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { Plus, Trash2, Copy, Check, RefreshCcw, ToggleLeft, ToggleRight, ExternalLink, Wifi, WifiOff, Loader2 } from 'lucide-react';
+import { useAuth } from '../../contexts/AuthContext';
+import { useLanguage } from '../../../contexts/LanguageContext';
+
+interface FeishuBotInfo {
+    id: string;
+    appId: string;
+    botName?: string;
+    enabled: boolean;
+    webhookUrl: string;
+    createdAt: string;
+}
+
+type WsState = 'disconnected' | 'connecting' | 'connected' | 'error';
+
+interface WsStatus {
+    botId: string;
+    state: WsState;
+    connectedAt?: string;
+    lastHeartbeat?: string;
+    error?: string;
+}
+
+export const FeishuPluginConfig: React.FC = () => {
+    const { apiKey } = useAuth();
+    const { t } = useLanguage();
+    const [bots, setBots] = useState<FeishuBotInfo[]>([]);
+    const [loading, setLoading] = useState(true);
+    const [showForm, setShowForm] = useState(false);
+    const [submitting, setSubmitting] = useState(false);
+    const [copiedId, setCopiedId] = useState<string | null>(null);
+    const [wsStatuses, setWsStatuses] = useState<Record<string, WsStatus>>({});
+    const [wsLoading, setWsLoading] = useState<Record<string, boolean>>({});
+    const [form, setForm] = useState({
+        appId: '',
+        appSecret: '',
+        botName: '',
+        verificationToken: '',
+        encryptKey: '',
+    });
+
+    const fetchBots = useCallback(async () => {
+        setLoading(true);
+        try {
+            const res = await fetch('/api/feishu/bots', {
+                headers: { 'x-api-key': apiKey },
+            });
+            if (res.ok) {
+                const data = await res.json();
+                setBots(data);
+            }
+        } catch (e) {
+            console.error('Failed to fetch Feishu bots', e);
+        } finally {
+            setLoading(false);
+        }
+    }, [apiKey]);
+
+    const fetchWsStatuses = useCallback(async () => {
+        try {
+            const res = await fetch('/api/feishu/ws/status', {
+                headers: { 'x-api-key': apiKey },
+            });
+            if (res.ok) {
+                const data = await res.json();
+                const map: Record<string, WsStatus> = {};
+                for (const s of (data.connections ?? [])) {
+                    map[s.botId] = s;
+                }
+                setWsStatuses(map);
+            }
+        } catch (e) {
+            console.error('Failed to fetch WS statuses', e);
+        }
+    }, [apiKey]);
+
+    useEffect(() => {
+        fetchBots();
+    }, [fetchBots]);
+
+    useEffect(() => {
+        fetchWsStatuses();
+    }, [fetchWsStatuses]);
+
+    const handleSubmit = async (e: React.FormEvent) => {
+        e.preventDefault();
+        if (!form.appId || !form.appSecret) return;
+        setSubmitting(true);
+        try {
+            const res = await fetch('/api/feishu/bots', {
+                method: 'POST',
+                headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
+                body: JSON.stringify(form),
+            });
+            if (res.ok) {
+                await fetchBots();
+                setShowForm(false);
+                setForm({ appId: '', appSecret: '', botName: '', verificationToken: '', encryptKey: '' });
+            }
+        } catch (e) {
+            console.error('Failed to create bot', e);
+        } finally {
+            setSubmitting(false);
+        }
+    };
+
+    const handleDelete = async (botId: string) => {
+        if (!window.confirm(t('feishuConfirmDelete'))) return;
+        try {
+            await fetch(`/api/feishu/bots/${botId}`, {
+                method: 'DELETE',
+                headers: { 'x-api-key': apiKey },
+            });
+            await fetchBots();
+        } catch (e) {
+            console.error('Failed to delete bot', e);
+        }
+    };
+
+    const handleToggle = async (bot: FeishuBotInfo) => {
+        try {
+            await fetch(`/api/feishu/bots/${bot.id}/toggle`, {
+                method: 'PATCH',
+                headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
+                body: JSON.stringify({ enabled: !bot.enabled }),
+            });
+            await fetchBots();
+        } catch (e) {
+            console.error('Failed to toggle bot', e);
+        }
+    };
+
+    const handleWsConnect = async (botId: string) => {
+        setWsLoading((prev) => ({ ...prev, [botId]: true }));
+        try {
+            const res = await fetch(`/api/feishu/bots/${botId}/ws/connect`, {
+                method: 'POST',
+                headers: { 'x-api-key': apiKey },
+            });
+            if (res.ok) {
+                setWsStatuses((prev) => ({
+                    ...prev,
+                    [botId]: { botId, state: 'connecting' },
+                }));
+                // Poll for status after a short delay
+                setTimeout(async () => {
+                    await fetchWsStatuses();
+                    setWsLoading((prev) => ({ ...prev, [botId]: false }));
+                }, 2000);
+            } else {
+                setWsLoading((prev) => ({ ...prev, [botId]: false }));
+            }
+        } catch (e) {
+            console.error('Failed to connect WS', e);
+            setWsLoading((prev) => ({ ...prev, [botId]: false }));
+        }
+    };
+
+    const handleWsDisconnect = async (botId: string) => {
+        setWsLoading((prev) => ({ ...prev, [botId]: true }));
+        try {
+            const res = await fetch(`/api/feishu/bots/${botId}/ws/disconnect`, {
+                method: 'POST',
+                headers: { 'x-api-key': apiKey },
+            });
+            if (res.ok) {
+                setWsStatuses((prev) => ({
+                    ...prev,
+                    [botId]: { botId, state: 'disconnected' },
+                }));
+            }
+        } catch (e) {
+            console.error('Failed to disconnect WS', e);
+        } finally {
+            setWsLoading((prev) => ({ ...prev, [botId]: false }));
+        }
+    };
+
+    const copyWebhookUrl = (url: string, id: string) => {
+        const fullUrl = `${window.location.origin}${url}`;
+        navigator.clipboard.writeText(fullUrl);
+        setCopiedId(id);
+        setTimeout(() => setCopiedId(null), 2000);
+    };
+
+    const getWsStateColor = (state: WsState) => {
+        switch (state) {
+            case 'connected': return 'text-emerald-500';
+            case 'connecting': return 'text-amber-500';
+            case 'error': return 'text-red-500';
+            default: return 'text-slate-400';
+        }
+    };
+
+    const getWsStateBg = (state: WsState) => {
+        switch (state) {
+            case 'connected': return 'bg-emerald-50 text-emerald-700 border-emerald-200';
+            case 'connecting': return 'bg-amber-50 text-amber-700 border-amber-200';
+            case 'error': return 'bg-red-50 text-red-700 border-red-200';
+            default: return 'bg-slate-50 text-slate-500 border-slate-200';
+        }
+    };
+
+    const getWsStateLabel = (state: WsState) => {
+        switch (state) {
+            case 'connected': return t('feishuWsConnected');
+            case 'connecting': return t('feishuWsConnecting');
+            case 'error': return t('feishuWsError');
+            default: return t('feishuWsDisconnected');
+        }
+    };
+
+    return (
+        <div className="max-w-3xl">
+            {/* Header */}
+            <div className="flex items-center justify-between mb-6">
+                <div className="flex items-center gap-3">
+                    <span className="text-3xl">🪶</span>
+                    <div>
+                        <h2 className="text-xl font-bold text-slate-900">{t('pluginFeishuName')}</h2>
+                        <p className="text-sm text-slate-500 mt-0.5">{t('pluginFeishuDesc')}</p>
+                    </div>
+                </div>
+                <div className="flex items-center gap-2">
+                    <button
+                        onClick={() => { fetchBots(); fetchWsStatuses(); }}
+                        className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
+                        title={t('refresh')}
+                    >
+                        <RefreshCcw size={16} />
+                    </button>
+                    <button
+                        onClick={() => setShowForm(!showForm)}
+                        className="flex items-center gap-2 px-4 h-9 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-semibold transition-colors"
+                    >
+                        <Plus size={15} />
+                        {t('feishuAddBot')}
+                    </button>
+                </div>
+            </div>
+
+            {/* Setup Guide */}
+            <div className="bg-indigo-50 border border-indigo-200 rounded-xl p-4 mb-6 text-sm text-indigo-800">
+                <p className="font-semibold mb-2">📌 {t('feishuSetupGuide')}</p>
+                <ol className="list-decimal list-inside space-y-1 text-indigo-700">
+                    <li>{t('feishuStep1')}</li>
+                    <li>{t('feishuStep2')}</li>
+                    <li>{t('feishuStep3')}</li>
+                    <li>{t('feishuStep4')}</li>
+                </ol>
+                <a
+                    href="https://open.feishu.cn/document/faq/bot"
+                    target="_blank"
+                    rel="noreferrer"
+                    className="inline-flex items-center gap-1 mt-2 text-indigo-600 hover:underline font-medium"
+                >
+                    {t('feishuDocs')} <ExternalLink size={12} />
+                </a>
+            </div>
+
+            {/* Add Bot Form */}
+            {showForm && (
+                <form
+                    onSubmit={handleSubmit}
+                    className="bg-white border border-slate-200 rounded-xl p-6 mb-6 shadow-sm"
+                >
+                    <h3 className="font-semibold text-slate-800 mb-4">{t('feishuAddBot')}</h3>
+                    <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                        <div>
+                            <label className="block text-xs font-semibold text-slate-600 mb-1">
+                                App ID <span className="text-red-500">*</span>
+                            </label>
+                            <input
+                                type="text"
+                                required
+                                value={form.appId}
+                                onChange={(e) => setForm((f) => ({ ...f, appId: e.target.value }))}
+                                placeholder="cli_xxxxxxxxxxxxxxxx"
+                                className="w-full h-9 px-3 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 outline-none transition"
+                            />
+                        </div>
+                        <div>
+                            <label className="block text-xs font-semibold text-slate-600 mb-1">
+                                App Secret <span className="text-red-500">*</span>
+                            </label>
+                            <input
+                                type="password"
+                                required
+                                value={form.appSecret}
+                                onChange={(e) => setForm((f) => ({ ...f, appSecret: e.target.value }))}
+                                placeholder="••••••••••••••••"
+                                className="w-full h-9 px-3 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 outline-none transition"
+                            />
+                        </div>
+                        <div>
+                            <label className="block text-xs font-semibold text-slate-600 mb-1">
+                                {t('feishuBotDisplayName')}
+                            </label>
+                            <input
+                                type="text"
+                                value={form.botName}
+                                onChange={(e) => setForm((f) => ({ ...f, botName: e.target.value }))}
+                                placeholder={t('feishuBotNamePlaceholder')}
+                                className="w-full h-9 px-3 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 outline-none transition"
+                            />
+                        </div>
+                        <div>
+                            <label className="block text-xs font-semibold text-slate-600 mb-1">
+                                Verification Token
+                            </label>
+                            <input
+                                type="text"
+                                value={form.verificationToken}
+                                onChange={(e) => setForm((f) => ({ ...f, verificationToken: e.target.value }))}
+                                placeholder={t('feishuTokenPlaceholder')}
+                                className="w-full h-9 px-3 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 outline-none transition"
+                            />
+                        </div>
+                        <div className="md:col-span-2">
+                            <label className="block text-xs font-semibold text-slate-600 mb-1">
+                                Encrypt Key ({t('optional')})
+                            </label>
+                            <input
+                                type="password"
+                                value={form.encryptKey}
+                                onChange={(e) => setForm((f) => ({ ...f, encryptKey: e.target.value }))}
+                                placeholder={t('feishuEncryptKeyPlaceholder')}
+                                className="w-full h-9 px-3 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 outline-none transition"
+                            />
+                        </div>
+                    </div>
+                    <div className="flex justify-end gap-3 mt-4">
+                        <button
+                            type="button"
+                            onClick={() => setShowForm(false)}
+                            className="px-4 h-9 text-sm text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
+                        >
+                            {t('cancel')}
+                        </button>
+                        <button
+                            type="submit"
+                            disabled={submitting}
+                            className="px-4 h-9 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-semibold transition-colors disabled:opacity-50"
+                        >
+                            {submitting ? t('saving') : t('save')}
+                        </button>
+                    </div>
+                </form>
+            )}
+
+            {/* Bot List */}
+            {loading ? (
+                <div className="text-center py-12 text-slate-400">{t('loading')}</div>
+            ) : bots.length === 0 ? (
+                <div className="text-center py-12 bg-white rounded-xl border border-dashed border-slate-200">
+                    <span className="text-4xl mb-3 block">🪶</span>
+                    <p className="text-slate-500 text-sm">{t('feishuNoBots')}</p>
+                </div>
+            ) : (
+                <div className="space-y-4">
+                    {bots.map((bot) => {
+                        const wsStatus = wsStatuses[bot.id];
+                        const wsState: WsState = wsStatus?.state ?? 'disconnected';
+                        const isWsLoading = wsLoading[bot.id] ?? false;
+                        const isWsConnected = wsState === 'connected';
+                        const isWsConnecting = wsState === 'connecting';
+
+                        return (
+                            <div
+                                key={bot.id}
+                                className="bg-white rounded-xl border border-slate-200 shadow-sm p-5"
+                            >
+                                {/* Bot Header */}
+                                <div className="flex items-center justify-between mb-3">
+                                    <div>
+                                        <p className="font-semibold text-slate-800">
+                                            {bot.botName || bot.appId}
+                                        </p>
+                                        <p className="text-xs text-slate-400 mt-0.5">App ID: {bot.appId}</p>
+                                    </div>
+                                    <div className="flex items-center gap-2">
+                                        <button
+                                            onClick={() => handleToggle(bot)}
+                                            className="flex items-center gap-1.5 text-xs font-medium transition-colors"
+                                            title={bot.enabled ? t('feishuDisableBot') : t('feishuEnableBot')}
+                                        >
+                                            {bot.enabled ? (
+                                                <ToggleRight size={22} className="text-emerald-500" />
+                                            ) : (
+                                                <ToggleLeft size={22} className="text-slate-300" />
+                                            )}
+                                        </button>
+                                        <button
+                                            onClick={() => handleDelete(bot.id)}
+                                            className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
+                                        >
+                                            <Trash2 size={15} />
+                                        </button>
+                                    </div>
+                                </div>
+
+                                {/* Webhook URL */}
+                                <div className="bg-slate-50 rounded-lg p-3 flex items-center justify-between gap-2 mb-3">
+                                    <div className="flex-1 min-w-0">
+                                        <p className="text-[11px] font-semibold text-slate-500 uppercase tracking-wide mb-0.5">
+                                            {t('feishuWebhookUrl')}
+                                        </p>
+                                        <code className="text-xs text-slate-700 break-all">
+                                            {window.location.origin}{bot.webhookUrl}
+                                        </code>
+                                    </div>
+                                    <button
+                                        onClick={() => copyWebhookUrl(bot.webhookUrl, bot.id)}
+                                        className="shrink-0 p-2 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors"
+                                    >
+                                        {copiedId === bot.id ? (
+                                            <Check size={15} className="text-emerald-500" />
+                                        ) : (
+                                            <Copy size={15} />
+                                        )}
+                                    </button>
+                                </div>
+
+                                {/* WebSocket Mode Panel */}
+                                <div className={`rounded-lg border p-3 ${isWsConnected ? 'border-emerald-200 bg-emerald-50/50' : 'border-slate-200 bg-slate-50'}`}>
+                                    <div className="flex items-start justify-between gap-3">
+                                        <div className="flex items-start gap-2 flex-1 min-w-0">
+                                            {isWsConnected ? (
+                                                <Wifi size={15} className="mt-0.5 shrink-0 text-emerald-500" />
+                                            ) : (
+                                                <WifiOff size={15} className="mt-0.5 shrink-0 text-slate-400" />
+                                            )}
+                                            <div>
+                                                <p className="text-xs font-semibold text-slate-700">
+                                                    {t('feishuWsMode')}
+                                                </p>
+                                                <p className="text-[11px] text-slate-500 mt-0.5">
+                                                    {t('feishuWsModeDesc')}
+                                                </p>
+                                            </div>
+                                        </div>
+
+                                        {/* WS State Badge + Button */}
+                                        <div className="flex items-center gap-2 shrink-0">
+                                            <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-semibold border ${getWsStateBg(wsState)}`}>
+                                                {getWsStateLabel(wsState)}
+                                            </span>
+                                            {isWsConnected || isWsConnecting ? (
+                                                <button
+                                                    onClick={() => handleWsDisconnect(bot.id)}
+                                                    disabled={isWsLoading || isWsConnecting}
+                                                    className="flex items-center gap-1.5 px-3 h-7 text-xs font-medium text-red-600 bg-red-50 hover:bg-red-100 rounded-lg transition-colors disabled:opacity-50"
+                                                >
+                                                    {isWsLoading ? <Loader2 size={12} className="animate-spin" /> : <WifiOff size={12} />}
+                                                    {t('feishuWsDisconnect')}
+                                                </button>
+                                            ) : (
+                                                <button
+                                                    onClick={() => handleWsConnect(bot.id)}
+                                                    disabled={isWsLoading || !bot.enabled}
+                                                    className="flex items-center gap-1.5 px-3 h-7 text-xs font-medium text-indigo-600 bg-indigo-50 hover:bg-indigo-100 rounded-lg transition-colors disabled:opacity-50"
+                                                    title={!bot.enabled ? t('feishuEnableBot') : undefined}
+                                                >
+                                                    {isWsLoading ? <Loader2 size={12} className="animate-spin" /> : <Wifi size={12} />}
+                                                    {t('feishuWsConnect')}
+                                                </button>
+                                            )}
+                                        </div>
+                                    </div>
+
+                                    {/* Hint text */}
+                                    {!isWsConnected && (
+                                        <p className="text-[11px] text-slate-400 mt-2 pl-5">
+                                            💡 {t('feishuWsConnectHint')}
+                                        </p>
+                                    )}
+
+                                    {/* Connected At info */}
+                                    {isWsConnected && wsStatus?.connectedAt && (
+                                        <p className="text-[11px] text-emerald-600 mt-2 pl-5">
+                                            ✓ {t('feishuWsConnected')} · {new Date(wsStatus.connectedAt).toLocaleTimeString()}
+                                        </p>
+                                    )}
+                                </div>
+
+                                {/* Footer badges */}
+                                <div className="mt-2 flex items-center gap-2">
+                                    <span
+                                        className={`inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-semibold ${
+                                            bot.enabled
+                                                ? 'bg-emerald-50 text-emerald-600'
+                                                : 'bg-slate-100 text-slate-500'
+                                        }`}
+                                    >
+                                        {bot.enabled ? t('statusRunning') : t('statusStopped')}
+                                    </span>
+                                    <span className="text-[11px] text-slate-400">
+                                        {t('createdAt')}: {new Date(bot.createdAt).toLocaleDateString()}
+                                    </span>
+                                </div>
+                            </div>
+                        );
+                    })}
+                </div>
+            )}
+        </div>
+    );
+};

+ 102 - 0
web/src/components/views/PluginsView.tsx

@@ -0,0 +1,102 @@
+import React, { useState } from 'react';
+import { Blocks } from 'lucide-react';
+import { useLanguage } from '../../../contexts/LanguageContext';
+
+import { FeishuPluginConfig } from '../plugins/FeishuPluginConfig';
+
+
+interface PluginDef {
+    id: string;
+    icon: string;
+    nameKey: string;
+    descKey: string;
+    available: boolean;
+    component?: React.ComponentType;
+}
+
+const PLUGIN_LIST: PluginDef[] = [
+    {
+        id: 'feishu',
+        icon: '🪶',
+        nameKey: 'pluginFeishuName',
+        descKey: 'pluginFeishuDesc',
+        available: true,
+        component: FeishuPluginConfig,
+    },
+];
+
+export const PluginsView: React.FC = () => {
+    const { t } = useLanguage();
+    const [activePlugin, setActivePlugin] = useState<PluginDef | null>(null);
+
+    if (activePlugin?.component) {
+        const ConfigComp = activePlugin.component;
+        return (
+            <div className="flex flex-col h-full bg-[#f4f7fb]">
+                <div className="px-8 pt-8 pb-4 shrink-0">
+                    <button
+                        onClick={() => setActivePlugin(null)}
+                        className="flex items-center gap-1.5 text-sm text-slate-500 hover:text-blue-600 transition-colors mb-4"
+                    >
+                        ← {t('back')}
+                    </button>
+                </div>
+                <div className="flex-1 overflow-y-auto px-8 pb-8">
+                    <ConfigComp />
+                </div>
+            </div>
+        );
+    }
+
+    return (
+        <div className="flex flex-col h-full bg-[#f4f7fb] overflow-hidden">
+            {/* Header */}
+            <div className="px-8 pt-8 pb-6 shrink-0">
+                <div className="flex items-center gap-3">
+                    <div className="w-10 h-10 bg-indigo-50 rounded-xl flex items-center justify-center">
+                        <Blocks size={20} className="text-indigo-600" />
+                    </div>
+                    <div>
+                        <h1 className="text-[22px] font-bold text-slate-900 leading-tight">
+                            {t('pluginViewTitle')}
+                        </h1>
+                        <p className="text-[14px] text-slate-500 mt-0.5">{t('pluginViewDesc')}</p>
+                    </div>
+                </div>
+            </div>
+
+            {/* Plugin Grid */}
+            <div className="px-8 pb-8 flex-1 overflow-y-auto">
+                <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 max-w-[1200px]">
+                    {PLUGIN_LIST.map((plugin) => (
+                        <div
+                            key={plugin.id}
+                            className="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md hover:border-indigo-200 transition-all cursor-pointer group"
+                            onClick={() => setActivePlugin(plugin)}
+                        >
+                            <div className="flex items-start justify-between mb-4">
+                                <div className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center text-2xl">
+                                    {plugin.icon}
+                                </div>
+                                <div className="px-2.5 py-1 text-[12px] font-semibold text-emerald-600 bg-emerald-50 rounded-full border border-emerald-100">
+                                    {t('pluginStatusAvailable')}
+                                </div>
+                            </div>
+                            <h3 className="font-bold text-slate-800 text-[17px] mb-1.5">
+                                {t(plugin.nameKey as any)}
+                            </h3>
+                            <p className="text-[13px] text-slate-500 leading-relaxed line-clamp-2">
+                                {t(plugin.descKey as any)}
+                            </p>
+                            <div className="mt-4 pt-4 border-t border-slate-50">
+                                <button className="text-sm font-bold text-indigo-600 hover:text-indigo-800 transition-colors group-hover:underline">
+                                    {t('pluginConfigure')} →
+                                </button>
+                            </div>
+                        </div>
+                    ))}
+                </div>
+            </div>
+        </div>
+    );
+};

+ 1 - 1
web/src/pages/workspace/PluginsPage.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { PluginsView } from '../../../components/views/PluginsView';
+import { PluginsView } from '../../components/views/PluginsView';
 
 const PluginsPage: React.FC = () => {
     return (

+ 98 - 6
web/utils/translations.ts

@@ -836,8 +836,40 @@ export const translations = {
     pluginCommunity: "社区",
     pluginBy: "由 ",
     pluginConfig: "插件配置",
+    pluginViewTitle: "插件中心",
+    pluginViewDesc: "集成外部工具与第三方平台,扩展系统能力。",
+    pluginStatusAvailable: "可用",
+    pluginConfigure: "配置插件",
+    pluginFeishuName: "飞书机器人",
+    pluginFeishuDesc: "将 AuraK 知识库问答能力接入飞书,支持私聊与群聊消息。",
+    feishuAddBot: "添加机器人",
+    feishuSetupGuide: "配置指引",
+    feishuStep1: "登录飞书开放平台,创建一个自建应用。",
+    feishuStep2: "在应用后台获取 App ID 和 App Secret。",
+    feishuStep3: "填写下方表单并保存,系统将生成 Webhook URL。",
+    feishuStep4: "将 Webhook URL 粘贴到应用的「事件订阅」配置中。",
+    feishuDocs: "查看官方文档",
+    feishuBotDisplayName: "机器人显示名称",
+    feishuBotNamePlaceholder: "例如: AuraK 助手",
+    feishuTokenPlaceholder: "飞书事件验证 Token",
+    feishuEncryptKeyPlaceholder: "飞书消息加密 Key (可选)",
+    feishuNoBots: "暂无绑定的机器人,点击右上角添加",
+    feishuWebhookUrl: "Webhook 回调地址",
+    feishuConfirmDelete: "确定要删除此机器人绑定吗?",
+    feishuEnableBot: "启用机器人",
+    feishuDisableBot: "禁用机器人",
+    feishuWsMode: "长连接模式 (WebSocket)",
+    feishuWsModeDesc: "无需公网域名,服务器主动连接飞书云端",
+    feishuWsConnect: "启用长连接",
+    feishuWsDisconnect: "断开长连接",
+    feishuWsStatus: "连接状态",
+    feishuWsConnected: "已连接",
+    feishuWsConnecting: "连接中...",
+    feishuWsDisconnected: "未连接",
+    feishuWsError: "连接错误",
+    feishuWsConnectHint: "启用后,飞书事件将通过 WebSocket 长连接推送,无需配置 Webhook 回调地址。",
+
 
-    // Plugin Mock Data
     plugin1Name: "Web 搜索",
     plugin1Desc: "赋予 AI 实时访问互联网的能力,获取最新信息。",
     plugin2Name: "PDF 文档解析",
@@ -1714,12 +1746,40 @@ export const translations = {
     pluginDesc: "Extend the functionality of your knowledge base with external tools and services.",
     searchPlugin: "Search plugins...",
     installPlugin: "Install",
-    installedPlugin: "Installed",
-    updatePlugin: "Update Available",
-    pluginOfficial: "OFFICIAL",
-    pluginCommunity: "COMMUNITY",
     pluginBy: "By ",
-    pluginConfig: "Configuration",
+    pluginConfig: "Plugin Config",
+    pluginViewTitle: "Plugins",
+    pluginViewDesc: "Integrate external tools and platforms to extend system capabilities.",
+    pluginStatusAvailable: "Available",
+    pluginConfigure: "Configure",
+    pluginFeishuName: "Feishu Bot",
+    pluginFeishuDesc: "Connect AuraK knowledge base Q&A to Feishu, supporting direct and group messages.",
+    feishuAddBot: "Add Bot",
+    feishuSetupGuide: "Setup Guide",
+    feishuStep1: "Log in to Feishu Open Platform and create a custom app.",
+    feishuStep2: "In the app settings, obtain the App ID and App Secret.",
+    feishuStep3: "Fill in the form below and save to generate a Webhook URL.",
+    feishuStep4: "Paste the Webhook URL into the app's Event Subscription configuration.",
+    feishuDocs: "View Official Docs",
+    feishuBotDisplayName: "Bot Display Name",
+    feishuBotNamePlaceholder: "e.g., AuraK Assistant",
+    feishuTokenPlaceholder: "Feishu Event Verification Token",
+    feishuEncryptKeyPlaceholder: "Feishu Message Encryption Key (optional)",
+    feishuNoBots: "No bots linked yet. Click the button above to add one.",
+    feishuWebhookUrl: "Webhook Callback URL",
+    feishuConfirmDelete: "Are you sure you want to remove this bot?",
+    feishuEnableBot: "Enable Bot",
+    feishuDisableBot: "Disable Bot",
+    feishuWsMode: "Long Connection Mode (WebSocket)",
+    feishuWsModeDesc: "No public domain required; server connects to Feishu cloud directly",
+    feishuWsConnect: "Enable Long Connection",
+    feishuWsDisconnect: "Disconnect",
+    feishuWsStatus: "Connection Status",
+    feishuWsConnected: "Connected",
+    feishuWsConnecting: "Connecting...",
+    feishuWsDisconnected: "Disconnected",
+    feishuWsError: "Connection Error",
+    feishuWsConnectHint: "When enabled, Feishu events are pushed via WebSocket. No Webhook callback URL required.",
 
     // Plugin Mock Data
     plugin1Name: "Web Search",
@@ -2597,6 +2657,38 @@ export const translations = {
     pluginCommunity: "コミュニティ",
     pluginBy: "開発者: ",
     pluginConfig: "設定",
+    pluginViewTitle: "プラグイン",
+    pluginViewDesc: "外部ツールやサードパーティプラットフォームを統合してシステムを拡張します。",
+    pluginStatusAvailable: "利用可能",
+    pluginConfigure: "設定する",
+    pluginFeishuName: "Feishu Bot",
+    pluginFeishuDesc: "AuraK ナレッジベースの Q&A 機能を Feishu に接続し、個人チャットとグループメッセージをサポートします。",
+    feishuAddBot: "Bot を追加",
+    feishuSetupGuide: "セットアップガイド",
+    feishuStep1: "Feishu オープンプラットフォームにログインし、カスタムアプリを作成します。",
+    feishuStep2: "アプリ設定から App ID と App Secret を取得します。",
+    feishuStep3: "以下のフォームに入力して保存すると、Webhook URL が生成されます。",
+    feishuStep4: "生成された Webhook URL をアプリのイベントサブスクリプション設定に貼り付けます。",
+    feishuDocs: "公式ドキュメントを確認",
+    feishuBotDisplayName: "Bot 表示名",
+    feishuBotNamePlaceholder: "例: AuraK アシスタント",
+    feishuTokenPlaceholder: "Feishu イベント検証 Token",
+    feishuEncryptKeyPlaceholder: "Feishu メッセージ暗号化 Key (省略可)",
+    feishuNoBots: "Bot がまだ登録されていません。右上のボタンから追加してください。",
+    feishuWebhookUrl: "Webhook コールバック URL",
+    feishuConfirmDelete: "この Bot の登録を削除してもよろしいですか?",
+    feishuEnableBot: "Bot を有効化",
+    feishuDisableBot: "Bot を無効化",
+    feishuWsMode: "長接続モード (WebSocket)",
+    feishuWsModeDesc: "公開ドメイン不要。サーバーが Feishu クラウドに接続します。",
+    feishuWsConnect: "長接続を有効化",
+    feishuWsDisconnect: "切断",
+    feishuWsStatus: "接続状態",
+    feishuWsConnected: "接続済み",
+    feishuWsConnecting: "接続中...",
+    feishuWsDisconnected: "未接続",
+    feishuWsError: "接続エラー",
+    feishuWsConnectHint: "有効にすると、Feishu イベントが WebSocket 経由でプッシュされます。Webhook コールバック URL は不要です。",
 
     // Plugin Mock Data
     plugin1Name: "Web 検索",

文件差異過大導致無法顯示
+ 585 - 135
yarn.lock


部分文件因文件數量過多而無法顯示