anhuiqiang 1 неделя назад
Родитель
Сommit
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.
 > **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)
 - NestJS (existing)
 - Feishu Open Platform APIs (im/v1/messages, auth/v3/tenant_access_token)
 - Feishu Open Platform APIs (im/v1/messages, auth/v3/tenant_access_token)
 - Event subscription (Webhooks)
 - 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
 ## File Structure
 
 
+
 ### New Files to Create
 ### New Files to Create
 
 
 ```
 ```
@@ -100,56 +106,21 @@ export class FeishuBot {
   id: string;
   id: string;
 
 
   @Column({ name: 'user_id' })
   @Column({ name: 'user_id' })
-  userId: string;
+  userId: string; // Plugin manages its own relationship to the core User
 
 
   @ManyToOne(() => User)
   @ManyToOne(() => User)
   @JoinColumn({ name: 'user_id' })
   @JoinColumn({ name: 'user_id' })
   user: User;
   user: User;
 
 
   @Column({ name: 'app_id', length: 64 })
   @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**
 - [ ] **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:**
 **Files:**
 - Create: `server/src/feishu/feishu.module.ts`
 - Create: `server/src/feishu/feishu.module.ts`
 - Modify: `server/src/app.module.ts`
 - Modify: `server/src/app.module.ts`
 
 
-- [ ] **Step 1: Create FeishuModule**
+- [ ] **Step 1: Create FeishuModule with isolated exports**
+
 
 
 ```typescript
 ```typescript
 import { Module, forwardRef } from '@nestjs/common';
 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)
 ## Chunk 6: Frontend Integration (Optional)
 
 
-### Task 6.1: Add Feishu settings UI
-
+### Task 6.1: Add Feishu sub-view to Plugins
 **Files:**
 **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`
 - 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`
 Modify: `web/src/services/api.ts`
 ```typescript
 ```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
 ## Testing Strategy
 
 
 ### Unit Tests
 ### Unit Tests
+
 - `feishu.service.spec.ts` - Test token refresh, message sending
 - `feishu.service.spec.ts` - Test token refresh, message sending
 - `feishu.controller.spec.ts` - Test webhook endpoints
 - `feishu.controller.spec.ts` - Test webhook endpoints
 
 
 ### Integration Tests
 ### Integration Tests
+
 - Test full flow: Feishu message → Webhook → ChatService → Response
 - Test full flow: Feishu message → Webhook → ChatService → Response
 
 
 ### Manual Testing
 ### Manual Testing
+
 1. Create Feishu app in 开放平台
 1. Create Feishu app in 开放平台
 2. Configure webhook URL (use ngrok for local)
 2. Configure webhook URL (use ngrok for local)
 3. Subscribe to message events
 3. Subscribe to message events
 4. Send message to bot
 4. Send message to bot
 5. Verify RAG response
 5. Verify RAG response
 
 
+
 ---
 ---
 
 
 ## Security Considerations
 ## 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
 ## 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/langgraph": "^1.0.4",
     "@langchain/openai": "^1.1.3",
     "@langchain/openai": "^1.1.3",
     "@langchain/textsplitters": "^1.0.1",
     "@langchain/textsplitters": "^1.0.1",
+    "@larksuiteoapi/node-sdk": "^1.59.0",
     "@nestjs/common": "^11.0.1",
     "@nestjs/common": "^11.0.1",
     "@nestjs/config": "^4.0.2",
     "@nestjs/config": "^4.0.2",
     "@nestjs/core": "^11.0.1",
     "@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 { TenantModule } from './tenant/tenant.module';
 import { SuperAdminModule } from './super-admin/super-admin.module';
 import { SuperAdminModule } from './super-admin/super-admin.module';
 import { AdminModule } from './admin/admin.module';
 import { AdminModule } from './admin/admin.module';
+import { FeishuModule } from './feishu/feishu.module';
+import { FeishuBot } from './feishu/entities/feishu-bot.entity';
+
 
 
 @Module({
 @Module({
   imports: [
   imports: [
@@ -91,6 +94,8 @@ import { AdminModule } from './admin/admin.module';
           TenantSetting,
           TenantSetting,
           TenantMember,
           TenantMember,
           ApiKey,
           ApiKey,
+          FeishuBot,
+
         ],
         ],
         synchronize: true, // Auto-create database schema. Disable in production.
         synchronize: true, // Auto-create database schema. Disable in production.
       }),
       }),
