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

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