mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-29 16:03:34 +08:00
feat(adapters): add attachment type and limit check module
This commit is contained in:
parent
63e5fb3a41
commit
e2ea346754
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
59
adapters/common/attachment/attachment-limits.ts
Normal file
59
adapters/common/attachment/attachment-limits.ts
Normal 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 }
|
||||||
|
}
|
||||||
29
adapters/common/attachment/attachment-types.ts
Normal file
29
adapters/common/attachment/attachment-types.ts
Normal 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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user