- browser.screenshot returns the page as an image content block (≤1280px wide, JPEG quality configurable) for vision-capable upstreams; non-vision upstreams get a redacted text snapshot. Login pages / password fields are marked sensitive and withheld unless a browser.sensitive_screenshot grant exists and allowSensitive is passed. - browser.click_at / type_at / scroll / wait act in screenshot pixels; the server converts via viewport/DPR, requires automation control and an allowlisted host, and writes a thumbnail evidence file (DATA_DIR/tmp/evidence) referenced from agent_run_event. - browser.solve_challenge runs a bounded child agent_run (vision caps only, maxSteps ≤ 12, budget), judges success from page facts, stops immediately on 2FA / SMS / device confirmation, and otherwise moves the session to waiting with evidence and a takeover link. - PiAgentRuntime passes modelImage outputs as pi-ai image content when model.input has image; the ledger never stores image data; coordinate actions use per-invocation idempotency keys. - Tests: image codec, runtime image content, E14 e2e (login solved, 2FA stop, step limit, sensitive withheld). Real camofox 1.17.0 check on example.com recorded in the runbook. Co-Authored-By: Claude Opus 5.5 <[email protected]>
110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import { z } from "zod";
|
|
import {
|
|
type AgentRunHandle,
|
|
CapabilityRegistry,
|
|
createFakeRuntime,
|
|
type FakeScript,
|
|
toolContent,
|
|
} from "../src/index.ts";
|
|
import { settings, tempDir } from "./helpers.ts";
|
|
|
|
// VIS-1:能力输出里的 modelImage 以 image content 交给支持视觉的模型;不支持时降级为文字提示,图片数据不进 JSON 文本。
|
|
|
|
const PNG_1X1 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
|
|
|
const opened: AgentRunHandle[] = [];
|
|
afterEach(async () => {
|
|
await Promise.all(opened.splice(0).map((r) => r.close()));
|
|
});
|
|
|
|
function registry(seen: Array<boolean | undefined>) {
|
|
const r = new CapabilityRegistry();
|
|
r.register(
|
|
{
|
|
id: "browser.screenshot",
|
|
version: 1,
|
|
title: "截图",
|
|
description: "截取页面",
|
|
inputSchema: z.object({ sessionId: z.string() }),
|
|
outputSchema: z.object({
|
|
url: z.string(),
|
|
modelImage: z
|
|
.object({ mimeType: z.string(), data: z.string(), width: z.number(), height: z.number() })
|
|
.nullable(),
|
|
}),
|
|
kind: "read",
|
|
scope: "read",
|
|
riskLevel: "low",
|
|
idempotent: true,
|
|
supportsPreview: false,
|
|
replay: "safe",
|
|
},
|
|
(_input, ctx) => {
|
|
seen.push(ctx.modelImages);
|
|
return {
|
|
url: "https://site.example/login",
|
|
modelImage: { mimeType: "image/png", data: PNG_1X1, width: 1, height: 1 },
|
|
};
|
|
},
|
|
);
|
|
return r;
|
|
}
|
|
|
|
const script: FakeScript = {
|
|
match: /看页面/,
|
|
steps: [{ toolCalls: [{ capability: "browser.screenshot", input: { sessionId: "s1" } }] }],
|
|
finalText: (r) => `images=${r[0]?.images};text=${r[0]?.text}`,
|
|
};
|
|
|
|
async function run(vision: boolean) {
|
|
const seen: Array<boolean | undefined> = [];
|
|
const { runtime, fake } = createFakeRuntime({ registry: registry(seen), scripts: [script], fake: { vision } });
|
|
const handle = await runtime.createRun({
|
|
runId: `vision-${vision}`,
|
|
goal: "看页面",
|
|
systemPrompt: "测试",
|
|
capabilities: ["browser.screenshot"],
|
|
settings: settings(),
|
|
sessionDir: await tempDir(),
|
|
actor: { actorId: "user:1", taskId: "task:1", scopeIds: [] },
|
|
});
|
|
opened.push(handle);
|
|
const outcome = await handle.prompt();
|
|
return { handle, fake, outcome, seen };
|
|
}
|
|
|
|
describe("VIS-1 图片内容块", () => {
|
|
it("支持视觉的模型:截图作为 image content 交给模型,JSON 文本里没有图片数据", async () => {
|
|
const { handle, fake, outcome, seen } = await run(true);
|
|
expect(handle.supportsImages).toBe(true);
|
|
expect(seen).toEqual([true]);
|
|
expect(fake.images).toEqual([{ mimeType: "image/png", bytes: PNG_1X1.length }]);
|
|
expect(outcome.finalText).toContain("images=1");
|
|
expect(outcome.finalText).not.toContain(PNG_1X1);
|
|
});
|
|
|
|
it("不支持视觉的模型:能力收到 modelImages=false,图片不发送并提示", async () => {
|
|
const { handle, fake, outcome, seen } = await run(false);
|
|
expect(handle.supportsImages).toBe(false);
|
|
expect(seen).toEqual([false]);
|
|
expect(fake.images).toEqual([]);
|
|
expect(outcome.finalText).toContain("images=0");
|
|
expect(outcome.finalText).not.toContain(PNG_1X1);
|
|
});
|
|
|
|
it("toolContent:摘出 modelImage;null 时只有文本", () => {
|
|
const img = toolContent({ a: 1, modelImage: { mimeType: "image/jpeg", data: "QUJD" } }, true);
|
|
expect(img).toEqual([
|
|
{ type: "text", text: '{"a":1}' },
|
|
{ type: "image", data: "QUJD", mimeType: "image/jpeg" },
|
|
]);
|
|
const none = toolContent({ a: 1, modelImage: null }, true);
|
|
expect(none).toEqual([{ type: "text", text: '{"a":1}' }]);
|
|
const text = toolContent({ a: 1, modelImage: { mimeType: "image/jpeg", data: "QUJD" } }, false);
|
|
expect(text).toHaveLength(2);
|
|
expect(text[1]).toMatchObject({ type: "text" });
|
|
expect(JSON.stringify(text)).not.toContain("QUJD");
|
|
});
|
|
});
|