@@ -118,6 +123,8 @@ import { AdminModule } from './admin/admin.module';
     AssessmentModule,
     AssessmentModule,
     SuperAdminModule,
     SuperAdminModule,
     AdminModule,
     AdminModule,
+    FeishuModule,
+
   ],
   ],
   controllers: [AppController],
   controllers: [AppController],
   providers: [
   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 { IS_PUBLIC_KEY } from './public.decorator';
 import { tenantStore } from '../tenant/tenant.store';
 import { tenantStore } from '../tenant/tenant.store';
 import { UserRole } from '../user/user-role.enum';
 import { UserRole } from '../user/user-role.enum';
+import * as fs from 'fs';
+import * as path from 'path';
 
 
 /**
 /**
  * A combined authentication guard that accepts either:
  * A combined authentication guard that accepts either:
@@ -35,9 +37,16 @@ export class CombinedAuthGuard implements CanActivate {
             context.getHandler(),
             context.getHandler(),
             context.getClass(),
             context.getClass(),
         ]);
         ]);
-        if (isPublic) return true;
-
+        
         const request = context.switchToHttp().getRequest<Request & { user?: any; tenantId?: string }>();
         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 ---
         // --- Try API Key first ---
         const apiKey = this.extractApiKey(request);
         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')}
                             isActive={location.pathname.startsWith('/agents')}
                             onClick={handleNavClick}
                             onClick={handleNavClick}
                         />
                         />
-                        {/* <SidebarItem
+                        <SidebarItem
                             icon={Blocks}
                             icon={Blocks}
                             label={t('navPlugin')}
                             label={t('navPlugin')}
                             path="/plugins"
                             path="/plugins"
                             isActive={location.pathname.startsWith('/plugins')}
                             isActive={location.pathname.startsWith('/plugins')}
                             onClick={handleNavClick}
                             onClick={handleNavClick}
-                        /> */}
+                        />
 
 
                         <SidebarItem
                         <SidebarItem
                             icon={Database}
                             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 React from 'react';
-import { PluginsView } from '../../../components/views/PluginsView';
+import { PluginsView } from '../../components/views/PluginsView';
 
 
 const PluginsPage: React.FC = () => {
 const PluginsPage: React.FC = () => {
     return (
     return (

+ 98 - 6
web/utils/translations.ts

@@ -836,8 +836,40 @@ export const translations = {
     pluginCommunity: "社区",
     pluginCommunity: "社区",
     pluginBy: "由 ",
     pluginBy: "由 ",
     pluginConfig: "插件配置",
     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 搜索",
     plugin1Name: "Web 搜索",
     plugin1Desc: "赋予 AI 实时访问互联网的能力,获取最新信息。",
     plugin1Desc: "赋予 AI 实时访问互联网的能力,获取最新信息。",
     plugin2Name: "PDF 文档解析",
     plugin2Name: "PDF 文档解析",
@@ -1714,12 +1746,40 @@ export const translations = {
     pluginDesc: "Extend the functionality of your knowledge base with external tools and services.",
     pluginDesc: "Extend the functionality of your knowledge base with external tools and services.",
     searchPlugin: "Search plugins...",
     searchPlugin: "Search plugins...",
     installPlugin: "Install",
     installPlugin: "Install",
-    installedPlugin: "Installed",
-    updatePlugin: "Update Available",
-    pluginOfficial: "OFFICIAL",
-    pluginCommunity: "COMMUNITY",
     pluginBy: "By ",
     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
     // Plugin Mock Data
     plugin1Name: "Web Search",
     plugin1Name: "Web Search",
@@ -2597,6 +2657,38 @@ export const translations = {
     pluginCommunity: "コミュニティ",
     pluginCommunity: "コミュニティ",
     pluginBy: "開発者: ",
     pluginBy: "開発者: ",
     pluginConfig: "設定",
     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
     // Plugin Mock Data
     plugin1Name: "Web 検索",
     plugin1Name: "Web 検索",

Разница между файлами не показана из-за своего большого размера
+ 585 - 135
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов