chore: release v0.98.0

This commit is contained in:
coso
2026-03-29 14:15:34 +08:00
parent dfbbb3d204
commit deceacc699
435 changed files with 35673 additions and 8535 deletions
+249 -3
View File
@@ -5,9 +5,13 @@ import process from "node:process";
const DEFAULTS = {
server: "ws://127.0.0.1:8787",
healthUrl: "http://127.0.0.1:3030/health",
invokeUrl: "http://127.0.0.1:3030/invoke",
key: "",
profile: "default",
timeoutMs: 15000,
intervalMs: 1000,
verifyForceDisconnect: true,
};
function parseArgs(argv) {
@@ -19,6 +23,16 @@ function parseArgs(argv) {
i += 1;
continue;
}
if (arg === "--health-url" && argv[i + 1]) {
args.healthUrl = argv[i + 1];
i += 1;
continue;
}
if (arg === "--invoke-url" && argv[i + 1]) {
args.invokeUrl = argv[i + 1];
i += 1;
continue;
}
if (arg === "--key" && argv[i + 1]) {
args.key = argv[i + 1];
i += 1;
@@ -34,6 +48,15 @@ function parseArgs(argv) {
i += 1;
continue;
}
if (arg === "--interval-ms" && argv[i + 1]) {
args.intervalMs = Number(argv[i + 1]);
i += 1;
continue;
}
if (arg === "--skip-force-disconnect") {
args.verifyForceDisconnect = false;
continue;
}
if (arg === "--help" || arg === "-h") {
printHelp();
process.exit(0);
@@ -51,13 +74,18 @@ Lime Chrome Bridge E2E 联调脚本
选项:
--server <ws_url> 服务地址,默认 ws://127.0.0.1:8787
--health-url <url> DevBridge 健康检查地址,默认 http://127.0.0.1:3030/health
--invoke-url <url> DevBridge invoke 地址,默认 http://127.0.0.1:3030/invoke
--key <api_key> Lime API Key(必填)
--profile <profile_key> profileKey,默认 default
--timeout-ms <ms> 单步超时毫秒,默认 15000
--interval-ms <ms> 状态轮询间隔毫秒,默认 1000
--skip-force-disconnect 跳过桌面端主动断开验证,仅校验 WebSocket 命令链路
-h, --help 显示帮助
示例:
node scripts/chrome-bridge-e2e.mjs --server ws://127.0.0.1:8787 --key proxy_cast --profile default
node scripts/chrome-bridge-e2e.mjs --server ws://127.0.0.1:8787 --key proxy_cast --profile default --skip-force-disconnect
`);
}
@@ -76,6 +104,124 @@ function normalizeServer(server) {
.replace(/\/$/, "");
}
function appendProfileKey(url, profileKey) {
const endpoint = new URL(url);
endpoint.searchParams.set("profileKey", profileKey);
return endpoint.toString();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
async function invoke(invokeUrl, cmd, args) {
const response = await fetch(invokeUrl, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ cmd, args }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const payload = await response.json();
if (payload?.error) {
throw new Error(String(payload.error));
}
return payload?.result;
}
async function waitForHealth(options) {
const startedAt = Date.now();
let lastError = null;
while (Date.now() - startedAt < options.timeoutMs) {
try {
const response = await fetch(options.healthUrl, { method: "GET" });
const payload = await response.json();
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
console.log(
`[E2E] DevBridge 已就绪 (${Date.now() - startedAt}ms)${
payload?.status ? ` status=${payload.status}` : ""
}`,
);
return;
} catch (error) {
lastError = error;
await sleep(options.intervalMs);
}
}
const detail =
lastError instanceof Error
? lastError.message
: String(lastError || "unknown error");
throw new Error(
`[E2E] DevBridge 未就绪,请先启动 npm run tauri:dev 或 npm run tauri:dev:headless。最后错误: ${detail}`,
);
}
function summarizeStatus(status) {
if (!status || typeof status !== "object") {
return "unknown";
}
return `observer=${status.observer_count ?? "?"}, control=${
status.control_count ?? "?"
}, pending=${status.pending_command_count ?? "?"}`;
}
function isStatusEmpty(status) {
return (
Number(status?.observer_count || 0) === 0 &&
Number(status?.control_count || 0) === 0 &&
Number(status?.pending_command_count || 0) === 0
);
}
function hasObserverForProfile(status, profileKey) {
return (status?.observers || []).some(
(observer) => observer?.profile_key === profileKey,
);
}
async function getBridgeStatus(invokeUrl) {
return invoke(invokeUrl, "get_chrome_bridge_status");
}
async function waitForStatus(invokeUrl, predicate, timeoutMs, intervalMs, desc) {
const startedAt = Date.now();
let lastStatus = null;
while (Date.now() - startedAt < timeoutMs) {
lastStatus = await getBridgeStatus(invokeUrl);
if (predicate(lastStatus)) {
return lastStatus;
}
await sleep(intervalMs);
}
throw new Error(
`[E2E] 等待桥接状态超时(${timeoutMs}ms): ${desc}\n最近状态: ${JSON.stringify(
lastStatus,
null,
2,
)}`,
);
}
function toText(data) {
if (typeof data === "string") return data;
if (Buffer.isBuffer(data)) return data.toString("utf8");
@@ -186,6 +332,9 @@ async function closeClient(client) {
}
async function main() {
if (typeof fetch !== "function") {
throw new Error("当前 Node 运行时不支持 fetch,请使用 Node 18+");
}
assertGlobalWebSocket();
const args = parseArgs(process.argv.slice(2));
@@ -196,12 +345,44 @@ async function main() {
if (!Number.isFinite(args.timeoutMs) || args.timeoutMs < 1000) {
throw new Error("--timeout-ms 必须是 >= 1000 的数字");
}
if (!Number.isFinite(args.intervalMs) || args.intervalMs < 100) {
throw new Error("--interval-ms 必须是 >= 100 的数字");
}
const server = normalizeServer(args.server);
const key = encodeURIComponent(args.key);
const profile = encodeURIComponent(args.profile || "default");
const observerUrl = `${server}/lime-chrome-observer/Lime_Key=${key}?profileKey=${profile}`;
const controlUrl = `${server}/lime-chrome-control/Lime_Key=${key}`;
const profile = String(args.profile || "default").trim() || "default";
let observerBaseUrl = `${server}/lime-chrome-observer/${key}`;
let controlUrl = `${server}/lime-chrome-control/${key}`;
let baselineStatus = null;
if (args.verifyForceDisconnect) {
console.log("[E2E] invoke :", args.invokeUrl);
await waitForHealth(args);
const endpointInfo = await invoke(args.invokeUrl, "get_chrome_bridge_endpoint_info");
if (typeof endpointInfo?.observer_ws_url === "string" && endpointInfo.observer_ws_url) {
observerBaseUrl = endpointInfo.observer_ws_url;
}
if (typeof endpointInfo?.control_ws_url === "string" && endpointInfo.control_ws_url) {
controlUrl = endpointInfo.control_ws_url;
}
if (
typeof endpointInfo?.bridge_key === "string" &&
endpointInfo.bridge_key &&
endpointInfo.bridge_key !== args.key
) {
console.warn(
`[E2E] 传入 key 与当前运行态 bridge_key 不一致,将以运行态 endpoint 为准: cli=${args.key} runtime=${endpointInfo.bridge_key}`,
);
}
baselineStatus = await getBridgeStatus(args.invokeUrl);
console.log("[E2E] 基线状态:", summarizeStatus(baselineStatus));
if (!isStatusEmpty(baselineStatus)) {
console.warn(
"[E2E] 检测到已有桥接连接,本次将校验 force_disconnect 消息和目标 profile 清理,但不强制要求全局状态归零。",
);
}
}
const observerUrl = appendProfileKey(observerBaseUrl, profile);
console.log("[E2E] observer:", observerUrl);
console.log("[E2E] control :", controlUrl);
@@ -341,6 +522,71 @@ async function main() {
);
console.log("[E2E] 非 wait_for_page_info 命令链路通过");
if (args.verifyForceDisconnect) {
const forceDisconnectObserver = waitForMessage(
observer,
(msg) => msg.type === "force_disconnect",
args.timeoutMs,
"observer 收到 force_disconnect",
);
const forceDisconnectControl = waitForMessage(
control,
(msg) => msg.type === "force_disconnect",
args.timeoutMs,
"control 收到 force_disconnect",
);
const disconnectResult = await invoke(
args.invokeUrl,
"disconnect_browser_connector_session",
{
profileKey: args.profile,
},
);
assert(
Number(disconnectResult?.disconnected_observer_count || 0) >= 1,
`disconnect_browser_connector_session 未断开 observer: ${JSON.stringify(
disconnectResult,
null,
2,
)}`,
);
assert(
Number(disconnectResult?.disconnected_control_count || 0) >= 1,
`disconnect_browser_connector_session 未断开 control: ${JSON.stringify(
disconnectResult,
null,
2,
)}`,
);
await Promise.all([forceDisconnectObserver, forceDisconnectControl]);
console.log("[E2E] force_disconnect 消息链路通过");
const finalStatus = await waitForStatus(
args.invokeUrl,
(status) =>
isStatusEmpty(baselineStatus)
? isStatusEmpty(status)
: !hasObserverForProfile(status, args.profile),
args.timeoutMs,
args.intervalMs,
isStatusEmpty(baselineStatus)
? "桥接状态归零"
: `profile=${args.profile} observer 已清理`,
);
if (isStatusEmpty(baselineStatus)) {
console.log("[E2E] force_disconnect 后状态归零:", summarizeStatus(finalStatus));
} else {
console.log(
"[E2E] force_disconnect 后目标 profile 已清理:",
summarizeStatus(finalStatus),
);
}
}
console.log("\n[E2E] ✅ Chrome Bridge 联调通过");
} finally {
await closeClient(control);
+75 -9
View File
@@ -1,22 +1,88 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import process from 'node:process';
const DEV_URL = process.env.LIME_WEB_BRIDGE_URL?.trim() || 'http://127.0.0.1:1420/';
const DEV_URL_TIMEOUT_MS = 1_500;
const ROOT_MARKERS = ['<title>Lime</title>', '<div id="root"></div>'];
const env = { ...process.env };
delete env.TAURI_ENV_PLATFORM;
env.LIME_BROWSER_BRIDGE = '1';
const child = spawn('npx', ['vite'], {
stdio: 'inherit',
shell: true,
env,
});
function isLimeDevShell(html) {
return ROOT_MARKERS.some((marker) => html.includes(marker));
}
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
async function probeExistingDevServer(url) {
if (typeof fetch !== 'function') {
return { reachable: false };
}
try {
const response = await fetch(url, {
method: 'GET',
signal: AbortSignal.timeout(DEV_URL_TIMEOUT_MS),
});
const html = await response.text();
return {
reachable: true,
status: response.status,
statusText: response.statusText,
isLimeDevShell: response.ok && isLimeDevShell(html),
};
} catch {
return { reachable: false };
}
}
async function waitForExitSignal() {
await new Promise((resolve) => {
const handleExit = () => resolve();
process.once('SIGINT', handleExit);
process.once('SIGTERM', handleExit);
});
}
function startVite() {
const child = spawn('npx', ['vite'], {
stdio: 'inherit',
shell: true,
env,
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 0);
});
}
async function main() {
const existingServer = await probeExistingDevServer(DEV_URL);
if (existingServer.reachable) {
if (!existingServer.isLimeDevShell) {
const statusLabel = `${existingServer.status} ${existingServer.statusText}`.trim();
throw new Error(
`[dev:web-bridge] ${DEV_URL} 已被其他服务占用,且返回内容不是 Lime dev shell(${statusLabel})。请先关闭占用进程后重试。`,
);
}
console.log(`[dev:web-bridge] 复用已存在的 Lime dev server: ${DEV_URL}`);
await waitForExitSignal();
return;
}
process.exit(code ?? 0);
startVite();
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});