backend-ts/src/app-feishu-messaging.ts
Rama main · réplica de lectura del repositorio original.
import { mkdirSync } from 'node:fs';
import type { Db } from './db/db.js';
import type { AppConfig } from './config/app-config.js';
import type { MysqlFeishuBotRepository } from './feishu/feishu_bot.repository.js';
import type { MysqlFeishuBindingRepository } from './feishu/binding.repository.js';
import type { MysqlUserRepository } from './user/user.repository.js';
import type { AgentService } from './agent/agent.service.js';
import type { ModelService } from './model/model.service.js';
import type { SessionService } from './session/session.service.js';
import type { LlmChatClient, LlmModelConfig } from './model/types.js';
import type { FeishuInboundContext } from './feishu/types.js';
import { MysqlFeishuMessageRepository } from './feishu/message.repository.js';
import { FeishuMessageService } from './feishu/message.service.js';
import { feishuSummarizerModelConfig, GroupContextSummarizer } from './feishu/group-context-summarizer.js';
import { createFeishuClientHelper } from './feishu-client.js';
import { resolveFeishuChatWorkspace } from './feishu-media.js';
export interface FeishuMessagingDeps {
db: Db;
cfg: AppConfig;
feishuBots: MysqlFeishuBotRepository;
feishuBinding: MysqlFeishuBindingRepository;
userRepo: MysqlUserRepository;
agentService: AgentService;
modelService: ModelService;
sessionService: SessionService;
routeChatClient: (config: LlmModelConfig) => LlmChatClient;
}
export interface FeishuMessagingResult {
feishuMessageRepository: MysqlFeishuMessageRepository;
feishuMessageService: FeishuMessageService;
getFeishuClient: ReturnType<typeof createFeishuClientHelper>['getFeishuClient'];
getFeishuChatTitle: ReturnType<typeof createFeishuClientHelper>['getFeishuChatTitle'];
sendFeishuText: ReturnType<typeof createFeishuClientHelper>['sendFeishuText'];
}
/**
* 飞书消息层:Bot 客户端、会话/群聊映射、消息落库。
*
* 该层早于 harness 装配:createDefaultToolRegistry 的飞书工具在执行期通过闭包取用
* 此处构造的 service/client,因此必须在 assembleHarnessEngine 之前完成装配。
*/
export function assembleFeishuMessaging(deps: FeishuMessagingDeps): FeishuMessagingResult {
const {
db, cfg, feishuBots, feishuBinding, userRepo, agentService, modelService, sessionService, routeChatClient,
} = deps;
const feishuMessageRepository = new MysqlFeishuMessageRepository(db);
const { getFeishuClient, getFeishuChatTitle, sendFeishuText } = createFeishuClientHelper({
feishuBots,
appSecretKey: cfg.feishu.bot.appSecretKey,
replyMaxLength: cfg.feishu.bot.reply.maxLength,
});
const feishuMessageService = new FeishuMessageService(
feishuMessageRepository,
{
create: async (accountId: string, context: FeishuInboundContext) => {
const bot = await feishuBots.findById(Number(accountId));
if (bot == null) throw new Error(`飞书Bot不存在: ${accountId}`);
const unionId = context.senderUnionId ?? context.senderId;
const userId = unionId == null ? null : await feishuBinding.findUserIdByUnionId(unionId);
const user = userId == null ? null : await userRepo.findById(userId);
if (user?.id == null) throw new Error(`飞书用户未绑定: ${context.senderId ?? ''}`);
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 workspaceLeaf = context.chatType === 'group' && context.chatId != null ? context.chatId! : `private-${user.id}`;
const workspace = resolveFeishuChatWorkspace(cfg.app.harness.workspaceRoot, accountId, workspaceLeaf);
mkdirSync(workspace, { recursive: true });
const isGroup = context.chatType === 'group' && context.chatId != null;
const title = isGroup && context.threadId == null
? (await getFeishuChatTitle(Number(accountId), context.chatId!)) || '飞书Bot会话'
: '飞书Bot会话';
const session = await sessionService.createSession(
user.id, agent.id, title, 'CLOUD', workspace, 'FULL', false,
'linux', '/bin/bash', 'Linux', model?.id ?? null,
isGroup ? `feishu-chat-${accountId}-${context.chatId}` : `feishu-${accountId}-private-${user.id}`,
'new', null, null, 'feishu',
);
return { sessionId: session.id!, ownerUserId: user.id, workspace: session.workspace };
},
},
cfg.feishu.bot.groupContext.maxItems,
cfg.feishu.bot.groupContext.maxMinutes,
new GroupContextSummarizer(routeChatClient, async (sessionId: number) => {
const session = await sessionService.getSession(sessionId);
const model = session?.modelId != null
? await modelService.getModel(session.modelId).catch(() => null)
: await modelService.getDefaultModel();
if (model == null || model.baseUrl === '' || model.modelId === '') return null;
return feishuSummarizerModelConfig(model);
}),
cfg.feishu.bot.groupContext.overflowItems,
);
return { feishuMessageRepository, feishuMessageService, getFeishuClient, getFeishuChatTitle, sendFeishuText };
}