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" },