Commit ab35460f authored by AI-甘富林's avatar AI-甘富林

feat(desktop): support packaged project workspace agent runner

- Add project-workspace-agent-runner.js to asarUnpack so it runs outside
  the asar archive in packaged builds
- Use absolute Windows System32 PowerShell path in project-bundle and
  project-workspace-executor to avoid Git Bash path hijacking
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent d91d9409
......@@ -4,6 +4,7 @@ compression: store
asar: true
asarUnpack:
- node_modules/keytar/build/Release/*.node
- dist/main/project-workspace-agent-runner.js
directories:
output: ../../dist/installer
artifactName: ${productName}-Setup-${version}.${ext}
......
......@@ -12,6 +12,15 @@ import type { RemoteSkillAsset } from "./skill-store.js";
const execFileAsync = promisify(execFile);
function getWindowsPowerShellPath(): string {
if (process.platform !== "win32") {
return "powershell.exe";
}
const systemRoot = process.env.SYSTEMROOT ?? process.env.WINDIR ?? "C:\\Windows";
return path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
}
interface BundleManifestRecord {
sourceUrl: string;
fileName: string;
......@@ -674,7 +683,7 @@ export class ProjectBundleService {
await mkdir(destinationPath, { recursive: true });
const escapedZipPath = zipPath.replace(/'/g, "''");
const escapedDestinationPath = destinationPath.replace(/'/g, "''");
await execFileAsync("powershell.exe", [
await execFileAsync(getWindowsPowerShellPath(), [
"-NoProfile",
"-Command",
`Expand-Archive -LiteralPath '${escapedZipPath}' -DestinationPath '${escapedDestinationPath}' -Force`
......
......@@ -33,6 +33,19 @@ interface RunnerEvent {
const EVENT_PREFIX = "QJC_WORKSPACE_EVENT\t";
function escapePowerShellSingleQuoted(value: string): string {
return value.replace(/'/g, "''");
}
function getWindowsPowerShellPath(): string {
if (process.platform !== "win32") {
return "powershell.exe";
}
const systemRoot = process.env.SYSTEMROOT ?? process.env.WINDIR ?? "C:\\Windows";
return path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
}
function toErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
......@@ -93,6 +106,23 @@ function extractReplyText(result: unknown): string {
return parts.join("\n\n").trim();
}
async function resolveRunnerScriptPath(): Promise<string> {
const directPath = path.resolve(__dirname, "./project-workspace-agent-runner.js");
const candidates = [directPath];
if (directPath.includes("app.asar")) {
candidates.unshift(directPath.replace("app.asar", "app.asar.unpacked"));
}
for (const candidate of candidates) {
if (await pathExists(candidate)) {
return candidate;
}
}
throw new Error(`Workspace runner script is missing: ${candidates.join(" | ")}`);
}
export class ProjectWorkspaceExecutorService {
private readonly runtimeManager: RuntimeManager;
......@@ -111,10 +141,7 @@ export class ProjectWorkspaceExecutorService {
await this.runtimeManager.syncManagedConfig("sync");
const paths = this.runtimeManager.resolveBundledPaths();
const runnerScriptPath = path.resolve(__dirname, "./project-workspace-agent-runner.js");
if (!(await pathExists(runnerScriptPath))) {
throw new Error(`Workspace runner script is missing: ${runnerScriptPath}`);
}
const runnerScriptPath = await resolveRunnerScriptPath();
const vendorPackageDir = path.join(paths.runtimeDir, "openclaw", "package");
const runId = input.runId?.trim() || randomUUID();
......@@ -127,21 +154,50 @@ export class ProjectWorkspaceExecutorService {
let stdoutBuffer = "";
let activeRunId = runId;
const child = spawn(paths.nodeExecutable, [runnerScriptPath], {
const childEnv = {
...process.env,
OPENCLAW_HOME: paths.runtimeDataDir,
OPENCLAW_STATE_DIR: paths.runtimeStateDir,
OPENCLAW_CONFIG_PATH: paths.managedConfigPath,
PLAYWRIGHT_BROWSERS_PATH: paths.playwrightBrowsersPath,
OPENCLAW_HIDE_BANNER: "1",
OPENCLAW_SUPPRESS_NOTES: "1",
NODE_NO_WARNINGS: process.env.NODE_NO_WARNINGS || "1"
};
const spawnOptions = {
cwd: input.projectRoot,
windowsHide: true,
stdio: ["pipe", "pipe", "pipe"],
env: {
...process.env,
OPENCLAW_HOME: paths.runtimeDataDir,
OPENCLAW_STATE_DIR: paths.runtimeStateDir,
OPENCLAW_CONFIG_PATH: paths.managedConfigPath,
PLAYWRIGHT_BROWSERS_PATH: paths.playwrightBrowsersPath,
OPENCLAW_HIDE_BANNER: "1",
OPENCLAW_SUPPRESS_NOTES: "1",
NODE_NO_WARNINGS: process.env.NODE_NO_WARNINGS || "1"
stdio: ["pipe", "pipe", "pipe"] as ["pipe", "pipe", "pipe"],
env: childEnv
};
let child;
try {
child = spawn(paths.nodeExecutable, [runnerScriptPath], spawnOptions);
} catch (error) {
const errorCode = error instanceof Error
? String((error as Error & { code?: number | string }).code ?? "")
: "";
if (process.platform !== "win32" || errorCode !== "EPERM") {
throw error;
}
});
const wrapperScript = [
`Set-Location -LiteralPath '${escapePowerShellSingleQuoted(input.projectRoot)}'`,
`& '${escapePowerShellSingleQuoted(paths.nodeExecutable)}' '${escapePowerShellSingleQuoted(runnerScriptPath)}'`,
"$exitCode = if ($LASTEXITCODE -is [int]) { $LASTEXITCODE } else { 0 }",
"exit $exitCode"
].join("; ");
child = spawn(getWindowsPowerShellPath(), [
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
wrapperScript
], spawnOptions);
}
const finishWithError = (message: string) => {
if (settled) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment