Add web multimodal image attachments

This commit is contained in:
Yichen Jiang
2026-07-23 15:20:47 +08:00
parent 3e3ea47296
commit cb4c11b869
116 changed files with 3177 additions and 151 deletions

View File

@@ -0,0 +1,19 @@
# @deepseek-ai/dsh-attachment
The durable attachment seam. `ctx.attachments` validates and atomically commits immutable image bytes, then returns a serializable `ImageAttachmentRef`; consumers never persist browser paths, object URLs, provider URLs, or base64 in session events.
Unsent composer images remain browser-owned temporary drafts. `saveImage` is called only at message submission or while committing structured provider output, before any model-visible session event is published. `readImage` verifies the content-addressed object against its logged metadata.
## Model Experience
Indirectly, through the role-neutral core `ImageBlock` and provider adapters that resolve its durable reference.
#### KV Cache effect
Adding an image changes the provider request and therefore invalidates the affected request suffix.
## Known Limitations and Deferred Work
- Version one accepts PNG, JPEG, WebP, and GIF only.
- Retention and garbage collection are deferred because resumed and forked sessions may share immutable objects.
- Generic files, audio, video, and persistent unsent drafts require separate lifecycle and provider contracts.

View File

@@ -0,0 +1,27 @@
{
"name": "@deepseek-ai/dsh-attachment",
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
"./invariant": { "types": "./lib/types/invariant.d.ts", "default": "./lib/invariant.js" },
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": ["lib/index.js", "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src"],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-brand": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.6"
},
"devDependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"cordis": "^4.0.0-rc.6"
}
}

View File

@@ -0,0 +1,51 @@
/** Durable attachment storage seam (`ctx.attachments`). @module @deepseek-ai/dsh-attachment */
import { Context, Service } from 'cordis'
import type {
ImageAttachmentLimits,
ImageAttachmentRef,
SaveImageAttachment,
StoredImageAttachment,
} from './types.ts'
export { AttachmentError, AttachmentId } from './types.ts'
export type {
AttachmentId as AttachmentIdType,
ImageAttachmentLimits,
ImageAttachmentRef,
ImageMediaType,
SaveImageAttachment,
StoredImageAttachment,
} from './types.ts'
declare module 'cordis' {
interface Context {
attachments: AttachmentStore
}
}
/** Immutable binary attachment service. Implementations validate bytes before publishing a reference. */
export abstract class AttachmentStore extends Service {
constructor(ctx: Context) {
super(ctx, 'attachments')
}
/** Deployment-resolved image policy used by authoritative and fast-path validation. */
abstract readonly imageLimits: ImageAttachmentLimits
/**
* Validate and durably commit one image before its owning session event is appended.
* @param input - encoded bytes, declared media type, and optional display name.
* @returns a durable content-addressed reference.
*/
abstract saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef>
/**
* Read one image and verify that bytes still match the recorded reference.
* @param ref - durable reference from the session log.
* @returns the verified bytes and canonical reference.
*/
abstract readImage(ref: ImageAttachmentRef): Promise<StoredImageAttachment>
}
export default AttachmentStore

View File

@@ -0,0 +1,20 @@
/** Package-owned invariant companion for `@deepseek-ai/dsh-attachment`. @module @deepseek-ai/dsh-attachment/invariant */
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-attachment'
/** Cordis companion plugin name. */
export const name = 'attachment-invariant'
/** Service required before package ownership can be reserved. */
export const inject = ['invariants']
/** No runtime invariant: this stateless seam owns types while implementations enforce immutable-store checks. */
const install: InvariantInstaller = () => {}
/**
* Register the package invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the registration disposer.
*/
export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */

View File

@@ -0,0 +1,75 @@
/** Durable attachment vocabulary. @module @deepseek-ai/dsh-attachment/types */
import type { Branded } from '@deepseek-ai/dsh-brand'
/** Opaque content-addressed identifier for one immutable attachment object. */
export type AttachmentId = Branded<'AttachmentId'>
/**
* Brand a validated storage identifier.
* @param value - backend-produced opaque identifier.
* @returns the branded identifier.
*/
export function AttachmentId(value: string): AttachmentId {
return value as AttachmentId
}
/** Raster image formats accepted by the version-one attachment path. */
export type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
/** Durable, serializable metadata for one immutable image object. */
export interface ImageAttachmentRef {
/** Opaque storage identifier; never a filesystem path or bearer URL. */
attachmentId: AttachmentId
/** Media type verified from the stored bytes. */
mediaType: ImageMediaType
/** Exact encoded byte length. */
bytes: number
/** Intrinsic encoded width in pixels. */
width: number
/** Intrinsic encoded height in pixels. */
height: number
/** Optional display name stripped of local path information. */
name?: string
}
/** Deployment-resolved limits shared by upload consumers and UI preflight. */
export interface ImageAttachmentLimits {
maxImageBytes: number
maxImagesPerMessage: number
maxMessageImageBytes: number
maxImagePixels: number
mediaTypes: readonly ImageMediaType[]
}
/** Request to validate and durably commit one image. */
export interface SaveImageAttachment {
data: Uint8Array
/** Caller-declared media type, checked against magic bytes. */
mediaType: ImageMediaType
/** Optional browser/provider display name; it is never interpreted as a path. */
name?: string
}
/** Stored image bytes returned after reference and digest verification. */
export interface StoredImageAttachment {
ref: ImageAttachmentRef
data: Uint8Array
}
/** Stable failures suitable for host RPC error mapping. */
export class AttachmentError extends Error {
/** Stable machine-routing failure code. */
readonly code: string
/**
* @param message - human-readable failure description without raw bytes or host paths.
* @param code - stable machine-routing code.
* @param options - optional chained cause.
*/
constructor(message: string, code: string, options?: ErrorOptions) {
super(message, options)
this.name = 'AttachmentError'
this.code = code
}
}

View File

@@ -0,0 +1,11 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": { "rootDir": "src", "outDir": "lib/types" },
"include": ["src"],
"references": [
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../util/brand" },
{ "path": "../../support/invariants" }
]
}