Commit 945b8992 authored by AI-甘富林's avatar AI-甘富林

fix(desktop): resolve workspace runner deps from app data

parent fc12bfc3
import { createHash, randomUUID } from "node:crypto";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { lstat, mkdir, readFile, readdir, readlink, rm, symlink, writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
......@@ -7,6 +7,7 @@ import type { ProjectResolvedAttachment } from "@qjclaw/shared-types";
interface RunnerInput {
vendorPackageDir: string;
instrumentationDir?: string;
projectRoot: string;
sessionId: string;
prompt: string;
......@@ -74,7 +75,91 @@ function buildInstrumentationKey(agentModulePath: string, modelSelectionSpecifie
.slice(0, 12);
}
async function ensureInstrumentedWorkspaceModules(agentModulePath: string): Promise<{
function resolveInstrumentationDir(input: RunnerInput): string {
const explicitDir = input.instrumentationDir?.trim();
if (explicitDir) {
return explicitDir;
}
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim();
if (stateDir) {
return path.join(stateDir, "workspace-runner", "instrumented-modules");
}
return path.join(input.projectRoot, "memory", "workspace-runner", "instrumented-modules");
}
function rewriteModuleSpecifiers(
source: string,
baseDir: string,
overrides: Record<string, string> = {}
): string {
const toModuleUrl = (specifier: string): string => {
const override = overrides[specifier];
if (override) {
return override;
}
if (specifier.startsWith(".")) {
return pathToFileURL(path.resolve(baseDir, specifier)).href;
}
return specifier;
};
return source
.replace(/(^|[\r\n])(\s*(?:import|export)\s+[^"'\r\n;]+?\s+from\s*)(["'])([^"'\r\n]+)\3/g, (
_match,
linePrefix: string,
prefix: string,
quote: string,
specifier: string
) => {
return `${linePrefix}${prefix}${quote}${toModuleUrl(specifier)}${quote}`;
})
.replace(/(^|[\r\n])(\s*import\s*)(["'])([^"'\r\n]+)\3/g, (
_match,
linePrefix: string,
prefix: string,
quote: string,
specifier: string
) => {
return `${linePrefix}${prefix}${quote}${toModuleUrl(specifier)}${quote}`;
})
.replace(/\b(import\s*\(\s*)(["'])([^"'\r\n]+)\2/g, (_match, prefix: string, quote: string, specifier: string) => {
return `${prefix}${quote}${toModuleUrl(specifier)}${quote}`;
});
}
async function ensureInstrumentationNodeModulesLink(instrumentationDir: string, vendorPackageDir: string): Promise<void> {
const vendorNodeModulesPath = path.join(vendorPackageDir, "node_modules");
try {
const vendorNodeModules = await lstat(vendorNodeModulesPath);
if (!vendorNodeModules.isDirectory()) {
throw new Error(`${vendorNodeModulesPath} is not a directory.`);
}
} catch (error) {
throw new Error(`Unable to locate OpenClaw node_modules at ${vendorNodeModulesPath}: ${toErrorMessage(error)}`);
}
const linkPath = path.join(instrumentationDir, "node_modules");
const normalizeForCompare = (value: string): string => path.resolve(value).toLowerCase();
const expectedTarget = normalizeForCompare(vendorNodeModulesPath);
const existing = await lstat(linkPath).catch(() => undefined);
if (existing) {
if (existing.isSymbolicLink()) {
const currentTarget = await readlink(linkPath);
const resolvedTarget = normalizeForCompare(path.resolve(path.dirname(linkPath), currentTarget));
if (resolvedTarget === expectedTarget) {
return;
}
}
await rm(linkPath, { recursive: true, force: true });
}
await symlink(vendorNodeModulesPath, linkPath, "junction");
}
async function ensureInstrumentedWorkspaceModules(agentModulePath: string, instrumentationDir: string): Promise<{
agentModuleUrl: string;
modelSelectionModuleUrl: string;
}> {
......@@ -86,23 +171,25 @@ async function ensureInstrumentedWorkspaceModules(agentModulePath: string): Prom
const modelSelectionSpecifier = modelSelectionImportMatch[1];
const distDir = path.dirname(agentModulePath);
const vendorPackageDir = path.dirname(distDir);
const modelSelectionPath = path.resolve(distDir, modelSelectionSpecifier);
const instrumentationKey = buildInstrumentationKey(agentModulePath, modelSelectionSpecifier);
const instrumentedAgentFileName = `.qjc-agent-${instrumentationKey}.js`;
const instrumentedModelSelectionFileName = `.qjc-model-selection-${instrumentationKey}.js`;
const instrumentedAgentPath = path.join(distDir, instrumentedAgentFileName);
const instrumentedModelSelectionPath = path.join(distDir, instrumentedModelSelectionFileName);
const instrumentedAgentPath = path.join(instrumentationDir, instrumentedAgentFileName);
const instrumentedModelSelectionPath = path.join(instrumentationDir, instrumentedModelSelectionFileName);
const instrumentedModelSelectionUrl = pathToFileURL(instrumentedModelSelectionPath).href;
await mkdir(distDir, { recursive: true });
await mkdir(instrumentationDir, { recursive: true });
await ensureInstrumentationNodeModulesLink(instrumentationDir, vendorPackageDir);
const modelSelectionSource = await readFile(modelSelectionPath, "utf8");
const instrumentedModelSelectionSource = `${modelSelectionSource}\nexport { onAgentEvent as __qjcOnAgentEvent };\n`;
const instrumentedModelSelectionSource = `${rewriteModuleSpecifiers(modelSelectionSource, distDir)}\nexport { onAgentEvent as __qjcOnAgentEvent };\n`;
await writeFile(instrumentedModelSelectionPath, instrumentedModelSelectionSource, "utf8");
const instrumentedAgentSource = agentSource.replace(
modelSelectionSpecifier,
`./${instrumentedModelSelectionFileName}`
);
const instrumentedAgentSource = rewriteModuleSpecifiers(agentSource, distDir, {
[modelSelectionSpecifier]: instrumentedModelSelectionUrl
});
await writeFile(instrumentedAgentPath, instrumentedAgentSource, "utf8");
return {
......@@ -191,7 +278,7 @@ async function main(): Promise<void> {
});
const agentModulePath = await resolveAgentModulePath(input.vendorPackageDir);
const instrumentedModules = await ensureInstrumentedWorkspaceModules(agentModulePath);
const instrumentedModules = await ensureInstrumentedWorkspaceModules(agentModulePath, resolveInstrumentationDir(input));
const agentModule = await import(instrumentedModules.agentModuleUrl) as AgentCommandModule;
const modelSelectionModule = await import(instrumentedModules.modelSelectionModuleUrl) as InstrumentedModelSelectionModule;
if (typeof agentModule.t !== "function") {
......
......@@ -326,6 +326,7 @@ export class ProjectWorkspaceExecutorService {
const runnerScriptPath = automationCommand ? null : await resolveRunnerScriptPath();
const vendorPackageDir = path.join(paths.runtimeDir, "openclaw", "package");
const instrumentationDir = path.join(paths.runtimeDataDir, "workspace-runner", "instrumented-modules");
const runId = input.runId?.trim() || randomUUID();
callbacks.onStatus?.(
......@@ -516,6 +517,7 @@ export class ProjectWorkspaceExecutorService {
const payload = JSON.stringify({
vendorPackageDir,
instrumentationDir,
projectRoot: input.projectRoot,
sessionId: input.sessionId,
prompt: input.prompt,
......
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