| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| | 1 | import Mime from "@effect/platform-node/Mime"; |
| 1 | import { | 2 | import { |
| 2 | ApprovalRequestId, | 3 | ApprovalRequestId, |
| 3 | type ChatAttachment, | 4 | type ChatAttachment, |
| ... | @@ -112,20 +113,13 @@ function parseBase64DataUrl( | ... | @@ -112,20 +113,13 @@ function parseBase64DataUrl( |
| 112 | } | 113 | } |
| 113 | | 114 | |
| 114 | function inferImageExtension(attachment: Extract<ChatAttachment, { type: "image" }>): string { | 115 | function inferImageExtension(attachment: Extract<ChatAttachment, { type: "image" }>): string { |
| 115 | const normalizedMimeType = attachment.mimeType.trim().toLowerCase(); | 116 | const normalizedMimeType = attachment.mimeType.toLowerCase(); |
| 116 | if (normalizedMimeType.startsWith("image/")) { | 117 | if (normalizedMimeType.startsWith("image/")) { |
| 117 | const fromMime = IMAGE_EXTENSION_BY_MIME_TYPE[normalizedMimeType]; | 118 | const fromMime = IMAGE_EXTENSION_BY_MIME_TYPE[normalizedMimeType]; |
| 118 | if (fromMime) { | 119 | if (fromMime) return fromMime; |
| 119 | return fromMime; | | |
| 120 | } | | |
| 121 | } | | |
| 122 | | | |
| 123 | const fromName = attachment.name.includes(".") | | |
| 124 | ? `.${attachment.name.split(".").slice(-1)[0]?.toLowerCase() ?? ""}` | | |
| 125 | : ""; | | |
| 126 | if (/^\.[a-z0-9]{1,12}$/.test(fromName) && SAFE_IMAGE_FILE_EXTENSIONS.has(fromName)) { | | |
| 127 | return fromName; | | |
| 128 | } | 120 | } |
| | 121 | const ext = Mime.getExtension(attachment.mimeType); |
| | 122 | if (ext && SAFE_IMAGE_FILE_EXTENSIONS.has(ext)) return ext; |
| 129 | return ".bin"; | 123 | return ".bin"; |
| 130 | } | 124 | } |
| 131 | | 125 | |
| ... | @@ -135,14 +129,10 @@ const materializeAttachmentsForProjection = Effect.fn(function* (input: { | ... | @@ -135,14 +129,10 @@ const materializeAttachmentsForProjection = Effect.fn(function* (input: { |
| 135 | readonly attachments: ReadonlyArray<ChatAttachment>; | 129 | readonly attachments: ReadonlyArray<ChatAttachment>; |
| 136 | readonly stageFileWrite: (pendingWrite: PendingAttachmentWrite) => void; | 130 | readonly stageFileWrite: (pendingWrite: PendingAttachmentWrite) => void; |
| 137 | }) { | 131 | }) { |
| 138 | if (input.attachments.length === 0) { | 132 | if (input.attachments.length === 0) return []; |
| 139 | return [] as ReadonlyArray<ChatAttachment>; | | |
| 140 | } | | |
| 141 | | 133 | |
| 142 | const serverConfig = yield* Effect.serviceOption(ServerConfig); | 134 | const serverConfig = yield* Effect.serviceOption(ServerConfig); |
| 143 | if (Option.isNone(serverConfig)) { | 135 | if (Option.isNone(serverConfig)) return input.attachments; |
| 144 | return input.attachments; | | |
| 145 | } | | |
| 146 | | 136 | |
| 147 | const path = yield* Path.Path; | 137 | const path = yield* Path.Path; |
| 148 | const attachmentsRootDir = path.join(serverConfig.value.stateDir, ATTACHMENTS_STATE_SUBDIRECTORY); | 138 | const attachmentsRootDir = path.join(serverConfig.value.stateDir, ATTACHMENTS_STATE_SUBDIRECTORY); |
| ... | @@ -166,9 +156,7 @@ const materializeAttachmentsForProjection = Effect.fn(function* (input: { | ... | @@ -166,9 +156,7 @@ const materializeAttachmentsForProjection = Effect.fn(function* (input: { |
| 166 | } | 156 | } |
| 167 | | 157 | |
| 168 | const bytes = Buffer.from(parsed.base64, "base64"); | 158 | const bytes = Buffer.from(parsed.base64, "base64"); |
| 169 | if (bytes.byteLength === 0) { | 159 | if (bytes.byteLength === 0) return attachment; |
| 170 | return attachment; | | |
| 171 | } | | |
| 172 | | 160 | |
| 173 | const fileName = `${index}${inferImageExtension({ | 161 | const fileName = `${index}${inferImageExtension({ |
| 174 | ...attachment, | 162 | ...attachment, |
| ... | @@ -375,9 +363,7 @@ const runAttachmentSideEffects = Effect.fn(function* (input: { | ... | @@ -375,9 +363,7 @@ const runAttachmentSideEffects = Effect.fn(function* (input: { |
| 375 | entries, | 363 | entries, |
| 376 | (entry) => | 364 | (entry) => |
| 377 | Effect.gen(function* () { | 365 | Effect.gen(function* () { |
| 378 | const threadRelativePath = entry | 366 | const threadRelativePath = entry.replace(/^[/\\]+/, "").replace(/\\/g, "/"); |
| 379 | .replace(/^[/\\]+/, "") | | |
| 380 | .replace(/\\/g, "/"); | | |
| 381 | if (threadRelativePath.length === 0 || threadRelativePath.startsWith("..")) { | 367 | if (threadRelativePath.length === 0 || threadRelativePath.startsWith("..")) { |
| 382 | return; | 368 | return; |
| 383 | } | 369 | } |
| ... | @@ -411,7 +397,9 @@ const runAttachmentSideEffects = Effect.fn(function* (input: { | ... | @@ -411,7 +397,9 @@ const runAttachmentSideEffects = Effect.fn(function* (input: { |
| 411 | writesByPath.entries(), | 397 | writesByPath.entries(), |
| 412 | ([absolutePath, bytes]) => | 398 | ([absolutePath, bytes]) => |
| 413 | Effect.gen(function* () { | 399 | Effect.gen(function* () { |
| 414 | yield* input.fileSystem.makeDirectory(input.path.dirname(absolutePath), { recursive: true }); | 400 | yield* input.fileSystem.makeDirectory(input.path.dirname(absolutePath), { |
| | 401 | recursive: true, |
| | 402 | }); |
| 415 | const alreadyExists = yield* input.fileSystem | 403 | const alreadyExists = yield* input.fileSystem |
| 416 | .exists(absolutePath) | 404 | .exists(absolutePath) |
| 417 | .pipe(Effect.catch(() => Effect.succeed(false))); | 405 | .pipe(Effect.catch(() => Effect.succeed(false))); |
| ... | @@ -615,7 +603,10 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { | ... | @@ -615,7 +603,10 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { |
| 615 | } | 603 | } |
| 616 | }); | 604 | }); |
| 617 | | 605 | |
| 618 | const applyThreadMessagesProjection: ProjectorDefinition["apply"] = (event, attachmentSideEffects) => | 606 | const applyThreadMessagesProjection: ProjectorDefinition["apply"] = ( |
| | 607 | event, |
| | 608 | attachmentSideEffects, |
| | 609 | ) => |
| 619 | Effect.gen(function* () { | 610 | Effect.gen(function* () { |
| 620 | switch (event.type) { | 611 | switch (event.type) { |
| 621 | case "thread.message-sent": { | 612 | case "thread.message-sent": { |
| ... | @@ -778,7 +769,10 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { | ... | @@ -778,7 +769,10 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { |
| 778 | }); | 769 | }); |
| 779 | }); | 770 | }); |
| 780 | | 771 | |
| 781 | const applyThreadTurnsProjection: ProjectorDefinition["apply"] = (event, _attachmentSideEffects) => | 772 | const applyThreadTurnsProjection: ProjectorDefinition["apply"] = ( |
| | 773 | event, |
| | 774 | _attachmentSideEffects, |
| | 775 | ) => |
| 782 | Effect.gen(function* () { | 776 | Effect.gen(function* () { |
| 783 | switch (event.type) { | 777 | switch (event.type) { |
| 784 | case "thread.turn-start-requested": { | 778 | case "thread.turn-start-requested": { |