feat(daemon): implement action routes and cookie management (Task 10)
- Add action endpoints: click/type/scroll/eval/wait/navigate - Add cookie endpoints: GET/POST/DELETE /pages/:id/cookies - Register actionRoutes in server.ts - 23 integration tests with mock BrowserManager
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
// Action routes — click/type/scroll/eval/wait/navigate + cookie management / 操作路由,点击/输入/滚动/执行/等待/导航 + Cookie 管理
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { BrowserManager } from '../browser-manager.js';
|
||||
import { safeStringify } from '@visionl/core';
|
||||
|
||||
type RouteHandler = (req: IncomingMessage, res: ServerResponse) => boolean;
|
||||
|
||||
function readBody(req: IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk; });
|
||||
req.on('end', () => resolve(body));
|
||||
});
|
||||
}
|
||||
|
||||
export function actionRoutes(bm: BrowserManager): RouteHandler {
|
||||
return (req, res) => {
|
||||
const url = new URL(req.url || '/', 'http://localhost');
|
||||
const path = url.pathname;
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
// Only handle /pages/:id/* routes / 只处理 /pages/:id/* 路由
|
||||
if (segments[0] !== 'pages' || segments.length < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const id = segments[1];
|
||||
const action = segments[2];
|
||||
|
||||
const entry = bm.getPage(id);
|
||||
if (!entry) {
|
||||
res.writeHead(404);
|
||||
res.end(safeStringify({ ok: false, error: { code: 'PAGE_NOT_FOUND', message: `Page "${id}" not found` } }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========== POST actions / POST 操作 ==========
|
||||
|
||||
// POST /pages/:id/click — click on a selector / 点击选择器
|
||||
if (req.method === 'POST' && action === 'click' && segments.length === 3) {
|
||||
readBody(req).then(async (body) => {
|
||||
try {
|
||||
const { selector } = JSON.parse(body);
|
||||
await entry.page.click(selector);
|
||||
res.writeHead(200);
|
||||
res.end(safeStringify({ ok: true, data: { success: true } }));
|
||||
} catch (err: any) {
|
||||
res.writeHead(500);
|
||||
res.end(safeStringify({ ok: false, error: { code: err.code || 'INTERNAL', message: err.message } }));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /pages/:id/type — fill text into a selector / 向选择器输入文本
|
||||
if (req.method === 'POST' && action === 'type' && segments.length === 3) {
|
||||
readBody(req).then(async (body) => {
|
||||
try {
|
||||
const { selector, text } = JSON.parse(body);
|
||||
await entry.page.fill(selector, text);
|
||||
res.writeHead(200);
|
||||
res.end(safeStringify({ ok: true, data: { success: true } }));
|
||||
} catch (err: any) {
|
||||
res.writeHead(500);
|
||||
res.end(safeStringify({ ok: false, error: { code: err.code || 'INTERNAL', message: err.message } }));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /pages/:id/scroll — scroll page by deltaY or to bottom / 滚动页面向下或到底部
|
||||
if (req.method === 'POST' && action === 'scroll' && segments.length === 3) {
|
||||
readBody(req).then(async (body) => {
|
||||
try {
|
||||
const { deltaY, toBottom } = JSON.parse(body);
|
||||
await entry.page.evaluate(({ deltaY: dy, toBottom: bottom }) => {
|
||||
if (bottom) {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
} else {
|
||||
window.scrollBy(0, dy || 0);
|
||||
}
|
||||
}, { deltaY: deltaY || 0, toBottom });
|
||||
res.writeHead(200);
|
||||
res.end(safeStringify({ ok: true, data: { success: true } }));
|
||||
} catch (err: any) {
|
||||
res.writeHead(500);
|
||||
res.end(safeStringify({ ok: false, error: { code: err.code || 'INTERNAL', message: err.message } }));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /pages/:id/eval — evaluate JavaScript in page context / 在页面中执行 JavaScript
|
||||
if (req.method === 'POST' && action === 'eval' && segments.length === 3) {
|
||||
readBody(req).then(async (body) => {
|
||||
try {
|
||||
const { code } = JSON.parse(body);
|
||||
const result = await entry.page.evaluate(code);
|
||||
res.writeHead(200);
|
||||
res.end(safeStringify({ ok: true, data: { result } }));
|
||||
} catch (err: any) {
|
||||
res.writeHead(500);
|
||||
res.end(safeStringify({ ok: false, error: { code: err.code || 'INTERNAL', message: err.message } }));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /pages/:id/wait — wait for selector or timeout / 等待选择器出现或超时
|
||||
if (req.method === 'POST' && action === 'wait' && segments.length === 3) {
|
||||
readBody(req).then(async (body) => {
|
||||
try {
|
||||
const { selector, timeout } = JSON.parse(body);
|
||||
if (selector) {
|
||||
await entry.page.waitForSelector(selector, { timeout: timeout || 30000 });
|
||||
} else {
|
||||
await entry.page.waitForTimeout(timeout || 1000);
|
||||
}
|
||||
res.writeHead(200);
|
||||
res.end(safeStringify({ ok: true, data: { success: true } }));
|
||||
} catch (err: any) {
|
||||
res.writeHead(500);
|
||||
res.end(safeStringify({ ok: false, error: { code: err.code || 'INTERNAL', message: err.message } }));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /pages/:id/navigate — navigate page to a new URL / 导航到新 URL
|
||||
if (req.method === 'POST' && action === 'navigate' && segments.length === 3) {
|
||||
readBody(req).then(async (body) => {
|
||||
try {
|
||||
const { url: pageUrl } = JSON.parse(body);
|
||||
await entry.page.goto(pageUrl, { waitUntil: 'domcontentloaded' });
|
||||
const title = await entry.page.title();
|
||||
res.writeHead(200);
|
||||
res.end(safeStringify({ ok: true, data: { url: pageUrl, title } }));
|
||||
} catch (err: any) {
|
||||
res.writeHead(500);
|
||||
res.end(safeStringify({ ok: false, error: { code: err.code || 'INTERNAL', message: err.message } }));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========== Cookie management / Cookie 管理 ==========
|
||||
|
||||
// GET /pages/:id/cookies — get all cookies for the page context / 获取页面上下文的所有 cookie
|
||||
if (req.method === 'GET' && action === 'cookies' && segments.length === 3) {
|
||||
entry.context.cookies().then((cookies) => {
|
||||
res.writeHead(200);
|
||||
res.end(safeStringify({ ok: true, data: { cookies } }));
|
||||
}).catch((err: any) => {
|
||||
res.writeHead(500);
|
||||
res.end(safeStringify({ ok: false, error: { code: err.code || 'INTERNAL', message: err.message } }));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /pages/:id/cookies — add a cookie to the page context / 向页面上下文添加 cookie
|
||||
if (req.method === 'POST' && action === 'cookies' && segments.length === 3) {
|
||||
readBody(req).then(async (body) => {
|
||||
try {
|
||||
const { name, value, domain, path: cookiePath, httpOnly, secure, sameSite } = JSON.parse(body);
|
||||
const cookie: any = { name, value };
|
||||
if (domain !== undefined) cookie.domain = domain;
|
||||
if (cookiePath !== undefined) cookie.path = cookiePath;
|
||||
if (httpOnly !== undefined) cookie.httpOnly = httpOnly;
|
||||
if (secure !== undefined) cookie.secure = secure;
|
||||
if (sameSite !== undefined) cookie.sameSite = sameSite;
|
||||
await entry.context.addCookies([cookie]);
|
||||
res.writeHead(200);
|
||||
res.end(safeStringify({ ok: true, data: { success: true } }));
|
||||
} catch (err: any) {
|
||||
res.writeHead(500);
|
||||
res.end(safeStringify({ ok: false, error: { code: err.code || 'INTERNAL', message: err.message } }));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// DELETE /pages/:id/cookies/:name — delete a specific cookie by name / 按名称删除特定 cookie
|
||||
if (req.method === 'DELETE' && action === 'cookies' && segments.length === 4) {
|
||||
const cookieName = segments[3];
|
||||
Promise.resolve().then(async () => {
|
||||
try {
|
||||
const existingCookies = await entry.context.cookies();
|
||||
const filtered = existingCookies.filter((c) => c.name !== cookieName);
|
||||
await entry.context.clearCookies();
|
||||
if (filtered.length > 0) {
|
||||
await entry.context.addCookies(filtered);
|
||||
}
|
||||
res.writeHead(200);
|
||||
res.end(safeStringify({ ok: true, data: { success: true } }));
|
||||
} catch (err: any) {
|
||||
res.writeHead(500);
|
||||
res.end(safeStringify({ ok: false, error: { code: err.code || 'INTERNAL', message: err.message } }));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user