app.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  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 } 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 { TeamService } from './teams/service'
  34. import { forgetGatewaySession } from './mcp-gateway/server'
  35. export interface AppLogger {
  36. info(o: object, m?: string): void
  37. warn(o: object, m?: string): void
  38. error(o: object, m?: string): void
  39. child(b: object): AppLogger
  40. }
  41. export function createApp(log: AppLogger, opts: { db?: DB; providers?: Partial<Record<ProviderId, AgentProvider>> } = {}) {
  42. const db = opts.db ?? createDb(dbPath())
  43. migrateDb(db)
  44. const settings = new SettingsService(db)
  45. const events = new EventStore(db)
  46. const files = new FileService(db)
  47. const profiles = new ProfileService(db, settings)
  48. const knowledge = new KnowledgeService(db, settings)
  49. const apiKeys = new ApiKeyService(db, profiles)
  50. const audit = new AuditService(db)
  51. const usage = new UsageService(db)
  52. const browserTokens = new BrowserTokenService(db)
  53. const apiPdf = new PdfRenderer()
  54. const mcpUpstream = new McpUpstream()
  55. const mcpServers = new McpServerService(db, mcpUpstream, () => settings.get(), summary => audit.log({ actor: 'admin', kind: 'admin', summary }))
  56. const adminAudit = (summary: string, detail?: unknown) => audit.log({ actor: 'admin', kind: 'admin', summary, detail })
  57. migrateManagedSkillDirs(log)
  58. const customSkills = new CustomSkillService(db, adminAudit)
  59. // 按数据库重建生效目录(缺失或被改动的技能目录重新发布)
  60. customSkills.reconcile(msg => log.warn({}, msg))
  61. const skillSources = new SkillSourceService(db, customSkills, () => settings.get(), adminAudit)
  62. const fileManager = new FileManagerService(db, () => settings.get(), () => knowledge.list(), files, adminAudit)
  63. const interactions = new InteractionService(db, events, settings)
  64. const sessions = new SessionService(db, settings, profiles, apiKeys, files, events)
  65. const adminAuth = new AdminAuthService(db, log, config.adminPassword)
  66. const plugins = new PluginService(settings, audit)
  67. const codexSkills = new CodexSkillService(summary => audit.log({ actor: 'admin', kind: 'admin', summary }))
  68. const communitySkills = new CommunitySkillService(summary => audit.log({ actor: 'admin', kind: 'admin', summary }))
  69. const gatewayTokens = new RunTokenRegistry()
  70. const claudeLogin = new ClaudeLoginManager(() => settings.get().providers.claude.cliPath, summary => audit.log({ actor: 'admin', kind: 'admin', summary }))
  71. const providers: Partial<Record<ProviderId, AgentProvider>> = opts.providers ?? { claude: new ClaudeProvider(), codex: new CodexProvider(log) }
  72. const codexPlugins = new CodexPluginService(
  73. () => (providers.codex instanceof CodexProvider ? providers.codex : null), () => settings.get(),
  74. summary => audit.log({ actor: 'admin', kind: 'admin', summary }),
  75. )
  76. sessions.extraModels = p => providers[p]?.knownModels?.() ?? []
  77. const runs = new RunManager({ db, log, settings, sessions, profiles, knowledge, files, events, interactions, audit, usage, plugins, communitySkills, mcpServers, customSkills, gatewayTokens, providers })
  78. const bots = new BotService(db, profiles)
  79. const botKeys = new BotKeyService(db, bots)
  80. usage.extraSourceNames = () => botKeys.names()
  81. const teams = new TeamService({
  82. db, log, settings, profiles, sessions, runs, files, events, interactions, bots, botKeys,
  83. forgetSession: id => {
  84. gatewayTokens.forgetSession(id)
  85. forgetGatewaySession(id)
  86. },
  87. })
  88. 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 }
  89. }
  90. export type App = ReturnType<typeof createApp>
  91. /**
  92. * 技能生效目录从 data/ 下移到顶层 managed-skills/ 后,一次性搬走旧目录。
  93. * systemd 的 ReadWritePaths 把每个目录单独挂载,跨目录改名会报 EXDEV,此时改为复制后删除
  94. */
  95. function migrateManagedSkillDirs(log: AppLogger) {
  96. const moves: [string, string][] = [
  97. [path.join(config.dataDir, 'data', 'community-skills'), communitySkillsDir()],
  98. [path.join(config.dataDir, 'data', 'custom-skills'), customSkillsDir()],
  99. ]
  100. for (const [from, to] of moves) {
  101. if (!fs.existsSync(from)) continue
  102. if (fs.existsSync(to)) {
  103. log.warn({ from, to }, '旧的技能目录仍然存在,新目录已有内容,保留旧目录不处理')
  104. continue
  105. }
  106. fs.mkdirSync(path.dirname(to), { recursive: true })
  107. try {
  108. fs.renameSync(from, to)
  109. } catch (e) {
  110. if ((e as NodeJS.ErrnoException).code !== 'EXDEV') throw e
  111. fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true })
  112. fs.rmSync(from, { recursive: true, force: true })
  113. }
  114. log.info({ from, to }, '技能目录已迁移')
  115. }
  116. }