mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 23:40:20 +00:00
fix(qmd): normalize direct file collection paths (#65212)
* fix(qmd): normalize direct file collection paths Port fix from PR #65212 to new package location. When a QMD custom collection path config entry points directly to a file instead of a directory, normalize into: - path = parent directory - pattern = exact filename This ensures direct file targets are handled correctly regardless of any user-supplied glob pattern. Original commit: 3570aa55a7 (fix/flow-runs-legacy-migration) * fix(qmd): escape direct file collection patterns * fix(qmd): escape direct file collection masks
This commit is contained in:
@@ -194,6 +194,32 @@ describe("resolveMemoryBackendConfig", () => {
|
||||
expect(custom.path).toBe(path.resolve("/workspace/root", "notes"));
|
||||
});
|
||||
|
||||
it("normalizes direct file qmd paths to escaped exact-file patterns", async () => {
|
||||
const workspaceDir = await createFixtureDir("direct-file-path");
|
||||
const notesPath = path.join(workspaceDir, "notes{a,b}[1].md");
|
||||
await fs.writeFile(notesPath, "# Notes\n", "utf8");
|
||||
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: { workspace: workspaceDir },
|
||||
list: [{ id: "main", workspace: workspaceDir }],
|
||||
},
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
paths: [{ path: "notes{a,b}[1].md", name: "direct-note", pattern: "**/*.md" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" });
|
||||
const custom = resolved.qmd?.collections.find((c) => c.name.startsWith("direct-note"));
|
||||
expect(custom).toMatchObject({
|
||||
path: workspaceDir,
|
||||
pattern: String.raw`notes\{a,b\}\[1\].md`,
|
||||
});
|
||||
});
|
||||
|
||||
it("scopes qmd collection names per agent", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
import { isPathInside } from "./fs-utils.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-utils.js";
|
||||
|
||||
function escapeQmdExactFilePattern(fileName: string): string {
|
||||
return fileName.replace(/[\\*?[\]{}()!+@]/g, "\\$&");
|
||||
}
|
||||
|
||||
export type ResolvedMemoryBackendConfig = {
|
||||
backend: MemoryBackend;
|
||||
citations: MemoryCitationsMode;
|
||||
@@ -291,26 +295,40 @@ function resolveCustomPaths(
|
||||
return;
|
||||
}
|
||||
let resolved: string;
|
||||
let collectionPath: string;
|
||||
try {
|
||||
resolved = resolvePath(trimmedPath, workspaceDir);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const pattern = entry.pattern?.trim() || "**/*.md";
|
||||
const dedupeKey = `${resolved}\u0000${pattern}`;
|
||||
collectionPath = resolved;
|
||||
let pattern = entry.pattern?.trim() || "**/*.md";
|
||||
try {
|
||||
const stat = fs.statSync(resolved);
|
||||
if (stat.isFile()) {
|
||||
// When the configured path points directly to a file, normalize into a
|
||||
// parent-directory collection with an exact-filename pattern, regardless
|
||||
// of any user-supplied glob (a glob does not apply to a single file).
|
||||
collectionPath = path.dirname(resolved);
|
||||
pattern = escapeQmdExactFilePattern(path.basename(resolved));
|
||||
}
|
||||
} catch {
|
||||
// not a file or can't stat, use as-is
|
||||
}
|
||||
const dedupeKey = `${collectionPath}\u0000${pattern}`;
|
||||
if (seenRoots.has(dedupeKey)) {
|
||||
return;
|
||||
}
|
||||
seenRoots.add(dedupeKey);
|
||||
const explicitName = entry.name?.trim();
|
||||
const baseName =
|
||||
explicitName && !isPathInsideRoot(resolved, workspaceDir)
|
||||
explicitName && !isPathInsideRoot(collectionPath, workspaceDir)
|
||||
? explicitName
|
||||
: scopeCollectionBase(explicitName || `custom-${index + 1}`, agentId);
|
||||
const name = ensureUniqueName(baseName, existing);
|
||||
collections.push({
|
||||
name,
|
||||
path: resolved,
|
||||
path: collectionPath,
|
||||
pattern,
|
||||
kind: "custom",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user