import { createHash } from "node:crypto"; export class HistoryCursorError extends Error {} type Cursor = { scope: string; kind: string; position?: number; anchor?: string; updatedAt?: string; id?: string }; export function historyScope(value: unknown) { return createHash("sha256").update(JSON.stringify(value)).digest("hex"); } function encode(value: Cursor) { return Buffer.from(JSON.stringify(value)).toString("base64url"); } function decode(value: string, scope: string, kind: string): Cursor { try { const cursor = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as Cursor; if (cursor.scope !== scope || cursor.kind !== kind) throw new Error(); return cursor; } catch { throw new HistoryCursorError("invalid history cursor"); } } export function sessionHistoryPage( records: T[], options: { scope: string; q?: string; limit: number; cursor?: string }, ) { const query = (options.q ?? "").trim().toLocaleLowerCase(); const scope = historyScope([options.scope, query]); const boundary = options.cursor ? decode(options.cursor, scope, "sessions") : null; if (boundary && (typeof boundary.updatedAt !== "string" || typeof boundary.id !== "string")) { throw new HistoryCursorError("invalid history cursor"); } const ordered = records.filter((record) => (record.title ?? "新对话").toLocaleLowerCase().includes(query) || Boolean(query && record.searchSnippet)) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || b.sessionId.localeCompare(a.sessionId)); const remaining = boundary ? ordered.filter((record) => record.updatedAt < boundary.updatedAt! || (record.updatedAt === boundary.updatedAt && record.sessionId.localeCompare(boundary.id!) < 0)) : ordered; const sessions = remaining.slice(0, options.limit); const has_more = remaining.length > sessions.length; const last = sessions.at(-1); return { sessions, has_more, next_cursor: has_more && last ? encode({ scope, kind: "sessions", updatedAt: last.updatedAt, id: last.sessionId }) : null }; } function messageAnchor(message: unknown) { if (message && typeof message === "object" && "id" in message && typeof message.id === "string") return message.id; return historyScope(message); } export function sessionMessagePage(messages: unknown[], options: { scope: string; limit: number; before?: string }) { const boundary = options.before ? decode(options.before, options.scope, "messages") : null; const end = boundary?.position ?? messages.length; if (!Number.isInteger(end) || end < 0 || end > messages.length || (boundary && (end >= messages.length || messageAnchor(messages[end]) !== boundary.anchor))) { throw new HistoryCursorError("message history changed; reload this conversation"); } const start = Math.max(0, end - options.limit); return { messages: messages.slice(start, end), message_page: { has_more: start > 0, before_cursor: start > 0 ? encode({ scope: options.scope, kind: "messages", position: start, anchor: messageAnchor(messages[start]) }) : null }, }; }