import cors from '@fastify/cors'; import multipart from '@fastify/multipart'; import swagger from '@fastify/swagger'; import swaggerUi from '@fastify/swagger-ui'; import Fastify, { type FastifyInstance, type FastifyRequest } from 'fastify'; import { mkdirSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { fastifyLoggerOptions } from './common/structured-logger.js'; import { loadConfig, type AppConfig } from './config/app-config.js'; import { corsForRequest } from './config/cors-policy.js'; import { loadTrustedProxyAddresses } from './config/trusted-proxy.js'; import { createPool, Db } from './db/db.js'; import { runFlywayIfEnabled } from './db/flyway.js'; import { JwtService } from './crypto/jwt.service.js'; import { hashPassword, matchesPassword } from './crypto/password.js'; import { AuthService } from './auth/auth.service.js'; import { LdapAuthService } from './auth/ldap-auth.service.js'; import { FeishuAuthService } from './auth/feishu-auth.service.js'; import { MysqlFeishuOauthStateRepository } from './auth/feishu-oauth.repository.js'; import { EcpAuthService } from './auth/ecp-auth.service.js'; import { createEcpCredentialsInjector } from './auth/ecp-credentials-injector.js'; import { EcpClient } from './auth/ecp.client.js'; import { EcpIdentityRepository } from './auth/ecp-identity.repository.js'; import { createLarkUatInjector } from './auth/lark-uat-injector.js'; import { MysqlEcpOauthStateRepository } from './auth/ecp-oauth.repository.js'; import { MysqlEcpSessionRepository } from './auth/ecp-session.repository.js'; import { MysqlUserRepository } from './user/user.repository.js'; import { UserService } from './user/user.service.js'; import { GitCredentialService, assertGitCredentialSecret } from './user/git-credential.service.js'; import { DefaultGitTokenResolver } from './user/git-token-resolver.js'; import { MysqlEnvVariableRepository } from './env/env-variable.repository.js'; import { EnvVariableService } from './env/env-variable.service.js'; import { MysqlPermissionRepository, MysqlRolePermissionRepository, MysqlRoleRepository, MysqlUserRoleRepository, } from './permission/permission.repository.js'; import { PermissionService } from './permission/permission.service.js'; import { MysqlAgentExperienceRepository, MysqlAgentRepository, MysqlAgentSuggestedQuestionRepository } from './agent/agent.repository.js'; import { AgentExperienceService } from './agent/agent-experience.service.js'; import { AgentSuggestedQuestionService } from './agent/agent-suggested-question.service.js'; import { AgentService } from './agent/agent.service.js'; import { MysqlMcpServerLookup } from './agent/mcp-validator.js'; import { MysqlLlmModelRepository, MysqlSessionModelRepository } from './model/model.repository.js'; import { OpenAiChatClient } from './model/llm-chat.client.js'; import { AnthropicChatClient } from './model/anthropic-chat.client.js'; import { ResponsesChatClient } from './model/responses-chat.client.js'; import type { LlmChatClient, LlmModelConfig } from './model/types.js'; import { ModelService } from './model/model.service.js'; import { MysqlSystemSettingRepository } from './settings/settings.repository.js'; import { SystemSettingService } from './settings/settings.service.js'; import { runSettingsBootstrap } from './settings/settings-bootstrap.js'; import { MysqlUserCommandRepository } from './command/command.repository.js'; import { UserCommandService } from './command/command.service.js'; import { MysqlUserOnboardingPreferenceRepository, MysqlUserTaskPanelPreferenceRepository, MysqlUserWeixinPreferenceRepository, } from './preference/preference.repository.js'; import { UserWeixinPreferenceService } from './preference/weixin-preference.service.js'; import { UserTaskPanelPreferenceService } from './preference/task-panel-preference.service.js'; import { ToolService } from './tool/tool.service.js'; import { MysqlAuditLogRepository } from './audit/audit.repository.js'; import { AuditLogService } from './audit/audit.service.js'; import { PathSandbox } from './harness/safety/path-sandbox.js'; import { SessionRepository, MessageRepository, FileChangeRepository } from './session/session.repository.js'; import { FailedSessionRecoveryService } from './session/failed-session-recovery.service.js'; import { SessionCompactionRepository, SessionCompactionEventRepository } from './session/session-compaction.repository.js'; import { SessionCompactionService } from './session/session-compaction.service.js'; import { SessionCompactionEventService } from './session/session-compaction-event.service.js'; import { GitOperationService } from './session/git-operation.service.js'; import { SessionService } from './session/session.service.js'; import { SessionTitleService } from './session/session-title.service.js'; import { ActivityService } from './session/activity.service.js'; import { RunTraceService } from './session/run-trace.service.js'; import { RunTraceRecordService } from './session/run-trace-record.service.js'; import { SessionActivityRepository, SessionTodoRepository, SubagentExecutionRepository } from './session/activity.repository.js'; import { MessageQueueService } from './session/message-queue.service.js'; import { MessageQueueRepository } from './session/message-queue.repository.js'; import { SessionActivityHeartbeat } from './session/session-activity-heartbeat.js'; import { ApprovalRuleRepository } from './approval-rule/approval-rule.repository.js'; import { ApprovalRuleService } from './approval-rule/approval-rule.service.js'; import { InboxRepository } from './inbox/inbox.repository.js'; import { InboxService } from './inbox/inbox.service.js'; import type { InboxCleanupStore } from './inbox/inbox.cleanup.js'; import { EnvironmentInfoProvider } from './harness/core/environment-info-provider.js'; import { FileEntityRepository, FileService } from './file/file.service.js'; import { WorkspaceBrowseService } from './file/workspace-browse.service.js'; import { WorkspaceGitService } from './file/workspace-git.service.js'; import { GitWriteOperationService } from './file/git-write-operation.service.js'; import { GitCommitMessageService } from './file/git-commit-message.service.js'; import { OssStsService, createAliyunAssumeRoleClient } from './oss/oss-sts.service.js'; import { SkillBundleService } from './skill/skill-bundle.service.js'; import { UserSkillService } from './skill/user-skill.service.js'; import { SkillDocService } from './skill/skill-doc.service.js'; import { SkillSyncService } from './harness/skill/skill-sync-service.js'; import { SkillViewService } from './harness/skill/skill-view.service.js'; import { SkillLoader } from './harness/skill/skill-loader.js'; import { ScheduledTaskDbStore } from './schedule/scheduled-task.store.js'; import { FeedbackRepository } from './feedback/feedback.repository.js'; import { FeedbackDbLookup, FeedbackService } from './feedback/feedback.service.js'; import { McpSyncService } from './harness/mcp/local/mcp-sync-service.js'; import { HttpApiExecutor } from './harness/tool/impl/http-api-executor.js'; import { DangerAssessor } from './harness/tool/danger-assessor.js'; import { ProxyApprover } from './harness/tool/proxy-approver.js'; import { JevRiskAssessor } from './harness/tool/jev-risk-assessor.js'; import { ApprovalModelResolver } from './harness/tool/approval-model-resolver.js'; import { BudgetRepository, BudgetSpendStore } from './budget/budget.repository.js'; import { BudgetService } from './budget/budget.service.js'; import { AskUserQuestionsRegistry } from './harness/tool/ask-user-questions-registry.js'; import { InFlightToolRegistry } from './harness/core/in-flight-tool-registry.js'; import { AgentLoop } from './harness/core/agent-loop.js'; import { HarnessService } from './harness/core/harness-service.js'; import { WorkspaceRulesSnapshotRepository } from './harness/core/workspace-rules-snapshot.repository.js'; import { WorkspaceRulesSnapshotService } from './harness/core/workspace-rules-snapshot.service.js'; import { ExperienceIndexService, EXPERIENCE_ROOT_SUBDIR } from './harness/experience/experience-index.service.js'; import { ExperienceMountService } from './harness/experience/experience-mount.service.js'; import { CompactionSignalBus } from './harness/core/compaction-signal-bus.js'; import { BackgroundTaskManager } from './harness/core/background-task-manager.js'; import { createAgentExecutor } from './harness/core/agent-executor.js'; import { LocalAgentsMdRegistry } from './harness/core/local-agents-md-registry.js'; import { RuntimeDataResolver } from './harness/runtime/runtime-data-resolver.js'; import { ShellSessionManager, OutputManager } from './harness/shell/shell-session-manager.js'; import { SessionTodoMapper } from './harness/todo/session-todo.mapper.js'; import { LocalSkillRegistry } from './harness/skill/local-skill-registry.js'; import { LocalToolSessionRegistry } from './harness/local/local-tool-session-registry.js'; import { LocalToolExecutor } from './harness/local/local-tool-executor.js'; import { AgentDefinitionRegistry } from './harness/delegate/agent-definition-registry.js'; import { SubagentExecutionMapper } from './harness/delegate/subagent-execution.mapper.js'; import { SubagentInvocationService } from './harness/delegate/subagent-invocation.service.js'; import { SubagentResultDeliveryService } from './harness/delegate/subagent-result-delivery.service.js'; import { ApprovalRegistry } from './harness/approval/approval-registry.js'; import { SessionTreeSignalPublisher } from './harness/approval/session-tree-signal-publisher.js'; import { StreamingWsRegistry } from './session/ws/streaming-ws-registry.js'; import { EmbedPageToolRegistry, resolveEmbedPageToolTimeoutMs } from './harness/embed-page-tool-registry.js'; import { TerminalManager } from './harness/terminal/terminal-manager.js'; import { TerminalWsHandler } from './harness/terminal/terminal-ws-handler.js'; import { WebhookSecretCipher } from './notification/task/webhook-secret-cipher.js'; import { LlmUsageRepository, LlmUsageService } from './usage/llm-usage.service.js'; import { LlmCallRepository } from './usage/llm-call.repository.js'; import { LlmCallService } from './usage/llm-call.service.js'; import { RecordingLlmChatClient } from './usage/recording-llm-chat-client.js'; import { MysqlFeishuBindingRepository } from './feishu/binding.repository.js'; import { FeishuInboundProcessor } from './feishu/inbound-processor.js'; import { MysqlFeishuPendingBindingRepository } from './feishu/pending-binding.repository.js'; import { replayFeishuPendingMessage } from './feishu/pending-replay.js'; import { completeFeishuPendingAfterEcp } from './feishu/ecp-inbound-gate.js'; import type { DingtalkMediaSendSupport } from './harness/tool/impl/dingtalk-tools.js'; import { FeishuAskFormStore } from './feishu/ask-form-store.js'; import { FeishuActiveProgressRegistry } from './feishu/active-progress.js'; import { createFeishuAskMount } from './feishu/ask-mount.js'; import { createPlatformQueueRelay, registerUploadStatic, createTerminalAudit } from './app-utils.js'; import type { FeishuNormalizedMessage } from './feishu/types.js'; import { MysqlApiTokenRepository, MysqlOutboundDeliveryRepository, MysqlOutboundSubscriptionRepository, MysqlWebhookTriggerRepository } from './openapi/openapi.repository.js'; import { ApiTokenService } from './openapi/api-token.service.js'; import { ApiTokenCipher } from './openapi/api-token-cipher.js'; import { FixedWindowRateLimiter } from './openapi/rate-limiter.js'; import { OutboundSubscriptionService } from './openapi/outbound-subscription.service.js'; import { GenericHttpWebhookSender } from './openapi/generic-webhook-sender.js'; import { OpenApiCallLogService } from './openapi/open-api-call-log.service.js'; import { MysqlHttpToolRepository } from './http-tool/http-tool.repository.js'; import { HttpToolService } from './http-tool/http-tool.service.js'; import { HttpToolValidatorImpl, MysqlHttpToolLookup } from './http-tool/http-tool-validator.js'; import { HttpToolHeaderCipher } from './http-tool/http-tool-cipher.js'; import { buildDepsEnv } from './harness/container/session-envelope-builder.js'; import { ContainerScriptProbe } from './harness/container/container-script-probe.js'; import { expandHome, resolveShareDir, runtimeSessionCleanup } from './app-utils.js'; import { registerAllRoutes } from './app-routes.js'; import { registerAppHooks } from './app-hooks.js'; import { assembleContainerRuntime } from './app-container-runtime.js'; import { assembleLlmEngine } from './app-llm-engine.js'; import { assembleNotificationServices } from './app-notification-services.js'; import { PENDING_ESCALATION_DELAY_MS } from './notification/task/delivery.service.js'; import type { WatcherService } from './signal/watcher.service.js'; import { assembleOpenApiServices } from './app-openapi-services.js'; import { assembleWeixinServices } from './app-weixin-services.js'; import { assembleFeishuHandler } from './app-feishu-handler.js'; import { assembleFeishuMessaging } from './app-feishu-messaging.js'; import { assembleLifecycle } from './app-lifecycle.js'; import { assembleWeixinMcpServices } from './app-weixin-mcp-services.js'; import { assembleHarnessEngine } from './app-harness-engine.js'; import { assembleWsHandler } from './app-ws-handler.js'; import { assembleSessionServices } from './app-session-services.js'; export interface MaoApp { app: FastifyInstance; close(): Promise; } /** * 装配期共享上下文:集中管理 createMaoApp 内部的延迟解引用变量。 * 这些变量在构造顺序上晚于某些 service 的构造,通过 ctx 属性访问 * 使依赖关系显式化,避免散落的 let + 闭包捕获。 */ export interface AppContext { db: Db; /** 飞书入站处理器:构造于 feishu AuthService 之后,pendingBinding 回调延迟解引用。 */ pendingBindingProcessor: FeishuInboundProcessor | undefined; /** 飞书待绑定消息不匹配通知:同上,延迟解引用。 */ notifyFeishuPendingMismatch: ((pending: { appId: number; event: FeishuNormalizedMessage }) => Promise) | undefined; /** 终端管理器:会话删除回调延迟解引用。 */ terminalManagerRef: TerminalManager | null; /** 定时任务清理回调:会话删除路径延迟解引用。 */ deleteScheduledTasksForSession: ((sessionId: number) => Promise) | null; /** MCP 同步服务:会话删除路径延迟解引用。 */ mcpSyncRef: McpSyncService | null; /** 容器运行时激活标志:幂等激活用。 */ containerRuntimeActive: boolean; /** 幂等关闭 Promise:drain 监听器与 SIGTERM 可能同时触发。 */ closing: Promise | null; } export async function createMaoApp(cfg: AppConfig = loadConfig(), existing?: FastifyInstance): Promise { assertGitCredentialSecret(cfg.app.gitCredential.secretKey); await runFlywayIfEnabled(cfg); const pool = createPool(cfg); const db = new Db(pool); const ctx: AppContext = { db, pendingBindingProcessor: undefined, notifyFeishuPendingMismatch: undefined, terminalManagerRef: null, deleteScheduledTasksForSession: null, mcpSyncRef: null, containerRuntimeActive: false, closing: null, }; const settingsSecret = process.env.SETTINGS_SECRET ?? ''; const settingRepo = new MysqlSystemSettingRepository(db); await runSettingsBootstrap(settingRepo, settingsSecret); // 上传上限后台可配:multipart 截断阈值取配置值与默认 1GB 的较大者,避免后台调大后被 multipart 层先截断 const bootstrapSettings = new SystemSettingService(settingRepo, { findById: async () => null }, { findById: async () => null }, { workspaceRoot: '', skillsDir: '' }, settingsSecret); const bootstrapUploadCfg = await bootstrapSettings.getUploadConfig(); const multipartLimitMb = Math.max(1024, bootstrapUploadCfg.maxSizeMb); // Agent 线程池/WS 超时/harness 调参启动时从 DB 构建,后台改动需重启生效;调度参数走 getter 即时生效 const agentRuntimeCfg = await bootstrapSettings.getAgentRuntimeConfig(); const harnessTuning = await bootstrapSettings.getHarnessTuningConfig(); const terminalCfg = await bootstrapSettings.getTerminalConfig(); const app = existing ?? Fastify({ logger: fastifyLoggerOptions, trustProxy: loadTrustedProxyAddresses(), bodyLimit: Math.max(52, multipartLimitMb + 2) * 1024 * 1024 }); const hasher = { hash: hashPassword, matches: matchesPassword }; const jwt = new JwtService(cfg.jwt.secret, cfg.jwt.expiration, cfg.jwt.refreshExpiration, cfg.jwt.shellExpiration); const apiPrefix = cfg.server.servlet.contextPath || '/api'; const ssoExchangePath = `${apiPrefix}/v1/auth/sso/exchange`; await app.register(cors, { delegator: async (request: FastifyRequest) => corsForRequest(request, ssoExchangePath, bootstrapSettings), }); await app.register(multipart, { limits: { fileSize: multipartLimitMb * 1024 * 1024, files: 500 } }); const uploadDir = resolve(expandHome(cfg.app.file.uploadDir)); mkdirSync(uploadDir, { recursive: true }); // 公共共享目录(共享目录方案 §3.1):与 uploadDir 同口径启动兜底创建, // 容器模式 ro 挂载前保证宿主机源目录存在(docker -v 对缺失源虽会自动建空目录, // 但属主会是 root,这里以后端运行用户创建,管理员投放时属主一致)。 // resolveShareDir 对空值/相对路径启动即拒:静默按 cwd 解析会把后端工作树 // (含 .env 密钥)ro 挂进所有用户容器(round-02 review Bug 1)。 const shareDir = resolveShareDir(cfg.app.harness.shareDir); mkdirSync(shareDir, { recursive: true }); // share 按 Agent 隔离(2026-11-09 形态 C):容器内改为挂两条子路径 // (agent-experience/{agentId} 与 agents/{agentId}),启动兜底创建 agents/ 父目录; // 具体 Agent 子目录由挂载校验(assertSafe)与运行时按需创建。 mkdirSync(join(shareDir, 'agents'), { recursive: true }); await registerUploadStatic(app, uploadDir, apiPrefix); await app.register(swagger, { openapi: { info: { title: 'Mao API', version: '0.1.0' } }, }); await app.register(swaggerUi, { routePrefix: '/swagger-ui' }); const userRepo = new MysqlUserRepository(db); const roleRepo = new MysqlRoleRepository(db); const permRepo = new MysqlPermissionRepository(db); const rolePermRepo = new MysqlRolePermissionRepository(db); const userRoleRepo = new MysqlUserRoleRepository(db); const permissionService = new PermissionService(roleRepo, permRepo, rolePermRepo, userRoleRepo, userRepo); // 邮箱变更监听(决策 2):闭包延迟绑定 notifPref(装配顺序上它构造于后), // 仅在资料更新的请求处理期被调用,不存在时序风险。 const userService = new UserService(userRepo, permissionService, hasher, (userId, email) => notifPref.refreshEmailAddress(userId, email)); const ldap = new LdapAuthService(userRepo, userRoleRepo, jwt, async () => (await settingService.getLdapConfig())); const authService = new AuthService( userRepo, jwt, hasher, ldap, permissionService, () => settingService.passwordLoginEnabled(), ); const earlyFeishuBinding = new MysqlFeishuBindingRepository(db); const pendingBindingMessages = new MysqlFeishuPendingBindingRepository(db); // pendingBindingProcessor / notifyFeishuPendingMismatch 已迁移到 ctx(延迟解引用) const feishu = new FeishuAuthService( userRepo, userRoleRepo, new MysqlFeishuOauthStateRepository(db), jwt, async () => (await settingService.getFeishuOAuthConfig()), undefined, async (user, targetUserId, state) => { if (user.id != null && user.feishuUserId != null && user.feishuUserId !== '') { await earlyFeishuBinding.bind(targetUserId ?? user.id, user.feishuUserId); } if (state != null && ctx.pendingBindingProcessor != null) { const pending = await pendingBindingMessages.claim(state); if (pending != null) { try { await replayFeishuPendingMessage(ctx.pendingBindingProcessor, pending); await pendingBindingMessages.complete(state!); } catch (error) { await pendingBindingMessages.release(state!); console.error(`恢复飞书待绑定消息失败, state=${state}`, error); } } } }, ); const ecpOAuthRepo = new MysqlEcpOauthStateRepository(db); const ecpIdentityRepo = new EcpIdentityRepository(db); const ecpSessionRepo = new MysqlEcpSessionRepository(db, settingsSecret); const ecpAuth = new EcpAuthService( userRepo, ecpOAuthRepo, ecpIdentityRepo, ecpSessionRepo, jwt, authService, () => settingService.getEcpConfig(), undefined, async (user, state) => { if (user.id == null) return; // 返回值透出重放结局:通道绑定回调据此分档渲染(不得把「重放失败」报成绑定成功) return completeFeishuPendingAfterEcp({ state, ecpUserId: user.id, claim: (s) => pendingBindingMessages.claim(s), findUserIdByUnionId: (unionId) => earlyFeishuBinding.findUserIdByUnionId(unionId), bind: (userId, unionId) => earlyFeishuBinding.bind(userId, unionId), replay: async (pending) => { if (ctx.pendingBindingProcessor == null) throw new Error('飞书入站处理器未就绪'); await replayFeishuPendingMessage(ctx.pendingBindingProcessor, pending); }, complete: (s) => pendingBindingMessages.complete(s), release: (s) => pendingBindingMessages.release(s), failClaimed: (s) => pendingBindingMessages.failClaimed(s), notifyMismatch: async (pending) => { await ctx.notifyFeishuPendingMismatch?.(pending); }, }); }, ); const gitCredentials = new GitCredentialService(db, cfg.app.gitCredential.secretKey); // 说明:凭据注入统一走 GitTokenResolver(技术方案 2026-11-06),其构造依赖 agentRepo(:384) / // settingService(:435) / sessionRepo(:467),因此延后到 sessionRepo 之后创建(见下方 gitTokenResolver)。 // gitCredentials 实例保留:仍供 user/admin 凭据路由(app-routes 侧)直接使用。 const auditRepo = new MysqlAuditLogRepository(db); const auditService = new AuditLogService(auditRepo); // 开放接口:API Token 身份层(preHandler 的 mao_ 前缀查库降级)+ 共享限流器 const apiTokenRepo = new MysqlApiTokenRepository(db); const apiTokenCipher = new ApiTokenCipher(cfg.app.openapi.tokenSecretKey); const apiTokenService = new ApiTokenService(apiTokenRepo, apiTokenCipher); const openApiRateLimiter = new FixedWindowRateLimiter(); const openCallLogHolder: { current: OpenApiCallLogService | null } = { current: null }; registerAppHooks({ app, jwt, apiTokenService, openCallLogHolder, auditService, userRepo, ssoExchangePath, }); const agentRepo = new MysqlAgentRepository(db); const experienceService = new AgentExperienceService(new MysqlAgentExperienceRepository(db)); const suggestedQuestionService = new AgentSuggestedQuestionService(new MysqlAgentSuggestedQuestionRepository(db)); const modelRepo = new MysqlLlmModelRepository(db); const agentService = new AgentService(agentRepo, experienceService, suggestedQuestionService, modelRepo); const llmCallService = new LlmCallService( new LlmCallRepository(db), { findUsername: async (id) => { const user = await userRepo.findById(id); return user ? { username: user.username, displayName: user.displayName } : null; }, }, { findName: async (id) => { const agent = await agentRepo.findById(id); return agent?.name ?? null; }, }, // 兜底价格缓存(60s TTL):价格未随 LlmModelConfig 下发的调用路径按 modelId 回查模型行 (id) => modelRepo.findById(id), ); const modelChatClient = new RecordingLlmChatClient( new OpenAiChatClient({ timeoutMs: harnessTuning.llm.callTimeoutSeconds * 1000 }), llmCallService, ); const anthropicChatClient = new RecordingLlmChatClient( new AnthropicChatClient({ timeoutMs: harnessTuning.llm.callTimeoutSeconds * 1000 }), llmCallService, ); const responsesChatClient = new RecordingLlmChatClient( new ResponsesChatClient({ timeoutMs: harnessTuning.llm.callTimeoutSeconds * 1000 }), llmCallService, ); const llmChatClients = new Map([ ['anthropic', anthropicChatClient], ['openai-responses', responsesChatClient], ]); /** 连通性测试、飞书溢出摘要等非流式链路共用的按 apiProtocol 协议路由(与 LlmAdapterFacade 同策略)。 */ const routeChatClient = (config: LlmModelConfig): LlmChatClient => { const code = config.apiProtocol?.trim().toLowerCase(); if (code != null && llmChatClients.has(code)) return llmChatClients.get(code)!; return modelChatClient; }; const modelService = new ModelService( modelRepo, new MysqlSessionModelRepository(db), modelChatClient, llmChatClients, ); const settingService = new SystemSettingService( settingRepo, { findById: (id) => agentRepo.findById(id) }, { findById: (id) => modelRepo.findById(id) }, { workspaceRoot: cfg.app.harness.workspaceRoot, skillsDir: cfg.app.harness.skillsDir, }, settingsSecret, ); const commandRepo = new MysqlUserCommandRepository(db); const commandService = new UserCommandService(commandRepo); // 三级环境变量(技术方案 §5.6):系统层 + 用户层 + 工作区层共用一套仓储/校验/缓存 const envVariableService = new EnvVariableService( new MysqlEnvVariableRepository(db), cfg.app.gitCredential.secretKey, cfg.app.harness.workspaceRoot, { shellEnvVarsEnabled: () => settingService.shellEnvVarsEnabled() }, async (userId) => (await gitCredentials.listByUserId(userId)) .map((credential) => GitCredentialService.envVarNameForDomain(credential.domain)), ); const weixinPref = new UserWeixinPreferenceService(new MysqlUserWeixinPreferenceRepository(db)); const taskPanelPref = new UserTaskPanelPreferenceService(new MysqlUserTaskPanelPreferenceRepository(db)); const onboardingPrefRepo = new MysqlUserOnboardingPreferenceRepository(db); const pathSandbox = new PathSandbox(cfg.app.harness.workspaceRoot); const runtimeRoot = cfg.app.harness.runtimeDir; // 放行 runtime 目录,使 Agent 工具(读文件/Shell/Grep/Glob 等)可访问会话 runtime 下的上传文件。 pathSandbox.addAllowedRoot(runtimeRoot); // 团队经验库(技术方案 2026-10-24 §3.7):工作区符号链接 团队经验 -> /agent-experience/, // 链接目标命中允许根才不会被沙箱当作路径逃逸拦截(容器严格模式同款放行见 container-tool-executor)。 pathSandbox.addAllowedRoot(shareDir); const sessionRepo = new SessionRepository(db); // Git 凭据统一解析器(技术方案 2026-11-06):六条注入通道的唯一凭据数据源 = // 用户 token ∩ Agent 允许域;依赖 gitCredentials / agentRepo / settingService,均已在本行之前构造。 const gitTokenResolver = new DefaultGitTokenResolver({ gitCredentialService: gitCredentials, agentLookup: agentRepo, sessionLookup: sessionRepo, scopePolicy: settingService, }); const messageRepo = new MessageRepository(db); // run 轨迹读模型:零新表,读时聚合消息 / llm_call / session_activity / 压缩事件。 const llmCallRepo = new LlmCallRepository(db); const runTraceService = new RunTraceService( messageRepo, llmCallRepo, new SessionActivityRepository(db), new SessionCompactionEventRepository(db), ); // run 轨迹记录详情读模型(技术方案 2026-10-22-run-trace-deep-dive §5.3):只读, // 按需拉取参数 / 结果 / 计时与元信息,与 buildTrace 共用 messageRepo / llmCallRepo。 const runTraceRecordService = new RunTraceRecordService(messageRepo, llmCallRepo); const feedbackRepository = new FeedbackRepository(db); const feedbackMessageLookup = new FeedbackDbLookup(db); const feedbackService = new FeedbackService( feedbackRepository, feedbackMessageLookup, async (sessionId) => { const session = await sessionRepo.findById(sessionId); return session?.userId ?? null; }, async (sessionId) => { const session = await sessionRepo.findById(sessionId); return session?.agentId ?? null; }, // 事务内回写反馈状态必须同事务(db.transaction 把连接包成新 Db 传回调) db, ); const fileChangeRepo = new FileChangeRepository(db); const compactionRepo = new SessionCompactionRepository(db); const sessionCompactionService = new SessionCompactionService(compactionRepo, messageRepo, sessionRepo); const sessionCompactionEventService = new SessionCompactionEventService(new SessionCompactionEventRepository(db)); const envInfo = new EnvironmentInfoProvider(); const runtimeResolver = new RuntimeDataResolver(cfg.app.harness.runtimeDir, cfg.app.harness.userHomeDir, cfg.app.harness.skillViewsDir); const gitOps = new GitOperationService(gitTokenResolver, runtimeResolver); const todoMapper = new SessionTodoMapper(db); const todoRepo = new SessionTodoRepository(db); // TerminalManager / 定时任务存储 / MCP 同步服务已迁移到 ctx(延迟解引用) // 审批放行规则(V135):repo/service 早于 sessionService 与 toolDispatcher 构造(双向依赖) const approvalRuleRepo = new ApprovalRuleRepository(db); const approvalRuleService = new ApprovalRuleService( approvalRuleRepo, { getValue: (key) => settingService.getValue(key) }, { getUserId: async (sessionId) => (await sessionRepo.findById(sessionId))?.userId ?? null }, ); // 会话级工作区规则快照(V153):只依赖 db,必须早于 sessionService 构造—— // onSessionDeleted 钩子在下面就要直接引用它(技术方案 §4.6 评审补充)。 const workspaceRulesSnapshotRepo = new WorkspaceRulesSnapshotRepository(db); const workspaceRulesSnapshotService = new WorkspaceRulesSnapshotService(workspaceRulesSnapshotRepo); // 团队经验库(技术方案 2026-10-24):索引/挂载服务,纯文件系统无 DB 依赖。 const experienceIndexService = new ExperienceIndexService(join(shareDir, EXPERIENCE_ROOT_SUBDIR)); const experienceMountService = new ExperienceMountService(experienceIndexService); const sessionService = new SessionService( sessionRepo, messageRepo, fileChangeRepo, { findById: (id) => agentRepo.findById(id) as never, findByIds: (ids) => agentRepo.findByIds(ids) as never, requireDefaultAgent: () => agentService.requireDefaultAgent() as never, listOptions: async () => (await agentRepo.selectList(null, true)).map((a) => ({ id: a.id!, name: a.name })), }, pathSandbox, envInfo, commandService, gitOps, sessionCompactionService, sessionCompactionEventService, todoRepo, runtimeSessionCleanup(runtimeRoot), (sessionId) => { ctx.terminalManagerRef?.closeBySession(sessionId); }, // 会话删除后清理绑定资源:定时任务 + CLOUD MCP 快照(技术方案 §4.5 第 3 点, // 快照存活期是会话级,只在会话删除路径回收)。mcpSync 晚于本 service 构造,故延迟解引用。 async (sessionId) => { await ctx.deleteScheduledTasksForSession?.(sessionId); ctx.mcpSyncRef?.clearCloudSnapshot(sessionId); // 工作区规则快照同理会话级资源,物理删行(技术方案 §4.6) await workspaceRulesSnapshotService.deleteBySessionId(sessionId); }, (sessionId) => approvalRuleRepo.deleteBySessionId(sessionId), experienceMountService, ); const sessionSvc = sessionService as never; const sessionMap = sessionRepo as never; const compactionSvc = sessionCompactionService as never; const activityService = new ActivityService(new SessionActivityRepository(db)); const messageQueueService = new MessageQueueService(new MessageQueueRepository(db)); const subagentExecutionRepo = new SubagentExecutionRepository(db); const activityHeartbeat = new SessionActivityHeartbeat(sessionService); const fileRepo = new FileEntityRepository(db); const fileService = new FileService(fileRepo, uploadDir, async () => (await settingService.getUploadConfig()).maxSizeMb); const workspaceBrowse = new WorkspaceBrowseService(pathSandbox); const workspaceGit = new WorkspaceGitService(pathSandbox); const ossSts = new OssStsService( () => settingService.getOssConfig(), // 客户端创建失败时保留原始原因:静默替换会让「配置错误」和「网络抖动」都显示成同一条无信息量的错误 async (sts) => { try { return await createAliyunAssumeRoleClient(sts); } catch (e) { console.error('创建 OSS STS 客户端失败', e); throw new Error(`创建 OSS STS 客户端失败: ${(e as Error).message}`); } }, ); const userSkillsDir = cfg.app.harness.userSkillsDir || resolve(process.env.HOME ?? '/tmp', '.mao/data/userskills'); const skillLoader = new SkillLoader(pathSandbox, cfg.app.harness.skillsDir, cfg.app.harness.skillsCacheSeconds); const ecpInjector = createEcpCredentialsInjector( ecpSessionRepo, (userId) => runtimeResolver.resolveUserHomeDir(userId), ); const larkUatInjector = createLarkUatInjector({ ecpInjector, ecpClient: new EcpClient(), getConfig: () => settingService.getEcpConfig(), }); const skillViewService = new SkillViewService(cfg.app.harness.skillViewsDir); const skillSync = new SkillSyncService(skillLoader, pathSandbox, runtimeResolver, userSkillsDir, skillViewService); const userSkillService = new UserSkillService(userSkillsDir, skillLoader); const skillDocService = new SkillDocService(skillLoader); // 资产分发闭环 P3:mao-skill-bundle v1 导出/导入 const skillBundleService = new SkillBundleService(skillLoader, userSkillService); const weixinMcpServices = assembleWeixinMcpServices({ db, cfg, ctx, pathSandbox, userRepo, agentRepo, settingService, earlyFeishuBinding, }); const { weixinConfig, weixinAccounts, feishuBots, feishuBinding, weixinTokens, weixinSend, weixinUpload, weixinMedia, weixinToolSupport, wechatBridges, mcpCipher, mcpMapper, mcpPref, mcpServerService, mcpClient, mcpToolsRegistry, userMcpLoadModeService, mcpLoadModeResolver, mcpSync } = weixinMcpServices; // 会话删除路径要回收 CLOUD MCP 快照,而 mcpSync 晚于 sessionService 构造:先用引用占位。 ctx.mcpSyncRef = mcpSync; // 飞书消息层先于 harness 装配:tool registry 的飞书工具在执行期通过闭包取用这里的 // message service / client,晚绑定语义与原 create-app 单体版本一致;同时保证后续 // wsHandler / 微信 / 钉钉 / GitCommitMessageService 拿到真实 harness 实例。 const feishuMessaging = assembleFeishuMessaging({ db, cfg, feishuBots, feishuBinding, userRepo, agentService, modelService, sessionService, routeChatClient, }); const { feishuMessageRepository, feishuMessageService, getFeishuClient, getFeishuChatTitle, sendFeishuText } = feishuMessaging; const mcpServerLookup = new MysqlMcpServerLookup(db); const llmEngine = assembleLlmEngine({ harnessTuning, llmCallService, skillLoader, pathSandbox, runtimeResolver, commandService, skillSync, workspaceRulesSnapshotService, sessionSvc, compactionSvc, sessionCompactionEventService, settingService, modelRepo, }); const { openAiLlmAdapter, anthropicLlmAdapter, responsesLlmAdapter, llmAdapter, promptEngine, tokenEstimator, compactionService, contextManager, compactionArchiveService, compactionConfig, historyLoader, activeContext, compactionModelResolver, orchestrator } = llmEngine; const backgroundTasks = new BackgroundTaskManager(); const shellManager = new ShellSessionManager( pathSandbox, runtimeResolver, harnessTuning.shell.maxSessionsPerConversation, harnessTuning.shell.sessionIdleTimeoutMinutes, harnessTuning.shell.sessionMaxLifetimeHours, undefined, undefined, // 三级环境变量:host 侧查库 + 解密,每条命令前随 refreshUserEnvironment 刷新 (userId, workspace) => envVariableService.resolveForSession(userId, workspace), ); // 云端终端:与 shellManager 并列(后者是 Agent 的管道 shell,本类是用户的交互式 PTY) const terminalAudit = createTerminalAudit(auditService); // ── 用户运行时容器隔离(技术方案 §5):host 模式下整组组件闲置、派发分支短路 ── const containerRuntime = await assembleContainerRuntime({ cfg, ctx, jwt, sessionRepo, userRepo, settingService, runtimeResolver, gitTokenResolver, envVariableService, uploadDir, userSkillsDir, shareDir, mcpClient, activityHeartbeat, ecpInjector, larkUatInjector, }); const { cloudRuntimeCfg, containerChannelHolder, channelServer, containerRegistry, containerHostsResolver, runtimeModeResolver, containerAutoUpgradeResolver, activateContainerRuntime, envelopeBuilder, dockerRouter, sendBytesBridge, mcpRuntimeProbe, routingMcpClient, ptyChannelHubs, ptyHubGeneration, containerPtyFactory, bootRuntimeMode, notifyAgentSwitched } = containerRuntime; // 会话内切换 Agent → 预热新 (userId, agentId) 容器(2026-11-07 复合容器方案 §5.8) sessionService.onAgentSwitched = notifyAgentSwitched; const terminalManager = new TerminalManager({ pathSandbox, runtimeResolver, gitTokenResolver, shellToken: jwt, userLookup: { findById: (id: number) => userRepo.findById(id) as Promise<{ username: string } | null> }, ecpInjector, larkUatInjector, baseUrlProvider: settingService, // §5.13:仅容器用户注入 deps env(npm/pip 安装落到每用户 /deps 挂载),host 用户零变化 depsEnvProvider: async (userId) => (await dockerRouter.enabledFor(userId)) ? buildDepsEnv(cloudRuntimeCfg.container.depsDir, userId) : null, envVariableService, config: terminalCfg, audit: terminalAudit, ptyFactory: containerPtyFactory, }); ctx.terminalManagerRef = terminalManager; const terminalWsHandler = new TerminalWsHandler({ terminalManager, jwtService: jwt, permissionService, audit: terminalAudit, }); const outputManager = new OutputManager( cfg.app.harness.shell.output.maxPreviewLines, cfg.app.harness.shell.output.maxPreviewChars, ); const localSkills = new LocalSkillRegistry(); const localAgentsMd = new LocalAgentsMdRegistry(); const wsRegistry = new StreamingWsRegistry(cfg.app.ws.outboundQueueCapacity); const embedPageToolRegistry = new EmbedPageToolRegistry( wsRegistry, resolveEmbedPageToolTimeoutMs(cfg.app.harness.embedPageToolTimeoutSeconds), ); const localToolSessions = new LocalToolSessionRegistry(wsRegistry, sessionMap); // 通知渠道与开放接口(触发器/订阅 secret)共用的 AES-GCM 密文工具(APP_NOTIFICATION_WEBHOOK_SECRET) const notifCipher = new WebhookSecretCipher(cfg.app.taskNotification.secretKey); // 开放接口出站订阅(P3):先于 inboxService 构造,question.pending 分发经 InboxService // 可选回调接入同一收口(ask_user 派发点在工具层直调 recordQuestionPending) const outboundSubscriptionRepo = new MysqlOutboundSubscriptionRepository(db); const outboundDeliveryRepo = new MysqlOutboundDeliveryRepository(db); // 通用出站发送器共享单实例:订阅服务(测试投递)与投递调度器(真实投递)同用 const genericWebhookSender = new GenericHttpWebhookSender(); const outboundSubscriptionService = new OutboundSubscriptionService({ subscriptionRepo: outboundSubscriptionRepo, deliveryRepo: outboundDeliveryRepo, cipher: notifCipher, sender: genericWebhookSender, }); const inboxRepo = new InboxRepository(db); const inboxService = new InboxService( inboxRepo, wsRegistry as never, { // 标题查询失败降级由 service 内部处理(子会话标题查不到时回落 taskDescription) userIdOf: async (sessionId) => (await sessionService.getSession(sessionId))?.userId ?? null, titleOf: async (sessionId) => (await sessionService.getSession(sessionId))?.title ?? null, }, // LOCAL/审批路径的 userId 兜底:复用 LocalToolSessionRegistry 的内存缓存 → session 表 // → SUBAGENT 父会话三级回退(与 tool-dispatcher 同一路径),不新造 session 查询 (sessionId) => localToolSessions.getUserIdForSession(sessionId), (input) => outboundSubscriptionService.dispatchQuestionPending(input), ); const inboxCleanupStore: InboxCleanupStore = { deleteHistory: (cutoff) => inboxRepo.deleteHistory(cutoff).then(() => undefined), }; const definitionRegistry = new AgentDefinitionRegistry(); const subagentMapper = new SubagentExecutionMapper(db); const subagentInvocation = new SubagentInvocationService(db); const subagentResultDelivery = new SubagentResultDeliveryService(db, fileChangeRepo as never, inboxService); const askUserQuestionsRegistry = new AskUserQuestionsRegistry(); const inFlightToolRegistry = new InFlightToolRegistry(); const feishuAskFormStore = new FeishuAskFormStore(); const feishuActiveProgress = new FeishuActiveProgressRegistry(); const feishuAskMount = createFeishuAskMount(feishuAskFormStore, feishuActiveProgress); const approvalRegistry = new ApprovalRegistry(sessionSvc, sessionMap, wsRegistry); const treeSignalPublisher = new SessionTreeSignalPublisher( sessionRepo, approvalRegistry, askUserQuestionsRegistry, wsRegistry, ); const localToolExecutor = new LocalToolExecutor( localToolSessions, approvalRegistry, treeSignalPublisher, cfg.app.harness.localToolTimeoutSeconds, inboxService, ); const dangerAssessor = new DangerAssessor(llmAdapter); const proxyApprover = new ProxyApprover(llmAdapter); const jevRiskAssessor = new JevRiskAssessor({ getValue: (key) => settingService.getValue(key), getSecretValue: (key) => settingService.getSecretValue(key), }); const approvalModelResolver = new ApprovalModelResolver( (key) => settingService.getValue(key), (id) => modelRepo.findById(id), ); // 用量预算服务(技术方案 §5.5-5.9):消耗查询 + admission BLOCK + WARN 结算 + CRUD。 // usage_budget 无外键:USER/AGENT 目标删除后预算行仍在(检查跳过、列表展示"已删除")。 const budgetService = new BudgetService( new BudgetRepository(db), new BudgetSpendStore(db), { findUser: async (id) => { const user = await userRepo.findById(id); return user ? { id: user.id!, name: user.username } : null; }, findAgent: async (id) => { const agent = await agentRepo.findById(id); return agent ? { id: agent.id!, name: agent.name } : null; }, }, inboxService, ); const agentExecutor = createAgentExecutor( agentRuntimeCfg.threadPoolSize, agentRuntimeCfg.threadPoolMax, agentRuntimeCfg.threadPoolQueue, ); const holder: { harness?: HarnessService; loop?: AgentLoop; compactionSignalBus?: CompactionSignalBus } = {}; const scheduledStore = new ScheduledTaskDbStore(db); ctx.deleteScheduledTasksForSession = (sessionId) => scheduledStore.deleteBySessionId(sessionId); const notifServices = assembleNotificationServices({ db, settingService, userRepo, sessionRepo, sessionService, messageRepo, messageQueueService, modelRepo, llmAdapter, notifCipher, }); const { senderRegistry, notifPref, weixinWebhookFallback, deliveryService, memoryRepo, memoryService, memoryExtraction, messageCitationService } = notifServices; localToolExecutor.setEscalationNotifier({ prepare: async (sessionId, requestId) => { const session = await sessionRepo.findById(sessionId) as never; return deliveryService.prepareEscalation({ session, phase: 'APPROVAL', requestId, delayMs: PENDING_ESCALATION_DELAY_MS, }); }, suppress: (delivery) => deliveryService.suppressPending(delivery), }); const weixinInboundMessagesRef: { current: any } = { current: null }; const sessionServices = assembleSessionServices({ db, sessionService, sessionSvc, sessionMap, messageRepo, messageQueueService, activityService, activityHeartbeat, todoMapper, modelRepo, llmCallRepo, fileChangeRepo, wsRegistry, treeSignalPublisher, inboxService, budgetService, deliveryService, memoryExtraction, messageCitationService, outboundSubscriptionService, weixinWebhookFallback, weixinSend, weixinAccounts, weixinTokens, weixinInboundMessagesRef, definitionRegistry, subagentMapper, subagentInvocation, localToolSessions, scheduledStore, agentExecutor, holder, // 等待子代理期间父侧无 AgentLoop 事件,飞书进度卡靠它每 10s 续更一次子代理轮次。 waitProgressNotifier: (sessionId: number) => { void feishuActiveProgress.refresh(sessionId).catch((error) => { console.warn(`飞书进度卡等待子代理期间刷新失败, sessionId=${sessionId}: ${error instanceof Error ? error.message : String(error)}`); }); }, }); const { taskTerminal, visibility, backgroundSubagentManager, scheduledService } = sessionServices; const dingtalkMediaHolder: { current: DingtalkMediaSendSupport | null } = { current: null }; // 自定义 HTTP 工具(V166):定义域 + 执行内核 + Agent 关联校验器。保留字校验要读内置工具名, // 而 registry 在下方创建,用惰性闭包打破循环(调用只发生在管理请求期)。 const httpToolRepo = new MysqlHttpToolRepository(db); const httpToolCipher = new HttpToolHeaderCipher(cfg.app.httpTool.secretKey); const httpToolService = new HttpToolService({ repo: httpToolRepo, cipher: httpToolCipher, builtinToolNames: () => (toolRegistry as any).getAllTools().map((tool: any) => tool.getName()), }); // 放行名单走系统设置(5s TTL 缓存由内核持有);试运行与 http_call 共用同一实例。 const httpApiExecutor = new HttpApiExecutor({ allowlistProvider: () => settingService.getPrivateAllowlistCidrs(), }); // Agent 关联校验(镜像 mcpValidator 接线):id 存在性 + 启用态。 const httpToolValidator = new HttpToolValidatorImpl(new MysqlHttpToolLookup(db)); // harness 必须在 wsHandler / 微信 / 钉钉 / GitCommitMessageService 之前装配: // 这些消费者在构造时就读取真实的 harness 与 agentLoop(holder 只是给运行期晚绑定用)。 // 信号哨兵服务同理(V189):watcherService 在 openapi 装配段才构造,工具执行期经 thunk 取。 let signalWatcherServiceRef: WatcherService | null = null; const harnessEngine = assembleHarnessEngine({ cfg, apiPrefix, uploadDir, harnessTuning, pathSandbox, todoMapper, scheduledService, getSignalWatcherService: () => { if (signalWatcherServiceRef == null) throw new Error('signal watcher service not assembled yet'); return signalWatcherServiceRef; }, sessionSvc, sessionMap, shellManager, outputManager, backgroundTasks, gitTokenResolver, jwt, userRepo, ecpInjector, larkUatInjector, settingService, runtimeResolver, modelService, wechatBridges, feishuMessageService, feishuMessageRepository, getFeishuClient, sendBytesBridge, dingtalkMediaHolder, definitionRegistry, holder, subagentMapper, localToolSessions, visibility, backgroundSubagentManager, messageRepo, compactionSvc, embedPageToolRegistry, mcpToolsRegistry, routingMcpClient, httpToolService, httpApiExecutor, llmAdapter, promptEngine, contextManager, compactionService, compactionArchiveService, compactionConfig, historyLoader, activeContext, orchestrator, tokenEstimator, compactionModelResolver, localToolExecutor, dangerAssessor, proxyApprover, jevRiskAssessor, approvalModelResolver, approvalRuleService, dockerRouter, approvalRegistry, askUserQuestionsRegistry, inFlightToolRegistry, treeSignalPublisher, deliveryService, feishuAskMount, inboxService, skillLoader, skillSync, localSkills, localAgentsMd, agentRepo, experienceService, modelRepo, fileChangeRepo, envInfo, db, mcpSync, mcpLoadModeResolver, activityHeartbeat, memoryService, wsRegistry, experienceIndexService, experienceMountService, }); const { toolRegistry, toolDispatcher, compactionSignalBus, agentLoop, harness } = harnessEngine; const sessionTitleService = new SessionTitleService( sessionRepo, messageRepo, commandService, llmAdapter, { selectById: (id: number) => modelRepo.findById(id), selectDefault: () => modelRepo.findDefault(), }, { getValue: (key: string) => settingService.getValue(key) }, wsRegistry, (fn) => agentExecutor.submit(fn), ); const usageService = new LlmUsageService(new LlmUsageRepository(db)); const gitCommitMsg = new GitCommitMessageService(llmAdapter as never, harness as never, usageService, { getValue: (key: string) => settingService.getValue(key) }); const gitWrite = new GitWriteOperationService( workspaceGit, gitCommitMsg, gitTokenResolver, runtimeResolver, activityService, ); const weixinServices = assembleWeixinServices({ db, sessionService, sessionRepo, agentService, modelService, settingService, weixinConfig, weixinAccounts, weixinTokens, weixinSend, weixinUpload, weixinMedia, weixinPref, harness, agentLoop, shellManager, wsRegistry, taskTerminal, activityService, activityHeartbeat, todoMapper, weixinWebhookFallback, llmAdapter, agentExecutor, }); const { weixinSession, voiceSynthesis, voiceCodec, voiceReply, weixinFiles, weixinInboundHandler, weixinInboundMessages, inboundProcessor, weixinMonitor, qrLogin } = weixinServices; weixinInboundMessagesRef.current = weixinInboundMessages; const restToolService = new ToolService({ getAllTools: () => (toolRegistry as any).getAllTools().map((t: any) => ({ name: t.getName(), description: t.getDescription() })), getTool: (name) => { const t = (toolRegistry as any).getTool(name); return t ? { name: t.getName(), description: t.getDescription() } : null; }, }); const openTriggerSettle: { current: ((triggerId: number, status: 'COMPLETED' | 'FAILED' | 'CANCELLED') => Promise) | null } = { current: null }; const openCallSettle: { current: ((callLogId: number, phase: 'COMPLETED' | 'FAILED' | 'CANCELLED', queueWaitMs: number | null, messageId: number | null) => Promise) | null } = { current: null }; const wsHandler = assembleWsHandler({ cfg, jwt, wsRegistry, sessionTitleService, harnessService: harness, agentLoop, compactionSignalBus, sessionService, taskTerminal, messageQueueService, embedPageToolRegistry, localToolSessions, askUserQuestionsRegistry, treeSignalPublisher, approvalRegistry, approvalRuleService, activityService, approvalEscalationSuppressor: { suppressByRequest: (sessionId, requestId) => deliveryService.suppressByEventKey(`approval:${sessionId}:${requestId}`), }, activityHeartbeat, todoMapper, backgroundSubagentManager, shellManager, skillSync, localSkills, localAgentsMd, mcpSync, routingMcpClient, agentRepo, modelRepo, userRepo, agentExecutor, budgetService, scheduledService, openTriggerSettle, openCallSettle, }); const containerScriptProbe = new ContainerScriptProbe({ enabledFor: (userId) => dockerRouter.enabledFor(userId), ensureRunning: (userId, agentId) => containerRegistry.ensureRunning(userId, agentId), scriptProbeRuntimeReady: (entry) => containerRegistry.scriptProbeRuntimeReady(entry), getConnection: (userId, agentId) => channelServer.getConnection(userId, agentId), depsEnvOf: (userId) => buildDepsEnv(cloudRuntimeCfg.container.depsDir, userId), }); const openApiServices = assembleOpenApiServices({ db, cfg, sessionService, messageQueueService, taskTerminal, agentRepo, wsHandler, budgetService, apiTokenService, inboxService, settingService, openApiRateLimiter, notifCipher, outboundSubscriptionService, holder, agentExecutor, openCallLogHolder, openTriggerSettle, openCallSettle, // 哨兵凭据占位符签发依赖(均构造于本调用点之前,时序无碍) jwt, userRepo, ecpSessionRepo, // 脚本探针 HOME 解析(V189):runtimeResolver 即 RuntimeDataResolver runtimeDataResolver: runtimeResolver, containerScriptProbe, }); const { openRunService, callLogService, callLogCleanup, webhookTriggerService, watcherService, watcherScheduler, signalCheckCleanup, digestService, digestScheduler, signalWatcherRepo } = openApiServices; // 晚绑定回填:openapi 装配完成后信号哨兵工具即可用(工具仅在会话运行期调用,时序无碍) signalWatcherServiceRef = watcherService; const feishuHandler = assembleFeishuHandler({ db, cfg, ctx, feishuBots, feishuBinding, userRepo, agentService, modelService, settingService, sessionService, sessionRepo, wsRegistry, wsHandler, harness, agentLoop, shellManager, feishuMessageRepository, feishuMessageService, getFeishuClient, getFeishuChatTitle, sendFeishuText, taskTerminal, scheduledService, feedbackService, feedbackMessageLookup, backgroundSubagentManager, localToolSessions, treeSignalPublisher, askUserQuestionsRegistry, ecpAuth, ecpSessionRepo, feishu, pendingBindingMessages, sessionTitleService, permissionService, routeChatClient, feishuActiveProgress, feishuAskFormStore, dingtalkMediaHolder, mcpServerLookup, activityService, activityHeartbeat, todoMapper, jwt, }); const { feishuInboundHandler, feishuInboundProcessor, feishuCardActionService, feishuMonitor, dingtalk, feishuTaskQueue, feishuProgressCardRepo, createFeishuRecoveryProgress, analyticsService, statisticsService, adminAnalytics, httpToolStatsStore, mcpValidator } = feishuHandler; // 跨通道队列接力(0.0.434):IM(飞书/钉钉/微信)执行收尾且队列排空后消费平台排队消息 // (message_queue:定时任务/Webhook/插队);WS 侧执行收尾且平台队列排空后消费 IM 入站队列。 // 两侧忙检查互可见(isSessionExecuting / registerExternalBusyCheck),避免两套执行簿记双跑。 const platformQueueRelay = createPlatformQueueRelay({ sessionService, wsHandler }); weixinInboundHandler.setOnQueuesDrained(platformQueueRelay); weixinInboundHandler.setIsSessionExecuting((sessionId) => wsHandler.hasExecutionClaim(sessionId)); wsHandler.registerExternalBusyCheck((sessionId) => feishuInboundHandler.hasRunningSession(sessionId)); wsHandler.registerExternalBusyCheck((sessionId) => dingtalk.hasRunningSession(sessionId) === true); wsHandler.registerExternalBusyCheck((sessionId) => weixinInboundHandler.hasRunningSession(sessionId)); wsHandler.registerImQueueDrainHandler((sessionId) => { void feishuInboundHandler.drainNextIfPending(sessionId); }); wsHandler.registerImQueueDrainHandler((sessionId) => { void dingtalk.drainNextIfPending(sessionId); }); // 失败会话批量恢复: // 候选查询编排 + 排除判定 + 预览/执行入口;续跑内核在 app-lifecycle 构造 CrashRecoveryRunner // 后经 attachKernel 注入(路由注册先于 lifecycle 装配,故服务在此提前构造)。 const failedSessionRecovery = new FailedSessionRecoveryService({ store: sessionRepo, userLookup: { findByIds: (ids: number[]) => userRepo.findByIds(ids) }, agentLookup: { findByIds: (ids: number[]) => agentRepo.findByIds(ids) }, messageLookup: { getLastUserMessage: (sessionId: number) => sessionService.getLastUserMessage(sessionId) }, progressCardLookup: feishuProgressCardRepo, isSessionClaimed: (sessionId: number) => wsHandler.hasExecutionClaim(sessionId), pushFinalReply: (sessionId: number) => scheduledService.pushFinalReplyIfApplicable(sessionId), }); await registerAllRoutes({ app, db, cfg, apiPrefix, jwt, agentRuntimeCfg, authService, feishu, ecpAuth, userService, userRepo, permissionService, gitCredentials, envVariableService, sessionRepo, httpToolService, httpApiExecutor, httpToolStatsStore, fileService, agentService, experienceService, experienceIndexService, suggestedQuestionService, mcpValidator, httpToolValidator, modelService, settingService, activateContainerRuntime, runtimeModeResolver, containerHostsResolver, containerAutoUpgradeResolver, containerOps: containerRegistry, commandService, commandRepo, skillLoader, skillSync, weixinPref, taskPanelPref, onboardingPrefRepo, restToolService, auditService, llmCallService, sessionService, activityService, messageQueueService, agentRepo, modelRepo, todoRepo, pathSandbox, subagentExecutionRepo, sessionCompactionEventService, sessionCompactionService, runTraceService, runTraceRecordService, approvalRegistry, askUserQuestionsRegistry, inFlightToolRegistry, treeSignalPublisher, failedSessionRecovery, workspaceBrowse, terminalManager, workspaceGit, gitCommitMsg, gitWrite, runtimeResolver, ossSts, skillBundleService, userSkillService, skillDocService, scheduledService, openRunService, apiTokenService, webhookTriggerService, outboundSubscriptionService, openApiRateLimiter, callLogService, watcherService, signalWatcherRepo, analyticsService, statisticsService, adminAnalytics, feedbackService, memoryService, approvalRuleService, inboxService, digestService, budgetService, mcpServerService, mcpClient, mcpPref, userMcpLoadModeService, mcpLoadModeResolver, qrLogin, weixinAccounts, weixinMonitor, feishuBots, feishuBinding, feishuMonitor, dingtalk, notifPref, wsHandler, terminalWsHandler, hasDeviceConnection: (userId: number, deviceId: string) => wsRegistry.hasDeviceConnection(userId, deviceId), getExecutorClientType: (userId: number, deviceId: string) => { const socket = wsRegistry.getDeviceExecutorSocket(userId, deviceId); if (!socket) return null; const type = wsRegistry.getClientType(socket); return type === 'electron' || type === 'cli' ? type : null; }, }); const { closeApp } = assembleLifecycle({ app, db, cfg, ctx, scheduledStore, scheduledService, settingService, notifCipher, senderRegistry, sessionService, outboundDeliveryRepo, outboundSubscriptionRepo, auditService, genericWebhookSender, agentExecutor, inboxCleanupStore, callLogService, callLogCleanup, ecpSessionRepo, weixinMonitor, feishuMonitor, dingtalk, pendingBindingMessages, shellManager, terminalManager, wsHandler, subagentMapper, sessionMap, sessionSvc, compactionSvc, definitionRegistry, backgroundSubagentManager, agentLoop: holder.loop!, visibility, localToolSessions, subagentResultDelivery, taskTerminal, harness: holder.harness!, wsRegistry, activityService, activityHeartbeat, todoMapper, modelRepo, feishuProgressCardRepo, feishuInboundHandler, feishuTaskQueue, createFeishuRecoveryProgress, failedSessionRecovery, weixinInboundHandler, feishuInboundProcessor, watcherScheduler, signalCheckCleanup, ptyChannelHubs, ptyHubGeneration, containerRegistry, channelServer, }); return { app, close: closeApp, }; }