feat(adapters): add attachment type and limit check module

This commit is contained in:
程序员阿江(Relakkes) 2026-04-11 15:29:25 +08:00
parent 63e5fb3a41
commit e2ea346754
3 changed files with 134 additions and 0 deletions

View File

@ -0,0 +1,46 @@
import { describe, it, expect } from 'bun:test'
import {
checkAttachmentLimit,
IMAGE_MAX_BYTES,
FILE_MAX_BYTES,
IMAGE_MIME_WHITELIST,
} from '../attachment-limits.js'
describe('checkAttachmentLimit', () => {
it('accepts a 1 MB PNG image', () => {
const result = checkAttachmentLimit('image', 1024 * 1024, 'image/png')
expect(result.ok).toBe(true)
})
it('rejects an 11 MB image as too_large', () => {
const result = checkAttachmentLimit('image', 11 * 1024 * 1024, 'image/png')
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.reason).toBe('too_large')
expect(result.hint).toContain('10')
}
})
it('rejects an unsupported image mime', () => {
const result = checkAttachmentLimit('image', 500_000, 'image/svg+xml')
expect(result.ok).toBe(false)
if (!result.ok) expect(result.reason).toBe('unsupported_mime')
})
it('accepts a 10 MB PDF file', () => {
const result = checkAttachmentLimit('file', 10 * 1024 * 1024, 'application/pdf')
expect(result.ok).toBe(true)
})
it('rejects a 31 MB file as too_large', () => {
const result = checkAttachmentLimit('file', 31 * 1024 * 1024, 'application/pdf')
expect(result.ok).toBe(false)
if (!result.ok) expect(result.reason).toBe('too_large')
})
it('exposes the limits as exports', () => {
expect(IMAGE_MAX_BYTES).toBe(10 * 1024 * 1024)
expect(FILE_MAX_BYTES).toBe(30 * 1024 * 1024)
expect(IMAGE_MIME_WHITELIST).toContain('image/png')
})
})

View File

@ -0,0 +1,59 @@
/**
* Size and MIME restrictions for IM attachments.
*
* Limits chosen to sit safely under both Feishu (10 MB image / 30 MB file)
* and Telegram Bot API (10 MB image / 50 MB file), and under Claude API's
* own image size bounds.
*/
export const IMAGE_MAX_BYTES = 10 * 1024 * 1024 // 10 MB
export const FILE_MAX_BYTES = 30 * 1024 * 1024 // 30 MB
export const IMAGE_MIME_WHITELIST = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/heic',
] as const
export type LimitCheckResult =
| { ok: true }
| { ok: false; reason: 'too_large' | 'unsupported_mime'; hint: string }
function formatMb(bytes: number): string {
return (bytes / (1024 * 1024)).toFixed(1)
}
export function checkAttachmentLimit(
kind: 'image' | 'file',
size: number,
mime?: string,
): LimitCheckResult {
if (kind === 'image') {
if (size > IMAGE_MAX_BYTES) {
return {
ok: false,
reason: 'too_large',
hint: `📎 图片过大(${formatMb(size)} MB),请控制在 10 MB 以内`,
}
}
if (mime && !IMAGE_MIME_WHITELIST.includes(mime as (typeof IMAGE_MIME_WHITELIST)[number])) {
return {
ok: false,
reason: 'unsupported_mime',
hint: `📎 暂不支持此图片格式(${mime})`,
}
}
return { ok: true }
}
// kind === 'file'
if (size > FILE_MAX_BYTES) {
return {
ok: false,
reason: 'too_large',
hint: `📎 文件过大(${formatMb(size)} MB),请控制在 30 MB 以内`,
}
}
return { ok: true }
}

View File

@ -0,0 +1,29 @@
/**
* Shared attachment types for IM adapters.
*/
import type { AttachmentRef } from '../ws-bridge.js'
export type { AttachmentRef }
/** Platform tag — used for local staging subdir and telemetry. */
export type ImPlatform = 'feishu' | 'telegram'
/** Result of downloading an IM resource into the local stage dir. */
export interface LocalAttachment {
kind: 'image' | 'file'
name: string // original filename, or synthesized if none
path: string // absolute path on disk (under ~/.claude/im-downloads)
size: number // bytes
mimeType: string // detected or provided
buffer: Buffer // raw bytes (kept so caller can choose base64 vs path)
}
/** Pending outbound media found in Agent stream output. */
export interface PendingUpload {
id: string // fingerprint, used for dedup
source:
| { kind: 'base64'; data: string; mime: string }
| { kind: 'path'; path: string; mime?: string }
| { kind: 'url'; url: string; mime?: string }
alt?: string
}