owner.js
3.89 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
import * as v from 'valibot'
import { codedError, requireUser } from './auth.js'
import { agentView, IdempotencyKey, PublishBody, REFUSAL_STATUS, taskView } from './v1.js'
const AgentParams = v.object({ id: v.pipe(v.string(), v.minLength(1), v.maxLength(64)) })
const LimitBody = v.object({ daily_spend_limit: v.nullable(v.pipe(v.number(), v.safeInteger(), v.minValue(0))) })
const StatusBody = v.object({ status: v.picklist(['active', 'suspended']) })
/**
* 主人在网页上管理自己的 Agent,并以其中一个 Agent 的身份看公会看板、发任务(扣该 Agent 的钱包)。
* 与公开 API 调用同一套 service。
* @param {import('fastify').FastifyInstance} app
* @param {Record<string, any>} services
*/
export function registerOwnerRoutes(app, services) {
const { agents, agentAccess, guilds, escrow, tasks, db } = services
/**
* 取当前用户名下的一个 Agent;不是自己的按不存在处理。
* @param {import('fastify').FastifyRequest} request
*/
async function ownAgent(request) {
const user = await requireUser(request, services)
const params = v.parse(AgentParams, request.params)
const mine = await agents.listByOwner(request.ctx, db, { owner_user_id: user.id })
const agent = mine.find((item) => item.id === params.id)
if (agent === undefined) throw codedError(`Agent not found: ${params.id}`, 'NOT_FOUND')
return { user, agent }
}
app.get('/api/voko/guilds', async (request) => {
await requireUser(request, services)
return { guilds: await guilds.list(request.ctx) }
})
app.get('/api/voko/agents', async (request) => {
const user = await requireUser(request, services)
const mine = await agents.listByOwner(request.ctx, db, { owner_user_id: user.id })
return {
agents: await Promise.all(mine.map(async (agent) => ({
...agentView(agent),
daily_spend_limit: agent.daily_spend_limit,
wallet: await escrow.walletOf(request.ctx, { wallet_user_id: agent.wallet_user_id }),
}))),
}
})
app.post('/api/voko/agents/:id/api-key/reset', async (request) => {
const { agent } = await ownAgent(request)
return agentAccess.resetApiKey(request.ctx, { agent_id: agent.id })
})
app.post('/api/voko/agents/:id/status', async (request) => {
const { user, agent } = await ownAgent(request)
const body = v.parse(StatusBody, request.body)
const changed = await agents.setStatus(request.ctx, db, { agent_id: agent.id, owner_user_id: user.id, to: body.status })
if (!changed) throw codedError(`Agent is already ${body.status}: ${agent.id}`, 'CONFLICT')
return { ok: true }
})
app.post('/api/voko/agents/:id/daily-spend-limit', async (request) => {
const { user, agent } = await ownAgent(request)
const body = v.parse(LimitBody, request.body)
await agents.setDailySpendLimit(request.ctx, db, { agent_id: agent.id, owner_user_id: user.id, daily_spend_limit: body.daily_spend_limit })
return { ok: true }
})
app.get('/api/voko/agents/:id/tasks', async (request) => {
const { agent } = await ownAgent(request)
const page = await tasks.list(request.ctx, { agent, limit: 100 })
return { tasks: page.tasks.map(taskView) }
})
app.post('/api/voko/agents/:id/tasks', async (request, reply) => {
const { agent } = await ownAgent(request)
if (agent.status !== 'active') throw codedError(`Agent is ${agent.status}: ${agent.id}`, 'CONFLICT')
const body = v.parse(PublishBody, request.body)
const result = await tasks.publish(request.ctx, { agent, title: body.title, spec: body.spec, template: body.template, reward: body.reward, deadline_at: body.deadline, acceptance: body.acceptance, client_request_key: v.parse(IdempotencyKey, request.headers['idempotency-key']) })
if (!result.ok) return reply.code(REFUSAL_STATUS[result.code]).send({ error: { code: result.code, message: result.hint, retryable: false } })
return { task: taskView(result.task) }
})
}