feat(browser): vision automation loop with screenshots, coordinate actions and bounded challenge solving
- 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]>
This commit is contained in:
1 parent
0765264a47
commit
c437c6bff6
34 files changed
+2194
-35
No files matched your search
@@ -25,6 +25,8 @@ export interface FakeToolResult {
|
||||
text: string;
|
||||
/** text 能解析成 JSON 时的值 */
|
||||
json?: unknown;
|
||||
/** 工具结果里的图片内容块数(视觉自动化:截图真的交给了模型) */
|
||||
images: number;
|
||||
}
|
||||
|
||||
export interface FakeToolCall {
|
||||
@@ -54,6 +56,8 @@ export interface FakeModelProviderOptions {
|
||||
maxTokens?: number;
|
||||
/** 压缩请求时返回的摘要文本 */
|
||||
summaryText?: string;
|
||||
/** 模拟支持图片输入的模型(model.input 含 image);缺省 false */
|
||||
vision?: boolean;
|
||||
}
|
||||
|
||||
const SUMMARY_MARKERS = ["context summarization assistant", "structured context checkpoint summary"];
|
||||
@@ -70,6 +74,8 @@ export class FakeModelProvider {
|
||||
calls = 0;
|
||||
/** 最近一次请求看到的系统提示与工具名(测试断言用) */
|
||||
lastRequest: { transcript: string } | undefined;
|
||||
/** 历次请求里收到的图片内容块(测试断言截图确实交给了模型) */
|
||||
images: { mimeType: string; bytes: number }[] = [];
|
||||
|
||||
constructor(scripts: FakeScript[], options: FakeModelProviderOptions = {}) {
|
||||
this.scripts = scripts;
|
||||
@@ -82,6 +88,7 @@ export class FakeModelProvider {
|
||||
contextWindow: options.contextWindow ?? 64_000,
|
||||
maxTokens: options.maxTokens ?? 4096,
|
||||
reasoning: true,
|
||||
input: options.vision ? ["text", "image"] : ["text"],
|
||||
},
|
||||
],
|
||||
tokensPerSecond: 0,
|
||||
@@ -105,6 +112,10 @@ export class FakeModelProvider {
|
||||
const messages = context.messages as Message[];
|
||||
const transcript = messages.map(messageText).join("\n");
|
||||
this.lastRequest = { transcript };
|
||||
const last = messages[messages.length - 1];
|
||||
if (last?.role === "toolResult")
|
||||
for (const c of last.content as Array<{ type: string; data?: string; mimeType?: string }>)
|
||||
if (c.type === "image") this.images.push({ mimeType: c.mimeType ?? "", bytes: (c.data ?? "").length });
|
||||
const head = messages[0] ? messageText(messages[0]) : "";
|
||||
if (SUMMARY_MARKERS.some((m) => head.includes(m) || lastUserText(messages).includes(m))) {
|
||||
return fauxAssistantMessage(this.summaryText);
|
||||
@@ -203,6 +214,7 @@ function toolResults(messages: Message[]): FakeToolResult[] {
|
||||
isError: m.isError,
|
||||
text,
|
||||
...(json !== undefined ? { json } : {}),
|
||||
images: (m.content as Array<{ type: string }>).filter((c) => c.type === "image").length,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
|
||||
Reference in new issue
Block a user