@tbox-dev-js/sdk 0.1.0

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.
@@ -0,0 +1,2951 @@
1
+ /**
2
+ * @tbox/sdk — 常量定义
3
+ */
4
+ /** 默认 API 基础 URL */
5
+ const TBOX_BASE_URL = 'https://o.tbox.cn';
6
+ /** SDK 版本号 */
7
+ const SDK_VERSION = '1.0.0';
8
+ /** SDK 版本标识 header key */
9
+ const SDK_VERSION_HEADER = 'X-Tbox-SDK-Version';
10
+
11
+ /**
12
+ * @tbox/sdk — 分层错误体系
13
+ *
14
+ * 对齐 coze-js 的 error.ts 设计:
15
+ * TboxError → APIError → 按 HTTP 状态码的子类
16
+ */
17
+ /** 所有 SDK 错误的基类 */
18
+ class TboxError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = 'TboxError';
22
+ Object.setPrototypeOf(this, new.target.prototype);
23
+ }
24
+ }
25
+ /** API 调用错误,包含 HTTP 状态码和业务错误信息 */
26
+ class APIError extends TboxError {
27
+ constructor(status, errorCode, errorMsg, traceId, rawResponse) {
28
+ const message = errorMsg ?? `API error (status ${status})`;
29
+ super(message);
30
+ this.name = 'APIError';
31
+ this.status = status;
32
+ this.errorCode = errorCode;
33
+ this.errorMsg = errorMsg;
34
+ this.traceId = traceId;
35
+ this.rawResponse = rawResponse;
36
+ }
37
+ /**
38
+ * Factory: create the appropriate error subclass based on HTTP status code.
39
+ */
40
+ static generate(status, body) {
41
+ const errorCode = body?.errorCode ?? (body?.code != null ? String(body.code) : undefined) ?? body?.resultCode;
42
+ const errorMsg = body?.errorMsg ?? body?.msg ?? body?.resultDesc;
43
+ const traceId = body?.traceId;
44
+ switch (status) {
45
+ case 400:
46
+ return new BadRequestError(errorCode, errorMsg, traceId, body);
47
+ case 401:
48
+ return new AuthenticationError(errorCode, errorMsg, traceId, body);
49
+ case 403:
50
+ return new ForbiddenError(errorCode, errorMsg, traceId, body);
51
+ case 404:
52
+ return new NotFoundError(errorCode, errorMsg, traceId, body);
53
+ case 429:
54
+ return new RateLimitError(errorCode, errorMsg, traceId, body);
55
+ case 500:
56
+ return new InternalServerError(errorCode, errorMsg, traceId, body);
57
+ case 502:
58
+ return new GatewayError(errorCode, errorMsg, traceId, body);
59
+ default:
60
+ return new APIError(status, errorCode, errorMsg, traceId, body);
61
+ }
62
+ }
63
+ }
64
+ class BadRequestError extends APIError {
65
+ constructor(errorCode, errorMsg, traceId, rawResponse) {
66
+ super(400, errorCode, errorMsg, traceId, rawResponse);
67
+ this.name = 'BadRequestError';
68
+ }
69
+ }
70
+ class AuthenticationError extends APIError {
71
+ constructor(errorCode, errorMsg, traceId, rawResponse) {
72
+ super(401, errorCode, errorMsg, traceId, rawResponse);
73
+ this.name = 'AuthenticationError';
74
+ }
75
+ }
76
+ class ForbiddenError extends APIError {
77
+ constructor(errorCode, errorMsg, traceId, rawResponse) {
78
+ super(403, errorCode, errorMsg, traceId, rawResponse);
79
+ this.name = 'ForbiddenError';
80
+ }
81
+ }
82
+ class NotFoundError extends APIError {
83
+ constructor(errorCode, errorMsg, traceId, rawResponse) {
84
+ super(404, errorCode, errorMsg, traceId, rawResponse);
85
+ this.name = 'NotFoundError';
86
+ }
87
+ }
88
+ class RateLimitError extends APIError {
89
+ constructor(errorCode, errorMsg, traceId, rawResponse) {
90
+ super(429, errorCode, errorMsg, traceId, rawResponse);
91
+ this.name = 'RateLimitError';
92
+ }
93
+ }
94
+ class InternalServerError extends APIError {
95
+ constructor(errorCode, errorMsg, traceId, rawResponse) {
96
+ super(500, errorCode, errorMsg, traceId, rawResponse);
97
+ this.name = 'InternalServerError';
98
+ }
99
+ }
100
+ class GatewayError extends APIError {
101
+ constructor(errorCode, errorMsg, traceId, rawResponse) {
102
+ super(502, errorCode, errorMsg, traceId, rawResponse);
103
+ this.name = 'GatewayError';
104
+ }
105
+ }
106
+ /** Network unreachable error */
107
+ class NetworkError extends TboxError {
108
+ constructor(message, cause) {
109
+ super(message);
110
+ this.name = 'NetworkError';
111
+ this.cause = cause;
112
+ }
113
+ }
114
+ /** Request timeout error */
115
+ class TimeoutError extends TboxError {
116
+ constructor(timeout) {
117
+ super(`Request timed out after ${timeout}ms`);
118
+ this.name = 'TimeoutError';
119
+ this.timeout = timeout;
120
+ }
121
+ }
122
+ /** JSON parse error */
123
+ class ParseError extends TboxError {
124
+ constructor(message, responseText) {
125
+ super(message);
126
+ this.name = 'ParseError';
127
+ this.responseText = responseText;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * @tbox/sdk — APIClient 核心基类
133
+ *
134
+ * 对齐 coze-js 的 core.ts 设计:
135
+ * - Token 管理(静态字符串 / 动态函数)
136
+ * - 请求构建(JSON / FormData / SSE)
137
+ * - 响应解包(兼容多种服务端格式)
138
+ * - 错误映射(HTTP 状态码 → 错误子类)
139
+ */
140
+ /**
141
+ * Check if a result field is a knowledge-style nested wrapper:
142
+ * { response: {...}, success: boolean, errorType?: string }
143
+ */
144
+ function isKnowledgeWrapper(value) {
145
+ return (typeof value === 'object' &&
146
+ value !== null &&
147
+ 'response' in value &&
148
+ 'success' in value);
149
+ }
150
+ /**
151
+ * Core request engine. All Resource classes call methods on this via `this._client`.
152
+ */
153
+ class APIClient {
154
+ constructor(options) {
155
+ this.baseURL = (options.baseURL ?? TBOX_BASE_URL).replace(/\/$/, '');
156
+ this.token = options.token;
157
+ this.timeout = options.timeout;
158
+ this.debug = options.debug ?? false;
159
+ this.defaultHeaders = options.headers;
160
+ }
161
+ /** Resolve the current token value (supports async token functions). */
162
+ async getToken() {
163
+ if (typeof this.token === 'function') {
164
+ return this.token();
165
+ }
166
+ return this.token;
167
+ }
168
+ /** JSON POST request — returns unwrapped business data. */
169
+ async post(path, body, options) {
170
+ const url = this.buildUrl(path, options);
171
+ const headers = await this.buildHeaders(options, 'application/json;charset=UTF-8');
172
+ this.debugLog(options, `POST ${url}`, body);
173
+ const response = await this.fetchWithTimeout(url, {
174
+ method: 'POST',
175
+ headers,
176
+ body: body != null ? JSON.stringify(body) : undefined,
177
+ signal: options?.signal,
178
+ }, options);
179
+ return this.parseAndUnwrap(response, options);
180
+ }
181
+ /** GET request — query params auto-appended to URL. */
182
+ async get(path, params, options) {
183
+ let url = this.buildUrl(path, options);
184
+ if (params) {
185
+ const searchParams = new URLSearchParams();
186
+ for (const [key, value] of Object.entries(params)) {
187
+ if (value !== undefined) {
188
+ searchParams.append(key, String(value));
189
+ }
190
+ }
191
+ const queryString = searchParams.toString();
192
+ if (queryString) {
193
+ url += `?${queryString}`;
194
+ }
195
+ }
196
+ const headers = await this.buildHeaders(options);
197
+ this.debugLog(options, `GET ${url}`);
198
+ const response = await this.fetchWithTimeout(url, {
199
+ method: 'GET',
200
+ headers,
201
+ signal: options?.signal,
202
+ }, options);
203
+ return this.parseAndUnwrap(response, options);
204
+ }
205
+ /** FormData POST request (file uploads). Content-Type is set automatically by fetch. */
206
+ async postFormData(path, formData, options) {
207
+ const url = this.buildUrl(path, options);
208
+ const headers = await this.buildHeaders(options);
209
+ // Do NOT set Content-Type — fetch/browser will set multipart/form-data with boundary
210
+ delete headers['Content-Type'];
211
+ this.debugLog(options, `POST (FormData) ${url}`);
212
+ const response = await this.fetchWithTimeout(url, {
213
+ method: 'POST',
214
+ headers,
215
+ body: formData,
216
+ signal: options?.signal,
217
+ }, options);
218
+ return this.parseAndUnwrap(response, options);
219
+ }
220
+ /** SSE streaming POST request — yields SSEEvent objects. */
221
+ async *postStream(path, body, options) {
222
+ const url = this.buildUrl(path, options);
223
+ const headers = await this.buildHeaders(options, 'application/json;charset=UTF-8');
224
+ this.debugLog(options, `POST (stream) ${url}`, body);
225
+ const response = await this.fetchWithTimeout(url, {
226
+ method: 'POST',
227
+ headers,
228
+ body: body != null ? JSON.stringify(body) : undefined,
229
+ signal: options?.signal,
230
+ }, options);
231
+ if (!response.ok) {
232
+ const raw = await this.safeParseJson(response);
233
+ throw APIError.generate(response.status, raw ?? undefined);
234
+ }
235
+ if (!response.body) {
236
+ throw new ParseError('Response body is null — streaming not supported');
237
+ }
238
+ yield* this.parseSSEStream(response);
239
+ }
240
+ // ================================================================
241
+ // Internal helpers
242
+ // ================================================================
243
+ buildUrl(path, options) {
244
+ const base = options?.baseURL?.replace(/\/$/, '') ?? this.baseURL;
245
+ return `${base}${path}`;
246
+ }
247
+ async buildHeaders(options, contentType) {
248
+ const token = await this.getToken();
249
+ const headers = {
250
+ Authorization: token,
251
+ [SDK_VERSION_HEADER]: SDK_VERSION,
252
+ ...this.defaultHeaders,
253
+ ...options?.headers,
254
+ };
255
+ if (contentType) {
256
+ headers['Content-Type'] = contentType;
257
+ }
258
+ return headers;
259
+ }
260
+ async fetchWithTimeout(url, init, options) {
261
+ const timeoutMs = options?.timeout ?? this.timeout;
262
+ if (!timeoutMs) {
263
+ try {
264
+ return await fetch(url, init);
265
+ }
266
+ catch (error) {
267
+ throw new NetworkError('Network request failed', error instanceof Error ? error : undefined);
268
+ }
269
+ }
270
+ const controller = new AbortController();
271
+ const existingSignal = init.signal;
272
+ // Merge external signal with timeout signal
273
+ if (existingSignal) {
274
+ existingSignal.addEventListener('abort', () => controller.abort(existingSignal.reason));
275
+ }
276
+ const timer = setTimeout(() => controller.abort('timeout'), timeoutMs);
277
+ try {
278
+ return await fetch(url, { ...init, signal: controller.signal });
279
+ }
280
+ catch (error) {
281
+ if (controller.signal.aborted && controller.signal.reason === 'timeout') {
282
+ throw new TimeoutError(timeoutMs);
283
+ }
284
+ throw new NetworkError('Network request failed', error instanceof Error ? error : undefined);
285
+ }
286
+ finally {
287
+ clearTimeout(timer);
288
+ }
289
+ }
290
+ async parseAndUnwrap(response, options) {
291
+ if (!response.ok) {
292
+ const raw = await this.safeParseJson(response);
293
+ throw APIError.generate(response.status, raw ?? undefined);
294
+ }
295
+ const raw = await this.parseJsonResponse(response);
296
+ this.debugLog(options, 'Response:', raw);
297
+ return this.unwrapResponse(raw, response.status);
298
+ }
299
+ /**
300
+ * Unwrap server response — extract business data from various envelope formats.
301
+ *
302
+ * Priority: data > resultObj > result.response (knowledge) > result > raw
303
+ */
304
+ unwrapResponse(raw, httpStatus) {
305
+ // Check outer success flag
306
+ if (raw.success === false) {
307
+ throw APIError.generate(httpStatus, raw);
308
+ }
309
+ // Priority 1: data field
310
+ if (raw.data !== undefined) {
311
+ return raw.data;
312
+ }
313
+ // Priority 2: resultObj field
314
+ if (raw.resultObj !== undefined) {
315
+ return raw.resultObj;
316
+ }
317
+ // Priority 3: result field (may be knowledge wrapper)
318
+ if (raw.result !== undefined) {
319
+ if (isKnowledgeWrapper(raw.result)) {
320
+ if (raw.result.success === false) {
321
+ throw APIError.generate(httpStatus, {
322
+ ...raw,
323
+ errorMsg: raw.result.errorType ?? raw.errorMsg,
324
+ });
325
+ }
326
+ return raw.result.response;
327
+ }
328
+ return raw.result;
329
+ }
330
+ // Fallback: return the entire raw response if it has meaningful fields,
331
+ // otherwise throw a ParseError to avoid silently returning an unexpected shape.
332
+ if (raw.success !== undefined || raw.code !== undefined || raw.resultCode !== undefined) {
333
+ return raw;
334
+ }
335
+ throw new ParseError('Unexpected response format — unable to extract business data', JSON.stringify(raw));
336
+ }
337
+ /** Parse JSON response using ArrayBuffer + TextDecoder for correct UTF-8 handling. */
338
+ async parseJsonResponse(response) {
339
+ let text;
340
+ try {
341
+ const buffer = await response.arrayBuffer();
342
+ text = new TextDecoder('utf-8').decode(buffer);
343
+ }
344
+ catch {
345
+ throw new ParseError('Failed to read response body');
346
+ }
347
+ try {
348
+ return JSON.parse(text);
349
+ }
350
+ catch {
351
+ throw new ParseError('Failed to parse response JSON', text);
352
+ }
353
+ }
354
+ /** Safely parse JSON without throwing (for error responses). */
355
+ async safeParseJson(response) {
356
+ try {
357
+ const buffer = await response.arrayBuffer();
358
+ const text = new TextDecoder('utf-8').decode(buffer);
359
+ return JSON.parse(text);
360
+ }
361
+ catch {
362
+ return null;
363
+ }
364
+ }
365
+ /** Parse SSE stream from ReadableStream. */
366
+ async *parseSSEStream(response) {
367
+ const reader = response.body.getReader();
368
+ const decoder = new TextDecoder();
369
+ let buffer = '';
370
+ try {
371
+ while (true) {
372
+ const { done, value } = await reader.read();
373
+ if (done)
374
+ break;
375
+ buffer += decoder.decode(value, { stream: true });
376
+ const lines = buffer.split('\n');
377
+ buffer = lines.pop() ?? '';
378
+ for (const line of lines) {
379
+ const trimmed = line.trim();
380
+ if (trimmed.startsWith('data: ')) {
381
+ const data = trimmed.slice(6).trim();
382
+ if (data === '[DONE]')
383
+ return;
384
+ yield { data };
385
+ }
386
+ }
387
+ }
388
+ }
389
+ finally {
390
+ reader.releaseLock();
391
+ }
392
+ }
393
+ /** Output debug log when debug mode is enabled. */
394
+ debugLog(options, ...messages) {
395
+ const isDebug = options?.debug ?? this.debug;
396
+ if (isDebug) {
397
+ console.log('[TboxSDK]', ...messages);
398
+ }
399
+ }
400
+ }
401
+
402
+ /**
403
+ * @tbox/sdk — APIResource 基类
404
+ *
405
+ * 所有 Resource 子类(Knowledge、Plugins 等)继承此类,
406
+ * 通过 this._client 引用 APIClient 发起请求。
407
+ */
408
+ class APIResource {
409
+ constructor(client) {
410
+ this._client = client;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * @tbox/sdk — Knowledge Documents sub-resource
416
+ *
417
+ * Handles knowledge base creation and update (multipart/form-data uploads).
418
+ */
419
+ /** Build FormData for knowledge base creation. */
420
+ function buildCreateFormData(params) {
421
+ const formData = new FormData();
422
+ formData.append('file', params.file);
423
+ formData.append('type', params.type);
424
+ if (params.tableSchema) {
425
+ formData.append('tableSchema', JSON.stringify(params.tableSchema));
426
+ }
427
+ if (params.indexColumns) {
428
+ for (const col of params.indexColumns) {
429
+ formData.append('indexColumns', col);
430
+ }
431
+ }
432
+ return formData;
433
+ }
434
+ /** Build FormData for knowledge base update. */
435
+ function buildUpdateFormData(params) {
436
+ const formData = new FormData();
437
+ formData.append('documentId', params.documentId);
438
+ formData.append('file', params.file);
439
+ formData.append('type', params.type);
440
+ if (params.updateMode) {
441
+ formData.append('updateMode', params.updateMode);
442
+ }
443
+ if (params.tableSchema) {
444
+ formData.append('tableSchema', JSON.stringify(params.tableSchema));
445
+ }
446
+ if (params.indexColumns) {
447
+ for (const col of params.indexColumns) {
448
+ formData.append('indexColumns', col);
449
+ }
450
+ }
451
+ return formData;
452
+ }
453
+ class Documents extends APIResource {
454
+ /**
455
+ * Create a knowledge base by uploading a file.
456
+ *
457
+ * POST /openapi/v1/knowledge/createKnowledgeBaseByDataSet
458
+ * Content-Type: multipart/form-data
459
+ */
460
+ async create(params, options) {
461
+ const formData = buildCreateFormData(params);
462
+ return this._client.postFormData('/openapi/v1/knowledge/createKnowledgeBaseByDataSet', formData, options);
463
+ }
464
+ /**
465
+ * Update an existing knowledge base document.
466
+ *
467
+ * POST /openapi/v1/knowledge/updateDocument
468
+ * Content-Type: multipart/form-data
469
+ */
470
+ async update(params, options) {
471
+ const formData = buildUpdateFormData(params);
472
+ return this._client.postFormData('/openapi/v1/knowledge/updateDocument', formData, options);
473
+ }
474
+ }
475
+
476
+ /**
477
+ * @tbox/sdk — Knowledge Resource
478
+ *
479
+ * Provides knowledge base list, semantic retrieval, and document management.
480
+ */
481
+ class Knowledge extends APIResource {
482
+ constructor(client) {
483
+ super(client);
484
+ this.documents = new Documents(client);
485
+ }
486
+ /**
487
+ * Query knowledge base list.
488
+ *
489
+ * GET /api/datasets/queryDatasetsList
490
+ */
491
+ async list(params, options) {
492
+ return this._client.get('/api/datasets/queryDatasetsList', params, options);
493
+ }
494
+ /**
495
+ * Semantic retrieval against a knowledge base.
496
+ *
497
+ * POST /api/datasets/retrieve
498
+ */
499
+ async retrieve(params, options) {
500
+ return this._client.post('/api/datasets/retrieve', params, options);
501
+ }
502
+ }
503
+
504
+ /**
505
+ * @tbox/sdk — ResourceSchema 类型体系
506
+ *
507
+ * 用 JSON Schema 风格统一描述所有 API 资源的入参、出参和元信息,
508
+ * 端侧可据此动态渲染表单、校验参数、生成配置。
509
+ *
510
+ * 设计原则:
511
+ * - 对齐 JSON Schema Draft 7 的 property 描述格式
512
+ * - 每个 API 操作(Operation)独立描述入参 + 出参 schema
513
+ * - 所有资源通过 ResourceDefinition 统一注册
514
+ * - 支持嵌套子资源(如 knowledge.documents)
515
+ */
516
+ // ================================================================
517
+ // Schema Registry — 统一注册和查询
518
+ // ================================================================
519
+ /** Registry that holds all resource definitions */
520
+ class SchemaRegistry {
521
+ constructor() {
522
+ this.resources = new Map();
523
+ }
524
+ /** Register a resource definition */
525
+ register(definition) {
526
+ this.resources.set(definition.name, definition);
527
+ }
528
+ /** Get a resource definition by name */
529
+ get(name) {
530
+ return this.resources.get(name);
531
+ }
532
+ /** Get all registered resource definitions */
533
+ list() {
534
+ return Array.from(this.resources.values());
535
+ }
536
+ /** Get a specific operation by resource name and operation ID */
537
+ getOperation(resourceName, operationId) {
538
+ const resource = this.resources.get(resourceName);
539
+ if (!resource)
540
+ return undefined;
541
+ const operation = resource.operations.find(op => op.operationId === operationId);
542
+ if (operation)
543
+ return operation;
544
+ // Search in sub-resources
545
+ for (const subResource of resource.subResources ?? []) {
546
+ const subOperation = subResource.operations.find(op => op.operationId === operationId);
547
+ if (subOperation)
548
+ return subOperation;
549
+ }
550
+ return undefined;
551
+ }
552
+ /** Export all schemas as a plain JSON-serializable object */
553
+ toJSON() {
554
+ const result = {};
555
+ for (const [name, definition] of this.resources) {
556
+ result[name] = definition;
557
+ }
558
+ return result;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * @tbox/sdk — Knowledge 模块 ResourceSchema 定义
564
+ *
565
+ * 用 JSON Schema 风格描述知识库的 4 个核心 API 操作,
566
+ * 端侧可据此动态渲染表单、校验参数、生成配置。
567
+ */
568
+ /** Knowledge documents sub-resource schema */
569
+ const documentsResource = {
570
+ name: 'documents',
571
+ displayName: '知识库文档',
572
+ description: '知识库文档的创建和更新(文件上传)',
573
+ operations: [
574
+ {
575
+ operationId: 'create',
576
+ summary: '创建知识库',
577
+ description: '上传文件创建新的知识库,支持结构化(STRUCTURED)和非结构化(UNSTRUCTURED)两种类型',
578
+ method: 'POST',
579
+ path: '/openapi/v1/knowledge/createKnowledgeBaseByDataSet',
580
+ contentType: 'multipart/form-data',
581
+ params: {
582
+ type: 'object',
583
+ properties: {
584
+ file: {
585
+ type: 'file',
586
+ description: '上传的文件(如 .xlsx、.csv、.pdf 等)',
587
+ required: true,
588
+ format: 'file-upload',
589
+ },
590
+ type: {
591
+ type: 'string',
592
+ description: '知识库类型',
593
+ required: true,
594
+ enum: ['STRUCTURED', 'UNSTRUCTURED'],
595
+ default: 'STRUCTURED',
596
+ format: 'select',
597
+ },
598
+ tableSchema: {
599
+ type: 'object',
600
+ description: '表结构定义(STRUCTURED 类型必填),JSON 格式',
601
+ required: false,
602
+ format: 'json-editor',
603
+ properties: {
604
+ name: {
605
+ type: 'string',
606
+ description: '表名',
607
+ required: true,
608
+ },
609
+ description: {
610
+ type: 'string',
611
+ description: '表描述',
612
+ required: false,
613
+ },
614
+ columns: {
615
+ type: 'array',
616
+ description: '列定义列表',
617
+ required: true,
618
+ items: {
619
+ type: 'object',
620
+ properties: {
621
+ name: {
622
+ type: 'string',
623
+ description: '列名',
624
+ required: true,
625
+ },
626
+ dataType: {
627
+ type: 'string',
628
+ description: '数据类型',
629
+ enum: ['STRING', 'INTEGER', 'FLOAT', 'BOOLEAN', 'DATE'],
630
+ default: 'STRING',
631
+ },
632
+ primaryKey: {
633
+ type: 'boolean',
634
+ description: '是否主键',
635
+ default: false,
636
+ },
637
+ required: {
638
+ type: 'boolean',
639
+ description: '是否必填',
640
+ default: false,
641
+ },
642
+ description: {
643
+ type: 'string',
644
+ description: '列描述',
645
+ },
646
+ defaultValue: {
647
+ type: 'string',
648
+ description: '默认值',
649
+ },
650
+ order: {
651
+ type: 'integer',
652
+ description: '排序序号',
653
+ minimum: 0,
654
+ },
655
+ },
656
+ },
657
+ },
658
+ },
659
+ },
660
+ indexColumns: {
661
+ type: 'array',
662
+ description: '索引列名列表',
663
+ required: false,
664
+ items: {
665
+ type: 'string',
666
+ description: '列名',
667
+ },
668
+ },
669
+ },
670
+ required: ['file', 'type'],
671
+ },
672
+ response: {
673
+ type: 'object',
674
+ properties: {
675
+ documentId: {
676
+ type: 'string',
677
+ description: '创建后的文档 ID,用于后续更新操作',
678
+ },
679
+ },
680
+ required: ['documentId'],
681
+ },
682
+ tags: ['knowledge', 'write'],
683
+ },
684
+ {
685
+ operationId: 'update',
686
+ summary: '更新知识库文档',
687
+ description: '上传新文件更新已有的知识库文档,支持覆盖(OVERWRITE)和增量(UPSERT)两种模式',
688
+ method: 'POST',
689
+ path: '/openapi/v1/knowledge/updateDocument',
690
+ contentType: 'multipart/form-data',
691
+ params: {
692
+ type: 'object',
693
+ properties: {
694
+ documentId: {
695
+ type: 'string',
696
+ description: '要更新的文档 ID(从创建响应中获取)',
697
+ required: true,
698
+ },
699
+ file: {
700
+ type: 'file',
701
+ description: '上传的新文件',
702
+ required: true,
703
+ format: 'file-upload',
704
+ },
705
+ type: {
706
+ type: 'string',
707
+ description: '知识库类型',
708
+ required: true,
709
+ enum: ['STRUCTURED', 'UNSTRUCTURED'],
710
+ format: 'select',
711
+ },
712
+ updateMode: {
713
+ type: 'string',
714
+ description: '更新模式',
715
+ required: false,
716
+ enum: ['OVERWRITE', 'UPSERT'],
717
+ default: 'UPSERT',
718
+ format: 'select',
719
+ },
720
+ tableSchema: {
721
+ type: 'object',
722
+ description: '表结构定义(同创建接口的 tableSchema)',
723
+ required: false,
724
+ format: 'json-editor',
725
+ properties: {
726
+ name: { type: 'string', description: '表名', required: true },
727
+ description: { type: 'string', description: '表描述' },
728
+ columns: {
729
+ type: 'array',
730
+ description: '列定义列表',
731
+ required: true,
732
+ items: {
733
+ type: 'object',
734
+ properties: {
735
+ name: { type: 'string', description: '列名', required: true },
736
+ dataType: { type: 'string', description: '数据类型', enum: ['STRING', 'INTEGER', 'FLOAT', 'BOOLEAN', 'DATE'] },
737
+ primaryKey: { type: 'boolean', description: '是否主键' },
738
+ required: { type: 'boolean', description: '是否必填' },
739
+ description: { type: 'string', description: '列描述' },
740
+ defaultValue: { type: 'string', description: '默认值' },
741
+ order: { type: 'integer', description: '排序序号', minimum: 0 },
742
+ },
743
+ },
744
+ },
745
+ },
746
+ },
747
+ indexColumns: {
748
+ type: 'array',
749
+ description: '索引列名列表',
750
+ required: false,
751
+ items: { type: 'string', description: '列名' },
752
+ },
753
+ },
754
+ required: ['documentId', 'file', 'type'],
755
+ },
756
+ response: {
757
+ type: 'object',
758
+ properties: {
759
+ documentId: {
760
+ type: 'string',
761
+ description: '更新后的文档 ID',
762
+ },
763
+ },
764
+ required: ['documentId'],
765
+ },
766
+ tags: ['knowledge', 'write'],
767
+ },
768
+ ],
769
+ };
770
+ /** Knowledge resource schema — the complete definition */
771
+ const knowledgeSchema = {
772
+ name: 'knowledge',
773
+ displayName: '知识库',
774
+ description: '知识库管理,包括列表查询、语义检索、文档创建和更新',
775
+ operations: [
776
+ {
777
+ operationId: 'list',
778
+ summary: '查询知识库列表',
779
+ description: '分页查询当前账号下的知识库列表,支持按名称模糊搜索',
780
+ method: 'GET',
781
+ path: '/api/datasets/queryDatasetsList',
782
+ contentType: 'none',
783
+ params: {
784
+ type: 'object',
785
+ properties: {
786
+ name: {
787
+ type: 'string',
788
+ description: '知识库名称(模糊搜索)',
789
+ required: false,
790
+ },
791
+ pageNo: {
792
+ type: 'integer',
793
+ description: '页码,从 1 开始',
794
+ required: false,
795
+ default: 1,
796
+ minimum: 1,
797
+ },
798
+ pageSize: {
799
+ type: 'integer',
800
+ description: '每页条数',
801
+ required: false,
802
+ default: 10,
803
+ minimum: 1,
804
+ maximum: 100,
805
+ },
806
+ },
807
+ },
808
+ response: {
809
+ type: 'object',
810
+ properties: {
811
+ data: {
812
+ type: 'array',
813
+ description: '知识库列表',
814
+ items: {
815
+ type: 'object',
816
+ properties: {
817
+ datasetId: { type: 'string', description: '知识库 ID' },
818
+ name: { type: 'string', description: '知识库名称' },
819
+ description: { type: 'string', description: '知识库描述' },
820
+ type: { type: 'string', description: '知识库类型', enum: ['STRUCTURED', 'UNSTRUCTURED'] },
821
+ documentCount: { type: 'integer', description: '文档数量' },
822
+ storageSize: { type: 'number', description: '存储大小' },
823
+ createdAt: { type: 'string', description: '创建时间' },
824
+ updatedAt: { type: 'string', description: '更新时间' },
825
+ },
826
+ },
827
+ },
828
+ totalCount: {
829
+ type: 'integer',
830
+ description: '总条数',
831
+ },
832
+ },
833
+ required: ['data', 'totalCount'],
834
+ },
835
+ tags: ['knowledge', 'read'],
836
+ },
837
+ {
838
+ operationId: 'retrieve',
839
+ summary: '语义检索',
840
+ description: '对指定知识库进行语义检索,返回与查询文本最相关的文档片段',
841
+ method: 'POST',
842
+ path: '/api/datasets/retrieve',
843
+ contentType: 'application/json',
844
+ params: {
845
+ type: 'object',
846
+ properties: {
847
+ datasetId: {
848
+ type: 'string',
849
+ description: '知识库 ID',
850
+ required: true,
851
+ },
852
+ query: {
853
+ type: 'string',
854
+ description: '检索查询文本',
855
+ required: true,
856
+ format: 'textarea',
857
+ },
858
+ topK: {
859
+ type: 'integer',
860
+ description: '返回结果数量上限',
861
+ required: false,
862
+ default: 5,
863
+ minimum: 1,
864
+ maximum: 50,
865
+ },
866
+ scoreThreshold: {
867
+ type: 'number',
868
+ description: '最低相关度分数阈值 (0~1)',
869
+ required: false,
870
+ minimum: 0,
871
+ maximum: 1,
872
+ example: 0.5,
873
+ },
874
+ },
875
+ required: ['datasetId', 'query'],
876
+ },
877
+ response: {
878
+ type: 'object',
879
+ properties: {
880
+ data: {
881
+ type: 'array',
882
+ description: '检索结果列表',
883
+ items: {
884
+ type: 'object',
885
+ properties: {
886
+ content: { type: 'string', description: '文档片段内容' },
887
+ score: { type: 'number', description: '相关度分数 (0~1)' },
888
+ originFileName: { type: 'string', description: '来源文件名' },
889
+ documentId: { type: 'string', description: '来源文档 ID' },
890
+ metadata: { type: 'object', description: '元数据', properties: {} },
891
+ },
892
+ },
893
+ },
894
+ },
895
+ required: ['data'],
896
+ },
897
+ tags: ['knowledge', 'read'],
898
+ },
899
+ ],
900
+ subResources: [documentsResource],
901
+ };
902
+
903
+ /**
904
+ * @tbox/sdk — TboxAPI 入口类
905
+ *
906
+ * SDK 的唯一入口,继承 APIClient,通过属性访问子模块。
907
+ * 内置 SchemaRegistry,自动注册所有资源的 JSON Schema 定义。
908
+ */
909
+ class TboxAPI extends APIClient {
910
+ constructor(options) {
911
+ super(options);
912
+ this.knowledge = new Knowledge(this);
913
+ // Initialize schema registry with all resource schemas
914
+ this.schemas = new SchemaRegistry();
915
+ this.schemas.register(knowledgeSchema);
916
+ }
917
+ }
918
+
919
+ /**
920
+ * @tbox/sdk — KnowledgeClient
921
+ *
922
+ * Configuration-driven, multi-knowledge-base client.
923
+ * Reuses the unified ClientOptions (token/baseURL/timeout) from core.ts.
924
+ *
925
+ * Supports multiple named knowledge bases via knowledges map in knowledge.json:
926
+ * client.use("产品数据").retrieve("查询文本")
927
+ * client.retrieve("产品数据", "查询文本")
928
+ */
929
+ /** Scoped handle returned by client.use(name), bound to a specific knowledge base */
930
+ class KnowledgeScopedClient {
931
+ constructor(tbox, globalConfig, instanceConfig, allowedTools) {
932
+ this.tbox = tbox;
933
+ this.instanceConfig = instanceConfig;
934
+ this.allowedTools = allowedTools;
935
+ const local = instanceConfig.config;
936
+ this.effectiveConfig = {
937
+ type: local?.type ?? globalConfig.type,
938
+ topK: local?.topK ?? globalConfig.topK,
939
+ scoreThreshold: local?.scoreThreshold ?? globalConfig.scoreThreshold,
940
+ updateMode: local?.updateMode ?? globalConfig.updateMode,
941
+ pageSize: local?.pageSize ?? globalConfig.pageSize,
942
+ };
943
+ this.currentDocumentId = instanceConfig.documentId;
944
+ }
945
+ /** Get the effective merged config for this knowledge base */
946
+ getConfig() {
947
+ return { ...this.effectiveConfig, ...this.instanceConfig, documentId: this.currentDocumentId };
948
+ }
949
+ /** Get the current documentId (may be auto-filled after create()) */
950
+ getDocumentId() {
951
+ return this.currentDocumentId;
952
+ }
953
+ /** Semantic retrieval against this knowledge base */
954
+ async retrieve(query, options) {
955
+ this.assertToolAllowed('retrieve');
956
+ const datasetId = this.instanceConfig.datasetId;
957
+ if (!datasetId) {
958
+ throw new Error('datasetId is not configured for this knowledge base.');
959
+ }
960
+ const params = {
961
+ datasetId,
962
+ query,
963
+ topK: options?.topK ?? this.effectiveConfig.topK,
964
+ scoreThreshold: options?.scoreThreshold ?? this.effectiveConfig.scoreThreshold,
965
+ };
966
+ return this.tbox.knowledge.retrieve(params);
967
+ }
968
+ /**
969
+ * Create this knowledge base by uploading a file.
970
+ *
971
+ * Side effect: on success, the returned documentId is cached in this scoped client
972
+ * so that subsequent update() calls can use it automatically.
973
+ * The original instanceConfig is NOT mutated.
974
+ */
975
+ async create(file, options) {
976
+ this.assertToolAllowed('create');
977
+ const params = {
978
+ file,
979
+ type: options?.type ?? this.effectiveConfig.type ?? 'STRUCTURED',
980
+ tableSchema: options?.tableSchema ?? this.instanceConfig.tableSchema,
981
+ indexColumns: options?.indexColumns ?? this.instanceConfig.indexColumns,
982
+ };
983
+ const result = await this.tbox.knowledge.documents.create(params);
984
+ if (result.documentId) {
985
+ this.currentDocumentId = result.documentId;
986
+ }
987
+ return result;
988
+ }
989
+ /** Update this knowledge base document */
990
+ async update(file, options) {
991
+ this.assertToolAllowed('update');
992
+ const documentId = this.currentDocumentId;
993
+ if (!documentId) {
994
+ throw new Error('documentId is not configured. Call create() first or set it in knowledge.json.');
995
+ }
996
+ const params = {
997
+ documentId,
998
+ file,
999
+ type: this.effectiveConfig.type ?? 'STRUCTURED',
1000
+ updateMode: options?.updateMode ?? this.effectiveConfig.updateMode ?? 'UPSERT',
1001
+ tableSchema: options?.tableSchema ?? this.instanceConfig.tableSchema,
1002
+ indexColumns: options?.indexColumns ?? this.instanceConfig.indexColumns,
1003
+ };
1004
+ return this.tbox.knowledge.documents.update(params);
1005
+ }
1006
+ /** Throw if the tool is not in the allowed set (when tools are configured) */
1007
+ assertToolAllowed(tool) {
1008
+ if (this.allowedTools && !this.allowedTools.has(tool)) {
1009
+ const allowed = Array.from(this.allowedTools).join(', ');
1010
+ throw new Error(`Tool "${tool}" is not allowed. Configured tools: [${allowed}]`);
1011
+ }
1012
+ }
1013
+ }
1014
+ /**
1015
+ * Configuration-driven, multi-knowledge-base client.
1016
+ *
1017
+ * Usage patterns:
1018
+ * // Scoped: bind to a named knowledge base, then call methods
1019
+ * const product = client.use("产品数据");
1020
+ * await product.retrieve("查询文本");
1021
+ *
1022
+ * // Direct: pass name as first arg
1023
+ * await client.retrieve("产品数据", "查询文本");
1024
+ *
1025
+ * // List: shared across all knowledge bases
1026
+ * await client.list("搜索关键词");
1027
+ */
1028
+ let KnowledgeClient$1 = class KnowledgeClient {
1029
+ constructor(options) {
1030
+ const { config, ...clientOptions } = options;
1031
+ this.tbox = new TboxAPI(clientOptions);
1032
+ this.serviceConfig = {};
1033
+ this.knowledges = new Map();
1034
+ this.resolveConfig(config);
1035
+ }
1036
+ /** Create from an existing TboxAPI instance */
1037
+ static fromTboxAPI(tbox, config) {
1038
+ const instance = Object.create(KnowledgeClient.prototype);
1039
+ instance.tbox = tbox;
1040
+ instance.serviceConfig = {};
1041
+ instance.knowledges = new Map();
1042
+ instance.resolveConfig(config);
1043
+ return instance;
1044
+ }
1045
+ /**
1046
+ * Load config from a knowledge.json file asynchronously (ESM-compatible).
1047
+ * Use this in ESM environments instead of passing a file path to the constructor.
1048
+ */
1049
+ static async fromFile(options, filePath) {
1050
+ const client = new KnowledgeClient(options);
1051
+ await client.loadConfigFromFileAsync(filePath);
1052
+ return client;
1053
+ }
1054
+ /** Resolve config from object, file path, or default */
1055
+ resolveConfig(config) {
1056
+ if (!config) {
1057
+ this.serviceConfig = { type: 'STRUCTURED' };
1058
+ return;
1059
+ }
1060
+ if (typeof config === 'string') {
1061
+ this.loadConfigFromFileSync(config);
1062
+ return;
1063
+ }
1064
+ // Single knowledge config passed as object — store as "default"
1065
+ this.serviceConfig = {
1066
+ type: config.type,
1067
+ topK: config.topK,
1068
+ scoreThreshold: config.scoreThreshold,
1069
+ updateMode: config.updateMode,
1070
+ pageSize: config.pageSize,
1071
+ };
1072
+ this.knowledges.set('default', {
1073
+ datasetId: config.datasetId,
1074
+ documentId: config.documentId,
1075
+ tableSchema: config.tableSchema,
1076
+ indexColumns: config.indexColumns,
1077
+ });
1078
+ }
1079
+ /** Apply parsed JSON schema to this client */
1080
+ applyJsonSchema(parsed) {
1081
+ this.serviceConfig = parsed.config ?? { type: 'STRUCTURED' };
1082
+ if (parsed.tools && parsed.tools.length > 0) {
1083
+ this.allowedTools = new Set(parsed.tools);
1084
+ }
1085
+ if (parsed.knowledges) {
1086
+ for (const [name, instanceConfig] of Object.entries(parsed.knowledges)) {
1087
+ this.knowledges.set(name, { ...instanceConfig });
1088
+ }
1089
+ }
1090
+ }
1091
+ /** Load config from knowledge.json file synchronously (CJS environments) */
1092
+ loadConfigFromFileSync(filePath) {
1093
+ try {
1094
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval -- dynamic require for CJS compat
1095
+ const dynamicRequire = new Function('id', 'return require(id)');
1096
+ const fs = dynamicRequire('fs');
1097
+ const raw = fs.readFileSync(filePath, 'utf-8');
1098
+ const parsed = JSON.parse(raw);
1099
+ this.applyJsonSchema(parsed);
1100
+ }
1101
+ catch (error) {
1102
+ throw new Error(`Failed to load knowledge config from "${filePath}": ${error instanceof Error ? error.message : String(error)}`);
1103
+ }
1104
+ }
1105
+ /** Load config from knowledge.json file asynchronously (ESM-compatible) */
1106
+ async loadConfigFromFileAsync(filePath) {
1107
+ try {
1108
+ // Dynamic import avoids bundler static analysis and works in both CJS and ESM
1109
+ const fsModule = await Function('return import("node:fs/promises")')();
1110
+ const raw = await fsModule.readFile(filePath, 'utf-8');
1111
+ const parsed = JSON.parse(raw);
1112
+ this.applyJsonSchema(parsed);
1113
+ }
1114
+ catch (error) {
1115
+ throw new Error(`Failed to load knowledge config from "${filePath}": ${error instanceof Error ? error.message : String(error)}`);
1116
+ }
1117
+ }
1118
+ /** Get all registered knowledge base names */
1119
+ names() {
1120
+ return Array.from(this.knowledges.keys());
1121
+ }
1122
+ /** Get the allowed tools set (undefined means all tools are allowed) */
1123
+ getAllowedTools() {
1124
+ return this.allowedTools;
1125
+ }
1126
+ /** Get the shared service config */
1127
+ getServiceConfig() {
1128
+ return { ...this.serviceConfig };
1129
+ }
1130
+ /** Get instance config for a named knowledge base */
1131
+ getInstanceConfig(name) {
1132
+ const config = this.knowledges.get(name);
1133
+ return config ? { ...config } : undefined;
1134
+ }
1135
+ /**
1136
+ * Get a scoped client bound to a named knowledge base.
1137
+ * All subsequent calls on the returned handle use that knowledge base's config.
1138
+ */
1139
+ use(name) {
1140
+ const instanceConfig = this.knowledges.get(name);
1141
+ if (!instanceConfig) {
1142
+ const available = this.names().join(', ');
1143
+ throw new Error(`Knowledge base "${name}" not found. Available: [${available}]`);
1144
+ }
1145
+ return new KnowledgeScopedClient(this.tbox, this.serviceConfig, instanceConfig, this.allowedTools);
1146
+ }
1147
+ /** Query knowledge base list (shared, not scoped to a specific knowledge base) */
1148
+ async list(name, pageNo, pageSize) {
1149
+ this.assertToolAllowed('list');
1150
+ const params = {};
1151
+ if (name !== undefined)
1152
+ params.name = name;
1153
+ if (pageNo !== undefined)
1154
+ params.pageNo = pageNo;
1155
+ params.pageSize = pageSize ?? this.serviceConfig.pageSize;
1156
+ return this.tbox.knowledge.list(params);
1157
+ }
1158
+ /** Semantic retrieval — pass knowledge base name as first arg to route */
1159
+ async retrieve(knowledgeName, query, options) {
1160
+ return this.use(knowledgeName).retrieve(query, options);
1161
+ }
1162
+ /** Create a knowledge base by name */
1163
+ async create(knowledgeName, file, options) {
1164
+ return this.use(knowledgeName).create(file, options);
1165
+ }
1166
+ /** Update a knowledge base by name */
1167
+ async update(knowledgeName, file, options) {
1168
+ return this.use(knowledgeName).update(file, options);
1169
+ }
1170
+ /** Throw if the tool is not in the allowed set (when tools are configured) */
1171
+ assertToolAllowed(tool) {
1172
+ if (this.allowedTools && !this.allowedTools.has(tool)) {
1173
+ const allowed = Array.from(this.allowedTools).join(', ');
1174
+ throw new Error(`Tool "${tool}" is not allowed. Configured tools: [${allowed}]`);
1175
+ }
1176
+ }
1177
+ };
1178
+
1179
+ /**
1180
+ * 百宝箱插件服务 SDK - 底层 HTTP 请求封装
1181
+ */
1182
+ /** 默认调用域名 */
1183
+ const DEFAULT_BASE_URL = 'https://o.tbox.cn';
1184
+ /**
1185
+ * 将服务端原始响应统一转换为 SdkResponse
1186
+ */
1187
+ function normalizeResponse(raw) {
1188
+ const success = raw.success !== false;
1189
+ const data = raw.data ?? raw.resultObj ?? raw.result;
1190
+ // 按优先级兼容多种错误码字段
1191
+ const errorCode = raw.errorCode ?? raw.code ?? raw.resultCode;
1192
+ const errorMsg = raw.errorMsg ?? raw.msg ?? raw.resultDesc;
1193
+ if (success) {
1194
+ return { success: true, data };
1195
+ }
1196
+ return { success: false, errorCode, errorMsg };
1197
+ }
1198
+ /**
1199
+ * 底层 HTTP 客户端
1200
+ */
1201
+ class HttpClient {
1202
+ constructor(config) {
1203
+ this.baseUrl = config.baseUrl.replace(/\/$/, '');
1204
+ this.apiKey = config.apiKey;
1205
+ this.timeout = config.timeout;
1206
+ }
1207
+ /** 构建通用请求头 */
1208
+ buildHeaders(extra) {
1209
+ return {
1210
+ 'Content-Type': 'application/json;charset=UTF-8',
1211
+ Authorization: this.apiKey,
1212
+ ...extra,
1213
+ };
1214
+ }
1215
+ /** 构建 FormData 请求头(不设置 Content-Type,让浏览器自动处理 boundary) */
1216
+ buildHeadersForFormData(extra) {
1217
+ return {
1218
+ Authorization: this.apiKey,
1219
+ ...extra,
1220
+ };
1221
+ }
1222
+ /** 发起 fetch,支持可选超时 */
1223
+ async fetchWithTimeout(url, init) {
1224
+ if (!this.timeout) {
1225
+ return fetch(url, init);
1226
+ }
1227
+ const controller = new AbortController();
1228
+ const timer = setTimeout(() => controller.abort(), this.timeout);
1229
+ try {
1230
+ return await fetch(url, { ...init, signal: controller.signal });
1231
+ }
1232
+ finally {
1233
+ clearTimeout(timer);
1234
+ }
1235
+ }
1236
+ /**
1237
+ * POST 请求
1238
+ */
1239
+ async post(path, body, queryParams) {
1240
+ const url = this.buildUrl(path, queryParams);
1241
+ let response;
1242
+ try {
1243
+ response = await this.fetchWithTimeout(url, {
1244
+ method: 'POST',
1245
+ headers: this.buildHeaders(),
1246
+ body: JSON.stringify(body),
1247
+ });
1248
+ }
1249
+ catch (err) {
1250
+ return {
1251
+ success: false,
1252
+ errorCode: 'NETWORK_ERROR',
1253
+ errorMsg: err instanceof Error ? err.message : String(err),
1254
+ };
1255
+ }
1256
+ return this.parseResponse(response);
1257
+ }
1258
+ /**
1259
+ * POST FormData 请求(用于文件上传)
1260
+ */
1261
+ async postFormData(path, formData, queryParams) {
1262
+ const url = this.buildUrl(path, queryParams);
1263
+ let response;
1264
+ try {
1265
+ // FormData 请求不设置 Content-Type,让浏览器自动设置 multipart/form-data 边界
1266
+ response = await this.fetchWithTimeout(url, {
1267
+ method: 'POST',
1268
+ headers: this.buildHeadersForFormData(),
1269
+ body: formData,
1270
+ });
1271
+ }
1272
+ catch (err) {
1273
+ return {
1274
+ success: false,
1275
+ errorCode: 'NETWORK_ERROR',
1276
+ errorMsg: err instanceof Error ? err.message : String(err),
1277
+ };
1278
+ }
1279
+ return this.parseResponse(response);
1280
+ }
1281
+ /**
1282
+ * GET 请求
1283
+ */
1284
+ async get(path, queryParams) {
1285
+ const url = this.buildUrl(path, queryParams);
1286
+ let response;
1287
+ try {
1288
+ response = await this.fetchWithTimeout(url, {
1289
+ method: 'GET',
1290
+ headers: this.buildHeaders(),
1291
+ });
1292
+ }
1293
+ catch (err) {
1294
+ return {
1295
+ success: false,
1296
+ errorCode: 'NETWORK_ERROR',
1297
+ errorMsg: err instanceof Error ? err.message : String(err),
1298
+ };
1299
+ }
1300
+ return this.parseResponse(response);
1301
+ }
1302
+ /** 构建完整 URL(含 query string) */
1303
+ buildUrl(path, queryParams) {
1304
+ // 如果 path 已经是完整 URL(以 http:// 或 https:// 开头),直接使用
1305
+ let base;
1306
+ if (path.startsWith('http://') || path.startsWith('https://')) {
1307
+ base = path;
1308
+ }
1309
+ else {
1310
+ base = `${this.baseUrl}${path}`;
1311
+ }
1312
+ if (!queryParams || Object.keys(queryParams).length === 0) {
1313
+ return base;
1314
+ }
1315
+ const qs = new URLSearchParams(queryParams).toString();
1316
+ return `${base}?${qs}`;
1317
+ }
1318
+ /** 解析响应体 */
1319
+ async parseResponse(response) {
1320
+ if (!response.ok) {
1321
+ return {
1322
+ success: false,
1323
+ errorCode: `HTTP_${response.status}`,
1324
+ errorMsg: `HTTP error: ${response.status} ${response.statusText}`,
1325
+ };
1326
+ }
1327
+ let json;
1328
+ try {
1329
+ // 使用 arrayBuffer + TextDecoder 确保多字节 UTF-8 字符(如中文)正确解码
1330
+ const buffer = await response.arrayBuffer();
1331
+ const text = new TextDecoder('utf-8').decode(buffer);
1332
+ json = JSON.parse(text);
1333
+ }
1334
+ catch {
1335
+ return {
1336
+ success: false,
1337
+ errorCode: 'PARSE_ERROR',
1338
+ errorMsg: 'Failed to parse response JSON',
1339
+ };
1340
+ }
1341
+ return normalizeResponse(json);
1342
+ }
1343
+ }
1344
+
1345
+ /**
1346
+ * 百宝箱插件服务 SDK - PluginClient 核心类
1347
+ *
1348
+ * 封装 /openapi/v1/plugin 下的全部接口
1349
+ */
1350
+ /** API 路径前缀 */
1351
+ const API_PREFIX$4 = '/openapi/v1/plugin';
1352
+ /**
1353
+ * 百宝箱插件服务客户端
1354
+ *
1355
+ * @example
1356
+ * ```ts
1357
+ * import { TboxPluginClient } from '@tbox/plugin-sdk';
1358
+ *
1359
+ * const client = new TboxPluginClient({ apiKey: 'your-api-key' });
1360
+ *
1361
+ * // 执行插件工具
1362
+ * const result = await client.run('tool-id-xxx', { params: { key: 'value' } });
1363
+ * ```
1364
+ */
1365
+ class TboxPluginClient {
1366
+ constructor(config) {
1367
+ this.http = new HttpClient({
1368
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
1369
+ apiKey: config.apiKey,
1370
+ timeout: config.timeout,
1371
+ });
1372
+ }
1373
+ // ============================================================
1374
+ // TTS - 文字转语音
1375
+ // ============================================================
1376
+ /**
1377
+ * 文字转语音
1378
+ *
1379
+ * POST /openapi/v1/plugin/doTts
1380
+ *
1381
+ * @param request - TTS 请求参数
1382
+ * @returns 音频数据(Base64 或 URL)
1383
+ *
1384
+ * @example
1385
+ * ```ts
1386
+ * const res = await client.doTts({ text: '你好,世界' });
1387
+ * if (res.success) {
1388
+ * console.log(res.data?.audioBase64);
1389
+ * }
1390
+ * ```
1391
+ */
1392
+ async doTts(request) {
1393
+ return this.http.post(`${API_PREFIX$4}/doTts`, request);
1394
+ }
1395
+ // ============================================================
1396
+ // ASR - 语音转文字
1397
+ // ============================================================
1398
+ /**
1399
+ * 语音转文字(Base64 音频输入)
1400
+ *
1401
+ * POST /openapi/v1/plugin/asrBase64V2
1402
+ *
1403
+ * @param request - ASR 请求参数,音频以 Base64 格式传入
1404
+ * @returns 识别出的文本
1405
+ *
1406
+ * @example
1407
+ * ```ts
1408
+ * const res = await client.asrBase64({ audioBase64: '<base64-string>', format: 'mp3' });
1409
+ * if (res.success) {
1410
+ * console.log(res.data?.text);
1411
+ * }
1412
+ * ```
1413
+ */
1414
+ async asrBase64(request) {
1415
+ return this.http.post(`${API_PREFIX$4}/asrBase64V2`, request);
1416
+ }
1417
+ // ============================================================
1418
+ // 插件工具执行
1419
+ // ============================================================
1420
+ /**
1421
+ * 按工具 ID 执行插件工具
1422
+ *
1423
+ * POST /openapi/v1/plugin/run/{pluginToolId}
1424
+ *
1425
+ * 适用于已知工具 ID 的场景,调用更直接、性能更好。
1426
+ *
1427
+ * @param pluginToolId - 插件工具 ID
1428
+ * @param request - 工具执行请求(含输入参数)
1429
+ * @returns 工具执行结果
1430
+ *
1431
+ * @example
1432
+ * ```ts
1433
+ * const res = await client.run('tool-id-xxx', {
1434
+ * inputs: { city: '杭州' },
1435
+ * });
1436
+ * if (res.success) {
1437
+ * console.log(res.data?.output);
1438
+ * }
1439
+ * ```
1440
+ */
1441
+ async run(pluginToolId, request) {
1442
+ return this.http.post(`${API_PREFIX$4}/run/${encodeURIComponent(pluginToolId)}`, request);
1443
+ }
1444
+ // ============================================================
1445
+ // 插件信息查询
1446
+ // ============================================================
1447
+ /**
1448
+ * 查询插件配置信息(含工具集)
1449
+ *
1450
+ * GET /openapi/v1/plugin/info/{pluginId}
1451
+ *
1452
+ * @param pluginId - 插件 ID
1453
+ * @returns 插件信息,包含插件名称、描述及工具列表
1454
+ *
1455
+ * @example
1456
+ * ```ts
1457
+ * const res = await client.getPluginInfo('plugin-id');
1458
+ * if (res.success) {
1459
+ * console.log(res.data?.pluginToolList);
1460
+ * }
1461
+ * ```
1462
+ */
1463
+ async getPluginInfo(pluginId) {
1464
+ return this.http.get(`${API_PREFIX$4}/info/${encodeURIComponent(pluginId)}`);
1465
+ }
1466
+ /**
1467
+ * 网络搜索
1468
+ *
1469
+ * 内部调用 run('20240611204600000001', ...) 实现,对外屏蔽工具 ID 细节。
1470
+ * 服务端响应层级:result.result.result.data[],SDK 自动解包为扁平的 items 列表。
1471
+ *
1472
+ * @param request - 搜索请求,包含关键词 q
1473
+ * @returns 搜索结果列表
1474
+ *
1475
+ * @example
1476
+ * ```ts
1477
+ * const res = await client.webSearch({ q: '上海今天天气' });
1478
+ * if (res.success) {
1479
+ * for (const item of res.data?.items ?? []) {
1480
+ * console.log(item.title, item.url);
1481
+ * }
1482
+ * }
1483
+ * ```
1484
+ */
1485
+ async webSearch(request) {
1486
+ // 按服务端约定,搜索关键词放在 params.q
1487
+ const raw = await this.run(TboxPluginClient.WEB_SEARCH_TOOL_ID, {
1488
+ params: { q: request.q },
1489
+ });
1490
+ if (!raw.success) {
1491
+ return {
1492
+ success: false,
1493
+ errorCode: raw.errorCode,
1494
+ errorMsg: raw.errorMsg,
1495
+ };
1496
+ }
1497
+ // 深层解包:raw.data → result.data[]
1498
+ // 服务端实际结构:{ cost, result: { data: [...], success, message } }
1499
+ const inner = raw.data;
1500
+ const level1 = inner?.['result'];
1501
+ const dataArr = level1?.['data'];
1502
+ const items = Array.isArray(dataArr)
1503
+ ? dataArr
1504
+ : [];
1505
+ return {
1506
+ success: true,
1507
+ data: { items },
1508
+ };
1509
+ }
1510
+ }
1511
+ // ============================================================
1512
+ // 网络搜索
1513
+ // ============================================================
1514
+ /** 网络搜索工具的固定工具 ID */
1515
+ TboxPluginClient.WEB_SEARCH_TOOL_ID = '20240611204600000001';
1516
+
1517
+ /**
1518
+ * 百宝箱会话管理 SDK - ConversationClient 核心类
1519
+ *
1520
+ * 封装 /openapi/v1/conversation 下的全部接口
1521
+ */
1522
+ /** API 路径前缀 */
1523
+ const API_PREFIX$3 = '/openapi/v1/conversation';
1524
+ /**
1525
+ * 百宝箱会话管理客户端
1526
+ *
1527
+ * @example
1528
+ * ```ts
1529
+ * import { TboxConversationClient } from '@tbox/plugin-sdk';
1530
+ *
1531
+ * const client = new TboxConversationClient({ apiKey: 'your-api-key' });
1532
+ *
1533
+ * // 创建新会话
1534
+ * const res = await client.createConversation({ agentId: 'app-id', userId: 'user-id' });
1535
+ * if (res.success) {
1536
+ * console.log('会话 ID:', res.data);
1537
+ * }
1538
+ * ```
1539
+ */
1540
+ class TboxConversationClient {
1541
+ constructor(config) {
1542
+ this.http = new HttpClient({
1543
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
1544
+ apiKey: config.apiKey,
1545
+ timeout: config.timeout,
1546
+ });
1547
+ }
1548
+ // ============================================================
1549
+ // 会话管理
1550
+ // ============================================================
1551
+ /**
1552
+ * 创建新会话
1553
+ *
1554
+ * POST /openapi/v1/conversation/create
1555
+ *
1556
+ * @param request - 创建会话请求,包含 agentId 和 userId
1557
+ * @returns 新会话 ID(字符串)
1558
+ *
1559
+ * @example
1560
+ * ```ts
1561
+ * const res = await client.createConversation({
1562
+ * agentId: 'your-app-id',
1563
+ * userId: 'user-123',
1564
+ * });
1565
+ * if (res.success) {
1566
+ * const conversationId = res.data;
1567
+ * }
1568
+ * ```
1569
+ */
1570
+ async createConversation(request) {
1571
+ return this.http.post(`${API_PREFIX$3}/create`, request);
1572
+ }
1573
+ /**
1574
+ * 查询会话列表(分页)
1575
+ *
1576
+ * POST /openapi/v1/conversation/list
1577
+ *
1578
+ * @param request - 查询请求,支持按 agentId、userId 过滤,支持分页
1579
+ * @returns 分页会话列表
1580
+ *
1581
+ * @example
1582
+ * ```ts
1583
+ * const res = await client.listConversations({
1584
+ * agentId: 'your-app-id',
1585
+ * userId: 'user-123',
1586
+ * pageNum: 1,
1587
+ * pageSize: 20,
1588
+ * });
1589
+ * if (res.success) {
1590
+ * console.log(res.data?.list);
1591
+ * }
1592
+ * ```
1593
+ */
1594
+ async listConversations(request) {
1595
+ return this.http.post(`${API_PREFIX$3}/list`, request);
1596
+ }
1597
+ // ============================================================
1598
+ // 消息管理
1599
+ // ============================================================
1600
+ /**
1601
+ * 查询消息列表(分页)
1602
+ *
1603
+ * POST /openapi/v1/conversation/message/list
1604
+ *
1605
+ * @param request - 查询请求,支持按 conversationId、agentId、userId 过滤,支持分页
1606
+ * @returns 分页消息列表
1607
+ *
1608
+ * @example
1609
+ * ```ts
1610
+ * const res = await client.listMessages({
1611
+ * conversationId: 'conv-id-xxx',
1612
+ * pageNum: 1,
1613
+ * pageSize: 20,
1614
+ * });
1615
+ * if (res.success) {
1616
+ * console.log(res.data?.list);
1617
+ * }
1618
+ * ```
1619
+ */
1620
+ async listMessages(request) {
1621
+ return this.http.post(`${API_PREFIX$3}/message/list`, request);
1622
+ }
1623
+ /**
1624
+ * 新增会话消息
1625
+ *
1626
+ * POST /openapi/v1/conversation/message/save
1627
+ *
1628
+ * 将一条对话消息(问题 + 回答)保存到指定会话中。
1629
+ *
1630
+ * @param request - 保存消息请求
1631
+ * @returns 消息 ID(字符串)
1632
+ *
1633
+ * @example
1634
+ * ```ts
1635
+ * const res = await client.saveMessage({
1636
+ * conversationId: 'conv-id-xxx',
1637
+ * query: '今天天气怎么样?',
1638
+ * answer: '今天杭州晴天,25°C。',
1639
+ * });
1640
+ * if (res.success) {
1641
+ * const messageId = res.data;
1642
+ * }
1643
+ * ```
1644
+ */
1645
+ async saveMessage(request) {
1646
+ return this.http.post(`${API_PREFIX$3}/message/save`, request);
1647
+ }
1648
+ }
1649
+
1650
+ /**
1651
+ * 百宝箱插件服务 SDK - 应用服务客户端
1652
+ *
1653
+ * 封装 /openapi/v1/app 下的接口
1654
+ */
1655
+ /**
1656
+ * 百宝箱应用服务客户端
1657
+ *
1658
+ * @example
1659
+ * ```ts
1660
+ * const client = new TboxAppClient({ apiKey: 'your-api-key' });
1661
+ *
1662
+ * // 生成 sessionId,用于免登录访问百宝箱应用
1663
+ * const result = await client.generateSession({
1664
+ * appId: 'your-app-id',
1665
+ * userId: 'user-123',
1666
+ * userInfo: [
1667
+ * { infoType: 'name', infoValue: '张三' },
1668
+ * ],
1669
+ * deviceInfo: {
1670
+ * platform: 'IOS',
1671
+ * appVersion: '1.0.0',
1672
+ * },
1673
+ * });
1674
+ *
1675
+ * if (result.success) {
1676
+ * console.log('sessionId:', result.data?.sessionId);
1677
+ * console.log('channel:', result.data?.channel);
1678
+ * }
1679
+ * ```
1680
+ */
1681
+ class TboxAppClient {
1682
+ constructor(config) {
1683
+ this.http = new HttpClient({
1684
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
1685
+ apiKey: config.apiKey,
1686
+ timeout: config.timeout,
1687
+ });
1688
+ }
1689
+ /**
1690
+ * 根据用户信息生成 sessionId 和 channel
1691
+ *
1692
+ * 接口:POST /openapi/v1/app/generate_session
1693
+ *
1694
+ * 生成的 sessionId 可用于拼接百宝箱 H5 访问链接,实现免登录跳转。
1695
+ *
1696
+ * @param request 请求参数,appId 和 userId 为必填
1697
+ * @returns sessionId 和 channel
1698
+ */
1699
+ async generateSession(request) {
1700
+ return this.http.post('/openapi/v1/app/generate_session', request);
1701
+ }
1702
+ }
1703
+
1704
+ /**
1705
+ * 高德地图 SDK - AmapClient 核心类
1706
+ *
1707
+ * 封装高德地图天气查询工具(amap_weather),
1708
+ * 通过百宝箱插件服务 /openapi/v1/plugin/run/{pluginToolId} 接口调用。
1709
+ */
1710
+ /** API 路径前缀 */
1711
+ const API_PREFIX$2 = '/openapi/v1/plugin';
1712
+ /**
1713
+ * 高德地图客户端
1714
+ *
1715
+ * @example
1716
+ * ```ts
1717
+ * import { AmapClient } from '@tbox/plugin-sdk';
1718
+ *
1719
+ * const client = new AmapClient({ apiKey: 'your-api-key' });
1720
+ *
1721
+ * // 查询实况天气
1722
+ * const res = await client.weather({ city: '杭州市' });
1723
+ * if (res.success) {
1724
+ * console.log(res.data?.lives?.[0]?.weather);
1725
+ * }
1726
+ *
1727
+ * // 查询预报天气
1728
+ * const forecast = await client.weather({ city: '上海市', extensions: 'all' });
1729
+ * if (forecast.success) {
1730
+ * console.log(forecast.data?.forecasts?.[0]?.casts);
1731
+ * }
1732
+ * ```
1733
+ */
1734
+ class AmapClient {
1735
+ constructor(config) {
1736
+ this.http = new HttpClient({
1737
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
1738
+ apiKey: config.apiKey,
1739
+ timeout: config.timeout,
1740
+ });
1741
+ }
1742
+ // ============================================================
1743
+ // 天气查询
1744
+ // ============================================================
1745
+ /**
1746
+ * 查询天气(实况或预报)
1747
+ *
1748
+ * 内部调用 run('20240627231800002183', ...) 实现,对外屏蔽工具 ID 细节。
1749
+ * 数据来源于中国气象局。
1750
+ *
1751
+ * @param request - 天气查询请求
1752
+ * - `city`(必填):省份名、市名、区/县/镇、乡/村或具体地点
1753
+ * - `extensions`(可选):`base`(实况,默认)或 `all`(预报)
1754
+ * @returns 天气查询结果
1755
+ * - `extensions=base` 时,`data.lives` 包含实况天气数据
1756
+ * - `extensions=all` 时,`data.forecasts` 包含逐日预报数据
1757
+ *
1758
+ * @example
1759
+ * ```ts
1760
+ * // 查询实况天气
1761
+ * const live = await client.weather({ city: '西湖区' });
1762
+ * if (live.success) {
1763
+ * const w = live.data?.lives?.[0];
1764
+ * console.log(`${w?.city} ${w?.weather} ${w?.temperature}℃`);
1765
+ * }
1766
+ *
1767
+ * // 查询未来几天预报
1768
+ * const forecast = await client.weather({ city: '北京市', extensions: 'all' });
1769
+ * if (forecast.success) {
1770
+ * for (const cast of forecast.data?.forecasts?.[0]?.casts ?? []) {
1771
+ * console.log(`${cast.date} 白天:${cast.dayweather} 夜间:${cast.nightweather}`);
1772
+ * }
1773
+ * }
1774
+ * ```
1775
+ */
1776
+ async weather(request) {
1777
+ const params = { city: request.city };
1778
+ if (request.extensions !== undefined) {
1779
+ params['extensions'] = request.extensions;
1780
+ }
1781
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_WEATHER_TOOL_ID)}`, { params });
1782
+ if (!raw.success) {
1783
+ return {
1784
+ success: false,
1785
+ errorCode: raw.errorCode,
1786
+ errorMsg: raw.errorMsg,
1787
+ };
1788
+ }
1789
+ // 从 raw.data.result 中解包天气数据
1790
+ // 服务端实际结构:{ success, result: { status, info, infocode, count, lives?, forecasts? }, cost, traceId }
1791
+ const execResult = raw.data;
1792
+ const weatherData = execResult?.result;
1793
+ return {
1794
+ success: true,
1795
+ data: weatherData ?? {},
1796
+ };
1797
+ }
1798
+ // ============================================================
1799
+ // 地址转经纬度
1800
+ // ============================================================
1801
+ /**
1802
+ * 地址转经纬度(地理编码)
1803
+ *
1804
+ * 将详细的地名,转换为经纬度坐标。
1805
+ * 内部调用 run('20240627233200002185', ...) 实现,对外屏蔽工具 ID 细节。
1806
+ *
1807
+ * @param request - 地址转经纬度请求
1808
+ * - `address`(必填):详细地址信息(国家、省份、城市、区县、城镇、乡村、街道、门牌号)
1809
+ * - `city`(可选):查询的城市,指定可提高查询精度
1810
+ * @returns 地理编码结果
1811
+ * - `data.geocodes` 包含地理编码信息列表
1812
+ *
1813
+ * @example
1814
+ * ```ts
1815
+ * // 地址转经纬度
1816
+ * const res = await client.geocode({ address: '北京市朝阳区阜通东大街6号', city: '北京' });
1817
+ * if (res.success) {
1818
+ * const geo = res.data?.geocodes?.[0];
1819
+ * console.log(`地址: ${geo?.formatted_address}`);
1820
+ * console.log(`经纬度: ${geo?.location}`);
1821
+ * console.log(`区域编码: ${geo?.adcode}`);
1822
+ * }
1823
+ * ```
1824
+ */
1825
+ async geocode(request) {
1826
+ const params = { address: request.address };
1827
+ if (request.city !== undefined) {
1828
+ params['city'] = request.city;
1829
+ }
1830
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_GEOCODE_TOOL_ID)}`, { params });
1831
+ if (!raw.success) {
1832
+ return {
1833
+ success: false,
1834
+ errorCode: raw.errorCode,
1835
+ errorMsg: raw.errorMsg,
1836
+ };
1837
+ }
1838
+ // 从 raw.data.result 中解包地理编码数据
1839
+ // 服务端实际结构:{ success, result: { code, status, infocode, count, geocodes? }, cost, traceId }
1840
+ const execResult = raw.data;
1841
+ const geocodeData = execResult?.result;
1842
+ return {
1843
+ success: true,
1844
+ data: geocodeData ?? {},
1845
+ };
1846
+ }
1847
+ // ============================================================
1848
+ // 骑行路线规划
1849
+ // ============================================================
1850
+ /**
1851
+ * 骑行路线规划
1852
+ *
1853
+ * 提供起点和终点的位置,返回骑行路线,支持多条路线。
1854
+ *
1855
+ * @param request - 骑行路线规划请求
1856
+ * - `origin`(必填):起点位置名称
1857
+ * - `destination`(必填):终点位置名称
1858
+ * @returns 骑行路线规划结果
1859
+ *
1860
+ * @example
1861
+ * ```ts
1862
+ * const res = await client.cyclingRoute({
1863
+ * origin: '杭州市西湖区',
1864
+ * destination: '杭州市余杭区'
1865
+ * });
1866
+ * if (res.success) {
1867
+ * const path = res.data?.route?.paths?.[0];
1868
+ * console.log(`距离: ${path?.distance}米, 时间: ${path?.duration}秒`);
1869
+ * }
1870
+ * ```
1871
+ */
1872
+ async cyclingRoute(request) {
1873
+ const params = {
1874
+ origin: request.origin,
1875
+ destination: request.destination,
1876
+ };
1877
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_CYCLING_ROUTE_TOOL_ID)}`, { params });
1878
+ if (!raw.success) {
1879
+ return {
1880
+ success: false,
1881
+ errorCode: raw.errorCode,
1882
+ errorMsg: raw.errorMsg,
1883
+ };
1884
+ }
1885
+ const execResult = raw.data;
1886
+ const routeData = execResult?.result;
1887
+ return {
1888
+ success: true,
1889
+ data: routeData ?? {},
1890
+ };
1891
+ }
1892
+ // ============================================================
1893
+ // IP 定位
1894
+ // ============================================================
1895
+ /**
1896
+ * IP 定位
1897
+ *
1898
+ * 定位输入的 IP 所在的地理位置(暂不支持 IPv6)。
1899
+ *
1900
+ * @param request - IP 定位请求
1901
+ * - `ip`(必填):需要定位的 IP 地址
1902
+ * @returns IP 定位结果
1903
+ *
1904
+ * @example
1905
+ * ```ts
1906
+ * const res = await client.ipLocation({ ip: '114.114.114.114' });
1907
+ * if (res.success) {
1908
+ * console.log(`省份: ${res.data?.province}, 城市: ${res.data?.city}`);
1909
+ * }
1910
+ * ```
1911
+ */
1912
+ async ipLocation(request) {
1913
+ const params = { ip: request.ip };
1914
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_IP_LOCATION_TOOL_ID)}`, { params });
1915
+ if (!raw.success) {
1916
+ return {
1917
+ success: false,
1918
+ errorCode: raw.errorCode,
1919
+ errorMsg: raw.errorMsg,
1920
+ };
1921
+ }
1922
+ const execResult = raw.data;
1923
+ const locationData = execResult?.result;
1924
+ return {
1925
+ success: true,
1926
+ data: locationData ?? {},
1927
+ };
1928
+ }
1929
+ // ============================================================
1930
+ // 周边搜索
1931
+ // ============================================================
1932
+ /**
1933
+ * 周边搜索
1934
+ *
1935
+ * 根据指定地点和关键词,搜索周边景点、美食、酒店、运动场所等。
1936
+ *
1937
+ * @param request - 周边搜索请求
1938
+ * - `address`(必填):中心点地址信息
1939
+ * - `keyword`(必填):检索关键词(景点、美食、游玩区域等,不超过 80 字符)
1940
+ * - `count`(可选):返回条数,默认 5
1941
+ * - `radius`(可选):搜索半径(米),范围 0-50000
1942
+ * @returns 周边搜索结果
1943
+ *
1944
+ * @example
1945
+ * ```ts
1946
+ * const res = await client.peripheralSearch({
1947
+ * address: '杭州市西湖区',
1948
+ * keyword: '美食',
1949
+ * count: 10,
1950
+ * radius: 1000
1951
+ * });
1952
+ * if (res.success) {
1953
+ * for (const poi of res.data?.pois ?? []) {
1954
+ * console.log(`${poi.name} - ${poi.address}`);
1955
+ * }
1956
+ * }
1957
+ * ```
1958
+ */
1959
+ async peripheralSearch(request) {
1960
+ const params = {
1961
+ address: request.address,
1962
+ keyword: request.keyword,
1963
+ };
1964
+ if (request.count !== undefined) {
1965
+ params['count'] = request.count;
1966
+ }
1967
+ if (request.radius !== undefined) {
1968
+ params['radius'] = request.radius;
1969
+ }
1970
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_PERIPHERAL_SEARCH_TOOL_ID)}`, { params });
1971
+ if (!raw.success) {
1972
+ return {
1973
+ success: false,
1974
+ errorCode: raw.errorCode,
1975
+ errorMsg: raw.errorMsg,
1976
+ };
1977
+ }
1978
+ const execResult = raw.data;
1979
+ const searchData = execResult?.result;
1980
+ return {
1981
+ success: true,
1982
+ data: searchData ?? {},
1983
+ };
1984
+ }
1985
+ // ============================================================
1986
+ // 步行路线规划
1987
+ // ============================================================
1988
+ /**
1989
+ * 步行路线规划
1990
+ *
1991
+ * 提供起点和终点的位置,返回步行路线,支持多条路线。
1992
+ *
1993
+ * @param request - 步行路线规划请求
1994
+ * - `origin`(必填):起点地址
1995
+ * - `destination`(必填):终点地址
1996
+ * @returns 步行路线规划结果
1997
+ *
1998
+ * @example
1999
+ * ```ts
2000
+ * const res = await client.walkingRoute({
2001
+ * origin: '杭州市西湖区古荡',
2002
+ * destination: '杭州市西湖区西溪湿地'
2003
+ * });
2004
+ * if (res.success) {
2005
+ * const path = res.data?.route?.paths?.[0];
2006
+ * console.log(`距离: ${path?.distance}米, 时间: ${path?.duration}秒`);
2007
+ * }
2008
+ * ```
2009
+ */
2010
+ async walkingRoute(request) {
2011
+ const params = {
2012
+ origin: request.origin,
2013
+ destination: request.destination,
2014
+ };
2015
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_WALKING_ROUTE_TOOL_ID)}`, { params });
2016
+ if (!raw.success) {
2017
+ return {
2018
+ success: false,
2019
+ errorCode: raw.errorCode,
2020
+ errorMsg: raw.errorMsg,
2021
+ };
2022
+ }
2023
+ const execResult = raw.data;
2024
+ const routeData = execResult?.result;
2025
+ return {
2026
+ success: true,
2027
+ data: routeData ?? {},
2028
+ };
2029
+ }
2030
+ // ============================================================
2031
+ // 驾车路线规划
2032
+ // ============================================================
2033
+ /**
2034
+ * 驾车路线规划
2035
+ *
2036
+ * 提供起点和终点的位置,返回驾车路线,支持多条路线。
2037
+ *
2038
+ * @param request - 驾车路线规划请求
2039
+ * - `origin`(必填):起点地址或经纬度
2040
+ * - `destination`(必填):终点地址或经纬度
2041
+ * @returns 驾车路线规划结果
2042
+ *
2043
+ * @example
2044
+ * ```ts
2045
+ * const res = await client.drivingRoute({
2046
+ * origin: '杭州西溪湿地',
2047
+ * destination: '绍兴市'
2048
+ * });
2049
+ * if (res.success) {
2050
+ * const path = res.data?.route?.paths?.[0];
2051
+ * console.log(`距离: ${path?.distance}米, 时间: ${path?.duration}秒`);
2052
+ * console.log(`出租车费用: ${res.data?.route?.taxi_cost}元`);
2053
+ * }
2054
+ * ```
2055
+ */
2056
+ async drivingRoute(request) {
2057
+ const params = {
2058
+ origin: request.origin,
2059
+ destination: request.destination,
2060
+ };
2061
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_DRIVING_ROUTE_TOOL_ID)}`, { params });
2062
+ if (!raw.success) {
2063
+ return {
2064
+ success: false,
2065
+ errorCode: raw.errorCode,
2066
+ errorMsg: raw.errorMsg,
2067
+ };
2068
+ }
2069
+ const execResult = raw.data;
2070
+ const routeData = execResult?.result;
2071
+ return {
2072
+ success: true,
2073
+ data: routeData ?? {},
2074
+ };
2075
+ }
2076
+ // ============================================================
2077
+ // 电动车路线规划
2078
+ // ============================================================
2079
+ /**
2080
+ * 电动车路线规划
2081
+ *
2082
+ * 提供起点和终点的位置,返回电动车骑行路线,支持多条路线。
2083
+ *
2084
+ * @param request - 电动车路线规划请求
2085
+ * - `origin`(必填):起点地址或经纬度
2086
+ * - `destination`(必填):终点地址或经纬度
2087
+ * @returns 电动车路线规划结果
2088
+ *
2089
+ * @example
2090
+ * ```ts
2091
+ * const res = await client.ebikeRoute({
2092
+ * origin: '杭州市西湖区',
2093
+ * destination: '杭州市余杭区'
2094
+ * });
2095
+ * if (res.success) {
2096
+ * const path = res.data?.route?.paths?.[0];
2097
+ * console.log(`距离: ${path?.distance}米, 时间: ${path?.duration}秒`);
2098
+ * }
2099
+ * ```
2100
+ */
2101
+ async ebikeRoute(request) {
2102
+ const params = {
2103
+ origin: request.origin,
2104
+ destination: request.destination,
2105
+ };
2106
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_EBIKE_ROUTE_TOOL_ID)}`, { params });
2107
+ if (!raw.success) {
2108
+ return {
2109
+ success: false,
2110
+ errorCode: raw.errorCode,
2111
+ errorMsg: raw.errorMsg,
2112
+ };
2113
+ }
2114
+ const execResult = raw.data;
2115
+ const routeData = execResult?.result;
2116
+ return {
2117
+ success: true,
2118
+ data: routeData ?? {},
2119
+ };
2120
+ }
2121
+ // ============================================================
2122
+ // 公交路线规划
2123
+ // ============================================================
2124
+ /**
2125
+ * 公交路线规划
2126
+ *
2127
+ * 提供起点和终点的位置,返回公交换乘方案,支持多条路线。
2128
+ *
2129
+ * @param request - 公交路线规划请求
2130
+ * - `origin`(必填):起点地址或经纬度
2131
+ * - `destination`(必填):终点地址或经纬度
2132
+ * @returns 公交路线规划结果
2133
+ *
2134
+ * @example
2135
+ * ```ts
2136
+ * const res = await client.transitRoute({
2137
+ * origin: '杭州火车站',
2138
+ * destination: '西湖'
2139
+ * });
2140
+ * if (res.success) {
2141
+ * for (const transit of res.data?.route?.transits ?? []) {
2142
+ * console.log(`费用: ${transit.cost}元, 时间: ${transit.duration}秒`);
2143
+ * }
2144
+ * }
2145
+ * ```
2146
+ */
2147
+ async transitRoute(request) {
2148
+ const params = {
2149
+ origin: request.origin,
2150
+ destination: request.destination,
2151
+ };
2152
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_TRANSIT_ROUTE_TOOL_ID)}`, { params });
2153
+ if (!raw.success) {
2154
+ return {
2155
+ success: false,
2156
+ errorCode: raw.errorCode,
2157
+ errorMsg: raw.errorMsg,
2158
+ };
2159
+ }
2160
+ const execResult = raw.data;
2161
+ const routeData = execResult?.result;
2162
+ return {
2163
+ success: true,
2164
+ data: routeData ?? {},
2165
+ };
2166
+ }
2167
+ // ============================================================
2168
+ // 行政区域查询
2169
+ // ============================================================
2170
+ /**
2171
+ * 行政区域查询
2172
+ *
2173
+ * 根据用户输入,快速查找特定的行政区域信息。
2174
+ *
2175
+ * @param request - 行政区域查询请求
2176
+ * - `keywords`(必填):查询关键字(行政区名称、citycode、adcode)
2177
+ * @returns 行政区域查询结果
2178
+ *
2179
+ * @example
2180
+ * ```ts
2181
+ * const res = await client.district({ keywords: '成都市' });
2182
+ * if (res.success) {
2183
+ * for (const district of res.data?.districts ?? []) {
2184
+ * console.log(`${district.name} (${district.adcode})`);
2185
+ * }
2186
+ * }
2187
+ * ```
2188
+ */
2189
+ async district(request) {
2190
+ const params = { keywords: request.keywords };
2191
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_DISTRICT_TOOL_ID)}`, { params });
2192
+ if (!raw.success) {
2193
+ return {
2194
+ success: false,
2195
+ errorCode: raw.errorCode,
2196
+ errorMsg: raw.errorMsg,
2197
+ };
2198
+ }
2199
+ const execResult = raw.data;
2200
+ const districtData = execResult?.result;
2201
+ return {
2202
+ success: true,
2203
+ data: districtData ?? {},
2204
+ };
2205
+ }
2206
+ // ============================================================
2207
+ // 经纬度转地址(逆地理编码)
2208
+ // ============================================================
2209
+ /**
2210
+ * 经纬度转地址(逆地理编码)
2211
+ *
2212
+ * 将经纬度转换为详细的地址,且返回附近周边的 POI、AOI 信息。
2213
+ *
2214
+ * @param request - 经纬度转地址请求
2215
+ * - `location`(必填):经纬度坐标(经度在前,纬度在后,英文逗号分隔)
2216
+ * - `extensions`(可选):base(基本地址)/ all(详细信息)
2217
+ * @returns 逆地理编码结果
2218
+ *
2219
+ * @example
2220
+ * ```ts
2221
+ * const res = await client.regeocode({
2222
+ * location: '116.480881,39.989410',
2223
+ * extensions: 'base'
2224
+ * });
2225
+ * if (res.success) {
2226
+ * console.log(`地址: ${res.data?.regeocode?.formatted_address}`);
2227
+ * }
2228
+ * ```
2229
+ */
2230
+ async regeocode(request) {
2231
+ const params = { location: request.location };
2232
+ if (request.extensions !== undefined) {
2233
+ params['extensions'] = request.extensions;
2234
+ }
2235
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_REGEOCODE_TOOL_ID)}`, { params });
2236
+ if (!raw.success) {
2237
+ return {
2238
+ success: false,
2239
+ errorCode: raw.errorCode,
2240
+ errorMsg: raw.errorMsg,
2241
+ };
2242
+ }
2243
+ const execResult = raw.data;
2244
+ const regeocodeData = execResult?.result;
2245
+ return {
2246
+ success: true,
2247
+ data: regeocodeData ?? {},
2248
+ };
2249
+ }
2250
+ // ============================================================
2251
+ // 坐标转换
2252
+ // ============================================================
2253
+ /**
2254
+ * 坐标转换
2255
+ *
2256
+ * 将非高德坐标(GPS、mapbar、baidu 坐标)转换成高德坐标。
2257
+ *
2258
+ * @param request - 坐标转换请求
2259
+ * - `locations`(必填):坐标点(经纬度用逗号分隔,多个坐标用 "|" 分隔,最多 40 个)
2260
+ * - `coordsys`(必填):原坐标系(gps / mapbar / baidu / autonavi)
2261
+ * @returns 坐标转换结果
2262
+ *
2263
+ * @example
2264
+ * ```ts
2265
+ * const res = await client.convert({
2266
+ * locations: '116.481488,39.990464',
2267
+ * coordsys: 'baidu'
2268
+ * });
2269
+ * if (res.success) {
2270
+ * console.log(`转换后的坐标: ${res.data?.locations}`);
2271
+ * }
2272
+ * ```
2273
+ */
2274
+ async convert(request) {
2275
+ const params = {
2276
+ locations: request.locations,
2277
+ coordsys: request.coordsys,
2278
+ };
2279
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_CONVERT_TOOL_ID)}`, { params });
2280
+ if (!raw.success) {
2281
+ return {
2282
+ success: false,
2283
+ errorCode: raw.errorCode,
2284
+ errorMsg: raw.errorMsg,
2285
+ };
2286
+ }
2287
+ const execResult = raw.data;
2288
+ const convertData = execResult?.result;
2289
+ return {
2290
+ success: true,
2291
+ data: convertData ?? {},
2292
+ };
2293
+ }
2294
+ // ============================================================
2295
+ // POI 关键字搜索
2296
+ // ============================================================
2297
+ /**
2298
+ * POI 关键字搜索
2299
+ *
2300
+ * 通过文本关键字搜索地点信息,支持结构化地址和 POI 名称搜索。
2301
+ *
2302
+ * @param request - POI 关键字搜索请求
2303
+ * - `keywords`(可选):查询关键字
2304
+ * - `types`(可选):指定地点类型
2305
+ * - `region`(可选):搜索区划(默认全国)
2306
+ * - `city_limit`(可选):是否限制在 region 内(true / false)
2307
+ * - `page_size`(可选):每页数据条数(1-25)
2308
+ * - `page_num`(可选):分页页码(1-100)
2309
+ * @returns POI 搜索结果
2310
+ *
2311
+ * @example
2312
+ * ```ts
2313
+ * const res = await client.poiSearch({
2314
+ * keywords: '首开广场',
2315
+ * region: '北京',
2316
+ * page_size: '10',
2317
+ * page_num: '1'
2318
+ * });
2319
+ * if (res.success) {
2320
+ * for (const poi of res.data?.pois ?? []) {
2321
+ * console.log(`${poi.name} - ${poi.address}`);
2322
+ * }
2323
+ * }
2324
+ * ```
2325
+ */
2326
+ async poiSearch(request) {
2327
+ const params = {};
2328
+ if (request.keywords !== undefined) {
2329
+ params['keywords'] = request.keywords;
2330
+ }
2331
+ if (request.types !== undefined) {
2332
+ params['types'] = request.types;
2333
+ }
2334
+ if (request.region !== undefined) {
2335
+ params['region'] = request.region;
2336
+ }
2337
+ if (request.city_limit !== undefined) {
2338
+ params['city_limit'] = request.city_limit;
2339
+ }
2340
+ if (request.page_size !== undefined) {
2341
+ params['page_size'] = request.page_size;
2342
+ }
2343
+ if (request.page_num !== undefined) {
2344
+ params['page_num'] = request.page_num;
2345
+ }
2346
+ const raw = await this.http.post(`${API_PREFIX$2}/run/${encodeURIComponent(AmapClient.AMAP_POI_SEARCH_TOOL_ID)}`, { params });
2347
+ if (!raw.success) {
2348
+ return {
2349
+ success: false,
2350
+ errorCode: raw.errorCode,
2351
+ errorMsg: raw.errorMsg,
2352
+ };
2353
+ }
2354
+ const execResult = raw.data;
2355
+ const poiData = execResult?.result;
2356
+ return {
2357
+ success: true,
2358
+ data: poiData ?? {},
2359
+ };
2360
+ }
2361
+ }
2362
+ /** 高德天气查询工具的固定工具 ID */
2363
+ AmapClient.AMAP_WEATHER_TOOL_ID = '20240627231800002183';
2364
+ /** 高德地址转经纬度工具的固定工具 ID */
2365
+ AmapClient.AMAP_GEOCODE_TOOL_ID = '20240627233200002185';
2366
+ /** 高德骑行路线规划工具的固定工具 ID */
2367
+ AmapClient.AMAP_CYCLING_ROUTE_TOOL_ID = '20240627233900002186';
2368
+ /** 高德 IP 定位工具的固定工具 ID */
2369
+ AmapClient.AMAP_IP_LOCATION_TOOL_ID = '20240627235900002187';
2370
+ /** 高德周边搜索工具的固定工具 ID */
2371
+ AmapClient.AMAP_PERIPHERAL_SEARCH_TOOL_ID = '20240628000200002188';
2372
+ /** 高德步行路线规划工具的固定工具 ID */
2373
+ AmapClient.AMAP_WALKING_ROUTE_TOOL_ID = '20240628000400002189';
2374
+ /** 高德驾车路线规划工具的固定工具 ID */
2375
+ AmapClient.AMAP_DRIVING_ROUTE_TOOL_ID = '20240628000900002190';
2376
+ /** 高德电动车路线规划工具的固定工具 ID */
2377
+ AmapClient.AMAP_EBIKE_ROUTE_TOOL_ID = '20240628001000002191';
2378
+ /** 高德公交路线规划工具的固定工具 ID */
2379
+ AmapClient.AMAP_TRANSIT_ROUTE_TOOL_ID = '20240628001100002192';
2380
+ /** 高德行政区域查询工具的固定工具 ID */
2381
+ AmapClient.AMAP_DISTRICT_TOOL_ID = '20240628001200002193';
2382
+ /** 高德经纬度转地址工具的固定工具 ID */
2383
+ AmapClient.AMAP_REGEOCODE_TOOL_ID = '20240629202400006671';
2384
+ /** 高德坐标转换工具的固定工具 ID */
2385
+ AmapClient.AMAP_CONVERT_TOOL_ID = '20251110ArTR04496110';
2386
+ /** 高德 POI 关键字搜索工具的固定工具 ID */
2387
+ AmapClient.AMAP_POI_SEARCH_TOOL_ID = '20251110crvO04497983';
2388
+
2389
+ /**
2390
+ * 知识库 SDK - KnowledgeClient 核心类
2391
+ *
2392
+ * 封装知识库管理功能,通过百宝箱服务接口实现知识库的创建和管理。
2393
+ */
2394
+ /** API 路径前缀 */
2395
+ const API_PREFIX$1 = '/openapi/v1';
2396
+ /**
2397
+ * 知识库客户端
2398
+ *
2399
+ * @example
2400
+ * ```ts
2401
+ * import { KnowledgeClient } from '@tbox/plugin-sdk';
2402
+ *
2403
+ * const client = new KnowledgeClient({ apiKey: 'your-api-key' });
2404
+ *
2405
+ * // 创建结构化知识库
2406
+ * const res = await client.createKnowledgeBase({
2407
+ * file: fileBlob,
2408
+ * type: 'STRUCTURED',
2409
+ * tableSchema: {
2410
+ * columns: [
2411
+ * { name: 'id', dataType: 'STRING', required: true, order: 1 },
2412
+ * { name: 'name', dataType: 'STRING', required: false, order: 2 }
2413
+ * ],
2414
+ * name: '示例表'
2415
+ * },
2416
+ * indexColumns: ['id']
2417
+ * });
2418
+ * if (res.success) {
2419
+ * console.log(`文档 ID: ${res.data?.documentId}`);
2420
+ * }
2421
+ * ```
2422
+ */
2423
+ class KnowledgeClient {
2424
+ constructor(config) {
2425
+ this.http = new HttpClient({
2426
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
2427
+ apiKey: config.apiKey,
2428
+ timeout: config.timeout,
2429
+ });
2430
+ }
2431
+ // ============================================================
2432
+ // 创建知识库
2433
+ // ============================================================
2434
+ /**
2435
+ * 创建知识库
2436
+ *
2437
+ * 通过上传文件创建知识库,支持结构化和非结构化数据。
2438
+ *
2439
+ * @param request - 创建知识库请求
2440
+ * - `file`(必填):上传的文件(如 .xlsx、.pdf 等)
2441
+ * - `type`(必填):数据类型,`STRUCTURED`(结构化)或其他类型
2442
+ * - `tableSchema`(条件必填):表格 Schema 定义,当 type = STRUCTURED 时必填
2443
+ * - `indexColumns`(条件必填):索引列名列表,当 type = STRUCTURED 时必填
2444
+ * @returns 创建知识库结果
2445
+ *
2446
+ * @example
2447
+ * ```ts
2448
+ * // 从本地文件创建结构化知识库
2449
+ * const fileInput = document.querySelector('input[type="file"]');
2450
+ * const file = fileInput.files[0];
2451
+ *
2452
+ * const res = await client.createKnowledgeBase({
2453
+ * file: file,
2454
+ * type: 'STRUCTURED',
2455
+ * tableSchema: {
2456
+ * columns: [
2457
+ * { name: 'id', dataType: 'STRING', required: true, order: 1 },
2458
+ * { name: 'message_id', dataType: 'STRING', required: false, order: 2 },
2459
+ * { name: 'content', dataType: 'STRING', required: false, order: 3 }
2460
+ * ],
2461
+ * name: '消息记录',
2462
+ * description: '用于存储消息数据'
2463
+ * },
2464
+ * indexColumns: ['id', 'message_id']
2465
+ * });
2466
+ *
2467
+ * if (res.success) {
2468
+ * console.log(`创建成功,文档 ID: ${res.data?.documentId}`);
2469
+ * } else {
2470
+ * console.error(`创建失败: ${res.errorMsg}`);
2471
+ * }
2472
+ * ```
2473
+ */
2474
+ async createKnowledgeBase(request) {
2475
+ // 构建 FormData
2476
+ const formData = new FormData();
2477
+ formData.append('file', request.file);
2478
+ formData.append('type', request.type);
2479
+ // 当 type 为 STRUCTURED 时,需要添加 tableSchema 和 indexColumns
2480
+ if (request.type === 'STRUCTURED') {
2481
+ if (request.tableSchema) {
2482
+ formData.append('tableSchema', JSON.stringify(request.tableSchema));
2483
+ }
2484
+ if (request.indexColumns && request.indexColumns.length > 0) {
2485
+ // indexColumns 作为数组传递,每个元素单独 append
2486
+ for (const col of request.indexColumns) {
2487
+ formData.append('indexColumns', col);
2488
+ }
2489
+ }
2490
+ }
2491
+ const raw = await this.http.postFormData(`${API_PREFIX$1}/knowledge/createKnowledgeBaseByDataSet`, formData);
2492
+ if (!raw.success) {
2493
+ return {
2494
+ success: false,
2495
+ errorCode: raw.errorCode,
2496
+ errorMsg: raw.errorMsg,
2497
+ };
2498
+ }
2499
+ return {
2500
+ success: true,
2501
+ data: raw.data,
2502
+ };
2503
+ }
2504
+ // ============================================================
2505
+ // 更新知识库
2506
+ // ============================================================
2507
+ /**
2508
+ * 更新知识库
2509
+ *
2510
+ * 通过上传文件更新已有的知识库文档,支持覆盖更新和增量更新。
2511
+ *
2512
+ * @param request - 更新知识库请求
2513
+ * - `file`(必填):上传的文件(如 .xlsx、.pdf 等)
2514
+ * - `type`(必填):数据类型,`STRUCTURED`(结构化)或其他类型
2515
+ * - `documentId`(必填):要更新的文档 ID(从创建响应中获取)
2516
+ * - `tableSchema`(可选):表格 Schema 定义,结构化数据需要
2517
+ * - `updateMode`(可选):更新模式,`OVERWRITE`(覆盖更新)或 `UPSERT`(增量更新,默认)
2518
+ * @returns 更新知识库结果
2519
+ *
2520
+ * @example
2521
+ * ```ts
2522
+ * // 覆盖更新知识库
2523
+ * const res = await client.updateKnowledgeBase({
2524
+ * file: newFile,
2525
+ * type: 'STRUCTURED',
2526
+ * documentId: '20251128999def29600J6WS04301922',
2527
+ * tableSchema: {
2528
+ * columns: [
2529
+ * { name: 'id', dataType: 'STRING', required: true, order: 1 },
2530
+ * { name: 'message_id', dataType: 'STRING', required: false, order: 2 },
2531
+ * { name: 'content', dataType: 'STRING', required: false, order: 3 }
2532
+ * ],
2533
+ * name: '消息记录'
2534
+ * },
2535
+ * updateMode: 'OVERWRITE'
2536
+ * });
2537
+ *
2538
+ * if (res.success) {
2539
+ * console.log(`更新成功,文档 ID: ${res.data?.documentId}`);
2540
+ * } else {
2541
+ * console.error(`更新失败: ${res.errorMsg}`);
2542
+ * }
2543
+ * ```
2544
+ */
2545
+ async updateKnowledgeBase(request) {
2546
+ // 构建 FormData
2547
+ const formData = new FormData();
2548
+ formData.append('file', request.file);
2549
+ formData.append('type', request.type);
2550
+ formData.append('documentId', request.documentId);
2551
+ // 可选:更新模式
2552
+ if (request.updateMode) {
2553
+ formData.append('updateMode', request.updateMode);
2554
+ }
2555
+ // 可选:tableSchema(结构化数据需要)
2556
+ if (request.tableSchema) {
2557
+ formData.append('tableSchema', JSON.stringify(request.tableSchema));
2558
+ }
2559
+ const raw = await this.http.postFormData(`${API_PREFIX$1}/knowledge/updateDocument`, formData);
2560
+ if (!raw.success) {
2561
+ return {
2562
+ success: false,
2563
+ errorCode: raw.errorCode,
2564
+ errorMsg: raw.errorMsg,
2565
+ };
2566
+ }
2567
+ return {
2568
+ success: true,
2569
+ data: raw.data,
2570
+ };
2571
+ }
2572
+ // ============================================================
2573
+ // 查询知识库列表
2574
+ // ============================================================
2575
+ /**
2576
+ * 查询知识库列表
2577
+ *
2578
+ * 通过名称模糊查询知识库,返回知识库 ID 和基本信息。
2579
+ *
2580
+ * @param request - 查询请求
2581
+ * - `name`(可选):知识库名称(支持模糊查询)
2582
+ * - `pageNo`(可选):页码,从 1 开始,默认 1
2583
+ * - `pageSize`(可选):每页条数,默认 10
2584
+ * @returns 知识库列表
2585
+ *
2586
+ * @example
2587
+ * ```ts
2588
+ * // 查询知识库列表
2589
+ * const res = await client.queryDatasetsList({
2590
+ * name: '用户知识库'
2591
+ * });
2592
+ *
2593
+ * if (res.success) {
2594
+ * for (const item of res.data?.datasets ?? []) {
2595
+ * console.log(`ID: ${item.datasetId}, 名称: ${item.name}`);
2596
+ * }
2597
+ * } else {
2598
+ * console.error(`查询失败: ${res.errorMsg}`);
2599
+ * }
2600
+ * ```
2601
+ */
2602
+ async queryDatasetsList(request = {}) {
2603
+ // 构建查询参数
2604
+ const queryParams = {};
2605
+ if (request.name) {
2606
+ queryParams.name = request.name;
2607
+ }
2608
+ if (request.pageNo !== undefined) {
2609
+ queryParams.pageNo = String(request.pageNo);
2610
+ }
2611
+ if (request.pageSize !== undefined) {
2612
+ queryParams.pageSize = String(request.pageSize);
2613
+ }
2614
+ // 查询知识库列表接口使用不同的域名 api.tbox.cn
2615
+ const raw = await this.http.get(`https://api.tbox.cn/api/datasets/queryDatasetsList`, queryParams);
2616
+ if (!raw.success) {
2617
+ return {
2618
+ success: false,
2619
+ errorCode: raw.errorCode,
2620
+ errorMsg: raw.errorMsg,
2621
+ };
2622
+ }
2623
+ return {
2624
+ success: true,
2625
+ data: raw.data,
2626
+ };
2627
+ }
2628
+ // ============================================================
2629
+ // 检索知识库
2630
+ // ============================================================
2631
+ /**
2632
+ * 检索知识库
2633
+ *
2634
+ * 通过自然语言查询知识库内容,返回相关度最高的内容片段。
2635
+ * 支持两种方式获取知识库:
2636
+ * 1. 直接传入 datasetId
2637
+ * 2. 传入 name,SDK 会自动查询获取 datasetId
2638
+ *
2639
+ * @param request - 检索知识库请求
2640
+ * - `name`(可选):知识库名称,SDK 会自动查询获取 datasetId
2641
+ * - `datasetId`(可选):知识库 ID(优先使用)
2642
+ * - `query`(必填):查询内容(自然语言描述)
2643
+ * @returns 检索结果
2644
+ *
2645
+ * @example
2646
+ * ```ts
2647
+ * // 方式 1: 通过名称自动查询
2648
+ * const res = await client.retrieve({
2649
+ * name: '用户知识库',
2650
+ * query: '消防安全管理制度'
2651
+ * });
2652
+ *
2653
+ * // 方式 2: 直接传入 datasetId
2654
+ * const res = await client.retrieve({
2655
+ * datasetId: '20251024999def29600U6J302721040',
2656
+ * query: '消防安全管理制度'
2657
+ * });
2658
+ *
2659
+ * if (res.success) {
2660
+ * for (const item of res.data?.data ?? []) {
2661
+ * console.log(`文件: ${item.originFileName}`);
2662
+ * console.log(`内容: ${item.content}`);
2663
+ * console.log(`相关度: ${item.score}`);
2664
+ * }
2665
+ * } else {
2666
+ * console.error(`检索失败: ${res.errorMsg}`);
2667
+ * }
2668
+ * ```
2669
+ */
2670
+ async retrieve(request) {
2671
+ let datasetId = request.datasetId;
2672
+ // 如果没有直接传入 datasetId,通过 name 查询
2673
+ if (!datasetId && request.name) {
2674
+ const queryRes = await this.queryDatasetsList({ name: request.name });
2675
+ if (!queryRes.success) {
2676
+ return {
2677
+ success: false,
2678
+ errorCode: queryRes.errorCode,
2679
+ errorMsg: queryRes.errorMsg,
2680
+ };
2681
+ }
2682
+ const datasets = queryRes.data?.datasets ?? [];
2683
+ if (datasets.length === 0) {
2684
+ return {
2685
+ success: false,
2686
+ errorCode: 'KNOWLEDGE_NOT_FOUND',
2687
+ errorMsg: `未找到名称为 "${request.name}" 的知识库`,
2688
+ };
2689
+ }
2690
+ datasetId = datasets[0].datasetId;
2691
+ }
2692
+ if (!datasetId) {
2693
+ return {
2694
+ success: false,
2695
+ errorCode: 'INVALID_REQUEST',
2696
+ errorMsg: '必须提供datasetId 参数',
2697
+ };
2698
+ }
2699
+ // 检索接口使用不同的域名 api.tbox.cn
2700
+ const raw = await this.http.post(`https://api.tbox.cn/api/datasets/retrieve`, {
2701
+ datasetId,
2702
+ query: request.query,
2703
+ });
2704
+ if (!raw.success) {
2705
+ return {
2706
+ success: false,
2707
+ errorCode: raw.errorCode,
2708
+ errorMsg: raw.errorMsg,
2709
+ };
2710
+ }
2711
+ return {
2712
+ success: true,
2713
+ data: raw.data,
2714
+ };
2715
+ }
2716
+ }
2717
+
2718
+ /**
2719
+ * LLM SDK - LlmClient 核心类
2720
+ *
2721
+ * 封装大语言模型调用功能,提供 OpenAI 兼容的 Chat Completions 接口。
2722
+ */
2723
+ /** API 路径前缀 */
2724
+ const API_PREFIX = '/compat/openai/v1';
2725
+ /**
2726
+ * LLM 客户端
2727
+ *
2728
+ * 提供大语言模型调用能力,支持流式和非流式输出。
2729
+ *
2730
+ * @example
2731
+ * ```ts
2732
+ * import { LlmClient } from '@tbox/plugin-sdk';
2733
+ *
2734
+ * const client = new LlmClient({ apiKey: 'your-api-key' });
2735
+ *
2736
+ * // 流式调用
2737
+ * const stream = await client.chatCompletions({
2738
+ * model: 'kimi-k2.5',
2739
+ * messages: [{ role: 'user', content: '你好' }],
2740
+ * stream: true
2741
+ * });
2742
+ *
2743
+ * for await (const chunk of stream) {
2744
+ * process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
2745
+ * }
2746
+ * ```
2747
+ */
2748
+ class LlmClient {
2749
+ constructor(config) {
2750
+ this.http = new HttpClient({
2751
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
2752
+ apiKey: config.apiKey,
2753
+ timeout: config.timeout,
2754
+ });
2755
+ }
2756
+ // ============================================================
2757
+ // Chat Completions
2758
+ // ============================================================
2759
+ /**
2760
+ * Chat Completions 对话补全
2761
+ *
2762
+ * 支持流式和非流式两种模式:
2763
+ * - 流式(stream=true):返回 AsyncIterable<ChatCompletionChunk>
2764
+ * - 非流式(stream=false):返回 ChatCompletionResponse
2765
+ *
2766
+ * @param request - 对话补全请求
2767
+ * - `model`(必填):模型名称
2768
+ * - `messages`(必填):对话消息列表
2769
+ * - `stream`(可选):是否流式输出,默认 false
2770
+ * - `temperature`(可选):温度参数,范围 0~2
2771
+ * - `top_p`(可选):核采样参数,范围 0~1,推荐 0.95
2772
+ * - `max_tokens`(可选):最大输出 token 数,推荐 8000
2773
+ * - `stream_options`(可选):流式选项,`{ include_usage: true }` 可在末尾返回 token 用量
2774
+ * @returns 流式返回 AsyncIterable,非流式返回完整响应
2775
+ *
2776
+ * @example
2777
+ * ```ts
2778
+ * // 流式调用
2779
+ * const stream = await client.chatCompletions({
2780
+ * model: 'kimi-k2.5',
2781
+ * messages: [
2782
+ * { role: 'system', content: '你是一个有帮助的助手' },
2783
+ * { role: 'user', content: '你好,请介绍一下你自己' }
2784
+ * ],
2785
+ * stream: true,
2786
+ * stream_options: { include_usage: true }
2787
+ * });
2788
+ *
2789
+ * for await (const chunk of stream) {
2790
+ * const content = chunk.choices[0]?.delta?.content;
2791
+ * if (content) {
2792
+ * process.stdout.write(content);
2793
+ * }
2794
+ * // 检查是否结束
2795
+ * if (chunk.choices[0]?.finish_reason === 'stop') {
2796
+ * console.log('\n对话结束');
2797
+ * }
2798
+ * // 获取 token 用量(需要 stream_options.include_usage=true)
2799
+ * if (chunk.usage) {
2800
+ * console.log(`Token 用量: ${chunk.usage.total_tokens}`);
2801
+ * }
2802
+ * }
2803
+ * ```
2804
+ *
2805
+ * @example
2806
+ * ```ts
2807
+ * // 非流式调用
2808
+ * const response = await client.chatCompletions({
2809
+ * model: 'kimi-k2.5',
2810
+ * messages: [
2811
+ * { role: 'user', content: '你好' }
2812
+ * ],
2813
+ * stream: false
2814
+ * });
2815
+ *
2816
+ * console.log(response.choices[0]?.message?.content);
2817
+ * ```
2818
+ */
2819
+ async chatCompletions(request) {
2820
+ // 流式模式
2821
+ if (request.stream) {
2822
+ return this.streamChatCompletions(request);
2823
+ }
2824
+ // 非流式模式
2825
+ const response = await this.http.post(`${API_PREFIX}/chat/completions`, request);
2826
+ if (!response.success) {
2827
+ throw new Error(response.errorMsg ?? 'Chat completion failed');
2828
+ }
2829
+ return response.data;
2830
+ }
2831
+ /**
2832
+ * 流式调用 Chat Completions
2833
+ */
2834
+ async *streamChatCompletions(request) {
2835
+ const url = `${this.http['baseUrl']}${API_PREFIX}/chat/completions`;
2836
+ const response = await fetch(url, {
2837
+ method: 'POST',
2838
+ headers: {
2839
+ 'Content-Type': 'application/json;charset=UTF-8',
2840
+ Authorization: this.http['apiKey'],
2841
+ },
2842
+ body: JSON.stringify(request),
2843
+ });
2844
+ if (!response.ok) {
2845
+ throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
2846
+ }
2847
+ const reader = response.body?.getReader();
2848
+ if (!reader) {
2849
+ throw new Error('No response body');
2850
+ }
2851
+ const decoder = new TextDecoder('utf-8');
2852
+ let buffer = '';
2853
+ try {
2854
+ while (true) {
2855
+ const { done, value } = await reader.read();
2856
+ if (done)
2857
+ break;
2858
+ buffer += decoder.decode(value, { stream: true });
2859
+ const lines = buffer.split('\n');
2860
+ buffer = lines.pop() ?? '';
2861
+ for (const line of lines) {
2862
+ const trimmed = line.trim();
2863
+ if (!trimmed || !trimmed.startsWith('data:'))
2864
+ continue;
2865
+ // 支持 "data:" 和 "data: " 两种格式
2866
+ const data = trimmed.startsWith('data: ')
2867
+ ? trimmed.slice(6)
2868
+ : trimmed.slice(5);
2869
+ if (data === '[DONE]')
2870
+ return;
2871
+ try {
2872
+ const chunk = JSON.parse(data);
2873
+ yield chunk;
2874
+ }
2875
+ catch {
2876
+ // 忽略解析错误
2877
+ }
2878
+ }
2879
+ }
2880
+ // 处理剩余 buffer
2881
+ if (buffer.trim()) {
2882
+ const trimmed = buffer.trim();
2883
+ if (trimmed.startsWith('data:')) {
2884
+ const data = trimmed.startsWith('data: ')
2885
+ ? trimmed.slice(6)
2886
+ : trimmed.slice(5);
2887
+ if (data !== '[DONE]') {
2888
+ try {
2889
+ const chunk = JSON.parse(data);
2890
+ yield chunk;
2891
+ }
2892
+ catch {
2893
+ // 忽略解析错误
2894
+ }
2895
+ }
2896
+ }
2897
+ }
2898
+ }
2899
+ finally {
2900
+ reader.releaseLock();
2901
+ }
2902
+ }
2903
+ /**
2904
+ * 简化的对话方法
2905
+ *
2906
+ * 快速发起一次对话,自动处理流式输出。
2907
+ *
2908
+ * @param model - 模型名称
2909
+ * @param messages - 对话消息列表
2910
+ * @param options - 可选参数
2911
+ * @returns 完整的对话内容(对于推理模型,返回推理内容 + 正式内容)
2912
+ *
2913
+ * @example
2914
+ * ```ts
2915
+ * const answer = await client.chat('kimi-k2.5', [
2916
+ * { role: 'user', content: '你好' }
2917
+ * ]);
2918
+ * console.log(answer);
2919
+ * ```
2920
+ */
2921
+ async chat(model, messages, options) {
2922
+ const stream = await this.chatCompletions({
2923
+ model,
2924
+ messages,
2925
+ stream: true,
2926
+ stream_options: { include_usage: true },
2927
+ ...options,
2928
+ });
2929
+ let fullContent = '';
2930
+ let reasoningContent = '';
2931
+ for await (const chunk of stream) {
2932
+ const content = chunk.choices?.[0]?.delta?.content;
2933
+ const reasoning = chunk.choices?.[0]?.delta?.reasoning_content;
2934
+ if (content) {
2935
+ fullContent += content;
2936
+ }
2937
+ if (reasoning) {
2938
+ reasoningContent += reasoning;
2939
+ }
2940
+ }
2941
+ // 如果有推理内容但没有正式内容,返回推理内容
2942
+ // 这对于 glm-5v-turbo 等推理模型很重要
2943
+ if (!fullContent && reasoningContent) {
2944
+ return reasoningContent;
2945
+ }
2946
+ return fullContent;
2947
+ }
2948
+ }
2949
+
2950
+ export { APIClient, APIError, APIResource, AmapClient, AuthenticationError, BadRequestError, Documents, ForbiddenError, GatewayError, InternalServerError, Knowledge, KnowledgeClient$1 as KnowledgeClient, KnowledgeScopedClient, KnowledgeClient as LegacyKnowledgeClient, LlmClient, NetworkError, NotFoundError, ParseError, RateLimitError, SDK_VERSION, SchemaRegistry, TBOX_BASE_URL, TboxAPI, TboxAppClient, TboxConversationClient, TboxError, TboxPluginClient, TimeoutError, knowledgeSchema };
2951
+ //# sourceMappingURL=index.esm.js.map