diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 609c5c379f..df25b46feb 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 5dcec11452e12400442d5dd539b6bc7e2491ed87 -config-catalog.zh.md: cde0be16f3f433cc24076029b671fde3783e6648 +config-catalog.md: af72b832723423745817fd340022e65c0c066d36 +config-catalog.zh.md: 6d961f22b3c29fbc4355e99c8de55823e014b74c diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 5dcec11452..af72b83272 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -262,7 +262,7 @@ export interface Config { } ``` -Source: [`packages/attachment/attachment-local/src/index.ts:64`](../packages/attachment/attachment-local/src/index.ts) +Source: [`packages/attachment/attachment-local/src/index.ts:61`](../packages/attachment/attachment-local/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index cde0be16f3..6d961f22b3 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -264,7 +264,7 @@ export interface Config { } ``` -来源:[`packages/attachment/attachment-local/src/index.ts:64`](../packages/attachment/attachment-local/src/index.ts) +来源:[`packages/attachment/attachment-local/src/index.ts:61`](../packages/attachment/attachment-local/src/index.ts) diff --git a/packages/attachment/attachment-local/src/index.ts b/packages/attachment/attachment-local/src/index.ts index 7058fb3244..7415bf940f 100644 --- a/packages/attachment/attachment-local/src/index.ts +++ b/packages/attachment/attachment-local/src/index.ts @@ -28,9 +28,6 @@ export { canPassThroughNormalization, normalizeImage } from './normalization.ts' export type { NormalizedImage, NormalizationPolicy } from './normalization.ts' export { commitPreparedImageFile, prepareImageFile, readImageFile, saveImageFile, validateImageFile } from './store.ts' export type { PreparedImageFile } from './store.ts' -export { - fileLeafName, readFileStreamVerbatim, saveFileStreamVerbatim, saveFileVerbatim, storedFilePath, -} from './file-store.ts' export { readRequestImageFile, requestImageVariantId } from './request-image.ts' /** Default maximum encoded bytes for one submitted image; oversized sources are refused, not shrunk. */ diff --git a/packages/client/file-upload/src/client/contract.ts b/packages/client/file-upload/src/client/contract.ts index 5f509ace06..998a71b2c3 100644 --- a/packages/client/file-upload/src/client/contract.ts +++ b/packages/client/file-upload/src/client/contract.ts @@ -33,5 +33,3 @@ export interface FileUploadService { onProgress?: (progress: FileUploadProgress) => void, ): Promise> } - -export type { ClientFileUploadHooks, FileUploadFetch } from '../types.ts' diff --git a/packages/client/file-upload/src/client/index.ts b/packages/client/file-upload/src/client/index.ts index a9b6bb3ef8..bf6696a423 100644 --- a/packages/client/file-upload/src/client/index.ts +++ b/packages/client/file-upload/src/client/index.ts @@ -4,13 +4,7 @@ import type { Context } from '@deepseek-ai/cordis' import { FileUploadRuntime } from './runtime.ts' import type { FileUploadService } from './contract.ts' -export type { - ClientFileUploadHooks, - FileUploadBody, - FileUploadFetch, - FileUploadProgress, - FileUploadService, -} from './contract.ts' +export type { FileUploadProgress, FileUploadService } from './contract.ts' export type { FileUploadReceiptId, FileUploadValue } from '../types.ts' declare module '@deepseek-ai/cordis' { diff --git a/packages/client/file-upload/src/client/runtime.ts b/packages/client/file-upload/src/client/runtime.ts index 38ab535a1f..a54e31121c 100644 --- a/packages/client/file-upload/src/client/runtime.ts +++ b/packages/client/file-upload/src/client/runtime.ts @@ -6,13 +6,10 @@ import { RemoteError } from '@deepseek-ai/dsh-typert-protocol' import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { FILE_UPLOAD_PATH } from '../protocol.ts' -import type { EncodedFileUploadRequest, FileUploadValue } from '../types.ts' import type { - ClientFileUploadHooks, - FileUploadBody, - FileUploadFetch, - FileUploadService, -} from './contract.ts' + ClientFileUploadHooks, EncodedFileUploadRequest, FileUploadFetch, FileUploadValue, +} from '../types.ts' +import type { FileUploadBody, FileUploadService } from './contract.ts' interface FileUploadRequest { readonly path: string diff --git a/packages/client/file-upload/src/http-route.ts b/packages/client/file-upload/src/http-route.ts new file mode 100644 index 0000000000..0f5539324e --- /dev/null +++ b/packages/client/file-upload/src/http-route.ts @@ -0,0 +1,81 @@ +/** Authenticated raw-byte upload route registered on the Connection fetch registry. */ + +import { brandString } from '@deepseek-ai/dsh-brand' +import type { SessionId } from '@deepseek-ai/dsh-session' +import { remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol' +import type { FileUploads } from './index.ts' +import type { FileUploadValue } from './types.ts' + +type FileUploadHttpResult = + | { readonly ok: true; readonly value: FileUploadValue } + | { + readonly ok: false + readonly error: { readonly code: string; readonly message: string; readonly details: object } + } + +/** + * Handle one authenticated raw-byte upload. + * @param service - Host upload service receiving streamed bytes. + * @param request - authenticated HTTP request from Connection. + * @returns JSON result using HTTP status 200 after request validation. + */ +export async function handleFileUploadHttp(service: FileUploads, request: Request): Promise { + if (request.method !== 'POST') { + return new Response(null, { status: 405, headers: { allow: 'POST' } }) + } + const mediaType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() + if (mediaType !== 'application/octet-stream') { + return new Response('content type must be application/octet-stream', { status: 415 }) + } + const url = new URL(request.url) + const sessionId = url.searchParams.get('sessionId') + if (sessionId === null || sessionId === '') { + return new Response('sessionId is required', { status: 400 }) + } + const name = url.searchParams.get('name') ?? undefined + let result: FileUploadHttpResult + try { + result = { + ok: true, + value: await service.uploadStream({ + sessionId: brandString(sessionId), + data: requestBodyChunks(request.body), + signal: request.signal, + ...(name === undefined ? {} : { name }), + }), + } + } catch (error) { + const failure = remoteErrorOf(error) + result = { + ok: false, + error: failure !== undefined + ? { code: failure.code, message: failure.message, details: failure.details } + : { + code: 'gateway/internal', + message: error instanceof Error ? error.message : String(error), + details: {}, + }, + } + } + return new Response(JSON.stringify(result), { + status: 200, + headers: { + 'content-type': 'application/json; charset=utf-8', + 'cache-control': 'no-store', + }, + }) +} + +async function* requestBodyChunks(body: ReadableStream | null): AsyncIterable { + if (body === null) return + const reader = body.getReader() + try { + while (true) { + const chunk = await reader.read() + if (chunk.done) return + yield chunk.value + } + } finally { + reader.releaseLock() + } +} diff --git a/packages/client/file-upload/src/index.ts b/packages/client/file-upload/src/index.ts index 531ec3e464..2520ba1225 100644 --- a/packages/client/file-upload/src/index.ts +++ b/packages/client/file-upload/src/index.ts @@ -4,16 +4,15 @@ import { randomUUID } from 'node:crypto' import type { Context } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment' -import { brandString } from '@deepseek-ai/dsh-brand' import type {} from '@deepseek-ai/dsh-client-connection' import type { CommandFileReceiptResolver } from '@deepseek-ai/dsh-commands' import { scopeOf } from '@deepseek-ai/dsh-scope' import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session' -import { Remote, RemoteError, TypertRemoteService, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol' +import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' +import { handleFileUploadHttp } from './http-route.ts' import { FILE_UPLOAD_PATH } from './protocol.ts' import type { EncodedFileUploadRequest, FileUploadReceiptId, FileUploadValue } from './types.ts' -export { FILE_UPLOAD_PATH } from './protocol.ts' export type * from './types.ts' declare module '@deepseek-ai/cordis' { @@ -38,13 +37,6 @@ export interface PromptFileBinding extends Disposable { commit(): void } -type FileUploadHttpResult = - | { readonly ok: true; readonly value: FileUploadValue } - | { - readonly ok: false - readonly error: { readonly code: string; readonly message: string; readonly details: object } - } - class PromptFileBindingGuard implements PromptFileBinding { private settled = false @@ -262,73 +254,6 @@ export class FileUploads extends TypertRemoteService { } } -/** - * Handle one authenticated raw-byte upload. - * @param service - Host upload service receiving streamed bytes. - * @param request - authenticated HTTP request from Connection. - * @returns JSON result using HTTP status 200 after request validation. - */ -export async function handleFileUploadHttp(service: FileUploads, request: Request): Promise { - if (request.method !== 'POST') { - return new Response(null, { status: 405, headers: { allow: 'POST' } }) - } - const mediaType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() - if (mediaType !== 'application/octet-stream') { - return new Response('content type must be application/octet-stream', { status: 415 }) - } - const url = new URL(request.url) - const sessionId = url.searchParams.get('sessionId') - if (sessionId === null || sessionId === '') { - return new Response('sessionId is required', { status: 400 }) - } - const name = url.searchParams.get('name') ?? undefined - let result: FileUploadHttpResult - try { - result = { - ok: true, - value: await service.uploadStream({ - sessionId: brandString(sessionId), - data: requestBodyChunks(request.body), - signal: request.signal, - ...(name === undefined ? {} : { name }), - }), - } - } catch (error) { - const failure = remoteErrorOf(error) - result = { - ok: false, - error: failure !== undefined - ? { code: failure.code, message: failure.message, details: failure.details } - : { - code: 'gateway/internal', - message: error instanceof Error ? error.message : String(error), - details: {}, - }, - } - } - return new Response(JSON.stringify(result), { - status: 200, - headers: { - 'content-type': 'application/json; charset=utf-8', - 'cache-control': 'no-store', - }, - }) -} - -async function* requestBodyChunks(body: ReadableStream | null): AsyncIterable { - if (body === null) return - const reader = body.getReader() - try { - while (true) { - const chunk = await reader.read() - if (chunk.done) return - yield chunk.value - } - } finally { - reader.releaseLock() - } -} - function fileNotStaged(): RemoteError { return new RemoteError( 'session/attachment-invalid' as never, diff --git a/packages/api/session-controller/tests/file-upload-http.host.spec.ts b/packages/client/file-upload/tests/file-upload-http.host.spec.ts similarity index 97% rename from packages/api/session-controller/tests/file-upload-http.host.spec.ts rename to packages/client/file-upload/tests/file-upload-http.host.spec.ts index 60fd54fea9..cd137857ce 100644 --- a/packages/api/session-controller/tests/file-upload-http.host.spec.ts +++ b/packages/client/file-upload/tests/file-upload-http.host.spec.ts @@ -2,7 +2,8 @@ import { runInNewContext } from 'node:vm' import { RemoteError } from '@deepseek-ai/dsh-typert-protocol' import { describe, expect, it, vi } from 'vitest' import type { Mock } from 'vitest' -import { FileUploads, handleFileUploadHttp } from '@deepseek-ai/dsh-client-file-upload' +import { handleFileUploadHttp } from '../src/http-route.ts' +import type { FileUploads } from '../src/index.ts' function request(input: { method?: string diff --git a/packages/client/file-upload/tests/file-upload.client.spec.ts b/packages/client/file-upload/tests/file-upload.client.spec.ts index 510993d94f..66b27b102a 100644 --- a/packages/client/file-upload/tests/file-upload.client.spec.ts +++ b/packages/client/file-upload/tests/file-upload.client.spec.ts @@ -3,7 +3,8 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types' import { afterEach, describe, expect, it, vi } from 'vitest' import { apply } from '../src/client/index.ts' import { fileUploadWorker, FileUploadRuntime } from '../src/client/runtime.ts' -import type { ClientFileUploadHooks, FileUploadBody } from '../src/client/contract.ts' +import type { FileUploadBody } from '../src/client/contract.ts' +import type { ClientFileUploadHooks } from '../src/types.ts' interface UploadGlobal { __DSH_FILE_UPLOAD__?: ClientFileUploadHooks diff --git a/packages/client/file-upload/tsconfig.host.json b/packages/client/file-upload/tsconfig.host.json index d0645185cf..6c8c29bc15 100644 --- a/packages/client/file-upload/tsconfig.host.json +++ b/packages/client/file-upload/tsconfig.host.json @@ -5,7 +5,7 @@ "outDir": "lib/types", "tsBuildInfoFile": "lib/tsconfig.host.tsbuildinfo" }, - "files": ["src/index.ts", "src/protocol.ts", "src/types.ts"], + "files": ["src/http-route.ts", "src/index.ts", "src/protocol.ts", "src/types.ts"], "references": [ { "path": "../../../vendor/cordis" }, { "path": "../../util/brand" },