Check cached embedded servers before reuse, isolate concurrent cancellation, and serialize request MCP registration and cleanup. Preserve native controls across session switches and recover usable partial inventories. Share the bundled offline OpenCode configuration with desktop and Agent launch paths, including model bridge environment forwarding. Validation: focused tests and typecheck passed; lint passed before the final fixes. The full test run exceeded 120 seconds and reported a DS stdio MCP failure. Current-source Agent host and real suspend/resume validation remain pending. Release note: 修复切换会话后 OpenCode 原生控件消失及并发取消相互影响的问题,并完善内嵌服务复用检查和桌面、Agent 的离线依赖配置。
214 lines
6.1 KiB
JavaScript
214 lines
6.1 KiB
JavaScript
const { createHash } = require("node:crypto");
|
|
const {
|
|
chmod,
|
|
mkdir,
|
|
readFile,
|
|
rename,
|
|
rm,
|
|
stat,
|
|
writeFile,
|
|
} = require("node:fs/promises");
|
|
const { existsSync } = require("node:fs");
|
|
const { join } = require("node:path");
|
|
const { spawnSync } = require("node:child_process");
|
|
const tar = require("tar");
|
|
const { sha256File } = require("./file-hash.cjs");
|
|
const {
|
|
opencodeVersion,
|
|
prepareBundledOpenCodeConfig,
|
|
} = require("./opencode-config.cjs");
|
|
|
|
const architectureNames = {
|
|
1: "x64",
|
|
3: "arm64",
|
|
};
|
|
const platformNames = {
|
|
darwin: "darwin",
|
|
linux: "linux",
|
|
win32: "windows",
|
|
};
|
|
|
|
function sha512Integrity(contents) {
|
|
return `sha512-${createHash("sha512").update(contents).digest("base64")}`;
|
|
}
|
|
|
|
async function lockedIntegrity(projectDir, packageName) {
|
|
const lock = JSON.parse(
|
|
await readFile(join(projectDir, "package-lock.json"), "utf8"),
|
|
);
|
|
const entry = lock.packages?.[`node_modules/${packageName}`];
|
|
if (
|
|
entry?.version !== opencodeVersion ||
|
|
typeof entry.integrity !== "string"
|
|
) {
|
|
throw new Error(
|
|
`Missing locked ${packageName}@${opencodeVersion} integrity`,
|
|
);
|
|
}
|
|
return entry.integrity;
|
|
}
|
|
|
|
function npmInvocation() {
|
|
const npmCli = process.env.npm_execpath;
|
|
if (npmCli) {
|
|
return {
|
|
command: process.execPath,
|
|
prefixArgs: [npmCli],
|
|
};
|
|
}
|
|
if (process.platform === "win32") {
|
|
throw new Error("npm_execpath is required to prepare bundled runtimes");
|
|
}
|
|
return {
|
|
command: "npm",
|
|
prefixArgs: [],
|
|
};
|
|
}
|
|
|
|
async function downloadPackage(projectDir, packageName, integrity) {
|
|
const cacheDirectory = join(projectDir, ".runtime-resources", "cache");
|
|
await mkdir(cacheDirectory, { recursive: true });
|
|
const archivePath = join(
|
|
cacheDirectory,
|
|
`${packageName}-${opencodeVersion}.tgz`,
|
|
);
|
|
if (existsSync(archivePath)) {
|
|
const cached = await readFile(archivePath);
|
|
if (sha512Integrity(cached) === integrity) {
|
|
return archivePath;
|
|
}
|
|
await rm(archivePath, { force: true });
|
|
}
|
|
|
|
const npm = npmInvocation();
|
|
const result = spawnSync(
|
|
npm.command,
|
|
[
|
|
...npm.prefixArgs,
|
|
"pack",
|
|
`${packageName}@${opencodeVersion}`,
|
|
"--ignore-scripts",
|
|
"--json",
|
|
"--pack-destination",
|
|
cacheDirectory,
|
|
],
|
|
{
|
|
cwd: projectDir,
|
|
encoding: "utf8",
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
shell: false,
|
|
windowsHide: true,
|
|
},
|
|
);
|
|
if (result.status !== 0) {
|
|
throw new Error(
|
|
`Unable to fetch ${packageName}: ${
|
|
result.error?.message ||
|
|
result.stderr ||
|
|
result.stdout ||
|
|
`npm exited with ${result.status}`
|
|
}`,
|
|
);
|
|
}
|
|
const output = JSON.parse(result.stdout);
|
|
const downloadedPath = join(cacheDirectory, output[0].filename);
|
|
const contents = await readFile(downloadedPath);
|
|
if (sha512Integrity(contents) !== integrity) {
|
|
await rm(downloadedPath, { force: true });
|
|
throw new Error(`Integrity verification failed for ${packageName}`);
|
|
}
|
|
if (downloadedPath !== archivePath) {
|
|
await rm(archivePath, { force: true });
|
|
await rename(downloadedPath, archivePath);
|
|
}
|
|
return archivePath;
|
|
}
|
|
|
|
module.exports = async function prepareBundledRuntimes(context) {
|
|
const platform = context.electronPlatformName;
|
|
const architecture = architectureNames[context.arch];
|
|
const packagePlatform = platformNames[platform];
|
|
if (!architecture || !packagePlatform) {
|
|
throw new Error(
|
|
`Bundled OpenCode does not support ${platform}/${context.arch}`,
|
|
);
|
|
}
|
|
|
|
const suffix =
|
|
architecture === "x64" ? `${architecture}-baseline` : architecture;
|
|
const packageName = `opencode-${packagePlatform}-${suffix}`;
|
|
const projectDir = context.packager.projectDir;
|
|
await prepareBundledOpenCodeConfig(projectDir);
|
|
const projectPackage = JSON.parse(
|
|
await readFile(join(projectDir, "package.json"), "utf8"),
|
|
);
|
|
await writeFile(
|
|
join(projectDir, "out", "main", "package.json"),
|
|
`${JSON.stringify(
|
|
{
|
|
name: "@deepseek-ai/dsh-llm",
|
|
version: projectPackage.dependencies["@deepseek-ai/dsh-llm"],
|
|
private: true,
|
|
type: "module",
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
"utf8",
|
|
);
|
|
const integrity = await lockedIntegrity(projectDir, packageName);
|
|
const targetDirectory = join(projectDir, ".runtime-resources", architecture);
|
|
const readyPath = join(targetDirectory, ".ready.json");
|
|
const executable = platform === "win32" ? "opencode.exe" : "opencode";
|
|
const preparedPath = join(targetDirectory, executable);
|
|
const identity = {
|
|
packageName,
|
|
version: opencodeVersion,
|
|
integrity,
|
|
};
|
|
try {
|
|
const ready = JSON.parse(await readFile(readyPath, "utf8"));
|
|
if (
|
|
ready.packageName === identity.packageName &&
|
|
ready.version === identity.version &&
|
|
ready.integrity === identity.integrity &&
|
|
typeof ready.executableSha256 === "string" &&
|
|
(platform === "win32" ||
|
|
((await stat(preparedPath)).mode & 0o111) !== 0) &&
|
|
(await sha256File(preparedPath)) === ready.executableSha256
|
|
) {
|
|
return;
|
|
}
|
|
} catch {
|
|
// Rebuild an incomplete or stale runtime cache.
|
|
}
|
|
|
|
const archivePath = await downloadPackage(projectDir, packageName, integrity);
|
|
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
|
|
await rm(stagingDirectory, { recursive: true, force: true });
|
|
await mkdir(stagingDirectory, { recursive: true });
|
|
try {
|
|
await tar.x({
|
|
file: archivePath,
|
|
cwd: stagingDirectory,
|
|
strip: 1,
|
|
});
|
|
const sourcePath = join(stagingDirectory, "bin", executable);
|
|
const stagingExecutable = join(stagingDirectory, executable);
|
|
await rename(sourcePath, stagingExecutable);
|
|
if (platform !== "win32") {
|
|
await chmod(stagingExecutable, 0o755);
|
|
}
|
|
const executableSha256 = await sha256File(stagingExecutable);
|
|
await writeFile(
|
|
join(stagingDirectory, ".ready.json"),
|
|
JSON.stringify({ ...identity, executableSha256 }),
|
|
"utf8",
|
|
);
|
|
await rm(targetDirectory, { recursive: true, force: true });
|
|
await rename(stagingDirectory, targetDirectory);
|
|
} finally {
|
|
await rm(stagingDirectory, { recursive: true, force: true });
|
|
}
|
|
};
|