Ctrl K
ComunidadGuías…

backend-ts/src/app-openapi-services.ts

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

Archivo original ↗Preguntar a KiBot
import type { Db } from './db/db.js';
import { resolve } from 'node:path';
import type { AppConfig } from './config/app-config.js';
import type { SessionService } from './session/session.service.js';
import type { MessageQueueService } from './session/message-queue.service.js';
import type { TaskTerminalService } from './session/task-terminal.service.js';
import type { MysqlAgentRepository } from './agent/agent.repository.js';
import type { StreamingWsHandler } from './session/ws/streaming-ws-handler.js';
import type { BudgetService } from './budget/budget.service.js';
import type { ApiTokenService } from './openapi/api-token.service.js';
import type { InboxService } from './inbox/inbox.service.js';
import type { SystemSettingService } from './settings/settings.service.js';
import type { FixedWindowRateLimiter } from './openapi/rate-limiter.js';
import type { WebhookSecretCipher } from './notification/task/webhook-secret-cipher.js';
import type { OutboundSubscriptionService } from './openapi/outbound-subscription.service.js';
import type { HarnessService } from './harness/core/harness-service.js';
import type { JwtService } from './crypto/jwt.service.js';
import type { MysqlUserRepository } from './user/user.repository.js';
import type { MysqlEcpSessionRepository } from './auth/ecp-session.repository.js';
import { createEcpCredentialsInjector } from './auth/ecp-credentials-injector.js';
import { createAgentExecutor } from './harness/core/agent-executor.js';

import { OpenRunService, type OpenRunDeps } from './openapi/open-run.service.js';
import { OpenApiCallLogRepository } from './openapi/open-api-call-log.repository.js';
import { OpenApiCallLogService } from './openapi/open-api-call-log.service.js';
import { OpenApiCallLogCleanup } from './openapi/open-api-call-log.cleanup.js';
import { MysqlWebhookTriggerRepository } from './openapi/openapi.repository.js';
import { WebhookTriggerService, type WebhookTriggerDeps } from './openapi/webhook-trigger.service.js';
import { SignalSecretCipher } from './signal/signal-cipher.js';
import { MysqlSignalWatcherRepository, MysqlSignalCheckRepository } from './signal/signal.repository.js';
import { WatcherService } from './signal/watcher.service.js';
import { createWatcherCredentialResolver } from './signal/watcher-credential-resolver.js';
import type { RuntimeDataResolver } from './harness/runtime/runtime-data-resolver.js';
import type { ContainerScriptProbe } from './harness/container/container-script-probe.js';
import { WatcherScheduler } from './signal/watcher-scheduler.js';
import { SignalCheckCleanup } from './signal/signal-check-cleanup.js';
import { DigestRepository } from './digest/repository.js';
import { DigestService } from './digest/service.js';
import { DigestScheduler } from './digest/scheduler.js';
import { budgetBlockMessage, createScheduledLiveExecution } from './session/ws/streaming-ws-handler.js';

export interface OpenApiServicesDeps {
  db: Db;
  cfg: AppConfig;
  sessionService: SessionService;
  messageQueueService: MessageQueueService;
  taskTerminal: TaskTerminalService;
  agentRepo: MysqlAgentRepository;
  wsHandler: StreamingWsHandler;
  budgetService: BudgetService;
  apiTokenService: ApiTokenService;
  inboxService: InboxService;
  settingService: SystemSettingService;
  openApiRateLimiter: FixedWindowRateLimiter;
  runtimeDataResolver: RuntimeDataResolver;
  notifCipher: WebhookSecretCipher;
  outboundSubscriptionService: OutboundSubscriptionService;
  holder: { harness?: HarnessService };
  agentExecutor: ReturnType<typeof createAgentExecutor>;
  openCallLogHolder: { current: OpenApiCallLogService | null };
  openTriggerSettle: { current: ((triggerId: number, status: 'COMPLETED' | 'FAILED' | 'CANCELLED') => Promise<void>) | null };
  openCallSettle: { current: ((callLogId: number, phase: 'COMPLETED' | 'FAILED' | 'CANCELLED', queueWaitMs: number | null, messageId: number | null) => Promise<void>) | null };
  /** 哨兵凭据占位符({{MAO_TOKEN}}/{{ECP_TOKEN}})签发依赖(2026-11-06 方案 §5.1.4)。 */
  jwt: JwtService;
  userRepo: MysqlUserRepository;
  ecpSessionRepo: MysqlEcpSessionRepository;
  /** Docker 模式脚本探针桥。host 模式由桥内部短路,哨兵服务不再自行判断运行模式。 */
  containerScriptProbe: ContainerScriptProbe;
}

export interface OpenApiServicesResult {
  openRunService: OpenRunService;
  callLogService: OpenApiCallLogService;
  callLogCleanup: OpenApiCallLogCleanup;
  webhookTriggerService: WebhookTriggerService;
  watcherService: WatcherService;
  watcherScheduler: WatcherScheduler;
  signalCheckCleanup: SignalCheckCleanup;
  digestService: DigestService;
  digestScheduler: DigestScheduler;
  signalWatcherRepo: MysqlSignalWatcherRepository;
}

