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:
1 parent
b4517feed5
commit
58ec1f0867
12 files changed
+96
-102
No files matched your search
@@ -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
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
-1
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user