refactor(api): update workspace tree and virtual-file routes
Use workspace lib for path resolution.
This commit is contained in:
parent
6084381346
commit
974ba61b48
@ -17,8 +17,8 @@ vi.mock("node:os", () => ({
|
||||
// Mock workspace
|
||||
vi.mock("@/lib/workspace", () => ({
|
||||
resolveWorkspaceRoot: vi.fn(() => null),
|
||||
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw"),
|
||||
getEffectiveProfile: vi.fn(() => "default"),
|
||||
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"),
|
||||
getActiveWorkspaceName: vi.fn(() => null),
|
||||
parseSimpleYaml: vi.fn(() => ({})),
|
||||
duckdbQueryAll: vi.fn(() => []),
|
||||
duckdbQueryAllAsync: vi.fn(async () => []),
|
||||
@ -57,8 +57,8 @@ describe("Workspace Tree & Browse API", () => {
|
||||
}));
|
||||
vi.mock("@/lib/workspace", () => ({
|
||||
resolveWorkspaceRoot: vi.fn(() => null),
|
||||
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw"),
|
||||
getEffectiveProfile: vi.fn(() => "default"),
|
||||
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"),
|
||||
getActiveWorkspaceName: vi.fn(() => null),
|
||||
parseSimpleYaml: vi.fn(() => ({})),
|
||||
duckdbQueryAll: vi.fn(() => []),
|
||||
duckdbQueryAllAsync: vi.fn(async () => []),
|
||||
@ -83,11 +83,13 @@ describe("Workspace Tree & Browse API", () => {
|
||||
const json = await res.json();
|
||||
expect(json.exists).toBe(false);
|
||||
expect(json.tree).toEqual([]);
|
||||
expect(json.workspace).toBeNull();
|
||||
});
|
||||
|
||||
it("returns tree with workspace files", async () => {
|
||||
const { resolveWorkspaceRoot } = await import("@/lib/workspace");
|
||||
const { resolveWorkspaceRoot, getActiveWorkspaceName } = await import("@/lib/workspace");
|
||||
vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws");
|
||||
vi.mocked(getActiveWorkspaceName).mockReturnValue("default");
|
||||
const { readdirSync: mockReaddir, existsSync: mockExists } = await import("node:fs");
|
||||
vi.mocked(mockExists).mockReturnValue(true);
|
||||
vi.mocked(mockReaddir).mockImplementation((dir) => {
|
||||
@ -106,6 +108,7 @@ describe("Workspace Tree & Browse API", () => {
|
||||
const json = await res.json();
|
||||
expect(json.exists).toBe(true);
|
||||
expect(json.tree.length).toBeGreaterThan(0);
|
||||
expect(json.workspace).toBe("default");
|
||||
});
|
||||
|
||||
it("includes workspaceRoot in response", async () => {
|
||||
@ -153,16 +156,16 @@ describe("Workspace Tree & Browse API", () => {
|
||||
const value = String(p);
|
||||
return (
|
||||
value === "/ws" ||
|
||||
value === "/home/testuser/.openclaw/skills" ||
|
||||
value === "/home/testuser/.openclaw/skills/alpha/SKILL.md" ||
|
||||
value === "/home/testuser/.openclaw/skills/dench/SKILL.md"
|
||||
value === "/ws/skills" ||
|
||||
value === "/ws/skills/alpha/SKILL.md" ||
|
||||
value === "/ws/skills/dench/SKILL.md"
|
||||
);
|
||||
});
|
||||
vi.mocked(mockReaddir).mockImplementation((dir) => {
|
||||
if (String(dir) === "/ws") {
|
||||
return [] as unknown as Dirent[];
|
||||
}
|
||||
if (String(dir) === "/home/testuser/.openclaw/skills") {
|
||||
if (String(dir) === "/ws/skills") {
|
||||
return [
|
||||
makeDirent("alpha", true),
|
||||
makeDirent("dench", true),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { readdirSync, readFileSync, existsSync, statSync, type Dirent } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { resolveWorkspaceRoot, resolveOpenClawStateDir, getEffectiveProfile, parseSimpleYaml, duckdbQueryAll, isDatabaseFile } from "@/lib/workspace";
|
||||
import { resolveWorkspaceRoot, resolveOpenClawStateDir, getActiveWorkspaceName, parseSimpleYaml, duckdbQueryAll, isDatabaseFile } from "@/lib/workspace";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
@ -186,12 +186,13 @@ function parseSkillFrontmatter(content: string): { name?: string; emoji?: string
|
||||
return { name: result.name, emoji: result.emoji };
|
||||
}
|
||||
|
||||
/** Build a virtual "Skills" folder from <stateDir>/skills/. */
|
||||
/** Build a virtual "Skills" folder from <workspace>/skills/. */
|
||||
function buildSkillsVirtualFolder(): TreeNode | null {
|
||||
const stateDir = resolveOpenClawStateDir();
|
||||
const dirs = [
|
||||
join(stateDir, "skills"),
|
||||
];
|
||||
const workspaceRoot = resolveWorkspaceRoot();
|
||||
if (!workspaceRoot) {
|
||||
return null;
|
||||
}
|
||||
const dirs = [join(workspaceRoot, "skills")];
|
||||
|
||||
const children: TreeNode[] = [];
|
||||
const seen = new Set<string>();
|
||||
@ -247,13 +248,13 @@ export async function GET(req: Request) {
|
||||
const showHidden = url.searchParams.get("showHidden") === "1";
|
||||
|
||||
const openclawDir = resolveOpenClawStateDir();
|
||||
const profile = getEffectiveProfile();
|
||||
const workspace = getActiveWorkspaceName();
|
||||
const root = resolveWorkspaceRoot();
|
||||
if (!root) {
|
||||
const tree: TreeNode[] = [];
|
||||
const skillsFolder = buildSkillsVirtualFolder();
|
||||
if (skillsFolder) {tree.push(skillsFolder);}
|
||||
return Response.json({ tree, exists: false, workspaceRoot: null, openclawDir, profile });
|
||||
return Response.json({ tree, exists: false, workspaceRoot: null, openclawDir, workspace });
|
||||
}
|
||||
|
||||
const dbObjects = loadDbObjects();
|
||||
@ -263,5 +264,5 @@ export async function GET(req: Request) {
|
||||
const skillsFolder = buildSkillsVirtualFolder();
|
||||
if (skillsFolder) {tree.push(skillsFolder);}
|
||||
|
||||
return Response.json({ tree, exists: true, workspaceRoot: root, openclawDir, profile });
|
||||
return Response.json({ tree, exists: true, workspaceRoot: root, openclawDir, workspace });
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
||||
import { join, dirname, resolve, normalize } from "node:path";
|
||||
import { resolveOpenClawStateDir, resolveWorkspaceRoot } from "@/lib/workspace";
|
||||
import { resolveWorkspaceRoot } from "@/lib/workspace";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
@ -10,8 +10,10 @@ export const runtime = "nodejs";
|
||||
* Returns null if the path is invalid or tries to escape.
|
||||
*/
|
||||
function resolveVirtualPath(virtualPath: string): string | null {
|
||||
const stateDir = resolveOpenClawStateDir();
|
||||
const workspaceDir = resolveWorkspaceRoot() ?? join(stateDir, "workspace");
|
||||
const workspaceDir = resolveWorkspaceRoot();
|
||||
if (!workspaceDir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (virtualPath.startsWith("~skills/")) {
|
||||
// ~skills/<skillName>/SKILL.md
|
||||
@ -27,18 +29,12 @@ function resolveVirtualPath(virtualPath: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check workspace skills first, then managed skills
|
||||
const candidates = [
|
||||
join(workspaceDir, "skills", skillName, "SKILL.md"),
|
||||
join(stateDir, "skills", skillName, "SKILL.md"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
const skillPath = join(workspaceDir, "skills", skillName, "SKILL.md");
|
||||
if (existsSync(skillPath)) {
|
||||
return skillPath;
|
||||
}
|
||||
// Default to workspace skills dir for new files
|
||||
return candidates[0];
|
||||
// Default to workspace skills dir for new files.
|
||||
return skillPath;
|
||||
}
|
||||
|
||||
if (virtualPath.startsWith("~memories/")) {
|
||||
@ -83,11 +79,12 @@ function resolveVirtualPath(virtualPath: string): string | null {
|
||||
* Double-check that the resolved path stays within expected directories.
|
||||
*/
|
||||
function isSafePath(absPath: string): boolean {
|
||||
const stateDir = resolveOpenClawStateDir();
|
||||
const workspaceDir = resolveWorkspaceRoot() ?? join(stateDir, "workspace");
|
||||
const workspaceDir = resolveWorkspaceRoot();
|
||||
if (!workspaceDir) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalize(resolve(absPath));
|
||||
const allowed = [
|
||||
normalize(join(stateDir, "skills")),
|
||||
normalize(join(workspaceDir, "skills")),
|
||||
normalize(workspaceDir),
|
||||
];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user