// 服务容器:组装数据库、各个 service 与引擎 import fs from 'node:fs' import path from 'node:path' import type { ProviderId } from '@agent-server/shared' import { communitySkillsDir, config, customSkillsDir, dbPath, teamCasesDir } from './config' import { createDb, migrateDb, type DB } from './db' import { ClaudeProvider } from './providers/claude/adapter' import { CodexProvider } from './providers/codex/adapter' import type { AgentProvider } from './providers/types' import { AdminAuthService } from './services/admin-auth' import { ApiKeyService, AuditService, KnowledgeService, ProfileService } from './services/catalog' import { EventStore } from './services/event-store' import { FileService } from './services/files' import { InteractionService } from './services/interactions' import { PluginService } from './services/plugins' import { ClaudeLoginManager } from './services/claude-login' import { CodexSkillService } from './services/codex-skills' import { CommunitySkillService } from './services/community-skills' import { CodexPluginService } from './services/codex-plugins' import { RunTokenRegistry } from './mcp-gateway/tokens' import { RunManager } from './services/runs' import { SessionService } from './services/sessions' import { SettingsService } from './services/settings' import { UsageService } from './services/usage' import { PdfRenderer } from './api-reference/pdf' import { McpUpstream } from './mcp-gateway/upstream' import { McpServerService } from './services/mcp-servers' import { CustomSkillService } from './custom-skills/service' import { SkillSourceService } from './custom-skills/sources' import { FileManagerService } from './services/file-manager' import { BrowserTokenService } from './services/browser-tokens' import { BotKeyService, BotService } from './teams/bots' import { BackgroundCallService } from './mcp-gateway/background' import { ScheduleService } from './schedules/service' import { TeamCaseService } from './teams/cases' import { ensureLeadProfiles } from './teams/lead-profile' import { CardService } from './teams/cards' import { TeamService } from './teams/service' import { createSkillCatalog } from './services/skill-catalog' import { forgetGatewaySession } from './mcp-gateway/server' export interface AppLogger { info(o: object, m?: string): void warn(o: object, m?: string): void error(o: object, m?: string): void child(b: object): AppLogger } export function createApp(log: AppLogger, opts: { db?: DB; providers?: Partial> } = {}) { const db = opts.db ?? createDb(dbPath()) migrateDb(db) const settings = new SettingsService(db) const events = new EventStore(db) const files = new FileService(db) const profiles = new ProfileService(db, settings) const knowledge = new KnowledgeService(db, settings) const apiKeys = new ApiKeyService(db, profiles) const audit = new AuditService(db) const usage = new UsageService(db) const browserTokens = new BrowserTokenService(db) const apiPdf = new PdfRenderer() const mcpUpstream = new McpUpstream() const mcpServers = new McpServerService(db, mcpUpstream, () => settings.get(), summary => audit.log({ actor: 'admin', kind: 'admin', summary })) const adminAudit = (summary: string, detail?: unknown) => audit.log({ actor: 'admin', kind: 'admin', summary, detail }) migrateManagedSkillDirs(log) const customSkills = new CustomSkillService(db, adminAudit) // 按数据库重建生效目录(缺失或被改动的技能目录重新发布) customSkills.reconcile(msg => log.warn({}, msg)) const skillSources = new SkillSourceService(db, customSkills, () => settings.get(), adminAudit) const fileManager = new FileManagerService(db, () => settings.get(), () => knowledge.list(), files, adminAudit) const interactions = new InteractionService(db, events, settings) const sessions = new SessionService(db, settings, profiles, apiKeys, files, events) const adminAuth = new AdminAuthService(db, log, config.adminPassword) const plugins = new PluginService(settings, audit) const codexSkills = new CodexSkillService(summary => audit.log({ actor: 'admin', kind: 'admin', summary })) const communitySkills = new CommunitySkillService(summary => audit.log({ actor: 'admin', kind: 'admin', summary })) const gatewayTokens = new RunTokenRegistry() const claudeLogin = new ClaudeLoginManager(() => settings.get().providers.claude.cliPath, summary => audit.log({ actor: 'admin', kind: 'admin', summary })) const providers: Partial> = opts.providers ?? { claude: new ClaudeProvider(), codex: new CodexProvider(log) } const codexPlugins = new CodexPluginService( () => (providers.codex instanceof CodexProvider ? providers.codex : null), () => settings.get(), summary => audit.log({ actor: 'admin', kind: 'admin', summary }), ) sessions.extraModels = p => providers[p]?.knownModels?.() ?? [] const runs = new RunManager({ db, log, settings, sessions, profiles, knowledge, files, events, interactions, audit, usage, plugins, communitySkills, mcpServers, customSkills, gatewayTokens, providers }) const bots = new BotService(db, profiles) ensureLeadProfiles({ profiles, customSkills, bots, settings }) const botKeys = new BotKeyService(db, bots) usage.extraSourceNames = () => botKeys.names() const skillCatalog = createSkillCatalog({ settings: () => settings.get(), plugins, communitySkills, customSkills, providers }) const teamCards = new CardService({ bots, profiles, knowledge, mcpServers, skillCatalog }) const teams = new TeamService({ db, log, settings, profiles, sessions, runs, files, events, interactions, bots, botKeys, cards: teamCards, forgetSession: id => { gatewayTokens.forgetSession(id) forgetGatewaySession(id) }, }) const teamCases = new TeamCaseService({ db, teams, dir: teamCasesDir() }) const backgroundCalls = new BackgroundCallService({ db, log, settings, runs, events, teams }) const schedules = new ScheduleService({ db, log, settings, sessions, runs, events, teams, bots, profiles }) return { db, log, settings, events, files, profiles, knowledge, apiKeys, audit, interactions, sessions, usage, browserTokens, apiPdf, mcpUpstream, mcpServers, customSkills, skillSources, fileManager, adminAuth, plugins, claudeLogin, codexSkills, communitySkills, codexPlugins, gatewayTokens, providers, runs, bots, botKeys, teams, teamCards, teamCases, schedules, backgroundCalls, skillCatalog } } export type App = ReturnType /** * 技能生效目录从 data/ 下移到顶层 managed-skills/ 后,一次性搬走旧目录。 * systemd 的 ReadWritePaths 把每个目录单独挂载,跨目录改名会报 EXDEV,此时改为复制后删除 */ function migrateManagedSkillDirs(log: AppLogger) { const moves: [string, string][] = [ [path.join(config.dataDir, 'data', 'community-skills'), communitySkillsDir()], [path.join(config.dataDir, 'data', 'custom-skills'), customSkillsDir()], ] for (const [from, to] of moves) { if (!fs.existsSync(from)) continue if (fs.existsSync(to)) { log.warn({ from, to }, '旧的技能目录仍然存在,新目录已有内容,保留旧目录不处理') continue } fs.mkdirSync(path.dirname(to), { recursive: true }) try { fs.renameSync(from, to) } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'EXDEV') throw e fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true }) fs.rmSync(from, { recursive: true, force: true }) } log.info({ from, to }, '技能目录已迁移') } }