mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(vom): two channels to deal with canvas
This commit is contained in:
@@ -73,10 +73,15 @@ CSS-only hover menu looks like from here. It hovers a bounded set of likely trig
|
||||
few seconds and touches the live page; once you know which element hides the menu, `bsk hover <ref>`
|
||||
is cheaper and more precise.
|
||||
|
||||
When `bsk observe` renders `@eN surface ... [visual-only; ...]`, the ref identifies a rendered
|
||||
canvas surface, not an interactable DOM control. Use
|
||||
`bsk screenshot --ref @eN --session <id>` to inspect its visible crop. Do not pass a visual surface
|
||||
ref to click, fill, hover, or select; those commands intentionally reject screenshot-only refs.
|
||||
When `bsk observe` renders
|
||||
`@eN surface ... [visual-only; requires=image-understanding; ...]`, the ref identifies rendered
|
||||
canvas content that is not represented by the text observation, not an interactable DOM control.
|
||||
Keep using any reliable DOM/AX text and controls that appear alongside it. If you can actually
|
||||
inspect image output, use `bsk screenshot --ref @eN --session <id>` to obtain the visible crop. If
|
||||
you lack multimodal or image-reading capability, do not take a screenshot and pretend to know its
|
||||
contents, and never guess coordinates; tell the user that they need to switch to a model with
|
||||
image-understanding capability. Do not pass a visual surface ref to click, fill, hover, or select;
|
||||
those commands intentionally reject screenshot-only refs.
|
||||
|
||||
Escalate page reading only as needed:
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ pub fn info_for_error(code: ErrorCode, data: Option<&serde_json::Value>) -> Rend
|
||||
(ErrorCode::PermissionDenied, reason::REF_CAPABILITY_DENIED) => RenderInfo {
|
||||
summary: "snapshot ref does not support this operation",
|
||||
hint: Some(
|
||||
"visual surface refs are screenshot-only; run `bsk screenshot --ref <ref> --session <id>`",
|
||||
"visual surface refs are screenshot-only; inspect `bsk screenshot --ref <ref> --session <id>` with image-understanding capability",
|
||||
),
|
||||
exit_code: base.exit_code,
|
||||
},
|
||||
|
||||
@@ -489,8 +489,9 @@ function defineBrowserOperations(deps: ToolDeps, register: DefinitionRegistrar):
|
||||
? "Capture an indented aria-tree snapshot of the session's active tab. Interactive elements " +
|
||||
"carry @eN refs for browser_interact actions. Prefer browser_inspect action=observe for a richer semantic view."
|
||||
: "Produce a semantic VOM observation of the session's active tab (roles, states, perception " +
|
||||
"probes) with @eN refs for browser_interact actions. Visual surface refs are screenshot-only; " +
|
||||
"DOM refs support interaction. Read-only: never submits input.";
|
||||
"probes) with @eN refs for browser_interact actions. Visual-only surfaces require image " +
|
||||
"understanding and their refs are screenshot-only; keep using any DOM/AX semantics rendered " +
|
||||
"alongside them. Read-only: never submits input.";
|
||||
register(
|
||||
defineTool({
|
||||
name,
|
||||
@@ -755,8 +756,11 @@ function defineBrowserOperations(deps: ToolDeps, register: DefinitionRegistrar):
|
||||
name: "inspect.screenshot",
|
||||
description:
|
||||
"Capture a PNG screenshot of the session's active tab, or crop to a snapshot ref element. " +
|
||||
"This is the supported operation for visual surface refs emitted by browser_observe. Returns " +
|
||||
"the image itself when the deployment supports image input, otherwise a file path.",
|
||||
"This is the supported operation for visual surface refs emitted by inspect.observe. Only use " +
|
||||
"it to interpret a visual-only surface when you can inspect image output. If you lack multimodal " +
|
||||
"or image-reading capability, tell the user to switch to a model with image understanding; do " +
|
||||
"not guess content or coordinates. " +
|
||||
"Returns the image itself when the deployment supports image input, otherwise a file path.",
|
||||
parameters: {
|
||||
session: SESSION_PARAM,
|
||||
ref: {
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("renderVom single-layer page", () => {
|
||||
const out = renderVom(input);
|
||||
|
||||
expect(out.text).toContain(
|
||||
'@e1 surface "Data grid" [rendering=canvas; layers=2; visual-only; use: bsk screenshot --ref @e1]',
|
||||
'@e1 surface "Data grid" [rendering=canvas; layers=2; visual-only; requires=image-understanding; use: bsk screenshot --ref @e1]',
|
||||
);
|
||||
expect(out.refs).toContainEqual(
|
||||
expect.objectContaining({
|
||||
@@ -77,7 +77,7 @@ describe("renderVom single-layer page", () => {
|
||||
const out = renderVom(input);
|
||||
|
||||
expect(out.text).toContain(
|
||||
'@e1 surface "canvas visual surface" [rendering=canvas; visual-only; use: bsk screenshot --ref @e1]',
|
||||
'@e1 surface "canvas visual surface" [rendering=canvas; visual-only; requires=image-understanding; use: bsk screenshot --ref @e1]',
|
||||
);
|
||||
expect(out.refs).toContainEqual(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -819,7 +819,7 @@ function emitVisualEntries(parentId: number | null, depth: number, state: Render
|
||||
const ref = `e${state.nextRef}`;
|
||||
const label = cleaned(surface.label) ?? `${surface.renderingKind} visual surface`;
|
||||
const layers = surface.memberCount > 1 ? `; layers=${surface.memberCount}` : "";
|
||||
const line = `${" ".repeat(depth)}@${ref} surface ${JSON.stringify(label)} [rendering=${surface.renderingKind}${layers}; visual-only; use: bsk screenshot --ref @${ref}]`;
|
||||
const line = `${" ".repeat(depth)}@${ref} surface ${JSON.stringify(label)} [rendering=${surface.renderingKind}${layers}; visual-only; requires=image-understanding; use: bsk screenshot --ref @${ref}]`;
|
||||
const nextTokens = state.tokens + estimateTokens(line);
|
||||
if (nextTokens > state.maxTokens) {
|
||||
state.truncated = true;
|
||||
@@ -872,24 +872,34 @@ function emitActiveScopeBlock(scope: ActiveScopeBlock, depth: number, state: Ren
|
||||
}
|
||||
}
|
||||
|
||||
type RenderTask =
|
||||
| { kind: "node"; node: VomNode; depth: number }
|
||||
| { kind: "visual"; parentId: number | null; depth: number };
|
||||
|
||||
function pushRenderChildren(
|
||||
stack: Array<{ node: VomNode; depth: number }>,
|
||||
stack: RenderTask[],
|
||||
children: readonly VomNode[],
|
||||
depth: number,
|
||||
): void {
|
||||
for (let index = children.length - 1; index >= 0; index -= 1) {
|
||||
stack.push({ node: children[index], depth });
|
||||
stack.push({ kind: "node", node: children[index], depth });
|
||||
}
|
||||
}
|
||||
|
||||
function renderTree(children: Map<number | null, VomNode[]>, state: RenderState): void {
|
||||
const stack: Array<{ node: VomNode; depth: number }> = [];
|
||||
const stack: RenderTask[] = [{ kind: "visual", parentId: null, depth: 1 }];
|
||||
pushRenderChildren(stack, children.get(null) ?? [], 1);
|
||||
|
||||
while (stack.length > 0 && !state.stopped) {
|
||||
const { node, depth } = stack.pop() as { node: VomNode; depth: number };
|
||||
const task = stack.pop() as RenderTask;
|
||||
if (task.kind === "visual") {
|
||||
emitVisualEntries(task.parentId, task.depth, state);
|
||||
continue;
|
||||
}
|
||||
const { node, depth } = task;
|
||||
|
||||
if (!shouldRender(node)) {
|
||||
stack.push({ kind: "visual", parentId: node.id, depth });
|
||||
pushRenderChildren(stack, children.get(node.id) ?? [], depth);
|
||||
continue;
|
||||
}
|
||||
@@ -935,9 +945,9 @@ function renderTree(children: Map<number | null, VomNode[]>, state: RenderState)
|
||||
emitVisualEntries(node.id, depth + 1, state);
|
||||
continue;
|
||||
}
|
||||
stack.push({ kind: "visual", parentId: node.id, depth: depth + 1 });
|
||||
pushRenderChildren(stack, children.get(node.id) ?? [], depth + 1);
|
||||
}
|
||||
emitVisualEntries(parentId, depth, state);
|
||||
}
|
||||
|
||||
function renderNodes(
|
||||
|
||||
+9
-4
@@ -73,10 +73,15 @@ CSS-only hover menu looks like from here. It hovers a bounded set of likely trig
|
||||
few seconds and touches the live page; once you know which element hides the menu, `bsk hover <ref>`
|
||||
is cheaper and more precise.
|
||||
|
||||
When `bsk observe` renders `@eN surface ... [visual-only; ...]`, the ref identifies a rendered
|
||||
canvas surface, not an interactable DOM control. Use
|
||||
`bsk screenshot --ref @eN --session <id>` to inspect its visible crop. Do not pass a visual surface
|
||||
ref to click, fill, hover, or select; those commands intentionally reject screenshot-only refs.
|
||||
When `bsk observe` renders
|
||||
`@eN surface ... [visual-only; requires=image-understanding; ...]`, the ref identifies rendered
|
||||
canvas content that is not represented by the text observation, not an interactable DOM control.
|
||||
Keep using any reliable DOM/AX text and controls that appear alongside it. If you can actually
|
||||
inspect image output, use `bsk screenshot --ref @eN --session <id>` to obtain the visible crop. If
|
||||
you lack multimodal or image-reading capability, do not take a screenshot and pretend to know its
|
||||
contents, and never guess coordinates; tell the user that they need to switch to a model with
|
||||
image-understanding capability. Do not pass a visual surface ref to click, fill, hover, or select;
|
||||
those commands intentionally reject screenshot-only refs.
|
||||
|
||||
Escalate page reading only as needed:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user