@openloaf-saas/sdk 0.1.9 → 0.1.11

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,520 @@
1
+ # @openloaf-saas/sdk
2
+
3
+ 统一入口 SDK,包含全部模块能力:
4
+ - **模块**:`auth` / `user` / `feedback` / `ai` / `aiTools` / `auxiliary`
5
+ - **模块**:`auth` / `user` / `feedback` / `ai` / `aiTools` / `auxiliary` / `skills`
6
+ - **tRPC**:`createTrpcClient`(仅 publicRouter)
7
+ - **契约**:`SaaSContract` + `Endpoint` 统一校验输入/输出
8
+
9
+ ---
10
+
11
+ ## 安装
12
+
13
+ ```bash
14
+ bun add @openloaf-saas/sdk
15
+ ```
16
+
17
+ ---
18
+
19
+ ## 入口
20
+
21
+ ```ts
22
+ import { SaaSClient, createTrpcClient } from "@openloaf-saas/sdk";
23
+ ```
24
+
25
+ ---
26
+
27
+ ## 快速上手
28
+
29
+ ### 初始化客户端
30
+
31
+ ```ts
32
+ import { SaaSClient } from "@openloaf-saas/sdk";
33
+
34
+ let accessToken = "";
35
+ let refreshToken = "";
36
+
37
+ const client = new SaaSClient({
38
+ baseUrl: "https://saas.example.com",
39
+ getAccessToken: () => accessToken,
40
+ });
41
+ ```
42
+
43
+ ### Auth:换码 + 刷新 + 退出
44
+
45
+ ```ts
46
+ const exchanged = await client.auth.exchange("<login-code>");
47
+ accessToken = exchanged.accessToken;
48
+ refreshToken = exchanged.refreshToken;
49
+
50
+ const refreshed = await client.auth.refresh(refreshToken);
51
+ if ("message" in refreshed) {
52
+ throw new Error(refreshed.message);
53
+ }
54
+ accessToken = refreshed.accessToken;
55
+ refreshToken = refreshed.refreshToken;
56
+
57
+ await client.auth.logout(refreshToken);
58
+ ```
59
+
60
+ ### User:获取当前用户
61
+
62
+ ```ts
63
+ const me = await client.user.self();
64
+ console.log(me.user.id, me.user.membershipLevel, me.user.creditsBalance);
65
+ ```
66
+
67
+ ### Feedback:提交/列表/详情
68
+
69
+ ```ts
70
+ await client.feedback.submit({
71
+ source: "tenas",
72
+ type: "performance",
73
+ content: "页面加载有点慢",
74
+ context: { page: "/dashboard", device: "macOS" },
75
+ email: "user@example.com",
76
+ });
77
+
78
+ const list = await client.feedback.list({
79
+ page: 1,
80
+ pageSize: 20,
81
+ type: "ui",
82
+ saasStatus: "unread",
83
+ keyword: "按钮",
84
+ });
85
+
86
+ const detail = await client.feedback.detail("<feedback-id>");
87
+ console.log(list, detail);
88
+ ```
89
+
90
+ ### 上传反馈附件
91
+
92
+ ```ts
93
+ const attachment = await client.feedback.uploadAttachment(file, "screenshot.png");
94
+ console.log(attachment.url, attachment.key);
95
+ ```
96
+
97
+ 说明:
98
+ - `feedback.submit` 支持匿名或登录调用(token 可选)
99
+ - `feedback.list`、`feedback.detail`、`feedback.uploadAttachment` 需要登录态(Bearer token)
100
+ - `feedback.uploadAttachment` 使用 multipart/form-data 上传
101
+
102
+ ### Skills:列表 / 详情 / 下载
103
+
104
+ ```ts
105
+ const skills = await client.skills.list({
106
+ page: 1,
107
+ pageSize: 12,
108
+ keyword: "sdk",
109
+ type: "development",
110
+ });
111
+
112
+ const detail = await client.skills.detail("<skill-id>");
113
+ const archive = await client.skills.download("<skill-id>");
114
+
115
+ console.log(skills.items.length, detail.skill.name, archive.fileName);
116
+ ```
117
+
118
+ 说明:
119
+ - `skills.list`、`skills.detail`、`skills.download` 需要登录态
120
+ - `skills.download` 返回二进制内容 `ArrayBuffer`
121
+
122
+ ---
123
+
124
+ ## AI 能力(ai)
125
+
126
+ ### 积分计费
127
+
128
+ 所有 AI 接口按积分计费,**不暴露原始 token 用量**,统一返回积分消耗:
129
+
130
+ | 接口类型 | 积分返回方式 |
131
+ |---------|------------|
132
+ | 图片/视频任务 | 轮询 `task()` 响应中的 `creditsConsumed` 字段 |
133
+ | Chat 非流式 | JSON body `x_credits_consumed` + header `x-credits-consumed` |
134
+ | Chat 流式 | 流末尾追加 SSE 事件 `data: {"x_credits_consumed": N}` |
135
+
136
+ 积分不足时返回 HTTP 402 + `{"error":{"message":"...","code":"INSUFFICIENT_CREDITS"}}`。
137
+
138
+ ### 提交图像任务
139
+
140
+ ```ts
141
+ const created = await client.ai.image({
142
+ modelId: "qwen-image-edit-plus",
143
+ prompt: "A dancing bear",
144
+ output: {
145
+ count: 1,
146
+ aspectRatio: "1:1",
147
+ quality: "standard",
148
+ },
149
+ });
150
+
151
+ if (!created.success) {
152
+ throw new Error(created.message);
153
+ }
154
+ ```
155
+
156
+ ### 查询任务与获取积分消耗
157
+
158
+ ```ts
159
+ const status = await client.ai.task(created.data.taskId);
160
+ if (status.success && status.data.status === "succeeded") {
161
+ console.log(status.data.resultUrls); // 结果地址
162
+ console.log(status.data.creditsConsumed); // 本次消耗积分
163
+ }
164
+
165
+ // 取消任务
166
+ await client.ai.cancelTask(created.data.taskId);
167
+ ```
168
+
169
+ ### Chat 聊天补全
170
+
171
+ Chat 接口兼容 OpenAI 格式(`/api/v1/chat/completions`),SDK 不封装,使用标准 AI SDK 调用。
172
+
173
+ 支持以下可选元数据字段,用于平台侧日志追踪与统计分析;所有字段均为可选,旧请求可继续按原样发送:
174
+
175
+ | 字段 | 类型 | 说明 |
176
+ |------|------|------|
177
+ | `chatSessionId` | `string?` | 会话 ID,用于关联同一对话的多次请求 |
178
+ | `clientId` | `string?` | 客户端实例 ID,区分同一用户的不同设备/实例 |
179
+ | `desktopVersion` | `string?` | Desktop 客户端版本号 |
180
+ | `serverVersion` | `string?` | Server 端版本号 |
181
+ | `webVersion` | `string?` | Web 客户端版本号 |
182
+
183
+ 这些字段仅用于平台内部日志记录,不会透传给上游 AI 供应商。
184
+
185
+ 如需在 TypeScript 中约束请求体,可使用 SDK 导出的 `AiChatCompletionsRequest` / `AiResponsesRequest` 类型。
186
+
187
+ **非流式 — 从响应 body 读取积分:**
188
+
189
+ ```ts
190
+ import type { AiChatCompletionsRequest } from "@openloaf-saas/sdk";
191
+
192
+ const payload: AiChatCompletionsRequest = {
193
+ model: "deepseek-chat",
194
+ chatSessionId: "chat_sess_123",
195
+ clientId: "device_abc",
196
+ desktopVersion: "1.2.0",
197
+ messages: [{ role: "user", content: "Hello" }],
198
+ };
199
+
200
+ const resp = await fetch(`${baseUrl}/api/v1/chat/completions`, {
201
+ method: "POST",
202
+ headers: {
203
+ "Content-Type": "application/json",
204
+ Authorization: `Bearer ${accessToken}`,
205
+ },
206
+ body: JSON.stringify(payload),
207
+ });
208
+ const data = await resp.json();
209
+ console.log(data.x_credits_consumed); // number — 本次消耗积分
210
+ // 注意:响应中不包含 usage(token 用量已剥离)
211
+ ```
212
+
213
+ **流式 — 从 SSE 事件读取积分:**
214
+
215
+ ```ts
216
+ const resp = await fetch(`${baseUrl}/api/v1/chat/completions`, {
217
+ method: "POST",
218
+ headers: {
219
+ "Content-Type": "application/json",
220
+ Authorization: `Bearer ${accessToken}`,
221
+ },
222
+ body: JSON.stringify({
223
+ model: "deepseek-chat",
224
+ chatSessionId: "chat_sess_123",
225
+ messages: [{ role: "user", content: "Hello" }],
226
+ stream: true,
227
+ }),
228
+ });
229
+
230
+ let creditsConsumed: number | null = null;
231
+ const reader = resp.body!.getReader();
232
+ const decoder = new TextDecoder();
233
+ let buffer = "";
234
+
235
+ while (true) {
236
+ const { done, value } = await reader.read();
237
+ if (done) break;
238
+ buffer += decoder.decode(value, { stream: true });
239
+ const lines = buffer.split("\n");
240
+ buffer = lines.pop()!;
241
+ for (const line of lines) {
242
+ const trimmed = line.trim();
243
+ if (!trimmed.startsWith("data:")) continue;
244
+ const payload = trimmed.slice(5).trim();
245
+ if (payload === "[DONE]") continue;
246
+ try {
247
+ const parsed = JSON.parse(payload);
248
+ // 流末尾的积分消耗事件
249
+ if (parsed.x_credits_consumed != null) {
250
+ creditsConsumed = parsed.x_credits_consumed;
251
+ }
252
+ } catch {}
253
+ }
254
+ }
255
+ console.log(creditsConsumed); // number — 本次消耗积分
256
+ ```
257
+
258
+ ### 模型与 Provider 模板
259
+
260
+ ```ts
261
+ const imageModels = await client.ai.imageModels();
262
+ const videoModels = await client.ai.videoModels();
263
+ const chatModels = await client.ai.chatModels();
264
+ const modelTimestamps = await client.ai.modelsUpdatedAt();
265
+
266
+ // 公开接口:不需要鉴权
267
+ const providers = await client.ai.providerTemplates();
268
+ console.log(imageModels, videoModels, chatModels, modelTimestamps, providers);
269
+ ```
270
+
271
+ ### 上传文件(AI 对话附件)
272
+
273
+ ```ts
274
+ const uploaded = await client.ai.uploadFile(file, "image.png");
275
+ console.log(uploaded.url);
276
+ ```
277
+
278
+ 说明:
279
+ - SDK 方法名为 `video`,对应服务端路径为 `/api/ai/video`
280
+ - `imageModels` / `videoModels` / `chatModels` / `modelsUpdatedAt` 对应公开路径 `/api/public/ai/*`
281
+ - `ai.uploadFile` 使用 multipart/form-data 上传,需要登录态
282
+
283
+ ### 模型缓存推荐用法
284
+
285
+ ```ts
286
+ const timestamps = await client.ai.modelsUpdatedAt();
287
+ if (!timestamps.success) {
288
+ throw new Error(timestamps.message);
289
+ }
290
+
291
+ const latestUpdatedAt = timestamps.data.latestUpdatedAt;
292
+ const cachedLatest = localStorage.getItem("ai_models_latest_updated_at");
293
+
294
+ if (cachedLatest !== latestUpdatedAt) {
295
+ const [imageModels, videoModels, chatModels] = await Promise.all([
296
+ client.ai.imageModels(),
297
+ client.ai.videoModels(),
298
+ client.ai.chatModels(),
299
+ ]);
300
+ localStorage.setItem("ai_models_latest_updated_at", latestUpdatedAt);
301
+ localStorage.setItem("ai_models_image_cache", JSON.stringify(imageModels));
302
+ localStorage.setItem("ai_models_video_cache", JSON.stringify(videoModels));
303
+ localStorage.setItem("ai_models_chat_cache", JSON.stringify(chatModels));
304
+ }
305
+ ```
306
+
307
+ ---
308
+
309
+ ## AI 辅助工具(aiTools)
310
+
311
+ 来自 `ai-tools` 模块的 3 个非流式接口:
312
+ - `analyzeSkills`:技能分析(翻译、分类、图标)
313
+ - `recommendActions`:行为推荐
314
+ - `summarize`:文本总结
315
+
316
+ 服务端行为说明(与 `.agents/skills/ai-auxiliary-tools.md` 对齐):
317
+ - 不扣积分,仅记录日志
318
+ - 按用户内存限流,每日 100 次
319
+ - 复用 `ai.default_auxiliary_model` 配置进行路由
320
+
321
+ ### analyzeSkills
322
+
323
+ ```ts
324
+ const analyzed = await client.aiTools.analyzeSkills({
325
+ skills: [
326
+ { name: "Prompt Engineering", description: "构建高质量提示词" },
327
+ { name: "TypeScript" },
328
+ ],
329
+ });
330
+
331
+ if (!analyzed.success) {
332
+ throw new Error(analyzed.message);
333
+ }
334
+ console.log(analyzed.data.skills);
335
+ ```
336
+
337
+ ### recommendActions
338
+
339
+ ```ts
340
+ const recommended = await client.aiTools.recommendActions({
341
+ text: "用户反馈移动端上传按钮不明显,转化率下降。",
342
+ scene: "product-optimization",
343
+ });
344
+
345
+ if (!recommended.success) {
346
+ throw new Error(recommended.message);
347
+ }
348
+ console.log(recommended.data.actions);
349
+ ```
350
+
351
+ ### summarize
352
+
353
+ ```ts
354
+ const summarized = await client.aiTools.summarize({
355
+ text: "这里放长文本内容...",
356
+ });
357
+
358
+ if (!summarized.success) {
359
+ throw new Error(summarized.message);
360
+ }
361
+ console.log(summarized.data.summary);
362
+ ```
363
+
364
+ 说明:
365
+ - `aiTools` 接口需要登录态(Bearer token)
366
+
367
+ ---
368
+
369
+ ## 辅助推理(auxiliary)
370
+
371
+ ### 通用推理
372
+
373
+ ```ts
374
+ const result = await client.auxiliary.infer({
375
+ capabilityKey: "translate",
376
+ input: { text: "Hello world", targetLang: "zh" },
377
+ });
378
+ console.log(result);
379
+ ```
380
+
381
+ ### 查询日配额
382
+
383
+ ```ts
384
+ const quota = await client.auxiliary.getQuota();
385
+ console.log(quota);
386
+ ```
387
+
388
+ 说明:
389
+ - `auxiliary` 接口需要登录态(Bearer token)
390
+ - 不扣积分,仅记录日志,按用户每日限流
391
+
392
+ ---
393
+
394
+ ## Public tRPC(仅 publicRouter)
395
+
396
+ ```ts
397
+ import { createTrpcClient } from "@openloaf-saas/sdk";
398
+
399
+ const trpc = createTrpcClient({
400
+ baseUrl: "https://saas.example.com",
401
+ getAccessToken: () => localStorage.getItem("saas_access_token") ?? "",
402
+ });
403
+
404
+ const health = await trpc.healthCheck.query();
405
+ await trpc.feedback.submit.mutate({
406
+ source: "tenas",
407
+ type: "bug",
408
+ content: "保存按钮偶尔无响应",
409
+ context: { page: "/settings" },
410
+ });
411
+
412
+ console.log(health);
413
+ ```
414
+
415
+ 说明:
416
+ - SDK 只暴露 publicRouter;内部 tRPC 不对外暴露
417
+
418
+ ---
419
+
420
+ ## 错误处理
421
+
422
+ ```ts
423
+ import {
424
+ SaaSHttpError,
425
+ SaaSNetworkError,
426
+ SaaSSchemaError,
427
+ } from "@openloaf-saas/sdk";
428
+
429
+ try {
430
+ await client.user.self();
431
+ } catch (error) {
432
+ if (error instanceof SaaSHttpError) {
433
+ console.error(error.status, error.statusText, error.payload);
434
+ } else if (error instanceof SaaSSchemaError) {
435
+ console.error(error.issues);
436
+ } else if (error instanceof SaaSNetworkError) {
437
+ console.error(error.cause);
438
+ }
439
+ }
440
+ ```
441
+
442
+ ---
443
+
444
+ ## 自定义 fetch 与请求头
445
+
446
+ ```ts
447
+ import fetch from "node-fetch";
448
+ import { SaaSClient } from "@openloaf-saas/sdk";
449
+
450
+ const client = new SaaSClient({
451
+ baseUrl: "https://saas.example.com",
452
+ fetcher: fetch,
453
+ headers: {
454
+ "x-sdk-app": "tenas-web",
455
+ },
456
+ });
457
+ ```
458
+
459
+ ---
460
+
461
+ ## 端点契约
462
+
463
+ ```ts
464
+ import { contract } from "@openloaf-saas/sdk";
465
+
466
+ contract.ai.image.method; // "POST"
467
+ contract.ai.image.path; // "/api/ai/image"
468
+
469
+ contract.aiTools.summarize.method; // "POST"
470
+ contract.aiTools.summarize.path; // "/api/ai/tools/summarize"
471
+ ```
472
+
473
+ ---
474
+
475
+ ## 当前支持的接口
476
+
477
+ ### REST — 认证
478
+ - `POST /api/auth/exchange` — 登录码换取令牌
479
+ - `POST /api/auth/refresh` — 刷新令牌
480
+ - `POST /api/auth/logout` — 退出登录
481
+
482
+ ### REST — 用户
483
+ - `GET /api/user/self` — 获取当前用户信息(含积分余额)
484
+
485
+ ### REST — 反馈
486
+ - `POST /api/public/feedback` — 提交反馈(可匿名)
487
+ - `POST /api/feedback/list` — 反馈列表
488
+ - `GET /api/feedback/{feedbackId}` — 反馈详情
489
+ - `POST /api/feedback/upload` — 上传反馈附件 (multipart/form-data)
490
+
491
+ ### REST — AI 媒体生成(返回 `creditsConsumed`)
492
+ - `POST /api/ai/image` — 提交图片生成任务
493
+ - `POST /api/ai/video` — 提交视频生成任务
494
+ - `GET /api/ai/task/{taskId}` — 查询任务状态(成功时含 `creditsConsumed`)
495
+ - `POST /api/ai/task/{taskId}/cancel` — 取消任务
496
+ - `POST /api/ai/file/upload` — 上传 AI 对话附件 (multipart/form-data)
497
+
498
+ ### REST — AI Chat(OpenAI 兼容,返回 `x_credits_consumed`)
499
+ - `POST /api/v1/chat/completions` — 聊天补全(流式/非流式)
500
+ - `POST /api/v1/responses` — 统一响应(流式/非流式)
501
+
502
+ ### REST — AI 模型(公开,无需认证)
503
+ - `GET /api/public/ai/image/models` — 图片模型列表
504
+ - `GET /api/public/ai/video/models` — 视频模型列表
505
+ - `GET /api/public/ai/chat/models` — 聊天模型列表
506
+ - `GET /api/public/ai/models/updated-at` — 模型更新时间
507
+ - `GET /api/public/ai/providers` — 供应商模板
508
+
509
+ ### REST — AI 辅助工具(不扣积分)
510
+ - `POST /api/ai/tools/analyze-skills` — 技能分析
511
+ - `POST /api/ai/tools/recommend-actions` — 行为推荐
512
+ - `POST /api/ai/tools/summarize` — 文本总结
513
+
514
+ ### REST — 辅助推理(不扣积分)
515
+ - `POST /api/auxiliary/infer` — 通用推理
516
+ - `POST /api/auxiliary/quota` — 查询日配额
517
+
518
+ ### Public tRPC
519
+ - `healthCheck`(publicRouter)
520
+ - `feedback.submit`(publicRouter)