diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f9925..a5db873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ 本文件的格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 并且本项目遵循 [语义化版本 (Semantic Versioning)](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。 +## [1.2.1] - 2026-01-14 + +### 修复 +- **WebSocket 初始化**: 修复了 `@larksuiteoapi/node-sdk` v1.56.0+ 中 WebSocket 初始化不正确的 `TypeError`。现在正确使用了 `WSClient` 类并修复了参数类型错误。 +- **事件处理**: 修正了 `im.chat.member.bot.added_v1` 事件的 Payload 解析逻辑。 +- **Hono 兼容性**: 修正了 `feishu-event.ts` 中 `lark.adaptDefault` 的错误用法。改为使用手动 Challenge 处理和 `eventDispatcher.invoke`,并通过原型链注入 Header 解决了与 Hono 请求/响应对象的兼容性以及签名校验失败的问题。 +- **群聊解绑**: 增加对 `im.chat.member.bot.deleted_v1` 事件的支持。当机器人被移除群聊时,自动清理 `known_group_chats` 和 `topic_group_chats` 关联,确保订阅关系自动解绑。 + +### 新增 +- **结构化日志**: 引入 `pino` 框架替代 `console.log`,实现结构化 JSON 日志输出。 + - 在开发环境集成 `pino-pretty` 提供人类友好格式。 + - 支持通过环境遍历控制日志级别。 + +## [1.2.0] - 2026-01-13 + +### 新增 +- **飞书群聊通知**: 支持将告警发送到飞书群聊 (App Bot 模式)。 + - 自动发现机器人所在的群组。 + - 支持在 Topic 中绑定群聊。 +- **长连接模式 (WebSocket)**: 引入 `@larksuiteoapi/node-sdk`,支持通过 WebSocket 接收飞书事件,解决内网环境无法使用 Webhook 的问题。 + - 可通过 `FEISHU_USE_WS=true` 开启。 +- **UI 改进**: 在 Topic 列表页新增了群聊管理入口。 + +### 变更 +- **数据库**: 新增 `topic_group_chats` 和 `known_group_chats` 表。 +- **底层架构**: 重构了飞书客户端 (`FeishuClient`) 和事件处理逻辑,统一了 Webhook 和 WebSocket 的事件分发。 ## [1.1.1] - 2026-01-13 ### 修复 diff --git a/README.md b/README.md index 46c3bee..502d979 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,16 @@ 支持通过 **Topic (主题)** 订阅模式分发告警,同时也提供 **Personal Inbox (个人信箱)** 功能,无需创建话题即可快速给自己推送消息。 ![Topics View](docs/images/topics_view.png) -### 2. 管理员看板 (Live Stats) +除了个人订阅外,您可以将 Topic 绑定至多个**飞书群聊**。 +> [!TIP] +> **群聊发现**:请先将机器人邀请进入目标群聊。机器人入群后会触发自动感应,此时刷新管理页面即可在下拉菜单中看到并绑定该群组。 + +### 2. 群聊告警分发 +支持将机器人加入飞书群聊,并将话题绑定到群聊中,实现告警的群组广播。 +![Group Binding](docs/images/group_binding.png) +![Group Alert](docs/images/group_alert.png) + +### 3. 管理员看板 (Live Stats) 实时追踪全系统的告警负载、分发成功率以及各话题的热度。 ![Admin Dashboard](docs/images/admin_dashboard.png) @@ -22,11 +31,13 @@ ## 🔥 核心特性 - **🚀 极简推送 (Personal Inbox)**: 每个用户拥有专属的 Webhook Token,直接向 `/dm` 接口发送即可在飞书收到私聊,零配置成本。 -- **📢 主题订阅 (Topic Model)**: 灵活的“发布-订阅”机制。告警发送至 Topic,系统自动分发给所有订阅成员,避免群聊骚扰。 +- **📢 主题订阅 (Topic Model)**: 灵活的“发布-订阅”机制。告警发送至 Topic,系统自动分发给所有订阅成员。 +- **👥 群聊分发 (Group Support)**: 告警可同步分发至绑定的飞书群聊,支持机器人自动发现与解绑。 - **🛡️ 权限与审计**: - 话题创建需经过管理员审批。 - - 记录完整的 `Alert Task` 日志,包含发送者、时间、审批人及分发成功率。 + - 记录完整的 `Alert Task` 日志,实现发送链路可追溯。 - **📊 实时看板**: Grafana 风格的监控界面,直观展示系统运行健壮性。 +- **🔌 长连接模式 (WebSocket)**: 支持飞书开放平台长连接,无需公网 IP 或域名即可在内网环境接收事件回调。 - **⚡ 高性能架构**: 基于 Bun + Hono 的全异步架构,毫秒级分发延迟。 --- diff --git a/apps/server/drizzle/0001_bumpy_orphan.sql b/apps/server/drizzle/0001_bumpy_orphan.sql new file mode 100644 index 0000000..039c725 --- /dev/null +++ b/apps/server/drizzle/0001_bumpy_orphan.sql @@ -0,0 +1,18 @@ +CREATE TABLE "topic_group_webhooks" ( + "id" text PRIMARY KEY NOT NULL, + "topic_id" text NOT NULL, + "name" text NOT NULL, + "url" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "created_by" text +); +--> statement-breakpoint +ALTER TABLE "alert_tasks" ALTER COLUMN "topic_slug" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "alert_tasks" ADD COLUMN "sender_id" text;--> statement-breakpoint +ALTER TABLE "topics" ADD COLUMN "approved_by" text;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "personal_token" text NOT NULL;--> statement-breakpoint +ALTER TABLE "topic_group_webhooks" ADD CONSTRAINT "topic_group_webhooks_topic_id_topics_id_fk" FOREIGN KEY ("topic_id") REFERENCES "public"."topics"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "topic_group_webhooks" ADD CONSTRAINT "topic_group_webhooks_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "alert_tasks" ADD CONSTRAINT "alert_tasks_sender_id_users_id_fk" FOREIGN KEY ("sender_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "topics" ADD CONSTRAINT "topics_approved_by_users_id_fk" FOREIGN KEY ("approved_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "users" ADD CONSTRAINT "users_personal_token_unique" UNIQUE("personal_token"); \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index d0f4851..4e6b277 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@alertmessagecenter/server", - "version": "1.0.0", + "version": "1.2.0", "scripts": { "dev": "bun run --env-file .env --watch src/index.ts", "start": "bun run src/index.ts", @@ -11,14 +11,17 @@ }, "dependencies": { "@hono/zod-validator": "^0.7.6", + "@larksuiteoapi/node-sdk": "^1.56.1", "drizzle-orm": "^0.45.1", "hono": "^4.11.3", + "pino": "^10.1.1", "postgres": "^3.4.8", "zod": "^3.0.0" }, "devDependencies": { "@types/node": "^20.0.0", "bun-types": "latest", - "drizzle-kit": "^0.31.8" + "drizzle-kit": "^0.31.8", + "pino-pretty": "^13.1.3" } } \ No newline at end of file diff --git a/apps/server/src/api.ts b/apps/server/src/api.ts index 5c4f41b..b8c2484 100644 --- a/apps/server/src/api.ts +++ b/apps/server/src/api.ts @@ -3,7 +3,7 @@ import { eq, and, desc, sql, gt, sum, count } from 'drizzle-orm'; import { z } from 'zod'; import { zValidator } from '@hono/zod-validator'; import { db } from './db'; -import { topics, users, subscriptions, alertTasks } from './db/schema'; +import { topics, users, subscriptions, alertTasks, topicGroupChats, knownGroupChats } from './db/schema'; import { requireAuth, requireAdmin, AuthSession } from './middleware'; const api = new Hono<{ Variables: { session: AuthSession } }>(); @@ -14,6 +14,11 @@ const topicSchema = z.object({ description: z.string().optional(), }); +const groupBindingSchema = z.object({ + chatId: z.string().min(1), + name: z.string().min(1), +}); + const userSchema = z.object({ name: z.string().min(1), feishuUserId: z.string().min(1), @@ -168,6 +173,8 @@ api.delete('/users/:id', requireAdmin, async (c) => { // --- Subscriptions --- +// --- Subscriptions --- + // Users can subscribe themselves or admins can subscribe anyone api.post('/topics/:topicId/subscribe/:userId', requireAuth, async (c) => { const { topicId, userId } = c.req.param(); @@ -200,6 +207,55 @@ api.delete('/topics/:topicId/subscribe/:userId', requireAuth, async (c) => { return c.json({ success: true }); }); +// --- Group Bindings (App Bot) --- + +// Get list of known groups (for selection) +api.get('/groups', requireAuth, async (c) => { + // Return recent active groups + const groups = await db.select().from(knownGroupChats) + .orderBy(desc(knownGroupChats.lastActiveAt)) + .limit(50); + return c.json(groups); +}); + +// Get bindings for a topic +api.get('/topics/:id/groups', requireAuth, async (c) => { + const topicId = c.req.param('id'); + const groups = await db.select().from(topicGroupChats) + .where(eq(topicGroupChats.topicId, topicId)) + .orderBy(desc(topicGroupChats.createdAt)); + return c.json(groups); +}); + +// Bind a group to a topic +api.post('/topics/:id/groups', requireAuth, zValidator('json', groupBindingSchema), async (c) => { + const topicId = c.req.param('id'); + const body = c.req.valid('json'); + const session = c.get('session'); + + const result = await db.insert(topicGroupChats).values({ + topicId, + chatId: body.chatId, + name: body.name, + createdBy: session.id, + }).returning(); + + return c.json(result[0]); +}); + +// Unbind a group +api.delete('/topics/:id/groups/:bindingId', requireAuth, async (c) => { + const { id: topicId, bindingId } = c.req.param(); + + await db.delete(topicGroupChats) + .where(and( + eq(topicGroupChats.id, bindingId), + eq(topicGroupChats.topicId, topicId) + )); + + return c.json({ success: true }); +}); + // --- Alert Tasks --- api.get('/alerts/tasks', requireAdmin, async (c) => { diff --git a/apps/server/src/api/feishu-event.ts b/apps/server/src/api/feishu-event.ts new file mode 100644 index 0000000..83321e4 --- /dev/null +++ b/apps/server/src/api/feishu-event.ts @@ -0,0 +1,43 @@ +import { Hono } from 'hono'; +import * as lark from '@larksuiteoapi/node-sdk'; +import { eventDispatcher } from '../event-handler'; + +const feishuEvent = new Hono(); + +// Helper to adapt Hono request to Lark SDK request + +feishuEvent.post('/', async (c) => { + try { + const headers = c.req.raw.headers; + const headerRecord: Record = {}; + headers.forEach((value, key) => { + headerRecord[key] = value; + }); + + const body = await c.req.json(); + + // Use the official SDK functions directly for Hono compatibility + // 1. Handle URL verification (Challenge) + const { isChallenge, challenge } = lark.generateChallenge(body, { + encryptKey: process.env.FEISHU_ENCRYPT_KEY || '' + }); + + if (isChallenge) { + return c.json(challenge); + } + + // 2. Dispatch event + // The dispatcher expects an object containing headers and body. + // We use Object.create to put headers on the prototype so they are accessible + // but not included in JSON.stringify, which preserves signature verification. + const payload = Object.assign(Object.create({ headers: headerRecord }), body); + const result = await eventDispatcher.invoke(payload); + + return c.json(result || {}); + } catch (e) { + console.error('[Feishu Event] Error:', e); + return c.json({ error: 'Internal Server Error' }, 500); + } +}); + +export default feishuEvent; diff --git a/apps/server/src/auth.ts b/apps/server/src/auth.ts index 78f1f72..a436770 100644 --- a/apps/server/src/auth.ts +++ b/apps/server/src/auth.ts @@ -1,4 +1,5 @@ import { Hono } from 'hono'; +import { logger } from './lib/logger'; import { setCookie, getCookie } from 'hono/cookie'; import { db } from './db'; import { users } from './db/schema'; @@ -100,7 +101,7 @@ auth.get('/callback', async (c) => { }, }); } catch (error) { - console.error('OAuth callback error:', error); + logger.error({ err: error }, 'OAuth callback error'); return c.json({ error: 'Authentication failed' }, 500); } }); @@ -125,7 +126,7 @@ auth.get('/me', (c) => { }; return c.json({ user }); } catch (error) { - console.error('[Auth] Failed to parse session cookie:', error); + logger.error({ err: error }, '[Auth] Failed to parse session cookie'); return c.json({ error: 'Invalid session' }, 401); } }); diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts index 51c06bd..f054317 100644 --- a/apps/server/src/db/schema.ts +++ b/apps/server/src/db/schema.ts @@ -1,7 +1,7 @@ import { pgTable, text, integer, primaryKey, boolean, jsonb, timestamp } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'; -// Topics: 类似于 Kafka 的 Topic 或 告警的 Tag,例如 "payment-service", "prod-env" +// Topics: 类似于 Kafka 的 Topic 或 告警的 Tag,例如 "payment-service", export const topics = pgTable('topics', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), slug: text('slug').notNull().unique(), // 告警发送时使用的 key @@ -13,8 +13,37 @@ export const topics = pgTable('topics', { createdAt: timestamp('created_at').defaultNow().notNull(), }); +// Group Chats: App Bot 所在的群绑定 +export const topicGroupChats = pgTable('topic_group_chats', { + id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), + topicId: text('topic_id').notNull().references(() => topics.id, { onDelete: 'cascade' }), + chatId: text('chat_id').notNull(), // 飞书群 chat_id + name: text('name').notNull(), // 群名称快照 + createdAt: timestamp('created_at').defaultNow().notNull(), + createdBy: text('created_by').references(() => users.id), +}); + +export const topicGroupChatsRelations = relations(topicGroupChats, ({ one }) => ({ + topic: one(topics, { + fields: [topicGroupChats.topicId], + references: [topics.id], + }), + creator: one(users, { + fields: [topicGroupChats.createdBy], + references: [users.id], + }), +})); + +// Known Group Chats: 机器人已知的群 (通过事件发现) +export const knownGroupChats = pgTable('known_group_chats', { + chatId: text('chat_id').primaryKey(), // 飞书 chat_id + name: text('name').notNull(), + lastActiveAt: timestamp('last_active_at').defaultNow(), +}); + export const topicsRelations = relations(topics, ({ many, one }) => ({ subscriptions: many(subscriptions), + groupChats: many(topicGroupChats), creator: one(users, { fields: [topics.createdBy], references: [users.id], diff --git a/apps/server/src/event-handler.ts b/apps/server/src/event-handler.ts new file mode 100644 index 0000000..dbfe924 --- /dev/null +++ b/apps/server/src/event-handler.ts @@ -0,0 +1,38 @@ +import { db } from './db'; +import { knownGroupChats, topicGroupChats } from './db/schema'; +import { eq } from 'drizzle-orm'; +import * as lark from '@larksuiteoapi/node-sdk'; +import { logger } from './lib/logger'; + +export const eventDispatcher = new lark.EventDispatcher({ + encryptKey: process.env.FEISHU_ENCRYPT_KEY, + verificationToken: process.env.FEISHU_VERIFICATION_TOKEN, +}).register({ + 'im.chat.member.bot.added_v1': async (data) => { + const { chat_id, name } = data as any; + logger.info({ chat_id, name }, '[Feishu Event] Bot added to group'); + + if (chat_id) { + await db.insert(knownGroupChats).values({ + chatId: chat_id, + name: name || 'Unknown Group', + lastActiveAt: new Date(), + }).onConflictDoUpdate({ + target: knownGroupChats.chatId, + set: { + name: name || 'Unknown Group', + lastActiveAt: new Date(), + } + }); + } + }, + 'im.chat.member.bot.deleted_v1': async (data) => { + const { chat_id } = data as any; + logger.info({ chat_id }, '[Feishu Event] Bot removed from group'); + + if (chat_id) { + await db.delete(knownGroupChats).where(eq(knownGroupChats.chatId, chat_id)); + await db.delete(topicGroupChats).where(eq(topicGroupChats.chatId, chat_id)); + } + }, +}); diff --git a/apps/server/src/feishu.ts b/apps/server/src/feishu.ts index b5e4b14..e79626a 100644 --- a/apps/server/src/feishu.ts +++ b/apps/server/src/feishu.ts @@ -1,94 +1,72 @@ +import * as lark from '@larksuiteoapi/node-sdk'; +import { logger } from './lib/logger'; + export class FeishuClient { - private appId: string; - private appSecret: string; - private token: string | null = null; - private tokenExpireAt: number = 0; + public client: lark.Client; + public appId: string; + public appSecret: string; constructor(appId: string, appSecret: string) { this.appId = appId; this.appSecret = appSecret; - } - - private async getTenantAccessToken(): Promise { - if (this.token && Date.now() < this.tokenExpireAt) { - return this.token; - } - - const res = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - app_id: this.appId, - app_secret: this.appSecret, - }), + this.client = new lark.Client({ + appId: appId, + appSecret: appSecret, + disableTokenCache: false, }); - - const data = await res.json(); - if (data.code !== 0) { - throw new Error(`Failed to get tenant access token: ${data.msg}`); - } - - this.token = data.tenant_access_token; - // Expire 5 minutes early to be safe - this.tokenExpireAt = Date.now() + (data.expire * 1000) - 300000; - return this.token!; } - async sendMessage(receiveId: string, receiveIdType: 'open_id' | 'user_id' | 'email', msgType: string, content: any) { - const token = await this.getTenantAccessToken(); - - // Content needs to be stringified for 'text' type, but might be object for 'interactive' - // Feishu API expects 'content' field to be a JSON string for most types + async sendMessage(receiveId: string, receiveIdType: 'open_id' | 'user_id' | 'email' | 'chat_id', msgType: string, content: any) { + // Content needs to be stringified for 'text' type in API, but SDK might handle it differently? + // Actually SDK expects 'content' as string JSON for 'im.v1.messages.create' const contentStr = typeof content === 'string' ? content : JSON.stringify(content); - const res = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify({ - receive_id: receiveId, - msg_type: msgType, - content: contentStr, - }), - }); + try { + const response = await this.client.im.message.create({ + params: { + receive_id_type: receiveIdType, + }, + data: { + receive_id: receiveId, + msg_type: msgType, + content: contentStr, + }, + }); - const data = await res.json(); - if (data.code !== 0) { - console.error('Feishu send message error:', data); - throw new Error(`Failed to send message: ${data.msg}`); + if (response.code !== 0) { + logger.error({ response }, 'Feishu send message error'); + throw new Error(`Failed to send message: ${response.msg}`); + } + return response.data; + } catch (e) { + console.error('Feishu SDK error:', e); + throw e; } - return data; } async getUserAccessToken(code: string): Promise { - const token = await this.getTenantAccessToken(); - - const res = await fetch('https://open.feishu.cn/open-apis/authen/v1/access_token', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify({ - grant_type: 'authorization_code', - code, - }), - }); + try { + const response = await this.client.authen.accessToken.create({ + data: { + grant_type: 'authorization_code', + code, + }, + }); - const data = await res.json(); - if (data.code !== 0) { - console.error('Feishu get user access token error:', data); - throw new Error(`Failed to get user access token: ${data.msg}`); + if (response.code !== 0) { + logger.error({ response }, 'Feishu get user access token error'); + throw new Error(`Failed to get user access token: ${response.msg}`); + } + return response.data; + } catch (e) { + console.error('Feishu SDK error:', e); + throw e; } - - return data.data; } } -// Singleton instance - replace with env vars in production +// Singleton instance export const feishuClient = new FeishuClient( - process.env.FEISHU_APP_ID || 'cli_xxx', - process.env.FEISHU_APP_SECRET || 'xxx' + process.env.FEISHU_APP_ID || '', + process.env.FEISHU_APP_SECRET || '' ); diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 02751a6..b10daa7 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,4 +1,5 @@ import { Hono } from 'hono'; +import { logger } from './lib/logger'; import { cors } from 'hono/cors'; import { serveStatic } from 'hono/bun'; import { db } from './db'; @@ -15,9 +16,14 @@ app.use('/*', cors({ credentials: true, })); +import feishuEvent from './api/feishu-event'; + +// ... + // API Routes const routes = app.route('/api/auth', auth) .route('/api', api) + .route('/api/feishu/event', feishuEvent) .route('/webhook', webhook); // Serve static files (Frontend) @@ -25,7 +31,7 @@ app.use('/*', serveStatic({ root: './public' })); app.get('*', serveStatic({ path: './public/index.html' })); app.onError((err, c) => { - console.error(`[Global Error] ${c.req.method} ${c.req.url}:`, err); + logger.error({ err, method: c.req.method, url: c.req.url }, 'Global Error'); return c.json({ error: err.message || 'Internal Server Error' }, 500); }); @@ -34,5 +40,9 @@ app.get('/topics', async (c) => { return c.json(allTopics); }); +// Start WebSocket if enabled +import { startWebSocket } from './ws'; +startWebSocket(); + export type AppType = typeof routes; export default app; diff --git a/apps/server/src/lib/logger.ts b/apps/server/src/lib/logger.ts new file mode 100644 index 0000000..0d60549 --- /dev/null +++ b/apps/server/src/lib/logger.ts @@ -0,0 +1,19 @@ +import pino from 'pino'; + +const isDevelopment = process.env.NODE_ENV !== 'production'; + +export const logger = pino({ + level: process.env.LOG_LEVEL || 'info', + transport: isDevelopment + ? { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'HH:MM:ss Z', + ignore: 'pid,hostname', + }, + } + : undefined, +}); + +export default logger; diff --git a/apps/server/src/webhook.ts b/apps/server/src/webhook.ts index 1f1ee18..6598d93 100644 --- a/apps/server/src/webhook.ts +++ b/apps/server/src/webhook.ts @@ -3,13 +3,14 @@ import { eq } from 'drizzle-orm'; import { db } from './db'; import { topics, alertTasks, alertLogs, users } from './db/schema'; import { feishuClient } from './feishu'; +import { logger } from './lib/logger'; const webhook = new Hono(); webhook.post('/:token/topic/:slug', async (c) => { const token = c.req.param('token'); const slug = c.req.param('slug'); - console.log(`[Webhook] Received request for token: ${token}, slug: ${slug}`); + logger.info({ token, slug }, '[Webhook] Received request'); // 0. Find the User by Token const user = await db.query.users.findFirst({ @@ -17,19 +18,19 @@ webhook.post('/:token/topic/:slug', async (c) => { }); if (!user) { - console.warn(`[Webhook] Invalid personal token: ${token}`); + logger.warn({ token }, '[Webhook] Invalid personal token'); return c.json({ error: 'Invalid personal token' }, 401); } let body; try { const rawBody = await c.req.text(); - console.log(`[Webhook] Raw body length: ${rawBody.length}, content: "${rawBody}"`); + logger.debug({ bodyLength: rawBody.length }, '[Webhook] Received raw body'); if (!rawBody || rawBody.trim() === '') { return c.json({ error: 'Empty body' }, 400); } body = JSON.parse(rawBody); } catch (e) { - console.error(`[Webhook] Failed to parse JSON body:`, e); + logger.error({ err: e }, '[Webhook] Failed to parse JSON body'); return c.json({ error: 'Invalid JSON body' }, 400); } @@ -41,32 +42,50 @@ webhook.post('/:token/topic/:slug', async (c) => { with: { user: true } - } + }, + groupChats: true } }); if (!topic) { - console.warn(`[Webhook] Topic not found: ${slug}`); + logger.warn({ slug }, '[Webhook] Topic not found'); return c.json({ error: 'Topic not found' }, 404); } - console.log(`[Webhook] Found topic: ${topic.name}, subscribers: ${topic.subscriptions.length}`); + logger.info({ topicName: topic.name }, '[Webhook] Found topic'); - // 2. Collect subscribers - const subscribers = topic.subscriptions + // 2. Collect recipients + const userRecipients = topic.subscriptions .map(sub => sub.user) - .filter(u => !!u && !!u.feishuUserId); + .filter(u => !!u && !!u.feishuUserId) + .map(u => ({ + type: 'user', + id: u.id, + name: u.name, + feishuId: u.feishuUserId, + idType: u.feishuUserId.startsWith('ou_') ? 'open_id' : 'user_id' + })); + + const groupRecipients = topic.groupChats.map(g => ({ + type: 'group', + id: g.id, // Binding ID + name: g.name, + feishuId: g.chatId, + idType: 'chat_id' + })); + + const allRecipients = [...userRecipients, ...groupRecipients]; const [task] = await db.insert(alertTasks).values({ topicSlug: topic.slug, senderId: user.id, status: 'processing', - recipientCount: subscribers.length, + recipientCount: allRecipients.length, successCount: 0, payload: body, }).returning(); - if (subscribers.length === 0) { + if (allRecipients.length === 0) { await db.update(alertTasks) .set({ status: 'completed', updatedAt: new Date() }) .where(eq(alertTasks.id, task.id)); @@ -78,8 +97,14 @@ webhook.post('/:token/topic/:slug', async (c) => { }); } + logger.info({ + taskId: task.id, + userCount: userRecipients.length, + groupCount: groupRecipients.length + }, '[Webhook] Dispatching alerts'); + // 4. Send Private Messages asynchronously - Promise.allSettled(subscribers.map(async (user) => { + Promise.allSettled(allRecipients.map(async (recipient) => { try { // Construct message content let msgType = body.msg_type || 'text'; @@ -88,6 +113,10 @@ webhook.post('/:token/topic/:slug', async (c) => { if (!content) { msgType = 'text'; content = { text: JSON.stringify(body, null, 2) }; + // Deep copy needed? usually content is new obj if we parsed body + } else { + // Deep clone content to avoid mutating shared object for parallel requests if we modify it + content = JSON.parse(JSON.stringify(content)); } // Add metadata @@ -98,13 +127,16 @@ webhook.post('/:token/topic/:slug', async (c) => { content.header.title.content = `[${topic.name}] ${content.header.title.content}`; } - const idType = user.feishuUserId.startsWith('ou_') ? 'open_id' : 'user_id'; - await feishuClient.sendMessage(user.feishuUserId, idType, msgType, content); + await feishuClient.sendMessage(recipient.feishuId, recipient.idType as any, msgType, content); - return { userId: user.id, status: 'sent', error: null }; + return { recipientId: recipient.id, status: 'sent', error: null }; } catch (error: any) { - console.error(`Failed to send to user ${user.name}:`, error); - return { userId: user.id, status: 'failed', error: error.message }; + logger.error({ + err: error, + recipientType: recipient.type, + recipientName: recipient.name + }, 'Failed to send alert'); + return { recipientId: recipient.id, status: 'failed', error: error.message }; } })).then(async (results) => { const successCount = results.filter(r => r.status === 'fulfilled' && (r.value as any).status === 'sent').length; @@ -124,22 +156,22 @@ webhook.post('/:token/topic/:slug', async (c) => { error: failures > 0 ? `Failed to send to ${failures} recipients` : null, }).where(eq(alertTasks.id, task.id)); - // Insert Logs (Optional: insert only failures to save space, or all for audit) - // Let's insert all for now + // Insert Logs const logs = results.map((r, index) => { - const user = subscribers[index]; + const recipient = allRecipients[index]; if (r.status === 'fulfilled') { const val = r.value as any; return { taskId: task.id, - userId: user.id, + userId: recipient.type === 'user' ? recipient.id : null, // Only link users + // We could add connection to group binding if we altered schema, but for now log it status: val.status, error: val.error, }; } else { return { taskId: task.id, - userId: user.id, + userId: recipient.type === 'user' ? recipient.id : null, status: 'failed', error: r.reason ? String(r.reason) : 'Unknown error', }; @@ -150,20 +182,25 @@ webhook.post('/:token/topic/:slug', async (c) => { await db.insert(alertLogs).values(logs as any); } - console.log(`[Webhook] Task ${task.id}: Sent ${successCount}/${subscribers.length} alerts for topic ${slug}`); + logger.info({ + taskId: task.id, + successCount, + totalCount: allRecipients.length, + slug + }, '[Webhook] Task processed'); }); return c.json({ message: 'Alert received and processing started', taskId: task.id, status: 'processing', - recipientCount: subscribers.length + recipientCount: allRecipients.length }); }); webhook.post('/:token/dm', async (c) => { const token = c.req.param('token'); - console.log(`[Webhook] Received DM request for token: ${token}`); + logger.info({ token }, '[Webhook] Received DM request'); // 0. Find the User by Token const user = await db.query.users.findFirst({ @@ -171,7 +208,7 @@ webhook.post('/:token/dm', async (c) => { }); if (!user) { - console.warn(`[Webhook] Invalid personal token: ${token}`); + logger.warn({ token }, '[Webhook] Invalid personal token'); return c.json({ error: 'Invalid personal token' }, 401); } @@ -237,7 +274,7 @@ webhook.post('/:token/dm', async (c) => { }); } catch (error: any) { - console.error(`Failed to send DM to user ${user.name}:`, error); + logger.error({ err: error, userName: user.name }, 'Failed to send DM'); await db.update(alertTasks).set({ status: 'failed', updatedAt: new Date(), diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts new file mode 100644 index 0000000..fb68aed --- /dev/null +++ b/apps/server/src/ws.ts @@ -0,0 +1,22 @@ +import * as lark from '@larksuiteoapi/node-sdk'; +import { feishuClient } from './feishu'; +import { eventDispatcher } from './event-handler'; +import { logger } from './lib/logger'; + +export const startWebSocket = async () => { + if (process.env.FEISHU_USE_WS !== 'true') { + return; + } + + logger.info('[Feishu WS] Starting WebSocket connection...'); + try { + const wsClient = new lark.WSClient({ + appId: feishuClient.appId, + appSecret: feishuClient.appSecret, + }); + await wsClient.start({ eventDispatcher }); + logger.info('[Feishu WS] Connected successfully'); + } catch (e) { + logger.error({ err: e }, '[Feishu WS] Connection failed'); + } +}; diff --git a/apps/web/package.json b/apps/web/package.json index ecec8bd..949cef5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@alertmessagecenter/web", - "version": "1.0.0", + "version": "1.2.0", "type": "module", "scripts": { "dev": "bun run --env-file .env vite", diff --git a/apps/web/src/components/GroupBindingsModal.tsx b/apps/web/src/components/GroupBindingsModal.tsx new file mode 100644 index 0000000..f61421e --- /dev/null +++ b/apps/web/src/components/GroupBindingsModal.tsx @@ -0,0 +1,196 @@ +import { useState, useEffect } from 'react'; +import { Trash2, Plus, MessageCircle } from 'lucide-react'; +import Modal from './Modal'; +import { client } from '../lib/client'; + +interface GroupBinding { + id: string; + chatId: string; + name: string; +} + +interface KnownGroup { + chatId: string; + name: string; + lastActiveAt: string; +} + +interface GroupBindingsModalProps { + isOpen: boolean; + onClose: () => void; + topicId: string; + topicName: string; +} + +export default function GroupBindingsModal({ isOpen, onClose, topicId, topicName }: GroupBindingsModalProps) { + // const { user } = useAuth(); // Unused + const [bindings, setBindings] = useState([]); + const [knownGroups, setKnownGroups] = useState([]); + const [selectedChatId, setSelectedChatId] = useState(''); + const [loading, setLoading] = useState(false); + const [status, setStatus] = useState<{ type: 'success' | 'error', message: string } | null>(null); + + useEffect(() => { + if (isOpen && topicId) { + fetchBindings(); + fetchKnownGroups(); + setStatus(null); + setSelectedChatId(''); + } + }, [isOpen, topicId]); + + const fetchBindings = async () => { + try { + const res = await client.api.topics[':id'].groups.$get({ + param: { id: topicId } + }, { + init: { credentials: 'include' } + }); + const data = await res.json(); + setBindings(data as any); + } catch (err) { + console.error(err); + } + }; + + const fetchKnownGroups = async () => { + try { + const res = await client.api.groups.$get(undefined, { + init: { credentials: 'include' } + }); + const data = await res.json(); + // Only verify uniqueness if needed, but here we just list what server returns + setKnownGroups(data as any); + } catch (err) { + console.error(err); + } + }; + + const handleBind = async () => { + if (!selectedChatId) return; + setLoading(true); + setStatus(null); + + const group = knownGroups.find(g => g.chatId === selectedChatId); + if (!group) return; + + try { + const res = await client.api.topics[':id'].groups.$post({ + param: { id: topicId }, + json: { + chatId: group.chatId, + name: group.name, + } + }, { + init: { credentials: 'include' } + }); + + if (res.ok) { + setStatus({ type: 'success', message: 'Group bound successfully!' }); + fetchBindings(); + setSelectedChatId(''); + } else { + await res.json(); // Consume body + setStatus({ type: 'error', message: 'Failed to bind group' }); + } + } catch (_) { // Ignore error + setStatus({ type: 'error', message: 'An error occurred' }); + } finally { + setLoading(false); + } + }; + + const handleUnbind = async (bindingId: string) => { + if (!confirm('Are you sure you want to remove this group binding?')) return; + + try { + const res = await client.api.topics[':id'].groups[':bindingId'].$delete({ + param: { id: topicId, bindingId } + }, { + init: { credentials: 'include' } + }); + + if (res.ok) { + setBindings(prev => prev.filter(b => b.id !== bindingId)); + } + } catch (err) { + console.error(err); + } + }; + + // Filter out groups that are already bound + const availableGroups = knownGroups.filter( + kg => !bindings.some(b => b.chatId === kg.chatId) + ); + + return ( + +
+
+

Bound Groups

+ {bindings.length === 0 ? ( +

No groups bound to this topic yet.

+ ) : ( +
    + {bindings.map(binding => ( +
  • +
    + + {binding.name} +
    + +
  • + ))} +
+ )} +
+ +
+

Add Group Binding

+

+ Select a group where the Feishu Bot has been added. If your group is not listed, try removing and re-adding the bot to the group. +

+ +
+ + +
+ {status && ( +

+ {status.message} +

+ )} +
+
+
+ ); +} diff --git a/apps/web/src/views/TopicsView.tsx b/apps/web/src/views/TopicsView.tsx index d73eda7..19b3439 100644 --- a/apps/web/src/views/TopicsView.tsx +++ b/apps/web/src/views/TopicsView.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; -import { Plus, Settings, UserPlus, UserMinus, Copy, Check, User, ShieldCheck } from 'lucide-react'; +import { Plus, Settings, UserPlus, UserMinus, Copy, Check, User, ShieldCheck, Users } from 'lucide-react'; import Modal from '../components/Modal'; +import GroupBindingsModal from '../components/GroupBindingsModal'; import { useAuth } from '../contexts/AuthContext'; import { client } from '../lib/client'; @@ -23,6 +24,7 @@ interface Topic { subscriptions: Subscription[]; creator?: User; approver?: User; + createdBy?: string; } export default function TopicsView() { @@ -34,6 +36,7 @@ export default function TopicsView() { const [isModalOpen, setIsModalOpen] = useState(false); const [isSubModalOpen, setIsSubModalOpen] = useState(false); const [selectedTopic, setSelectedTopic] = useState(null); + const [isGroupModalOpen, setIsGroupModalOpen] = useState(false); const [copiedId, setCopiedId] = useState(null); const [formData, setFormData] = useState>({ @@ -129,6 +132,11 @@ export default function TopicsView() { setIsSubModalOpen(true); }; + const handleGroupClick = (topic: Topic) => { + setSelectedTopic(topic); + setIsGroupModalOpen(true); + }; + const toggleSubscription = async (topicId: string, userId: string, isSubscribed: boolean) => { try { console.log('Toggling subscription:', { topicId, userId, isSubscribed }); @@ -315,14 +323,23 @@ export default function TopicsView() { )} - {currentUser?.isAdmin && ( + {currentUser && (currentUser.isAdmin || currentUser.id === topic.createdBy) && ( <> + {currentUser.isAdmin && ( + + )} )} @@ -537,6 +554,15 @@ export default function TopicsView() { + + {selectedTopic && ( + setIsGroupModalOpen(false)} + topicId={selectedTopic.id} + topicName={selectedTopic.name} + /> + )} ); } diff --git a/bun.lock b/bun.lock index 8b194ba..66e86c0 100644 --- a/bun.lock +++ b/bun.lock @@ -9,11 +9,13 @@ }, "apps/server": { "name": "@alertmessagecenter/server", - "version": "1.0.0", + "version": "1.2.0", "dependencies": { "@hono/zod-validator": "^0.7.6", + "@larksuiteoapi/node-sdk": "^1.56.1", "drizzle-orm": "^0.45.1", "hono": "^4.11.3", + "pino": "^10.1.1", "postgres": "^3.4.8", "zod": "^3.0.0", }, @@ -21,11 +23,12 @@ "@types/node": "^20.0.0", "bun-types": "latest", "drizzle-kit": "^0.31.8", + "pino-pretty": "^13.1.3", }, }, "apps/web": { "name": "@alertmessagecenter/web", - "version": "1.0.0", + "version": "1.2.0", "dependencies": { "clsx": "^2.0.0", "hono": "^4.11.3", @@ -165,12 +168,36 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@larksuiteoapi/node-sdk": ["@larksuiteoapi/node-sdk@1.56.1", "", { "dependencies": { "axios": "0.27.2", "lodash.identity": "^3.0.0", "lodash.merge": "^4.6.2", "lodash.pickby": "^4.6.0", "protobufjs": "^7.2.6", "qs": "^6.13.0", "ws": "^8.16.0" } }, "sha512-/ixtyJnWOmcupKgDXz+6G6qTLMi3cNrR+LGOuq2PMwcJ6hhXTUJNyAF+ADY7ah9OoeDniGU/UJwMb2gqKdxwcA=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], @@ -243,8 +270,14 @@ "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], + "axios": ["axios@0.27.2", "", { "dependencies": { "follow-redirects": "^1.14.9", "form-data": "^4.0.0" } }, "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], @@ -267,6 +300,10 @@ "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], "caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="], @@ -277,6 +314,10 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -285,12 +326,16 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], @@ -301,10 +346,20 @@ "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], @@ -313,8 +368,12 @@ "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -323,6 +382,10 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], @@ -333,14 +396,26 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + "hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -361,6 +436,8 @@ "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -371,16 +448,30 @@ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "lodash.identity": ["lodash.identity@3.0.0", "", {}, "sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.pickby": ["lodash.pickby@4.6.0", "", {}, "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lucide-react": ["lucide-react@0.300.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-rQxUUCmWAvNLoAsMZ5j04b2+OJv6UuNLYMY7VK0eVlm4aTwUEjEEHc09/DipkNIlhXUSDn2xoyIzVT0uh7dRsg=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -405,6 +496,10 @@ "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], @@ -415,6 +510,14 @@ "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + "pino": ["pino@10.1.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-3qqVfpJtRQUCAOs4rTOEwLH6mwJJ/CSAlbis8fKOiMzTtXh0HN/VLsn3UWVTJ7U8DsWmxeNon2IpGb+wORXH4g=="], + + "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], + + "pino-pretty": ["pino-pretty@13.1.3", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg=="], + + "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], @@ -435,10 +538,18 @@ "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], @@ -453,6 +564,8 @@ "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], @@ -465,23 +578,39 @@ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], @@ -499,6 +628,8 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -519,10 +650,14 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@alertmessagecenter/server/bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -531,6 +666,8 @@ "node-abi/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], diff --git a/docs/copilot-context.md b/docs/copilot-context.md index ca879b7..26320aa 100644 --- a/docs/copilot-context.md +++ b/docs/copilot-context.md @@ -1,4 +1,4 @@ -# Project Context for GitHub Copilot (v1.1.1) +# Project Context for GitHub Copilot (v1.2.1) This document provides technical context, architectural decisions, and code conventions for the **Alert Message Center** project. It is intended to help AI assistants understand the codebase. @@ -9,7 +9,8 @@ This document provides technical context, architectural decisions, and code conv - **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**. + - **Group Chat**: Alerts can be dispatched to Feishu Group Chats where the App Bot is a member. + - **Dispatch**: The system sends messages via **Feishu (Lark) Private Messages** or **Group Messages**. - **Runtime**: Bun (JavaScript/TypeScript runtime). ## 2. Tech Stack @@ -21,7 +22,7 @@ This document provides technical context, architectural decisions, and code conv - **Database**: PostgreSQL. - **ORM**: Drizzle ORM. - **Authentication**: Feishu OAuth2 (Session-based with cookies). - - **External API**: Feishu Open Platform (Server-side API). + - **External API**: Feishu Open Platform (Server-side API via `@larksuiteoapi/node-sdk`). - **Frontend**: - **Build Tool**: Vite. - **Framework**: React. @@ -56,6 +57,36 @@ The database schema is defined in `apps/server/src/db/schema.ts`. - `userId`: Foreign Key -> `users.id`. - **Relationship**: Many-to-Many between Topics and Users. +4. **Topic Group Chat** (`topic_group_chats`) + - `id`: UUID (Primary Key). + - `topicId`: Foreign Key -> `topics.id`. + - `chatId`: The Feishu `chat_id`. + - `name`: Group name (snapshot). + - **Relationship**: Many-to-Many between Topics and Feishu Groups. + +5. **Known Group Chat** (`known_group_chats`) + - `chatId`: Feishu `chat_id` (Primary Key). + - `name`: Group name. + - `lastActiveAt`: Timestamp of last event from this group. + - **Purpose**: Caches groups the bot has been added to, facilitating easy selection in the UI. +7. **Alert Task** (`alert_tasks`) + - `id`: UUID (Primary Key). + - `topicSlug`: The slug of the target topic (or `NULL` for DM). + - `senderId`: Foreign Key -> `users.id` (who triggered the webhook). + - `status`: `pending`, `processing`, `completed`, or `failed`. + - `recipientCount`: Total recipients (subscribers + groups). + - `successCount`: Number of successful deliveries. + - `payload`: Snapshot of the incoming webhook body (JSONB). + - `error`: Last error message if failed. + - **Purpose**: Tracks the lifecycle of a single alert ingestion events. + +8. **Alert Log** (`alert_logs`) + - `id`: UUID (Primary Key). + - `taskId`: Foreign Key -> `alert_tasks.id`. + - `userId`: Target user open_id (snapshot). + - `status`: `sent` or `failed`. + - **Purpose**: Granular tracking for each individual delivery within a task. + ## 4. Key Workflows ### Authentication @@ -87,7 +118,26 @@ The database schema is defined in `apps/server/src/db/schema.ts`. - Call `FeishuClient.sendMessage` for each recipient. - **Payload**: Supports `text` and `interactive` (Feishu Card) message types. -### Subscription Management + - Call `FeishuClient.sendMessage` for each recipient. + - **Payload**: Supports `text` and `interactive` (Feishu Card) message types. + +### Feishu Group Chat Integration +- **Strategy**: App Bot in Group. +- **Discovery**: + - The system listens for `im.chat.member.bot.added_v1` events (via Webhook or WebSocket). + - When the bot is added to a group, the group details are cached in `known_group_chats`. +- **Bot Removal**: + - The system listens for `im.chat.member.bot.deleted_v1` events. + - When the bot is removed, the cached group is deleted from `known_group_chats`. + - **Auto-Unbind**: All bindings in `topic_group_chats` for that `chat_id` are automatically deleted to ensure data consistency. +- **Binding**: Admins bind a Topic to a known Feishu Group in the UI. +- **Dispatch**: Alerts for the topic are sent to all bound `chat_id`s in addition to individual subscribers. + +### Long Connection (WebSocket) +- **Problem**: Intranet deployments cannot receive public Webhook callbacks from Feishu. +- **Solution**: Use Feishu Open Platform's WebSocket mode. +- **Configuration**: Set `FEISHU_USE_WS=true` in `.env`. +- **Implementation**: Uses `@larksuiteoapi/node-sdk` to establish a persistent connection and receive events like `im.chat.member.bot.added_v1`. - Users can subscribe/unsubscribe themselves to any topic. - Admins can manage subscriptions for other users globally in `AdminView`. - **Topic Deletion**: Centralized in the **Admin Dashboard (All Topics Tab)** to avoid accidental deletion from the main topic list. @@ -114,7 +164,18 @@ The database schema is defined in `apps/server/src/db/schema.ts`. - `POST /api/topics/:id/subscribe/:userId`: Subscribe. - `DELETE /api/topics/:id/subscribe/:userId`: Unsubscribe. - `GET /api/users`: List users (Admin only). +- `GET /api/users`: List users (Admin only). +### Feishu Group Management +- `GET /api/groups`: List known groups (cached from bot events). +- `GET /api/topics/:id/groups`: List group bindings for a topic. +- `POST /api/topics/:id/groups`: Bind a group to a topic. +- `DELETE /api/topics/:id/groups/:bindingId`: Unbind a group. + +### Feishu Event +- `POST /api/feishu/event`: Endpoint for receiving Feishu events (Webhook mode). + - **Note**: This endpoint uses **manual challenge handling** (`lark.generateChallenge`) and `eventDispatcher.invoke` instead of the SDK's `adaptDefault` to maintain compatibility with Hono's non-standard Node.js response object. + - **Signature Verification Hack**: To preserve Feishu's signature verification, the internal `invoke` call uses `Object.create({ headers })` to inject HTTP headers on the prototype of the payload object. This ensures headers are accessible to the SDK's internal verification logic but are **excluded** from `JSON.stringify`, which is critical for matching the SHA256 content checksum. ### Webhook - `POST /api/webhook/:token/topic/:slug`: Trigger an alert for a topic. @@ -123,9 +184,9 @@ The database schema is defined in `apps/server/src/db/schema.ts`. ## 6. Future Roadmap (Planned) - [ ] **Message Preview**: Preview Feishu card JSON in the UI. -- [ ] **History/Logs**: Keep a log of sent alerts for auditing. +- [x] **History/Logs**: Tracking for sent alerts (Alert Tasks/Logs). - [ ] **Retry Mechanism**: Handle Feishu API failures. -- [x] **Deployment**: Dockerfile and deployment scripts. +- [x] **Deployment**: Dockerfile and CI/CD. ## 7. Development Conventions @@ -133,9 +194,19 @@ The database schema is defined in `apps/server/src/db/schema.ts`. - **Styling**: Use Tailwind utility classes directly in JSX. - **Async/Await**: Prefer `async/await` over `.then()`. - **Type Safety**: strict TypeScript usage. Backend and Frontend share types via Hono RPC or shared interfaces. +- **Logging**: + - Framework: `pino`. + - **Structured Log**: Use JSON format for easy parsing and aggregation. + - **Contextual Data**: Pass objects as the first argument to `logger` methods (e.g., `logger.error({ err, chatId }, 'message')`) for indexed search. + - **Dev Mode**: Uses `pino-pretty` for human-friendly output during development. - **Environment Isolation**: - Each workspace (`apps/server`, `apps/web`) uses its own `.env` file via Bun's `--env-file .env` flag. - Development proxy target for the frontend is configurable via `VITE_API_URL` (default: `http://localhost:3000`). +- **Critical Environment Variables**: + - `FEISHU_ENCRYPT_KEY`: Essential for the `lark.generateChallenge` and event signature verification. + - `FEISHU_VERIFICATION_TOKEN`: Used by `EventDispatcher` for event authentication. + - `FEISHU_USE_WS`: Set to `true` to enable WebSocket mode (bypasses `feishu-event.ts`). + - `ADMIN_EMAILS`: Comma-separated list of emails that automatically receive `isAdmin=true` upon first login. - **CI/CD**: - GitHub Actions automates building a multi-stage Docker image and pushing it to GitHub Container Registry (GHCR). - Image path: `ghcr.io/${USER}/alert-message-center`. diff --git a/docs/images/group_alert.png b/docs/images/group_alert.png new file mode 100644 index 0000000..07f3fb5 Binary files /dev/null and b/docs/images/group_alert.png differ diff --git a/docs/images/group_binding.png b/docs/images/group_binding.png new file mode 100644 index 0000000..268f9fb Binary files /dev/null and b/docs/images/group_binding.png differ diff --git a/docs/images/topics_view.png b/docs/images/topics_view.png index 0a871ff..a95c30f 100644 Binary files a/docs/images/topics_view.png and b/docs/images/topics_view.png differ diff --git a/todo.md b/todo.md index 353b022..b0ef349 100644 --- a/todo.md +++ b/todo.md @@ -22,4 +22,8 @@ - [x] **Personal Inbox**: Direct alert delivery bypassing topics. - [ ] **Retry Mechanism**: Handle Feishu API failures. - [x] **Deployment**: Dockerfile and CI/CD (GitHub Actions + GHCR). +- [x] **Feishu Group Chat**: Event-based group discovery and alerting (App Bot). +- [x] **Auto-Cleanup**: Unbind subscriptions when bot is removed from group. +- [x] **Long Connection**: WebSocket support for intranet deployments. +- [x] **Structured Logging**: Integrated `pino` for better observability.