/* eslint-disable @typescript-eslint/no-explicit-any */ import { mkdirSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import type { Db } from './db/db.js'; import type { AppConfig } from './config/app-config.js'; import type { AppContext } from './create-app.js'; import type { MysqlFeishuMessageRepository } from './feishu/message.repository.js'; import type { FeishuMessageService } from './feishu/message.service.js'; import type { FeishuClientHelper } from './feishu-client.js'; import type { HarnessService } from './harness/core/harness-service.js'; import type { AgentLoop } from './harness/core/agent-loop.js'; import type { StreamingWsHandler } from './session/ws/streaming-ws-handler.js'; import { createFeishuSessionHelper } from './feishu-session.js'; import { FEISHU_ECP_IDENTITY_MISMATCH_TEXT } from './feishu/ecp-inbound-gate.js'; import { createFeishuQueueCardHelper } from './feishu-cards.js'; import { createFeishuSessionUrlHelper } from './feishu-session.js'; import { MysqlFeishuCardSnapshotRepository } from './feishu/card-snapshot.repository.js'; import { MysqlFeishuProgressCardRepository } from './feishu/progress-card.repository.js'; import { createPatchedProgressHelper } from './feishu-cards.js'; import { createFeishuProgressCardHelper } from './feishu-cards.js'; import { createFeishuRoundOffsetHelper } from './feishu-client.js'; import { createFeishuRecoveryProgressHelper } from './feishu-cards.js'; import { createFeishuUserIdHelper } from './feishu-session.js'; import { FeishuInboundQueueRepository } from './feishu/inbound-queue.repository.js'; import { FeishuTaskQueueService } from './feishu/inbound-queue.service.js'; import { createQueuePositionHelper } from './app-utils.js'; import { createPersistCancelledHelper } from './app-utils.js'; import { createPlatformQueueRelay } from './app-utils.js'; import { AgentFeishuInboundHandler } from './feishu/agent-inbound-handler.js'; import { channelAssistantReply } from './session/util/citation-markers.js'; import { wsEvent } from './session/ws/ws-event.js'; import { WsStreamingEventListener } from './session/ws/ws-streaming-event-listener.js'; import { buildFeishuProgressCard } from './feishu/progress-card.js'; import { feishuSendTargetOf } from './feishu/media-sender.js'; import { createFeishuMediaHelper, IMAGE_EXT_BY_CONTENT_TYPE } from './feishu-media.js'; import { sanitizeFeishuFileName, downloadFeishuMediaBuffer, dataUriOf } from './feishu-media.js'; import { createThreadSessionHelper } from './feishu-session.js'; import { FeishuInboundProcessor } from './feishu/inbound-processor.js'; import { createResolveQuotedMessageHelper } from './feishu-session.js'; import { createResolveMessageTextHelper } from './feishu-media.js'; import { createResolveUserIdHelper } from './feishu-session.js'; import { createAuthorizeSenderHelper } from './feishu-session.js'; import { createRenewAuthLinkHelper, createSendUnauthorizedCardHelper } from './feishu-cards.js'; import { createUnauthorizedTextHelper } from './feishu-cards.js'; import { FeishuCardActionService } from './feishu/card-action.service.js'; import { persistFeishuCancelIfIdle } from './feishu/cancel-running.js'; import { FEISHU_DISLIKE_SOURCE } from './feedback/feedback.repository.js'; import { inboundFileItems, inboundImageKeys } from './feishu/event-normalizer.js'; import { chatFilesDirOf, resolveChatFileTarget } from './feishu/chat-files.js'; import { parseSqlTimeMs } from './app-utils.js'; import { createDingtalkRuntime } from './dingtalk/runtime.js'; import { FeishuMonitorService } from './feishu/monitor.service.js'; import { AnalyticsService, AnalyticsDbStore } from './analytics/analytics.service.js'; import { StatisticsService, StatisticsDbStore } from './statistics/statistics.service.js'; import { AdminAnalyticsService, AdminAnalyticsDbStore } from './admin/admin-analytics.service.js'; import { McpServerValidatorImpl } from './agent/mcp-validator.js'; import { fetchFeishuMessageDetail } from './feishu/message-detail.js'; import { sendFeishuFile, sendFeishuImage } from './feishu/media-sender.js'; export interface FeishuHandlerDeps { db: Db; cfg: AppConfig; ctx: AppContext; /** 飞书消息层(app-feishu-messaging)先于 harness 装配,这里只做消费。 */ feishuMessageRepository: MysqlFeishuMessageRepository; feishuMessageService: FeishuMessageService; getFeishuClient: FeishuClientHelper['getFeishuClient']; getFeishuChatTitle: FeishuClientHelper['getFeishuChatTitle']; sendFeishuText: FeishuClientHelper['sendFeishuText']; /** harness/agentLoop 已于本模块之前装配完成(钉钉运行时需要真实实例)。 */ harness: HarnessService; agentLoop: AgentLoop; /** WS handler(0.0.434 跨通道队列接力/忙检查):装配在 create-app 传入,漏传编译期报错。 */ wsHandler: Pick; // 其余依赖仍以 any 透传,避免逐个下沉类型 [key: string]: any; } export interface FeishuHandlerResult { feishuInboundHandler: AgentFeishuInboundHandler; feishuInboundProcessor: FeishuInboundProcessor; feishuCardActionService: FeishuCardActionService; feishuMonitor: FeishuMonitorService; dingtalk: any; feishuTaskQueue: FeishuTaskQueueService; feishuProgressCardRepo: MysqlFeishuProgressCardRepository; createFeishuRecoveryProgress: any; analyticsService: AnalyticsService; statisticsService: StatisticsService; adminAnalytics: AdminAnalyticsService; httpToolStatsStore: AdminAnalyticsDbStore; mcpValidator: McpServerValidatorImpl; } export function assembleFeishuHandler(deps: FeishuHandlerDeps): FeishuHandlerResult { const d = deps as any; const { db, cfg, ctx } = d; const { feishuMessageRepository, feishuMessageService, getFeishuClient, getFeishuChatTitle, sendFeishuText, } = d; const feishuSenderNames = new Map>(); const { applyFeishuBotConfig, ensureFeishuSessionTitle } = createFeishuSessionHelper({ feishuBots: d.feishuBots, sessionService: d.sessionService, sessionRepo: d.sessionRepo, agentService: d.agentService, modelService: d.modelService, getFeishuChatTitle, }); ctx.notifyFeishuPendingMismatch = async (pending: any) => { await sendFeishuText(pending.appId, pending.event, FEISHU_ECP_IDENTITY_MISMATCH_TEXT); }; const feishuQueueCard = createFeishuQueueCardHelper({ getFeishuClient, feishuMessageService }); const resolveFeishuSessionDetailUrl = createFeishuSessionUrlHelper({ settingService: d.settingService }); const feishuCardSnapshotRepo = new MysqlFeishuCardSnapshotRepository(db); const feishuProgressCardRepo = new MysqlFeishuProgressCardRepository(db); const createPatchedProgress = createPatchedProgressHelper({ feishuActiveProgress: d.feishuActiveProgress, feishuAskFormStore: d.feishuAskFormStore, feishuCardSnapshotRepo, backgroundSubagentManager: d.backgroundSubagentManager, feedbackMessageLookup: d.feedbackMessageLookup, }); const createFeishuProgressCard = createFeishuProgressCardHelper({ getFeishuClient, feishuMessageService, feishuProgressCardRepo, resolveFeishuSessionDetailUrl, createPatchedProgress, }); const resolveFeishuRoundOffset = createFeishuRoundOffsetHelper({ sessionService: d.sessionService }); const createFeishuRecoveryProgress = createFeishuRecoveryProgressHelper({ getFeishuClient, feishuProgressCardRepo, sessionService: d.sessionService, resolveFeishuSessionDetailUrl, resolveFeishuRoundOffset, createPatchedProgress, }); const resolveFeishuUserId = createFeishuUserIdHelper({ feishuBinding: d.feishuBinding }); const feishuInboundQueueRepo = new FeishuInboundQueueRepository(db); const feishuTaskQueue = new FeishuTaskQueueService(feishuInboundQueueRepo); const queuePositionOf = createQueuePositionHelper({ feishuInboundQueueRepo }); const persistCancelledIfActive = createPersistCancelledHelper({ sessionService: d.sessionService, taskTerminal: d.taskTerminal, feishuProgressCardRepo }); const feishuInboundHandler = new AgentFeishuInboundHandler({ registry: d.wsRegistry, cardToolTickMs: cfg.feishu.bot.cardToolTickMs, // 跨通道队列接力(0.0.434):WS 侧执行占用时让行;飞书队列排空后消费平台排队消息。 isSessionExecuting: (sessionId: number) => d.wsHandler?.hasExecutionClaim(sessionId) === true, onQueuesDrained: createPlatformQueueRelay({ sessionService: d.sessionService, wsHandler: d.wsHandler }), resolveStreamUserId: async (sessionId: number) => { const session = await d.sessionService.getSession(sessionId).catch(() => null); return session?.userId ?? null; }, sessionService: { getOrCreateSession: async (accountId: any, context: any) => { const triggerUserId = await resolveFeishuUserId(accountId, context); const conversation = context.chatType === 'group' ? await feishuMessageService.getOrCreateGroup(accountId, context) : await feishuMessageService.getOrCreateP2p(accountId, context, triggerUserId); await applyFeishuBotConfig(conversation.sessionId, Number(accountId)); void ensureFeishuSessionTitle(conversation.sessionId, Number(accountId), context); return { id: conversation.sessionId, workspace: conversation.workspace ?? null, executionUserId: triggerUserId ?? null }; }, saveUserMessage: async (sessionId: number, content: any, metadata: any) => { await d.sessionService.saveMessage(sessionId, 'USER', content, null, null, null, 0, null, metadata ?? null); }, updatePhase: async (sessionId: number, phase: any) => { await d.sessionService.updatePhase(sessionId, phase); }, cleanupIncompleteTail: async (sessionId: number) => d.sessionService.cleanupIncompleteTail(sessionId), getPhase: async (sessionId: number) => (await d.sessionService.getSession(sessionId))?.phase ?? null, // 最近一次执行的运行身份(技术方案 2026-11-04 §5.2):失败卡重试透传。 // 不在此吞错:读取异常交给 runRetry 的 catch 记录并回落 owner,避免「身份漂移」静默复现。 getLastRuntimeUserId: async (sessionId: number) => (await d.sessionService.getSession(sessionId))?.lastRuntimeUserId ?? null, getLatestAssistantReplyWithId: async (sessionId: number) => { const messages = await d.sessionService.getMessages(sessionId); let messageId: number | null = null; for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].role === 'ASSISTANT') { messageId = messages[i].id ?? null; break; } } return { text: channelAssistantReply(messages, '抱歉,暂时无法生成回复。'), messageId }; }, getMessages: async (sessionId: number) => d.sessionService.getMessages(sessionId), }, p2pSessionControl: { findActiveSession: async (accountId: any, context: any) => { const triggerUserId = await resolveFeishuUserId(accountId, context); const conversation = await feishuMessageService.findActiveP2p(accountId, context, triggerUserId); return conversation == null ? null : { id: conversation.sessionId }; }, createSession: async (accountId: any, context: any) => { const triggerUserId = await resolveFeishuUserId(accountId, context); if (triggerUserId == null) throw new Error(`飞书用户未绑定: ${context.senderId ?? ''}`); const conversation = await feishuMessageService.createP2pSession(accountId, context, triggerUserId); await applyFeishuBotConfig(conversation.sessionId, Number(accountId)); return { id: conversation.sessionId }; }, switchSession: async (accountId: any, context: any, targetSessionId: number) => { const triggerUserId = await resolveFeishuUserId(accountId, context); if (triggerUserId != null) { const targetSession = await d.sessionRepo.findById(targetSessionId); if (targetSession == null || targetSession.userId !== triggerUserId) { console.warn(`飞书引用切换拒绝: 目标会话不存在或归属其他用户, sessionId=${targetSessionId}, trigger=${triggerUserId}`); return null; } } const conversation = await feishuMessageService.switchP2pSession(accountId, context, targetSessionId, triggerUserId); if (conversation == null) return null; await applyFeishuBotConfig(conversation.sessionId, Number(accountId)); return { id: conversation.sessionId }; }, findSessionByMessageId: (accountId: any, messageId: any) => feishuMessageService.findP2pMessageSession(accountId, messageId), recordMessageMapping: (accountId: any, messageId: any, sessionId: number, direction: any) => feishuMessageService.recordP2pMessage(accountId, messageId, sessionId, direction), finalizeNewSessionTitle: async (sessionId: number, title: string) => { const channel = await feishuMessageService.findSessionChannel(sessionId); if (channel?.awaitingFirstMessageTitle !== 1) return false; const session = await d.sessionService.getSession(sessionId); if (session == null) return false; await feishuMessageService.clearAwaitingFirstMessageTitle(sessionId); if (session.title != null && session.title !== '飞书Bot会话') return false; await d.sessionRepo.updateFields(sessionId, { title }); d.wsRegistry.send(session.userId!, wsEvent('session_title_updated', sessionId, { title, parentSessionId: session.parentSessionId ?? null, sessionType: session.sessionType ?? 'NORMAL', })); return true; }, isAwaitingFirstMessageTitle: async (sessionId: number) => (await feishuMessageService.findSessionChannel(sessionId))?.awaitingFirstMessageTitle === 1, }, threadSessionControl: { findSession: async (accountId: any, context: any) => { const result = await feishuMessageService.findThreadSession(accountId, context.threadId); return result == null ? null : { sessionId: result.sessionId, rootMessageId: result.rootMessageId }; }, getOrCreateSession: async (accountId: any, context: any) => { const result = await feishuMessageService.getOrCreateThreadSession(accountId, context); if (result == null) return null; const triggerUserId = await resolveFeishuUserId(accountId, context); await applyFeishuBotConfig(result.sessionId, Number(accountId)); return { sessionId: result.sessionId, rootMessageId: result.rootMessageId, workspace: result.workspace ?? null, executionUserId: triggerUserId ?? null }; }, }, titleGenerator: { generate: (sessionId: number, text: string) => d.sessionTitleService.generateExternalTitle(sessionId, text), }, onReply: async (context: any, text: string, sessionId?: number) => { const messageId = await sendFeishuText(Number(context.accountId), context, text); if (messageId != null && sessionId != null && context.chatType === 'p2p') { await feishuMessageService.recordP2pMessage(context.accountId, messageId, sessionId, 'OUT'); } return messageId; }, harnessService: { execute: (...args: any[]) => d.harness.execute(...args), prepareMessage: (...args: any[]) => d.harness.prepareMessage(...args) } as never, createCancelFlag: (sessionId: number) => d.agentLoop.registerCancelFlag(sessionId), releaseCancelFlag: (sessionId: number) => d.agentLoop.removeCancelFlag(sessionId), createProgressCard: (context: any, sessionId: number) => createFeishuProgressCard(context, sessionId), onInterruptRunning: (sessionId: number) => { d.agentLoop.requestCancel(sessionId); try { d.shellManager.closeByConversation(sessionId); } catch (error) { console.debug(`关闭飞书会话 Shell 失败, sessionId=${sessionId}: ${error instanceof Error ? error.message : String(error)}`); } d.feishuAskFormStore.clearSession(sessionId); d.askUserQuestionsRegistry.failAllForSession(sessionId); }, resolveIdleRunning: async (sessionId: number) => { if (d.agentLoop.getCancelFlag(sessionId) != null) return; await persistCancelledIfActive(sessionId); }, queueService: feishuTaskQueue, createQueueCard: async (context: any, _queueId: any, sessionId: number) => feishuQueueCard(context, _queueId, await queuePositionOf(sessionId), sessionId), resolveBotId: (accountId: any) => Number(accountId), downloadMedia: async (context: any, workspace: string | null) => { const botId = Number(context.accountId); const client = await getFeishuClient(botId); if (client == null) return null; const inboundKeys = inboundImageKeys(context); const fileItems = inboundFileItems(context); const isMedia = inboundKeys.length > 0 || fileItems.length > 0; if (!isMedia || context.messageId == null) return null; const maxBytes = Math.max(1, cfg.feishu.bot.file.maxInboundFileMb) * 1024 * 1024; const images: string[] = []; const imagePaths: string[] = []; const filePaths: string[] = []; const errors: string[] = []; try { let imageIndex = 0; for (const imageKey of inboundKeys) { try { const { buffer, contentType } = await downloadFeishuMediaBuffer(client, context.messageId, imageKey, 'image', maxBytes); if (buffer.length === 0) { errors.push('图片(接收失败)'); continue; } images.push(dataUriOf(contentType, buffer)); if (workspace != null && workspace !== '') { try { const ext = IMAGE_EXT_BY_CONTENT_TYPE[contentType] ?? '.jpg'; const dir = chatFilesDirOf(workspace); mkdirSync(dir, { recursive: true }); const name = imageIndex === 0 ? `feishu-image-${context.messageId}${ext}` : `feishu-image-${context.messageId}-${imageIndex + 1}${ext}`; const target = resolve(dir, name); await writeFile(target, buffer); imagePaths.push(target); } catch (error) { console.warn(`飞书图片落盘失败(不影响直传), bot=${botId}, messageId=${context.messageId}: ${error instanceof Error ? error.message : String(error)}`); } } imageIndex++; } catch (error) { console.error(`飞书图片下载失败, bot=${botId}, messageId=${context.messageId}`, error); errors.push('图片(接收失败)'); } } // 单文件消息(msg_type=file)与多文件消息(post.files)统一按文件项逐个下载。 if (fileItems.length > 0) { if (workspace == null || workspace === '') { for (const item of fileItems) errors.push(item.fileName ?? '文件(接收失败)'); } else { for (let fileIndex = 0; fileIndex < fileItems.length; fileIndex++) { const item = fileItems[fileIndex]; try { const { buffer } = await downloadFeishuMediaBuffer(client, context.messageId, item.fileKey, 'file', maxBytes); if (buffer.length === 0) { errors.push(item.fileName ?? '文件(接收失败)'); continue; } const fileName = sanitizeFeishuFileName(item.fileName, `feishu-${item.fileKey}`); const dir = chatFilesDirOf(workspace); mkdirSync(dir, { recursive: true }); const target = resolveChatFileTarget(dir, fileName, context.messageId, fileIndex); await writeFile(target, buffer); filePaths.push(target); } catch (error) { console.error(`飞书文件下载失败, bot=${botId}, messageId=${context.messageId}`, error); errors.push(item.fileName ?? '文件(接收失败)'); } } } } } finally { /* 下载失败已收集 errors */ } return { images, imagePaths, filePaths, errors }; }, listenerFactory: async (sessionId: number, context: any, executionId: any) => { const session = await d.sessionService.getSession(sessionId); const ownerUserId = session.userId!; return new WsStreamingEventListener({ registry: d.wsRegistry, activityService: d.activityService as never, activityHeartbeat: d.activityHeartbeat, sessionTodoMapper: d.todoMapper, sessionService: d.sessionService as never, }, sessionId, ownerUserId, executionId, session.modelId != null && (await d.modelService.getModel(session.modelId))?.supportsVision === 1); }, onExecutionFinished: async (sessionId: number, _context: any, executionId: any, phase: any, failureReason: any) => { const session = await d.sessionService.getSession(sessionId); await d.taskTerminal.finishExecution(sessionId, session.userId!, phase, executionId, failureReason); if (phase === 'FAILED') return; try { await feishuProgressCardRepo.deleteBySessionId(sessionId); } catch (error) { console.warn(`清理飞书进度卡片映射失败, sessionId=${sessionId}: ${error instanceof Error ? error.message : String(error)}`); } }, }); d.scheduledService.setFeishuResultPusher(async (sessionId: number, text: string) => { const channel = await feishuMessageService.findSessionChannel(sessionId); if (channel == null) return; const client = await getFeishuClient(Number(channel.appId)); if (client == null) return; const target = feishuSendTargetOf(channel.appId, channel.chatId); const data = { msg_type: 'interactive', content: JSON.stringify(buildFeishuProgressCard('COMPLETED', 0, text, [], undefined, undefined, await resolveFeishuSessionDetailUrl(sessionId))), }; const threadRootMessageId = channel.chatType === 'group' ? await feishuMessageService.findThreadRootMessageId(sessionId) : null; const response = threadRootMessageId != null ? await client.im.v1.message.reply({ path: { message_id: threadRootMessageId }, data }) : await client.im.v1.message.create({ params: { receive_id_type: target.receiveIdType }, data: { ...data, receive_id: target.receiveId }, }); const pushedMessageId = (response as { data?: { message_id?: string } }).data?.message_id ?? null; if (pushedMessageId != null && channel.chatType === 'p2p') { await feishuMessageService.recordP2pMessage(channel.appId, pushedMessageId, sessionId, 'OUT'); } }); const feishuMediaHelper = createFeishuMediaHelper({ getFeishuClient, feishuSenderNames, feishuBinding: d.feishuBinding, feishuMessageRepository, userRepo: d.userRepo, workspaceRoot: cfg.app.harness.workspaceRoot, maxInboundFileMb: cfg.feishu.bot.file.maxInboundFileMb, }); const { resolveFeishuSenderName, resolveFeishuUserNames, resolveMergeWorkspace, expandMergeForward, resolveQuotedWorkspace, downloadFeishuGroupImage, downloadFeishuImageTo, downloadFeishuGroupFile } = feishuMediaHelper; const threadSessionHelper = createThreadSessionHelper({ feishuMessageService }); const feishuInboundProcessor = new FeishuInboundProcessor(feishuInboundHandler, { messageService: feishuMessageService, mergeWindowMs: cfg.feishu.bot.mergeWindowMs, resolveSenderName: resolveFeishuSenderName, downloadGroupImage: downloadFeishuGroupImage, downloadGroupFile: downloadFeishuGroupFile, resolveThreadSession: threadSessionHelper.resolveThreadSession, ensureThreadSession: threadSessionHelper.ensureThreadSession, resolveQuotedMessage: createResolveQuotedMessageHelper({ getFeishuClient, feishuMessageRepository, resolveQuotedWorkspace, expandMergeForward, }), expandMergeForward, resolveMergeWorkspace, resolveMessageText: createResolveMessageTextHelper({ getFeishuClient }), resolveUserId: createResolveUserIdHelper({ feishuBinding: d.feishuBinding }), authorizeSender: createAuthorizeSenderHelper({ feishuBinding: d.feishuBinding, feishuMessageRepository, ecpAuth: d.ecpAuth, ecpSessionRepo: d.ecpSessionRepo }), sendReply: async (accountId: any, event: any, text: string) => { await sendFeishuText(Number(accountId), event, text); }, sendUnauthorizedCard: createSendUnauthorizedCardHelper({ getFeishuClient, ecpAuth: d.ecpAuth, feishu: d.feishu, pendingBindingMessages: d.pendingBindingMessages }), unauthorizedText: createUnauthorizedTextHelper({ ecpAuth: d.ecpAuth }), }); ctx.pendingBindingProcessor = feishuInboundProcessor; const feishuCardActionService = new FeishuCardActionService({ queuePort: feishuTaskQueue, renewAuthLink: createRenewAuthLinkHelper({ ecpAuth: d.ecpAuth, feishu: d.feishu, pendingBindingMessages: d.pendingBindingMessages, }), interrupt: (sessionId: number) => feishuInboundHandler.interrupt(sessionId), interruptAndDrain: (sessionId: number) => feishuInboundHandler.interruptAndDrain(sessionId), cancelRunning: async (sessionId: number) => { feishuInboundHandler.cancel(sessionId); return persistFeishuCancelIfIdle({ sessionId, hadLoop: d.agentLoop.getCancelFlag(sessionId) != null, persistCancelledIfActive, drainNextIfPending: (id: number) => feishuInboundHandler.drainNextIfPending(id), }); }, toggleDislike: async (sessionId: number, cardMessageId: string | null) => { const session = await d.sessionService.getSession(sessionId).catch(() => null); if (session == null) return null; let messageId: number | undefined; if (cardMessageId != null) { const snapshot = await feishuCardSnapshotRepo.findByCardMessageId(cardMessageId).catch((error: any) => { console.warn(`飞书点踩查询卡片快照失败, cardMessageId=${cardMessageId}, sessionId=${sessionId}: ${error instanceof Error ? error.message : String(error)}`); return null; }); messageId = snapshot?.messageId; } const result = await d.feedbackService.toggleSessionDislike(session.userId, sessionId, FEISHU_DISLIKE_SOURCE, messageId); if (result == null) return null; if (cardMessageId == null) { d.feishuActiveProgress.current(sessionId)?.setDisliked(result.disliked); } else { d.feishuActiveProgress.setDislikedByCard(cardMessageId, result.disliked); } return result; }, retryFailed: async (sessionId: number, cardMessageId: string) => { const mapping = await feishuProgressCardRepo.findBySessionId(sessionId); if (mapping == null) return { ok: false, reason: 'NO_PROGRESS' }; return feishuInboundHandler.retryExecution(sessionId, async () => { const client = await getFeishuClient(mapping.botId); if (client == null) return null; const sender = mapping.senderOpenId ?? ''; const sessionDetailUrl = await resolveFeishuSessionDetailUrl(sessionId); const session = await d.sessionService.getSession(sessionId).catch(() => null); const startedAtMs = parseSqlTimeMs(session?.startedAt) ?? Date.now(); try { await feishuProgressCardRepo.upsert({ sessionId, botId: mapping.botId, cardMessageId, chatType: mapping.chatType, chatId: mapping.chatId, senderOpenId: mapping.senderOpenId, }); } catch (error) { console.warn(`飞书重试进度卡片映射刷新失败, sessionId=${sessionId}: ${error instanceof Error ? error.message : String(error)}`); } return createPatchedProgress( client, cardMessageId, sessionId, { sessionId, sender, botId: mapping.botId }, startedAtMs, sessionDetailUrl, { content: '正在重试,请稍候…' }, mapping.botId, ); }); }, patchCard: async (botId: number, cardMessageId: string, card: any) => { const client = await getFeishuClient(botId); if (client == null) throw new Error(`飞书客户端不可用, botId=${botId}, cardMessageId=${cardMessageId}`); const response = await client.im.v1.message.patch({ path: { message_id: cardMessageId }, data: { content: JSON.stringify(card) } }); if (response?.code !== 0) { throw new Error(`飞书卡片更新失败, code=${response?.code ?? 'missing'}, msg=${response?.msg ?? 'unknown'}`); } }, sessionDetailUrl: (sessionId: number) => resolveFeishuSessionDetailUrl(sessionId), askForms: d.feishuAskFormStore, renderProgressCard: (sessionId: number) => d.feishuActiveProgress.render(sessionId), renderProgressCardByCard: (cardMessageId: string) => d.feishuActiveProgress.renderByCard(cardMessageId), rebuildCardFromSnapshot: async (sessionId: number, cardMessageId: string, disliked: boolean, sender: string) => { const snapshot = await feishuCardSnapshotRepo.findByCardMessageId(cardMessageId).catch((error: any) => { console.warn(`飞书点踩查询卡片快照失败, cardMessageId=${cardMessageId}, sessionId=${sessionId}: ${error instanceof Error ? error.message : String(error)}`); return null; }); if (snapshot == null) return null; return buildFeishuProgressCard( 'COMPLETED', snapshot.round, snapshot.content ?? '', [], { sessionId, sender }, snapshot.elapsedMs, await resolveFeishuSessionDetailUrl(sessionId), undefined, disliked, ); }, refreshProgress: (sessionId: number) => d.feishuActiveProgress.refresh(sessionId), completeAsk: (sessionId: number, requestId: string, resultJson: string) => d.askUserQuestionsRegistry.complete(sessionId, requestId, resultJson), notifyAskAnswered: (sessionId: number, requestId: string) => { d.treeSignalPublisher.publishForSession(sessionId); void d.localToolSessions.getUserIdForSession(sessionId).then((userId: number | null) => { if (userId != null) { d.wsRegistry.send(userId, wsEvent('ask_user_questions_cancelled', sessionId, { requestId })); } }).catch((error: any) => { console.warn(`飞书提问结果通知客户端失败, sessionId=${sessionId}`, error); }); }, }); const dingtalk = createDingtalkRuntime({ db, config: { enabled: cfg.dingtalk.enabled, appSecretKey: cfg.dingtalk.appSecretKey, reconcileIntervalMs: cfg.dingtalk.reconcileIntervalMs, reconnectBaseMs: cfg.dingtalk.reconnectBaseMs, reconnectMaxMs: cfg.dingtalk.reconnectMaxMs, maxConsecutiveFailures: cfg.dingtalk.maxConsecutiveFailures, progressCardTemplateId: cfg.dingtalk.progressCardTemplateId, queueCardTemplateId: cfg.dingtalk.queueCardTemplateId, replyMaxLength: cfg.dingtalk.replyMaxLength, groupContextMaxItems: cfg.dingtalk.groupContext.maxItems, groupContextMaxMinutes: cfg.dingtalk.groupContext.maxMinutes, mergeWindowMs: cfg.dingtalk.mergeWindowMs, }, workspaceRoot: cfg.app.harness.workspaceRoot, jwt: d.jwt, permissionService: d.permissionService, sessionService: d.sessionService, sessionRepo: d.sessionRepo, harness: d.harness, agentLoop: d.agentLoop, wsRegistry: d.wsRegistry, agentService: d.agentService, modelService: d.modelService, userRepo: d.userRepo, settingService: d.settingService, taskTerminal: d.taskTerminal, activityService: d.activityService, activityHeartbeat: d.activityHeartbeat, todoMapper: d.todoMapper, shellManager: d.shellManager, // 跨通道队列接力(0.0.434):同飞书 handler —— WS 侧占用让行 + 队列排空后消费平台排队消息。 isSessionExecuting: (sessionId: number) => d.wsHandler?.hasExecutionClaim(sessionId) === true, onQueuesDrained: createPlatformQueueRelay({ sessionService: d.sessionService, wsHandler: d.wsHandler }), }); d.dingtalkMediaHolder.current = dingtalk.mediaSend; d.scheduledService.setDingtalkResultPusher((sessionId: number, text: string) => dingtalk.pushScheduledResult(sessionId, text)); const feishuMonitor = new FeishuMonitorService(cfg.feishu.bot, d.feishuBots, feishuInboundProcessor, async (data: any) => feishuCardActionService.handle(data, '')); const analyticsService = new AnalyticsService(new AnalyticsDbStore(db)); // wsRegistry:在线设备页快照来源(create-app 装配时已传入,见 FeishuHandlerDeps 的 any 透传) const statisticsService = new StatisticsService(new StatisticsDbStore(db), d.wsRegistry); const adminAnalytics = new AdminAnalyticsService(statisticsService, new AdminAnalyticsDbStore(db)); const httpToolStatsStore = new AdminAnalyticsDbStore(db); const mcpValidator = new McpServerValidatorImpl(d.mcpServerLookup); return { feishuInboundHandler, feishuInboundProcessor, feishuCardActionService, feishuMonitor, dingtalk, feishuTaskQueue, feishuProgressCardRepo, createFeishuRecoveryProgress, analyticsService, statisticsService, adminAnalytics, httpToolStatsStore, mcpValidator, }; }