Ctrl K
ComunidadGuías…

backend-ts/src/app-harness-engine.ts

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

Archivo original ↗Preguntar a KiBot
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { AppConfig } from './config/app-config.js';
import type { JwtService } from './crypto/jwt.service.js';
import type { PathSandbox } from './harness/safety/path-sandbox.js';
import type { SessionTodoMapper } from './harness/todo/session-todo.mapper.js';
import type { ScheduledTaskService } from './schedule/scheduled-task.service.js';
import type { WatcherService } from './signal/watcher.service.js';
import type { SessionService } from './session/session.service.js';
import type { SessionRepository } from './session/session.repository.js';
import type { GitTokenResolver } from './session/types.js';
import type { ShellSessionManager, OutputManager } from './harness/shell/shell-session-manager.js';
import type { BackgroundTaskManager } from './harness/core/background-task-manager.js';
import type { RuntimeDataResolver } from './harness/runtime/runtime-data-resolver.js';
import type { ModelService } from './model/model.service.js';
import type { SystemSettingService } from './settings/settings.service.js';
import type { FeishuMessageService } from './feishu/message.service.js';
import type { MysqlFeishuMessageRepository } from './feishu/message.repository.js';
import type { DockerSendBytesBridge } from './harness/container/read-bytes-bridge.js';
import type { DingtalkMediaSendSupport } from './harness/tool/impl/dingtalk-tools.js';
import type { AgentDefinitionRegistry } from './harness/delegate/agent-definition-registry.js';
import type { SubagentExecutionMapper } from './harness/delegate/subagent-execution.mapper.js';
import type { LocalToolSessionRegistry } from './harness/local/local-tool-session-registry.js';
import type { SubAgentVisibilityService } from './harness/delegate/subagent-visibility-service.js';
import type { BackgroundSubagentManager } from './harness/delegate/background-subagent-manager.js';
import type { MessageRepository } from './session/session.repository.js';
import type { SessionCompactionService } from './session/session-compaction.service.js';
import type { EmbedPageToolRegistry } from './harness/embed-page-tool-registry.js';
import type { McpToolsRegistry } from './harness/mcp/local/mcp-tools-registry.js';
import type { McpClientManager } from './harness/mcp/mcp-client-manager.js';
import type { HttpToolService } from './http-tool/http-tool.service.js';
import type { HttpApiExecutor } from './harness/tool/impl/http-api-executor.js';
import type { HarnessTuningSettings } from './settings/types.js';
import type { LlmAdapter } from './harness/llm/chat-request.js';
import type { PromptEngine } from './harness/core/prompt-engine.js';
import type { ContextManager } from './harness/core/context-manager.js';
import type { CompactionService } from './harness/core/compaction-service.js';
import type { CompactionArchiveService } from './harness/core/compaction-archive.service.js';
import type { CompactionConfig } from './harness/core/compaction-config.js';
import type { SessionHistoryLoader } from './harness/core/session-history-loader.js';
import type { ActiveContextCalculator } from './harness/core/active-context-calculator.js';
import type { SessionCompactionOrchestrator } from './harness/core/session-compaction-orchestrator.js';
import type { TokenEstimator } from './harness/core/token-estimator.js';
import type { CompactionModelResolver } from './harness/tool/compaction-model-resolver.js';
import type { LocalToolExecutor } from './harness/local/local-tool-executor.js';
import type { DangerAssessor } from './harness/tool/danger-assessor.js';
import type { ProxyApprover } from './harness/tool/proxy-approver.js';
import type { JevRiskAssessor } from './harness/tool/jev-risk-assessor.js';
import type { ApprovalModelResolver } from './harness/tool/approval-model-resolver.js';
import type { ApprovalRuleService } from './approval-rule/approval-rule.service.js';
import type { DockerToolRouter } from './harness/container/docker-tool-router.js';
import type { ApprovalRegistry } from './harness/approval/approval-registry.js';
import type { AskUserQuestionsRegistry } from './harness/tool/ask-user-questions-registry.js';
import type { InFlightToolRegistry } from './harness/core/in-flight-tool-registry.js';
import type { SessionTreeSignalPublisher } from './harness/approval/session-tree-signal-publisher.js';
import type { TaskNotificationDeliveryService } from './notification/task/delivery.service.js';
import type { InboxService } from './inbox/inbox.service.js';
import type { SkillLoader } from './harness/skill/skill-loader.js';
import type { SkillSyncService } from './harness/skill/skill-sync-service.js';
import type { LocalSkillRegistry } from './harness/skill/local-skill-registry.js';
import type { LocalAgentsMdRegistry } from './harness/core/local-agents-md-registry.js';
import type { AgentExperienceService } from './agent/agent-experience.service.js';
import type { EnvironmentInfoProvider } from './harness/core/environment-info-provider.js';
import type { FileChangeRepository } from './session/session.repository.js';
import type { MysqlAgentRepository } from './agent/agent.repository.js';
import type { MysqlLlmModelRepository } from './model/model.repository.js';
import type { SessionActivityHeartbeat } from './session/session-activity-heartbeat.js';
import type { MemoryService } from './memory/memory.service.js';
import type { McpLoadModeResolver } from './harness/mcp/workingset/mcp-load-mode.js';
import type { McpSyncService } from './harness/mcp/local/mcp-sync-service.js';
import type { RoutingMcpClientManager } from './harness/container/routing-mcp-client-manager.js';
import type { Db } from './db/db.js';

