backend-ts/src/app-harness-engine.ts
Rama main · réplica de lectura del repositorio original.
/* 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 };
}