179 lines · 5.9 KB · TypeScript
RawDownload
import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Path from "effect/Path";import {  type ClientOrchestrationCommand,  type IsoDateTime,  type OrchestrationCommand,  OrchestrationDispatchCommandError,  PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,} from "@t3tools/contracts"; import { createAttachmentId, resolveAttachmentPath } from "../attachmentStore.ts";import { ServerConfig } from "../config.ts";import { parseBase64DataUrl } from "../imageMime.ts";import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; export const canonicalizeClientCommandTimestamps = (  command: ClientOrchestrationCommand,  receivedAt: IsoDateTime,): ClientOrchestrationCommand => {  const canonicalCommand =    "createdAt" in command      ? {          ...command,          createdAt: receivedAt,        }      : command;   if (canonicalCommand.type !== "thread.turn.start" || !canonicalCommand.bootstrap?.createThread) {    return canonicalCommand;  }   return {    ...canonicalCommand,    bootstrap: {      ...canonicalCommand.bootstrap,      createThread: {        ...canonicalCommand.bootstrap.createThread,        createdAt: receivedAt,      },    },  };}; export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>  Effect.gen(function* () {    const receivedAt = DateTime.formatIso(yield* DateTime.now);    const canonicalCommand = canonicalizeClientCommandTimestamps(command, receivedAt);    const fileSystem = yield* FileSystem.FileSystem;    const path = yield* Path.Path;    const serverConfig = yield* ServerConfig;    const workspacePaths = yield* WorkspacePaths.WorkspacePaths;     const normalizeProjectWorkspaceRoot = (workspaceRoot: string) =>      workspacePaths.normalizeWorkspaceRoot(workspaceRoot).pipe(        Effect.mapError(          (cause) =>            new OrchestrationDispatchCommandError({              message: cause.message,            }),        ),      );     const normalizeProjectWorkspaceRootForCreate = (      workspaceRoot: string,      createIfMissing: boolean | undefined,    ) =>      workspacePaths        .normalizeWorkspaceRoot(workspaceRoot, {          createIfMissing: createIfMissing === true,        })        .pipe(          Effect.mapError(            (cause) =>              new OrchestrationDispatchCommandError({                message: cause.message,              }),          ),        );     if (canonicalCommand.type === "project.create") {      return {        ...canonicalCommand,        workspaceRoot: yield* normalizeProjectWorkspaceRootForCreate(          canonicalCommand.workspaceRoot,          canonicalCommand.createWorkspaceRootIfMissing,        ),        createWorkspaceRootIfMissing: canonicalCommand.createWorkspaceRootIfMissing === true,      } satisfies OrchestrationCommand;    }     if (      canonicalCommand.type === "project.meta.update" &&      canonicalCommand.workspaceRoot !== undefined    ) {      return {        ...canonicalCommand,        workspaceRoot: yield* normalizeProjectWorkspaceRoot(canonicalCommand.workspaceRoot),      } satisfies OrchestrationCommand;    }     if (canonicalCommand.type !== "thread.turn.start") {      return canonicalCommand as OrchestrationCommand;    }     const normalizedAttachments = yield* Effect.forEach(      canonicalCommand.message.attachments,      (attachment) =>        Effect.gen(function* () {          const parsed = parseBase64DataUrl(attachment.dataUrl);          if (!parsed || !parsed.mimeType.startsWith("image/")) {            return yield* new OrchestrationDispatchCommandError({              message: `Invalid image attachment payload for '${attachment.name}'.`,            });          }           const bytes = Buffer.from(parsed.base64, "base64");          if (bytes.byteLength === 0 || bytes.byteLength > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) {            return yield* new OrchestrationDispatchCommandError({              message: `Image attachment '${attachment.name}' is empty or too large.`,            });          }           const attachmentId = createAttachmentId(canonicalCommand.threadId);          if (!attachmentId) {            return yield* new OrchestrationDispatchCommandError({              message: "Failed to create a safe attachment id.",            });          }           const persistedAttachment = {            type: "image" as const,            id: attachmentId,            name: attachment.name,            mimeType: parsed.mimeType.toLowerCase(),            sizeBytes: bytes.byteLength,          };           const attachmentPath = resolveAttachmentPath({            attachmentsDir: serverConfig.attachmentsDir,            attachment: persistedAttachment,          });          if (!attachmentPath) {            return yield* new OrchestrationDispatchCommandError({              message: `Failed to resolve persisted path for '${attachment.name}'.`,            });          }           yield* fileSystem.makeDirectory(path.dirname(attachmentPath), { recursive: true }).pipe(            Effect.mapError(              () =>                new OrchestrationDispatchCommandError({                  message: `Failed to create attachment directory for '${attachment.name}'.`,                }),            ),          );          yield* fileSystem.writeFile(attachmentPath, bytes).pipe(            Effect.mapError(              () =>                new OrchestrationDispatchCommandError({                  message: `Failed to persist attachment '${attachment.name}'.`,                }),            ),          );           return persistedAttachment;        }),      { concurrency: 1 },    );     return {      ...canonicalCommand,      message: {        ...canonicalCommand.message,        attachments: normalizedAttachments,      },    } satisfies OrchestrationCommand;  });