export function assembleOpenApiServices(deps: OpenApiServicesDeps): OpenApiServicesResult {
  const { db, cfg, sessionService, messageQueueService, taskTerminal, agentRepo, wsHandler, budgetService, apiTokenService, inboxService, settingService, openApiRateLimiter, notifCipher, outboundSubscriptionService, holder, agentExecutor, openCallLogHolder, openTriggerSettle, openCallSettle, jwt, userRepo, ecpSessionRepo, runtimeDataResolver, containerScriptProbe } = deps;

  const openRunService = new OpenRunService({
    sessionService: sessionService as unknown as OpenRunDeps['sessionService'],
    messageQueueService,
    harnessService: { executeFromEvent: (sessionId, executionId, listener) => holder.harness!.executeFromEvent(sessionId, executionId, listener as never) },
    taskTerminalService: taskTerminal,
    agentLookup: { findById: (id: number) => agentRepo.findById(id) },
    isSessionBusy: (sessionId) => wsHandler.hasCrossChannelBusy(sessionId),
    budgetCheck: async (target) => {
      const block = await budgetService.checkAdmission(target);
      return block == null ? null : { message: budgetBlockMessage(block) };
    },
    liveExecution: createScheduledLiveExecution(wsHandler),
  });
  const callLogService = new OpenApiCallLogService({
    repo: new OpenApiCallLogRepository(db),
    tokens: apiTokenService,
    inbox: inboxService,
    openRun: openRunService,
  });
  openCallLogHolder.current = callLogService;
  openCallSettle.current = (callLogId, phase, queueWaitMs, messageId) => callLogService.settleQueued(callLogId, phase, queueWaitMs, messageId);
  apiTokenService.setOutcomePolicy({
    enabled: () => settingService.isOpenApiTokenAutoDisableEnabled(),
    threshold: () => settingService.getOpenApiTokenAutoDisableThreshold(),
    notifyDisabled: (input) => inboxService.recordTokenDisabled(input),
  });
  const callLogCleanup = new OpenApiCallLogCleanup(
    new OpenApiCallLogRepository(db),
    () => settingService.getOpenApiCallLogRetentionDays(),
  );
  const webhookTriggerService = new WebhookTriggerService({
    triggerRepo: new MysqlWebhookTriggerRepository(db),
    sessionService: sessionService as unknown as WebhookTriggerDeps['sessionService'],
    agentLookup: { findById: (id: number) => agentRepo.findById(id) },
    cipher: notifCipher,
    openRun: openRunService,
    rateLimiter: openApiRateLimiter,
    disableNotifier: {
      notifyTriggerDisabled: async (input) => {
        await inboxService.recordTriggerDisabled(input);
      },
    },
    callLog: callLogService,
  });
  openTriggerSettle.current = (triggerId, status) => webhookTriggerService.handleQueueSettled(triggerId, status);

  const signalCipher = new SignalSecretCipher(cfg.app.signal.secretKey);
  const signalWatcherRepo = new MysqlSignalWatcherRepository(db);
  const signalCheckRepo = new MysqlSignalCheckRepository(db);
  // 哨兵凭据占位符 resolver(2026-11-06 方案 §5.1.4):MAO_TOKEN 用生产签发链(jwt + userRepo),
  // ECP_TOKEN 必须在装配层新建**只读** injector——resolveUserHome 返回 null 时写/清 AccessOne
  // 布局的分支全部跳过。不能传 create-app 里的 ecpInjector(带文件副作用,探针跑一轮会把
  // 用户 shell 的 ECP 布局清掉),只传 ecpSessionRepo 让这里自己建。
  const watcherCredentialResolver = createWatcherCredentialResolver({
    tokenIssuer: jwt,
    userLookup: userRepo,
    ecpInjector: createEcpCredentialsInjector(ecpSessionRepo, () => null),
  });
  const watcherService = new WatcherService({
    watcherRepo: signalWatcherRepo,
    checkRepo: signalCheckRepo,
    cipher: signalCipher,
    openRun: openRunService,
    // 脚本探针(V189):存储根 = {workspaceRoot}/{userId}/projects,HOME = 属主运行时目录
    scriptProjectsRoot: (userId) => resolve(cfg.app.harness.workspaceRoot, String(userId), 'projects'),
    scriptUserHome: (userId) => runtimeDataResolver.resolveUserHomeDir(userId),
    containerScriptProbe,
    // 复用指针有效性预检(技术方案 2026-10-27 §3.2.3)
    getSession: (id) => sessionService.getSession(id),
    disableNotifier: {
      notifyWatcherDisabled: async (input) => {
        await inboxService.recordWatcherDisabled(input);
      },
    },
    credentialResolver: watcherCredentialResolver,
  });
  const watcherScheduler = new WatcherScheduler({
    watcherRepo: signalWatcherRepo,
    runCheck: (id) => watcherService.runCheck(id),
    execute: (fn) => agentExecutor.submit(fn),
  });
  watcherScheduler.start();
  const signalCheckCleanup = new SignalCheckCleanup(
    signalCheckRepo,
    () => settingService.getSignalCheckRetentionDays(),
  );
  signalCheckCleanup.start();

  const digestRepo = new DigestRepository(db);
  const digestService = new DigestService({
    repo: digestRepo,
    inbox: {
      writeDigestEntry: (input) => inboxService.writeDigestEntry(input),
      hasDigestEntry: (userId, dedupTail) => inboxService.hasDigestEntry(userId, dedupTail),
    },
    outbound: {
      dispatchDigest: (input) => outboundSubscriptionService.dispatchDigest(input),
    },
  });
  const digestScheduler = new DigestScheduler({
    listEnabledUsers: () => digestRepo.listEnabledUsers(),
    processUser: (userId) => digestService.processUser(userId),
  });
  digestScheduler.start();

  return {
    openRunService, callLogService, callLogCleanup, webhookTriggerService,
    watcherService, watcherScheduler, signalCheckCleanup, digestService, digestScheduler,
    signalWatcherRepo,
  };
}