| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- // 服务容器:组装数据库、各个 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<Record<ProviderId, AgentProvider>> } = {}) {
- 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<Record<ProviderId, AgentProvider>> = 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<typeof createApp>
- /**
- * 技能生效目录从 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 }, '技能目录已迁移')
- }
- }
|