feat: fix ws error and unify logger

Signed-off-by: d0zingcat <iamtangli42@gmail.com>
This commit is contained in:
2026-01-14 11:43:39 +08:00
parent a1c6141b31
commit 9afe1f093c
12 changed files with 155 additions and 31 deletions

View File

@@ -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
### 新增

View File

@@ -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"
}
}

View File

@@ -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);
}
});

View File

@@ -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));
}
},
});

View File

@@ -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;

View File

@@ -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);
});

View File

@@ -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;

View File

@@ -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(),

View File

@@ -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');
}
};

View File

@@ -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=="],

View File

@@ -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`).

View File

@@ -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.