mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fee7c0983c | ||
|
|
51473c01ff | ||
|
|
49744233f7 | ||
|
|
95571f8b70 | ||
|
|
8344e311fa |
@@ -23,7 +23,7 @@ describe("BorrowConfirmationOverlay", () => {
|
||||
id: "req-1",
|
||||
isActiveTab: true,
|
||||
tabTitle: "Example",
|
||||
timeoutMs: 5000,
|
||||
timeoutMs: 60_000,
|
||||
onAllow,
|
||||
onDeny,
|
||||
},
|
||||
@@ -32,7 +32,7 @@ describe("BorrowConfirmationOverlay", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText("允许借用标签页?")).toBeTruthy();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
});
|
||||
@@ -66,7 +66,7 @@ describe("BorrowConfirmationOverlay", () => {
|
||||
id: "req-fallback",
|
||||
isActiveTab: true,
|
||||
tabTitle: "Example",
|
||||
timeoutMs: 5000,
|
||||
timeoutMs: 60_000,
|
||||
onAllow,
|
||||
onDeny,
|
||||
},
|
||||
@@ -74,7 +74,7 @@ describe("BorrowConfirmationOverlay", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
});
|
||||
|
||||
@@ -127,6 +127,38 @@ describe("requestBorrowConfirmation", () => {
|
||||
expect(message.requestId.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("proactively focuses the user window before sending the overlay message", async () => {
|
||||
tabs.get.mockResolvedValueOnce({ id: 42, title: "Tab" });
|
||||
windows.getLastFocused.mockResolvedValueOnce(
|
||||
userWindowWithActiveTab({ windowId: 77, tabId: 7, url: "https://app.example/" }),
|
||||
);
|
||||
tabs.sendMessage.mockResolvedValueOnce({ type: "borrow-response", allowed: true });
|
||||
|
||||
const pending = requestBorrowConfirmation(42, {
|
||||
deps: { tabs, windows, notifications },
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await pending;
|
||||
|
||||
expect(windows.update).toHaveBeenCalledWith(77, { focused: true });
|
||||
});
|
||||
|
||||
it("still proceeds when proactive window focus fails", async () => {
|
||||
tabs.get.mockResolvedValueOnce({ id: 42, title: "Tab" });
|
||||
windows.getLastFocused.mockResolvedValueOnce(
|
||||
userWindowWithActiveTab({ windowId: 77, tabId: 7, url: "https://app.example/" }),
|
||||
);
|
||||
windows.update.mockRejectedValueOnce(new Error("window gone"));
|
||||
tabs.sendMessage.mockResolvedValueOnce({ type: "borrow-response", allowed: true });
|
||||
|
||||
const pending = requestBorrowConfirmation(42, {
|
||||
deps: { tabs, windows, notifications },
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(await pending).toBe(true);
|
||||
});
|
||||
|
||||
it("sends confirmation to the borrow target when it is the active tab", async () => {
|
||||
tabs.get.mockResolvedValueOnce({ id: 42, title: "Active Tab" });
|
||||
windows.getLastFocused.mockResolvedValueOnce(
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface BorrowResponseMessage {
|
||||
allowed: boolean;
|
||||
}
|
||||
|
||||
export const CONFIRMATION_TIMEOUT_MS = 5000;
|
||||
export const CONFIRMATION_TIMEOUT_MS = 60_000;
|
||||
const EXIT_ANIMATION_MS = 150;
|
||||
/** Matches BorrowConfirmationOverlay progress ring/bar transition (duration-1000). */
|
||||
export const PROGRESS_TRANSITION_MS = 1000;
|
||||
@@ -480,6 +480,13 @@ export async function requestBorrowConfirmation(
|
||||
settle(false);
|
||||
}, BACKGROUND_TIMEOUT_MS);
|
||||
|
||||
// Bring the user window to the front so the overlay is visible even
|
||||
// when the Agent Window has stolen focus. This is fire-and-forget: if
|
||||
// it fails the overlay / OS notification still give the user a path.
|
||||
void windowsApi.update(notificationAnchor.windowId, { focused: true }).catch((err) => {
|
||||
console.debug("[bsk borrow] proactive focus of user window failed", err);
|
||||
});
|
||||
|
||||
// Surface the OS notification *before* messaging any candidate so the
|
||||
// user has a parallel signal even if every content script is missing.
|
||||
// The Allow / Deny buttons let the user authorize explicitly when the
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
RiArrowDownSLine,
|
||||
RiCheckLine,
|
||||
RiCloseCircleLine,
|
||||
RiCloseLine,
|
||||
RiErrorWarningLine,
|
||||
RiPictureInPicture2Line,
|
||||
RiPushpinFill,
|
||||
@@ -28,6 +29,7 @@ const asIcon = (component: unknown): IconComponent => component as IconComponent
|
||||
const IconStop = asIcon(RiStopCircleLine);
|
||||
const IconPip = asIcon(RiPictureInPicture2Line);
|
||||
const IconDown = asIcon(RiArrowDownSLine);
|
||||
const IconClose = asIcon(RiCloseLine);
|
||||
const IconWarn = asIcon(RiErrorWarningLine);
|
||||
const IconPin = asIcon(RiPushpinFill);
|
||||
const IconCloseSession = asIcon(RiCloseCircleLine);
|
||||
@@ -320,6 +322,7 @@ export function OverlayBody(props: {
|
||||
now: number;
|
||||
onPopOut?: (() => void) | undefined;
|
||||
onCollapse?: (() => void) | undefined;
|
||||
onClosePip?: (() => void) | undefined;
|
||||
inPip: boolean;
|
||||
onHeaderPointerDown?: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
}) {
|
||||
@@ -333,6 +336,7 @@ export function OverlayBody(props: {
|
||||
now,
|
||||
onPopOut,
|
||||
onCollapse,
|
||||
onClosePip,
|
||||
inPip,
|
||||
onHeaderPointerDown,
|
||||
} = props;
|
||||
@@ -389,6 +393,16 @@ export function OverlayBody(props: {
|
||||
<IconDown size={14} />
|
||||
</button>
|
||||
) : null}
|
||||
{onClosePip !== undefined ? (
|
||||
<button
|
||||
type="button"
|
||||
className={css["icon-button"]}
|
||||
aria-label="Close mini window"
|
||||
onClick={onClosePip}
|
||||
>
|
||||
<IconClose size={14} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={css.stage}>
|
||||
{displayUrl !== undefined ? (
|
||||
@@ -572,6 +586,7 @@ export function ObservationOverlay({ store }: { store: ObservationClientStore })
|
||||
: undefined
|
||||
}
|
||||
onCollapse={pipWindow === null ? () => setCollapsed(true) : undefined}
|
||||
onClosePip={pipWindow !== null ? () => pipWindow.close() : undefined}
|
||||
onHeaderPointerDown={pipWindow === null ? beginMove : undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -149,6 +149,7 @@ export function ObservationSidebarTab({
|
||||
now={now}
|
||||
inPip={pipWindow !== null}
|
||||
onPopOut={pipWindow === null && pipSupported ? () => popOut() : undefined}
|
||||
onClosePip={pipWindow !== null ? () => pipWindow.close() : undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ interface LlmLike {
|
||||
* @param data - first bytes of the capture.
|
||||
* @returns the detected media type, or undefined when unrecognized.
|
||||
*/
|
||||
function sniffImageMediaType(data: Uint8Array): string | undefined {
|
||||
export function sniffImageMediaType(data: Uint8Array): string | undefined {
|
||||
if (data.length >= 4) {
|
||||
// PNG signature: 89 50 4E 47 (0D 0A 1A 0A)
|
||||
if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { readFile, unlink } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import { sniffImageMediaType } from "./image";
|
||||
import type { KeyedExecutor } from "./queue";
|
||||
import { BskError, type BskRunner, parseBskJson, runWithSessionBusyRetry } from "./runner";
|
||||
import type { SessionRegistry } from "./sessions";
|
||||
@@ -467,7 +468,7 @@ export class ObservationService {
|
||||
const seq = (this.seqs.get(sessionId) ?? 0) + 1;
|
||||
this.seqs.set(sessionId, seq);
|
||||
const id = `obs-${sessionId}-${seq}`;
|
||||
this.frames.set(id, { data, mediaType: "image/png" });
|
||||
this.frames.set(id, { data, mediaType: sniffImageMediaType(data) ?? "image/png" });
|
||||
this.hashes.set(sessionId, hash);
|
||||
const ring = this.rings.get(sessionId) ?? [];
|
||||
ring.push(id);
|
||||
|
||||
@@ -298,6 +298,33 @@ describe("ObservationOverlay", () => {
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("shows a close button inside the PiP window that closes it", async () => {
|
||||
const h = makeHarness([BUSY]);
|
||||
const pipDoc = document.implementation.createHTMLDocument("pip");
|
||||
const close = vi.fn();
|
||||
const listeners = new Map<string, (() => void)[]>();
|
||||
const requestWindow = vi.fn(async () => {
|
||||
return {
|
||||
document: pipDoc,
|
||||
addEventListener: (name: string, fn: () => void) => {
|
||||
listeners.set(name, [...(listeners.get(name) ?? []), fn]);
|
||||
},
|
||||
close,
|
||||
} as unknown as Window;
|
||||
});
|
||||
(window as unknown as Record<string, unknown>).documentPictureInPicture = { requestWindow };
|
||||
render(<ObservationOverlay store={h.store} />);
|
||||
const popout = await screen.findByRole("button", { name: /Pop out/ });
|
||||
fireEvent.click(popout);
|
||||
await waitFor(() => expect(pipDoc.body.textContent).toContain("s1"));
|
||||
const closeBtn = pipDoc.body.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Close mini window"]',
|
||||
);
|
||||
expect(closeBtn).not.toBeNull();
|
||||
closeBtn!.click();
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders the strip for two sessions and pins focus on click", async () => {
|
||||
const older: SessionObservation = {
|
||||
sessionId: "s1",
|
||||
|
||||
@@ -335,6 +335,18 @@ describe("thumbnail cadence", () => {
|
||||
await waitFor(() => service.getState()[0]?.thumbnailAttachmentId === "obs-s1-1");
|
||||
expect((await service.readThumbnail("obs-s1-1"))?.mediaType).toBe("image/png");
|
||||
});
|
||||
|
||||
it("detects JPEG thumbnail bytes instead of hardcoding image/png", async () => {
|
||||
const scheduler = fakeScheduler();
|
||||
const runner = fakeRunner({
|
||||
screenshotBytes: () => new Uint8Array([0xff, 0xd8, 0xff, 0xdb, 0x00, 0x01]),
|
||||
});
|
||||
const { service } = setup({ runner, scheduler });
|
||||
service.addSession("s1");
|
||||
scheduler.runNext();
|
||||
await waitFor(() => service.getState()[0]?.thumbnailAttachmentId === "obs-s1-1");
|
||||
expect((await service.readThumbnail("obs-s1-1"))?.mediaType).toBe("image/jpeg");
|
||||
});
|
||||
});
|
||||
|
||||
describe("observation traffic isolation", () => {
|
||||
|
||||
Reference in New Issue
Block a user