oomol-cloud-block-sdk 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,427 @@
1
+ # OOMOL Cloud Block SDK
2
+
3
+ [![npm version](https://badge.fury.io/js/oomol-cloud-block-sdk.svg)](https://www.npmjs.com/package/oomol-cloud-block-sdk)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ OOMOL Cloud Block SDK 是一个用于与 OOMOL Cloud Block API 交互的 TypeScript/JavaScript SDK,支持创建和执行无服务器 Block 任务。
7
+
8
+ ## 特性
9
+
10
+ - 🚀 **简单易用** - 提供简洁的 API 接口
11
+ - 📦 **TypeScript 支持** - 完整的类型定义
12
+ - 🔄 **自动轮询** - 内置任务状态轮询机制
13
+ - ⬆️ **文件上传** - 支持大文件分片上传
14
+ - ♻️ **自动重试** - 内置错误处理和重试机制
15
+ - 🎯 **进度跟踪** - 实时进度回调
16
+ - 🛑 **可取消** - 支持 AbortController 取消操作
17
+
18
+ ## 安装
19
+
20
+ ```bash
21
+ npm install oomol-cloud-block-sdk
22
+ ```
23
+
24
+ 或使用 yarn:
25
+
26
+ ```bash
27
+ yarn add oomol-cloud-block-sdk
28
+ ```
29
+
30
+ 或使用 pnpm:
31
+
32
+ ```bash
33
+ pnpm add oomol-cloud-block-sdk
34
+ ```
35
+
36
+ ## 快速开始
37
+
38
+ ### 基本使用
39
+
40
+ ```typescript
41
+ import { OomolCloudBlockSDK } from "oomol-cloud-block-sdk";
42
+
43
+ // 创建 SDK 实例
44
+ const sdk = new OomolCloudBlockSDK();
45
+
46
+ // 创建并执行任务
47
+ const result = await sdk.runTask(
48
+ {
49
+ blockName: "my-block",
50
+ packageName: "my-package",
51
+ packageVersion: "1.0.0",
52
+ inputValues: {
53
+ input1: "value1",
54
+ input2: "value2",
55
+ },
56
+ },
57
+ {
58
+ onProgress: (progress) => {
59
+ console.log(`进度: ${progress}%`);
60
+ },
61
+ }
62
+ );
63
+
64
+ console.log("任务完成:", result.resultData);
65
+ ```
66
+
67
+ ### 分步执行
68
+
69
+ ```typescript
70
+ import { OomolCloudBlockSDK } from "oomol-cloud-block-sdk";
71
+
72
+ const sdk = new OomolCloudBlockSDK();
73
+
74
+ // 步骤 1: 创建任务
75
+ const { taskID } = await sdk.createTask({
76
+ blockName: "image-processor",
77
+ packageName: "image-tools",
78
+ packageVersion: "latest",
79
+ inputValues: {
80
+ imageUrl: "https://example.com/image.png",
81
+ filter: "blur",
82
+ },
83
+ });
84
+
85
+ console.log("任务已创建:", taskID);
86
+
87
+ // 步骤 2: 等待任务完成
88
+ const result = await sdk.awaitTaskResult(taskID, {
89
+ intervalMs: 2000, // 每 2 秒轮询一次
90
+ maxTimeoutMs: 10 * 60 * 1000, // 最多等待 10 分钟
91
+ onProgress: (progress) => {
92
+ console.log(`进度: ${progress}%`);
93
+ },
94
+ });
95
+
96
+ if (result.status === "success") {
97
+ console.log("任务成功:", result.resultData);
98
+ console.log("结果 URL:", result.resultURL);
99
+ }
100
+ ```
101
+
102
+ ### 文件上传
103
+
104
+ ```typescript
105
+ import { OomolCloudBlockSDK } from "oomol-cloud-block-sdk";
106
+
107
+ const sdk = new OomolCloudBlockSDK();
108
+
109
+ // 从 input[type="file"] 获取文件
110
+ const fileInput = document.querySelector('input[type="file"]');
111
+ const file = fileInput.files[0];
112
+
113
+ // 上传文件
114
+ const fileUrl = await sdk.uploadFile(file, {
115
+ onProgress: (progress) => {
116
+ console.log(`上传进度: ${progress}%`);
117
+ },
118
+ retries: 3, // 失败时重试 3 次
119
+ });
120
+
121
+ console.log("文件已上传:", fileUrl);
122
+
123
+ // 使用上传的文件 URL 创建任务
124
+ const result = await sdk.runTask({
125
+ blockName: "document-analyzer",
126
+ packageName: "document-tools",
127
+ packageVersion: "1.0.0",
128
+ inputValues: {
129
+ documentUrl: fileUrl,
130
+ },
131
+ });
132
+ ```
133
+
134
+ ### 取消操作
135
+
136
+ ```typescript
137
+ import { OomolCloudBlockSDK } from "oomol-cloud-block-sdk";
138
+
139
+ const sdk = new OomolCloudBlockSDK();
140
+ const controller = new AbortController();
141
+
142
+ // 5 秒后取消操作
143
+ setTimeout(() => {
144
+ controller.abort();
145
+ console.log("操作已取消");
146
+ }, 5000);
147
+
148
+ try {
149
+ const result = await sdk.runTask(
150
+ {
151
+ blockName: "long-running-task",
152
+ packageName: "tools",
153
+ packageVersion: "1.0.0",
154
+ inputValues: {},
155
+ },
156
+ {
157
+ signal: controller.signal,
158
+ }
159
+ );
160
+ } catch (error) {
161
+ if (error.name === "AbortError") {
162
+ console.log("任务被用户取消");
163
+ } else {
164
+ console.error("任务失败:", error);
165
+ }
166
+ }
167
+ ```
168
+
169
+ ### 错误处理
170
+
171
+ ```typescript
172
+ import { OomolCloudBlockSDK, OomolCloudBlockError } from "oomol-cloud-block-sdk";
173
+
174
+ const sdk = new OomolCloudBlockSDK();
175
+
176
+ try {
177
+ const result = await sdk.runTask({
178
+ blockName: "my-block",
179
+ packageName: "my-package",
180
+ packageVersion: "1.0.0",
181
+ inputValues: {},
182
+ });
183
+
184
+ console.log("成功:", result);
185
+ } catch (error) {
186
+ if (error instanceof OomolCloudBlockError) {
187
+ console.error("SDK 错误:");
188
+ console.error("- 消息:", error.message);
189
+ console.error("- 错误码:", error.code);
190
+ console.error("- 状态码:", error.statusCode);
191
+ } else {
192
+ console.error("未知错误:", error);
193
+ }
194
+ }
195
+ ```
196
+
197
+ ## API 文档
198
+
199
+ ### OomolCloudBlockSDK
200
+
201
+ 主 SDK 类。
202
+
203
+ #### 构造函数
204
+
205
+ ```typescript
206
+ constructor(config?: OomolCloudBlockSDKConfig)
207
+ ```
208
+
209
+ **参数:**
210
+
211
+ - `config.taskApiBase` (可选) - 任务 API 基础 URL,默认 `https://cloud-task.oomol.com/v1`
212
+ - `config.uploadApiBase` (可选) - 上传 API 基础 URL,默认 `https://llm.oomol.com/api/tasks/files/remote-cache`
213
+ - `config.credentials` (可选) - 是否包含凭证(cookies),默认 `true`
214
+
215
+ #### 方法
216
+
217
+ ##### `createTask(params: RunBlockParams): Promise<RunBlockResponse>`
218
+
219
+ 创建一个 Block 任务。
220
+
221
+ **参数:**
222
+
223
+ - `params.blockName` - Block 名称
224
+ - `params.packageName` - Package 名称
225
+ - `params.packageVersion` - Package 版本
226
+ - `params.inputValues` - 输入参数对象
227
+
228
+ **返回:** 包含 `taskID` 的响应对象
229
+
230
+ ##### `getTaskResult(taskID: string, signal?: AbortSignal): Promise<TaskResultResponse>`
231
+
232
+ 获取任务执行结果(单次查询)。
233
+
234
+ **参数:**
235
+
236
+ - `taskID` - 任务 ID
237
+ - `signal` (可选) - AbortSignal,用于取消请求
238
+
239
+ **返回:** 任务结果对象
240
+
241
+ ##### `awaitTaskResult(taskID: string, options?: AwaitTaskOptions): Promise<TaskResultResponse>`
242
+
243
+ 等待任务完成(自动轮询)。
244
+
245
+ **参数:**
246
+
247
+ - `taskID` - 任务 ID
248
+ - `options.intervalMs` (可选) - 轮询间隔(毫秒),默认 1000
249
+ - `options.maxIntervalMs` (可选) - 最大轮询间隔(毫秒),默认 10000
250
+ - `options.maxTimeoutMs` (可选) - 最大超时时间(毫秒),默认 30 分钟
251
+ - `options.onProgress` (可选) - 进度回调函数 `(progress: number) => void`
252
+ - `options.signal` (可选) - AbortSignal,用于取消轮询
253
+
254
+ **返回:** 任务完成时的结果(status 为 "success")
255
+
256
+ ##### `uploadFile(file: File, options?: UploadOptions): Promise<string>`
257
+
258
+ 上传文件到云端缓存。
259
+
260
+ **参数:**
261
+
262
+ - `file` - 要上传的文件对象
263
+ - `options.signal` (可选) - AbortSignal,用于取消上传
264
+ - `options.onProgress` (可选) - 进度回调函数 `(progress: number) => void`
265
+ - `options.retries` (可选) - 重试次数,默认 3
266
+
267
+ **返回:** 上传后的文件 URL
268
+
269
+ ##### `runTask(params: RunBlockParams, awaitOptions?: AwaitTaskOptions): Promise<TaskResultResponse>`
270
+
271
+ 完整流程:创建任务并等待完成。
272
+
273
+ **参数:**
274
+
275
+ - `params` - 任务参数(同 `createTask`)
276
+ - `awaitOptions` - 等待选项(同 `awaitTaskResult`)
277
+
278
+ **返回:** 任务完成时的结果
279
+
280
+ ### 类型定义
281
+
282
+ #### TaskResultResponse
283
+
284
+ 任务结果响应(联合类型):
285
+
286
+ ```typescript
287
+ type TaskResultResponse =
288
+ | {
289
+ status: "pending";
290
+ progress: number; // 0-100
291
+ }
292
+ | {
293
+ status: "success";
294
+ resultURL: string;
295
+ resultData: Record<string, any>;
296
+ }
297
+ | {
298
+ status: "failed";
299
+ failedMessage: string;
300
+ };
301
+ ```
302
+
303
+ #### OomolCloudBlockError
304
+
305
+ SDK 自定义错误类:
306
+
307
+ ```typescript
308
+ class OomolCloudBlockError extends Error {
309
+ code?: string; // 错误码
310
+ statusCode?: number; // HTTP 状态码
311
+ }
312
+ ```
313
+
314
+ 常见错误码:
315
+
316
+ - `CREATE_TASK_FAILED` - 创建任务失败
317
+ - `GET_RESULT_FAILED` - 获取结果失败
318
+ - `TASK_FAILED` - 任务执行失败
319
+ - `POLLING_TIMEOUT` - 轮询超时
320
+ - `NETWORK_ERROR` - 网络错误
321
+ - `INIT_UPLOAD_FAILED` - 初始化上传失败
322
+ - `RETRY_EXHAUSTED` - 重试次数用尽
323
+
324
+ ## 高级用法
325
+
326
+ ### 自定义配置
327
+
328
+ ```typescript
329
+ import { OomolCloudBlockSDK } from "oomol-cloud-block-sdk";
330
+
331
+ const sdk = new OomolCloudBlockSDK({
332
+ taskApiBase: "https://custom-api.example.com/v1",
333
+ uploadApiBase: "https://custom-upload.example.com/api",
334
+ credentials: false, // 不发送 cookies
335
+ });
336
+ ```
337
+
338
+ ### 进度监控
339
+
340
+ ```typescript
341
+ const sdk = new OomolCloudBlockSDK();
342
+
343
+ let lastProgress = 0;
344
+
345
+ const result = await sdk.runTask(
346
+ {
347
+ blockName: "video-processor",
348
+ packageName: "media-tools",
349
+ packageVersion: "2.0.0",
350
+ inputValues: { videoUrl: "..." },
351
+ },
352
+ {
353
+ onProgress: (progress) => {
354
+ if (progress > lastProgress) {
355
+ console.log(`进度更新: ${lastProgress}% -> ${progress}%`);
356
+ lastProgress = progress;
357
+
358
+ // 更新 UI 进度条
359
+ document.getElementById("progress-bar").style.width = `${progress}%`;
360
+ }
361
+ },
362
+ }
363
+ );
364
+ ```
365
+
366
+ ### 批量任务处理
367
+
368
+ ```typescript
369
+ const sdk = new OomolCloudBlockSDK();
370
+
371
+ const tasks = [
372
+ { blockName: "task1", inputValues: { input: "a" } },
373
+ { blockName: "task2", inputValues: { input: "b" } },
374
+ { blockName: "task3", inputValues: { input: "c" } },
375
+ ];
376
+
377
+ // 并行创建所有任务
378
+ const taskIDs = await Promise.all(
379
+ tasks.map((task) =>
380
+ sdk.createTask({
381
+ ...task,
382
+ packageName: "batch-processor",
383
+ packageVersion: "1.0.0",
384
+ })
385
+ )
386
+ );
387
+
388
+ console.log("创建的任务 IDs:", taskIDs.map((r) => r.taskID));
389
+
390
+ // 并行等待所有任务完成
391
+ const results = await Promise.all(
392
+ taskIDs.map((r) =>
393
+ sdk.awaitTaskResult(r.taskID, {
394
+ onProgress: (progress) => {
395
+ console.log(`任务 ${r.taskID}: ${progress}%`);
396
+ },
397
+ })
398
+ )
399
+ );
400
+
401
+ console.log("所有任务完成:", results);
402
+ ```
403
+
404
+ ## 开发
405
+
406
+ ```bash
407
+ # 安装依赖
408
+ npm install
409
+
410
+ # 开发模式(监听文件变化)
411
+ npm run dev
412
+
413
+ # 构建
414
+ npm run build
415
+ ```
416
+
417
+ ## 许可证
418
+
419
+ MIT
420
+
421
+ ## 贡献
422
+
423
+ 欢迎提交 Issue 和 Pull Request!
424
+
425
+ ## 支持
426
+
427
+ 如有问题,请访问 [GitHub Issues](https://github.com/your-org/oomol-cloud-block-sdk/issues)
@@ -0,0 +1,18 @@
1
+ export declare class ApiError extends Error {
2
+ readonly status: number;
3
+ readonly body?: unknown;
4
+ constructor(message: string, status: number, body?: unknown);
5
+ }
6
+ export declare class TaskFailedError extends Error {
7
+ readonly taskID: string;
8
+ readonly detail?: unknown;
9
+ constructor(taskID: string, detail?: unknown);
10
+ }
11
+ export declare class TimeoutError extends Error {
12
+ constructor(message?: string);
13
+ }
14
+ export declare class UploadError extends Error {
15
+ readonly statusCode?: number;
16
+ readonly code?: string;
17
+ constructor(message: string, statusCode?: number, code?: string);
18
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,30 @@
1
+ export class ApiError extends Error {
2
+ constructor(message, status, body) {
3
+ super(message);
4
+ this.name = "ApiError";
5
+ this.status = status;
6
+ this.body = body;
7
+ }
8
+ }
9
+ export class TaskFailedError extends Error {
10
+ constructor(taskID, detail) {
11
+ super("Task execution failed");
12
+ this.name = "TaskFailedError";
13
+ this.taskID = taskID;
14
+ this.detail = detail;
15
+ }
16
+ }
17
+ export class TimeoutError extends Error {
18
+ constructor(message = "Operation timed out") {
19
+ super(message);
20
+ this.name = "TimeoutError";
21
+ }
22
+ }
23
+ export class UploadError extends Error {
24
+ constructor(message, statusCode, code) {
25
+ super(message);
26
+ this.name = "UploadError";
27
+ this.statusCode = statusCode;
28
+ this.code = code;
29
+ }
30
+ }
@@ -0,0 +1,25 @@
1
+ import { AwaitOptions, BlockInfo, BlockTaskRequest, BlockTaskResponse, ClientOptions, ListBlocksRequest, TaskResultResponse, UploadOptions } from "./types.js";
2
+ export declare class OomolBlockClient {
3
+ private readonly apiKey;
4
+ private readonly baseUrl;
5
+ private readonly fetchFn;
6
+ private readonly defaultHeaders;
7
+ constructor(options: ClientOptions);
8
+ createTask(request: BlockTaskRequest): Promise<BlockTaskResponse>;
9
+ getTaskResult<T = unknown>(taskID: string): Promise<TaskResultResponse<T>>;
10
+ awaitResult<T = unknown>(taskID: string, options?: AwaitOptions): Promise<TaskResultResponse<T>>;
11
+ createAndWait<T = unknown>(request: BlockTaskRequest, awaitOptions?: AwaitOptions): Promise<{
12
+ taskID: string;
13
+ result: TaskResultResponse<T>;
14
+ }>;
15
+ listBlocks(request: ListBlocksRequest): Promise<BlockInfo[]>;
16
+ uploadFile(file: File, options?: UploadOptions): Promise<string>;
17
+ private uploadInit;
18
+ private uploadParts;
19
+ private uploadPart;
20
+ private uploadFinal;
21
+ private request;
22
+ private buildUrl;
23
+ }
24
+ export * from "./types.js";
25
+ export * from "./errors.js";
package/dist/index.js ADDED
@@ -0,0 +1,269 @@
1
+ import { ApiError, TaskFailedError, TimeoutError, UploadError } from "./errors.js";
2
+ import { BackoffStrategy, } from "./types.js";
3
+ const DEFAULT_BASE_URL = "https://cloud-task.oomol.com/v1";
4
+ const DEFAULT_UPLOAD_BASE_URL = "https://llm.oomol.com/api/tasks/files/remote-cache";
5
+ export class OomolBlockClient {
6
+ constructor(options) {
7
+ this.apiKey = options.apiKey;
8
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
9
+ this.fetchFn = options.fetch ?? fetch;
10
+ this.defaultHeaders = options.defaultHeaders ?? {};
11
+ }
12
+ async createTask(request) {
13
+ const body = {
14
+ blockName: request.blockName,
15
+ packageName: request.packageName,
16
+ packageVersion: request.packageVersion,
17
+ inputValues: request.inputValues,
18
+ };
19
+ if (request.webhookUrl)
20
+ body.webhookUrl = request.webhookUrl;
21
+ if (request.metadata)
22
+ body.metadata = request.metadata;
23
+ const res = await this.request("/task/serverless", {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify(body),
27
+ });
28
+ return res;
29
+ }
30
+ async getTaskResult(taskID) {
31
+ const res = await this.request(`/task/${taskID}/result`, { method: "GET" });
32
+ return res;
33
+ }
34
+ async awaitResult(taskID, options = {}) {
35
+ const intervalBase = options.intervalMs ?? 3000;
36
+ const maxInterval = options.backoff?.maxIntervalMs ?? 3000;
37
+ const strategy = options.backoff?.strategy ?? BackoffStrategy.Exponential;
38
+ const controller = new AbortController();
39
+ const externalSignal = options.signal;
40
+ let aborted = false;
41
+ if (externalSignal) {
42
+ if (externalSignal.aborted)
43
+ aborted = true;
44
+ externalSignal.addEventListener("abort", () => {
45
+ aborted = true;
46
+ controller.abort();
47
+ });
48
+ }
49
+ const timeoutMs = options.timeoutMs;
50
+ let timeoutId;
51
+ if (typeof timeoutMs === "number" && timeoutMs > 0) {
52
+ timeoutId = setTimeout(() => {
53
+ aborted = true;
54
+ controller.abort();
55
+ }, timeoutMs);
56
+ }
57
+ try {
58
+ let attempt = 0;
59
+ while (true) {
60
+ if (aborted)
61
+ throw new TimeoutError();
62
+ const result = await this.getTaskResult(taskID);
63
+ if (result.status === "success") {
64
+ return result;
65
+ }
66
+ if (result.status === "failed") {
67
+ throw new TaskFailedError(taskID, result.error ?? result);
68
+ }
69
+ options.onProgress?.(result.progress, result.status);
70
+ attempt += 1;
71
+ const nextInterval = strategy === BackoffStrategy.Exponential
72
+ ? Math.min(maxInterval, intervalBase * Math.pow(1.5, attempt))
73
+ : intervalBase;
74
+ await new Promise((r) => setTimeout(r, nextInterval));
75
+ }
76
+ }
77
+ finally {
78
+ if (timeoutId)
79
+ clearTimeout(timeoutId);
80
+ }
81
+ }
82
+ async createAndWait(request, awaitOptions = {}) {
83
+ const { taskID } = await this.createTask(request);
84
+ const result = await this.awaitResult(taskID, awaitOptions);
85
+ return { taskID, result };
86
+ }
87
+ async listBlocks(request) {
88
+ const { packageName, packageVersion, lang } = request;
89
+ const url = new URL(`https://registry.oomol.com/-/oomol/packages/${packageName}/${packageVersion}/public-blocks`);
90
+ if (lang) {
91
+ url.searchParams.set("lang", lang);
92
+ }
93
+ const res = await this.fetchFn(url.toString(), {
94
+ method: "GET",
95
+ headers: {
96
+ Authorization: `Bearer ${this.apiKey}`,
97
+ ...this.defaultHeaders,
98
+ },
99
+ });
100
+ if (!res.ok) {
101
+ let body;
102
+ try {
103
+ body = await res.json();
104
+ }
105
+ catch {
106
+ body = undefined;
107
+ }
108
+ throw new ApiError(`Failed to list blocks: ${res.status}`, res.status, body);
109
+ }
110
+ return res.json();
111
+ }
112
+ async uploadFile(file, options = {}) {
113
+ const uploadBaseUrl = options.uploadBaseUrl ?? DEFAULT_UPLOAD_BASE_URL;
114
+ const retries = options.retries ?? 3;
115
+ const { onProgress, signal } = options;
116
+ const fileSize = file.size;
117
+ const fileExtension = (file.name.includes(".") && file.name.split(".").pop()) || "";
118
+ const initResponse = await this.uploadInit(uploadBaseUrl, fileExtension, fileSize, signal);
119
+ const { upload_id, part_size, total_parts, presigned_urls } = initResponse.data;
120
+ await this.uploadParts(file, part_size, total_parts, presigned_urls, fileSize, onProgress, retries, signal);
121
+ const finalUrl = await this.uploadFinal(uploadBaseUrl, upload_id, signal);
122
+ onProgress?.(100);
123
+ return finalUrl;
124
+ }
125
+ async uploadInit(uploadBaseUrl, fileExtension, fileSize, signal) {
126
+ const res = await this.fetchFn(`${uploadBaseUrl}/init`, {
127
+ method: "POST",
128
+ headers: {
129
+ "Content-Type": "application/json",
130
+ Authorization: `Bearer ${this.apiKey}`,
131
+ },
132
+ body: JSON.stringify({
133
+ file_extension: `.${fileExtension}`,
134
+ size: fileSize,
135
+ }),
136
+ signal,
137
+ });
138
+ if (!res.ok) {
139
+ let body;
140
+ try {
141
+ body = await res.json();
142
+ }
143
+ catch {
144
+ body = undefined;
145
+ }
146
+ throw new UploadError(`Failed to initialize upload: ${res.status}`, res.status, "INIT_UPLOAD_FAILED");
147
+ }
148
+ return res.json();
149
+ }
150
+ async uploadParts(file, partSize, totalParts, presignedUrls, fileSize, onProgress, retries = 3, signal) {
151
+ const uploadPromises = [];
152
+ const partProgress = {};
153
+ const updateProgress = (partNumber, loaded) => {
154
+ partProgress[partNumber] = loaded;
155
+ const totalUploaded = Object.values(partProgress).reduce((sum, bytes) => sum + bytes, 0);
156
+ const progress = fileSize > 0 ? Math.floor((totalUploaded / fileSize) * 100) : 0;
157
+ onProgress?.(progress >= 100 ? 99 : progress);
158
+ };
159
+ for (let partNumber = 1; partNumber <= totalParts; partNumber++) {
160
+ const start = (partNumber - 1) * partSize;
161
+ const end = Math.min(start + partSize, fileSize);
162
+ const partData = file.slice(start, end);
163
+ const presignedUrl = presignedUrls[partNumber];
164
+ if (!presignedUrl) {
165
+ throw new UploadError(`Missing presigned URL for part ${partNumber}`, undefined, "MISSING_PRESIGNED_URL");
166
+ }
167
+ uploadPromises.push(this.uploadPart(partData, presignedUrl, (loaded) => updateProgress(partNumber, loaded), retries, signal));
168
+ }
169
+ await Promise.all(uploadPromises);
170
+ }
171
+ async uploadPart(partData, presignedUrl, onProgress, retries = 3, signal) {
172
+ let lastError;
173
+ for (let attempt = 1; attempt <= retries; attempt++) {
174
+ if (signal?.aborted) {
175
+ throw new UploadError("Upload cancelled", undefined, "UPLOAD_CANCELLED");
176
+ }
177
+ try {
178
+ const xhr = new XMLHttpRequest();
179
+ return await new Promise((resolve, reject) => {
180
+ xhr.upload.addEventListener("progress", (e) => {
181
+ if (e.lengthComputable && onProgress) {
182
+ onProgress(e.loaded);
183
+ }
184
+ });
185
+ xhr.addEventListener("load", () => {
186
+ if (xhr.status >= 200 && xhr.status < 300) {
187
+ resolve();
188
+ }
189
+ else {
190
+ reject(new UploadError(`Upload failed with status ${xhr.status}`, xhr.status, "UPLOAD_FAILED"));
191
+ }
192
+ });
193
+ xhr.addEventListener("error", () => {
194
+ reject(new UploadError("Network error during upload", undefined, "NETWORK_ERROR"));
195
+ });
196
+ if (signal) {
197
+ signal.addEventListener("abort", () => {
198
+ xhr.abort();
199
+ reject(new UploadError("Upload cancelled", undefined, "UPLOAD_CANCELLED"));
200
+ });
201
+ }
202
+ xhr.open("PUT", presignedUrl);
203
+ xhr.setRequestHeader("Content-Type", "application/octet-stream");
204
+ xhr.send(partData);
205
+ });
206
+ }
207
+ catch (error) {
208
+ lastError = error;
209
+ if (lastError.name === "UploadError" && lastError.code === "UPLOAD_CANCELLED") {
210
+ throw lastError;
211
+ }
212
+ if (attempt < retries) {
213
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 30000);
214
+ await new Promise((r) => setTimeout(r, delay));
215
+ }
216
+ }
217
+ }
218
+ throw new UploadError(`Failed after ${retries} attempts: ${lastError?.message || "Unknown error"}`, undefined, "RETRY_EXHAUSTED");
219
+ }
220
+ async uploadFinal(uploadBaseUrl, uploadId, signal) {
221
+ const res = await this.fetchFn(`${uploadBaseUrl}/${encodeURIComponent(uploadId)}/url`, {
222
+ method: "GET",
223
+ headers: {
224
+ Authorization: `Bearer ${this.apiKey}`,
225
+ },
226
+ signal,
227
+ });
228
+ if (!res.ok) {
229
+ let body;
230
+ try {
231
+ body = await res.json();
232
+ }
233
+ catch {
234
+ body = undefined;
235
+ }
236
+ throw new UploadError(`Failed to get upload URL: ${res.status}`, res.status, "GET_UPLOAD_URL_FAILED");
237
+ }
238
+ const result = await res.json();
239
+ return result.data.url;
240
+ }
241
+ async request(path, init) {
242
+ const url = this.buildUrl(path);
243
+ const headers = {
244
+ Authorization: `Bearer ${this.apiKey}`,
245
+ ...this.defaultHeaders,
246
+ ...(init.headers ?? {}),
247
+ };
248
+ const res = await this.fetchFn(url, { ...init, headers });
249
+ if (!res.ok) {
250
+ let body;
251
+ try {
252
+ body = await res.json();
253
+ }
254
+ catch {
255
+ body = undefined;
256
+ }
257
+ throw new ApiError(`Request failed: ${res.status}`, res.status, body);
258
+ }
259
+ const data = await res.json();
260
+ return data;
261
+ }
262
+ buildUrl(path) {
263
+ const base = this.baseUrl.endsWith("/") ? this.baseUrl.slice(0, -1) : this.baseUrl;
264
+ const p = path.startsWith("/") ? path : `/${path}`;
265
+ return `${base}${p}`;
266
+ }
267
+ }
268
+ export * from "./types.js";
269
+ export * from "./errors.js";
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Possible states of a block task during its lifecycle.
3
+ * - `pending`: Task is queued and waiting to be processed
4
+ * - `running`: Task is currently being executed
5
+ * - `success`: Task completed successfully
6
+ * - `failed`: Task failed with an error
7
+ */
8
+ export type TaskStatus = "pending" | "running" | "success" | "failed";
9
+ /**
10
+ * Request payload for creating a new block task.
11
+ */
12
+ export interface BlockTaskRequest {
13
+ /** The name of the block to execute */
14
+ blockName: string;
15
+ /** The package name containing the block */
16
+ packageName: string;
17
+ /** The package version. @default "latest" */
18
+ packageVersion: string;
19
+ /** Input values to pass to the block */
20
+ inputValues: Record<string, unknown>;
21
+ /** Optional webhook URL to receive task completion notifications */
22
+ webhookUrl?: string;
23
+ /** Optional metadata to attach to the task */
24
+ metadata?: Record<string, unknown>;
25
+ }
26
+ /**
27
+ * Response returned after creating a block task.
28
+ */
29
+ export interface BlockTaskResponse {
30
+ /** The unique identifier of the created task */
31
+ taskID: string;
32
+ }
33
+ /**
34
+ * Response containing the result of a block task.
35
+ * @template T - Type of the result data
36
+ */
37
+ export interface TaskResultResponse<T = unknown> {
38
+ /** Current status of the task */
39
+ status: TaskStatus;
40
+ /** Progress percentage (0-100), only available when task is running */
41
+ progress?: number;
42
+ /** Result data returned by the task on success */
43
+ resultData?: T;
44
+ /** Result file URL (if available) */
45
+ resultURL?: string;
46
+ /** Error message if the task failed */
47
+ error?: string;
48
+ }
49
+ /**
50
+ * Strategy for polling interval backoff.
51
+ */
52
+ export declare enum BackoffStrategy {
53
+ /** Use a fixed interval between polls */
54
+ Fixed = "fixed",
55
+ /** Increase interval exponentially between polls (recommended for long-running tasks) */
56
+ Exponential = "exp"
57
+ }
58
+ /**
59
+ * Options for awaiting task completion.
60
+ */
61
+ export interface AwaitOptions {
62
+ /** Base polling interval in milliseconds. @default 3000 */
63
+ intervalMs?: number;
64
+ /** Maximum time to wait for task completion in milliseconds. If exceeded, throws TimeoutError */
65
+ timeoutMs?: number;
66
+ /** Callback invoked on each poll with current progress and status */
67
+ onProgress?: (progress: number | undefined, status: TaskStatus) => void;
68
+ /** AbortSignal to cancel the polling operation */
69
+ signal?: AbortSignal;
70
+ /** Backoff configuration for polling intervals */
71
+ backoff?: {
72
+ /** Backoff strategy to use. @default BackoffStrategy.Exponential */
73
+ strategy?: BackoffStrategy;
74
+ /** Maximum interval between polls in milliseconds. @default 3000 */
75
+ maxIntervalMs?: number;
76
+ };
77
+ }
78
+ /**
79
+ * Configuration options for the OomolBlockClient.
80
+ */
81
+ export interface ClientOptions {
82
+ /** API key for authentication */
83
+ apiKey: string;
84
+ /** Base URL of the task API. @default "https://cloud-task.oomol.com/v1" */
85
+ baseUrl?: string;
86
+ /** Custom fetch implementation (useful for testing or environments without native fetch) */
87
+ fetch?: typeof fetch;
88
+ /** Additional headers to include in all requests */
89
+ defaultHeaders?: Record<string, string>;
90
+ }
91
+ /**
92
+ * Options for file upload operations.
93
+ */
94
+ export interface UploadOptions {
95
+ /** Base URL of the upload API. @default "https://llm.oomol.com/api/tasks/files/remote-cache" */
96
+ uploadBaseUrl?: string;
97
+ /** Progress callback function (0-100) */
98
+ onProgress?: (progress: number) => void;
99
+ /** Number of retry attempts for failed uploads. @default 3 */
100
+ retries?: number;
101
+ /** AbortSignal to cancel the upload operation */
102
+ signal?: AbortSignal;
103
+ }
104
+ /**
105
+ * Parameters for listing blocks in a package.
106
+ */
107
+ export interface ListBlocksRequest {
108
+ /** Package name */
109
+ packageName: string;
110
+ /** Package version */
111
+ packageVersion: string;
112
+ /** Language code (optional), e.g., "zh-CN" */
113
+ lang?: string;
114
+ }
115
+ /**
116
+ * Block type enumeration.
117
+ */
118
+ export type BlockType = "task" | "subflow";
119
+ /**
120
+ * Block input handle definition.
121
+ */
122
+ export interface BlockInputHandle {
123
+ handle: string;
124
+ description?: string;
125
+ jsonSchema?: unknown;
126
+ nullable?: boolean;
127
+ value?: unknown;
128
+ }
129
+ /**
130
+ * Block output handle definition.
131
+ */
132
+ export interface BlockOutputHandle {
133
+ handle: string;
134
+ description?: string;
135
+ jsonSchema?: unknown;
136
+ nullable?: boolean;
137
+ }
138
+ /**
139
+ * Block information structure.
140
+ */
141
+ export interface BlockInfo {
142
+ type: BlockType;
143
+ resourceName: string;
144
+ name: string;
145
+ title?: string;
146
+ description?: string;
147
+ icon?: string;
148
+ inputHandleDefs?: BlockInputHandle[];
149
+ outputHandleDefs?: BlockOutputHandle[];
150
+ }
package/dist/types.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Strategy for polling interval backoff.
3
+ */
4
+ export var BackoffStrategy;
5
+ (function (BackoffStrategy) {
6
+ /** Use a fixed interval between polls */
7
+ BackoffStrategy["Fixed"] = "fixed";
8
+ /** Increase interval exponentially between polls (recommended for long-running tasks) */
9
+ BackoffStrategy["Exponential"] = "exp";
10
+ })(BackoffStrategy || (BackoffStrategy = {}));
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "oomol-cloud-block-sdk",
3
+ "version": "1.0.1",
4
+ "description": "TypeScript SDK for Oomol Cloud Block API",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "keywords": [
14
+ "oomol",
15
+ "cloud-block",
16
+ "sdk",
17
+ "typescript"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^24.10.1",
24
+ "typescript": "^5.9.3"
25
+ }
26
+ }