import { createDefaultToolRegistry } from './harness/tool/tool-registry.js';
import { ToolDispatcher } from './harness/tool/tool-dispatcher.js';
import { createMcpReadOnlyLookup } from './harness/mcp/workingset/mcp-read-only-lookup.js';
import { CompactionSignalBus } from './harness/core/compaction-signal-bus.js';
import { AgentLoop } from './harness/core/agent-loop.js';
import { HarnessService } from './harness/core/harness-service.js';
import type { ExperienceIndexService } from './harness/experience/experience-index.service.js';
import { downloadFeishuMediaBuffer } from './feishu-media.js';
import { fetchFeishuMessageDetail } from './feishu/message-detail.js';
import { feishuSendTargetOf, sendFeishuFile, sendFeishuImage } from './feishu/media-sender.js';

export interface HarnessEngineDeps {
  cfg: AppConfig;
  apiPrefix: string;
  uploadDir: string;
  harnessTuning: HarnessTuningSettings;
  pathSandbox: PathSandbox;
  todoMapper: SessionTodoMapper;
  scheduledService: ScheduledTaskService;
  /** 信号哨兵服务晚绑定(V189):harness 装配先于 openapi 装配,工具执行期经 thunk 取实例。 */
  getSignalWatcherService: () => WatcherService;
  sessionSvc: SessionService;
  sessionMap: SessionRepository;
  shellManager: ShellSessionManager;
  outputManager: OutputManager;
  backgroundTasks: BackgroundTaskManager;
  gitTokenResolver: GitTokenResolver;
  jwt: JwtService;
  userRepo: any;
  ecpInjector: any;
  larkUatInjector: any;
  settingService: SystemSettingService;
  runtimeResolver: RuntimeDataResolver;
  modelService: ModelService;
  wechatBridges: any;
  feishuMessageService: FeishuMessageService;
  feishuMessageRepository: MysqlFeishuMessageRepository;
  getFeishuClient: (botId: number) => Promise<any>;
  sendBytesBridge: DockerSendBytesBridge;
  dingtalkMediaHolder: { current: DingtalkMediaSendSupport | null };
  definitionRegistry: AgentDefinitionRegistry;
  holder: { harness?: HarnessService; loop?: AgentLoop };
  subagentMapper: SubagentExecutionMapper;
  localToolSessions: LocalToolSessionRegistry;
  visibility: SubAgentVisibilityService;
  backgroundSubagentManager: BackgroundSubagentManager;
  messageRepo: MessageRepository;
  compactionSvc: SessionCompactionService;
  embedPageToolRegistry: EmbedPageToolRegistry;
  mcpToolsRegistry: McpToolsRegistry;
  routingMcpClient: McpClientManager;
  httpToolService: HttpToolService;
  httpApiExecutor: HttpApiExecutor;
  llmAdapter: LlmAdapter;
  promptEngine: PromptEngine;
  contextManager: ContextManager;
  compactionService: CompactionService;
  compactionArchiveService: CompactionArchiveService;
  compactionConfig: CompactionConfig;
  historyLoader: SessionHistoryLoader;
  activeContext: ActiveContextCalculator;
  orchestrator: SessionCompactionOrchestrator;
  tokenEstimator: TokenEstimator;
  compactionModelResolver: CompactionModelResolver;
  localToolExecutor: LocalToolExecutor;
  dangerAssessor: DangerAssessor;
  proxyApprover: ProxyApprover;
  jevRiskAssessor: JevRiskAssessor;
  approvalModelResolver: ApprovalModelResolver;
  approvalRuleService: ApprovalRuleService;
  dockerRouter: DockerToolRouter;
  approvalRegistry: ApprovalRegistry;
  askUserQuestionsRegistry: AskUserQuestionsRegistry;
  inFlightToolRegistry: InFlightToolRegistry;
  treeSignalPublisher: SessionTreeSignalPublisher;
  deliveryService: TaskNotificationDeliveryService;
  feishuAskMount: any;
  inboxService: InboxService;
  skillLoader: SkillLoader;
  skillSync: SkillSyncService;
  localSkills: LocalSkillRegistry;
  localAgentsMd: LocalAgentsMdRegistry;
  agentRepo: MysqlAgentRepository;
  experienceService: AgentExperienceService;
  modelRepo: MysqlLlmModelRepository;
  fileChangeRepo: FileChangeRepository;
  envInfo: EnvironmentInfoProvider;
  db: Db;
  mcpSync: McpSyncService;
  mcpLoadModeResolver: McpLoadModeResolver;
  activityHeartbeat: SessionActivityHeartbeat;
  memoryService: MemoryService;
  wsRegistry: any;
  /** 团队经验库索引服务(技术方案 2026-10-24):write_file/edit_file 落盘回调刷新索引。可选注入。 */
  experienceIndexService?: ExperienceIndexService | null;
  /** 团队经验库挂载服务:buildContext 每轮幂等补挂(存量会话兜底)。可选注入。 */
  experienceMountService?: { syncForSession(workspace: string, agentId: number, enabled: boolean, refreshIndex?: boolean): void } | null;
}

