feat: improve direct model workspace tools

Direct models now use bundled ripgrep for workspace discovery, read files through resumable pages, and edit multiple files with apply_patch. Ask can search and read while Execute retains write access, and fixed total input limits no longer constrain these tools.

Release note: 直连模型现可使用随包 ripgrep 搜索项目、分页读取任意大小的文本,并通过多文件补丁修改代码;设置中的相关能力统一显示为工作区文件和开发工具。
This commit is contained in:
mesalogo
2026-09-10 00:45:03 +08:00
parent c1f8ba80ee
commit ec100f8176
28 changed files with 1542 additions and 171 deletions
+7
View File
@@ -141,6 +141,13 @@ function portableRequiredPaths(directory) {
'opencode',
'opencode.exe'
),
join(
directory,
'resources',
'runtimes',
'opencode',
'rg.exe'
),
join(
directory,
'resources',
+17 -4
View File
@@ -94,6 +94,8 @@ const portableRequiredFiles = [
'resources/tray-icon.png',
'resources/tool-environment/managed-python-artifacts.json',
'resources/runtimes/opencode/opencode.exe',
'resources/runtimes/opencode/rg.exe',
'resources/licenses/vscode-ripgrep-MIT.txt',
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/package.json',
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/dist/index.js',
'resources/runtimes/continue/package.json',
@@ -128,7 +130,8 @@ const platformDefinitions = {
supportedFormats: ['nsis', 'portable'],
unpackedPattern: /^win(?:-.+)?-unpacked$/u,
executable: [`${productName}.exe`],
runtimeExecutable: 'opencode.exe'
runtimeExecutable: 'opencode.exe',
ripgrepExecutable: 'rg.exe'
},
macos: {
builderFlag: '--mac',
@@ -141,7 +144,8 @@ const platformDefinitions = {
'MacOS',
productName
],
runtimeExecutable: 'opencode'
runtimeExecutable: 'opencode',
ripgrepExecutable: 'rg'
},
linux: {
builderFlag: '--linux',
@@ -149,7 +153,8 @@ const platformDefinitions = {
supportedFormats: ['AppImage', 'deb', 'rpm'],
unpackedPattern: /^linux(?:-.+)?-unpacked$/u,
executable: [packageJson.name],
runtimeExecutable: 'opencode'
runtimeExecutable: 'opencode',
ripgrepExecutable: 'rg'
}
}
const formatExtensions = {
@@ -1070,6 +1075,12 @@ function verifyUnpackedOutput(directory, options) {
'opencode',
definition.runtimeExecutable
)
const ripgrepExecutable = join(
resources,
'runtimes',
'opencode',
definition.ripgrepExecutable
)
assertFile(applicationExecutable, '应用主程序')
assertFile(join(resources, 'app.asar'), '应用 ASAR')
assertFile(join(resources, 'release-notes.json'), '版本更新说明')
@@ -1094,6 +1105,7 @@ function verifyUnpackedOutput(directory, options) {
'Remote Runtime 锁定清单'
)
assertFile(runtimeExecutable, 'OpenCode Runtime')
assertFile(ripgrepExecutable, 'ripgrep Runtime')
verifyOpenCodeConfig(
resources,
join(root, '.runtime-resources', 'opencode-config')
@@ -1125,7 +1137,8 @@ function verifyUnpackedOutput(directory, options) {
verifyHarnessPackage(resources, options)
for (const [filePath, label] of [
[applicationExecutable, '应用主程序'],
[runtimeExecutable, 'OpenCode Runtime']
[runtimeExecutable, 'OpenCode Runtime'],
[ripgrepExecutable, 'ripgrep Runtime']
]) {
const actualArchitecture = binaryArchitecture(filePath)
if (actualArchitecture !== options.arch) {
+93 -8
View File
@@ -33,28 +33,28 @@ function sha512Integrity(contents) {
return `sha512-${createHash("sha512").update(contents).digest("base64")}`;
}
async function lockedIntegrity(projectDir, packageName) {
async function lockedIntegrity(projectDir, packageName, version) {
const lock = JSON.parse(
await readFile(join(projectDir, "package-lock.json"), "utf8"),
);
const entry = lock.packages?.[`node_modules/${packageName}`];
if (
entry?.version !== opencodeVersion ||
entry?.version !== version ||
typeof entry.integrity !== "string"
) {
throw new Error(
`Missing locked ${packageName}@${opencodeVersion} integrity`,
`Missing locked ${packageName}@${version} integrity`,
);
}
return entry.integrity;
}
async function downloadPackage(projectDir, packageName, integrity) {
async function downloadPackage(projectDir, packageName, version, integrity) {
const cacheDirectory = join(projectDir, ".runtime-resources", "cache");
await mkdir(cacheDirectory, { recursive: true });
const archivePath = join(
cacheDirectory,
`${packageName}-${opencodeVersion}.tgz`,
`${packageName.replaceAll("/", "-").replaceAll("@", "")}-${version}.tgz`,
);
if (existsSync(archivePath)) {
const cached = await readFile(archivePath);
@@ -70,7 +70,7 @@ async function downloadPackage(projectDir, packageName, integrity) {
[
...npm.prefixArgs,
"pack",
`${packageName}@${opencodeVersion}`,
`${packageName}@${version}`,
"--ignore-scripts",
"--json",
"--pack-destination",
@@ -108,6 +108,68 @@ async function downloadPackage(projectDir, packageName, integrity) {
return archivePath;
}
async function prepareRipgrep(
projectDir,
targetDirectory,
platform,
architecture,
version,
) {
const packageName = `@vscode/ripgrep-${platform}-${architecture}`;
const integrity = await lockedIntegrity(projectDir, packageName, version);
const executable = platform === "win32" ? "rg.exe" : "rg";
const preparedPath = join(targetDirectory, executable);
const readyPath = join(targetDirectory, ".ripgrep-ready.json");
const identity = { packageName, version, 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 ripgrep cache.
}
const archivePath = await downloadPackage(
projectDir,
packageName,
version,
integrity,
);
const stagingDirectory = `${targetDirectory}.ripgrep-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 executableSha256 = await sha256File(sourcePath);
if (platform !== "win32") {
await chmod(sourcePath, 0o755);
}
await mkdir(targetDirectory, { recursive: true });
await rm(preparedPath, { force: true });
await rename(sourcePath, preparedPath);
await writeFile(
readyPath,
JSON.stringify({ ...identity, executableSha256 }),
"utf8",
);
} finally {
await rm(stagingDirectory, { recursive: true, force: true });
}
}
module.exports = async function prepareBundledRuntimes(context) {
const platform = context.electronPlatformName;
const architecture = architectureNames[context.arch];
@@ -126,6 +188,10 @@ module.exports = async function prepareBundledRuntimes(context) {
const projectPackage = JSON.parse(
await readFile(join(projectDir, "package.json"), "utf8"),
);
const ripgrepVersion = projectPackage.dependencies["@vscode/ripgrep"];
if (typeof ripgrepVersion !== "string" || !/^\d+\.\d+\.\d+$/u.test(ripgrepVersion)) {
throw new Error("Missing exact @vscode/ripgrep dependency version");
}
await writeFile(
join(projectDir, "out", "main", "package.json"),
`${JSON.stringify(
@@ -140,7 +206,7 @@ module.exports = async function prepareBundledRuntimes(context) {
)}\n`,
"utf8",
);
const integrity = await lockedIntegrity(projectDir, packageName);
const integrity = await lockedIntegrity(projectDir, packageName, opencodeVersion);
const targetDirectory = join(projectDir, ".runtime-resources", architecture);
const readyPath = join(targetDirectory, ".ready.json");
const executable = platform === "win32" ? "opencode.exe" : "opencode";
@@ -161,13 +227,25 @@ module.exports = async function prepareBundledRuntimes(context) {
((await stat(preparedPath)).mode & 0o111) !== 0) &&
(await sha256File(preparedPath)) === ready.executableSha256
) {
await prepareRipgrep(
projectDir,
targetDirectory,
platform,
architecture,
ripgrepVersion,
);
return;
}
} catch {
// Rebuild an incomplete or stale runtime cache.
}
const archivePath = await downloadPackage(projectDir, packageName, integrity);
const archivePath = await downloadPackage(
projectDir,
packageName,
opencodeVersion,
integrity,
);
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
await rm(stagingDirectory, { recursive: true, force: true });
await mkdir(stagingDirectory, { recursive: true });
@@ -194,4 +272,11 @@ module.exports = async function prepareBundledRuntimes(context) {
} finally {
await rm(stagingDirectory, { recursive: true, force: true });
}
await prepareRipgrep(
projectDir,
targetDirectory,
platform,
architecture,
ripgrepVersion,
);
};