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>`
|
few seconds and touches the live page; once you know which element hides the menu, `bsk hover <ref>`
|
||||||
is cheaper and more precise.
|
is cheaper and more precise.
|
||||||
|
|
||||||
When `bsk observe` renders `@eN surface ... [visual-only; ...]`, the ref identifies a rendered
|
When `bsk observe` renders
|
||||||
canvas surface, not an interactable DOM control. Use
|
`@eN surface ... [visual-only; requires=image-understanding; ...]`, the ref identifies rendered
|
||||||
`bsk screenshot --ref @eN --session <id>` to inspect its visible crop. Do not pass a visual surface
|
canvas content that is not represented by the text observation, not an interactable DOM control.
|
||||||
ref to click, fill, hover, or select; those commands intentionally reject screenshot-only refs.
|
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:
|
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 {
|
(ErrorCode::PermissionDenied, reason::REF_CAPABILITY_DENIED) => RenderInfo {
|
||||||
summary: "snapshot ref does not support this operation",
|
summary: "snapshot ref does not support this operation",
|
||||||
hint: Some(
|
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,
|
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 " +
|
? "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."
|
"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 " +
|
: "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; " +
|
"probes) with @eN refs for browser_interact actions. Visual-only surfaces require image " +
|
||||||
"DOM refs support interaction. Read-only: never submits input.";
|
"understanding and their refs are screenshot-only; keep using any DOM/AX semantics rendered " +
|
||||||
|
"alongside them. Read-only: never submits input.";
|
||||||
register(
|
register(
|
||||||
defineTool({
|
defineTool({
|
||||||
name,
|
name,
|
||||||
@@ -755,8 +756,11 @@ function defineBrowserOperations(deps: ToolDeps, register: DefinitionRegistrar):
|
|||||||
name: "inspect.screenshot",
|
name: "inspect.screenshot",
|
||||||
description:
|
description:
|
||||||
"Capture a PNG screenshot of the session's active tab, or crop to a snapshot ref element. " +
|
"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 " +
|
"This is the supported operation for visual surface refs emitted by inspect.observe. Only use " +
|
||||||
"the image itself when the deployment supports image input, otherwise a file path.",
|
"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: {
|
parameters: {
|
||||||
session: SESSION_PARAM,
|
session: SESSION_PARAM,
|
||||||
ref: {
|
ref: {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ describe("renderVom single-layer page", () => {
|
|||||||
const out = renderVom(input);
|
const out = renderVom(input);
|
||||||
|
|
||||||
expect(out.text).toContain(
|
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(out.refs).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -77,7 +77,7 @@ describe("renderVom single-layer page", () => {
|
|||||||
const out = renderVom(input);
|
const out = renderVom(input);
|
||||||
|
|
||||||
expect(out.text).toContain(
|
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(out.refs).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@@ -819,7 +819,7 @@ function emitVisualEntries(parentId: number | null, depth: number, state: Render
|
|||||||
const ref = `e${state.nextRef}`;
|
const ref = `e${state.nextRef}`;
|
||||||
const label = cleaned(surface.label) ?? `${surface.renderingKind} visual surface`;
|
const label = cleaned(surface.label) ?? `${surface.renderingKind} visual surface`;
|
||||||
const layers = surface.memberCount > 1 ? `; layers=${surface.memberCount}` : "";
|
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);
|
const nextTokens = state.tokens + estimateTokens(line);
|
||||||
if (nextTokens > state.maxTokens) {
|
if (nextTokens > state.maxTokens) {
|
||||||
state.truncated = true;
|
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(
|
function pushRenderChildren(
|
||||||
stack: Array<{ node: VomNode; depth: number }>,
|
stack: RenderTask[],
|
||||||
children: readonly VomNode[],
|
children: readonly VomNode[],
|
||||||
depth: number,
|
depth: number,
|
||||||
): void {
|
): void {
|
||||||
for (let index = children.length - 1; index >= 0; index -= 1) {
|
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 {
|
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);
|
pushRenderChildren(stack, children.get(null) ?? [], 1);
|
||||||
|
|
||||||
while (stack.length > 0 && !state.stopped) {
|
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)) {
|
if (!shouldRender(node)) {
|
||||||
|
stack.push({ kind: "visual", parentId: node.id, depth });
|
||||||
pushRenderChildren(stack, children.get(node.id) ?? [], depth);
|
pushRenderChildren(stack, children.get(node.id) ?? [], depth);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -935,9 +945,9 @@ function renderTree(children: Map<number | null, VomNode[]>, state: RenderState)
|
|||||||
emitVisualEntries(node.id, depth + 1, state);
|
emitVisualEntries(node.id, depth + 1, state);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
stack.push({ kind: "visual", parentId: node.id, depth: depth + 1 });
|
||||||
pushRenderChildren(stack, children.get(node.id) ?? [], depth + 1);
|
pushRenderChildren(stack, children.get(node.id) ?? [], depth + 1);
|
||||||
}
|
}
|
||||||
emitVisualEntries(parentId, depth, state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNodes(
|
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>`
|
few seconds and touches the live page; once you know which element hides the menu, `bsk hover <ref>`
|
||||||
is cheaper and more precise.
|
is cheaper and more precise.
|
||||||
|
|
||||||
When `bsk observe` renders `@eN surface ... [visual-only; ...]`, the ref identifies a rendered
|
When `bsk observe` renders
|
||||||
canvas surface, not an interactable DOM control. Use
|
`@eN surface ... [visual-only; requires=image-understanding; ...]`, the ref identifies rendered
|
||||||
`bsk screenshot --ref @eN --session <id>` to inspect its visible crop. Do not pass a visual surface
|
canvas content that is not represented by the text observation, not an interactable DOM control.
|
||||||
ref to click, fill, hover, or select; those commands intentionally reject screenshot-only refs.
|
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:
|
Escalate page reading only as needed:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user