export interface HarnessEngineResult {
  toolRegistry: any;
  toolDispatcher: ToolDispatcher;
  compactionSignalBus: CompactionSignalBus;
  agentLoop: AgentLoop;
  harness: HarnessService;
}

export function assembleHarnessEngine(deps: HarnessEngineDeps): HarnessEngineResult {
  const d = deps as any;
  const { cfg, apiPrefix, uploadDir, harnessTuning } = d;

  const toolRegistry = createDefaultToolRegistry({
    pathSandbox: d.pathSandbox,
    sessionTodoMapper: d.todoMapper,
    scheduledTaskService: d.scheduledService,
    getSignalWatcherService: d.getSignalWatcherService,
    sessionService: d.sessionSvc,
    sessionMapper: d.sessionMap,
    shellSessionManager: d.shellManager,
    outputManager: d.outputManager,
    backgroundTaskManager: d.backgroundTasks,
    gitTokenResolver: d.gitTokenResolver,
    jwtService: d.jwt,
    shellUserLookup: { findById: (id: number) => d.userRepo.findById(id) },
    shellEcpInjector: d.ecpInjector,
    shellLarkUatInjector: d.larkUatInjector,
    shellBaseUrlProvider: { getSiteBaseUrl: () => d.settingService.getSiteBaseUrl() },
    webSearch: () => d.settingService.getWebSearchConfig(),
    webPage: harnessTuning.webPage,
    runtimeDataResolver: d.runtimeResolver,
    imageModelLookup: d.modelService,
    uploadDir,
    onFileChanged: d.experienceIndexService != null
      ? (absPath: string) => d.experienceIndexService!.refreshIfUnderRoot(absPath)
      : undefined,
    getUploadBaseUrl: async () => (await d.settingService.getUploadConfig()).baseUrl,
    uploadContextPath: apiPrefix,
    weixinToolSupport: d.wechatBridges.toolSupport,
    weixinUploadService: d.wechatBridges.uploadService,
    weixinSendService: d.wechatBridges.sendService,
    feishuToolSupport: {
      resolveBotAppId: async (sessionId: number | null) => {
        if (sessionId == null) return null;
        const channel = await d.feishuMessageService.findSessionChannel(sessionId);
        return channel?.appId ?? null;
      },
    },
    feishuMediaDownloader: {
      download: async (appId: string, messageId: string, fileKey: string, type: 'image' | 'file', maxBytes: number) => {
        const client = await d.getFeishuClient(Number(appId));
        if (client == null) throw new Error(`飞书Bot不存在或未启用: ${appId}`);
        return downloadFeishuMediaBuffer(client, messageId, fileKey, type, maxBytes);
      },
    },
    feishuGroupMediaLookup: { findMediaByMessageId: (messageId: string) => d.feishuMessageRepository.findMediaByMessageId(messageId) } as never,
    feishuMessageDetailFetcher: {
      fetchMessageDetail: async (appId: string, messageId: string) => {
        const client = await d.getFeishuClient(Number(appId));
        if (client == null) throw new Error(`飞书Bot不存在或未启用: ${appId}`);
        const detail = await fetchFeishuMessageDetail(client, messageId);
        return detail == null ? null : { fileKey: detail.fileKey ?? null, fileName: detail.fileName ?? null, msgType: detail.msgType, postFiles: detail.postFiles ?? null };
      },
    },
    feishuMaxInboundFileBytes: Math.max(1, cfg.feishu.bot.file.maxInboundFileMb) * 1024 * 1024,
    feishuMediaSendSupport: {
      resolveSendTarget: async (sessionId: number | null) => {
        if (sessionId == null) return null;
        const channel = await d.feishuMessageService.findSessionChannel(sessionId);
        if (channel == null) return null;
        const target = feishuSendTargetOf(channel.appId, channel.chatId);
        const replyMessageId = await d.feishuMessageService.findLatestInboundMessageId(sessionId, channel);
        if (replyMessageId != null) target.replyMessageId = replyMessageId;
        return target;
      },
      sendImage: async (target: any, image: any, sessionId: number | null) => {
        const client = await d.getFeishuClient(Number(target.appId));
        if (client == null) throw new Error(`飞书Bot不存在或未启用: ${target.appId}`);
        const messageId = await sendFeishuImage(client, target, image);
        if (messageId != null && sessionId != null && target.receiveIdType !== 'chat_id') {
          await d.feishuMessageService.recordP2pMessage(target.appId, messageId, sessionId, 'OUT');
        }
      },
      sendFile: async (target: any, fileName: string, file: any, sessionId: number | null) => {
        const client = await d.getFeishuClient(Number(target.appId));
        if (client == null) throw new Error(`飞书Bot不存在或未启用: ${target.appId}`);
        const messageId = await sendFeishuFile(client, target, fileName, file);
        if (messageId != null && sessionId != null && target.receiveIdType !== 'chat_id') {
          await d.feishuMessageService.recordP2pMessage(target.appId, messageId, sessionId, 'OUT');
        }
      },
    },
    sendBytesSupport: d.sendBytesBridge,
    dingtalkMediaSendSupport: {
      resolveSendTarget: (sessionId: number | null) => d.dingtalkMediaHolder.current?.resolveSendTarget(sessionId) ?? Promise.resolve(null),
      sendImage: async (target: any, bytes: any, fileName: string) => {
        if (d.dingtalkMediaHolder.current == null) throw new Error('钉钉通道未就绪');
        return d.dingtalkMediaHolder.current.sendImage(target, bytes, fileName);
      },
      sendFile: async (target: any, fileName: string, bytes: any) => {
        if (d.dingtalkMediaHolder.current == null) throw new Error('钉钉通道未就绪');
        return d.dingtalkMediaHolder.current.sendFile(target, fileName, bytes);
      },
    },
    definitionRegistry: d.definitionRegistry,
    get harnessService() { return d.holder.harness!; },
    get agentLoop() { return d.holder.loop!; },
    subagentExecutionMapper: d.subagentMapper,
    localToolSessionRegistry: d.localToolSessions,
    localToolExecutor: d.localToolExecutor,
    visibilityService: d.visibility,
    backgroundSubagentManager: d.backgroundSubagentManager,
    messageMapper: d.messageRepo as never,
    sessionCompactionService: d.compactionSvc,
    embedPageToolRegistry: d.embedPageToolRegistry,
    mcpToolsRegistry: d.mcpToolsRegistry,
    mcpClientManager: d.routingMcpClient,
    httpToolService: d.httpToolService,
    httpApiExecutor: d.httpApiExecutor,
    agentMapper: d.agentRepo as never,
  });

  const toolDispatcher = new ToolDispatcher(
    toolRegistry, d.localToolExecutor, d.dangerAssessor, d.sessionMap,
    d.wsRegistry, d.askUserQuestionsRegistry, d.localToolSessions, d.treeSignalPublisher,
    d.backgroundTasks, d.deliveryService, d.feishuAskMount,
    d.proxyApprover, d.jevRiskAssessor, d.approvalModelResolver,
    d.inboxService,
    d.approvalRuleService,
    d.dockerRouter,
    createMcpReadOnlyLookup({
      sessionMapper: d.sessionMap,
      loadModeResolver: d.mcpLoadModeResolver,
      toolsRegistry: d.mcpToolsRegistry,
    }),
  );
  const compactionSignalBus = new CompactionSignalBus();
  const agentLoop = new AgentLoop(
    d.llmAdapter, d.promptEngine, d.contextManager, toolDispatcher, d.backgroundTasks,
    d.shellManager, d.activityHeartbeat, d.sessionSvc, d.orchestrator, d.activeContext, d.routingMcpClient,
    () => d.backgroundSubagentManager,
    compactionSignalBus,
    d.inFlightToolRegistry,
  );
  d.holder.loop = agentLoop;
  d.holder.compactionSignalBus = compactionSignalBus;
  const harness = new HarnessService(
    agentLoop, toolRegistry, d.skillLoader, d.skillSync, d.localSkills, d.localAgentsMd,
    d.sessionMap, d.agentRepo as never, d.experienceService, d.modelRepo as never, d.fileChangeRepo as never,
    d.sessionSvc, d.compactionSvc, d.historyLoader, d.orchestrator,
    d.promptEngine, d.activeContext, d.compactionConfig, d.envInfo, d.db, d.routingMcpClient, d.mcpSync,
    {
      isEmbedSession: (sid: number) => d.embedPageToolRegistry.isEmbedSession(sid),
      isEmbedPageSession: (sid: number) => d.embedPageToolRegistry.isEmbedPageSession(sid),
    },
    d.memoryService,
    d.mcpLoadModeResolver,
    d.httpToolService,
    d.experienceMountService ?? null,
    cfg.app.harness.runtimeIdentityMode,
  );
  d.holder.harness = harness;

  return { toolRegistry, toolDispatcher, compactionSignalBus, agentLoop, harness };
}