company-agent.mjs
5.4 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
#!/usr/bin/env node
/**
* 公司 Agent 参考实现(tools/voko-company-agent)的真实链路验收:
* 起一个真实的 `agent.mjs --once` 进程,只凭公开 API 完成「验收一份提交」和「按简报拟任务并发布到待审核」。
* 真实调用大模型(复用 apps/voko-server/.env 的 LLM_*)。
*/
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { config as loadDotenv } from 'dotenv'
import { runScenario } from './_lib.mjs'
loadDotenv({ path: fileURLToPath(new URL('../../../.env', import.meta.url)) })
const agentDir = fileURLToPath(new URL('../../../../../tools/voko-company-agent/', import.meta.url))
await runScenario('ca', async ({ db, check, call, admin, createOwner, createGuild, createAgent, publish, wallet, claim }) => {
const alice = await createOwner('alice')
const bob = await createOwner('bob')
const guild = await createGuild('main', alice.user_id, { require_lead_approval: true })
const company = await createAgent('company', alice, guild, 'company')
const worker = await createAgent('worker', bob, guild)
const adminToken = await admin()
await call('POST', `/api/voko/admin/guilds/${guild}/budget`, { token: adminToken, idempotency_key: `ca-${company.id}`, body: { amount: 400, note: 'company agent scenario budget' } })
await call('PUT', `/api/voko/admin/guilds/${guild}/weekly-goal`, { token: adminToken, body: { weekly_goal: 1000 } })
// 一份明显合格的提交在等公司 Agent 验收。
const acceptance = [{ id: 'c1', text: 'The summary says the article is finished.' }, { id: 'c2', text: 'The submission includes a link to the article.' }]
const task = (await publish(company, { title: 'Write a short how-to article', spec: 'Write a 300-word how-to article about resetting a password.', reward: 40, acceptance })).json.data
await call('POST', `/api/voko/approvals/${task.id}`, { token: alice.token, body: { decision: 'approve' } })
await claim(worker, task.id)
// 交付物的链接说明要与任务相符:公共库默认的说明是「translation」,真实模型会据此(正确地)判第二条不通过。
const delivered = await call('POST', `/v1/tasks/${task.id}/submit`, { token: worker.key, body: { summary: 'The article is finished. It is a 300-word how-to about resetting a password; the link to the article is attached.', artifacts: [{ type: 'link', url: 'https://example.com/articles/reset-password', note: 'The finished how-to article' }] } })
check('员工 Agent 提交', delivered.status === 200, delivered)
const ran = spawnSync('node', ['agent.mjs', '--once'], {
cwd: agentDir,
encoding: 'utf8',
timeout: 240_000,
env: {
PATH: process.env.PATH,
VOKO_API_BASE: `${(process.env.VOKO_API || 'http://localhost:9201').replace(/\/$/, '')}/v1`,
VOKO_API_KEY: company.key,
LLM_BASE_URL: process.env.LLM_BASE_URL,
LLM_API_KEY: process.env.LLM_API_KEY,
LLM_MODEL: process.env.LLM_MODEL,
COMPANY_BRIEF_FILE: `${agentDir}brief.example.md`,
MAX_OPEN_TASKS: '2',
MAX_REWARD: '60',
},
})
const lines = ran.stdout.trim().split('\n').filter((line) => line !== '').map((line) => JSON.parse(line))
check('公司 Agent 进程正常结束一次心跳', ran.status === 0, { status: ran.status, stderr: ran.stderr.slice(-800), stdout: ran.stdout.slice(-800) })
// 验收:逐条结果覆盖清单,动作与结果一致,任务随之结算或返工。
const reviewed = (await call('GET', `/v1/tasks/${task.id}`, { token: company.key })).json.data
const review = await db('vk_reviews').where({ task_id: task.id, reviewer_kind: 'publisher' }).first()
const checks = review === undefined ? [] : await db('vk_review_checks').where({ review_id: review.id })
const allPass = checks.every((item) => item.pass === 1)
check('公司 Agent 按清单逐条验收了提交', review !== undefined && checks.length === 2 && ((allPass && review.action === 'accept' && reviewed.status === 'settled') || (!allPass && review.action === 'reject' && reviewed.status === 'rework')), { review, checks, status: reviewed.status, log: lines })
check('这份合格的提交被通过并结算给员工 Agent', reviewed.status === 'settled' && (await wallet(worker)).available === 140, { status: reviewed.status, checks, wallet: await wallet(worker) })
// 发布:按简报拟出的任务进入待审核,托管已冻结,不超过设定的条数与单笔上限。
const drafted = (await db('vk_tasks').where({ publisher_agent_id: company.id, status: 'pending_approval' })).filter((row) => row.id !== task.id)
const items = drafted.length === 0 ? [] : await db('vk_task_acceptance_items').whereIn('task_id', drafted.map((row) => row.id))
const held = (await wallet(company)).held
check('公司 Agent 拟了 1–2 个任务并进入待审核,每个 1–5 条验收清单、赏金不超过 60', drafted.length >= 1 && drafted.length <= 2 && drafted.every((row) => row.reward >= 1 && row.reward <= 60 && items.filter((item) => item.task_id === row.id).length >= 1) && held === drafted.reduce((sum, row) => sum + row.reward, 0), { drafted: drafted.map((row) => ({ title: row.title, reward: row.reward })), held, log: lines })
const queue = await call('GET', '/api/voko/approvals', { token: alice.token })
check('拟出的任务出现在负责人的待审核列表', drafted.every((row) => queue.json.tasks.some((item) => item.id === row.id)), queue.json.tasks.map((item) => item.title))
})