Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
262 lines
6.9 KiB
TypeScript
262 lines
6.9 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import type { ArtifactStore } from "@rakazo/adapter-kit";
|
|
import type { Actor } from "@rakazo/contracts";
|
|
import { ATTACHMENT_MAX_COUNT } from "@rakazo/contracts";
|
|
import {
|
|
AttachmentValidationError,
|
|
decodeAttachmentBase64,
|
|
messageBlockForArtifact,
|
|
promptTextForAttachments,
|
|
validateAttachmentMimeType,
|
|
} from "@rakazo/core";
|
|
import { IsolationError, type PrismaClient } from "@rakazo/db";
|
|
|
|
function adapterContext(actor: Actor, botId: string, operationId: string) {
|
|
return {
|
|
operationId,
|
|
traceId: operationId,
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
botId,
|
|
signal: new AbortController().signal,
|
|
};
|
|
}
|
|
|
|
export async function createOwnedArtifact(
|
|
deps: {
|
|
prisma: PrismaClient;
|
|
artifacts: ArtifactStore;
|
|
},
|
|
actor: Actor,
|
|
input: {
|
|
botId: string;
|
|
groupId?: string;
|
|
name: string;
|
|
mimeType: string;
|
|
contentBase64: string;
|
|
},
|
|
) {
|
|
validateAttachmentMimeType(input.mimeType);
|
|
const bytes = decodeAttachmentBase64(input.contentBase64);
|
|
const context = adapterContext(actor, input.botId, `artifact-create:${input.botId}`);
|
|
const stored = await deps.artifacts.put(
|
|
{ name: input.name, mimeType: input.mimeType, bytes },
|
|
context,
|
|
);
|
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
const row = await deps.prisma.artifact
|
|
.create({
|
|
data: {
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
botId: input.botId,
|
|
groupId: input.groupId,
|
|
name: input.name,
|
|
mimeType: input.mimeType,
|
|
size: bytes.byteLength,
|
|
hash,
|
|
storageKey: stored.id,
|
|
},
|
|
})
|
|
.catch(async (error) => {
|
|
await deps.artifacts.remove(stored.id, context).catch(() => undefined);
|
|
throw error;
|
|
});
|
|
return {
|
|
id: row.id,
|
|
botId: row.botId,
|
|
groupId: row.groupId,
|
|
runId: row.runId,
|
|
name: row.name,
|
|
mimeType: row.mimeType,
|
|
size: row.size,
|
|
createdAt: row.createdAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
export async function getOwnedArtifact(
|
|
deps: {
|
|
prisma: PrismaClient;
|
|
artifacts: ArtifactStore;
|
|
},
|
|
actor: Actor,
|
|
input: { botId: string; artifactId: string },
|
|
) {
|
|
const row = await deps.prisma.artifact.findFirst({
|
|
where: {
|
|
id: input.artifactId,
|
|
botId: input.botId,
|
|
groupId: null,
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
},
|
|
});
|
|
if (!row) throw new IsolationError();
|
|
return readArtifact(deps.artifacts, actor, row, input.botId);
|
|
}
|
|
|
|
export async function getSpaceArtifact(
|
|
deps: {
|
|
prisma: PrismaClient;
|
|
artifacts: ArtifactStore;
|
|
},
|
|
actor: Actor,
|
|
input: { artifactId: string; groupId: string; contextBotId: string },
|
|
) {
|
|
const row = await deps.prisma.artifact.findFirst({
|
|
where: {
|
|
id: input.artifactId,
|
|
groupId: input.groupId,
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
},
|
|
});
|
|
if (!row) throw new IsolationError();
|
|
return readArtifact(deps.artifacts, actor, row, input.contextBotId);
|
|
}
|
|
|
|
async function readArtifact(
|
|
artifacts: ArtifactStore,
|
|
actor: Actor,
|
|
row: {
|
|
id: string;
|
|
botId: string | null;
|
|
groupId: string | null;
|
|
runId: string | null;
|
|
storageKey: string;
|
|
name: string;
|
|
mimeType: string;
|
|
size: number;
|
|
createdAt: Date;
|
|
},
|
|
contextBotId: string,
|
|
) {
|
|
const bytes = await artifacts.get(
|
|
row.storageKey,
|
|
adapterContext(actor, contextBotId, `artifact-get:${row.id}`),
|
|
);
|
|
return {
|
|
id: row.id,
|
|
botId: row.botId,
|
|
groupId: row.groupId,
|
|
runId: row.runId,
|
|
name: row.name,
|
|
mimeType: row.mimeType,
|
|
size: row.size,
|
|
createdAt: row.createdAt.toISOString(),
|
|
contentBase64: Buffer.from(bytes).toString("base64"),
|
|
};
|
|
}
|
|
|
|
type SendAttachmentRow = {
|
|
id: string;
|
|
name: string;
|
|
mimeType: string;
|
|
size: number;
|
|
storageKey: string;
|
|
};
|
|
|
|
function normalizeAttachmentIds(artifactIds: string[] | undefined) {
|
|
const ids = [...new Set(artifactIds ?? [])];
|
|
if (ids.length > ATTACHMENT_MAX_COUNT) {
|
|
throw new AttachmentValidationError(`At most ${ATTACHMENT_MAX_COUNT} attachments per message`);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
function toAttachmentResolution<T extends SendAttachmentRow>(ids: string[], rows: T[]) {
|
|
if (rows.length !== ids.length) throw new IsolationError();
|
|
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
const artifacts = ids.map((id) => byId.get(id)!);
|
|
const blocks = artifacts.map((row) =>
|
|
messageBlockForArtifact({
|
|
id: row.id,
|
|
name: row.name,
|
|
mimeType: row.mimeType,
|
|
size: row.size,
|
|
}),
|
|
);
|
|
return { blocks, artifacts };
|
|
}
|
|
|
|
export async function resolveSendAttachments(
|
|
deps: { prisma: Pick<PrismaClient, "artifact"> },
|
|
actor: Actor,
|
|
botId: string,
|
|
artifactIds: string[] | undefined,
|
|
) {
|
|
const ids = normalizeAttachmentIds(artifactIds);
|
|
if (!ids.length) return toAttachmentResolution(ids, [] as SendAttachmentRow[]);
|
|
|
|
const rows = await deps.prisma.artifact.findMany({
|
|
where: {
|
|
id: { in: ids },
|
|
botId,
|
|
groupId: null,
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
},
|
|
});
|
|
return toAttachmentResolution(ids, rows);
|
|
}
|
|
|
|
export async function resolveGroupSendAttachments(
|
|
deps: { prisma: Pick<PrismaClient, "artifact"> },
|
|
actor: Actor,
|
|
groupId: string,
|
|
memberBotIds: string[],
|
|
artifactIds: string[] | undefined,
|
|
) {
|
|
const ids = normalizeAttachmentIds(artifactIds);
|
|
if (!ids.length) return toAttachmentResolution(ids, [] as SendAttachmentRow[]);
|
|
|
|
const rows = await deps.prisma.artifact.findMany({
|
|
where: {
|
|
id: { in: ids },
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
OR: [
|
|
{ groupId },
|
|
// Accept artifacts uploaded by a current member before group ownership
|
|
// was persisted. Removing that member revokes this legacy fallback.
|
|
{ groupId: null, botId: { in: memberBotIds } },
|
|
],
|
|
},
|
|
});
|
|
return toAttachmentResolution(ids, rows);
|
|
}
|
|
|
|
export function buildUserMessageBlocks(
|
|
text: string | undefined,
|
|
attachmentBlocks: ReturnType<typeof messageBlockForArtifact>[],
|
|
) {
|
|
const blocks = [];
|
|
const caption = text?.trim();
|
|
if (caption) blocks.push({ kind: "text" as const, text: caption });
|
|
blocks.push(...attachmentBlocks);
|
|
return blocks;
|
|
}
|
|
|
|
export function buildSendPrompt(
|
|
text: string | undefined,
|
|
artifacts: Array<{ name: string; mimeType: string; size: number }>,
|
|
connectorNames: string[] = [],
|
|
) {
|
|
const prompt = promptTextForAttachments(text, artifacts);
|
|
if (connectorNames.length === 0) return prompt;
|
|
const marker = "Use these connectors if relevant:";
|
|
const existing = new RegExp(`^${marker} (.*)\\.$`, "m").exec(prompt);
|
|
const names = [
|
|
...new Set([
|
|
...(existing?.[1]
|
|
?.split(",")
|
|
.map((name) => name.trim())
|
|
.filter(Boolean) ?? []),
|
|
...connectorNames.map((name) => name.trim()).filter(Boolean),
|
|
]),
|
|
];
|
|
const line = `${marker} ${names.join(", ")}.`;
|
|
if (existing) return prompt.replace(existing[0], () => line);
|
|
return prompt ? `${prompt}\n\n${line}` : line;
|
|
}
|