mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:20:44 +00:00
fix: discover symlinked plugin directories
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Google Meet: route local Chrome joins through OpenClaw browser control instead of raw default Chrome, so agents use the configured OpenClaw browser profile when opening Meet. Thanks @openclaw.
|
||||
- Plugins/discovery: follow symlinked plugin directories in global and workspace plugin roots while keeping broken links ignored and existing package safety checks in place. Fixes #36754; carries forward #72695 and #63206. Thanks @Quackstro, @ming1523, and @xsfX20.
|
||||
- Plugins/startup: reuse canonical realpath lookups throughout each plugin discovery pass, including package and manifest boundary checks, so Windows npm-global startups no longer repeat expensive path resolution for the same plugin roots. Fixes #65733. Thanks @welfo-beo.
|
||||
- Reply/link understanding: keep media and link preprocessing on stable runtime entrypoints and continue with raw message content if optional enrichment fails, so URL-bearing messages are no longer dropped after stale runtime chunk upgrades. Fixes #68466. Thanks @songshikang0111.
|
||||
- Discord: persist routed model-picker overrides when the hidden `/model` dispatch succeeds but the bound thread session store is still stale, including LM Studio suffixed model ids. Carries forward #61473. Thanks @Nanako0129.
|
||||
|
||||
@@ -18,13 +18,17 @@ function makeTempDir() {
|
||||
|
||||
const mkdirSafe = mkdirSafeDir;
|
||||
|
||||
function symlinkDirectory(target: string, linkPath: string): void {
|
||||
fs.symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
|
||||
}
|
||||
|
||||
const canCreateDirectorySymlinks = (() => {
|
||||
const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-symlink-probe-"));
|
||||
const targetDir = path.join(probeDir, "target");
|
||||
const linkDir = path.join(probeDir, "link");
|
||||
try {
|
||||
fs.mkdirSync(targetDir);
|
||||
fs.symlinkSync(targetDir, linkDir, process.platform === "win32" ? "junction" : "dir");
|
||||
symlinkDirectory(targetDir, linkDir);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -318,6 +322,72 @@ describe("discoverOpenClawPlugins", () => {
|
||||
expectCandidateIds(candidates, { includes: ["alpha", "beta"] });
|
||||
});
|
||||
|
||||
it.skipIf(!canCreateDirectorySymlinks)(
|
||||
"discovers symlinked plugin directories in global roots",
|
||||
async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const globalExt = path.join(stateDir, "extensions");
|
||||
mkdirSafe(globalExt);
|
||||
|
||||
const linkedPluginDir = path.join(stateDir, "linked-plugin-src");
|
||||
createPackagePluginWithEntry({
|
||||
packageDir: linkedPluginDir,
|
||||
packageName: "@openclaw/linked-plugin",
|
||||
pluginId: "linked-plugin",
|
||||
});
|
||||
|
||||
symlinkDirectory(linkedPluginDir, path.join(globalExt, "linked-plugin"));
|
||||
|
||||
const { candidates, diagnostics } = await discoverWithStateDir(stateDir, {});
|
||||
expectCandidateIds(candidates, { includes: ["linked-plugin"] });
|
||||
expect(findCandidateById(candidates, "linked-plugin")?.rootDir).toBe(
|
||||
fs.realpathSync(linkedPluginDir),
|
||||
);
|
||||
expect(diagnostics).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!canCreateDirectorySymlinks)(
|
||||
"discovers symlinked plugin directories in workspace roots",
|
||||
async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const workspaceDir = path.join(stateDir, "workspace");
|
||||
const workspaceExt = path.join(workspaceDir, ".openclaw", "extensions");
|
||||
mkdirSafe(workspaceExt);
|
||||
|
||||
const linkedPluginDir = path.join(stateDir, "workspace-linked-plugin-src");
|
||||
createPackagePluginWithEntry({
|
||||
packageDir: linkedPluginDir,
|
||||
packageName: "@openclaw/workspace-linked-plugin",
|
||||
pluginId: "workspace-linked-plugin",
|
||||
});
|
||||
|
||||
symlinkDirectory(linkedPluginDir, path.join(workspaceExt, "workspace-linked-plugin"));
|
||||
|
||||
const { candidates, diagnostics } = await discoverWithStateDir(stateDir, { workspaceDir });
|
||||
expectCandidateIds(candidates, { includes: ["workspace-linked-plugin"] });
|
||||
expect(findCandidateById(candidates, "workspace-linked-plugin")?.rootDir).toBe(
|
||||
fs.realpathSync(linkedPluginDir),
|
||||
);
|
||||
expect(diagnostics).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32" || !canCreateDirectorySymlinks)(
|
||||
"ignores broken symlinked plugin directories in scanned roots",
|
||||
async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const globalExt = path.join(stateDir, "extensions");
|
||||
mkdirSafe(globalExt);
|
||||
|
||||
symlinkDirectory(path.join(stateDir, "missing-plugin-src"), path.join(globalExt, "missing"));
|
||||
|
||||
const { candidates, diagnostics } = await discoverWithStateDir(stateDir, {});
|
||||
expectCandidateIds(candidates, { excludes: ["missing"] });
|
||||
expect(diagnostics).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not recurse arbitrary workspace directories for plugin auto-discovery", () => {
|
||||
const stateDir = makeTempDir();
|
||||
const workspaceDir = path.join(stateDir, "workspace");
|
||||
@@ -631,11 +701,7 @@ describe("discoverOpenClawPlugins", () => {
|
||||
writePluginEntry(path.join(realPackageDir, "src", "index.ts"));
|
||||
|
||||
const linkedPackageDir = path.join(stateDir, "linked-pack");
|
||||
fs.symlinkSync(
|
||||
realPackageDir,
|
||||
linkedPackageDir,
|
||||
process.platform === "win32" ? "junction" : "dir",
|
||||
);
|
||||
symlinkDirectory(realPackageDir, linkedPackageDir);
|
||||
const canonicalPackageDir = fs.realpathSync(realPackageDir);
|
||||
|
||||
const realpathSync = vi.spyOn(fs, "realpathSync");
|
||||
@@ -1100,7 +1166,7 @@ describe("discoverOpenClawPlugins", () => {
|
||||
mkdirSafe(outsideDir);
|
||||
fs.writeFileSync(path.join(outsideDir, "escape.ts"), "export default {}", "utf-8");
|
||||
try {
|
||||
fs.symlinkSync(outsideDir, linkedDir, process.platform === "win32" ? "junction" : "dir");
|
||||
symlinkDirectory(outsideDir, linkedDir);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -353,6 +353,30 @@ function shouldIgnoreScannedDirectory(dirName: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveScannedEntryType(entry: fs.Dirent, fullPath: string): "file" | "directory" | null {
|
||||
if (entry.isFile()) {
|
||||
return "file";
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
return "directory";
|
||||
}
|
||||
if (!entry.isSymbolicLink()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stat = safeStatSync(fullPath);
|
||||
if (!stat) {
|
||||
return null;
|
||||
}
|
||||
if (stat.isFile()) {
|
||||
return "file";
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
return "directory";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvesToSameDirectory(
|
||||
left: string | undefined,
|
||||
right: string | undefined,
|
||||
@@ -621,7 +645,8 @@ function discoverInDirectory(params: {
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(params.dir, entry.name);
|
||||
if (entry.isFile()) {
|
||||
const entryType = resolveScannedEntryType(entry, fullPath);
|
||||
if (entryType === "file") {
|
||||
if (!isExtensionFile(fullPath)) {
|
||||
continue;
|
||||
}
|
||||
@@ -637,8 +662,9 @@ function discoverInDirectory(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
realpathCache: params.realpathCache,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!entry.isDirectory()) {
|
||||
if (entryType !== "directory") {
|
||||
continue;
|
||||
}
|
||||
if (params.skipDirectories?.has(entry.name)) {
|
||||
|
||||
Reference in New Issue
Block a user