Export Feishu workspace artifact helpers
This commit is contained in:
parent
50ce9ac1c6
commit
5c88d62d75
@ -189,6 +189,10 @@
|
||||
"types": "./dist/plugin-sdk/discord-core.d.ts",
|
||||
"default": "./dist/plugin-sdk/discord-core.js"
|
||||
},
|
||||
"./plugin-sdk/feishu": {
|
||||
"types": "./dist/plugin-sdk/feishu.d.ts",
|
||||
"default": "./dist/plugin-sdk/feishu.js"
|
||||
},
|
||||
"./plugin-sdk/matrix": {
|
||||
"types": "./dist/plugin-sdk/matrix.d.ts",
|
||||
"default": "./dist/plugin-sdk/matrix.js"
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
"telegram-core",
|
||||
"discord",
|
||||
"discord-core",
|
||||
"feishu",
|
||||
"matrix",
|
||||
"slack",
|
||||
"slack-core",
|
||||
|
||||
@ -78,7 +78,11 @@ export {
|
||||
buildRuntimeAccountStatusSnapshot,
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "./status-helpers.js";
|
||||
export { withTempDownloadPath } from "./temp-path.js";
|
||||
export {
|
||||
buildAgentWorkspaceArtifactPath,
|
||||
resolveAgentWorkspaceOutputPath,
|
||||
withTempDownloadPath,
|
||||
} from "./temp-path.js";
|
||||
export {
|
||||
buildFeishuConversationId,
|
||||
parseFeishuConversationId,
|
||||
|
||||
@ -12,6 +12,7 @@ import type {
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import * as directoryRuntimeSdk from "openclaw/plugin-sdk/directory-runtime";
|
||||
import * as discordSdk from "openclaw/plugin-sdk/discord";
|
||||
import * as feishuSdk from "openclaw/plugin-sdk/feishu";
|
||||
import * as imessageSdk from "openclaw/plugin-sdk/imessage";
|
||||
import * as imessageCoreSdk from "openclaw/plugin-sdk/imessage-core";
|
||||
import * as lazyRuntimeSdk from "openclaw/plugin-sdk/lazy-runtime";
|
||||
@ -61,7 +62,6 @@ describe("plugin-sdk subpath exports", () => {
|
||||
expect(pluginSdkSubpaths).not.toContain("acpx");
|
||||
expect(pluginSdkSubpaths).not.toContain("compat");
|
||||
expect(pluginSdkSubpaths).not.toContain("device-pair");
|
||||
expect(pluginSdkSubpaths).not.toContain("feishu");
|
||||
expect(pluginSdkSubpaths).not.toContain("google");
|
||||
expect(pluginSdkSubpaths).not.toContain("googlechat");
|
||||
expect(pluginSdkSubpaths).not.toContain("irc");
|
||||
@ -137,6 +137,12 @@ describe("plugin-sdk subpath exports", () => {
|
||||
expect(typeof directoryRuntimeSdk.listResolvedDirectoryEntriesFromSources).toBe("function");
|
||||
});
|
||||
|
||||
it("exports feishu helpers from the dedicated subpath", () => {
|
||||
expect(typeof feishuSdk.buildAgentWorkspaceArtifactPath).toBe("function");
|
||||
expect(typeof feishuSdk.resolveAgentWorkspaceOutputPath).toBe("function");
|
||||
expect(typeof feishuSdk.withTempDownloadPath).toBe("function");
|
||||
});
|
||||
|
||||
it("exports channel runtime helpers from the dedicated subpath", () => {
|
||||
expect(typeof channelRuntimeSdk.createChannelDirectoryAdapter).toBe("function");
|
||||
expect(typeof channelRuntimeSdk.createRuntimeOutboundDelegates).toBe("function");
|
||||
|
||||
@ -2,7 +2,26 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
import { buildRandomTempFilePath, withTempDownloadPath } from "./temp-path.js";
|
||||
import {
|
||||
buildAgentWorkspaceArtifactPath,
|
||||
buildRandomTempFilePath,
|
||||
resolveAgentWorkspaceOutputPath,
|
||||
withTempDownloadPath,
|
||||
} from "./temp-path.js";
|
||||
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/Users/admin/.openclaw/workspace",
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "ops",
|
||||
workspace: "/Users/admin/.openclaw/workspace-ops",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe("buildRandomTempFilePath", () => {
|
||||
it("builds deterministic paths when now/uuid are provided", () => {
|
||||
@ -69,3 +88,65 @@ describe("withTempDownloadPath", () => {
|
||||
expect(capturedPath).not.toContain("..");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAgentWorkspaceArtifactPath", () => {
|
||||
it("places artifacts inside the resolved agent workspace", () => {
|
||||
const output = buildAgentWorkspaceArtifactPath({
|
||||
cfg,
|
||||
agentId: "ops",
|
||||
prefix: "feishu-resource",
|
||||
preferredFileName: "weekly-report.xlsx",
|
||||
pathSegments: [".openclaw", "artifacts", "feishu"],
|
||||
now: 123,
|
||||
uuid: "abc",
|
||||
});
|
||||
|
||||
expect(output.workspaceDir).toBe("/Users/admin/.openclaw/workspace-ops");
|
||||
expect(output.absolutePath).toBe(
|
||||
"/Users/admin/.openclaw/workspace-ops/.openclaw/artifacts/feishu/weekly-report-123-abc.xlsx",
|
||||
);
|
||||
expect(output.workspacePath).toBe(".openclaw/artifacts/feishu/weekly-report-123-abc.xlsx");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveAgentWorkspaceOutputPath", () => {
|
||||
it("auto-generates a workspace-local artifact path when output_path is omitted", () => {
|
||||
const output = resolveAgentWorkspaceOutputPath({
|
||||
cfg,
|
||||
agentId: "ops",
|
||||
prefix: "drive-file",
|
||||
preferredFileName: "summary.pdf",
|
||||
pathSegments: [".openclaw", "artifacts", "feishu"],
|
||||
now: 456,
|
||||
uuid: "xyz",
|
||||
});
|
||||
|
||||
expect(output.absolutePath).toBe(
|
||||
"/Users/admin/.openclaw/workspace-ops/.openclaw/artifacts/feishu/summary-456-xyz.pdf",
|
||||
);
|
||||
expect(output.workspacePath).toBe(".openclaw/artifacts/feishu/summary-456-xyz.pdf");
|
||||
});
|
||||
|
||||
it("resolves relative output paths inside the agent workspace", () => {
|
||||
const output = resolveAgentWorkspaceOutputPath({
|
||||
cfg,
|
||||
agentId: "ops",
|
||||
prefix: "drive-file",
|
||||
outputPath: "exports/report.xlsx",
|
||||
});
|
||||
|
||||
expect(output.absolutePath).toBe("/Users/admin/.openclaw/workspace-ops/exports/report.xlsx");
|
||||
expect(output.workspacePath).toBe("exports/report.xlsx");
|
||||
});
|
||||
|
||||
it("rejects relative output paths that escape the agent workspace", () => {
|
||||
expect(() =>
|
||||
resolveAgentWorkspaceOutputPath({
|
||||
cfg,
|
||||
agentId: "ops",
|
||||
prefix: "drive-file",
|
||||
outputPath: "../outside/report.xlsx",
|
||||
}),
|
||||
).toThrow("Relative output_path must stay within the current workspace");
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import crypto from "node:crypto";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
|
||||
function sanitizePrefix(prefix: string): string {
|
||||
@ -27,6 +29,15 @@ function sanitizeFileName(fileName: string): string {
|
||||
return normalized || "download.bin";
|
||||
}
|
||||
|
||||
function sanitizeWorkspacePathSegment(segment: string): string {
|
||||
const normalized = segment
|
||||
.normalize("NFKC")
|
||||
.replace(/[\\/]+/g, "-")
|
||||
.replace(/\p{Cc}/gu, "")
|
||||
.trim();
|
||||
return normalized || "artifact";
|
||||
}
|
||||
|
||||
function resolveTempRoot(tmpDir?: string): string {
|
||||
return tmpDir ?? resolvePreferredOpenClawTmpDir();
|
||||
}
|
||||
@ -59,6 +70,115 @@ export function buildRandomTempFilePath(params: {
|
||||
return path.join(resolveTempRoot(params.tmpDir), `${prefix}-${now}-${uuid}${extension}`);
|
||||
}
|
||||
|
||||
export type WorkspaceArtifactPath = {
|
||||
workspaceDir: string;
|
||||
absolutePath: string;
|
||||
workspacePath?: string;
|
||||
};
|
||||
|
||||
function toWorkspaceRelativePath(workspaceDir: string, absolutePath: string): string | undefined {
|
||||
const relativePath = path.relative(workspaceDir, absolutePath);
|
||||
if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
||||
return undefined;
|
||||
}
|
||||
return relativePath.split(path.sep).join(path.posix.sep);
|
||||
}
|
||||
|
||||
function normalizeWorkspaceExtension(params: {
|
||||
extension?: string;
|
||||
preferredFileName?: string;
|
||||
}): string {
|
||||
if (params.extension) {
|
||||
return sanitizeExtension(params.extension);
|
||||
}
|
||||
const preferredExt = params.preferredFileName ? path.extname(params.preferredFileName) : "";
|
||||
return sanitizeExtension(preferredExt);
|
||||
}
|
||||
|
||||
function buildWorkspaceArtifactFileName(params: {
|
||||
prefix: string;
|
||||
extension?: string;
|
||||
preferredFileName?: string;
|
||||
now?: number;
|
||||
uuid?: string;
|
||||
}): string {
|
||||
const baseName = params.preferredFileName
|
||||
? path.basename(params.preferredFileName, path.extname(params.preferredFileName))
|
||||
: params.prefix;
|
||||
const normalizedBase = sanitizeWorkspacePathSegment(baseName);
|
||||
const nowCandidate = params.now;
|
||||
const now =
|
||||
typeof nowCandidate === "number" && Number.isFinite(nowCandidate)
|
||||
? Math.trunc(nowCandidate)
|
||||
: Date.now();
|
||||
const uuid = params.uuid?.trim() || crypto.randomUUID();
|
||||
return `${normalizedBase}-${now}-${uuid}${normalizeWorkspaceExtension(params)}`;
|
||||
}
|
||||
|
||||
function normalizeArtifactSegments(segments?: string[]): string[] {
|
||||
const normalized = (segments ?? [".openclaw", "artifacts", "downloads"])
|
||||
.map((segment) => sanitizeWorkspacePathSegment(segment))
|
||||
.filter(Boolean);
|
||||
return normalized.length > 0 ? normalized : [".openclaw", "artifacts", "downloads"];
|
||||
}
|
||||
|
||||
export function buildAgentWorkspaceArtifactPath(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
prefix: string;
|
||||
extension?: string;
|
||||
preferredFileName?: string;
|
||||
pathSegments?: string[];
|
||||
now?: number;
|
||||
uuid?: string;
|
||||
}): WorkspaceArtifactPath {
|
||||
const workspaceDir = path.resolve(resolveAgentWorkspaceDir(params.cfg, params.agentId));
|
||||
const fileName = buildWorkspaceArtifactFileName(params);
|
||||
const absolutePath = path.join(
|
||||
workspaceDir,
|
||||
...normalizeArtifactSegments(params.pathSegments),
|
||||
fileName,
|
||||
);
|
||||
return {
|
||||
workspaceDir,
|
||||
absolutePath,
|
||||
workspacePath: toWorkspaceRelativePath(workspaceDir, absolutePath),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAgentWorkspaceOutputPath(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
outputPath?: string;
|
||||
prefix: string;
|
||||
extension?: string;
|
||||
preferredFileName?: string;
|
||||
pathSegments?: string[];
|
||||
now?: number;
|
||||
uuid?: string;
|
||||
}): WorkspaceArtifactPath {
|
||||
if (!params.outputPath?.trim()) {
|
||||
return buildAgentWorkspaceArtifactPath(params);
|
||||
}
|
||||
|
||||
const workspaceDir = path.resolve(resolveAgentWorkspaceDir(params.cfg, params.agentId));
|
||||
const rawOutputPath = params.outputPath.trim();
|
||||
const absolutePath = path.isAbsolute(rawOutputPath)
|
||||
? rawOutputPath
|
||||
: path.resolve(workspaceDir, rawOutputPath);
|
||||
const workspacePath = toWorkspaceRelativePath(workspaceDir, absolutePath);
|
||||
|
||||
if (!path.isAbsolute(rawOutputPath) && !workspacePath) {
|
||||
throw new Error("Relative output_path must stay within the current workspace");
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceDir,
|
||||
absolutePath,
|
||||
workspacePath,
|
||||
};
|
||||
}
|
||||
|
||||
/** Create a temporary download directory, run the callback, then clean it up best-effort. */
|
||||
export async function withTempDownloadPath<T>(
|
||||
params: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user