app.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. // 服务容器:组装数据库、各个 service 与引擎
  2. import fs from 'node:fs'
  3. import path from 'node:path'
  4. import type { ProviderId } from '@agent-server/shared'
  5. import { communitySkillsDir, config, customSkillsDir, dbPath, teamCasesDir } from './config'
  6. import { createDb, migrateDb, type DB } from './db'
  7. import { ClaudeProvider } from './providers/claude/adapter'
  8. import { CodexProvider } from './providers/codex/adapter'
  9. import type { AgentProvider } from './providers/types'
  10. import { AdminAuthService } from './services/admin-auth'
  11. import { ApiKeyService, AuditService, KnowledgeService, ProfileService } from './services/catalog'
  12. import { EventStore } from './services/event-store'
  13. import { FileService } from './services/files'
  14. import { InteractionService } from './services/interactions'
  15. import { PluginService } from './services/plugins'
  16. import { ClaudeLoginManager } from './services/claude-login'
  17. import { CodexSkillService } from './services/codex-skills'
  18. import { CommunitySkillService } from './services/community-skills'
  19. import { CodexPluginService } from './services/codex-plugins'
  20. import { RunTokenRegistry } from './mcp-gateway/tokens'
  21. import { RunManager } from './services/runs'
  22. import { SessionService } from './services/sessions'
  23. import { SettingsService } from './services/settings'
  24. import { UsageService } from './services/usage'
  25. import { PdfRenderer } from './api-reference/pdf'
  26. import { McpUpstream } from './mcp-gateway/upstream'
  27. import { McpServerService } from './services/mcp-servers'
  28. import { CustomSkillService } from './custom-skills/service'
  29. import { SkillSourceService } from './custom-skills/sources'
  30. import { FileManagerService } from './services/file-manager'
  31. import { BrowserTokenService } from './services/browser-tokens'
  32. import { BotKeyService, BotService } from './teams/bots'
  33. import { BackgroundCallService } from './mcp-gateway/background'
  34. import { ScheduleService } from './schedules/service'
  35. import { TeamCaseService } from './teams/cases'
  36. import { ensureLeadProfiles } from './teams/lead-profile'
  37. import { CardService } from './teams/cards'
  38. import { TeamService } from './teams/service'
  39. import { createSkillCatalog } from './services/skill-catalog'
  40. import { forgetGatewaySession } from './mcp-gateway/server'
  41. export interface AppLogger {
  42. info(o: object, m?: string): void
  43. warn(o: object, m?: string): void
  44. error(o: object, m?: string): void
  45. child(b: object): AppLogger
  46. }
  47. export function createApp(log: AppLogger, opts: { db?: DB; providers?: Partial<Record<ProviderId, AgentProvider>> } = {}) {
  48. const db = opts.db ?? createDb(dbPath())
  49. migrateDb(db)
  50. const settings = new SettingsService(db)
  51. const events = new EventStore(db)
  52. const files = new FileService(db)
  53. const profiles = new ProfileService(db, settings)
  54. const knowledge = new KnowledgeService(db, settings)
  55. const apiKeys = new ApiKeyService(db, profiles)
  56. const audit = new AuditService(db)
  57. const usage = new UsageService(db)
  58. const browserTokens = new BrowserTokenService(db)
  59. const apiPdf = new PdfRenderer()
  60. const mcpUpstream = new McpUpstream()
  61. const mcpServers = new McpServerService(db, mcpUpstream, () => settings.get(), summary => audit.log({ actor: 'admin', kind: 'admin', summary }))
  62. const adminAudit = (summary: string, detail?: unknown) => audit.log({ actor: 'admin', kind: 'admin', summary, detail })
  63. migrateManagedSkillDirs(log)
  64. const customSkills = new CustomSkillService(db, adminAudit)
  65. // 按数据库重建生效目录(缺失或被改动的技能目录重新发布)
  66. customSkills.reconcile(msg => log.warn({}, msg))
  67. const skillSources = new SkillSourceService(db, customSkills, () => settings.get(), adminAudit)
  68. const fileManager = new FileManagerService(db, () => settings.get(), () => knowledge.list(), files, adminAudit)
  69. const interactions = new InteractionService(db, events, settings)
  70. const sessions = new SessionService(db, settings, profiles, apiKeys, files, events)
  71. const adminAuth = new AdminAuthService(db, log, config.adminPassword)
  72. const plugins = new PluginService(settings, audit)
  73. const codexSkills = new CodexSkillService(summary => audit.log({ actor: 'admin', kind: 'admin', summary }))
  74. const communitySkills = new CommunitySkillService(summary => audit.log({ actor: 'admin', kind: 'admin', summary }))
  75. const gatewayTokens = new RunTokenRegistry()
  76. const claudeLogin = new ClaudeLoginManager(() => settings.get().providers.claude.cliPath, summary => audit.log({ actor: 'admin', kind: 'admin', summary }))
  77. const providers: Partial<Record<ProviderId, AgentProvider>> = opts.providers ?? { claude: new ClaudeProvider(), codex: new CodexProvider(log) }
  78. const codexPlugins = new CodexPluginService(
  79. () => (providers.codex instanceof CodexProvider ? providers.codex : null), () => settings.get(),
  80. summary => audit.log({ actor: 'admin', kind: 'admin', summary }),
  81. )
  82. sessions.extraModels = p => providers[p]?.knownModels?.() ?? []
  83. const runs = new RunManager({ db, log, settings, sessions, profiles, knowledge, files, events, interactions, audit, usage, plugins, communitySkills, mcpServers, customSkills, gatewayTokens, providers })
  84. const bots = new BotService(db, profiles)
  85. ensureLeadProfiles({ profiles, customSkills, bots, settings })
  86. const botKeys = new BotKeyService(db, bots)
  87. usage.extraSourceNames = () => botKeys.names()
  88. const skillCatalog = createSkillCatalog({ settings: () => settings.get(), plugins, communitySkills, customSkills, providers })
  89. const teamCards = new CardService({ bots, profiles, knowledge, mcpServers, skillCatalog })
  90. const teams = new TeamService({
  91. db, log, settings, profiles, sessions, runs, files, events, interactions, bots, botKeys, cards: teamCards,
  92. forgetSession: id => {
  93. gatewayTokens.forgetSession(id)
  94. forgetGatewaySession(id)
  95. },
  96. })
  97. const teamCases = new TeamCaseService({ db, teams, dir: teamCasesDir() })
  98. const backgroundCalls = new BackgroundCallService({ db, log, settings, runs, events, teams })
  99. const schedules = new ScheduleService({ db, log, settings, sessions, runs, events, teams, bots, profiles })
  100. 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 }
  101. }
  102. export type App = ReturnType<typeof createApp>
  103. /**
  104. * 技能生效目录从 data/ 下移到顶层 managed-skills/ 后,一次性搬走旧目录。
  105. * systemd 的 ReadWritePaths 把每个目录单独挂载,跨目录改名会报 EXDEV,此时改为复制后删除
  106. */
  107. function migrateManagedSkillDirs(log: AppLogger) {
  108. const moves: [string, string][] = [
  109. [path.join(config.dataDir, 'data', 'community-skills'), communitySkillsDir()],
  110. [path.join(config.dataDir, 'data', 'custom-skills'), customSkillsDir()],
  111. ]
  112. for (const [from, to] of moves) {
  113. if (!fs.existsSync(from)) continue
  114. if (fs.existsSync(to)) {
  115. log.warn({ from, to }, '旧的技能目录仍然存在,新目录已有内容,保留旧目录不处理')
  116. continue
  117. }
  118. fs.mkdirSync(path.dirname(to), { recursive: true })
  119. try {
  120. fs.renameSync(from, to)
  121. } catch (e) {
  122. if ((e as NodeJS.ErrnoException).code !== 'EXDEV') throw e
  123. fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true })
  124. fs.rmSync(from, { recursive: true, force: true })
  125. }
  126. log.info({ from, to }, '技能目录已迁移')
  127. }
  128. }