refactor(file-upload): keep storage helpers and the HTTP route package-private

Stop re-exporting the verbatim file-store helpers from attachment-local, move
the raw-byte upload route handler into a non-entry module of file-upload, and
drop the redundant hook type re-exports from the Client entry. The route test
moves next to the module it exercises.
This commit is contained in:
creatixchu committed 2026-09-04 15:41:25 +08:00
1 parent b4517feed5
commit 58ec1f0867
12 files changed
+96 -102

No files matched your search

+2 -2
View File
@@ -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
+1 -1
View File
@@ -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)
<a id="deepseek-aidsh-bash-local"></a>
+1 -1
View File
@@ -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)
<a id="deepseek-aidsh-bash-local"></a>
@@ -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. */
@@ -33,5 +33,3 @@ export interface FileUploadService {
onProgress?: (progress: FileUploadProgress) => void,
): Promise<RemoteResult<FileUploadValue>>
}
export type { ClientFileUploadHooks, FileUploadFetch } from '../types.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' {
@@ -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
@@ -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<Response> {
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>(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<Uint8Array> | null): AsyncIterable<Uint8Array> {
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()
}
}
+2 -77
View File
@@ -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<Response> {
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>(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<Uint8Array> | null): AsyncIterable<Uint8Array> {
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,
@@ -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
@@ -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
@@ -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" },