118 lines
8.4 KiB
JavaScript
118 lines
8.4 KiB
JavaScript
const assert = require('node:assert/strict')
|
||||
|
|
const fs = require('node:fs')
|
|||
|
|
const path = require('node:path')
|
|||
|
|
const { execFileSync } = require('node:child_process')
|
|||
|
|
|
|||
|
|
const directory = path.resolve(__dirname, '../../acceptance')
|
|||
|
|
const base = 'http://127.0.0.1:18080/api/v1'
|
|||
|
|
const resumed = process.env.RESUME_REAL_GENERATION === '1' ? JSON.parse(fs.readFileSync(path.join(directory, 'real-generation-checks.json'), 'utf8')) : null
|
|||
|
|
const prefix = resumed?.prefix || `real-generation-${Date.now()}`
|
|||
|
|
const checks = [], actions = [], fixtures = []
|
|||
|
|
let auth, agent, stage = 'login', failure
|
|||
|
|
const sql = statement => execFileSync('docker', ['--context', 'colima-yawan', 'exec', 'WeKnora-postgres', 'psql', '-U', 'postgres', '-d', 'yawan_native_acceptance_20260915', '-v', 'ON_ERROR_STOP=1', '-Atc', statement], {encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}).trim()
|
|||
|
|
const literal = value => "'" + String(value).replaceAll("'", "''") + "'"
|
|||
|
|
function check(name, value) {
|
|||
|
|
for (let i = checks.length - 1; i >= 0; i--) if (checks[i].name === name) checks.splice(i, 1)
|
|||
|
|
checks.push({name, passed: !!value}); assert.ok(value, name)
|
|||
|
|
}
|
|||
|
|
async function api(route, method = 'GET', body, expected = 200) {
|
|||
|
|
const response = await fetch(base + route, {method, headers: {'Content-Type': 'application/json', ...(auth ? {Authorization: `Bearer ${auth.token}`, 'X-Tenant-ID': '10059'} : {})}, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60000)})
|
|||
|
|
const result = await response.json()
|
|||
|
|
assert.equal(response.status, expected, `${stage}: ${method} ${route}: HTTP ${response.status}`)
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
async function create(kind, data) {
|
|||
|
|
const resource = (await api(`/workbench/resources/${kind}`, 'POST', {name: `${prefix}-${kind}`, data}, 201)).data
|
|||
|
|
fixtures.push(resource)
|
|||
|
|
return resource
|
|||
|
|
}
|
|||
|
|
async function action(resource, name, args = {}) {
|
|||
|
|
stage = `${resource.kind}:${name}`
|
|||
|
|
const start = Date.now()
|
|||
|
|
const job = (await api(`/workbench/resources/${resource.kind}/${resource.id}/actions/${name}`, 'POST', {version: resource.version, ...args}, 202)).data
|
|||
|
|
let current, lastStage
|
|||
|
|
do {
|
|||
|
|
await new Promise(resolve => setTimeout(resolve, 1200))
|
|||
|
|
current = (await api(`/workbench/jobs/${job.id}`)).data
|
|||
|
|
if (current.stage !== lastStage) { console.log(JSON.stringify({action: name, stage: current.stage, elapsed_seconds: Math.round((Date.now() - start) / 1000)})); lastStage = current.stage }
|
|||
|
|
} while (['queued', 'running'].includes(current.status) && Date.now() - start < 12 * 60 * 1000)
|
|||
|
|
actions.push({action: name, job_id: job.id, status: current.status, elapsed_ms: Date.now() - start, error: current.error || ''})
|
|||
|
|
if (current.status !== 'succeeded') throw new Error(`AI action ${name} ended ${current.status}; see private verification report`)
|
|||
|
|
return (await api(`/workbench/resources/${resource.kind}/${resource.id}`)).data
|
|||
|
|
}
|
|||
|
|
async function main() {
|
|||
|
|
const credentials = JSON.parse(fs.readFileSync(path.join(directory, 'super-admin-login.json'), 'utf8'))
|
|||
|
|
auth = await api('/auth/login', 'POST', {email: credentials.email, password: credentials.password})
|
|||
|
|
const models = (await api('/models')).data
|
|||
|
|
const model = models.find(m => m.name === 'deepseek-flash' && m.type === 'KnowledgeQA')
|
|||
|
|
assert.ok(model, 'Migrated live generation model is configured')
|
|||
|
|
const kb = (await api('/knowledge-bases')).data.find(k => k.name === '销售逻辑')
|
|||
|
|
assert.ok(kb, 'Migrated knowledge base exists')
|
|||
|
|
const before = (await api('/workbench/balance')).data
|
|||
|
|
let map, task
|
|||
|
|
if (resumed) {
|
|||
|
|
fixtures.push(...resumed.fixtures)
|
|||
|
|
actions.push(...resumed.actions)
|
|||
|
|
checks.push(...resumed.checks)
|
|||
|
|
agent = (await api(`/agents/${resumed.agent_id}`)).data
|
|||
|
|
map = (await api(`/workbench/resources/training-maps/${resumed.fixtures.find(r => r.kind === 'training-maps').id}`)).data
|
|||
|
|
task = (await api(`/workbench/resources/content-tasks/${resumed.fixtures.find(r => r.kind === 'content-tasks').id}`)).data
|
|||
|
|
} else {
|
|||
|
|
agent = (await api('/agents', 'POST', {name: prefix, config: {
|
|||
|
|
model_id: model.id, agent_mode: 'quick-answer', kb_selection_mode: 'selected', knowledge_bases: [kb.id],
|
|||
|
|
system_prompt: '本次是普通消费者沟通课程的验收样例,只讨论需求澄清、预算与理性决策。不要输出成人性描写。课程只安排两天,每天两个主要框架节点。拆解为三个内容段,每段正文约 250 字,保持术语和逻辑一致。所有输出遵循当前任务要求的 JSON 或正文格式。', temperature: 0.2,
|
|||
|
|
}}, 201)).data
|
|||
|
|
const flow = await create('flows', {nodes: [{id: 'writer', label: '课程作者', agent_id: agent.id}], edges: []})
|
|||
|
|
map = await create('training-maps', {topic: '成年人如何澄清购物需求并做出理性消费决策', days_count: 2, days_auto: false, agent_id: agent.id, knowledge_base_ids: [kb.id], reading_mode: 'full'})
|
|||
|
|
map = await action(map, 'generate')
|
|||
|
|
check('two_real_generated_days', map.data.days.length === 2)
|
|||
|
|
check('native_evidence_retrieved', map.data.reading?.evidence_chunks > 0)
|
|||
|
|
check('layered_reading_reports_native_documents', map.data.reading?.documents > 0)
|
|||
|
|
map = await action(map, 'confirm')
|
|||
|
|
map = await action(map, 'generate-day', {day: 1})
|
|||
|
|
map = await action(map, 'confirm-day', {day: 1})
|
|||
|
|
task = await create('content-tasks', {map_id: map.id, day: 1, flow_id: flow.id, reading_mode: 'full'})
|
|||
|
|
task = await action(task, 'decompose')
|
|||
|
|
check('real_decomposition_has_segments', task.data.segments.length >= 2)
|
|||
|
|
check('configuration_is_frozen', !!task.data.flow_snapshot && Object.keys(task.data.agent_snapshots || {}).length > 0)
|
|||
|
|
}
|
|||
|
|
const selected = task.data.segments.slice(0, 3).map(s => s.id)
|
|||
|
|
for (const segment of task.data.segments) {
|
|||
|
|
if (['accepted', 'skipped'].includes(segment.status)) continue
|
|||
|
|
if (segment.id === selected[1] || !selected.includes(segment.id)) task = await action(task, 'skip', {segment_id: segment.id})
|
|||
|
|
else {
|
|||
|
|
task = await action(task, 'generate', {segment_id: segment.id})
|
|||
|
|
check(`generated_${segment.id}_has_evidence`, task.data.segments.find(s => s.id === segment.id).source_ids.length > 0)
|
|||
|
|
task = await action(task, 'accept', {segment_id: segment.id})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (!task.data.quality) task = await action(task, 'review')
|
|||
|
|
check('structured_real_quality_report', !!task.data.quality?.manuscript_hash && Array.isArray(task.data.quality.issues))
|
|||
|
|
if (!task.data.quality.passed) {
|
|||
|
|
const issues = task.data.quality.issues
|
|||
|
|
const ids = new Set(issues.filter(i => i.severity === 'blocking').flatMap(i => i.segment_ids))
|
|||
|
|
for (const segment of task.data.segments) {
|
|||
|
|
if (segment.status !== 'accepted' || !ids.has(segment.id)) continue
|
|||
|
|
const advice = issues.filter(i => i.segment_ids.includes(segment.id)).map(i => `${i.problem};${i.suggestion}`).join('\n')
|
|||
|
|
task = await action(task, 'revise', {segment_id: segment.id, advice})
|
|||
|
|
task = await action(task, 'accept', {segment_id: segment.id})
|
|||
|
|
}
|
|||
|
|
for (const segment of task.data.segments) if (segment.status === 'needs_review') task = await action(task, 'accept', {segment_id: segment.id})
|
|||
|
|
task = await action(task, 'review')
|
|||
|
|
}
|
|||
|
|
check('real_quality_passed_after_review', task.data.quality.passed)
|
|||
|
|
task = await action(task, 'compose')
|
|||
|
|
check('real_final_manuscript_saved', task.status === 'completed' && task.data.composed.length > 100)
|
|||
|
|
const after = (await api('/workbench/balance')).data
|
|||
|
|
check('real_usage_debited_once_and_holds_released', after.balance < before.balance && after.reserved === before.reserved)
|
|||
|
|
const jobIDs = actions.map(a => literal(a.job_id)).join(',')
|
|||
|
|
const usage = JSON.parse(sql(`SELECT coalesce(json_agg(json_build_object('action', action, 'tokens', input_tokens+output_tokens, 'points', points, 'status',status)), '[]'::json) FROM workbench_usage WHERE tenant_id=10059 AND job_id IN (${jobIDs})`))
|
|||
|
|
check('live_model_usage_recorded', usage.some(r => r.tokens > 0 && r.points > 0))
|
|||
|
|
fs.writeFileSync(path.join(directory, 'real-generation-manuscript.json'), JSON.stringify({map, task, usage}, null, 2), {mode: 0o600})
|
|||
|
|
}
|
|||
|
|
main().catch(error => { failure = error.message; process.exitCode = 1 }).finally(async () => {
|
|||
|
|
// Keep generated material for inspection; mark its disposable provenance.
|
|||
|
|
fs.writeFileSync(path.join(directory, 'real-generation-checks.json'), JSON.stringify({time: new Date().toISOString(), prefix, stage, checks, actions, fixtures: fixtures.map(r => ({id: r.id, kind: r.kind})), agent_id: agent?.id, failure, passed: !failure}, null, 2), {mode: 0o600})
|
|||
|
|
console.log(JSON.stringify({stage, checks, failure, passed: !failure}))
|
|||
|
|
})
|