diff --git a/CHANGELOG.md b/CHANGELOG.md index 266355c..8e25c1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ 本文件的格式基于 [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 解析逻辑。 +- **群聊解绑**: 增加对 `im.chat.member.bot.deleted_v1` 事件的支持。当机器人被移除群聊时,自动清理 `known_group_chats` 和 `topic_group_chats` 关联,确保订阅关系自动解绑。 + +### 新增 +- **结构化日志**: 引入 `pino` 框架替代 `console.log`,实现结构化 JSON 日志输出。 + - 在开发环境集成 `pino-pretty` 提供人类友好格式。 + - 支持通过环境遍历控制日志级别。 + ## [1.2.0] - 2026-01-13 ### 新增 diff --git a/apps/server/package.json b/apps/server/package.json index 29727ec..4e6b277 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -14,12 +14,14 @@ "@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/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/event-handler.ts b/apps/server/src/event-handler.ts index c260038..1f112de 100644 --- a/apps/server/src/event-handler.ts +++ b/apps/server/src/event-handler.ts @@ -1,13 +1,13 @@ import { db } from './db'; -import { knownGroupChats } from './db/schema'; +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({}).register({ 'im.chat.member.bot.added_v1': async (data) => { - const payload = data as any; - const { chat_id, name } = payload.chat || payload.message?.chat || {}; - console.log(`[Feishu Event] Bot added to group: ${name} (${chat_id})`); + 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({ @@ -23,4 +23,13 @@ export const eventDispatcher = new lark.EventDispatcher({}).register({ }); } }, + '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 96033ae..e79626a 100644 --- a/apps/server/src/feishu.ts +++ b/apps/server/src/feishu.ts @@ -1,11 +1,14 @@ import * as lark from '@larksuiteoapi/node-sdk'; +import { logger } from './lib/logger'; export class FeishuClient { public client: lark.Client; - private appId: string; + public appId: string; + public appSecret: string; constructor(appId: string, appSecret: string) { this.appId = appId; + this.appSecret = appSecret; this.client = new lark.Client({ appId: appId, appSecret: appSecret, @@ -31,7 +34,7 @@ export class FeishuClient { }); if (response.code !== 0) { - console.error('Feishu send message error:', response); + logger.error({ response }, 'Feishu send message error'); throw new Error(`Failed to send message: ${response.msg}`); } return response.data; @@ -51,7 +54,7 @@ export class FeishuClient { }); if (response.code !== 0) { - console.error('Feishu get user access token error:', response); + logger.error({ response }, 'Feishu get user access token error'); throw new Error(`Failed to get user access token: ${response.msg}`); } return response.data; diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index ff5cc2b..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'; @@ -30,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); }); 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 1d54679..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); } @@ -47,11 +48,11 @@ webhook.post('/:token/topic/:slug', async (c) => { }); 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}`); + logger.info({ topicName: topic.name }, '[Webhook] Found topic'); // 2. Collect recipients const userRecipients = topic.subscriptions @@ -96,7 +97,11 @@ webhook.post('/:token/topic/:slug', async (c) => { }); } - console.log(`[Webhook] Task ${task.id}: Dispatching to ${userRecipients.length} users and ${groupRecipients.length} groups`); + logger.info({ + taskId: task.id, + userCount: userRecipients.length, + groupCount: groupRecipients.length + }, '[Webhook] Dispatching alerts'); // 4. Send Private Messages asynchronously Promise.allSettled(allRecipients.map(async (recipient) => { @@ -126,7 +131,11 @@ webhook.post('/:token/topic/:slug', async (c) => { return { recipientId: recipient.id, status: 'sent', error: null }; } catch (error: any) { - console.error(`Failed to send to ${recipient.type} ${recipient.name}:`, error); + 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) => { @@ -173,7 +182,12 @@ webhook.post('/:token/topic/:slug', async (c) => { await db.insert(alertLogs).values(logs as any); } - console.log(`[Webhook] Task ${task.id}: Sent ${successCount}/${allRecipients.length} alerts for topic ${slug}`); + logger.info({ + taskId: task.id, + successCount, + totalCount: allRecipients.length, + slug + }, '[Webhook] Task processed'); }); return c.json({ @@ -186,7 +200,7 @@ 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}`); + logger.info({ token }, '[Webhook] Received DM request'); // 0. Find the User by Token const user = await db.query.users.findFirst({ @@ -194,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); } @@ -260,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 index 2c8db94..fb68aed 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -1,16 +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; } - console.log('[Feishu WS] Starting WebSocket connection...'); + logger.info('[Feishu WS] Starting WebSocket connection...'); try { - await (feishuClient.client as any).ws.start(eventDispatcher); - console.log('[Feishu WS] Connected successfully'); + const wsClient = new lark.WSClient({ + appId: feishuClient.appId, + appSecret: feishuClient.appSecret, + }); + await wsClient.start({ eventDispatcher }); + logger.info('[Feishu WS] Connected successfully'); } catch (e) { - console.error('[Feishu WS] Connection failed:', e); + logger.error({ err: e }, '[Feishu WS] Connection failed'); } }; diff --git a/bun.lock b/bun.lock index 9eb1200..66e86c0 100644 --- a/bun.lock +++ b/bun.lock @@ -9,12 +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", }, @@ -22,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", @@ -174,6 +176,8 @@ "@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=="], @@ -268,6 +272,8 @@ "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=="], @@ -308,6 +314,8 @@ "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=="], @@ -318,6 +326,8 @@ "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=="], @@ -358,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=="], @@ -400,6 +414,8 @@ "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=="], @@ -420,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=="], @@ -480,6 +498,8 @@ "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=="], @@ -490,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=="], @@ -510,6 +538,8 @@ "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=="], @@ -518,6 +548,8 @@ "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=="], @@ -532,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=="], @@ -544,8 +578,12 @@ "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=="], @@ -560,15 +598,19 @@ "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=="], @@ -586,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=="], @@ -622,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 7117519..6ff79ca 100644 --- a/docs/copilot-context.md +++ b/docs/copilot-context.md @@ -1,4 +1,4 @@ -# Project Context for GitHub Copilot (v1.1.2) +# 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. @@ -109,6 +109,10 @@ The database schema is defined in `apps/server/src/db/schema.ts`. - **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. @@ -171,6 +175,11 @@ 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`). diff --git a/todo.md b/todo.md index 2e5c188..b0ef349 100644 --- a/todo.md +++ b/todo.md @@ -23,5 +23,7 @@ - [ ] **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.