import * as Lark from '@larksuiteoapi/node-sdk'; import type { MysqlFeishuBotRepository } from './feishu/feishu_bot.repository.js'; import type { SessionService } from './session/session.service.js'; import type { SessionRepository } from './session/session.repository.js'; import type { AgentService } from './agent/agent.service.js'; import type { ModelService } from './model/model.service.js'; import type { FeishuInboundContext, FeishuNormalizedMessage } from './feishu/types.js'; import type { FeishuMessageService } from './feishu/message.service.js'; import type { MysqlFeishuBindingRepository } from './feishu/binding.repository.js'; import type { MysqlFeishuMessageRepository } from './feishu/message.repository.js'; import type { EcpAuthService } from './auth/ecp-auth.service.js'; import type { MysqlEcpSessionRepository } from './auth/ecp-session.repository.js'; import type { SystemSettingService } from './settings/settings.service.js'; import { createSessionDetailUrlResolver } from './feishu/progress-card.js'; import { senderName } from './feishu/message.service.js'; import { hasUsableEcpSession } from './auth/ecp-session.repository.js'; import { senderUnionIdOf } from './feishu/ecp-inbound-gate.js'; import { buildQuotedInjection } from './feishu/quoted-injection.js'; import { resolveFeishuQuotedText } from './feishu/merge-forward.js'; import { fetchFeishuMessageDetail } from './feishu/message-detail.js'; import { formatGroupTime } from './feishu/message.service.js'; // ========== 会话配置 ========== export interface FeishuSessionDeps { feishuBots: MysqlFeishuBotRepository; sessionService: SessionService; sessionRepo: SessionRepository; agentService: AgentService; modelService: ModelService; getFeishuChatTitle: (botId: number, chatId: string) => Promise; } export interface FeishuSessionHelper { applyFeishuBotConfig(sessionId: number, botId: number): Promise; ensureFeishuSessionTitle(sessionId: number, accountId: number, context: FeishuInboundContext): Promise; } export function createFeishuSessionHelper(deps: FeishuSessionDeps): FeishuSessionHelper { const { feishuBots, sessionService, sessionRepo, agentService, modelService, getFeishuChatTitle } = deps; const applyFeishuBotConfig = async (sessionId: number, botId: number): Promise => { const bot = await feishuBots.findById(botId); if (bot == null) return; const session = await sessionService.getSession(sessionId); if (session == null || session.id == null) return; const agent = bot.agentId != null ? await agentService.getAgent(bot.agentId) : await agentService.requireDefaultAgent(); const model = bot.modelId != null ? await modelService.getModel(bot.modelId) : await modelService.getDefaultModel(); const agentId = agent?.id ?? null; const modelId = model?.id ?? null; const fields: Record = {}; if (agentId != null && session.agentId !== agentId) fields.agentId = agentId; if (session.modelId !== modelId) fields.modelId = modelId; if (Object.keys(fields).length > 0) { console.info(`飞书会话热切换 Agent/模型, sessionId=${sessionId}, botId=${botId}`, fields); await sessionRepo.updateFields(session.id, fields); } }; const ensureFeishuSessionTitle = async (sessionId: number, accountId: number, context: FeishuInboundContext): Promise => { try { if (context.chatType !== 'group' || context.chatId == null) return; const session = await sessionService.getSession(sessionId); if (session?.title !== '飞书Bot会话') return; const title = await getFeishuChatTitle(accountId, context.chatId); if (title === '') return; await sessionRepo.updateFields(sessionId, { title }); console.info(`飞书会话标题更新为群名称, sessionId=${sessionId}, chatId=${context.chatId}, title=${title}`); } catch (error) { console.warn(`更新飞书会话标题失败, sessionId=${sessionId}: ${error instanceof Error ? error.message : String(error)}`); } }; return { applyFeishuBotConfig, ensureFeishuSessionTitle }; } // ========== 会话详情 URL ========== export interface FeishuSessionUrlDeps { settingService: SystemSettingService; } export function createFeishuSessionUrlHelper(deps: FeishuSessionUrlDeps) { const { settingService } = deps; return createSessionDetailUrlResolver({ getSiteBaseUrl: () => settingService.getSiteBaseUrl(), }); } // ========== 话题会话 ========== export interface ThreadSessionDeps { feishuMessageService: FeishuMessageService; } export function createThreadSessionHelper(deps: ThreadSessionDeps) { const { feishuMessageService } = deps; return { resolveThreadSession: async (accountId: string, event: FeishuNormalizedMessage): Promise<{ sessionId: number } | null> => { const result = await feishuMessageService.findThreadSession(String(accountId), event.threadId); return result == null ? null : { sessionId: result.sessionId }; }, ensureThreadSession: async (accountId: string, context: FeishuInboundContext): Promise<{ sessionId: number } | null> => { const result = await feishuMessageService.getOrCreateThreadSession(String(accountId), context); return result == null ? null : { sessionId: result.sessionId }; }, }; } // ========== 用户 ID 解析 ========== export interface FeishuUserIdDeps { feishuBinding: MysqlFeishuBindingRepository; } export function createFeishuUserIdHelper(deps: FeishuUserIdDeps) { const { feishuBinding } = deps; return async (accountId: string, context: FeishuInboundContext): Promise => { const unionId = context.senderUnionId ?? context.senderId; if (unionId == null) return undefined; return (await feishuBinding.findUserIdByUnionId(unionId)) ?? undefined; }; } export function createResolveUserIdHelper(deps: FeishuUserIdDeps) { const { feishuBinding } = deps; return async (_accountId: string, event: FeishuNormalizedMessage): Promise => { const unionId = senderUnionIdOf(event); if (unionId == null) return null; return (await feishuBinding.findUserIdByUnionId(unionId)) ?? null; }; } // ========== 发送者授权 ========== export interface AuthorizeSenderDeps { feishuBinding: MysqlFeishuBindingRepository; feishuMessageRepository: MysqlFeishuMessageRepository; ecpAuth: EcpAuthService; ecpSessionRepo: MysqlEcpSessionRepository; } export function createAuthorizeSenderHelper(deps: AuthorizeSenderDeps) { const { feishuBinding, feishuMessageRepository, ecpAuth, ecpSessionRepo } = deps; return async (accountId: string, event: FeishuNormalizedMessage): Promise => { const unionId = senderUnionIdOf(event); if (unionId == null) return false; const userId = await feishuBinding.findUserIdByUnionId(unionId); if (userId == null) return false; if (await ecpAuth.isEnabled() && !(await hasUsableEcpSession(ecpSessionRepo, userId))) return false; if (event.chatType === 'group') { if (event.chatId == null) return false; await feishuMessageRepository.addGroupMember(String(accountId), event.chatId, userId, event.senderId!, senderName(event)); return true; } return true; }; } // ========== 引用消息解析 ========== export interface ResolveQuotedMessageDeps { getFeishuClient: (botId: number) => Promise; feishuMessageRepository: MysqlFeishuMessageRepository; resolveQuotedWorkspace: (accountId: string, event: FeishuNormalizedMessage) => Promise; expandMergeForward: (accountId: string, messageId: string, workspace: string | null) => Promise; } export function createResolveQuotedMessageHelper(deps: ResolveQuotedMessageDeps) { const { getFeishuClient, feishuMessageRepository, resolveQuotedWorkspace, expandMergeForward } = deps; return async (accountId: string, event: FeishuNormalizedMessage): Promise => { if (event.parentId == null) return null; const persist = async (raw: string): Promise => { return buildQuotedInjection(raw, { parentMessageId: event.parentId!, workspace: await resolveQuotedWorkspace(accountId, event) }); }; const fromLog = event.chatId != null ? await feishuMessageRepository.findGroupMessageByMessageId(String(accountId), event.chatId, event.parentId) : null; return resolveFeishuQuotedText({ log: fromLog == null ? null : { msgType: fromLog.msgType, content: fromLog.content, line: `[${formatGroupTime(fromLog.createdAt)}] ${fromLog.senderName}:${fromLog.content ?? ''}`, }, fetchDetail: async () => { const client = await getFeishuClient(Number(accountId)); if (client == null) return null; const detail = await fetchFeishuMessageDetail(client, event.parentId!); return detail == null ? null : { msgType: detail.msgType, text: detail.text }; }, expand: (workspace) => expandMergeForward(accountId, event.parentId!, workspace), resolveWorkspace: () => resolveQuotedWorkspace(accountId, event), persist, }); }; }