mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 10:10:42 +00:00
179 lines
4.8 KiB
TypeScript
179 lines
4.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import path from "node:path";
|
|
import {
|
|
ackJsonDurableQueueEntry,
|
|
ensureJsonDurableQueueDirs,
|
|
jsonDurableQueueEntryExists,
|
|
loadJsonDurableQueueEntry,
|
|
loadPendingJsonDurableQueueEntries,
|
|
moveJsonDurableQueueEntryToFailed,
|
|
readJsonDurableQueueEntry,
|
|
resolveJsonDurableQueueEntryPaths,
|
|
writeJsonDurableQueueEntry,
|
|
} from "@openclaw/fs-safe/store";
|
|
import type { ChatType } from "../channels/chat-type.js";
|
|
import { resolveStateDir } from "../config/paths.js";
|
|
import { generateSecureUuid } from "./secure-random.js";
|
|
|
|
const QUEUE_DIRNAME = "session-delivery-queue";
|
|
const FAILED_DIRNAME = "failed";
|
|
const TMP_SWEEP_MAX_AGE_MS = 5_000;
|
|
const QUEUE_TEMP_PREFIX = ".session-delivery-queue";
|
|
|
|
type SessionDeliveryContext = {
|
|
channel?: string;
|
|
to?: string;
|
|
accountId?: string;
|
|
threadId?: string | number;
|
|
};
|
|
|
|
export type SessionDeliveryRoute = {
|
|
channel: string;
|
|
to: string;
|
|
accountId?: string;
|
|
replyToId?: string;
|
|
threadId?: string;
|
|
chatType: ChatType;
|
|
};
|
|
|
|
export type QueuedSessionDeliveryPayload =
|
|
| {
|
|
kind: "systemEvent";
|
|
sessionKey: string;
|
|
text: string;
|
|
deliveryContext?: SessionDeliveryContext;
|
|
idempotencyKey?: string;
|
|
}
|
|
| {
|
|
kind: "agentTurn";
|
|
sessionKey: string;
|
|
message: string;
|
|
messageId: string;
|
|
route?: SessionDeliveryRoute;
|
|
deliveryContext?: SessionDeliveryContext;
|
|
idempotencyKey?: string;
|
|
};
|
|
|
|
export type QueuedSessionDelivery = QueuedSessionDeliveryPayload & {
|
|
id: string;
|
|
enqueuedAt: number;
|
|
retryCount: number;
|
|
lastAttemptAt?: number;
|
|
lastError?: string;
|
|
};
|
|
|
|
function buildEntryId(idempotencyKey?: string): string {
|
|
if (!idempotencyKey) {
|
|
return generateSecureUuid();
|
|
}
|
|
return createHash("sha256").update(idempotencyKey).digest("hex");
|
|
}
|
|
|
|
async function writeQueueEntry(filePath: string, entry: QueuedSessionDelivery): Promise<void> {
|
|
await writeJsonDurableQueueEntry({
|
|
filePath,
|
|
entry,
|
|
tempPrefix: QUEUE_TEMP_PREFIX,
|
|
});
|
|
}
|
|
|
|
async function readQueueEntry(filePath: string): Promise<QueuedSessionDelivery> {
|
|
return await readJsonDurableQueueEntry<QueuedSessionDelivery>(filePath);
|
|
}
|
|
|
|
export function resolveSessionDeliveryQueueDir(stateDir?: string): string {
|
|
const base = stateDir ?? resolveStateDir();
|
|
return path.join(base, QUEUE_DIRNAME);
|
|
}
|
|
|
|
function resolveFailedDir(stateDir?: string): string {
|
|
return path.join(resolveSessionDeliveryQueueDir(stateDir), FAILED_DIRNAME);
|
|
}
|
|
|
|
function resolveQueueEntryPaths(
|
|
id: string,
|
|
stateDir?: string,
|
|
): {
|
|
jsonPath: string;
|
|
deliveredPath: string;
|
|
} {
|
|
return resolveJsonDurableQueueEntryPaths(resolveSessionDeliveryQueueDir(stateDir), id);
|
|
}
|
|
|
|
async function ensureSessionDeliveryQueueDir(stateDir?: string): Promise<string> {
|
|
const queueDir = resolveSessionDeliveryQueueDir(stateDir);
|
|
await ensureJsonDurableQueueDirs({
|
|
queueDir,
|
|
failedDir: resolveFailedDir(stateDir),
|
|
});
|
|
return queueDir;
|
|
}
|
|
|
|
export async function enqueueSessionDelivery(
|
|
params: QueuedSessionDeliveryPayload,
|
|
stateDir?: string,
|
|
): Promise<string> {
|
|
const queueDir = await ensureSessionDeliveryQueueDir(stateDir);
|
|
const id = buildEntryId(params.idempotencyKey);
|
|
const filePath = path.join(queueDir, `${id}.json`);
|
|
|
|
if (params.idempotencyKey) {
|
|
if (await jsonDurableQueueEntryExists(filePath)) {
|
|
return id;
|
|
}
|
|
}
|
|
|
|
await writeQueueEntry(filePath, {
|
|
...params,
|
|
id,
|
|
enqueuedAt: Date.now(),
|
|
retryCount: 0,
|
|
});
|
|
return id;
|
|
}
|
|
|
|
export async function ackSessionDelivery(id: string, stateDir?: string): Promise<void> {
|
|
await ackJsonDurableQueueEntry(resolveQueueEntryPaths(id, stateDir));
|
|
}
|
|
|
|
export async function failSessionDelivery(
|
|
id: string,
|
|
error: string,
|
|
stateDir?: string,
|
|
): Promise<void> {
|
|
const filePath = path.join(resolveSessionDeliveryQueueDir(stateDir), `${id}.json`);
|
|
const entry = await readQueueEntry(filePath);
|
|
entry.retryCount += 1;
|
|
entry.lastAttemptAt = Date.now();
|
|
entry.lastError = error;
|
|
await writeQueueEntry(filePath, entry);
|
|
}
|
|
|
|
export async function loadPendingSessionDelivery(
|
|
id: string,
|
|
stateDir?: string,
|
|
): Promise<QueuedSessionDelivery | null> {
|
|
return await loadJsonDurableQueueEntry({
|
|
paths: resolveQueueEntryPaths(id, stateDir),
|
|
tempPrefix: QUEUE_TEMP_PREFIX,
|
|
});
|
|
}
|
|
|
|
export async function loadPendingSessionDeliveries(
|
|
stateDir?: string,
|
|
): Promise<QueuedSessionDelivery[]> {
|
|
return await loadPendingJsonDurableQueueEntries({
|
|
queueDir: resolveSessionDeliveryQueueDir(stateDir),
|
|
tempPrefix: QUEUE_TEMP_PREFIX,
|
|
cleanupTmpMaxAgeMs: TMP_SWEEP_MAX_AGE_MS,
|
|
});
|
|
}
|
|
|
|
export async function moveSessionDeliveryToFailed(id: string, stateDir?: string): Promise<void> {
|
|
await moveJsonDurableQueueEntryToFailed({
|
|
queueDir: resolveSessionDeliveryQueueDir(stateDir),
|
|
failedDir: resolveFailedDir(stateDir),
|
|
id,
|
|
});
|
|
}
|