Ctrl K
ComunidadGuías…

backend-ts/src/create-app.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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<void>;
}

/**
 * 装配期共享上下文:集中管理 createMaoApp 内部的延迟解引用变量。
 * 这些变量在构造顺序上晚于某些 service 的构造,通过 ctx 属性访问
 * 使依赖关系显式化,避免散落的 let + 闭包捕获。
 */
export interface AppContext {
  db: Db;
  /** 飞书入站处理器:构造于 feishu AuthService 之后,pendingBinding 回调延迟解引用。 */
  pendingBindingProcessor: FeishuInboundProcessor | undefined;
  /** 飞书待绑定消息不匹配通知:同上,延迟解引用。 */
  notifyFeishuPendingMismatch: ((pending: { appId: number; event: FeishuNormalizedMessage }) => Promise<void>) | undefined;
  /** 终端管理器:会话删除回调延迟解引用。 */
  terminalManagerRef: TerminalManager | null;
  /** 定时任务清理回调:会话删除路径延迟解引用。 */
  deleteScheduledTasksForSession: ((sessionId: number) => Promise<void>) | null;
  /** MCP 同步服务:会话删除路径延迟解引用。 */
  mcpSyncRef: McpSyncService | null;
  /** 容器运行时激活标志:幂等激活用。 */
  containerRuntimeActive: boolean;
  /** 幂等关闭 Promise:drain 监听器与 SIGTERM 可能同时触发。 */
  closing: Promise<void> | null;
}

export async function createMaoApp(cfg: AppConfig = loadConfig(), existing?: FastifyInstance): Promise<MaoApp> {
  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<string, LlmChatClient>([
    ['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):工作区符号链接 团队经验 -> <shareDir>/agent-experience/<agentId>,
  // 链接目标命中允许根才不会被沙箱当作路径逃逸拦截(容器严格模式同款放行见 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<void>) | null } = { current: null };
  const openCallSettle: { current: ((callLogId: number, phase: 'COMPLETED' | 'FAILED' | 'CANCELLED', queueWaitMs: number | null, messageId: number | null) => Promise<void>) | 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,
  };
}