Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/feishu/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ class FeishuClient extends EventEmitter {
// 机器人自身信息
private botOpenId: string | null = null;

// message_id 去重缓存:WebSocket 可能重复投递同一条消息
private _recentMessageIds = new Map<string, number>();
private readonly _RECENT_MESSAGE_TTL_MS = 30000;

constructor() {
super();
this.client = new lark.Client({
Expand Down Expand Up @@ -153,6 +157,10 @@ class FeishuClient extends EventEmitter {

// 启动长连接
async start(): Promise<void> {
if (this.connectionState === 'connected' || this.connectionState === 'connecting') {
console.log('[飞书] 长连接已连接/连接中,忽略重复调用');
return;
}
console.log('[飞书] 正在启动长连接...');
this.connectionState = 'connecting';

Expand Down Expand Up @@ -243,6 +251,26 @@ class FeishuClient extends EventEmitter {
return;
}

// 消息去重:飞书 WebSocket 可能重复投递同一条消息
const msgId = message.message_id;
if (msgId) {
const now = Date.now();
const recent = this._recentMessageIds.get(msgId);
if (recent !== undefined && now - recent < this._RECENT_MESSAGE_TTL_MS) {
console.log(`[飞书] 忽略重复消息: messageId=${msgId.slice(0, 16)}...`);
return;
}
this._recentMessageIds.set(msgId, now);
// 惰性清理:每收到 500 条消息后扫一遍过期记录
if (this._recentMessageIds.size >= 500) {
for (const [id, ts] of this._recentMessageIds) {
if (now - ts >= this._RECENT_MESSAGE_TTL_MS) {
this._recentMessageIds.delete(id);
}
}
}
}

const msgType = message.message_type;
let content = '';
let parsedContent: Record<string, unknown> | null = null;
Expand Down
20 changes: 20 additions & 0 deletions src/handlers/p2p.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export class P2PHandler {
private createChatDirectoryInputMap: Map<string, { value: string; expiresAt: number }> = new Map();
private createChatNameInputMap: Map<string, { value: string; expiresAt: number }> = new Map();

// per-chatId 互斥锁,防 ensurePrivateSession TOCTOU 竞态
private _ensureSessionLocks = new Map<string, Promise<EnsurePrivateSessionResult | null>>();

private async safeReply(
messageId: string | undefined,
chatId: string | undefined,
Expand Down Expand Up @@ -470,6 +473,23 @@ export class P2PHandler {
}

private async ensurePrivateSession(chatId: string, senderId: string): Promise<EnsurePrivateSessionResult | null> {
// per-chatId 互斥锁:等第一个完成,防止并发的 handleMessage 都走到创建 session
const pending = this._ensureSessionLocks.get(chatId);
if (pending) {
return await pending;
}
const promise = this._ensurePrivateSessionImpl(chatId, senderId);
this._ensureSessionLocks.set(chatId, promise);
try {
return await promise;
} finally {
if (this._ensureSessionLocks.get(chatId) === promise) {
this._ensureSessionLocks.delete(chatId);
}
}
}

private async _ensurePrivateSessionImpl(chatId: string, senderId: string): Promise<EnsurePrivateSessionResult | null> {
const current = chatSessionStore.getSession(chatId);
if (current?.sessionId) {
const missing = await this.isSessionMissingInOpenCode(current.sessionId);
Expand Down