fix(dsh-plugin-browserskill): serve observation thumbnails over the plugin's own route

The session-authorized client RPC (readAttachment) refuses images that
no session log references — observation frames are plugin-owned runtime
data, so every overlay thumbnail failed with ATTACHMENT_NOT_REFERENCED.
The host now keeps the full attachment ref per frame and serves bytes
through GET /bsk-observation/thumbnail/<id> (verified readImage path);
the client loader is a plain fetch of that route.
This commit is contained in:
BB-fat
2026-08-14 06:49:15 +00:00
parent bc5d9610a3
commit 461d204ca3
4 changed files with 101 additions and 22 deletions
@@ -5,9 +5,10 @@
* the event allowlist lives in dsh-api-remotes), so the plugin serves its
* observation channel through the documented `webServer` route seam instead:
*
* GET /bsk-observation/state → { sessions: SessionObservation[] }
* GET /bsk-observation/state → { sessions, available }
* GET /bsk-observation/events → SSE stream of ObservationEvent
* POST /bsk-observation/interrupt → body {sessionId?} → {interrupted: boolean}
* GET /bsk-observation/thumbnail/<attachmentId> → image bytes
*
* Routes exist only when a webServer service is mounted (web composition);
* headless deployments skip registration entirely.
@@ -112,6 +113,28 @@ export function registerObservationRoutes(
});
},
}),
webServer.register({
kind: "prefix",
path: `${ROUTE_BASE}/thumbnail/`,
handler: async (req, res) => {
if (req.method !== "GET") {
sendJson(res, 405, { error: "method not allowed" });
return;
}
const pathname = decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname);
const attachmentId = pathname.slice(`${ROUTE_BASE}/thumbnail/`.length);
const frame = await observation.readThumbnail(attachmentId);
if (frame === undefined) {
sendJson(res, 404, { error: "unknown thumbnail" });
return;
}
res.writeHead(200, {
"content-type": frame.mediaType,
"cache-control": "no-cache",
});
res.end(Buffer.from(frame.data));
},
}),
];
return () => {
for (const dispose of disposers) dispose();