源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import net from "node:net";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const desktopRoot = resolve(__dirname, "..");
|
||||
const repoRoot = resolve(desktopRoot, "../..");
|
||||
const electronSidecarDir = resolve(desktopRoot, "resources", "sidecars");
|
||||
const electronHelperDir = resolve(desktopRoot, "resources", "helpers");
|
||||
const defaultDevDataDir = resolve(
|
||||
process.env.HOME ?? process.env.USERPROFILE ?? repoRoot,
|
||||
".openwork",
|
||||
"openwork-server-dev",
|
||||
);
|
||||
|
||||
const pnpmCmd = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
const nodeCmd = process.execPath;
|
||||
|
||||
async function findFreeTcpPort() {
|
||||
return new Promise((resolvePort, rejectPort) => {
|
||||
const server = net.createServer();
|
||||
server.once("error", rejectPort);
|
||||
server.listen({ port: 0, host: "127.0.0.1" }, () => {
|
||||
const address = server.address();
|
||||
server.close(() => {
|
||||
if (address && typeof address === "object") {
|
||||
resolvePort(address.port);
|
||||
return;
|
||||
}
|
||||
rejectPort(new Error("Unable to resolve a free Vite dev port"));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const rawPort = process.env.PORT?.trim() ?? "";
|
||||
const portValue = Number.parseInt(rawPort, 10);
|
||||
const devPort = rawPort === "0"
|
||||
? await findFreeTcpPort()
|
||||
: Number.isFinite(portValue) && portValue > 0 ? portValue : 5173;
|
||||
if (rawPort === "0") {
|
||||
console.log(`[electron-dev] Vite dev server will use free port ${devPort}`);
|
||||
}
|
||||
const explicitStartUrl = process.env.OPENWORK_ELECTRON_START_URL?.trim() || "";
|
||||
const startUrl = explicitStartUrl || `http://localhost:${devPort}`;
|
||||
const viteProbeUrls = explicitStartUrl
|
||||
? [explicitStartUrl]
|
||||
: [
|
||||
`http://127.0.0.1:${devPort}`,
|
||||
`http://[::1]:${devPort}`,
|
||||
`http://localhost:${devPort}`,
|
||||
];
|
||||
|
||||
function needsShell(command) {
|
||||
return process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return spawn(command, args, {
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
shell: needsShell(command),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function runSync(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
stdio: "inherit",
|
||||
shell: needsShell(command),
|
||||
...options,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, timeoutMs = 4000) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetch(url, { signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function probeHost(host, port) {
|
||||
return new Promise((resolveCheck) => {
|
||||
const socket = net.createConnection({ host, port });
|
||||
const onDone = (ready) => {
|
||||
socket.removeAllListeners();
|
||||
socket.destroy();
|
||||
resolveCheck(ready);
|
||||
};
|
||||
socket.setTimeout(1200);
|
||||
socket.once("connect", () => onDone(true));
|
||||
socket.once("timeout", () => onDone(false));
|
||||
socket.once("error", () => onDone(false));
|
||||
});
|
||||
}
|
||||
|
||||
async function looksLikeVite(url) {
|
||||
try {
|
||||
const response = await fetchWithTimeout(`${url}/@vite/client`);
|
||||
if (!response.ok) return false;
|
||||
const body = await response.text();
|
||||
return body.includes("@vite/client") || body.includes("import.meta.hot");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function portIsOpenForVite(url) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const host = parsed.hostname.replace(/^\[|\]$/g, "");
|
||||
const port = Number.parseInt(parsed.port || (parsed.protocol === "https:" ? "443" : "80"), 10);
|
||||
if (!Number.isFinite(port)) return false;
|
||||
return probeHost(host, port);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForVite(url, timeoutMs = 60_000) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
for (const candidate of [url, ...viteProbeUrls].filter(Boolean)) {
|
||||
if (await looksLikeVite(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
for (const candidate of [url, ...viteProbeUrls].filter(Boolean)) {
|
||||
if (await portIsOpenForVite(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
await new Promise((resolveDelay) => setTimeout(resolveDelay, 500));
|
||||
}
|
||||
throw new Error(`Timed out waiting for Vite dev server at ${viteProbeUrls.join(", ")}`);
|
||||
}
|
||||
|
||||
function signalTree(child, signal) {
|
||||
if (!child?.pid) return;
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(-child.pid, signal);
|
||||
} catch {
|
||||
try {
|
||||
child.kill(signal);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function restoreTerminal() {
|
||||
try {
|
||||
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
||||
process.stdin.setRawMode(false);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function waitForExit(child, timeoutMs) {
|
||||
if (!child || child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
|
||||
return new Promise((resolveWait) => {
|
||||
let settled = false;
|
||||
const finish = (clean) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
child.off("exit", onExit);
|
||||
resolveWait(clean);
|
||||
};
|
||||
const onExit = () => finish(true);
|
||||
const timer = setTimeout(() => finish(false), timeoutMs);
|
||||
child.once("exit", onExit);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForChildren(children, timeoutMs) {
|
||||
const results = await Promise.all(children.map((child) => waitForExit(child, timeoutMs)));
|
||||
return results.every(Boolean);
|
||||
}
|
||||
|
||||
let uiChild = null;
|
||||
let electronChild = null;
|
||||
let stopping = false;
|
||||
|
||||
async function stopAll(exitCode = 0) {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
restoreTerminal();
|
||||
|
||||
const children = [electronChild, uiChild].filter(Boolean);
|
||||
for (const child of children) signalTree(child, "SIGINT");
|
||||
|
||||
const stoppedCleanly = await waitForChildren(children, 2_000);
|
||||
if (!stoppedCleanly) {
|
||||
for (const child of children) signalTree(child, "SIGTERM");
|
||||
await waitForChildren(children, 1_000);
|
||||
}
|
||||
|
||||
restoreTerminal();
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
process.once("SIGINT", () => void stopAll(130));
|
||||
process.once("SIGTERM", () => void stopAll(143));
|
||||
|
||||
if (process.env.OPENWORK_ELECTRON_SKIP_SHARED_PREPARE !== "1") {
|
||||
runSync(nodeCmd, [resolve(__dirname, "prepare-sidecar.mjs"), "--force", "--outdir", electronSidecarDir], { cwd: desktopRoot });
|
||||
runSync(nodeCmd, [resolve(__dirname, "prepare-computer-use-helper.mjs"), "--force", "--outdir", electronHelperDir], { cwd: desktopRoot });
|
||||
}
|
||||
|
||||
// Build workspace packages that Electron imports from their dist output.
|
||||
console.log("[electron-dev] Building Electron workspace dependencies...");
|
||||
runSync(pnpmCmd, ["--filter", "@openwork/headless-threads", "build"], { cwd: repoRoot });
|
||||
runSync(pnpmCmd, ["--filter", "openwork-server", "build"], { cwd: repoRoot });
|
||||
|
||||
const initialProbeUrls = [startUrl, ...viteProbeUrls].filter(Boolean);
|
||||
let viteReady = false;
|
||||
for (const candidate of initialProbeUrls) {
|
||||
if (await looksLikeVite(candidate)) {
|
||||
viteReady = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!viteReady) {
|
||||
for (const candidate of initialProbeUrls) {
|
||||
if (await portIsOpenForVite(candidate)) {
|
||||
viteReady = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!viteReady) {
|
||||
uiChild = run(pnpmCmd, ["-w", "dev:ui"], {
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: String(devPort),
|
||||
OPENWORK_DEV_MODE: process.env.OPENWORK_DEV_MODE ?? "1",
|
||||
OPENWORK_DATA_DIR: process.env.OPENWORK_DATA_DIR ?? defaultDevDataDir,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const resolvedStartUrl = await waitForVite(startUrl);
|
||||
|
||||
// Native dependencies installed for the host Node ABI must be rebuilt before
|
||||
// Electron loads the embedded server and terminal runtime.
|
||||
if (process.env.OPENWORK_ELECTRON_SKIP_NATIVE_REBUILD === "1") {
|
||||
console.log("[electron-dev] Using prebuilt Electron native dependencies.");
|
||||
} else {
|
||||
console.log("[electron-dev] Rebuilding native dependencies for Electron...");
|
||||
runSync(pnpmCmd, ["--filter", "@openwork/desktop", "run", "rebuild:electron-native"], { cwd: repoRoot });
|
||||
}
|
||||
|
||||
// Optional Electron CDP for external debugging / raw CDP clients.
|
||||
// NOT required for the built-in browser (uses native webContents APIs).
|
||||
// Set OPENWORK_ELECTRON_REMOTE_DEBUG_PORT=9823 to enable.
|
||||
const cdpPortRaw = process.env.OPENWORK_ELECTRON_REMOTE_DEBUG_PORT?.trim() ?? "";
|
||||
const cdpPort = cdpPortRaw === "" || cdpPortRaw === "0" ? "" : cdpPortRaw;
|
||||
|
||||
const blankSlateArgs = process.argv.includes("--blank-slate") ? ["--blank-slate"] : [];
|
||||
electronChild = run(pnpmCmd, ["exec", "electron", "./electron/main.mjs", ...blankSlateArgs], {
|
||||
cwd: desktopRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
OPENWORK_DEV_MODE: process.env.OPENWORK_DEV_MODE ?? "1",
|
||||
OPENWORK_DATA_DIR: process.env.OPENWORK_DATA_DIR ?? defaultDevDataDir,
|
||||
OPENWORK_ELECTRON_START_URL: resolvedStartUrl,
|
||||
...(cdpPort ? { OPENWORK_ELECTRON_REMOTE_DEBUG_PORT: cdpPort } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (cdpPort) {
|
||||
console.log(`[openwork] Electron CDP exposed at http://127.0.0.1:${cdpPort}`);
|
||||
}
|
||||
|
||||
electronChild.on("exit", (code) => {
|
||||
if (stopping) return;
|
||||
void stopAll(code ?? 0);
|
||||
});
|
||||
Reference in New Issue
Block a user