runtime.js
6.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
* 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()
},
}
}