Files
deepseek-harness/packages/attachment/attachment-local/src/image.ts
creatixchu 0d1250f743 fix: address ds-review-bot v7 findings on the merged image-input head
- gate model selection on steering-placement image carriers from enqueue
  until their steering/message event publishes; release the gate when an
  admission ends idle without publication (both behaviorally asserted)
- reject session.updateQueue edits carrying non-text blocks at the RPC
  boundary (queue edits cannot bypass image admission)
- extend the durable-directory walk past a first-created DSH_HOME to the
  deepest pre-existing ancestor
- strip Windows-style separators from attachment display names on POSIX
- verify attachment reads with a header-only probe (digest already proves
  the bytes decoded fully at admission); document the read path
- make SessionInputShell.addImages refusal observable and keep workspace
  transfers/composer intake from leaking refused drafts
- own ONE recursive image walk (dsh-llm contentHasImage) across apiproxy,
  pi-ai, compact-basic, and the DeepSeek text-only assertion
- drop the redundant canonical-base64 regex and the no-op role read
- move AttachmentId/AttachmentError out of types.ts (brand.ts/error.ts);
  document why AttachmentError does not extend HarnessError
- document the hard attachments inject in both consumer READMEs
2026-07-30 14:34:08 +08:00

67 lines
2.5 KiB
TypeScript

/** Raster inspection: full decode at admission, header-only probe on verified reads. */
import sharp, { type Sharp } from 'sharp'
import { AttachmentError } from '@deepseek-ai/dsh-attachment'
import type { ImageMediaType } from '@deepseek-ai/dsh-attachment'
/** Decoded metadata from a supported image. */
export interface DetectedImage {
mediaType: ImageMediaType
width: number
height: number
}
const MEDIA_TYPES: Readonly<Record<string, ImageMediaType>> = {
png: 'image/png',
jpeg: 'image/jpeg',
webp: 'image/webp',
gif: 'image/gif',
}
async function imageMetadata(image: Sharp): Promise<DetectedImage> {
const metadata = await image.metadata()
const mediaType = MEDIA_TYPES[metadata.format as string]
if (mediaType === undefined) {
throw new AttachmentError('Unsupported or malformed image data.', 'INVALID_IMAGE')
}
return { mediaType, width: metadata.width, height: metadata.height }
}
/**
* Parse a supported raster's header and return its intrinsic metadata without
* decoding pixels. Digest-verified reads use this: admission already proved
* that these exact bytes decode completely, so the read path only re-derives
* the reference fields instead of paying the full-raster decode again.
* @param data - complete encoded image bytes.
* @returns verified format and dimensions.
*/
export async function probeImage(data: Uint8Array): Promise<DetectedImage> {
try {
return await imageMetadata(sharp(data, { failOn: 'error', limitInputPixels: false }))
} catch (error) {
if (error instanceof AttachmentError) throw error
throw new AttachmentError('Unsupported or malformed image data.', 'INVALID_IMAGE', { cause: error })
}
}
/**
* Fully decode a supported raster and return its intrinsic metadata.
* @param data - complete encoded image bytes.
* @param maxPixels - decoded-pixel admission limit.
* @returns verified format and dimensions.
*/
export async function detectImage(data: Uint8Array, maxPixels?: number): Promise<DetectedImage> {
try {
const image = sharp(data, { failOn: 'error', limitInputPixels: false })
const detected = await imageMetadata(image)
if (maxPixels !== undefined && detected.width * detected.height > maxPixels) {
throw new AttachmentError('Image exceeds the configured decoded-pixel limit.', 'IMAGE_TOO_MANY_PIXELS')
}
await image.raw().toBuffer()
return detected
} catch (error) {
if (error instanceof AttachmentError) throw error
throw new AttachmentError('Unsupported or malformed image data.', 'INVALID_IMAGE', { cause: error })
}
}