mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:40:44 +00:00
[codex] Extract filesystem safety primitives (#77918)
* refactor: extract filesystem safety primitives * refactor: use fs-safe for file access helpers * refactor: reuse fs-safe for media reads * refactor: use fs-safe for image reads * refactor: reuse fs-safe in qqbot media opener * refactor: reuse fs-safe for local media checks * refactor: consume cleaner fs-safe api * refactor: align fs-safe json option names * fix: preserve fs-safe migration contracts * refactor: use fs-safe primitive subpaths * refactor: use grouped fs-safe subpaths * refactor: align fs-safe api usage * refactor: adapt private state store api * chore: refresh proof gate * refactor: follow fs-safe json api split * refactor: follow reduced fs-safe surface * build: default fs-safe python helper off * fix: preserve fs-safe plugin sdk aliases * refactor: consolidate fs-safe usage * refactor: unify fs-safe store usage * refactor: trim fs-safe temp workspace usage * refactor: hide low-level fs-safe primitives * build: use published fs-safe package * fix: preserve outbound recovery durability after rebase * chore: refresh pr checks
This commit is contained in:
committed by
GitHub
parent
61481eb34f
commit
538605ff44
@@ -1,3 +1,4 @@
|
||||
import { realpathSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
|
||||
@@ -61,7 +62,7 @@ function expectPathIsolatedToTmpRoot(pathValue: string, key: string): void {
|
||||
expect(pathValue).not.toContain(key);
|
||||
expect(pathValue).not.toContain("..");
|
||||
|
||||
const tmpRoot = path.resolve(resolvePreferredOpenClawTmpDir());
|
||||
const tmpRoot = realpathSync(resolvePreferredOpenClawTmpDir());
|
||||
const resolved = path.resolve(pathValue);
|
||||
const rel = path.relative(tmpRoot, resolved);
|
||||
expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
|
||||
|
||||
@@ -5,8 +5,10 @@ import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { MessageReceipt } from "openclaw/plugin-sdk/channel-message";
|
||||
import { mediaKindFromMime } from "openclaw/plugin-sdk/media-mime";
|
||||
import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS, runFfmpeg } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { readRegularFile } from "openclaw/plugin-sdk/security-runtime";
|
||||
import {
|
||||
resolvePreferredOpenClawTmpDir,
|
||||
withTempWorkspace,
|
||||
withTempDownloadPath,
|
||||
} from "openclaw/plugin-sdk/temp-path";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
@@ -421,10 +423,11 @@ export async function uploadImageFeishu(params: {
|
||||
const { cfg, image, imageType = "message", accountId } = params;
|
||||
const { client } = createConfiguredFeishuMediaClient({ cfg, accountId });
|
||||
|
||||
// SDK accepts Buffer directly or fs.ReadStream for file paths
|
||||
// Using Readable.from(buffer) causes issues with form-data library
|
||||
// SDK accepts Buffer directly. Keep string path support on this helper, but
|
||||
// verify the path as a regular local file before uploading it.
|
||||
// See: https://github.com/larksuite/node-sdk/issues/121
|
||||
const imageData = typeof image === "string" ? fs.createReadStream(image) : image;
|
||||
const imageData =
|
||||
typeof image === "string" ? (await readRegularFile({ filePath: image })).buffer : image;
|
||||
|
||||
const response = await requestFeishuApi(
|
||||
() =>
|
||||
@@ -475,10 +478,11 @@ export async function uploadFileFeishu(params: {
|
||||
const { cfg, file, fileName, fileType, duration, accountId } = params;
|
||||
const { client } = createConfiguredFeishuMediaClient({ cfg, accountId });
|
||||
|
||||
// SDK accepts Buffer directly or fs.ReadStream for file paths
|
||||
// Using Readable.from(buffer) causes issues with form-data library
|
||||
// SDK accepts Buffer directly. Keep string path support on this helper, but
|
||||
// verify the path as a regular local file before uploading it.
|
||||
// See: https://github.com/larksuite/node-sdk/issues/121
|
||||
const fileData = typeof file === "string" ? fs.createReadStream(file) : file;
|
||||
const fileData =
|
||||
typeof file === "string" ? (await readRegularFile({ filePath: file })).buffer : file;
|
||||
|
||||
const safeFileName = sanitizeFileNameForUpload(fileName);
|
||||
|
||||
@@ -747,45 +751,42 @@ async function transcodeToFeishuVoiceOpus(params: {
|
||||
fileName: string;
|
||||
contentType?: string;
|
||||
}): Promise<{ buffer: Buffer; fileName: string; contentType: string }> {
|
||||
const tempRoot = resolvePreferredOpenClawTmpDir();
|
||||
await fs.promises.mkdir(tempRoot, { recursive: true, mode: 0o700 });
|
||||
const tempDir = await fs.promises.mkdtemp(path.join(tempRoot, "feishu-voice-"));
|
||||
try {
|
||||
const ext = normalizeLowercaseStringOrEmpty(path.extname(params.fileName));
|
||||
const inputExt = ext && ext.length <= 12 ? ext : ".audio";
|
||||
const inputPath = path.join(tempDir, `input${inputExt}`);
|
||||
const outputPath = path.join(tempDir, FEISHU_VOICE_FILE_NAME);
|
||||
await fs.promises.writeFile(inputPath, params.buffer, { mode: 0o600 });
|
||||
await runFfmpeg([
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-y",
|
||||
"-i",
|
||||
inputPath,
|
||||
"-vn",
|
||||
"-sn",
|
||||
"-dn",
|
||||
"-t",
|
||||
String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS),
|
||||
"-ar",
|
||||
String(FEISHU_VOICE_SAMPLE_RATE_HZ),
|
||||
"-ac",
|
||||
"1",
|
||||
"-c:a",
|
||||
"libopus",
|
||||
"-b:a",
|
||||
FEISHU_VOICE_BITRATE,
|
||||
outputPath,
|
||||
]);
|
||||
return {
|
||||
buffer: await fs.promises.readFile(outputPath),
|
||||
fileName: FEISHU_VOICE_FILE_NAME,
|
||||
contentType: "audio/ogg",
|
||||
};
|
||||
} finally {
|
||||
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
return await withTempWorkspace(
|
||||
{ rootDir: resolvePreferredOpenClawTmpDir(), prefix: "feishu-voice-" },
|
||||
async (workspace) => {
|
||||
const ext = normalizeLowercaseStringOrEmpty(path.extname(params.fileName));
|
||||
const inputExt = ext && ext.length <= 12 ? ext : ".audio";
|
||||
const inputPath = await workspace.write(`input${inputExt}`, params.buffer);
|
||||
const outputPath = workspace.path(FEISHU_VOICE_FILE_NAME);
|
||||
await runFfmpeg([
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-y",
|
||||
"-i",
|
||||
inputPath,
|
||||
"-vn",
|
||||
"-sn",
|
||||
"-dn",
|
||||
"-t",
|
||||
String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS),
|
||||
"-ar",
|
||||
String(FEISHU_VOICE_SAMPLE_RATE_HZ),
|
||||
"-ac",
|
||||
"1",
|
||||
"-c:a",
|
||||
"libopus",
|
||||
"-b:a",
|
||||
FEISHU_VOICE_BITRATE,
|
||||
outputPath,
|
||||
]);
|
||||
return {
|
||||
buffer: await workspace.read(FEISHU_VOICE_FILE_NAME),
|
||||
fileName: FEISHU_VOICE_FILE_NAME,
|
||||
contentType: "audio/ogg",
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function prepareFeishuVoiceMedia(params: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
attachChannelToResult,
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
sendPayloadMediaSequenceAndFinalize,
|
||||
sendTextMediaPayload,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
import { statRegularFileSync } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuCardInteractionEnvelope } from "./card-interaction.js";
|
||||
@@ -66,18 +66,12 @@ function normalizePossibleLocalImagePath(text: string | undefined): string | nul
|
||||
if (!path.isAbsolute(raw)) {
|
||||
return null;
|
||||
}
|
||||
if (!fs.existsSync(raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fix race condition: wrap statSync in try-catch to handle file deletion
|
||||
// between existsSync and statSync
|
||||
try {
|
||||
if (!fs.statSync(raw).isFile()) {
|
||||
const stat = statRegularFileSync(raw);
|
||||
if (stat.missing) {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
// File may have been deleted or became inaccessible between checks
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user