chatablex-web-sdk 1.0.31 → 1.0.32
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 +61 -0
- package/README.zh-CN.md +68 -0
- package/dist/index.d.mts +99 -1
- package/dist/index.d.ts +99 -1
- package/dist/index.js +194 -2
- package/dist/index.mjs +190 -2
- package/package.json +1 -1
- package/src/index.ts +15 -1
- package/src/modules/cloud.ts +297 -0
- package/src/types.ts +82 -0
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Your WebUI runs inside a WebView. Many capabilities — native dialogs, file pic
|
|
|
34
34
|
- [sdk.tools](#sdktools)
|
|
35
35
|
- [sdk.platform](#sdkplatform)
|
|
36
36
|
- [sdk.auth](#sdkauth)
|
|
37
|
+
- [sdk.cloud](#sdkcloud)
|
|
37
38
|
- [Events Reference](#events-reference)
|
|
38
39
|
- [Permissions](#permissions)
|
|
39
40
|
- [Host Capability Matrix](#host-capability-matrix)
|
|
@@ -586,6 +587,65 @@ if (res.status === 401 && (await sdk.auth.refresh())) {
|
|
|
586
587
|
|
|
587
588
|
---
|
|
588
589
|
|
|
590
|
+
### `sdk.cloud`
|
|
591
|
+
|
|
592
|
+
Unified cloud storage for **all** WebUI apps — keep user files saved across
|
|
593
|
+
devices without losing them. The SDK handles uploads, downloads, and retries;
|
|
594
|
+
you just call the methods. The `appId` (from `ChatableX.init`) is added
|
|
595
|
+
automatically, and each app's data is isolated so files never get mixed up.
|
|
596
|
+
|
|
597
|
+
> Requires: (1) the user is signed in (`sdk.auth`); (2) a configured cloud
|
|
598
|
+
> storage URL — pass `ChatableX.init({ apiBaseUrl })` (in a hosted environment
|
|
599
|
+
> ChatableX can also provide it automatically). Uploading is a **paid
|
|
600
|
+
> capability**: the user must purchase the corresponding tool before uploading,
|
|
601
|
+
> otherwise the upload is rejected.
|
|
602
|
+
|
|
603
|
+
| Method | Signature | Notes |
|
|
604
|
+
|--------|-----------|-------|
|
|
605
|
+
| `upload` | `(fileKey, data, opts?) => Promise<CloudUploadResult>` | Upload (overwrite). `data` accepts `Blob`/`ArrayBuffer`/`TypedArray`/`string`. |
|
|
606
|
+
| `download` | `(fileKey) => Promise<Blob>` | Download a file's bytes. |
|
|
607
|
+
| `getDownloadUrl` | `(fileKey) => Promise<string>` | Short-lived download URL (e.g. for `<img src>`). |
|
|
608
|
+
| `list` | `(opts?) => Promise<CloudFileInfo[]>` | List this app's files for the user; filter by `prefix`. |
|
|
609
|
+
| `delete` | `(fileKey) => Promise<void>` | Delete a file. |
|
|
610
|
+
| `usage` | `() => Promise<CloudUsage>` | Read the account's storage usage / quota. |
|
|
611
|
+
|
|
612
|
+
```ts
|
|
613
|
+
const sdk = await ChatableX.init({
|
|
614
|
+
appId: 'math-studio',
|
|
615
|
+
apiBaseUrl: import.meta.env.VITE_CHATABLEX_API_BASE,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
await sdk.cloud.upload('scenes/abc/scene.json.gz', blob, { contentType: 'application/gzip' });
|
|
619
|
+
const files = await sdk.cloud.list({ prefix: 'scenes/' });
|
|
620
|
+
const bytes = await sdk.cloud.download('scenes/abc/scene.json.gz');
|
|
621
|
+
await sdk.cloud.delete('scenes/abc/scene.json.gz');
|
|
622
|
+
const { usedBytes, quotaBytes } = await sdk.cloud.usage();
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Error handling** (all `instanceof`-checkable, exported from the package root):
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
import {
|
|
629
|
+
CloudAuthRequiredError, // not logged in
|
|
630
|
+
CloudSubscriptionRequiredError, // tool not purchased — prompt the user to buy it
|
|
631
|
+
CloudQuotaExceededError, // over storage quota; has usedBytes / quotaBytes
|
|
632
|
+
CloudError, // other cloud errors; has code
|
|
633
|
+
} from 'chatablex-web-sdk';
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**Behavior & guarantees**
|
|
637
|
+
|
|
638
|
+
- **Transparent auth.** Reuses `sdk.auth` internally; an expired session is
|
|
639
|
+
refreshed automatically and the request retried once. When the user is not
|
|
640
|
+
signed in it throws `CloudAuthRequiredError` and sends no request.
|
|
641
|
+
- **Data isolation.** Each user's and each app's data is isolated; apps and
|
|
642
|
+
users can never read each other's files.
|
|
643
|
+
- **Paid capability.** Uploading requires the user to have purchased the
|
|
644
|
+
corresponding tool; catch `CloudSubscriptionRequiredError` to drive a purchase
|
|
645
|
+
prompt.
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
589
649
|
## Events Reference
|
|
590
650
|
|
|
591
651
|
| Event | Payload | When fired |
|
|
@@ -630,6 +690,7 @@ SDK methods are thin RPC wrappers. Some host handlers are fully implemented; oth
|
|
|
630
690
|
| `sdk.ui.updateState` | **Production** | Delegates to host |
|
|
631
691
|
| `sdk.platform.openInBrowser` | **Production** | Auth handoff |
|
|
632
692
|
| `sdk.auth.*` | **Production** | Hosted: reuses desktop login via `host.getAuthToken` (pre-refresh) |
|
|
693
|
+
| `sdk.cloud.*` | **Production** | Cloud file storage; needs `apiBaseUrl`, uploading requires purchasing the tool |
|
|
633
694
|
| `sdk.ai.chat` | **Production** | Requires `ai_chat` + delegate |
|
|
634
695
|
| `sdk.ai.getContext` | **Partial** | Returns minimal context |
|
|
635
696
|
| `sdk.ai.chatStream` | **Partial** | Returns `{ streaming: true }`; tokens via events |
|
package/README.zh-CN.md
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
- [sdk.tools](#sdktools)
|
|
35
35
|
- [sdk.platform](#sdkplatform)
|
|
36
36
|
- [sdk.auth](#sdkauth)
|
|
37
|
+
- [sdk.cloud](#sdkcloud)
|
|
37
38
|
- [事件参考](#事件参考)
|
|
38
39
|
- [权限声明](#权限声明)
|
|
39
40
|
- [宿主能力矩阵](#宿主能力矩阵)
|
|
@@ -585,6 +586,72 @@ if (res.status === 401 && (await sdk.auth.refresh())) {
|
|
|
585
586
|
|
|
586
587
|
---
|
|
587
588
|
|
|
589
|
+
### `sdk.cloud`
|
|
590
|
+
|
|
591
|
+
面向**所有** WebUI 应用的统一云存储,让用户文件跨设备保存、不丢失。上传、下载、
|
|
592
|
+
重试等细节都由 SDK 处理,你只管调方法。`appId`(来自 `ChatableX.init`)会自动带上,
|
|
593
|
+
每个应用的数据互相隔离,不会串。
|
|
594
|
+
|
|
595
|
+
> 需要:(1) 用户已登录(`sdk.auth`);(2) 已配置云存储服务地址——在
|
|
596
|
+
> `ChatableX.init({ apiBaseUrl })` 传入(宿主环境下也可由 ChatableX 自动提供)。
|
|
597
|
+
> 上传是**付费能力**:用户需购买对应工具后才能上传,未购买时上传会被拒绝。
|
|
598
|
+
|
|
599
|
+
| 方法 | 签名 | 说明 |
|
|
600
|
+
|------|------|------|
|
|
601
|
+
| `upload` | `(fileKey, data, opts?) => Promise<CloudUploadResult>` | 上传(覆盖写)。`data` 支持 `Blob`/`ArrayBuffer`/`TypedArray`/`string`。 |
|
|
602
|
+
| `download` | `(fileKey) => Promise<Blob>` | 下载文件字节。 |
|
|
603
|
+
| `getDownloadUrl` | `(fileKey) => Promise<string>` | 获取短期有效的下载链接(如喂给 `<img src>`)。 |
|
|
604
|
+
| `list` | `(opts?) => Promise<CloudFileInfo[]>` | 列出当前应用在该用户下的文件,可按 `prefix` 过滤。 |
|
|
605
|
+
| `delete` | `(fileKey) => Promise<void>` | 删除文件。 |
|
|
606
|
+
| `usage` | `() => Promise<CloudUsage>` | 读取账户存储用量/配额。 |
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
const sdk = await ChatableX.init({
|
|
610
|
+
appId: 'math-studio',
|
|
611
|
+
apiBaseUrl: import.meta.env.VITE_CHATABLEX_API_BASE,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// 上传(app_id 自动注入,无需手动传)
|
|
615
|
+
await sdk.cloud.upload('scenes/abc/scene.json.gz', blob, { contentType: 'application/gzip' });
|
|
616
|
+
|
|
617
|
+
// 列表 / 下载 / 删除 / 用量
|
|
618
|
+
const files = await sdk.cloud.list({ prefix: 'scenes/' });
|
|
619
|
+
const bytes = await sdk.cloud.download('scenes/abc/scene.json.gz');
|
|
620
|
+
await sdk.cloud.delete('scenes/abc/scene.json.gz');
|
|
621
|
+
const { usedBytes, quotaBytes } = await sdk.cloud.usage();
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**错误处理**(均可 `instanceof` 判断,从包根导出):
|
|
625
|
+
|
|
626
|
+
```ts
|
|
627
|
+
import {
|
|
628
|
+
CloudAuthRequiredError, // 未登录:提示登录 ChatableX
|
|
629
|
+
CloudSubscriptionRequiredError, // 未购买:引导用户购买工具
|
|
630
|
+
CloudQuotaExceededError, // 超出存储配额,含 usedBytes / quotaBytes
|
|
631
|
+
CloudError, // 其他云存储错误,含 code
|
|
632
|
+
} from 'chatablex-web-sdk';
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
await sdk.cloud.upload('big.bin', buf);
|
|
636
|
+
} catch (e) {
|
|
637
|
+
if (e instanceof CloudSubscriptionRequiredError) {/* 弹出购买引导 */}
|
|
638
|
+
else if (e instanceof CloudQuotaExceededError) {/* 提示清理 / 升级,e.usedBytes/e.quotaBytes */}
|
|
639
|
+
else if (e instanceof CloudAuthRequiredError) {/* 提示登录 */}
|
|
640
|
+
else throw e;
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
**行为与保证**
|
|
645
|
+
|
|
646
|
+
- **鉴权透明。** 内部复用 `sdk.auth`,登录态过期会自动刷新并重试一次;用户未登录
|
|
647
|
+
时直接抛 `CloudAuthRequiredError`,不会发出无效请求。
|
|
648
|
+
- **数据隔离。** 每个用户、每个应用的数据互相隔离,应用之间、用户之间都不会读到
|
|
649
|
+
对方的文件。
|
|
650
|
+
- **付费能力。** 上传需用户已购买对应工具;通过捕获
|
|
651
|
+
`CloudSubscriptionRequiredError` 引导用户购买。
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
588
655
|
## 事件参考
|
|
589
656
|
|
|
590
657
|
| 事件 | 载荷 | 触发时机 |
|
|
@@ -629,6 +696,7 @@ SDK 方法是薄 RPC 封装。部分宿主处理器已完整实现,部分返
|
|
|
629
696
|
| `sdk.ui.updateState` | **生产可用** | 委托给宿主 |
|
|
630
697
|
| `sdk.platform.openInBrowser` | **生产可用** | 鉴权传递 |
|
|
631
698
|
| `sdk.auth.*` | **生产可用** | 宿主态:经 `host.getAuthToken` 复用桌面登录(下发前刷新) |
|
|
699
|
+
| `sdk.cloud.*` | **生产可用** | 云端文件存储;需配置 `apiBaseUrl`,上传需购买对应工具 |
|
|
632
700
|
| `sdk.ai.chat` | **生产可用** | 需要 `ai_chat` + delegate |
|
|
633
701
|
| `sdk.ai.getContext` | **部分实现** | 返回最小上下文 |
|
|
634
702
|
| `sdk.ai.chatStream` | **部分实现** | 返回 `{ streaming: true }`;token 走事件 |
|
package/dist/index.d.mts
CHANGED
|
@@ -119,6 +119,14 @@ interface ChatableXInitConfig {
|
|
|
119
119
|
debug?: boolean;
|
|
120
120
|
/** Timeout in ms for the handshake with Flutter (default: 10000) */
|
|
121
121
|
timeout?: number;
|
|
122
|
+
/**
|
|
123
|
+
* Base URL of the ChatableX cloud API (auth-fc), e.g.
|
|
124
|
+
* `https://chatabl-fc-prod-xxxx.cn-hangzhou.fcapp.run`. Required for
|
|
125
|
+
* `sdk.cloud` to work. When omitted, the SDK best-effort asks the host via
|
|
126
|
+
* the `host.getApiBaseUrl` bridge call; if that is also unavailable, cloud
|
|
127
|
+
* calls reject with a clear error.
|
|
128
|
+
*/
|
|
129
|
+
apiBaseUrl?: string;
|
|
122
130
|
}
|
|
123
131
|
interface ChatableXAI {
|
|
124
132
|
chat(message: string, options?: ChatOptions): Promise<ChatResponse>;
|
|
@@ -191,6 +199,68 @@ interface ChatableXAuth {
|
|
|
191
199
|
/** Force a token refresh via the host. Resolves `true` on success. */
|
|
192
200
|
refresh(): Promise<boolean>;
|
|
193
201
|
}
|
|
202
|
+
/** Binary payload accepted by `sdk.cloud.upload`. */
|
|
203
|
+
type CloudUploadData = Blob | ArrayBuffer | ArrayBufferView | string;
|
|
204
|
+
interface CloudUploadOptions {
|
|
205
|
+
/**
|
|
206
|
+
* MIME type to store the object as. Defaults to the `Blob.type` when a Blob
|
|
207
|
+
* is given, otherwise `application/octet-stream`. Must be one allowed by the
|
|
208
|
+
* server's content-type whitelist.
|
|
209
|
+
*/
|
|
210
|
+
contentType?: string;
|
|
211
|
+
}
|
|
212
|
+
/** Result of a successful `sdk.cloud.upload`. */
|
|
213
|
+
interface CloudUploadResult {
|
|
214
|
+
/** App-relative key (the same `fileKey` passed to `upload`). */
|
|
215
|
+
fileKey: string;
|
|
216
|
+
/** Fully-qualified OSS object key (`user-data/{user_id}/{app_id}/{fileKey}`). */
|
|
217
|
+
objectKey: string;
|
|
218
|
+
/** Bytes uploaded. */
|
|
219
|
+
size: number;
|
|
220
|
+
/** MIME type the object was stored as. */
|
|
221
|
+
contentType: string;
|
|
222
|
+
}
|
|
223
|
+
/** A single file in the user's cloud storage for this app. */
|
|
224
|
+
interface CloudFileInfo {
|
|
225
|
+
fileKey: string;
|
|
226
|
+
size: number;
|
|
227
|
+
/** ISO-8601 timestamp. */
|
|
228
|
+
lastModified: string;
|
|
229
|
+
}
|
|
230
|
+
interface CloudListOptions {
|
|
231
|
+
/** Restrict the listing to keys under this app-relative prefix. */
|
|
232
|
+
prefix?: string;
|
|
233
|
+
}
|
|
234
|
+
/** The user's storage usage / quota for the whole account. */
|
|
235
|
+
interface CloudUsage {
|
|
236
|
+
usedBytes: number;
|
|
237
|
+
quotaBytes: number;
|
|
238
|
+
fileCount: number;
|
|
239
|
+
/** ISO-8601 timestamp of the last server-side reconciliation, if any. */
|
|
240
|
+
reconciledAt?: string;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Cloud storage for WebUI apps. Backed by auth-fc presigned OSS URLs; the
|
|
244
|
+
* app's `appId` (from `ChatableX.init`) is injected automatically so every key
|
|
245
|
+
* is namespaced to `{user}/{app}` and apps cannot reach into each other's data.
|
|
246
|
+
*
|
|
247
|
+
* Requires an authenticated session (`sdk.auth`) and a configured cloud API
|
|
248
|
+
* base URL (see `ChatableXInitConfig.apiBaseUrl`).
|
|
249
|
+
*/
|
|
250
|
+
interface ChatableXCloud {
|
|
251
|
+
/** Upload (overwrite) a file. Resolves once the bytes are stored in OSS. */
|
|
252
|
+
upload(fileKey: string, data: CloudUploadData, options?: CloudUploadOptions): Promise<CloudUploadResult>;
|
|
253
|
+
/** Download a file's bytes as a Blob. */
|
|
254
|
+
download(fileKey: string): Promise<Blob>;
|
|
255
|
+
/** Get a short-lived presigned GET URL (e.g. to feed an `<img src>`). */
|
|
256
|
+
getDownloadUrl(fileKey: string): Promise<string>;
|
|
257
|
+
/** List the current app's files for this user. */
|
|
258
|
+
list(options?: CloudListOptions): Promise<CloudFileInfo[]>;
|
|
259
|
+
/** Delete a file. Resolves even if the object did not exist. */
|
|
260
|
+
delete(fileKey: string): Promise<void>;
|
|
261
|
+
/** Read the account's storage usage / quota. */
|
|
262
|
+
usage(): Promise<CloudUsage>;
|
|
263
|
+
}
|
|
194
264
|
interface ChatableXSDK {
|
|
195
265
|
ai: ChatableXAI;
|
|
196
266
|
tools: ChatableXTools;
|
|
@@ -200,6 +270,7 @@ interface ChatableXSDK {
|
|
|
200
270
|
tool: ChatableXToolModule;
|
|
201
271
|
platform: ChatableXPlatform;
|
|
202
272
|
auth: ChatableXAuth;
|
|
273
|
+
cloud: ChatableXCloud;
|
|
203
274
|
}
|
|
204
275
|
declare global {
|
|
205
276
|
interface Window {
|
|
@@ -246,6 +317,33 @@ declare class Bridge {
|
|
|
246
317
|
destroy(): void;
|
|
247
318
|
}
|
|
248
319
|
|
|
320
|
+
/** Base error for all `sdk.cloud` failures. */
|
|
321
|
+
declare class CloudError extends Error {
|
|
322
|
+
/** Business code from auth-fc (or the HTTP status when none was returned). */
|
|
323
|
+
readonly code: number;
|
|
324
|
+
constructor(message: string, code: number);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Thrown when no authenticated session is available. The app should prompt the
|
|
328
|
+
* user to log in to ChatableX before retrying.
|
|
329
|
+
*/
|
|
330
|
+
declare class CloudAuthRequiredError extends CloudError {
|
|
331
|
+
constructor(message?: string);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Thrown when the user lacks the entitlement (purchased tool / membership)
|
|
335
|
+
* required to write to cloud storage. Maps to auth-fc code `40302`.
|
|
336
|
+
*/
|
|
337
|
+
declare class CloudSubscriptionRequiredError extends CloudError {
|
|
338
|
+
constructor(message?: string);
|
|
339
|
+
}
|
|
340
|
+
/** Thrown when the upload would exceed the user's storage quota (code `40301`). */
|
|
341
|
+
declare class CloudQuotaExceededError extends CloudError {
|
|
342
|
+
readonly usedBytes: number;
|
|
343
|
+
readonly quotaBytes: number;
|
|
344
|
+
constructor(usedBytes: number, quotaBytes: number, message?: string);
|
|
345
|
+
}
|
|
346
|
+
|
|
249
347
|
/**
|
|
250
348
|
* chatablex-web-sdk
|
|
251
349
|
*
|
|
@@ -288,4 +386,4 @@ declare const ChatableX: {
|
|
|
288
386
|
version: string;
|
|
289
387
|
};
|
|
290
388
|
|
|
291
|
-
export { type AiResponseEventData, type AuthTokenData, Bridge, type ChatOptions, type ChatResponse, ChatableX, type ChatableXAI, type ChatableXAuth, type ChatableXEvents, type ChatableXInitConfig, type ChatableXPlatform, type ChatableXSDK, type ChatableXStorage, type ChatableXToolModule, type ChatableXTools, type ChatableXUI, type CloseEventData, type EventCallbackMap, type EventType, type FilePickerOptions, type Message, type NotificationType, SDK_VERSION, type SessionContext, type StateUpdate, type StreamingContentEventData, type TabConfig, type ToolCall, type ToolExecuteHandler, type ToolExecutionEventData, type ToolInfo, type ToolParameter, type ToolResult, type Unsubscribe, type UserMessageEventData };
|
|
389
|
+
export { type AiResponseEventData, type AuthTokenData, Bridge, type ChatOptions, type ChatResponse, ChatableX, type ChatableXAI, type ChatableXAuth, type ChatableXCloud, type ChatableXEvents, type ChatableXInitConfig, type ChatableXPlatform, type ChatableXSDK, type ChatableXStorage, type ChatableXToolModule, type ChatableXTools, type ChatableXUI, type CloseEventData, CloudAuthRequiredError, CloudError, type CloudFileInfo, type CloudListOptions, CloudQuotaExceededError, CloudSubscriptionRequiredError, type CloudUploadData, type CloudUploadOptions, type CloudUploadResult, type CloudUsage, type EventCallbackMap, type EventType, type FilePickerOptions, type Message, type NotificationType, SDK_VERSION, type SessionContext, type StateUpdate, type StreamingContentEventData, type TabConfig, type ToolCall, type ToolExecuteHandler, type ToolExecutionEventData, type ToolInfo, type ToolParameter, type ToolResult, type Unsubscribe, type UserMessageEventData };
|
package/dist/index.d.ts
CHANGED
|
@@ -119,6 +119,14 @@ interface ChatableXInitConfig {
|
|
|
119
119
|
debug?: boolean;
|
|
120
120
|
/** Timeout in ms for the handshake with Flutter (default: 10000) */
|
|
121
121
|
timeout?: number;
|
|
122
|
+
/**
|
|
123
|
+
* Base URL of the ChatableX cloud API (auth-fc), e.g.
|
|
124
|
+
* `https://chatabl-fc-prod-xxxx.cn-hangzhou.fcapp.run`. Required for
|
|
125
|
+
* `sdk.cloud` to work. When omitted, the SDK best-effort asks the host via
|
|
126
|
+
* the `host.getApiBaseUrl` bridge call; if that is also unavailable, cloud
|
|
127
|
+
* calls reject with a clear error.
|
|
128
|
+
*/
|
|
129
|
+
apiBaseUrl?: string;
|
|
122
130
|
}
|
|
123
131
|
interface ChatableXAI {
|
|
124
132
|
chat(message: string, options?: ChatOptions): Promise<ChatResponse>;
|
|
@@ -191,6 +199,68 @@ interface ChatableXAuth {
|
|
|
191
199
|
/** Force a token refresh via the host. Resolves `true` on success. */
|
|
192
200
|
refresh(): Promise<boolean>;
|
|
193
201
|
}
|
|
202
|
+
/** Binary payload accepted by `sdk.cloud.upload`. */
|
|
203
|
+
type CloudUploadData = Blob | ArrayBuffer | ArrayBufferView | string;
|
|
204
|
+
interface CloudUploadOptions {
|
|
205
|
+
/**
|
|
206
|
+
* MIME type to store the object as. Defaults to the `Blob.type` when a Blob
|
|
207
|
+
* is given, otherwise `application/octet-stream`. Must be one allowed by the
|
|
208
|
+
* server's content-type whitelist.
|
|
209
|
+
*/
|
|
210
|
+
contentType?: string;
|
|
211
|
+
}
|
|
212
|
+
/** Result of a successful `sdk.cloud.upload`. */
|
|
213
|
+
interface CloudUploadResult {
|
|
214
|
+
/** App-relative key (the same `fileKey` passed to `upload`). */
|
|
215
|
+
fileKey: string;
|
|
216
|
+
/** Fully-qualified OSS object key (`user-data/{user_id}/{app_id}/{fileKey}`). */
|
|
217
|
+
objectKey: string;
|
|
218
|
+
/** Bytes uploaded. */
|
|
219
|
+
size: number;
|
|
220
|
+
/** MIME type the object was stored as. */
|
|
221
|
+
contentType: string;
|
|
222
|
+
}
|
|
223
|
+
/** A single file in the user's cloud storage for this app. */
|
|
224
|
+
interface CloudFileInfo {
|
|
225
|
+
fileKey: string;
|
|
226
|
+
size: number;
|
|
227
|
+
/** ISO-8601 timestamp. */
|
|
228
|
+
lastModified: string;
|
|
229
|
+
}
|
|
230
|
+
interface CloudListOptions {
|
|
231
|
+
/** Restrict the listing to keys under this app-relative prefix. */
|
|
232
|
+
prefix?: string;
|
|
233
|
+
}
|
|
234
|
+
/** The user's storage usage / quota for the whole account. */
|
|
235
|
+
interface CloudUsage {
|
|
236
|
+
usedBytes: number;
|
|
237
|
+
quotaBytes: number;
|
|
238
|
+
fileCount: number;
|
|
239
|
+
/** ISO-8601 timestamp of the last server-side reconciliation, if any. */
|
|
240
|
+
reconciledAt?: string;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Cloud storage for WebUI apps. Backed by auth-fc presigned OSS URLs; the
|
|
244
|
+
* app's `appId` (from `ChatableX.init`) is injected automatically so every key
|
|
245
|
+
* is namespaced to `{user}/{app}` and apps cannot reach into each other's data.
|
|
246
|
+
*
|
|
247
|
+
* Requires an authenticated session (`sdk.auth`) and a configured cloud API
|
|
248
|
+
* base URL (see `ChatableXInitConfig.apiBaseUrl`).
|
|
249
|
+
*/
|
|
250
|
+
interface ChatableXCloud {
|
|
251
|
+
/** Upload (overwrite) a file. Resolves once the bytes are stored in OSS. */
|
|
252
|
+
upload(fileKey: string, data: CloudUploadData, options?: CloudUploadOptions): Promise<CloudUploadResult>;
|
|
253
|
+
/** Download a file's bytes as a Blob. */
|
|
254
|
+
download(fileKey: string): Promise<Blob>;
|
|
255
|
+
/** Get a short-lived presigned GET URL (e.g. to feed an `<img src>`). */
|
|
256
|
+
getDownloadUrl(fileKey: string): Promise<string>;
|
|
257
|
+
/** List the current app's files for this user. */
|
|
258
|
+
list(options?: CloudListOptions): Promise<CloudFileInfo[]>;
|
|
259
|
+
/** Delete a file. Resolves even if the object did not exist. */
|
|
260
|
+
delete(fileKey: string): Promise<void>;
|
|
261
|
+
/** Read the account's storage usage / quota. */
|
|
262
|
+
usage(): Promise<CloudUsage>;
|
|
263
|
+
}
|
|
194
264
|
interface ChatableXSDK {
|
|
195
265
|
ai: ChatableXAI;
|
|
196
266
|
tools: ChatableXTools;
|
|
@@ -200,6 +270,7 @@ interface ChatableXSDK {
|
|
|
200
270
|
tool: ChatableXToolModule;
|
|
201
271
|
platform: ChatableXPlatform;
|
|
202
272
|
auth: ChatableXAuth;
|
|
273
|
+
cloud: ChatableXCloud;
|
|
203
274
|
}
|
|
204
275
|
declare global {
|
|
205
276
|
interface Window {
|
|
@@ -246,6 +317,33 @@ declare class Bridge {
|
|
|
246
317
|
destroy(): void;
|
|
247
318
|
}
|
|
248
319
|
|
|
320
|
+
/** Base error for all `sdk.cloud` failures. */
|
|
321
|
+
declare class CloudError extends Error {
|
|
322
|
+
/** Business code from auth-fc (or the HTTP status when none was returned). */
|
|
323
|
+
readonly code: number;
|
|
324
|
+
constructor(message: string, code: number);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Thrown when no authenticated session is available. The app should prompt the
|
|
328
|
+
* user to log in to ChatableX before retrying.
|
|
329
|
+
*/
|
|
330
|
+
declare class CloudAuthRequiredError extends CloudError {
|
|
331
|
+
constructor(message?: string);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Thrown when the user lacks the entitlement (purchased tool / membership)
|
|
335
|
+
* required to write to cloud storage. Maps to auth-fc code `40302`.
|
|
336
|
+
*/
|
|
337
|
+
declare class CloudSubscriptionRequiredError extends CloudError {
|
|
338
|
+
constructor(message?: string);
|
|
339
|
+
}
|
|
340
|
+
/** Thrown when the upload would exceed the user's storage quota (code `40301`). */
|
|
341
|
+
declare class CloudQuotaExceededError extends CloudError {
|
|
342
|
+
readonly usedBytes: number;
|
|
343
|
+
readonly quotaBytes: number;
|
|
344
|
+
constructor(usedBytes: number, quotaBytes: number, message?: string);
|
|
345
|
+
}
|
|
346
|
+
|
|
249
347
|
/**
|
|
250
348
|
* chatablex-web-sdk
|
|
251
349
|
*
|
|
@@ -288,4 +386,4 @@ declare const ChatableX: {
|
|
|
288
386
|
version: string;
|
|
289
387
|
};
|
|
290
388
|
|
|
291
|
-
export { type AiResponseEventData, type AuthTokenData, Bridge, type ChatOptions, type ChatResponse, ChatableX, type ChatableXAI, type ChatableXAuth, type ChatableXEvents, type ChatableXInitConfig, type ChatableXPlatform, type ChatableXSDK, type ChatableXStorage, type ChatableXToolModule, type ChatableXTools, type ChatableXUI, type CloseEventData, type EventCallbackMap, type EventType, type FilePickerOptions, type Message, type NotificationType, SDK_VERSION, type SessionContext, type StateUpdate, type StreamingContentEventData, type TabConfig, type ToolCall, type ToolExecuteHandler, type ToolExecutionEventData, type ToolInfo, type ToolParameter, type ToolResult, type Unsubscribe, type UserMessageEventData };
|
|
389
|
+
export { type AiResponseEventData, type AuthTokenData, Bridge, type ChatOptions, type ChatResponse, ChatableX, type ChatableXAI, type ChatableXAuth, type ChatableXCloud, type ChatableXEvents, type ChatableXInitConfig, type ChatableXPlatform, type ChatableXSDK, type ChatableXStorage, type ChatableXToolModule, type ChatableXTools, type ChatableXUI, type CloseEventData, CloudAuthRequiredError, CloudError, type CloudFileInfo, type CloudListOptions, CloudQuotaExceededError, CloudSubscriptionRequiredError, type CloudUploadData, type CloudUploadOptions, type CloudUploadResult, type CloudUsage, type EventCallbackMap, type EventType, type FilePickerOptions, type Message, type NotificationType, SDK_VERSION, type SessionContext, type StateUpdate, type StreamingContentEventData, type TabConfig, type ToolCall, type ToolExecuteHandler, type ToolExecutionEventData, type ToolInfo, type ToolParameter, type ToolResult, type Unsubscribe, type UserMessageEventData };
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,10 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Bridge: () => Bridge,
|
|
24
24
|
ChatableX: () => ChatableX,
|
|
25
|
+
CloudAuthRequiredError: () => CloudAuthRequiredError,
|
|
26
|
+
CloudError: () => CloudError,
|
|
27
|
+
CloudQuotaExceededError: () => CloudQuotaExceededError,
|
|
28
|
+
CloudSubscriptionRequiredError: () => CloudSubscriptionRequiredError,
|
|
25
29
|
SDK_VERSION: () => SDK_VERSION
|
|
26
30
|
});
|
|
27
31
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -357,10 +361,188 @@ function createAuthModule(bridge) {
|
|
|
357
361
|
return new HostAuthProvider(bridge);
|
|
358
362
|
}
|
|
359
363
|
|
|
364
|
+
// src/modules/cloud.ts
|
|
365
|
+
var CloudError = class extends Error {
|
|
366
|
+
constructor(message, code) {
|
|
367
|
+
super(message);
|
|
368
|
+
this.name = "CloudError";
|
|
369
|
+
this.code = code;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var CloudAuthRequiredError = class extends CloudError {
|
|
373
|
+
constructor(message = "cloud storage requires an authenticated session") {
|
|
374
|
+
super(message, 401);
|
|
375
|
+
this.name = "CloudAuthRequiredError";
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
var CloudSubscriptionRequiredError = class extends CloudError {
|
|
379
|
+
constructor(message = "cloud storage requires an active subscription") {
|
|
380
|
+
super(message, 40302);
|
|
381
|
+
this.name = "CloudSubscriptionRequiredError";
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
var CloudQuotaExceededError = class extends CloudError {
|
|
385
|
+
constructor(usedBytes, quotaBytes, message = "storage quota exceeded") {
|
|
386
|
+
super(message, 40301);
|
|
387
|
+
this.name = "CloudQuotaExceededError";
|
|
388
|
+
this.usedBytes = usedBytes;
|
|
389
|
+
this.quotaBytes = quotaBytes;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
var DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
393
|
+
function normalizeData(data) {
|
|
394
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
395
|
+
return { body: data, size: data.size, type: data.type || "" };
|
|
396
|
+
}
|
|
397
|
+
if (typeof data === "string") {
|
|
398
|
+
const blob = new Blob([data]);
|
|
399
|
+
return { body: blob, size: blob.size, type: "" };
|
|
400
|
+
}
|
|
401
|
+
if (data instanceof ArrayBuffer) {
|
|
402
|
+
return { body: data, size: data.byteLength, type: "" };
|
|
403
|
+
}
|
|
404
|
+
if (ArrayBuffer.isView(data)) {
|
|
405
|
+
return { body: data, size: data.byteLength, type: "" };
|
|
406
|
+
}
|
|
407
|
+
throw new CloudError("unsupported upload data type", 400);
|
|
408
|
+
}
|
|
409
|
+
function createCloudModule(bridge, deps) {
|
|
410
|
+
const { appId, auth } = deps;
|
|
411
|
+
let resolvedBase = deps.apiBaseUrl ? stripTrailingSlash(deps.apiBaseUrl) : null;
|
|
412
|
+
function stripTrailingSlash(url) {
|
|
413
|
+
return url.replace(/\/+$/, "");
|
|
414
|
+
}
|
|
415
|
+
async function baseUrl() {
|
|
416
|
+
if (resolvedBase) return resolvedBase;
|
|
417
|
+
try {
|
|
418
|
+
const r = await bridge.sendMessage("host.getApiBaseUrl", {}, 5e3);
|
|
419
|
+
const url = typeof r === "string" ? r : r && typeof r === "object" && typeof r.base_url === "string" ? r.base_url : "";
|
|
420
|
+
if (url) {
|
|
421
|
+
resolvedBase = stripTrailingSlash(url);
|
|
422
|
+
return resolvedBase;
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
throw new CloudError(
|
|
427
|
+
"cloud API base URL is not configured; pass apiBaseUrl to ChatableX.init()",
|
|
428
|
+
0
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
async function authedFetch(path, init) {
|
|
432
|
+
const token = await auth.getToken();
|
|
433
|
+
if (!token) throw new CloudAuthRequiredError();
|
|
434
|
+
const base = await baseUrl();
|
|
435
|
+
const url = `${base}${path}`;
|
|
436
|
+
const build = async () => ({
|
|
437
|
+
...init,
|
|
438
|
+
headers: { ...init.headers ?? {}, ...await auth.getAuthHeaders() }
|
|
439
|
+
});
|
|
440
|
+
let res = await fetch(url, await build());
|
|
441
|
+
if (res.status === 401 && await auth.refresh()) {
|
|
442
|
+
res = await fetch(url, await build());
|
|
443
|
+
}
|
|
444
|
+
return res;
|
|
445
|
+
}
|
|
446
|
+
async function callApi(path, init) {
|
|
447
|
+
const res = await authedFetch(path, init);
|
|
448
|
+
let body = null;
|
|
449
|
+
try {
|
|
450
|
+
body = await res.json();
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
const code = body?.code;
|
|
454
|
+
const message = body?.message || `HTTP ${res.status}`;
|
|
455
|
+
if (!res.ok || body?.success === false) {
|
|
456
|
+
if (code === 40301) {
|
|
457
|
+
const q = body?.data ?? {};
|
|
458
|
+
throw new CloudQuotaExceededError(q.used_bytes ?? 0, q.quota_bytes ?? 0, message);
|
|
459
|
+
}
|
|
460
|
+
if (code === 40302) throw new CloudSubscriptionRequiredError(message);
|
|
461
|
+
if (res.status === 401) throw new CloudAuthRequiredError(message);
|
|
462
|
+
throw new CloudError(message, code ?? res.status);
|
|
463
|
+
}
|
|
464
|
+
return body?.data ?? null;
|
|
465
|
+
}
|
|
466
|
+
function validateFileKey(fileKey) {
|
|
467
|
+
if (!fileKey || typeof fileKey !== "string") {
|
|
468
|
+
throw new CloudError("fileKey is required", 400);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
async upload(fileKey, data, options = {}) {
|
|
473
|
+
validateFileKey(fileKey);
|
|
474
|
+
const { body, size, type } = normalizeData(data);
|
|
475
|
+
const contentType = options.contentType || type || DEFAULT_CONTENT_TYPE;
|
|
476
|
+
const signed = await callApi("/api/storage/upload-url", {
|
|
477
|
+
method: "POST",
|
|
478
|
+
headers: { "Content-Type": "application/json" },
|
|
479
|
+
body: JSON.stringify({
|
|
480
|
+
app_id: appId,
|
|
481
|
+
file_key: fileKey,
|
|
482
|
+
content_type: contentType,
|
|
483
|
+
size_bytes: size
|
|
484
|
+
})
|
|
485
|
+
});
|
|
486
|
+
const put = await fetch(signed.upload_url, {
|
|
487
|
+
method: "PUT",
|
|
488
|
+
headers: { "Content-Type": contentType },
|
|
489
|
+
body
|
|
490
|
+
});
|
|
491
|
+
if (!put.ok) {
|
|
492
|
+
throw new CloudError(`OSS upload failed: HTTP ${put.status}`, put.status);
|
|
493
|
+
}
|
|
494
|
+
return { fileKey, objectKey: signed.object_key, size, contentType };
|
|
495
|
+
},
|
|
496
|
+
async getDownloadUrl(fileKey) {
|
|
497
|
+
validateFileKey(fileKey);
|
|
498
|
+
const signed = await callApi("/api/storage/download-url", {
|
|
499
|
+
method: "POST",
|
|
500
|
+
headers: { "Content-Type": "application/json" },
|
|
501
|
+
body: JSON.stringify({ app_id: appId, file_key: fileKey })
|
|
502
|
+
});
|
|
503
|
+
return signed.download_url;
|
|
504
|
+
},
|
|
505
|
+
async download(fileKey) {
|
|
506
|
+
const url = await this.getDownloadUrl(fileKey);
|
|
507
|
+
const res = await fetch(url, { method: "GET" });
|
|
508
|
+
if (!res.ok) {
|
|
509
|
+
throw new CloudError(`OSS download failed: HTTP ${res.status}`, res.status);
|
|
510
|
+
}
|
|
511
|
+
return res.blob();
|
|
512
|
+
},
|
|
513
|
+
async list(options = {}) {
|
|
514
|
+
const qs = new URLSearchParams({ app_id: appId });
|
|
515
|
+
if (options.prefix) qs.set("prefix", options.prefix);
|
|
516
|
+
const data = await callApi(`/api/storage/files?${qs.toString()}`, {
|
|
517
|
+
method: "GET"
|
|
518
|
+
});
|
|
519
|
+
return (data?.files ?? []).map((f) => ({
|
|
520
|
+
fileKey: f.file_key,
|
|
521
|
+
size: f.size,
|
|
522
|
+
lastModified: f.last_modified
|
|
523
|
+
}));
|
|
524
|
+
},
|
|
525
|
+
async delete(fileKey) {
|
|
526
|
+
validateFileKey(fileKey);
|
|
527
|
+
const qs = new URLSearchParams({ app_id: appId, file_key: fileKey });
|
|
528
|
+
await callApi(`/api/storage/files?${qs.toString()}`, { method: "DELETE" });
|
|
529
|
+
},
|
|
530
|
+
async usage() {
|
|
531
|
+
const data = await callApi("/api/storage/usage", { method: "GET" });
|
|
532
|
+
return {
|
|
533
|
+
usedBytes: data.used_bytes,
|
|
534
|
+
quotaBytes: data.quota_bytes,
|
|
535
|
+
fileCount: data.file_count,
|
|
536
|
+
reconciledAt: data.reconciled_at
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
360
542
|
// package.json
|
|
361
543
|
var package_default = {
|
|
362
544
|
name: "chatablex-web-sdk",
|
|
363
|
-
version: "1.0.
|
|
545
|
+
version: "1.0.32",
|
|
364
546
|
description: "ChatableX Web SDK for AI App WebUI development. Provides bridge communication with the ChatableX Flutter client.",
|
|
365
547
|
main: "dist/index.js",
|
|
366
548
|
module: "dist/index.mjs",
|
|
@@ -452,6 +634,7 @@ var ChatableX = {
|
|
|
452
634
|
}
|
|
453
635
|
const toolModule = createToolModule(bridge, config.appId);
|
|
454
636
|
if (toolConfig) toolModule._setInfo(toolConfig);
|
|
637
|
+
const authModule = createAuthModule(bridge);
|
|
455
638
|
const sdk = {
|
|
456
639
|
ai: createAIModule(bridge),
|
|
457
640
|
tools: createToolsModule(bridge),
|
|
@@ -460,7 +643,12 @@ var ChatableX = {
|
|
|
460
643
|
storage: createStorageModule(bridge),
|
|
461
644
|
tool: toolModule,
|
|
462
645
|
platform: createPlatformModule(bridge),
|
|
463
|
-
auth:
|
|
646
|
+
auth: authModule,
|
|
647
|
+
cloud: createCloudModule(bridge, {
|
|
648
|
+
appId: config.appId,
|
|
649
|
+
auth: authModule,
|
|
650
|
+
apiBaseUrl: config.apiBaseUrl
|
|
651
|
+
})
|
|
464
652
|
};
|
|
465
653
|
window.ChatableX = sdk;
|
|
466
654
|
_instance = sdk;
|
|
@@ -483,5 +671,9 @@ var ChatableX = {
|
|
|
483
671
|
0 && (module.exports = {
|
|
484
672
|
Bridge,
|
|
485
673
|
ChatableX,
|
|
674
|
+
CloudAuthRequiredError,
|
|
675
|
+
CloudError,
|
|
676
|
+
CloudQuotaExceededError,
|
|
677
|
+
CloudSubscriptionRequiredError,
|
|
486
678
|
SDK_VERSION
|
|
487
679
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -329,10 +329,188 @@ function createAuthModule(bridge) {
|
|
|
329
329
|
return new HostAuthProvider(bridge);
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
// src/modules/cloud.ts
|
|
333
|
+
var CloudError = class extends Error {
|
|
334
|
+
constructor(message, code) {
|
|
335
|
+
super(message);
|
|
336
|
+
this.name = "CloudError";
|
|
337
|
+
this.code = code;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
var CloudAuthRequiredError = class extends CloudError {
|
|
341
|
+
constructor(message = "cloud storage requires an authenticated session") {
|
|
342
|
+
super(message, 401);
|
|
343
|
+
this.name = "CloudAuthRequiredError";
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
var CloudSubscriptionRequiredError = class extends CloudError {
|
|
347
|
+
constructor(message = "cloud storage requires an active subscription") {
|
|
348
|
+
super(message, 40302);
|
|
349
|
+
this.name = "CloudSubscriptionRequiredError";
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
var CloudQuotaExceededError = class extends CloudError {
|
|
353
|
+
constructor(usedBytes, quotaBytes, message = "storage quota exceeded") {
|
|
354
|
+
super(message, 40301);
|
|
355
|
+
this.name = "CloudQuotaExceededError";
|
|
356
|
+
this.usedBytes = usedBytes;
|
|
357
|
+
this.quotaBytes = quotaBytes;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
var DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
361
|
+
function normalizeData(data) {
|
|
362
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
363
|
+
return { body: data, size: data.size, type: data.type || "" };
|
|
364
|
+
}
|
|
365
|
+
if (typeof data === "string") {
|
|
366
|
+
const blob = new Blob([data]);
|
|
367
|
+
return { body: blob, size: blob.size, type: "" };
|
|
368
|
+
}
|
|
369
|
+
if (data instanceof ArrayBuffer) {
|
|
370
|
+
return { body: data, size: data.byteLength, type: "" };
|
|
371
|
+
}
|
|
372
|
+
if (ArrayBuffer.isView(data)) {
|
|
373
|
+
return { body: data, size: data.byteLength, type: "" };
|
|
374
|
+
}
|
|
375
|
+
throw new CloudError("unsupported upload data type", 400);
|
|
376
|
+
}
|
|
377
|
+
function createCloudModule(bridge, deps) {
|
|
378
|
+
const { appId, auth } = deps;
|
|
379
|
+
let resolvedBase = deps.apiBaseUrl ? stripTrailingSlash(deps.apiBaseUrl) : null;
|
|
380
|
+
function stripTrailingSlash(url) {
|
|
381
|
+
return url.replace(/\/+$/, "");
|
|
382
|
+
}
|
|
383
|
+
async function baseUrl() {
|
|
384
|
+
if (resolvedBase) return resolvedBase;
|
|
385
|
+
try {
|
|
386
|
+
const r = await bridge.sendMessage("host.getApiBaseUrl", {}, 5e3);
|
|
387
|
+
const url = typeof r === "string" ? r : r && typeof r === "object" && typeof r.base_url === "string" ? r.base_url : "";
|
|
388
|
+
if (url) {
|
|
389
|
+
resolvedBase = stripTrailingSlash(url);
|
|
390
|
+
return resolvedBase;
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
throw new CloudError(
|
|
395
|
+
"cloud API base URL is not configured; pass apiBaseUrl to ChatableX.init()",
|
|
396
|
+
0
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
async function authedFetch(path, init) {
|
|
400
|
+
const token = await auth.getToken();
|
|
401
|
+
if (!token) throw new CloudAuthRequiredError();
|
|
402
|
+
const base = await baseUrl();
|
|
403
|
+
const url = `${base}${path}`;
|
|
404
|
+
const build = async () => ({
|
|
405
|
+
...init,
|
|
406
|
+
headers: { ...init.headers ?? {}, ...await auth.getAuthHeaders() }
|
|
407
|
+
});
|
|
408
|
+
let res = await fetch(url, await build());
|
|
409
|
+
if (res.status === 401 && await auth.refresh()) {
|
|
410
|
+
res = await fetch(url, await build());
|
|
411
|
+
}
|
|
412
|
+
return res;
|
|
413
|
+
}
|
|
414
|
+
async function callApi(path, init) {
|
|
415
|
+
const res = await authedFetch(path, init);
|
|
416
|
+
let body = null;
|
|
417
|
+
try {
|
|
418
|
+
body = await res.json();
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
const code = body?.code;
|
|
422
|
+
const message = body?.message || `HTTP ${res.status}`;
|
|
423
|
+
if (!res.ok || body?.success === false) {
|
|
424
|
+
if (code === 40301) {
|
|
425
|
+
const q = body?.data ?? {};
|
|
426
|
+
throw new CloudQuotaExceededError(q.used_bytes ?? 0, q.quota_bytes ?? 0, message);
|
|
427
|
+
}
|
|
428
|
+
if (code === 40302) throw new CloudSubscriptionRequiredError(message);
|
|
429
|
+
if (res.status === 401) throw new CloudAuthRequiredError(message);
|
|
430
|
+
throw new CloudError(message, code ?? res.status);
|
|
431
|
+
}
|
|
432
|
+
return body?.data ?? null;
|
|
433
|
+
}
|
|
434
|
+
function validateFileKey(fileKey) {
|
|
435
|
+
if (!fileKey || typeof fileKey !== "string") {
|
|
436
|
+
throw new CloudError("fileKey is required", 400);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
async upload(fileKey, data, options = {}) {
|
|
441
|
+
validateFileKey(fileKey);
|
|
442
|
+
const { body, size, type } = normalizeData(data);
|
|
443
|
+
const contentType = options.contentType || type || DEFAULT_CONTENT_TYPE;
|
|
444
|
+
const signed = await callApi("/api/storage/upload-url", {
|
|
445
|
+
method: "POST",
|
|
446
|
+
headers: { "Content-Type": "application/json" },
|
|
447
|
+
body: JSON.stringify({
|
|
448
|
+
app_id: appId,
|
|
449
|
+
file_key: fileKey,
|
|
450
|
+
content_type: contentType,
|
|
451
|
+
size_bytes: size
|
|
452
|
+
})
|
|
453
|
+
});
|
|
454
|
+
const put = await fetch(signed.upload_url, {
|
|
455
|
+
method: "PUT",
|
|
456
|
+
headers: { "Content-Type": contentType },
|
|
457
|
+
body
|
|
458
|
+
});
|
|
459
|
+
if (!put.ok) {
|
|
460
|
+
throw new CloudError(`OSS upload failed: HTTP ${put.status}`, put.status);
|
|
461
|
+
}
|
|
462
|
+
return { fileKey, objectKey: signed.object_key, size, contentType };
|
|
463
|
+
},
|
|
464
|
+
async getDownloadUrl(fileKey) {
|
|
465
|
+
validateFileKey(fileKey);
|
|
466
|
+
const signed = await callApi("/api/storage/download-url", {
|
|
467
|
+
method: "POST",
|
|
468
|
+
headers: { "Content-Type": "application/json" },
|
|
469
|
+
body: JSON.stringify({ app_id: appId, file_key: fileKey })
|
|
470
|
+
});
|
|
471
|
+
return signed.download_url;
|
|
472
|
+
},
|
|
473
|
+
async download(fileKey) {
|
|
474
|
+
const url = await this.getDownloadUrl(fileKey);
|
|
475
|
+
const res = await fetch(url, { method: "GET" });
|
|
476
|
+
if (!res.ok) {
|
|
477
|
+
throw new CloudError(`OSS download failed: HTTP ${res.status}`, res.status);
|
|
478
|
+
}
|
|
479
|
+
return res.blob();
|
|
480
|
+
},
|
|
481
|
+
async list(options = {}) {
|
|
482
|
+
const qs = new URLSearchParams({ app_id: appId });
|
|
483
|
+
if (options.prefix) qs.set("prefix", options.prefix);
|
|
484
|
+
const data = await callApi(`/api/storage/files?${qs.toString()}`, {
|
|
485
|
+
method: "GET"
|
|
486
|
+
});
|
|
487
|
+
return (data?.files ?? []).map((f) => ({
|
|
488
|
+
fileKey: f.file_key,
|
|
489
|
+
size: f.size,
|
|
490
|
+
lastModified: f.last_modified
|
|
491
|
+
}));
|
|
492
|
+
},
|
|
493
|
+
async delete(fileKey) {
|
|
494
|
+
validateFileKey(fileKey);
|
|
495
|
+
const qs = new URLSearchParams({ app_id: appId, file_key: fileKey });
|
|
496
|
+
await callApi(`/api/storage/files?${qs.toString()}`, { method: "DELETE" });
|
|
497
|
+
},
|
|
498
|
+
async usage() {
|
|
499
|
+
const data = await callApi("/api/storage/usage", { method: "GET" });
|
|
500
|
+
return {
|
|
501
|
+
usedBytes: data.used_bytes,
|
|
502
|
+
quotaBytes: data.quota_bytes,
|
|
503
|
+
fileCount: data.file_count,
|
|
504
|
+
reconciledAt: data.reconciled_at
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
332
510
|
// package.json
|
|
333
511
|
var package_default = {
|
|
334
512
|
name: "chatablex-web-sdk",
|
|
335
|
-
version: "1.0.
|
|
513
|
+
version: "1.0.32",
|
|
336
514
|
description: "ChatableX Web SDK for AI App WebUI development. Provides bridge communication with the ChatableX Flutter client.",
|
|
337
515
|
main: "dist/index.js",
|
|
338
516
|
module: "dist/index.mjs",
|
|
@@ -424,6 +602,7 @@ var ChatableX = {
|
|
|
424
602
|
}
|
|
425
603
|
const toolModule = createToolModule(bridge, config.appId);
|
|
426
604
|
if (toolConfig) toolModule._setInfo(toolConfig);
|
|
605
|
+
const authModule = createAuthModule(bridge);
|
|
427
606
|
const sdk = {
|
|
428
607
|
ai: createAIModule(bridge),
|
|
429
608
|
tools: createToolsModule(bridge),
|
|
@@ -432,7 +611,12 @@ var ChatableX = {
|
|
|
432
611
|
storage: createStorageModule(bridge),
|
|
433
612
|
tool: toolModule,
|
|
434
613
|
platform: createPlatformModule(bridge),
|
|
435
|
-
auth:
|
|
614
|
+
auth: authModule,
|
|
615
|
+
cloud: createCloudModule(bridge, {
|
|
616
|
+
appId: config.appId,
|
|
617
|
+
auth: authModule,
|
|
618
|
+
apiBaseUrl: config.apiBaseUrl
|
|
619
|
+
})
|
|
436
620
|
};
|
|
437
621
|
window.ChatableX = sdk;
|
|
438
622
|
_instance = sdk;
|
|
@@ -454,5 +638,9 @@ var ChatableX = {
|
|
|
454
638
|
export {
|
|
455
639
|
Bridge,
|
|
456
640
|
ChatableX,
|
|
641
|
+
CloudAuthRequiredError,
|
|
642
|
+
CloudError,
|
|
643
|
+
CloudQuotaExceededError,
|
|
644
|
+
CloudSubscriptionRequiredError,
|
|
457
645
|
SDK_VERSION
|
|
458
646
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatablex-web-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"description": "ChatableX Web SDK for AI App WebUI development. Provides bridge communication with the ChatableX Flutter client.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { createStorageModule } from './modules/storage';
|
|
|
27
27
|
import { createToolsModule } from './modules/tools';
|
|
28
28
|
import { createPlatformModule } from './modules/platform';
|
|
29
29
|
import { createAuthModule } from './modules/auth';
|
|
30
|
+
import { createCloudModule } from './modules/cloud';
|
|
30
31
|
import type { ChatableXSDK, ChatableXInitConfig, ToolInfo } from './types';
|
|
31
32
|
import pkg from '../package.json';
|
|
32
33
|
|
|
@@ -79,6 +80,8 @@ export const ChatableX = {
|
|
|
79
80
|
const toolModule = createToolModule(bridge, config.appId);
|
|
80
81
|
if (toolConfig) toolModule._setInfo(toolConfig);
|
|
81
82
|
|
|
83
|
+
const authModule = createAuthModule(bridge);
|
|
84
|
+
|
|
82
85
|
const sdk: ChatableXSDK = {
|
|
83
86
|
ai: createAIModule(bridge),
|
|
84
87
|
tools: createToolsModule(bridge),
|
|
@@ -87,7 +90,12 @@ export const ChatableX = {
|
|
|
87
90
|
storage: createStorageModule(bridge),
|
|
88
91
|
tool: toolModule,
|
|
89
92
|
platform: createPlatformModule(bridge),
|
|
90
|
-
auth:
|
|
93
|
+
auth: authModule,
|
|
94
|
+
cloud: createCloudModule(bridge, {
|
|
95
|
+
appId: config.appId,
|
|
96
|
+
auth: authModule,
|
|
97
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
98
|
+
}),
|
|
91
99
|
};
|
|
92
100
|
|
|
93
101
|
// Expose on window for debugging / Flutter interop
|
|
@@ -116,3 +124,9 @@ export const ChatableX = {
|
|
|
116
124
|
// Re-export all types
|
|
117
125
|
export * from './types';
|
|
118
126
|
export { Bridge } from './bridge';
|
|
127
|
+
export {
|
|
128
|
+
CloudError,
|
|
129
|
+
CloudAuthRequiredError,
|
|
130
|
+
CloudSubscriptionRequiredError,
|
|
131
|
+
CloudQuotaExceededError,
|
|
132
|
+
} from './modules/cloud';
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import type { Bridge } from '../bridge';
|
|
2
|
+
import type {
|
|
3
|
+
ChatableXAuth,
|
|
4
|
+
ChatableXCloud,
|
|
5
|
+
CloudFileInfo,
|
|
6
|
+
CloudListOptions,
|
|
7
|
+
CloudUploadData,
|
|
8
|
+
CloudUploadOptions,
|
|
9
|
+
CloudUploadResult,
|
|
10
|
+
CloudUsage,
|
|
11
|
+
} from '../types';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Errors (instanceof-checkable so apps can branch their UI)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** Base error for all `sdk.cloud` failures. */
|
|
18
|
+
export class CloudError extends Error {
|
|
19
|
+
/** Business code from auth-fc (or the HTTP status when none was returned). */
|
|
20
|
+
readonly code: number;
|
|
21
|
+
constructor(message: string, code: number) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'CloudError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Thrown when no authenticated session is available. The app should prompt the
|
|
30
|
+
* user to log in to ChatableX before retrying.
|
|
31
|
+
*/
|
|
32
|
+
export class CloudAuthRequiredError extends CloudError {
|
|
33
|
+
constructor(message = 'cloud storage requires an authenticated session') {
|
|
34
|
+
super(message, 401);
|
|
35
|
+
this.name = 'CloudAuthRequiredError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Thrown when the user lacks the entitlement (purchased tool / membership)
|
|
41
|
+
* required to write to cloud storage. Maps to auth-fc code `40302`.
|
|
42
|
+
*/
|
|
43
|
+
export class CloudSubscriptionRequiredError extends CloudError {
|
|
44
|
+
constructor(message = 'cloud storage requires an active subscription') {
|
|
45
|
+
super(message, 40302);
|
|
46
|
+
this.name = 'CloudSubscriptionRequiredError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Thrown when the upload would exceed the user's storage quota (code `40301`). */
|
|
51
|
+
export class CloudQuotaExceededError extends CloudError {
|
|
52
|
+
readonly usedBytes: number;
|
|
53
|
+
readonly quotaBytes: number;
|
|
54
|
+
constructor(usedBytes: number, quotaBytes: number, message = 'storage quota exceeded') {
|
|
55
|
+
super(message, 40301);
|
|
56
|
+
this.name = 'CloudQuotaExceededError';
|
|
57
|
+
this.usedBytes = usedBytes;
|
|
58
|
+
this.quotaBytes = quotaBytes;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Internal helpers
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
|
|
67
|
+
|
|
68
|
+
/** auth-fc response envelope: { success, code, message, data }. */
|
|
69
|
+
interface ApiEnvelope<T> {
|
|
70
|
+
success?: boolean;
|
|
71
|
+
code?: number;
|
|
72
|
+
message?: string;
|
|
73
|
+
data?: T;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface UploadURLData {
|
|
77
|
+
upload_url: string;
|
|
78
|
+
object_key: string;
|
|
79
|
+
expires_in: number;
|
|
80
|
+
}
|
|
81
|
+
interface DownloadURLData {
|
|
82
|
+
download_url: string;
|
|
83
|
+
object_key: string;
|
|
84
|
+
expires_in: number;
|
|
85
|
+
}
|
|
86
|
+
interface ListFilesData {
|
|
87
|
+
files: Array<{ file_key: string; size: number; last_modified: string }>;
|
|
88
|
+
total: number;
|
|
89
|
+
}
|
|
90
|
+
interface UsageData {
|
|
91
|
+
used_bytes: number;
|
|
92
|
+
quota_bytes: number;
|
|
93
|
+
file_count: number;
|
|
94
|
+
reconciled_at?: string;
|
|
95
|
+
}
|
|
96
|
+
interface QuotaErrorData {
|
|
97
|
+
used_bytes?: number;
|
|
98
|
+
quota_bytes?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeData(data: CloudUploadData): { body: BodyInit; size: number; type: string } {
|
|
102
|
+
if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
|
103
|
+
return { body: data, size: data.size, type: data.type || '' };
|
|
104
|
+
}
|
|
105
|
+
if (typeof data === 'string') {
|
|
106
|
+
const blob = new Blob([data]);
|
|
107
|
+
return { body: blob, size: blob.size, type: '' };
|
|
108
|
+
}
|
|
109
|
+
if (data instanceof ArrayBuffer) {
|
|
110
|
+
return { body: data, size: data.byteLength, type: '' };
|
|
111
|
+
}
|
|
112
|
+
if (ArrayBuffer.isView(data)) {
|
|
113
|
+
return { body: data as unknown as BodyInit, size: data.byteLength, type: '' };
|
|
114
|
+
}
|
|
115
|
+
throw new CloudError('unsupported upload data type', 400);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface CloudModuleDeps {
|
|
119
|
+
appId: string;
|
|
120
|
+
auth: ChatableXAuth;
|
|
121
|
+
/** Explicit cloud API base URL (overrides the host-provided one). */
|
|
122
|
+
apiBaseUrl?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createCloudModule(bridge: Bridge, deps: CloudModuleDeps): ChatableXCloud {
|
|
126
|
+
const { appId, auth } = deps;
|
|
127
|
+
let resolvedBase: string | null = deps.apiBaseUrl ? stripTrailingSlash(deps.apiBaseUrl) : null;
|
|
128
|
+
|
|
129
|
+
function stripTrailingSlash(url: string): string {
|
|
130
|
+
return url.replace(/\/+$/, '');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Resolve the cloud API base URL: explicit config > host bridge > error. */
|
|
134
|
+
async function baseUrl(): Promise<string> {
|
|
135
|
+
if (resolvedBase) return resolvedBase;
|
|
136
|
+
try {
|
|
137
|
+
// Short timeout: a hosted-but-unimplemented method shouldn't hang the call.
|
|
138
|
+
const r = await bridge.sendMessage('host.getApiBaseUrl', {}, 5_000);
|
|
139
|
+
const url =
|
|
140
|
+
typeof r === 'string'
|
|
141
|
+
? r
|
|
142
|
+
: r && typeof r === 'object' && typeof (r as { base_url?: unknown }).base_url === 'string'
|
|
143
|
+
? (r as { base_url: string }).base_url
|
|
144
|
+
: '';
|
|
145
|
+
if (url) {
|
|
146
|
+
resolvedBase = stripTrailingSlash(url);
|
|
147
|
+
return resolvedBase;
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// host doesn't implement it / not hosted — fall through to error
|
|
151
|
+
}
|
|
152
|
+
throw new CloudError(
|
|
153
|
+
'cloud API base URL is not configured; pass apiBaseUrl to ChatableX.init()',
|
|
154
|
+
0,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* fetch against auth-fc that injects the host login session and retries once
|
|
160
|
+
* on 401 after letting `sdk.auth` refresh. Rejects with CloudAuthRequiredError
|
|
161
|
+
* when there is no valid token (no unauthenticated request is sent).
|
|
162
|
+
*/
|
|
163
|
+
async function authedFetch(path: string, init: RequestInit): Promise<Response> {
|
|
164
|
+
const token = await auth.getToken();
|
|
165
|
+
if (!token) throw new CloudAuthRequiredError();
|
|
166
|
+
|
|
167
|
+
const base = await baseUrl();
|
|
168
|
+
const url = `${base}${path}`;
|
|
169
|
+
|
|
170
|
+
const build = async (): Promise<RequestInit> => ({
|
|
171
|
+
...init,
|
|
172
|
+
headers: { ...(init.headers ?? {}), ...(await auth.getAuthHeaders()) },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
let res = await fetch(url, await build());
|
|
176
|
+
if (res.status === 401 && (await auth.refresh())) {
|
|
177
|
+
res = await fetch(url, await build());
|
|
178
|
+
}
|
|
179
|
+
return res;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Call an auth-fc JSON endpoint and unwrap the `data` payload. */
|
|
183
|
+
async function callApi<T>(path: string, init: RequestInit): Promise<T> {
|
|
184
|
+
const res = await authedFetch(path, init);
|
|
185
|
+
|
|
186
|
+
let body: ApiEnvelope<unknown> | null = null;
|
|
187
|
+
try {
|
|
188
|
+
body = (await res.json()) as ApiEnvelope<unknown>;
|
|
189
|
+
} catch {
|
|
190
|
+
// non-JSON body
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const code = body?.code;
|
|
194
|
+
const message = body?.message || `HTTP ${res.status}`;
|
|
195
|
+
|
|
196
|
+
if (!res.ok || body?.success === false) {
|
|
197
|
+
if (code === 40301) {
|
|
198
|
+
const q = (body?.data ?? {}) as QuotaErrorData;
|
|
199
|
+
throw new CloudQuotaExceededError(q.used_bytes ?? 0, q.quota_bytes ?? 0, message);
|
|
200
|
+
}
|
|
201
|
+
if (code === 40302) throw new CloudSubscriptionRequiredError(message);
|
|
202
|
+
if (res.status === 401) throw new CloudAuthRequiredError(message);
|
|
203
|
+
throw new CloudError(message, code ?? res.status);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (body?.data ?? null) as T;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function validateFileKey(fileKey: string): void {
|
|
210
|
+
if (!fileKey || typeof fileKey !== 'string') {
|
|
211
|
+
throw new CloudError('fileKey is required', 400);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
async upload(
|
|
217
|
+
fileKey: string,
|
|
218
|
+
data: CloudUploadData,
|
|
219
|
+
options: CloudUploadOptions = {},
|
|
220
|
+
): Promise<CloudUploadResult> {
|
|
221
|
+
validateFileKey(fileKey);
|
|
222
|
+
const { body, size, type } = normalizeData(data);
|
|
223
|
+
const contentType = options.contentType || type || DEFAULT_CONTENT_TYPE;
|
|
224
|
+
|
|
225
|
+
const signed = await callApi<UploadURLData>('/api/storage/upload-url', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: { 'Content-Type': 'application/json' },
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
app_id: appId,
|
|
230
|
+
file_key: fileKey,
|
|
231
|
+
content_type: contentType,
|
|
232
|
+
size_bytes: size,
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Direct client → OSS PUT. Content-Type MUST match what was signed.
|
|
237
|
+
const put = await fetch(signed.upload_url, {
|
|
238
|
+
method: 'PUT',
|
|
239
|
+
headers: { 'Content-Type': contentType },
|
|
240
|
+
body,
|
|
241
|
+
});
|
|
242
|
+
if (!put.ok) {
|
|
243
|
+
throw new CloudError(`OSS upload failed: HTTP ${put.status}`, put.status);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { fileKey, objectKey: signed.object_key, size, contentType };
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
async getDownloadUrl(fileKey: string): Promise<string> {
|
|
250
|
+
validateFileKey(fileKey);
|
|
251
|
+
const signed = await callApi<DownloadURLData>('/api/storage/download-url', {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: { 'Content-Type': 'application/json' },
|
|
254
|
+
body: JSON.stringify({ app_id: appId, file_key: fileKey }),
|
|
255
|
+
});
|
|
256
|
+
return signed.download_url;
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
async download(fileKey: string): Promise<Blob> {
|
|
260
|
+
const url = await this.getDownloadUrl(fileKey);
|
|
261
|
+
const res = await fetch(url, { method: 'GET' });
|
|
262
|
+
if (!res.ok) {
|
|
263
|
+
throw new CloudError(`OSS download failed: HTTP ${res.status}`, res.status);
|
|
264
|
+
}
|
|
265
|
+
return res.blob();
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
async list(options: CloudListOptions = {}): Promise<CloudFileInfo[]> {
|
|
269
|
+
const qs = new URLSearchParams({ app_id: appId });
|
|
270
|
+
if (options.prefix) qs.set('prefix', options.prefix);
|
|
271
|
+
const data = await callApi<ListFilesData>(`/api/storage/files?${qs.toString()}`, {
|
|
272
|
+
method: 'GET',
|
|
273
|
+
});
|
|
274
|
+
return (data?.files ?? []).map((f) => ({
|
|
275
|
+
fileKey: f.file_key,
|
|
276
|
+
size: f.size,
|
|
277
|
+
lastModified: f.last_modified,
|
|
278
|
+
}));
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async delete(fileKey: string): Promise<void> {
|
|
282
|
+
validateFileKey(fileKey);
|
|
283
|
+
const qs = new URLSearchParams({ app_id: appId, file_key: fileKey });
|
|
284
|
+
await callApi<unknown>(`/api/storage/files?${qs.toString()}`, { method: 'DELETE' });
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
async usage(): Promise<CloudUsage> {
|
|
288
|
+
const data = await callApi<UsageData>('/api/storage/usage', { method: 'GET' });
|
|
289
|
+
return {
|
|
290
|
+
usedBytes: data.used_bytes,
|
|
291
|
+
quotaBytes: data.quota_bytes,
|
|
292
|
+
fileCount: data.file_count,
|
|
293
|
+
reconciledAt: data.reconciled_at,
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -161,6 +161,14 @@ export interface ChatableXInitConfig {
|
|
|
161
161
|
debug?: boolean;
|
|
162
162
|
/** Timeout in ms for the handshake with Flutter (default: 10000) */
|
|
163
163
|
timeout?: number;
|
|
164
|
+
/**
|
|
165
|
+
* Base URL of the ChatableX cloud API (auth-fc), e.g.
|
|
166
|
+
* `https://chatabl-fc-prod-xxxx.cn-hangzhou.fcapp.run`. Required for
|
|
167
|
+
* `sdk.cloud` to work. When omitted, the SDK best-effort asks the host via
|
|
168
|
+
* the `host.getApiBaseUrl` bridge call; if that is also unavailable, cloud
|
|
169
|
+
* calls reject with a clear error.
|
|
170
|
+
*/
|
|
171
|
+
apiBaseUrl?: string;
|
|
164
172
|
}
|
|
165
173
|
|
|
166
174
|
// ---------------------------------------------------------------------------
|
|
@@ -251,6 +259,79 @@ export interface ChatableXAuth {
|
|
|
251
259
|
refresh(): Promise<boolean>;
|
|
252
260
|
}
|
|
253
261
|
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Cloud Storage
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/** Binary payload accepted by `sdk.cloud.upload`. */
|
|
267
|
+
export type CloudUploadData = Blob | ArrayBuffer | ArrayBufferView | string;
|
|
268
|
+
|
|
269
|
+
export interface CloudUploadOptions {
|
|
270
|
+
/**
|
|
271
|
+
* MIME type to store the object as. Defaults to the `Blob.type` when a Blob
|
|
272
|
+
* is given, otherwise `application/octet-stream`. Must be one allowed by the
|
|
273
|
+
* server's content-type whitelist.
|
|
274
|
+
*/
|
|
275
|
+
contentType?: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Result of a successful `sdk.cloud.upload`. */
|
|
279
|
+
export interface CloudUploadResult {
|
|
280
|
+
/** App-relative key (the same `fileKey` passed to `upload`). */
|
|
281
|
+
fileKey: string;
|
|
282
|
+
/** Fully-qualified OSS object key (`user-data/{user_id}/{app_id}/{fileKey}`). */
|
|
283
|
+
objectKey: string;
|
|
284
|
+
/** Bytes uploaded. */
|
|
285
|
+
size: number;
|
|
286
|
+
/** MIME type the object was stored as. */
|
|
287
|
+
contentType: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** A single file in the user's cloud storage for this app. */
|
|
291
|
+
export interface CloudFileInfo {
|
|
292
|
+
fileKey: string;
|
|
293
|
+
size: number;
|
|
294
|
+
/** ISO-8601 timestamp. */
|
|
295
|
+
lastModified: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface CloudListOptions {
|
|
299
|
+
/** Restrict the listing to keys under this app-relative prefix. */
|
|
300
|
+
prefix?: string;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The user's storage usage / quota for the whole account. */
|
|
304
|
+
export interface CloudUsage {
|
|
305
|
+
usedBytes: number;
|
|
306
|
+
quotaBytes: number;
|
|
307
|
+
fileCount: number;
|
|
308
|
+
/** ISO-8601 timestamp of the last server-side reconciliation, if any. */
|
|
309
|
+
reconciledAt?: string;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Cloud storage for WebUI apps. Backed by auth-fc presigned OSS URLs; the
|
|
314
|
+
* app's `appId` (from `ChatableX.init`) is injected automatically so every key
|
|
315
|
+
* is namespaced to `{user}/{app}` and apps cannot reach into each other's data.
|
|
316
|
+
*
|
|
317
|
+
* Requires an authenticated session (`sdk.auth`) and a configured cloud API
|
|
318
|
+
* base URL (see `ChatableXInitConfig.apiBaseUrl`).
|
|
319
|
+
*/
|
|
320
|
+
export interface ChatableXCloud {
|
|
321
|
+
/** Upload (overwrite) a file. Resolves once the bytes are stored in OSS. */
|
|
322
|
+
upload(fileKey: string, data: CloudUploadData, options?: CloudUploadOptions): Promise<CloudUploadResult>;
|
|
323
|
+
/** Download a file's bytes as a Blob. */
|
|
324
|
+
download(fileKey: string): Promise<Blob>;
|
|
325
|
+
/** Get a short-lived presigned GET URL (e.g. to feed an `<img src>`). */
|
|
326
|
+
getDownloadUrl(fileKey: string): Promise<string>;
|
|
327
|
+
/** List the current app's files for this user. */
|
|
328
|
+
list(options?: CloudListOptions): Promise<CloudFileInfo[]>;
|
|
329
|
+
/** Delete a file. Resolves even if the object did not exist. */
|
|
330
|
+
delete(fileKey: string): Promise<void>;
|
|
331
|
+
/** Read the account's storage usage / quota. */
|
|
332
|
+
usage(): Promise<CloudUsage>;
|
|
333
|
+
}
|
|
334
|
+
|
|
254
335
|
export interface ChatableXSDK {
|
|
255
336
|
ai: ChatableXAI;
|
|
256
337
|
tools: ChatableXTools;
|
|
@@ -260,6 +341,7 @@ export interface ChatableXSDK {
|
|
|
260
341
|
tool: ChatableXToolModule;
|
|
261
342
|
platform: ChatableXPlatform;
|
|
262
343
|
auth: ChatableXAuth;
|
|
344
|
+
cloud: ChatableXCloud;
|
|
263
345
|
}
|
|
264
346
|
|
|
265
347
|
// ---------------------------------------------------------------------------
|