mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 15:54:58 +00:00
refactor(browser): dedupe tab route profile and error handling
This commit is contained in:
@@ -1,144 +1,180 @@
|
|||||||
import type { BrowserRouteContext } from "../server-context.js";
|
import type { BrowserRouteContext, ProfileContext } from "../server-context.js";
|
||||||
import type { BrowserRouteRegistrar } from "./types.js";
|
import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js";
|
||||||
import { getProfileContext, jsonError, toNumber, toStringOrEmpty } from "./utils.js";
|
import { getProfileContext, jsonError, toNumber, toStringOrEmpty } from "./utils.js";
|
||||||
|
|
||||||
|
function resolveTabsProfileContext(
|
||||||
|
req: BrowserRequest,
|
||||||
|
res: BrowserResponse,
|
||||||
|
ctx: BrowserRouteContext,
|
||||||
|
) {
|
||||||
|
const profileCtx = getProfileContext(req, ctx);
|
||||||
|
if ("error" in profileCtx) {
|
||||||
|
jsonError(res, profileCtx.status, profileCtx.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return profileCtx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabsRouteError(
|
||||||
|
ctx: BrowserRouteContext,
|
||||||
|
res: BrowserResponse,
|
||||||
|
err: unknown,
|
||||||
|
opts?: { mapTabError?: boolean },
|
||||||
|
) {
|
||||||
|
if (opts?.mapTabError) {
|
||||||
|
const mapped = ctx.mapTabError(err);
|
||||||
|
if (mapped) {
|
||||||
|
return jsonError(res, mapped.status, mapped.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonError(res, 500, String(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withTabsProfileRoute(params: {
|
||||||
|
req: BrowserRequest;
|
||||||
|
res: BrowserResponse;
|
||||||
|
ctx: BrowserRouteContext;
|
||||||
|
mapTabError?: boolean;
|
||||||
|
run: (profileCtx: ProfileContext) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const profileCtx = resolveTabsProfileContext(params.req, params.res, params.ctx);
|
||||||
|
if (!profileCtx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await params.run(profileCtx);
|
||||||
|
} catch (err) {
|
||||||
|
handleTabsRouteError(params.ctx, params.res, err, { mapTabError: params.mapTabError });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function registerBrowserTabRoutes(app: BrowserRouteRegistrar, ctx: BrowserRouteContext) {
|
export function registerBrowserTabRoutes(app: BrowserRouteRegistrar, ctx: BrowserRouteContext) {
|
||||||
app.get("/tabs", async (req, res) => {
|
app.get("/tabs", async (req, res) => {
|
||||||
const profileCtx = getProfileContext(req, ctx);
|
await withTabsProfileRoute({
|
||||||
if ("error" in profileCtx) {
|
req,
|
||||||
return jsonError(res, profileCtx.status, profileCtx.error);
|
res,
|
||||||
}
|
ctx,
|
||||||
try {
|
run: async (profileCtx) => {
|
||||||
const reachable = await profileCtx.isReachable(300);
|
const reachable = await profileCtx.isReachable(300);
|
||||||
if (!reachable) {
|
if (!reachable) {
|
||||||
return res.json({ running: false, tabs: [] as unknown[] });
|
return res.json({ running: false, tabs: [] as unknown[] });
|
||||||
}
|
}
|
||||||
const tabs = await profileCtx.listTabs();
|
const tabs = await profileCtx.listTabs();
|
||||||
res.json({ running: true, tabs });
|
res.json({ running: true, tabs });
|
||||||
} catch (err) {
|
},
|
||||||
jsonError(res, 500, String(err));
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/tabs/open", async (req, res) => {
|
app.post("/tabs/open", async (req, res) => {
|
||||||
const profileCtx = getProfileContext(req, ctx);
|
|
||||||
if ("error" in profileCtx) {
|
|
||||||
return jsonError(res, profileCtx.status, profileCtx.error);
|
|
||||||
}
|
|
||||||
const url = toStringOrEmpty((req.body as { url?: unknown })?.url);
|
const url = toStringOrEmpty((req.body as { url?: unknown })?.url);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return jsonError(res, 400, "url is required");
|
return jsonError(res, 400, "url is required");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await profileCtx.ensureBrowserAvailable();
|
await withTabsProfileRoute({
|
||||||
const tab = await profileCtx.openTab(url);
|
req,
|
||||||
res.json(tab);
|
res,
|
||||||
} catch (err) {
|
ctx,
|
||||||
jsonError(res, 500, String(err));
|
run: async (profileCtx) => {
|
||||||
}
|
await profileCtx.ensureBrowserAvailable();
|
||||||
|
const tab = await profileCtx.openTab(url);
|
||||||
|
res.json(tab);
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/tabs/focus", async (req, res) => {
|
app.post("/tabs/focus", async (req, res) => {
|
||||||
const profileCtx = getProfileContext(req, ctx);
|
|
||||||
if ("error" in profileCtx) {
|
|
||||||
return jsonError(res, profileCtx.status, profileCtx.error);
|
|
||||||
}
|
|
||||||
const targetId = toStringOrEmpty((req.body as { targetId?: unknown })?.targetId);
|
const targetId = toStringOrEmpty((req.body as { targetId?: unknown })?.targetId);
|
||||||
if (!targetId) {
|
if (!targetId) {
|
||||||
return jsonError(res, 400, "targetId is required");
|
return jsonError(res, 400, "targetId is required");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
if (!(await profileCtx.isReachable(300))) {
|
await withTabsProfileRoute({
|
||||||
return jsonError(res, 409, "browser not running");
|
req,
|
||||||
}
|
res,
|
||||||
await profileCtx.focusTab(targetId);
|
ctx,
|
||||||
res.json({ ok: true });
|
mapTabError: true,
|
||||||
} catch (err) {
|
run: async (profileCtx) => {
|
||||||
const mapped = ctx.mapTabError(err);
|
if (!(await profileCtx.isReachable(300))) {
|
||||||
if (mapped) {
|
return jsonError(res, 409, "browser not running");
|
||||||
return jsonError(res, mapped.status, mapped.message);
|
}
|
||||||
}
|
await profileCtx.focusTab(targetId);
|
||||||
jsonError(res, 500, String(err));
|
res.json({ ok: true });
|
||||||
}
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete("/tabs/:targetId", async (req, res) => {
|
app.delete("/tabs/:targetId", async (req, res) => {
|
||||||
const profileCtx = getProfileContext(req, ctx);
|
|
||||||
if ("error" in profileCtx) {
|
|
||||||
return jsonError(res, profileCtx.status, profileCtx.error);
|
|
||||||
}
|
|
||||||
const targetId = toStringOrEmpty(req.params.targetId);
|
const targetId = toStringOrEmpty(req.params.targetId);
|
||||||
if (!targetId) {
|
if (!targetId) {
|
||||||
return jsonError(res, 400, "targetId is required");
|
return jsonError(res, 400, "targetId is required");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
if (!(await profileCtx.isReachable(300))) {
|
await withTabsProfileRoute({
|
||||||
return jsonError(res, 409, "browser not running");
|
req,
|
||||||
}
|
res,
|
||||||
await profileCtx.closeTab(targetId);
|
ctx,
|
||||||
res.json({ ok: true });
|
mapTabError: true,
|
||||||
} catch (err) {
|
run: async (profileCtx) => {
|
||||||
const mapped = ctx.mapTabError(err);
|
if (!(await profileCtx.isReachable(300))) {
|
||||||
if (mapped) {
|
return jsonError(res, 409, "browser not running");
|
||||||
return jsonError(res, mapped.status, mapped.message);
|
}
|
||||||
}
|
await profileCtx.closeTab(targetId);
|
||||||
jsonError(res, 500, String(err));
|
res.json({ ok: true });
|
||||||
}
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/tabs/action", async (req, res) => {
|
app.post("/tabs/action", async (req, res) => {
|
||||||
const profileCtx = getProfileContext(req, ctx);
|
|
||||||
if ("error" in profileCtx) {
|
|
||||||
return jsonError(res, profileCtx.status, profileCtx.error);
|
|
||||||
}
|
|
||||||
const action = toStringOrEmpty((req.body as { action?: unknown })?.action);
|
const action = toStringOrEmpty((req.body as { action?: unknown })?.action);
|
||||||
const index = toNumber((req.body as { index?: unknown })?.index);
|
const index = toNumber((req.body as { index?: unknown })?.index);
|
||||||
try {
|
|
||||||
if (action === "list") {
|
|
||||||
const reachable = await profileCtx.isReachable(300);
|
|
||||||
if (!reachable) {
|
|
||||||
return res.json({ ok: true, tabs: [] as unknown[] });
|
|
||||||
}
|
|
||||||
const tabs = await profileCtx.listTabs();
|
|
||||||
return res.json({ ok: true, tabs });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "new") {
|
await withTabsProfileRoute({
|
||||||
await profileCtx.ensureBrowserAvailable();
|
req,
|
||||||
const tab = await profileCtx.openTab("about:blank");
|
res,
|
||||||
return res.json({ ok: true, tab });
|
ctx,
|
||||||
}
|
mapTabError: true,
|
||||||
|
run: async (profileCtx) => {
|
||||||
if (action === "close") {
|
if (action === "list") {
|
||||||
const tabs = await profileCtx.listTabs();
|
const reachable = await profileCtx.isReachable(300);
|
||||||
const target = typeof index === "number" ? tabs[index] : tabs.at(0);
|
if (!reachable) {
|
||||||
if (!target) {
|
return res.json({ ok: true, tabs: [] as unknown[] });
|
||||||
return jsonError(res, 404, "tab not found");
|
}
|
||||||
|
const tabs = await profileCtx.listTabs();
|
||||||
|
return res.json({ ok: true, tabs });
|
||||||
}
|
}
|
||||||
await profileCtx.closeTab(target.targetId);
|
|
||||||
return res.json({ ok: true, targetId: target.targetId });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "select") {
|
if (action === "new") {
|
||||||
if (typeof index !== "number") {
|
await profileCtx.ensureBrowserAvailable();
|
||||||
return jsonError(res, 400, "index is required");
|
const tab = await profileCtx.openTab("about:blank");
|
||||||
|
return res.json({ ok: true, tab });
|
||||||
}
|
}
|
||||||
const tabs = await profileCtx.listTabs();
|
|
||||||
const target = tabs[index];
|
|
||||||
if (!target) {
|
|
||||||
return jsonError(res, 404, "tab not found");
|
|
||||||
}
|
|
||||||
await profileCtx.focusTab(target.targetId);
|
|
||||||
return res.json({ ok: true, targetId: target.targetId });
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonError(res, 400, "unknown tab action");
|
if (action === "close") {
|
||||||
} catch (err) {
|
const tabs = await profileCtx.listTabs();
|
||||||
const mapped = ctx.mapTabError(err);
|
const target = typeof index === "number" ? tabs[index] : tabs.at(0);
|
||||||
if (mapped) {
|
if (!target) {
|
||||||
return jsonError(res, mapped.status, mapped.message);
|
return jsonError(res, 404, "tab not found");
|
||||||
}
|
}
|
||||||
jsonError(res, 500, String(err));
|
await profileCtx.closeTab(target.targetId);
|
||||||
}
|
return res.json({ ok: true, targetId: target.targetId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "select") {
|
||||||
|
if (typeof index !== "number") {
|
||||||
|
return jsonError(res, 400, "index is required");
|
||||||
|
}
|
||||||
|
const tabs = await profileCtx.listTabs();
|
||||||
|
const target = tabs[index];
|
||||||
|
if (!target) {
|
||||||
|
return jsonError(res, 404, "tab not found");
|
||||||
|
}
|
||||||
|
await profileCtx.focusTab(target.targetId);
|
||||||
|
return res.json({ ok: true, targetId: target.targetId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonError(res, 400, "unknown tab action");
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user