From 89b3d5c0150ae6f79daf6596b23bb1d0dfa33cb3 Mon Sep 17 00:00:00 2001 From: NianJiuZst <3235467914@qq.com> Date: Wed, 13 May 2026 01:24:13 +0800 Subject: [PATCH] feat: add FileSDK module for file storage operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new `file` property to MiniMaxSDK that exposes: - upload(filePath, purpose?) — upload a local file via multipart/form-data - list() — list all files in storage - delete(fileId) — delete a file by ID - retrieve(fileId) — get file metadata and optional download URL The module reuses existing endpoint definitions and API types from the shared client layer. FormData construction and file existence checks mirror the CLI's file upload command behavior. Co-Authored-By: Claude Opus 4.6 --- src/sdk/file/index.ts | 77 +++++++++++++++++++++++++++++++++++++++++++ src/sdk/index.ts | 3 ++ test/sdk/file.test.ts | 30 +++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 src/sdk/file/index.ts create mode 100644 test/sdk/file.test.ts diff --git a/src/sdk/file/index.ts b/src/sdk/file/index.ts new file mode 100644 index 0000000..211ebb5 --- /dev/null +++ b/src/sdk/file/index.ts @@ -0,0 +1,77 @@ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { resolve, basename } from 'node:path'; +import { Client } from '../client'; +import { + fileUploadEndpoint, + fileListEndpoint, + fileDeleteEndpoint, + fileRetrieveEndpoint, +} from '../../client/endpoints'; +import type { + FileUploadResponse, + FileListResponse, + FileDeleteResponse, + FileRetrieveResponse, +} from '../../types/api'; +import { SDKError } from '../../errors/base'; +import { ExitCode } from '../../errors/codes'; + +export class FileSDK extends Client { + /** + * Upload a file to MiniMax storage. + * + * @param filePath - Absolute or relative path to the file on disk. + * @param purpose - File purpose, defaults to `"retrieval"`. + */ + async upload(filePath: string, purpose = 'retrieval'): Promise { + const fullPath = resolve(filePath); + if (!existsSync(fullPath)) { + throw new SDKError(`File not found: ${fullPath}`, ExitCode.USAGE); + } + + const fileData = await readFile(fullPath); + const fileName = basename(fullPath); + + const formData = new FormData(); + formData.append('file', new Blob([fileData]), fileName); + formData.append('purpose', purpose); + + const url = fileUploadEndpoint(this.config.baseUrl); + return this.requestJson({ + url, + method: 'POST', + body: formData, + }); + } + + /** List all files in MiniMax storage. */ + async list(): Promise { + const url = fileListEndpoint(this.config.baseUrl); + return this.requestJson({ url, method: 'GET' }); + } + + /** + * Delete a file from MiniMax storage by its file ID. + * + * @param fileId - The ID of the file to delete (string or number). + */ + async delete(fileId: string | number): Promise { + const url = fileDeleteEndpoint(this.config.baseUrl); + return this.requestJson({ + url, + method: 'POST', + body: { file_id: Number(fileId) }, + }); + } + + /** + * Retrieve metadata (and optional download URL) for a file. + * + * @param fileId - The ID of the file to retrieve. + */ + async retrieve(fileId: string): Promise { + const url = fileRetrieveEndpoint(this.config.baseUrl, fileId); + return this.requestJson({ url, method: 'GET' }); + } +} diff --git a/src/sdk/index.ts b/src/sdk/index.ts index 41c0d16..fc46f3b 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -6,6 +6,7 @@ import { MusicSDK } from "./music"; import { SearchSDK } from "./search"; import { VisionSDK } from "./vision"; import { QuotaSDK } from "./quota"; +import { FileSDK } from "./file"; import { Client } from "./client"; import { MiniMaxSDKOptions } from "./types"; @@ -18,6 +19,7 @@ export class MiniMaxSDK extends Client { readonly search: SearchSDK; readonly vision: VisionSDK; readonly quota: QuotaSDK; + readonly file: FileSDK; constructor(options: MiniMaxSDKOptions) { super(options); @@ -29,5 +31,6 @@ export class MiniMaxSDK extends Client { this.search = new SearchSDK(options); this.vision = new VisionSDK(options); this.quota = new QuotaSDK(options); + this.file = new FileSDK(options); } } diff --git a/test/sdk/file.test.ts b/test/sdk/file.test.ts new file mode 100644 index 0000000..506ce3e --- /dev/null +++ b/test/sdk/file.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'bun:test'; +import { FileSDK } from '../../src/sdk/file'; +import { existsSync, unlinkSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; + +describe('FileSDK', () => { + it('throws SDKError when file does not exist', async () => { + const sdk = new FileSDK({ apiKey: 'sk-test', region: 'global' }); + await expect(sdk.upload('/tmp/nonexistent-file-xxxxx.bin', 'retrieval')) + .rejects + .toThrow('File not found'); + }); + + it('gets past file existence check for a valid file', async () => { + const tmpFile = join(tmpdir(), 'mmx-sdk-test-upload.txt'); + writeFileSync(tmpFile, 'hello world'); + + try { + const sdk = new FileSDK({ apiKey: 'sk-test', region: 'global' }); + await sdk.upload(tmpFile, 'retrieval'); + // Should not reach here (no mock server), but if it does, fail informatively + } catch (err) { + // Must NOT be "File not found" — proves file existence check passed + expect((err as Error).message).not.toContain('File not found'); + } finally { + if (existsSync(tmpFile)) unlinkSync(tmpFile); + } + }); +});