/** * voko-server 的 composition root:读取 env、创建连接、装配服务,并统一管理资源生命周期。 */ import { fileURLToPath } from 'node:url' import { config as loadDotenv } from 'dotenv' import { createAuth, createUserRepo } from '@drama/auth' import { createBillingRepo, createLedger } from '@drama/billing' import { createDb } from '@drama/db' import { createLLM, createStaticKeyProvider } from '@drama/llm' import { createLogger } from '@drama/observability' import { createAccessRequests, createAgentAccess, createOwnerSettings, nextAllowedSendTime } from '@voko/access' import { createMemoryStore, createRateLimiter } from '@drama/ratelimit' import { createEscrow, createTips } from '@voko/clearing' import { createAgents, createEvents, createGuilds, createHelp, createJobs, createJudge, createTasks } from '@voko/exchange' import { readEnv } from '../env.js' import { createEmailPorts } from './email.js' import { createGoogleOauthPort } from './google-oauth.js' import { createDevOauthPort, createHmacTokenSigner, createScryptHasher } from './security.js' /** * @param {{ service: string }} opts */ export async function createRuntime({ service }) { // 1. 加载 .env 并解析配置。 loadDotenv({ path: fileURLToPath(new URL('../../.env', import.meta.url)) }) const env = readEnv(process.env) const logger = createLogger({ service, level: env.logLevel, format: env.nodeEnv === 'production' ? 'json' : 'pretty', }) // 2. 连接与基础端口。 const db = createDb({ url: env.databaseUrl }) const { authEmail, accessNotifier, ownerNotifier } = createEmailPorts({ env, db, logger }) const tokenSigner = createHmacTokenSigner({ secret: env.authTokenSecret }) const hasher = createScryptHasher() const oauth = env.oauth.googleClientId === null ? createDevOauthPort() : createGoogleOauthPort({ clientId: env.oauth.googleClientId, clientSecret: env.oauth.googleClientSecret }) // 3. 领域服务。 const userRepo = createUserRepo(db) const auth = createAuth({ userRepo, email: authEmail, oauth, tokenSigner, hasher }) const access = createAccessRequests({ db, notifier: accessNotifier }) const agents = createAgents() const guilds = createGuilds({ db }) const escrow = createEscrow({ db }) // 大模型最小装配:一个静态 key 直连供应商,不接成本、审计与扣费端口(Alpha 只有平台裁判两处调用)。 const llm = createLLM({ provider: env.llm.provider, model: env.llm.model, keyProvider: createStaticKeyProvider({ api_key: env.llm.apiKey, base_url: env.llm.baseUrl }), }) const judge = createJudge({ chat: (request) => llm.chat(request) }) const tasks = createTasks({ db, agents, escrow, judge }) const events = createEvents({ db }) const ownerSettings = createOwnerSettings({ db }) const help = createHelp({ db, mailer: ownerNotifier, // 求助邮件:主人关了就不发;在安静时段内推迟到结束时刻。 async sendAfter(ctx, { user_id, now }) { const settings = await ownerSettings.get(ctx, { user_id }) return settings.email_help ? nextAllowedSendTime(now, settings) : null }, }) const tips = createTips({ db }) const jobs = createJobs({ db, tasks, help, logger }) /** * 给已结算任务的赢家打赏;收款事件与动账同事务。 * @param {object} ctx * @param {{ agent: import('@voko/exchange').Agent, task_id: string, amount: number, client_request_key: string }} input */ async function tipTask(ctx, { agent, task_id, amount, client_request_key }) { const seen = await tasks.get(ctx, { agent, task_id }) if (!seen.ok) return seen return tips.send(ctx, { task_id, from_agent_id: agent.id, from_wallet: agent.wallet_user_id, amount, client_request_key, async onSent(trx, { to_wallet }) { const recipient = await agents.getByWallet(ctx, trx, { wallet_user_id: to_wallet }) await events.emit(ctx, trx, { guild_id: seen.task.guild_id, task_id, type: 'tip.received', target_agent_id: recipient.id }) }, }) } // 公开 API 按 Agent 限流(接入说明:读 120/分、写 40/分、发任务 30/小时)。单进程部署用内存 store,多实例时换 MySQL 或 Redis store。 const rateLimiter = createRateLimiter({ store: createMemoryStore(), rules: [ { endpoint: 'read', by: 'anon', capacity: 120, refillPerSec: 120 / 60 }, { endpoint: 'write', by: 'anon', capacity: 40, refillPerSec: 40 / 60 }, { endpoint: 'publish', by: 'anon', capacity: 30, refillPerSec: 30 / 3600 }, ], }) const agentAccess = createAgentAccess({ db, agents, public_url: env.publicUrl, notifier: ownerNotifier, // 启动积分 100:在认领事务里记账,幂等键由 agent_id 唯一决定。 async grantStarter(_ctx, trx, { agent_id, wallet_user_id }) { await createLedger(createBillingRepo(db, trx)).grant({ user_id: wallet_user_id, amount: 100, source: 'voko', credit_kind: 'admin', reason: 'vk_starter', ref_type: 'vk_agent', ref_id: agent_id, idempotency_key: `vk:starter:${agent_id}`, }) }, }) /** * 注册:验证码校验与建用户在同一事务里完成。 * @param {{ email: string, password: string, verification_code: string, locale: string }} input */ async function register(input) { return db.transaction(async (trx) => { const trxAuth = createAuth({ userRepo: createUserRepo(trx), email: authEmail, oauth, tokenSigner, hasher }) await trxAuth.verifyCode({ email: input.email, code: input.verification_code, purpose: 'register' }) return trxAuth.register({ email: input.email, password: input.password, locale: input.locale, email_verified_at: new Date() }) }) } return { env, logger, services: { db, auth, userRepo, access, agents, guilds, agentAccess, escrow, tasks, events, jobs, help, ownerSettings, tipTask, rateLimiter, register, oauthEnabled: env.oauth.googleClientId !== null }, async close() { await db.destroy() }, } }