diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts index b05c368..51c06bd 100644 --- a/apps/server/src/db/schema.ts +++ b/apps/server/src/db/schema.ts @@ -65,7 +65,7 @@ export const subscriptionsRelations = relations(subscriptions, ({ one }) => ({ // API Tasks: 记录 webhook 请求的处理状态 export const alertTasks = pgTable('alert_tasks', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), - topicSlug: text('topic_slug').notNull(), + topicSlug: text('topic_slug'), senderId: text('sender_id').references(() => users.id), // 记录是谁发送的 (通过 personal_token) status: text('status', { enum: ['pending', 'processing', 'completed', 'failed'] }).default('pending').notNull(), recipientCount: integer('recipient_count').default(0), diff --git a/apps/server/src/webhook.ts b/apps/server/src/webhook.ts index 1220a23..1f1ee18 100644 --- a/apps/server/src/webhook.ts +++ b/apps/server/src/webhook.ts @@ -161,6 +161,106 @@ webhook.post('/:token/topic/:slug', async (c) => { }); }); +webhook.post('/:token/dm', async (c) => { + const token = c.req.param('token'); + console.log(`[Webhook] Received DM request for token: ${token}`); + + // 0. Find the User by Token + const user = await db.query.users.findFirst({ + where: eq(users.personalToken, token), + }); + + if (!user) { + console.warn(`[Webhook] Invalid personal token: ${token}`); + return c.json({ error: 'Invalid personal token' }, 401); + } + + if (!user.feishuUserId) { + return c.json({ error: 'User has no Feishu ID linked' }, 400); + } + + let body; + try { + const rawBody = await c.req.text(); + if (!rawBody || rawBody.trim() === '') { + return c.json({ error: 'Empty body' }, 400); + } + body = JSON.parse(rawBody); + } catch (e) { + return c.json({ error: 'Invalid JSON body' }, 400); + } + + // 1. Create Task (topicSlug is null for DM) + const [task] = await db.insert(alertTasks).values({ + topicSlug: null, + senderId: user.id, + status: 'processing', + recipientCount: 1, + successCount: 0, + payload: body, + }).returning(); + + // 2. Send Message + (async () => { + try { + let msgType = body.msg_type || 'text'; + let content = body.content; + + if (!content) { + msgType = 'text'; + content = { text: JSON.stringify(body, null, 2) }; + } + + // Add metadata + if (msgType === 'text' && content.text) { + content.text = `[Direct Message]\n${content.text}`; + } + if (msgType === 'interactive' && content.header) { + content.header.title.content = `[DM] ${content.header.title.content}`; + } + + const idType = user.feishuUserId.startsWith('ou_') ? 'open_id' : 'user_id'; + await feishuClient.sendMessage(user.feishuUserId, idType, msgType, content); + + // Update Task + await db.update(alertTasks).set({ + status: 'completed', + successCount: 1, + updatedAt: new Date(), + }).where(eq(alertTasks.id, task.id)); + + // Insert Log + await db.insert(alertLogs).values({ + taskId: task.id, + userId: user.id, + status: 'sent', + }); + + } catch (error: any) { + console.error(`Failed to send DM to user ${user.name}:`, error); + await db.update(alertTasks).set({ + status: 'failed', + updatedAt: new Date(), + error: error.message, + }).where(eq(alertTasks.id, task.id)); + + await db.insert(alertLogs).values({ + taskId: task.id, + userId: user.id, + status: 'failed', + error: error.message, + }); + } + })(); + + return c.json({ + message: 'DM received and processing started', + taskId: task.id, + status: 'processing', + recipientCount: 1 + }); +}); + // Help message for non-POST requests or malformed URLs webhook.all('/:token/topic/:slug', (c) => { return c.json({ diff --git a/apps/web/src/views/SystemLoadView.tsx b/apps/web/src/views/SystemLoadView.tsx index 3c2ec94..a8fd657 100644 --- a/apps/web/src/views/SystemLoadView.tsx +++ b/apps/web/src/views/SystemLoadView.tsx @@ -135,8 +135,8 @@ export default function SystemLoadView() { return ( - - {topic.topicSlug} + + {topic.topicSlug || '[Private DM]'} {topic.totalTasks} @@ -193,8 +193,8 @@ export default function SystemLoadView() { {new Date(task.createdAt).toLocaleString()} - - {task.topicSlug} + + {task.topicSlug || '[Private DM]'} diff --git a/apps/web/src/views/TopicsView.tsx b/apps/web/src/views/TopicsView.tsx index 4c9c597..d73eda7 100644 --- a/apps/web/src/views/TopicsView.tsx +++ b/apps/web/src/views/TopicsView.tsx @@ -197,6 +197,12 @@ export default function TopicsView() { return `${baseUrl}/webhook/${currentUser.personalToken}/topic/${topicSlug}`; }; + const getDmWebhookUrl = () => { + if (!currentUser?.personalToken) return ''; + const baseUrl = ((import.meta as any).env.VITE_WEBHOOK_BASE_URL || window.location.origin).replace(/\/$/, ''); + return `${baseUrl}/webhook/${currentUser.personalToken}/dm`; + }; + if (loading) return
Loading...
; return ( @@ -208,7 +214,7 @@ export default function TopicsView() {
  • Subscribe: Click Subscribe on any topic to start receiving alerts via Feishu private message.
  • -
  • Personal Webhook: Each topic provides a unique URL just for you. Send JSON alerts to this URL to notify yourself.
  • +
  • Personal Webhook: Use topic-specific URLs to notify all subscribers, or use your Personal Inbox to notify only yourself.
  • Need more? If you can't find a suitable topic, click Request Topic to ask admins for a new one.
@@ -216,6 +222,53 @@ export default function TopicsView() { +
+
+ +

Personal Inbox

+
+
+
+
+

Your private alert endpoint. No topic required.

+
+
+ Inbox Webhook URL + +
+
+ {getDmWebhookUrl()} +
+
+
+
+
+ +
+
+
Direct Push
+
Always delivered to you
+
+
+
+
+
+

Topics

diff --git a/docs/copilot-context.md b/docs/copilot-context.md index 9d416de..acb91b8 100644 --- a/docs/copilot-context.md +++ b/docs/copilot-context.md @@ -6,7 +6,10 @@ This document provides technical context, architectural decisions, and code conv **Alert Message Center** (formerly Alert Manager) is a centralized alert dispatching system. - **Goal**: Decouple alert sources from alert recipients. -- **Mechanism**: Alerts are sent to a **Topic**. Users subscribe to Topics. The system dispatches alerts to subscribers via **Feishu (Lark) Private Messages**. +- **Mechanism**: + - **Topics**: Alerts are sent to a **Topic**. Users subscribe to Topics to receive messages. + - **Personal Inbox**: Users can send alerts directly to themselves via a private webhook URL, bypassing Topic creation and approval. + - **Dispatch**: The system sends messages via **Feishu (Lark) Private Messages**. - **Runtime**: Bun (JavaScript/TypeScript runtime). ## 2. Tech Stack @@ -15,7 +18,7 @@ This document provides technical context, architectural decisions, and code conv - **Backend**: - **Runtime**: Bun. - **Framework**: Hono (Web Standard based). - - **Database**: SQLite (via `better-sqlite3`). + - **Database**: PostgreSQL. - **ORM**: Drizzle ORM. - **Authentication**: Feishu OAuth2 (Session-based with cookies). - **External API**: Feishu Open Platform (Server-side API). @@ -37,6 +40,9 @@ The database schema is defined in `apps/server/src/db/schema.ts`. - `name`: Display name (e.g., "Payment Service Errors"). - `slug`: URL-safe identifier (e.g., `payment-errors`). Used in webhook URLs. - `description`: Optional text. + - `status`: `pending`, `approved`, or `rejected`. + - `createdBy`: Foreign Key -> `users.id`. + - `approvedBy`: Foreign Key -> `users.id`. 2. **User** (`users`) - `id`: UUID (Primary Key). @@ -60,18 +66,26 @@ The database schema is defined in `apps/server/src/db/schema.ts`. 3. Server exchanges code for token, gets user info, creates/updates user in DB. 4. Server sets `session` cookie (httpOnly). - **Context**: `AuthContext.tsx` manages user state on frontend. + +### Personal Inbox (Direct Messaging) +- **Strategy**: Direct delivery to a specific user. +- **Mechanism**: + 1. Each user has a `personalToken`. + 2. Sending to `POST /api/webhook/:token/dm` routes messages directly to the user associated with the token. + 3. No Topic or Subscription is required. ### Alert Ingestion & Dispatch **File**: `apps/server/src/webhook.ts` -1. **Ingest**: `POST /api/webhook/:slug` receives a JSON payload. +1. **Ingest**: + - **Topic-based**: `POST /api/webhook/:token/topic/:slug` + - **Direct (Inbox)**: `POST /api/webhook/:token/dm` 2. **Lookup**: - - Find `Topic` by `slug`. - - Fetch all `subscriptions` for this topic, including the associated `user`. + - For Topic-based: Find `Topic` by `slug` and fetch all `subscriptions`. + - For Direct: Identify the user via `token`. 3. **Dispatch**: - - Iterate through subscribers. - - For each user, call `FeishuClient.sendMessage`. - - **Payload**: The `content` and `msg_type` from the request body are passed directly to Feishu. + - Call `FeishuClient.sendMessage` for each recipient. + - **Payload**: Supports `text` and `interactive` (Feishu Card) message types. ### Subscription Management - Users can subscribe/unsubscribe themselves to any topic. @@ -103,7 +117,8 @@ The database schema is defined in `apps/server/src/db/schema.ts`. ### Webhook -- `POST /api/webhook/:slug`: Trigger an alert for a topic. +- `POST /api/webhook/:token/topic/:slug`: Trigger an alert for a topic. +- `POST /api/webhook/:token/dm`: Trigger a direct alert to the user's private inbox. ## 6. Future Roadmap (Planned) diff --git a/todo.md b/todo.md index fc2001d..9d6bbf5 100644 --- a/todo.md +++ b/todo.md @@ -3,7 +3,7 @@ ## Phase 1: Core Functionality (Completed) - [x] Initialize project structure (Bun, Monorepo) - [x] Setup Backend (Hono + Bun) - - [x] Setup Drizzle (SQLite) + - [x] Setup Drizzle (PostgreSQL) - [x] **Refactor Schema**: Switch from Bots/Roles to Topics/Users/Subscriptions - [x] **Feishu Integration**: Implement Tenant Access Token & Private Message sending - [x] Implement Webhook API (`POST /api/webhook/:slug`) @@ -18,7 +18,8 @@ - [x] **Global Monitoring Dashboard**: Real-time System Load metrics (Grafana-style). - [ ] **Message Preview**: Preview Feishu card JSON in the UI. - [x] **History/Logs**: Basic tracking for sent alerts (Alert Tasks/Logs). -- [x] **Admin Topic Management**: Approve, reject, and delete topics. +- [x] **Admin Topic Management**: Approve, reject, and delete topics (with audit trail). +- [x] **Personal Inbox**: Direct alert delivery bypassing topics. - [ ] **Retry Mechanism**: Handle Feishu API failures. - [ ] **Deployment**: Dockerfile and deployment scripts.