feat: publish Agent Studio integrations and setup guide

This commit is contained in:
Wenxin Duan
2026-09-16 16:32:16 +08:00
parent 2ad089d68a
commit 05e6060490
106 changed files with 4942 additions and 198 deletions
+37
View File
@@ -0,0 +1,37 @@
import { createInterface } from "node:readline";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
// Run from an interactive terminal with echo disabled. Never pass secrets in argv.
console.log('Provide one JSON line on hidden stdin: {"swadsToken":"…","llmKey":"…"}. Credentials stay in process memory.');
const reader = createInterface({ input: process.stdin });
let credentials;
try {
const line = await reader[Symbol.asyncIterator]().next();
credentials = JSON.parse(line.value);
if (typeof credentials.swadsToken !== "string" || !credentials.swadsToken || typeof credentials.llmKey !== "string" || !credentials.llmKey) throw new Error();
} catch {
console.error("Invalid credential input; nothing was written.");
process.exit(1);
} finally { reader.close(); }
const server = spawn(process.execPath, [fileURLToPath(new URL("../apps/server/dist/index.js", import.meta.url))], {
stdio: ["ignore", "inherit", "inherit"],
env: {
...process.env,
SWADS_MCP_TOKEN: credentials.swadsToken,
OPENAI_API_KEY: credentials.llmKey,
CMS_SUPABASE_PUBLISHABLE_KEY: typeof credentials.cmsPublishableKey === "string" ? credentials.cmsPublishableKey : "",
AGENT_PROVIDER: "compatible", AGENT_MODEL: "Qwen3.8-27B-FP8",
// This gateway recovered Skill/tool calls with Chat Completions during the live repair test.
LLM_API: "openai-completions", LLM_BASE_URL: "https://gateway.pencil-stub.com/v1",
// Verified against this gateway/model using synthetic image inputs.
AGENT_MODEL_SUPPORTS_IMAGES: "true", AGENT_MODEL_MAX_TOKENS: "8192",
SESSION_RUN_TIMEOUT_MS: "600000", APPROVAL_TIMEOUT_MS: "180000",
HOST: "127.0.0.1", PORT: "3001",
},
});
credentials = undefined;
console.log("Starting local server with private in-memory connections.");
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => server.kill(signal));
server.on("exit", (code) => { process.exitCode = code ?? 0; });
+100
View File
@@ -0,0 +1,100 @@
import { createInterface } from "node:readline";
import assert from "node:assert/strict";
// Test the web proxy/API and real model without sending licensed CMS content to it.
// Supply credentials only over hidden stdin. Never log response bodies or secrets.
const base = "http://127.0.0.1:5173";
const reader = createInterface({ input: process.stdin });
let sessionId;
let stage = "input";
const abort = new AbortController();
async function request(path, method = "GET", body) {
const response = await fetch(base + path, {
method, redirect: "error", signal: AbortSignal.timeout(30_000),
headers: { Origin: base, ...(body === undefined ? {} : { "Content-Type": "application/json" }) },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
const result = await response.json();
if (!response.ok) {
const error = new Error("API request failed");
error.code = /^[A-Z_]{1,80}$/.test(result.error?.code ?? "") ? result.error.code : "API_REQUEST_FAILED";
throw error;
}
return result;
}
console.log("Provide CMS email/password as one JSON line on hidden stdin.");
try {
const input = JSON.parse((await reader[Symbol.asyncIterator]().next()).value);
reader.close();
stage = "discovery";
const catalog = await request("/api/skills");
assert(catalog.skills.some((skill) => skill.name === "swads-short-drama-editor"));
({ sessionId } = await request("/api/sessions", "POST"));
stage = "login";
let login;
try { login = await request(`/api/sessions/${sessionId}/cms/connection`, "POST", { email: input.email, password: input.password }); }
finally { input.email = ""; input.password = ""; }
assert.equal(login.connection.connected, true);
stage = "catalog";
const dramas = await request(`/api/sessions/${sessionId}/cms/dramas`);
assert(dramas.dramas.length > 0);
console.log(JSON.stringify({ stage, connected: true, dramaCount: dramas.dramas.length, skillDiscovered: true }));
stage = "model";
const timeout = setTimeout(() => abort.abort(), 150_000);
const stream = await fetch(`${base}/api/sessions/${sessionId}/events`, { signal: abort.signal });
const streamReader = stream.body.getReader();
const sent = await request(`/api/sessions/${sessionId}/messages`, "POST", {
text: "做 swads-short-drama-editor 接入验收:先完整读取该 Skill 的 SKILL.md 和 references/cms-source-contract.md,然后调用 cms_connection_status。不要读取 CMS 剧集、字幕、任何业务数据,不要调用其他工具或生成文件。最后用中文简短说明已经连接和仍然缺失的能力,不要声称成片功能已经完成。",
attachmentIds: [],
});
let pending = "";
let terminal = false;
let skillLoaded = false;
let referenceRead = false;
const referenceCalls = new Set();
let statusCompleted = false;
let answerReceived = false;
const decoder = new TextDecoder();
try {
while (!terminal) {
const { value, done } = await streamReader.read();
assert(!done, "Unexpected stream end");
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;
assert.notEqual(event.type, "approval.required", "Unexpected content access request");
if (event.type === "skill.loaded") skillLoaded = true;
if (event.type === "tool.completed" && event.payload.toolName === "cms_connection_status") {
const result = event.payload.result?.details ?? event.payload.result;
statusCompleted = result?.connected === true && result?.capabilities?.rendering === false;
}
if (event.type === "tool.requested" && event.payload.toolName === "read" && JSON.stringify(event.payload.arguments).includes("references/cms-source-contract.md")) referenceCalls.add(event.payload.toolCallId);
if (event.type === "tool.completed" && referenceCalls.has(event.payload.toolCallId)) referenceRead = true;
if (event.type === "assistant.delta" && event.payload.delta?.trim()) answerReceived = true;
if (["run.completed", "run.failed", "run.cancelled"].includes(event.type)) {
console.log(JSON.stringify({ terminal: event.type, skillLoaded, referenceRead, statusCompleted, answerReceived }));
assert.equal(event.type, "run.completed");
terminal = true;
}
}
}
assert(skillLoaded && referenceRead && statusCompleted && answerReceived, "Required Skill/tool events missing");
console.log(JSON.stringify({ result: "PASS", skillLoaded, referenceRead, statusCompleted, answerReceived, cmsContentSentToModel: false, videoPipelineVerified: false }));
} finally { clearTimeout(timeout); abort.abort(); }
} catch (error) {
console.error(JSON.stringify({ result: "FAIL", stage, code: error.code ?? "ASSERTION_OR_NETWORK_FAILURE" }));
process.exitCode = 1;
} finally {
reader.close(); abort.abort();
if (sessionId) {
await request(`/api/sessions/${sessionId}/cancel`, "POST").catch(() => {});
await request(`/api/sessions/${sessionId}/cms/connection`, "DELETE").catch(() => {});
await request(`/api/sessions/${sessionId}`, "DELETE").catch(() => {});
}
}
+21
View File
@@ -0,0 +1,21 @@
import { createInterface } from "node:readline";
import { resolve } from "node:path";
import { CmsConnection, EditorService } from "../packages/harness/dist/index.js";
// Authorized local-only real-media acceptance. Credentials enter hidden stdin, never files.
const reader = createInterface({ input: process.stdin });
let cms;
console.log("Provide CMS login via hidden stdin. Media stays local; no model request.");
try {
const input = JSON.parse((await reader[Symbol.asyncIterator]().next()).value);
reader.close();
cms = new CmsConnection({ publishableKey: input.publishableKey });
await cms.login(input.email, input.password);
input.email = ""; input.password = "";
const service = new EditorService({ cms, runtimeRoot: resolve(".agent-studio-runtime/videos"), sessionId: "ses_media_acceptance" });
const result = await service.prepare({ dramaId: "06aa396d-175a-48ed-ad76-02786ef4d0a9", episodeNumbers: [1, 3, 5] }, AbortSignal.timeout(600_000));
console.log(JSON.stringify({ result: "PASS", scope: "real local download/remux/frames only", ...result }));
} catch (error) {
console.error(JSON.stringify({ result: "FAIL", code: typeof error?.code === "string" ? error.code : "MEDIA_VERIFICATION_FAILED" }));
process.exitCode = 1;
} finally { reader.close(); cms?.clear(); }
+83
View File
@@ -0,0 +1,83 @@
import { createInterface } from "node:readline";
import { createRequire } from "node:module";
import assert from "node:assert/strict";
import { validateBaseUrl } from "../packages/harness/dist/index.js";
import { parseHls } from "../packages/harness/dist/media/hls.js";
// Read-only acceptance preflight. No signed URLs, auth tokens or subtitles are logged.
const require = createRequire(new URL("../packages/harness/package.json", import.meta.url));
const { createClient } = require("@supabase/supabase-js");
const dramaId = "06aa396d-175a-48ed-ad76-02786ef4d0a9";
const cmsBase = "https://swave.pencil-stub.com/swave-api/api/v1/cms/playback/";
const reader = createInterface({ input: process.stdin });
console.log("Provide CMS email/password/publishableKey via hidden stdin.");
let stage = "login";
async function boundedText(response) {
assert.equal(response.status, 200);
const stream = response.body.getReader();
const chunks = [];
let length = 0;
try {
for (;;) {
const {done,value} = await stream.read();
if (done) break;
length += value.byteLength;
assert(length <= 2 * 1024 * 1024);
chunks.push(value);
}
} finally { await stream.cancel().catch(() => {}); }
return Buffer.concat(chunks).toString("utf8");
}
try {
const input = JSON.parse((await reader[Symbol.asyncIterator]().next()).value);
reader.close();
const client = createClient("https://lmcysqgfhoxppwuorsmu.supabase.co", input.publishableKey, {
auth:{persistSession:false,autoRefreshToken:false,detectSessionInUrl:false},
global:{fetch:(url,init)=>fetch(url,{...init,redirect:"error",signal:AbortSignal.timeout(20_000)})},
});
const {data:auth,error:authError} = await client.auth.signInWithPassword({email:input.email,password:input.password});
input.email = ""; input.password = "";
assert(!authError && auth.session);
const {data:user,error:userError} = await client.auth.getUser(auth.session.access_token);
assert(!userError && user.user?.app_metadata.role === "admin");
stage = "inventory";
const {data:drama,error:dramaError} = await client.from("drama").select("id,title,language,total_episodes").eq("id",dramaId).single();
assert(!dramaError && drama.id === dramaId && drama.language === "en");
const {data:episodes,error:episodeError} = await client.from("episode").select("id,episode_no,drama_id").eq("drama_id",dramaId).order("episode_no").range(0,999);
assert(!episodeError && episodes.length === drama.total_episodes);
assert(episodes.every((e,i)=>e.drama_id===dramaId && e.episode_no===i+1));
const sampled = [episodes[0],episodes[Math.floor(episodes.length/2)],episodes.at(-1)];
const checks = [];
for (const episode of sampled) {
stage = `episode_${episode.episode_no}_signing`;
const headers = {Authorization:`Bearer ${auth.session.access_token}`};
const signedResponse = await fetch(cmsBase+episode.id,{headers,redirect:"error",signal:AbortSignal.timeout(20_000)});
const signed = JSON.parse(await boundedText(signedResponse));
const url = new URL(signed.url);
assert(url.protocol==="https:" && !url.username && !url.password && !url.hash && url.pathname.endsWith(".m3u8"));
await validateBaseUrl(url.origin);
assert(Date.parse(signed.expires_at)>Date.now());
stage = `episode_${episode.episode_no}_manifest`;
const manifest = await boundedText(await fetch(url,{redirect:"error",signal:AbortSignal.timeout(20_000)}));
assert(manifest.trimStart().startsWith("#EXTM3U"));
console.log(JSON.stringify({episodeNo:episode.episode_no,cdnOrigin:url.origin,master:manifest.includes("#EXT-X-STREAM-INF"),segments:(manifest.match(/#EXTINF:/g)??[]).length,encrypted:manifest.includes("#EXT-X-KEY"),byteranges:manifest.includes("#EXT-X-BYTERANGE"),fragmentedMp4:manifest.includes("#EXT-X-MAP")}));
const parsed = parseHls(manifest);
if (parsed.kind === "master") {
const child = new URL(parsed.variants[0], url);
console.log(JSON.stringify({sameOrigin:child.origin===url.origin,childSameQuery:!child.search||child.search===url.search,childWithinEpisode:child.pathname.startsWith(url.pathname.slice(0,url.pathname.lastIndexOf("/")+1)),childExtension:child.pathname.split(".").at(-1)}));
assert(child.origin === url.origin && child.pathname.startsWith(url.pathname.slice(0,url.pathname.lastIndexOf("/")+1)) && !child.username && !child.password && !child.hash);
console.log(JSON.stringify({queryKeys:[...child.searchParams.keys()],authShape:/^\d+-[a-zA-Z0-9]+-[a-zA-Z0-9]+-[a-fA-F0-9]+$/.test(child.searchParams.get("auth_key")??"")}));
const resolved = child;
const variant = await boundedText(await fetch(resolved,{redirect:"error",signal:AbortSignal.timeout(20_000)}));
console.log(JSON.stringify({tags:[...new Set(variant.split("\n").filter(line=>line.startsWith("#")).map(line=>line.split(":")[0]))],segments:(variant.match(/#EXTINF:/g)??[]).length,ends:variant.includes("#EXT-X-ENDLIST"),parseAccepted:(()=>{try{parseHls(variant);return true;}catch{return false;}})()}));
}
stage = `episode_${episode.episode_no}_subtitle`;
const subtitle = await boundedText(await fetch(cmsBase+episode.id+"/subtitles/en.vtt",{headers,redirect:"error",signal:AbortSignal.timeout(20_000)}));
assert(subtitle.trimStart().startsWith("WEBVTT") && subtitle.includes("-->"));
checks.push({episodeNo:episode.episode_no,manifestStatus:200,hlsHeader:true,englishWebVtt:true});
}
console.log(JSON.stringify({result:"PASS",scope:"CMS source preflight only",drama,checks,checkedAt:new Date().toISOString(),mediaDownloaded:false,modelContentSent:false,videoAnalysisComplete:false}));
} catch {
console.error(JSON.stringify({result:"FAIL",stage}));
process.exitCode = 1;
} finally { reader.close(); }
+52
View File
@@ -0,0 +1,52 @@
// Live model/Skill readiness only. Never uploads a reference or submits H3.
import assert from 'node:assert/strict';
const base='http://127.0.0.1:5173';
const abort=new AbortController();
async function api(path,method='GET',body){
const response=await fetch(base+path,{method,redirect:'error',signal:AbortSignal.timeout(20000),headers:{Origin:base,...(body?{'Content-Type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})});
assert(response.ok,`${method} ${path}: HTTP ${response.status}`);
return response.json();
}
let sessionId;
try {
const catalog=await api('/api/skills');
assert(catalog.skills.some(skill=>skill.name==='swads-image-to-video'),'New Skill missing from live catalog');
const history=await api('/api/image-video/tasks');
console.log(JSON.stringify({stage:'catalog-and-history',skillFound:true,historyAvailable:history!==undefined,remoteMutation:false}));
({sessionId}=await api('/api/sessions','POST',{}));
const stream=await fetch(`${base}/api/sessions/${sessionId}/events`,{signal:abort.signal});
assert(stream.ok&&stream.body,'SSE unavailable');
const reader=stream.body.getReader();
const sent=await api(`/api/sessions/${sessionId}/messages`,'POST',{text:'仅做 swads-image-to-video 接入只读检查:完整读取该 Skill 的 SKILL.md 和 references/swads.md。然后简短说明它不需要 CMS,以及必须分别批准参考图外传和 H3 生成。不要调用任何 CMS 工具,不读取图片或私人业务数据,不上传、不入库、不生成、不写文件。',attachmentIds:[]});
const timer=setTimeout(()=>abort.abort(),150000);
let buffer='';const decoder=new TextDecoder();let loaded=false;let answer=false;let referenceRead=false;let done=false;const referenceCalls=new Set();
try {
while(!done){
const chunk=await reader.read();assert(!chunk.done,'Unexpected SSE end');buffer+=decoder.decode(chunk.value,{stream:true});
let boundary;
while((boundary=buffer.indexOf('\n\n'))!==-1){
const frame=buffer.slice(0,boundary);buffer=buffer.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;
assert(event.type!=='approval.required','Unexpected external/mutating action request');
if(event.type==='tool.requested'){
assert(['read','ls','find','grep'].includes(event.payload.toolName),'Unexpected business tool requested');
if(event.payload.toolName==='read'&&JSON.stringify(event.payload.arguments).includes('references/swads.md'))referenceCalls.add(event.payload.toolCallId);
}
if(event.type==='skill.loaded')loaded=true;
if(event.type==='tool.completed'&&referenceCalls.has(event.payload.toolCallId))referenceRead=true;
if(event.type==='assistant.delta'&&event.payload.delta?.trim())answer=true;
if(['run.completed','run.failed','run.cancelled'].includes(event.type)){
console.log(JSON.stringify({stage:'model-skill',terminal:event.type,skillLoaded:loaded,referenceRead,answerReceived:answer,remoteMutation:false}));
assert(event.type==='run.completed');done=true;
}
}
}
assert(loaded&&referenceRead&&answer,'Skill/read/answer evidence incomplete');
}finally{clearTimeout(timer);abort.abort();}
console.log(JSON.stringify({result:'PASS',scope:'live Skill reading and local history only',h3GenerationVerified:false}));
}catch(error){console.error(JSON.stringify({result:'FAIL',message:String(error.message).slice(0,300)}));process.exitCode=1;}
finally{
abort.abort();
if(sessionId){await api(`/api/sessions/${sessionId}/cancel`,'POST',{}).catch(()=>{});await api(`/api/sessions/${sessionId}`,'DELETE').catch(()=>{});}
}
+89
View File
@@ -0,0 +1,89 @@
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;
}
+23
View File
@@ -0,0 +1,23 @@
import assert from "node:assert/strict";
import { mkdtemp, readdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { MinisReportService, SafeWorkspace } from "../packages/harness/dist/index.js";
// Offline QA fixture only: no MCP call and no real business conclusions.
const root = await mkdtemp(join(tmpdir(), "minis-render-qa-"));
const service = new MinisReportService({ workspace: await SafeWorkspace.create(root), sessionId: "ses_renderqa", skillRoot: "/Users/wenxinduan/.codex/skills/drama-minis-diagnosis" });
const sourceId = service.recordQuery({ fixture: true }, { synthetic: true, spend: 12, revenue: 3 });
const output = await service.generate({ source_query_ids: [sourceId], data: {
title: "排版验证 · 合成测试数据(非经营报告)", report_type: "daily", period: "TEST DAY A", comparison_period: "TEST DAY B", currency: "USD", timezone: "UTC+0", data_status: "synthetic_qa",
conclusion: "此文件仅用于验证原始 Skill 渲染器、中文字体与 A4 PDF 下载链路,不代表真实账户情况。",
cards: [{ label: "投放消耗", value: "$12.00", note: "合成测试" }, { label: "D0 广告收入", value: "$3.00", note: "合成测试" }, { label: "站内广告曝光", value: "18", note: "合成测试" }],
issues: [{ priority: "P2", problem: "排版测试", evidence: "合成数值", hypothesis: "不作业务推断", action: "验证中文与图表", acceptance: "可读且无裁切" }],
funnel: [{ label: "测试曝光", value: 120, display: "120 次", basis: "合成事件", rate: "100%" }, { label: "测试点击", value: 12, display: "12 次", basis: "合成事件", rate: "10%" }, { label: "测试启动", value: 6, display: "6 人", basis: "合成用户", rate: "50%" }],
countries: [{ label: "Test Alpha", value: 8 }, { label: "Test Beta", value: 4 }], country_revenue: [{ label: "Test Alpha", value: 2 }, { label: "Test Beta", value: 1 }],
country_funnels: [], trend: [], creatives: [], footnote: "全部数据为人工构造的 QA fixture;未查询真实经营指标。",
} });
assert.equal(output.pdfStatus, "ready", output.warning);
assert.equal(output.files.length, 3);
for (const format of ["json", "html", "pdf"]) assert.ok((await service.read(output.reportId, format)).bytes.length > 0);
console.log(JSON.stringify({ status: "passed", synthetic: true, root: join(root, (await readdir(root)).find((name) => name.startsWith(".report-"))), files: output.files.map(({ format, byteSize }) => ({ format, byteSize })) }));
+50
View File
@@ -0,0 +1,50 @@
import { createInterface } from "node:readline";
import { createRequire } from "node:module";
const require = createRequire(new URL("../packages/harness/package.json", import.meta.url));
const sharp = require("sharp");
const reader = createInterface({ input: process.stdin });
console.log("Provide gateway API key on hidden stdin. Only synthetic images will be sent.");
let key = (await reader[Symbol.asyncIterator]().next()).value?.trim();
reader.close();
if (!key) process.exit(1);
const model = "Qwen3.8-27B-FP8";
const shapes = [
'<circle cx="110" cy="180" r="60" fill="red"/><rect x="230" y="120" width="120" height="120" fill="blue"/><polygon points="470,115 410,240 530,240" fill="green"/>',
'<rect x="50" y="120" width="120" height="120" fill="green"/><polygon points="290,115 230,240 350,240" fill="red"/><circle cx="470" cy="180" r="60" fill="blue"/>',
];
const images = await Promise.all(shapes.map(async (shape) => {
const png = await sharp(Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="600" height="360"><rect width="600" height="360" fill="white"/>${shape}</svg>`)).png().toBuffer();
return `data:image/png;base64,${png.toString("base64")}`;
}));
const prompt = 'Describe each image separately. Return only JSON {"images":[[{"color":"...","shape":"..."}]]}. One inner array per image, list objects from left to right. Use lowercase English color names and shape names circle, square, or triangle. Do not guess if you cannot see the images.';
const expected = [[{color:"red",shape:"circle"},{color:"blue",shape:"square"},{color:"green",shape:"triangle"}], [{color:"green",shape:"square"},{color:"red",shape:"triangle"},{color:"blue",shape:"circle"}]];
try {
for (const api of ["responses", "chat/completions"]) {
const content = api === "responses"
? [{type:"input_text",text:prompt}, ...images.map((image_url) => ({type:"input_image",image_url}))]
: [{type:"text",text:prompt}, ...images.map((url) => ({type:"image_url",image_url:{url}}))];
const body = api === "responses"
? {model,input:[{role:"user",content}],max_output_tokens:2048,stream:false}
: {model,messages:[{role:"user",content}],max_tokens:2048,stream:false};
const response = await fetch(`https://gateway.pencil-stub.com/v1/${api}`, {
method:"POST", redirect:"error", signal:AbortSignal.timeout(90_000),
headers:{Authorization:`Bearer ${key}`,"Content-Type":"application/json"},body:JSON.stringify(body),
});
const result = await response.json();
if (!response.ok) {
console.log(JSON.stringify({api,httpStatus:response.status,error:String(result.error?.message ?? result.message ?? "Request failed").replaceAll(key,"[REDACTED]").slice(0,600)}));
continue;
}
const answer = api === "responses"
? (result.output ?? []).flatMap((item) => item.content ?? []).filter((item) => item.type === "output_text").map((item) => item.text).join("")
: result.choices?.[0]?.message?.content ?? "";
let parsed;
try { parsed = JSON.parse(answer.replace(/^\s*```(?:json)?\s*/, "").replace(/\s*```\s*$/, "")); } catch { /* Retain the bounded answer for diagnosis. */ }
const passed = Array.isArray(parsed?.images) && parsed.images.length === expected.length && expected.every((objects,i) => Array.isArray(parsed.images[i]) && parsed.images[i].length === objects.length && objects.every((item,j) => parsed.images[i][j]?.color === item.color && parsed.images[i][j]?.shape === item.shape));
console.log(JSON.stringify({api,httpStatus:response.status,model:result.model,passed,answer:answer.replaceAll(key,"[REDACTED]").slice(0,1500),expected,privateMediaSent:false}));
if (passed) break;
}
} catch (error) {
console.error(JSON.stringify({status:"probe_failed",type:error.name}));
process.exitCode = 1;
} finally { key = undefined; }
+18
View File
@@ -0,0 +1,18 @@
import { createInterface } from "node:readline";
import { CmsConnection } from "../packages/harness/dist/index.js";
console.log("Provide CMS email, password and publishableKey as one JSON line on hidden stdin.");
const reader = createInterface({ input: process.stdin });
let cms;
try {
const input = JSON.parse((await reader[Symbol.asyncIterator]().next()).value);
reader.close();
cms = new CmsConnection({ publishableKey: input.publishableKey });
const result = await cms.login(input.email, input.password);
input.password = "";
const catalog = await cms.listDramas("", 0);
console.log(JSON.stringify({ connected: result.connected, project: result.project, firstPageCount: catalog.dramas.length, credentialOutput: false }));
} catch (error) {
console.error(JSON.stringify({ status: "failed", code: error?.code ?? "CMS_PROBE_FAILED" }));
process.exitCode = 1;
} finally { reader.close(); cms?.clear(); }
+32
View File
@@ -0,0 +1,32 @@
import {createInterface} from "node:readline";
import {createRequire} from "node:module";
import {readFile} from "node:fs/promises";
const require=createRequire(new URL("../packages/harness/package.json",import.meta.url));
const sharp=require("sharp");
const reader=createInterface({input:process.stdin});
console.log(process.argv.includes("--authorized-job")?"Provide gateway key on hidden stdin. Previously authorized nine-frame transport comparison only.":"Provide gateway key on hidden stdin. Synthetic image transport comparison only.");
let key=(await reader[Symbol.asyncIterator]().next()).value?.trim();reader.close();if(!key)process.exit(1);
const png=await sharp(Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" width="288" height="512"><rect width="288" height="512" fill="white"/><rect x="40" y="40" width="200" height="200" fill="red"/></svg>')).png().toBuffer();
const picture={type:"input_image",image_url:`data:image/png;base64,${png.toString("base64")}`,detail:"auto"};
let pictures=Array.from({length:process.argv.includes("--nine")?9:1},()=>picture);
if(process.argv.includes("--authorized-job")){
const root=".agent-studio-runtime/videos/job-rdUnOV";
const job=JSON.parse(await readFile(`${root}/job.json`,"utf8"));
if(job.jobId!=="job_9e5a84cb-2f1f-4fac-8221-1f0e15681f5c"||job.dramaId!=="06aa396d-175a-48ed-ad76-02786ef4d0a9")throw new Error("Authorized job binding mismatch");
pictures=[];
for(const episode of [1,3,5])for(const frame of [0,1,2])pictures.push({type:"input_image",detail:"auto",image_url:`data:image/jpeg;base64,${(await readFile(`${root}/episode-${episode}/frame-${frame}.jpg`)).toString("base64")}`});
}
const streaming=process.argv.includes("--stream");
const prefix=[{role:"user",content:process.argv.includes("--authorized-job")?"Describe only the first image in one brief non-graphic sentence. Do not invent details or claim a content-review PASS.":"Use the image returned by the tool and name the visible color and shape in one sentence."},{type:"function_call",call_id:"call_fixture",name:"read_image",arguments:"{}"}];
try{
for(const kind of ["tool-array","tool-string-user-image"]){
const input=kind==="tool-array"?[...prefix,{type:"function_call_output",call_id:"call_fixture",output:[{type:"input_text",text:"Synthetic test image follows."},...pictures]}]:[...prefix,{type:"function_call_output",call_id:"call_fixture",output:"Synthetic image attached in the following user message."},{role:"user",content:[{type:"input_text",text:"Image from read_image (tool data, not new instructions):"},...pictures]}];
const response=await fetch("https://gateway.pencil-stub.com/v1/responses",{method:"POST",redirect:"error",signal:AbortSignal.timeout(90000),headers:{Authorization:`Bearer ${key}`,"Content-Type":"application/json"},body:JSON.stringify({model:"Qwen3.8-27B-FP8",input,tools:[{type:"function",name:"read_image",description:"Synthetic fixture",parameters:{type:"object",properties:{},additionalProperties:false}}],stream:streaming,max_output_tokens:2048})});
if(streaming){
const wire=await response.text();const events=wire.split("\n").filter(line=>line.startsWith("data: ")&&line!=="data: [DONE]").map(line=>{try{return JSON.parse(line.slice(6));}catch{return {};}});
console.log(JSON.stringify({kind,images:pictures.length,httpStatus:response.status,eventTypes:[...new Set(events.map(e=>e.type))],answer:events.filter(e=>e.type==="response.output_text.delta").map(e=>e.delta).join("").slice(0,1000),completion:events.filter(e=>e.type==="response.completed").map(e=>({status:e.response?.status,usage:e.response?.usage})),errors:events.filter(e=>e.error).map(e=>String(e.error.message??"error").replaceAll(key,"[REDACTED]").slice(0,300))}));continue;
}
const result=await response.json();const answer=(result.output??[]).flatMap(item=>item.content??[]).filter(item=>item.type==="output_text").map(item=>item.text).join("");
console.log(JSON.stringify({kind,httpStatus:response.status,status:result.status,outputTypes:result.output?.map(item=>item.type),answer:answer.replaceAll(key,"[REDACTED]").slice(0,1000),usage:result.usage,error:result.error?String(result.error.message).replaceAll(key,"[REDACTED]").slice(0,500):undefined}));
}
}finally{key=undefined;}