feat(chat): add local-first conversation workspace

This commit is contained in:
2026-08-13 15:55:15 +08:00
parent d1d50d722c
commit 8991f78f9f
43 changed files with 5450 additions and 592 deletions
+29
View File
@@ -0,0 +1,29 @@
import type {StoredChatMessage} from "./conversation-types";
function canonicalValue(value: unknown): unknown {
if (Array.isArray(value)) return value.map(canonicalValue);
if (!value || typeof value !== "object") return value;
return Object.fromEntries(Object.entries(value as Record<string, unknown>)
.filter(([, entry]) => entry !== undefined)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, entry]) => [key, canonicalValue(entry)]));
}
export function canonicalMessage(message: Omit<StoredChatMessage, "id">) {
return JSON.stringify(canonicalValue(message));
}
export async function messageObjectId(message: Omit<StoredChatMessage, "id">, namespace = "") {
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${namespace}\0${canonicalMessage(message)}`));
return `sha256:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
}
export async function createMessageObject(message: Omit<StoredChatMessage, "id">, namespace = ""): Promise<StoredChatMessage> {
return {...message, id: await messageObjectId(message, namespace)};
}
export async function validMessageObjectId(message: StoredChatMessage, namespace = "") {
if (!message.id.startsWith("sha256:")) return false;
const {id: _id, ...content} = message;
return message.id === await messageObjectId(content, namespace);
}