/** * Filesystem-backed pending upload store for the FileConsentCard flow. * * The CLI `message send --media` path runs in a different process from the * gateway's bot monitor that receives the `fileConsent/invoke` callback. * An in-memory `pending-uploads.ts` store cannot bridge those processes, so * when the user clicks "Allow" the monitor handler's lookup misses and the * user sees "card action not supported". * * This FS store persists pending uploads to a JSON file (with the file buffer * base64-encoded) so any process that shares the OpenClaw state dir can read * them back. The in-memory store in `pending-uploads.ts` is still the fast * path for same-process flows (for example the messenger reply path); this FS * store is a cross-process fallback.
*/
import { resolveMSTeamsStorePath } from "./storage.js"; import { readJsonFile, withFileLock, writeJsonFile } from "./store-fs.js";
export type PendingUploadFsRecord = {
id: string;
bufferBase64: string;
filename: string;
contentType?: string;
conversationId: string; /** Activity ID of the original FileConsentCard, used to replace it after upload */
consentCardActivityId?: string;
createdAt: number;
};
/** * Persist a pending upload record so another process can read it back. * Pass in the pre-generated id (same as the one placed in the consent card * context) so the in-memory and FS stores share the same key.
*/
export async function storePendingUploadFs(
upload: {
id: string;
buffer: Buffer;
filename: string;
contentType?: string;
conversationId: string;
consentCardActivityId?: string;
},
options?: PendingUploadsFsOptions,
): Promise<void> { const ttlMs = options?.ttlMs ?? PENDING_UPLOAD_TTL_MS; const filePath = resolveFilePath(options);
await withFileLock(filePath, empty, async () => { const store = await readStore(filePath, ttlMs);
store.uploads[upload.id] = {
id: upload.id,
bufferBase64: upload.buffer.toString("base64"),
filename: upload.filename,
contentType: upload.contentType,
conversationId: upload.conversationId,
consentCardActivityId: upload.consentCardActivityId,
createdAt: Date.now(),
};
store.uploads = pruneToLimit(pruneExpired(store.uploads, Date.now(), ttlMs));
await writeJsonFile(filePath, store);
});
}
/** * Retrieve a persisted pending upload. Expired entries are treated as absent.
*/
export async function getPendingUploadFs(
id: string | undefined,
options?: PendingUploadsFsOptions,
): Promise<PendingUploadFs | undefined> { if (!id) { return undefined;
} const ttlMs = options?.ttlMs ?? PENDING_UPLOAD_TTL_MS; const filePath = resolveFilePath(options); const store = await readStore(filePath, ttlMs); const record = store.uploads[id]; if (!record) { return undefined;
} if (Date.now() - record.createdAt > ttlMs) { return undefined;
} return recordToUpload(record);
}
/** * Remove a persisted pending upload (after successful upload or decline). * No-op if the entry is already gone.
*/
export async function removePendingUploadFs(
id: string | undefined,
options?: PendingUploadsFsOptions,
): Promise<void> { if (!id) { return;
} const ttlMs = options?.ttlMs ?? PENDING_UPLOAD_TTL_MS; const filePath = resolveFilePath(options);
await withFileLock(filePath, empty, async () => { const store = await readStore(filePath, ttlMs); if (!(id in store.uploads)) { return;
} delete store.uploads[id];
await writeJsonFile(filePath, store);
});
}
/** * Set the consent card activity ID on a persisted entry. Called after the * FileConsentCard activity is sent and we know its message id.
*/
export async function setPendingUploadActivityIdFs(
id: string,
activityId: string,
options?: PendingUploadsFsOptions,
): Promise<void> { const ttlMs = options?.ttlMs ?? PENDING_UPLOAD_TTL_MS; const filePath = resolveFilePath(options);
await withFileLock(filePath, empty, async () => { const store = await readStore(filePath, ttlMs); const record = store.uploads[id]; if (!record) { return;
}
record.consentCardActivityId = activityId;
await writeJsonFile(filePath, store);
});
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-05-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.