90 lines
4.9 KiB
JavaScript
90 lines
4.9 KiB
JavaScript
import { createInterface } from "node:readline";
|
|
import { readFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const base = "http://127.0.0.1:3001";
|
|
const input = createInterface({ input: process.stdin });
|
|
console.log("Awaiting in-memory credential (no file persistence)");
|
|
const key = (await input[Symbol.asyncIterator]().next()).value?.trim();
|
|
input.close();
|
|
if (!key) throw new Error("Credential required on stdin");
|
|
const api = process.argv[2] ?? "openai-responses";
|
|
const existing = process.argv[3];
|
|
const safe = (value) => JSON.stringify(value).replaceAll(key, "[REDACTED]");
|
|
async function request(path, body, method = "POST") {
|
|
const response = await fetch(base + path, {
|
|
method,
|
|
...(body === undefined ? {} : { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }),
|
|
signal: AbortSignal.timeout(30_000),
|
|
});
|
|
const result = await response.json();
|
|
if (!response.ok) throw new Error(safe({ status: response.status, result }));
|
|
return result;
|
|
}
|
|
const { sessionId } = existing ? { sessionId: existing } : await request("/api/sessions");
|
|
console.log(safe({ sessionId, api }));
|
|
await request(`/api/sessions/${sessionId}/model-config`, {
|
|
providerId: "compatible", api, baseUrl: "https://gateway.pencil-stub.com/v1",
|
|
apiKey: key, modelId: "Qwen3.8-27B-FP8", supportsImages: false, maxTokens: 4096,
|
|
}, "PUT");
|
|
console.log("Model configured in Session memory");
|
|
|
|
async function run(text, approve = false) {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), 150_000);
|
|
const response = await fetch(`${base}/api/sessions/${sessionId}/events`, { signal: controller.signal });
|
|
const reader = response.body.getReader();
|
|
const sent = await request(`/api/sessions/${sessionId}/messages`, { text, attachmentIds: [] });
|
|
const events = [];
|
|
let pending = "";
|
|
let answer = "";
|
|
const decoder = new TextDecoder();
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
pending += decoder.decode(value, { stream: true });
|
|
let boundary;
|
|
while ((boundary = pending.indexOf("\n\n")) !== -1) {
|
|
const frame = pending.slice(0, boundary);
|
|
pending = pending.slice(boundary + 2);
|
|
const data = frame.split("\n").find((line) => line.startsWith("data: "))?.slice(6);
|
|
if (!data) continue;
|
|
const event = JSON.parse(data);
|
|
if (event.runId !== sent.runId) continue;
|
|
events.push(event);
|
|
if (event.type === "assistant.delta") answer += event.payload.delta;
|
|
else console.log(safe({ type: event.type, payload: event.type === "tool.completed" && event.payload.toolName === "read" ? { toolName: "read", toolCallId: event.payload.toolCallId } : event.payload }));
|
|
if (event.type === "approval.required") {
|
|
if (!approve || event.payload.toolName !== "save_report_draft") throw new Error("Unexpected approval request");
|
|
if (process.env.LIVE_APPROVAL_MODE !== "manual") await request(`/api/approvals/${event.payload.approvalId}/approve`);
|
|
}
|
|
if (["run.completed", "run.failed", "run.cancelled"].includes(event.type)) {
|
|
console.log(safe({ runId: sent.runId, answer, deltaCount: events.filter((e) => e.type === "assistant.delta").length }));
|
|
if (event.type !== "run.completed" || !answer.trim()) throw new Error("Run did not produce a successful visible answer");
|
|
return events;
|
|
}
|
|
}
|
|
}
|
|
throw new Error("Event stream ended without a terminal run event");
|
|
} finally {
|
|
clearTimeout(timer);
|
|
controller.abort();
|
|
}
|
|
}
|
|
|
|
try {
|
|
await run("请只回复:连接成功。不要调用工具。");
|
|
const events = await run("请分析 demo-account 最近 7 天广告数据,并调用 save_report_draft 保存中文报告。先读取 ads-analysis 和 report-writer 两个 Skill,再调用 mock_ads_metrics。报告正文严格控制在 400 字内,保留五个章节,每章 1 至 2 句,不要表格;数字引用工具结果,注明模拟数据。写入由本次测试批准,成功后只回复保存成功及路径。", true);
|
|
for (const name of ["mock_ads_metrics", "save_report_draft"]) {
|
|
if (!events.some((e) => e.type === "tool.completed" && e.payload.toolName === name)) throw new Error(`Tool did not complete: ${name}`);
|
|
}
|
|
if (!events.some((e) => e.type === "approval.resolved" && e.payload.status === "approved")) throw new Error("Approval was not observed");
|
|
if (events.filter((e) => e.type === "skill.loaded").length < 2) throw new Error("Both Skills must be loaded successfully");
|
|
const report = await readFile(fileURLToPath(new URL(`../.agent-studio-sessions/${sessionId}/report.md`, import.meta.url)), "utf8");
|
|
console.log(safe({ result: "PASS", sessionId, reportCharacters: report.length, skillLoaded: events.filter((e) => e.type === "skill.loaded").length }));
|
|
} catch (error) {
|
|
console.error(safe({ result: "FAIL", message: error.message, sessionId }));
|
|
process.exitCode = 1;
|
|
}
|