@chatu-ai/app-sdk 0.7.6 → 0.7.7
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 +1 -0
- package/dist/ai.js +5 -5
- package/dist/config.d.ts +22 -3
- package/dist/config.js +50 -3
- package/dist/edgeone.d.ts +8 -0
- package/dist/edgeone.js +205 -0
- package/dist/edgeone.test.d.ts +1 -0
- package/dist/edgeone.test.js +75 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/kv.js +3 -2
- package/dist/storage.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ const src = await storage.url('avatars/u1.png', { expiresIn: 3600 }) //
|
|
|
19
19
|
| --- | --- | --- |
|
|
20
20
|
| `CHATU_DATA_URL` + `CHATU_APP_KEY` (+ `CHATU_DATA_ENV=dev\|prod`) | **platform** — ChatU hosted Data API, metered | set automatically in the Builder preview; copy from the publish panel for your own server |
|
|
21
21
|
| `REDIS_URL` and/or `S3_BUCKET` (+ `S3_ENDPOINT` `S3_REGION` `S3_ACCESS_KEY` `S3_SECRET_KEY` `S3_PREFIX`) | **byo** — your own Redis / S3-compatible bucket (Tencent COS, MinIO, AWS) | install optional deps: `npm i ioredis @aws-sdk/client-s3 @aws-sdk/s3-request-presigner` |
|
|
22
|
+
| `CHATU_DATA_DRIVER=edgeone` (+ optional `CHATU_EDGEONE_KV_STORE` / `CHATU_EDGEONE_STORAGE_STORE`, external access: `EDGEONE_BLOB_PROJECT_ID` + `EDGEONE_BLOB_TOKEN`) | **edgeone** — Tencent EdgeOne Pages Blob for both `kv` (JSON envelope, TTL emulated) and `storage` (presigned PUT via `createUploadUrl`; `storage.url()` returns the in-app proxy path `/_chatu/blob/<key>` served by the template route) | `npm i @edgeone/pages-blob` (preinstalled in the Builder template); credential-free inside Pages Functions. `ai` keeps using `CHATU_DATA_URL` + `CHATU_APP_KEY` |
|
|
22
23
|
| none | **memory** — in-process, lost on restart | local dev / fallback |
|
|
23
24
|
|
|
24
25
|
## AI (LLM relay)
|
package/dist/ai.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveAiConfig } from './config.js';
|
|
2
2
|
import { AppSdkError } from './errors.js';
|
|
3
3
|
const toMessages = (input) => (typeof input === 'string' ? [{ role: 'user', content: input }] : input);
|
|
4
4
|
function buildBody(cfg, messages, opts, stream) {
|
|
@@ -130,11 +130,11 @@ function notConfigured() {
|
|
|
130
130
|
let cached = null;
|
|
131
131
|
/** 按当前配置取 AI 客户端(惰性、缓存;configure() 后自动重建) */
|
|
132
132
|
export function getAi() {
|
|
133
|
-
const cfg =
|
|
134
|
-
const key = cfg
|
|
135
|
-
const fetchImpl = cfg
|
|
133
|
+
const cfg = resolveAiConfig();
|
|
134
|
+
const key = cfg ? `platform|${cfg.aiBaseUrl}|${cfg.aiModel ?? ''}|${cfg.apiKey.slice(-4)}` : 'none';
|
|
135
|
+
const fetchImpl = cfg?.fetchImpl;
|
|
136
136
|
if (!cached || cached.key !== key || cached.fetchImpl !== fetchImpl)
|
|
137
|
-
cached = { key, fetchImpl, client: cfg
|
|
137
|
+
cached = { key, fetchImpl, client: cfg ? platformAi(cfg) : notConfigured() };
|
|
138
138
|
return cached.client;
|
|
139
139
|
}
|
|
140
140
|
/** 便捷单例:`import { ai } from '@chatu-ai/app-sdk'` */
|
package/dist/config.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* - 都没有 → memory(进程内存,重启即丢;本地开发/无配置降级)
|
|
5
5
|
* 只在服务端使用(Route Handler / Server Component / Server Action);密钥不得暴露给浏览器。
|
|
6
6
|
*/
|
|
7
|
-
export type DriverKind = 'platform' | 'byo' | 'memory';
|
|
7
|
+
export type DriverKind = 'platform' | 'byo' | 'memory' | 'edgeone';
|
|
8
8
|
export interface PlatformConfig {
|
|
9
9
|
kind: 'platform';
|
|
10
10
|
baseUrl: string;
|
|
@@ -34,7 +34,21 @@ export interface ByoConfig {
|
|
|
34
34
|
forcePathStyle: boolean;
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* EdgeOne Pages Blob(部署到 EdgeOne 时可选):kv 与 storage 都落在 Pages Blob(`@edgeone/pages-blob`)
|
|
39
|
+
* - Pages 函数内免凭据;外部访问(如平台侧只读浏览)需 projectId + API token
|
|
40
|
+
* - CHATU_DATA_DRIVER=edgeone 启用;store 名可用 CHATU_EDGEONE_KV_STORE / CHATU_EDGEONE_STORAGE_STORE 覆盖
|
|
41
|
+
*/
|
|
42
|
+
export interface EdgeoneConfig {
|
|
43
|
+
kind: 'edgeone';
|
|
44
|
+
kvStore: string;
|
|
45
|
+
storageStore: string;
|
|
46
|
+
projectId?: string;
|
|
47
|
+
token?: string;
|
|
48
|
+
/** 应用内代理读取路由前缀(storage.url() 返回 `${publicPathPrefix}/<key>`;模板内置 /_chatu/blob) */
|
|
49
|
+
publicPathPrefix: string;
|
|
50
|
+
}
|
|
51
|
+
export type ResolvedConfig = PlatformConfig | ByoConfig | MemoryConfig | EdgeoneConfig;
|
|
38
52
|
export interface ConfigureOptions {
|
|
39
53
|
baseUrl?: string;
|
|
40
54
|
apiKey?: string;
|
|
@@ -59,5 +73,10 @@ export declare function describe(): {
|
|
|
59
73
|
kv?: string;
|
|
60
74
|
storage?: string;
|
|
61
75
|
};
|
|
62
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* AI 中继配置与数据驱动解耦:只要有 CHATU_DATA_URL + CHATU_APP_KEY 就可用(数据走 EdgeOne/byo 时 ai 仍走平台)
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveAiConfig(): PlatformConfig | null;
|
|
80
|
+
/** 测试/打包器场景:预注册可选依赖模块,optionalImport 直接返回(不走动态 import) */
|
|
81
|
+
export declare function registerOptionalModule(name: string, mod: unknown): void;
|
|
63
82
|
export declare function optionalImport<T = any>(name: string, hint: string): Promise<T>;
|
package/dist/config.js
CHANGED
|
@@ -11,7 +11,21 @@ export function resolveConfig() {
|
|
|
11
11
|
const apiKey = override.apiKey ?? env.CHATU_APP_KEY ?? env.CHATU_CUSTOMER_API_KEY;
|
|
12
12
|
const redisUrl = env.REDIS_URL;
|
|
13
13
|
const s3Bucket = env.S3_BUCKET;
|
|
14
|
-
const
|
|
14
|
+
const envDriver = (env.CHATU_DATA_DRIVER ?? '').toLowerCase();
|
|
15
|
+
const driver = override.driver ??
|
|
16
|
+
(envDriver === 'edgeone' || envDriver === 'byo' || envDriver === 'memory' || envDriver === 'platform'
|
|
17
|
+
? envDriver
|
|
18
|
+
: baseUrl && apiKey ? 'platform' : redisUrl || s3Bucket ? 'byo' : 'memory');
|
|
19
|
+
if (driver === 'edgeone') {
|
|
20
|
+
return {
|
|
21
|
+
kind: 'edgeone',
|
|
22
|
+
kvStore: env.CHATU_EDGEONE_KV_STORE || 'chatu-kv',
|
|
23
|
+
storageStore: env.CHATU_EDGEONE_STORAGE_STORE || 'chatu-storage',
|
|
24
|
+
projectId: env.EDGEONE_BLOB_PROJECT_ID || undefined,
|
|
25
|
+
token: env.EDGEONE_BLOB_TOKEN || undefined,
|
|
26
|
+
publicPathPrefix: (env.CHATU_BLOB_PUBLIC_PATH || '/_chatu/blob').replace(/\/+$/, ''),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
15
29
|
if (driver === 'byo') {
|
|
16
30
|
return {
|
|
17
31
|
kind: 'byo',
|
|
@@ -66,15 +80,48 @@ export function describe() {
|
|
|
66
80
|
return { driver: 'platform', env: c.env, baseUrl: c.baseUrl };
|
|
67
81
|
if (c.kind === 'byo')
|
|
68
82
|
return { driver: 'byo', kv: c.redisUrl ? 'redis' : 'memory', storage: c.s3 ? 's3' : 'memory' };
|
|
83
|
+
if (c.kind === 'edgeone')
|
|
84
|
+
return { driver: 'edgeone', kv: `blob:${c.kvStore}`, storage: `blob:${c.storageStore}` };
|
|
69
85
|
return { driver: 'memory' };
|
|
70
86
|
}
|
|
71
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* AI 中继配置与数据驱动解耦:只要有 CHATU_DATA_URL + CHATU_APP_KEY 就可用(数据走 EdgeOne/byo 时 ai 仍走平台)
|
|
89
|
+
*/
|
|
90
|
+
export function resolveAiConfig() {
|
|
91
|
+
const c = resolveConfig();
|
|
92
|
+
if (c.kind === 'platform')
|
|
93
|
+
return c;
|
|
94
|
+
const proc = globalThis.process;
|
|
95
|
+
const env = proc?.env ?? {};
|
|
96
|
+
const baseUrl = override.baseUrl ?? env.CHATU_DATA_URL;
|
|
97
|
+
const apiKey = override.apiKey ?? env.CHATU_APP_KEY ?? env.CHATU_CUSTOMER_API_KEY;
|
|
98
|
+
if (!baseUrl || !apiKey)
|
|
99
|
+
return null;
|
|
100
|
+
const normalizedBase = baseUrl.replace(/\/+$/, '');
|
|
101
|
+
return {
|
|
102
|
+
kind: 'platform',
|
|
103
|
+
baseUrl: normalizedBase,
|
|
104
|
+
apiKey,
|
|
105
|
+
env: (override.env ?? env.CHATU_DATA_ENV ?? 'dev').toLowerCase() === 'prod' ? 'prod' : 'dev',
|
|
106
|
+
fetchImpl: override.fetchImpl ?? fetch,
|
|
107
|
+
aiBaseUrl: (override.aiBaseUrl ?? env.CHATU_AI_URL ?? deriveAiBaseUrl(normalizedBase)).replace(/\/+$/, ''),
|
|
108
|
+
aiModel: override.model ?? env.CHATU_AI_MODEL ?? env.PRIMARY_MODEL,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** 动态加载可选依赖(ioredis / @aws-sdk/* / @edgeone/pages-blob),不参与打包静态分析;缺失时给出可操作的错误 */
|
|
112
|
+
const registeredModules = new Map();
|
|
113
|
+
/** 测试/打包器场景:预注册可选依赖模块,optionalImport 直接返回(不走动态 import) */
|
|
114
|
+
export function registerOptionalModule(name, mod) {
|
|
115
|
+
registeredModules.set(name, mod);
|
|
116
|
+
}
|
|
72
117
|
export async function optionalImport(name, hint) {
|
|
118
|
+
if (registeredModules.has(name))
|
|
119
|
+
return registeredModules.get(name);
|
|
73
120
|
try {
|
|
74
121
|
const dyn = new Function('m', 'return import(m)');
|
|
75
122
|
return await dyn(name);
|
|
76
123
|
}
|
|
77
124
|
catch {
|
|
78
|
-
throw new Error(`@chatu-ai/app-sdk:
|
|
125
|
+
throw new Error(`@chatu-ai/app-sdk: driver requires "${name}" — ${hint}`);
|
|
79
126
|
}
|
|
80
127
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EdgeoneConfig } from './config.js';
|
|
2
|
+
import type { KvClient } from './kv.js';
|
|
3
|
+
import type { StorageClient } from './storage.js';
|
|
4
|
+
/** kv 键 → Blob key:逐字符编码保持前缀关系(list(prefix) 可用),'/' 保留为目录分隔 */
|
|
5
|
+
export declare function encodeKvKey(key: string): string;
|
|
6
|
+
export declare function decodeKvKey(key: string): string;
|
|
7
|
+
export declare function edgeoneKv(cfg: EdgeoneConfig): KvClient;
|
|
8
|
+
export declare function edgeoneStorage(cfg: EdgeoneConfig): StorageClient;
|
package/dist/edgeone.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { optionalImport } from './config.js';
|
|
2
|
+
import { AppSdkError } from './errors.js';
|
|
3
|
+
const HINT = 'run `npm i @edgeone/pages-blob` (preinstalled in the ChatU Builder template)';
|
|
4
|
+
function storeFactory(cfg) {
|
|
5
|
+
let modPromise = null;
|
|
6
|
+
const cache = new Map();
|
|
7
|
+
return (name) => {
|
|
8
|
+
let p = cache.get(name);
|
|
9
|
+
if (!p) {
|
|
10
|
+
p = (modPromise ??= optionalImport('@edgeone/pages-blob', HINT)).then(m => {
|
|
11
|
+
const getStore = m.getStore ?? m.default?.getStore;
|
|
12
|
+
if (typeof getStore !== 'function')
|
|
13
|
+
throw new AppSdkError('EDGEONE_SDK', '@edgeone/pages-blob: getStore not found');
|
|
14
|
+
return cfg.projectId && cfg.token
|
|
15
|
+
? getStore({ name, projectId: cfg.projectId, token: cfg.token, consistency: 'strong' })
|
|
16
|
+
: getStore(name);
|
|
17
|
+
});
|
|
18
|
+
cache.set(name, p);
|
|
19
|
+
}
|
|
20
|
+
return p;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** kv 键 → Blob key:逐字符编码保持前缀关系(list(prefix) 可用),'/' 保留为目录分隔 */
|
|
24
|
+
export function encodeKvKey(key) {
|
|
25
|
+
return key
|
|
26
|
+
.split('/')
|
|
27
|
+
.map(seg => encodeURIComponent(seg).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`))
|
|
28
|
+
.join('/');
|
|
29
|
+
}
|
|
30
|
+
export function decodeKvKey(key) {
|
|
31
|
+
try {
|
|
32
|
+
return key.split('/').map(decodeURIComponent).join('/');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return key;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function wrapErr(e, what) {
|
|
39
|
+
if (e instanceof AppSdkError)
|
|
40
|
+
return e;
|
|
41
|
+
const code = typeof e?.code === 'string' ? e.code : 'EDGEONE_BLOB';
|
|
42
|
+
return new AppSdkError(code, `${what}: ${e?.message ?? String(e)}`);
|
|
43
|
+
}
|
|
44
|
+
export function edgeoneKv(cfg) {
|
|
45
|
+
const getStore = storeFactory(cfg);
|
|
46
|
+
const store = () => getStore(cfg.kvStore);
|
|
47
|
+
const readEnv = async (key) => {
|
|
48
|
+
const s = await store();
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
raw = await s.get(encodeKvKey(key), { type: 'json', consistency: 'strong' });
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
throw wrapErr(e, 'kv get');
|
|
55
|
+
}
|
|
56
|
+
if (raw === null || raw === undefined)
|
|
57
|
+
return null;
|
|
58
|
+
const env = raw && typeof raw === 'object' && 'v' in raw ? raw : { v: raw };
|
|
59
|
+
if (env.exp !== undefined && Date.now() > env.exp) {
|
|
60
|
+
void s.delete(encodeKvKey(key)).catch(() => undefined);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return env;
|
|
64
|
+
};
|
|
65
|
+
const writeEnv = async (key, env) => {
|
|
66
|
+
const s = await store();
|
|
67
|
+
try {
|
|
68
|
+
await s.setJSON(encodeKvKey(key), env);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
throw wrapErr(e, 'kv set');
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
async get(key) { return (await readEnv(key))?.v ?? null; },
|
|
76
|
+
async set(key, value, opts) { await writeEnv(key, { v: value, exp: opts?.ex ? Date.now() + opts.ex * 1000 : undefined }); },
|
|
77
|
+
async del(key) {
|
|
78
|
+
const s = await store();
|
|
79
|
+
const existed = (await readEnv(key)) !== null;
|
|
80
|
+
try {
|
|
81
|
+
await s.delete(encodeKvKey(key));
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
throw wrapErr(e, 'kv del');
|
|
85
|
+
}
|
|
86
|
+
return existed;
|
|
87
|
+
},
|
|
88
|
+
async incr(key, by = 1) {
|
|
89
|
+
const env = await readEnv(key);
|
|
90
|
+
const cur = Number(env?.v ?? 0);
|
|
91
|
+
if (!Number.isInteger(cur))
|
|
92
|
+
throw new AppSdkError('NOT_AN_INTEGER', 'value is not an integer');
|
|
93
|
+
const next = cur + by;
|
|
94
|
+
await writeEnv(key, { v: next, exp: env?.exp });
|
|
95
|
+
return next;
|
|
96
|
+
},
|
|
97
|
+
async expire(key, seconds) {
|
|
98
|
+
const env = await readEnv(key);
|
|
99
|
+
if (!env)
|
|
100
|
+
return false;
|
|
101
|
+
await writeEnv(key, { v: env.v, exp: Date.now() + seconds * 1000 });
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
async mget(keys) { return Promise.all(keys.map(async (k) => (await readEnv(k))?.v ?? null)); },
|
|
105
|
+
async list(prefix = '', opts) {
|
|
106
|
+
const s = await store();
|
|
107
|
+
let r;
|
|
108
|
+
try {
|
|
109
|
+
r = await s.list({ prefix: encodeKvKey(prefix), cursor: opts?.cursor ?? undefined, limit: opts?.limit ?? 100, paginate: false, consistency: 'strong' });
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
throw wrapErr(e, 'kv list');
|
|
113
|
+
}
|
|
114
|
+
return { keys: r.blobs.map(b => decodeKvKey(b.key)), nextCursor: r.cursor ?? null };
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function edgeoneStorage(cfg) {
|
|
119
|
+
const getStore = storeFactory(cfg);
|
|
120
|
+
const store = () => getStore(cfg.storageStore);
|
|
121
|
+
const enc = (key) => key.split('/').map(encodeURIComponent).join('/');
|
|
122
|
+
const toBlob = async (data, contentType) => {
|
|
123
|
+
if (data instanceof Blob)
|
|
124
|
+
return contentType && data.type !== contentType ? new Blob([await data.arrayBuffer()], { type: contentType }) : data;
|
|
125
|
+
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
126
|
+
return new Blob([bytes], { type: contentType ?? 'application/octet-stream' });
|
|
127
|
+
};
|
|
128
|
+
const head = async (key) => {
|
|
129
|
+
const s = await store();
|
|
130
|
+
let m;
|
|
131
|
+
try {
|
|
132
|
+
m = await s.getMetadata(key, { consistency: 'strong' });
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
throw wrapErr(e, 'storage head');
|
|
136
|
+
}
|
|
137
|
+
if (!m)
|
|
138
|
+
return null;
|
|
139
|
+
const len = Number(m.headers?.['content-length'] ?? m.headers?.['Content-Length'] ?? 0);
|
|
140
|
+
const lm = m.headers?.['last-modified'] ?? m.headers?.['Last-Modified'] ?? null;
|
|
141
|
+
return { key, size: Number.isFinite(len) ? len : 0, lastModified: lm };
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
async put(key, data, opts) {
|
|
145
|
+
const s = await store();
|
|
146
|
+
const blob = await toBlob(data, opts?.contentType);
|
|
147
|
+
try {
|
|
148
|
+
await s.set(key, blob);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
throw wrapErr(e, 'storage put');
|
|
152
|
+
}
|
|
153
|
+
return { key, size: blob.size };
|
|
154
|
+
},
|
|
155
|
+
async uploadUrl(key, opts) {
|
|
156
|
+
const s = await store();
|
|
157
|
+
const expireSeconds = 600;
|
|
158
|
+
let r;
|
|
159
|
+
try {
|
|
160
|
+
r = await s.createUploadUrl(key, { expireSeconds, contentType: opts?.contentType });
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
throw wrapErr(e, 'storage uploadUrl');
|
|
164
|
+
}
|
|
165
|
+
return { url: r.url, method: 'PUT', expiresIn: expireSeconds, headers: opts?.contentType ? { 'content-type': opts.contentType } : undefined };
|
|
166
|
+
},
|
|
167
|
+
async get(key) {
|
|
168
|
+
const s = await store();
|
|
169
|
+
let buf;
|
|
170
|
+
try {
|
|
171
|
+
buf = await s.get(key, { type: 'arrayBuffer', consistency: 'strong' });
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
throw wrapErr(e, 'storage get');
|
|
175
|
+
}
|
|
176
|
+
return buf ? new Uint8Array(buf) : null;
|
|
177
|
+
},
|
|
178
|
+
async url(key, opts) {
|
|
179
|
+
// Pages Blob 无公开读地址:走应用内代理路由(模板内置),下载名通过 query 传给路由
|
|
180
|
+
const q = opts?.downloadName ? `?download=${encodeURIComponent(opts.downloadName)}` : '';
|
|
181
|
+
return `${cfg.publicPathPrefix}/${enc(key)}${q}`;
|
|
182
|
+
},
|
|
183
|
+
head,
|
|
184
|
+
async delete(key) {
|
|
185
|
+
const s = await store();
|
|
186
|
+
try {
|
|
187
|
+
await s.delete(key);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
throw wrapErr(e, 'storage delete');
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
async list(prefix = '', opts) {
|
|
194
|
+
const s = await store();
|
|
195
|
+
let r;
|
|
196
|
+
try {
|
|
197
|
+
r = await s.list({ prefix, cursor: opts?.cursor ?? undefined, limit: opts?.limit ?? 100, paginate: false, consistency: 'strong' });
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
throw wrapErr(e, 'storage list');
|
|
201
|
+
}
|
|
202
|
+
return { items: r.blobs.map(b => ({ key: b.key, size: 0, lastModified: null })), nextCursor: r.cursor ?? null };
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { beforeEach, describe as d, expect, it } from 'vitest';
|
|
2
|
+
import { configure, describe, kv, storage, registerOptionalModule, encodeKvKey, decodeKvKey } from './index';
|
|
3
|
+
/** 内存版 Pages Blob store:模拟 @edgeone/pages-blob 的 getStore */
|
|
4
|
+
function fakeBlobModule() {
|
|
5
|
+
const stores = new Map();
|
|
6
|
+
const getStore = (arg) => {
|
|
7
|
+
const name = typeof arg === 'string' ? arg : arg.name;
|
|
8
|
+
let m = stores.get(name);
|
|
9
|
+
if (!m) {
|
|
10
|
+
m = new Map();
|
|
11
|
+
stores.set(name, m);
|
|
12
|
+
}
|
|
13
|
+
const map = m;
|
|
14
|
+
return {
|
|
15
|
+
async set(key, value) { map.set(key, { body: value }); },
|
|
16
|
+
async setJSON(key, value) { map.set(key, { body: JSON.stringify(value), json: value }); },
|
|
17
|
+
async get(key, opts) {
|
|
18
|
+
const e = map.get(key);
|
|
19
|
+
if (!e)
|
|
20
|
+
return null;
|
|
21
|
+
if (opts?.type === 'json')
|
|
22
|
+
return e.json ?? JSON.parse(String(e.body));
|
|
23
|
+
if (opts?.type === 'arrayBuffer')
|
|
24
|
+
return e.body instanceof Blob ? await e.body.arrayBuffer() : new TextEncoder().encode(String(e.body)).buffer;
|
|
25
|
+
return e.body instanceof Blob ? await e.body.text() : String(e.body);
|
|
26
|
+
},
|
|
27
|
+
async getMetadata(key) { const e = map.get(key); return e ? { headers: { 'content-length': String(e.body instanceof Blob ? e.body.size : String(e.body).length) } } : null; },
|
|
28
|
+
async delete(key) { map.delete(key); },
|
|
29
|
+
async list(opts) {
|
|
30
|
+
const keys = [...map.keys()].filter(k => k.startsWith(opts?.prefix ?? '')).sort();
|
|
31
|
+
return { blobs: keys.slice(0, opts?.limit ?? 100).map(key => ({ key, etag: 'x' })) };
|
|
32
|
+
},
|
|
33
|
+
async createUploadUrl(key, opts) { return { url: `https://blob.test/${name}/${key}?sig=1`, key, expiresAt: Date.now() + (opts?.expireSeconds ?? 3600) * 1000 }; },
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
return { getStore, stores };
|
|
37
|
+
}
|
|
38
|
+
d('edgeone driver (Pages Blob)', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
registerOptionalModule('@edgeone/pages-blob', fakeBlobModule());
|
|
41
|
+
configure({ driver: 'edgeone' });
|
|
42
|
+
});
|
|
43
|
+
it('kv: envelope, ttl, incr, list with encoded prefix', async () => {
|
|
44
|
+
expect(describe().driver).toBe('edgeone');
|
|
45
|
+
await kv.set('todos:1', { a: 1 });
|
|
46
|
+
expect(await kv.get('todos:1')).toEqual({ a: 1 });
|
|
47
|
+
expect(await kv.incr('views')).toBe(1);
|
|
48
|
+
expect(await kv.incr('views', 5)).toBe(6);
|
|
49
|
+
await kv.set('todos:2', 'x', { ex: -1 }); // 已过期
|
|
50
|
+
expect(await kv.get('todos:2')).toBeNull();
|
|
51
|
+
await kv.set('todos:3', 'y');
|
|
52
|
+
expect((await kv.list('todos:')).keys).toEqual(['todos:1', 'todos:3']);
|
|
53
|
+
expect(await kv.mget(['todos:1', 'nope'])).toEqual([{ a: 1 }, null]);
|
|
54
|
+
expect(await kv.del('todos:1')).toBe(true);
|
|
55
|
+
expect(await kv.del('todos:1')).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
it('storage: put/get/head/list/uploadUrl/url', async () => {
|
|
58
|
+
const r = await storage.put('photos/a.txt', 'hello', { contentType: 'text/plain' });
|
|
59
|
+
expect(r.size).toBe(5);
|
|
60
|
+
expect(new TextDecoder().decode((await storage.get('photos/a.txt')))).toBe('hello');
|
|
61
|
+
expect((await storage.head('photos/a.txt'))?.size).toBe(5);
|
|
62
|
+
expect((await storage.list('photos/')).items.map(i => i.key)).toEqual(['photos/a.txt']);
|
|
63
|
+
const up = await storage.uploadUrl('photos/b.png', { contentType: 'image/png' });
|
|
64
|
+
expect(up.method).toBe('PUT');
|
|
65
|
+
expect(up.url).toContain('photos/b.png');
|
|
66
|
+
expect(await storage.url('photos/a.txt', { downloadName: 'x.txt' })).toBe('/_chatu/blob/photos/a.txt?download=x.txt');
|
|
67
|
+
await storage.delete('photos/a.txt');
|
|
68
|
+
expect(await storage.get('photos/a.txt')).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
it('key encoding keeps prefix order and round-trips', () => {
|
|
71
|
+
expect(encodeKvKey('todos:1/x y')).toBe('todos%3A1/x%20y');
|
|
72
|
+
expect(decodeKvKey(encodeKvKey('a:b/c d'))).toBe('a:b/c d');
|
|
73
|
+
expect(encodeKvKey('todos:12').startsWith(encodeKvKey('todos:1'))).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { kv, getKv } from './kv.js';
|
|
2
2
|
export type { KvClient, KvSetOptions, KvListResult } from './kv.js';
|
|
3
|
-
export { configure, describe } from './config.js';
|
|
3
|
+
export { configure, describe, registerOptionalModule } from './config.js';
|
|
4
4
|
export type { ConfigureOptions, DriverKind } from './config.js';
|
|
5
5
|
export { AppSdkError } from './errors.js';
|
|
6
6
|
export { storage, getStorage } from './storage.js';
|
|
7
7
|
export type { StorageClient, StorageObject, StorageListResult, UploadUrlResult } from './storage.js';
|
|
8
8
|
export { ai, getAi } from './ai.js';
|
|
9
9
|
export type { AiClient, AiMessage, AiChatOptions, AiChatResult, AiUsage } from './ai.js';
|
|
10
|
+
export { encodeKvKey, decodeKvKey } from './edgeone.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { kv, getKv } from './kv.js';
|
|
2
|
-
export { configure, describe } from './config.js';
|
|
2
|
+
export { configure, describe, registerOptionalModule } from './config.js';
|
|
3
3
|
export { AppSdkError } from './errors.js';
|
|
4
4
|
export { storage, getStorage } from './storage.js';
|
|
5
5
|
export { ai, getAi } from './ai.js';
|
|
6
|
+
export { encodeKvKey, decodeKvKey } from './edgeone.js';
|
package/dist/kv.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveConfig } from './config.js';
|
|
2
2
|
import { AppSdkError } from './errors.js';
|
|
3
3
|
import { byoKv } from './byo.js';
|
|
4
|
+
import { edgeoneKv } from './edgeone.js';
|
|
4
5
|
// ---------- platform driver ----------
|
|
5
6
|
function platformKv(cfg) {
|
|
6
7
|
const headers = { 'x-api-key': cfg.apiKey, 'x-chatu-env': cfg.env, 'content-type': 'application/json' };
|
|
@@ -68,9 +69,9 @@ let cached = null;
|
|
|
68
69
|
/** 按当前配置取 KV 客户端(惰性、缓存;configure() 后自动重建) */
|
|
69
70
|
export function getKv() {
|
|
70
71
|
const cfg = resolveConfig();
|
|
71
|
-
const key = cfg.kind === 'platform' ? `platform|${cfg.baseUrl}|${cfg.env}|${cfg.apiKey.slice(-4)}` : cfg.kind === 'byo' ? `byo|${cfg.redisUrl ?? ''}|${cfg.kvPrefix}` : 'memory';
|
|
72
|
+
const key = cfg.kind === 'platform' ? `platform|${cfg.baseUrl}|${cfg.env}|${cfg.apiKey.slice(-4)}` : cfg.kind === 'byo' ? `byo|${cfg.redisUrl ?? ''}|${cfg.kvPrefix}` : cfg.kind === 'edgeone' ? `edgeone|${cfg.kvStore}|${cfg.projectId ?? ''}` : 'memory';
|
|
72
73
|
if (!cached || cached.key !== key)
|
|
73
|
-
cached = { key, client: cfg.kind === 'platform' ? platformKv(cfg) : cfg.kind === 'byo' ? byoKv(cfg, memoryKv()) : memoryKv() };
|
|
74
|
+
cached = { key, client: cfg.kind === 'platform' ? platformKv(cfg) : cfg.kind === 'byo' ? byoKv(cfg, memoryKv()) : cfg.kind === 'edgeone' ? edgeoneKv(cfg) : memoryKv() };
|
|
74
75
|
return cached.client;
|
|
75
76
|
}
|
|
76
77
|
/** 便捷单例:`import { kv } from '@chatu-ai/app-sdk'` */
|
package/dist/storage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveConfig } from './config.js';
|
|
2
|
+
import { edgeoneStorage } from './edgeone.js';
|
|
2
3
|
import { AppSdkError } from './errors.js';
|
|
3
4
|
import { byoStorage } from './byo.js';
|
|
4
5
|
function toBytes(data) {
|
|
@@ -97,9 +98,9 @@ function memoryStorage() {
|
|
|
97
98
|
let cached = null;
|
|
98
99
|
export function getStorage() {
|
|
99
100
|
const cfg = resolveConfig();
|
|
100
|
-
const key = cfg.kind === 'platform' ? `platform|${cfg.baseUrl}|${cfg.env}|${cfg.apiKey.slice(-4)}` : cfg.kind === 'byo' ? `byo|${cfg.s3?.bucket ?? ''}|${cfg.s3?.prefix ?? ''}` : 'memory';
|
|
101
|
+
const key = cfg.kind === 'platform' ? `platform|${cfg.baseUrl}|${cfg.env}|${cfg.apiKey.slice(-4)}` : cfg.kind === 'byo' ? `byo|${cfg.s3?.bucket ?? ''}|${cfg.s3?.prefix ?? ''}` : cfg.kind === 'edgeone' ? `edgeone|${cfg.storageStore}|${cfg.projectId ?? ''}` : 'memory';
|
|
101
102
|
if (!cached || cached.key !== key)
|
|
102
|
-
cached = { key, client: cfg.kind === 'platform' ? platformStorage(cfg) : cfg.kind === 'byo' ? byoStorage(cfg, memoryStorage()) : memoryStorage() };
|
|
103
|
+
cached = { key, client: cfg.kind === 'platform' ? platformStorage(cfg) : cfg.kind === 'byo' ? byoStorage(cfg, memoryStorage()) : cfg.kind === 'edgeone' ? edgeoneStorage(cfg) : memoryStorage() };
|
|
103
104
|
return cached.client;
|
|
104
105
|
}
|
|
105
106
|
export const storage = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatu-ai/app-sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Runtime SDK for apps generated by ChatU Builder: kv, storage and ai (OpenAI-compatible LLM relay) with platform / byo / memory drivers selected by environment variables",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|