@cloudbase/node-sdk 3.15.0 → 3.16.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,58 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.createAI = void 0;
27
+ const ai_1 = require("@cloudbase/ai");
28
+ const request_adapter_1 = require("./request-adapter");
29
+ const tcbopenapiendpoint_1 = require("../utils/tcbopenapiendpoint");
30
+ const symbol_1 = require("../const/symbol");
31
+ const openapicommonrequester = __importStar(require("../utils/tcbopenapicommonrequester"));
32
+ const app_1 = require("@cloudbase/app");
33
+ /**
34
+ * 创建 AI 实例
35
+ * @param cloudbase CloudBase 实例
36
+ * @returns AI 实例
37
+ */
38
+ function createAI(cloudbase) {
39
+ const config = cloudbase.config;
40
+ // 获取环境 ID
41
+ const envId = config.envName === symbol_1.SYMBOL_CURRENT_ENV ? openapicommonrequester.getEnvIdFromContext() : config.envName;
42
+ // 构建 AI 基础 URL
43
+ const baseUrl = (0, tcbopenapiendpoint_1.buildCommonOpenApiUrlWithPath)({
44
+ serviceUrl: config.serviceUrl,
45
+ envId,
46
+ path: '/v1',
47
+ region: config.region
48
+ });
49
+ // 创建请求适配器
50
+ const requestAdapter = new request_adapter_1.AIRequestAdapter(config, async () => {
51
+ const credential = await cloudbase.auth().getClientCredential();
52
+ return credential.access_token;
53
+ });
54
+ // 创建 AI 实例
55
+ const ai = new ai_1.AI(requestAdapter, baseUrl, { t: (s) => s, lang: app_1.LANGS.ZH, LANG_HEADER_KEY: 'Accept-Language' });
56
+ return ai;
57
+ }
58
+ exports.createAI = createAI;
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.AIRequestAdapter = void 0;
27
+ const openapicommonrequester = __importStar(require("../utils/tcbopenapicommonrequester"));
28
+ const web_streams_polyfill_1 = require("web-streams-polyfill");
29
+ const utils_1 = require("../utils/utils");
30
+ class AIRequestAdapter {
31
+ constructor(config, getAccessToken) {
32
+ this.config = config;
33
+ this.getAccessToken = getAccessToken;
34
+ }
35
+ async fetch(options) {
36
+ var _a, _b;
37
+ const { url, stream = false, timeout, method = 'POST', headers, body } = options;
38
+ const headersObj = {};
39
+ if (isHeaders(headers)) {
40
+ headers.forEach((value, key) => {
41
+ headersObj[key] = value;
42
+ });
43
+ }
44
+ else if (Array.isArray(headers)) {
45
+ headers.forEach(([k, v]) => (headersObj[k] = v));
46
+ }
47
+ else {
48
+ Object.assign(headersObj, headers);
49
+ }
50
+ let parsedBody;
51
+ if (typeof body === 'string') {
52
+ try {
53
+ parsedBody = JSON.parse(body);
54
+ }
55
+ catch (_c) {
56
+ parsedBody = body;
57
+ }
58
+ }
59
+ else {
60
+ parsedBody = body;
61
+ }
62
+ const token = await this.getAccessToken();
63
+ const result = await openapicommonrequester.request({
64
+ config: this.config,
65
+ data: parsedBody,
66
+ method: (method === null || method === void 0 ? void 0 : method.toUpperCase()) || 'POST',
67
+ url,
68
+ headers: Object.assign({ 'Content-Type': 'application/json' }, headersObj),
69
+ token,
70
+ opts: {
71
+ timeout: timeout || this.config.timeout,
72
+ stream
73
+ }
74
+ });
75
+ const { body: bodyData, headers: responseHeaders, statusCode } = result;
76
+ if (statusCode < 200 || statusCode >= 300) {
77
+ let errorMessage = `Request failed with status code ${statusCode}`;
78
+ let errorCode = `${statusCode}`;
79
+ let requestId = '';
80
+ let errorBody = null;
81
+ if (typeof bodyData === 'string') {
82
+ errorBody = bodyData;
83
+ }
84
+ else if (Buffer.isBuffer(bodyData)) {
85
+ errorBody = bodyData.toString('utf-8');
86
+ }
87
+ else if (bodyData && typeof bodyData === 'object' && typeof bodyData.on === 'function') {
88
+ errorBody = await readStreamToString(bodyData);
89
+ }
90
+ if (errorBody) {
91
+ try {
92
+ const errorData = JSON.parse(errorBody);
93
+ if ((_a = errorData.error) === null || _a === void 0 ? void 0 : _a.message) {
94
+ errorMessage = errorData.error.message;
95
+ }
96
+ else if (errorData.message) {
97
+ errorMessage = errorData.message;
98
+ }
99
+ if ((_b = errorData.error) === null || _b === void 0 ? void 0 : _b.code) {
100
+ errorCode = errorData.error.code;
101
+ }
102
+ else if (errorData.code) {
103
+ errorCode = errorData.code;
104
+ }
105
+ if (errorData.requestId) {
106
+ requestId = errorData.requestId;
107
+ }
108
+ }
109
+ catch (_d) {
110
+ errorMessage = errorBody || errorMessage;
111
+ }
112
+ }
113
+ // 从响应头中获取 requestId
114
+ if (!requestId && responseHeaders) {
115
+ const headerRequestId = responseHeaders['x-cloudbase-request-id'] || responseHeaders['x-request-id'] || '';
116
+ requestId = Array.isArray(headerRequestId) ? headerRequestId[0] : headerRequestId;
117
+ }
118
+ throw (0, utils_1.E)({
119
+ code: errorCode,
120
+ message: errorMessage,
121
+ requestId
122
+ });
123
+ }
124
+ if (stream) {
125
+ // 对于流式响应,将 Node.js 原生流转换为 Web ReadableStream
126
+ let readableStream;
127
+ if (bodyData && typeof bodyData === 'object' && 'on' in bodyData && typeof bodyData.on === 'function') {
128
+ const nodeStream = bodyData;
129
+ // Node 12 兼容: 使用标志位追踪 stream 状态,避免重复 close 导致异常
130
+ let streamClosed = false;
131
+ readableStream = new web_streams_polyfill_1.ReadableStream({
132
+ start(controller) {
133
+ nodeStream.on('data', (chunk) => {
134
+ if (streamClosed)
135
+ return;
136
+ controller.enqueue(new Uint8Array(chunk));
137
+ });
138
+ nodeStream.on('end', () => {
139
+ if (streamClosed)
140
+ return;
141
+ streamClosed = true;
142
+ controller.close();
143
+ });
144
+ nodeStream.on('error', (err) => {
145
+ if (streamClosed)
146
+ return;
147
+ streamClosed = true;
148
+ controller.error(err);
149
+ });
150
+ },
151
+ cancel() {
152
+ streamClosed = true;
153
+ nodeStream.destroy();
154
+ }
155
+ });
156
+ }
157
+ else if (bodyData instanceof Buffer) {
158
+ readableStream = new web_streams_polyfill_1.ReadableStream({
159
+ start(controller) {
160
+ controller.enqueue(new Uint8Array(bodyData));
161
+ controller.close();
162
+ }
163
+ });
164
+ }
165
+ else if (typeof bodyData === 'string') {
166
+ const encoder = new TextEncoder();
167
+ readableStream = new web_streams_polyfill_1.ReadableStream({
168
+ start(controller) {
169
+ controller.enqueue(encoder.encode(bodyData));
170
+ controller.close();
171
+ }
172
+ });
173
+ }
174
+ else {
175
+ readableStream = new web_streams_polyfill_1.ReadableStream({
176
+ start(controller) {
177
+ controller.close();
178
+ }
179
+ });
180
+ }
181
+ return {
182
+ data: readableStream,
183
+ statusCode,
184
+ header: responseHeaders
185
+ };
186
+ }
187
+ let responseData;
188
+ if (typeof bodyData === 'string') {
189
+ try {
190
+ responseData = JSON.parse(bodyData);
191
+ }
192
+ catch (_e) {
193
+ responseData = bodyData;
194
+ }
195
+ }
196
+ else if (bodyData instanceof Buffer) {
197
+ const bodyString = bodyData.toString('utf-8');
198
+ try {
199
+ responseData = JSON.parse(bodyString);
200
+ }
201
+ catch (_f) {
202
+ responseData = bodyString;
203
+ }
204
+ }
205
+ else {
206
+ responseData = bodyData;
207
+ }
208
+ return {
209
+ data: Promise.resolve(responseData),
210
+ statusCode,
211
+ header: responseHeaders
212
+ };
213
+ }
214
+ /**
215
+ * post 方法 - AI 模块可能不使用,但需要实现接口
216
+ */
217
+ async post() {
218
+ throw new Error('post method is not supported in AI module');
219
+ }
220
+ /**
221
+ * upload 方法 - AI 模块可能不使用,但需要实现接口
222
+ */
223
+ async upload() {
224
+ throw new Error('upload method is not supported in AI module');
225
+ }
226
+ /**
227
+ * download 方法 - AI 模块可能不使用,但需要实现接口
228
+ */
229
+ async download() {
230
+ throw new Error('download method is not supported in AI module');
231
+ }
232
+ }
233
+ exports.AIRequestAdapter = AIRequestAdapter;
234
+ function isHeaders(h) {
235
+ try {
236
+ // Node.js 低版本可能没有 Headers
237
+ return h instanceof Headers;
238
+ }
239
+ catch (_) {
240
+ return false;
241
+ }
242
+ }
243
+ /**
244
+ * 从 Node.js 流中读取完整内容为字符串
245
+ */
246
+ async function readStreamToString(stream) {
247
+ return await new Promise((resolve, reject) => {
248
+ const chunks = [];
249
+ stream.on('data', (chunk) => {
250
+ chunks.push(chunk);
251
+ });
252
+ stream.on('end', () => {
253
+ resolve(Buffer.concat(chunks).toString('utf-8'));
254
+ });
255
+ stream.on('error', (err) => {
256
+ reject(err);
257
+ });
258
+ });
259
+ }
package/dist/cloudbase.js CHANGED
@@ -47,6 +47,7 @@ const database_1 = require("./database");
47
47
  const storage_1 = require("./storage");
48
48
  const wx_1 = require("./wx");
49
49
  const analytics_1 = require("./analytics");
50
+ const ai_1 = require("./ai");
50
51
  const logger_1 = require("./logger");
51
52
  const code_1 = require("./const/code");
52
53
  const utils = __importStar(require("./utils/utils"));
@@ -75,8 +76,7 @@ class CloudBase {
75
76
  /* eslint-disable-next-line */
76
77
  (0, cloudplatform_1.preflightRuntimeCloudPlatform)();
77
78
  const { debug, secretId, secretKey, sessionToken, env, timeout, headers = {} } = config, restConfig = __rest(config, ["debug", "secretId", "secretKey", "sessionToken", "env", "timeout", "headers"]);
78
- if (('secretId' in config && !('secretKey' in config))
79
- || (!('secretId' in config) && 'secretKey' in config)) {
79
+ if (('secretId' in config && !('secretKey' in config)) || (!('secretId' in config) && 'secretKey' in config)) {
80
80
  throw utils.E(Object.assign(Object.assign({}, code_1.ERROR.INVALID_PARAM), { message: 'secretId and secretKey must be a pair' }));
81
81
  }
82
82
  const newConfig = Object.assign(Object.assign({}, restConfig), { debug: !!debug, secretId,
@@ -115,14 +115,70 @@ class CloudBase {
115
115
  token: (await this.auth().getClientCredential()).access_token
116
116
  });
117
117
  return result.body;
118
- }, (0, tcbopenapiendpoint_1.buildCommonOpenApiUrlWithPath)({ serviceUrl: this.config.serviceUrl, envId, path: '/v1/model' }), {
119
- sqlBaseUrl: (0, tcbopenapiendpoint_1.buildCommonOpenApiUrlWithPath)({ serviceUrl: this.config.serviceUrl, envId, path: '/v1/sql' })
118
+ }, (0, tcbopenapiendpoint_1.buildCommonOpenApiUrlWithPath)({ serviceUrl: this.config.serviceUrl, envId, path: '/v1/model', region: this.config.region }), {
119
+ sqlBaseUrl: (0, tcbopenapiendpoint_1.buildCommonOpenApiUrlWithPath)({ serviceUrl: this.config.serviceUrl, envId, path: '/v1/sql', region: this.config.region })
120
120
  });
121
121
  this.models = httpClient;
122
122
  }
123
123
  catch (e) {
124
124
  // ignore
125
125
  }
126
+ try {
127
+ const getEntity = (options) => {
128
+ const envId = this.config.envName === symbol_1.SYMBOL_CURRENT_ENV
129
+ ? openapicommonrequester.getEnvIdFromContext()
130
+ : this.config.envName;
131
+ const { instance = 'default', database = envId } = options || {};
132
+ const mysqlClient = wx_cloud_client_sdk_1.default.generateMySQLClient(this, {
133
+ mysqlBaseUrl: (0, tcbopenapiendpoint_1.buildCommonOpenApiUrlWithPath)({
134
+ serviceUrl: this.config.serviceUrl,
135
+ envId,
136
+ path: '/v1/rdb/rest',
137
+ region: this.config.region
138
+ }),
139
+ fetch: async (url, options) => {
140
+ var _a;
141
+ let headers = {};
142
+ if (options.headers instanceof Headers) {
143
+ options.headers.forEach((value, key) => {
144
+ headers[key] = value;
145
+ });
146
+ }
147
+ else {
148
+ headers = options.headers || {};
149
+ }
150
+ const result = await openapicommonrequester.request({
151
+ config: this.config,
152
+ data: safeParseJSON(options.body),
153
+ method: (_a = options.method) === null || _a === void 0 ? void 0 : _a.toUpperCase(),
154
+ url: url instanceof URL ? url.href : String(url),
155
+ headers: Object.assign({ 'Content-Type': 'application/json' }, headersInitToRecord(Object.assign({ 'X-Db-Instance': instance, 'Accept-Profile': database, 'Content-Profile': database }, headers))),
156
+ token: (await this.auth().getClientCredential()).access_token
157
+ });
158
+ const data = result.body;
159
+ const res = {
160
+ ok: (result === null || result === void 0 ? void 0 : result.statusCode) >= 200 && (result === null || result === void 0 ? void 0 : result.statusCode) < 300,
161
+ status: (result === null || result === void 0 ? void 0 : result.statusCode) || 200,
162
+ statusText: (result === null || result === void 0 ? void 0 : result.statusMessage) || 'OK',
163
+ json: async () => await Promise.resolve(data || {}),
164
+ text: async () => await Promise.resolve(typeof data === 'string' ? data : JSON.stringify(data || {})),
165
+ headers: new Headers(incomingHttpHeadersToHeadersInit((result === null || result === void 0 ? void 0 : result.headers) || {}))
166
+ };
167
+ return res;
168
+ }
169
+ });
170
+ return mysqlClient;
171
+ };
172
+ this.mysql = (options) => {
173
+ return getEntity(options)(options);
174
+ };
175
+ this.rdb = (options) => {
176
+ return getEntity(options)(options);
177
+ };
178
+ }
179
+ catch (e) {
180
+ // ignore
181
+ }
126
182
  }
127
183
  logger() {
128
184
  if (!this.clsLogger) {
@@ -136,6 +192,12 @@ class CloudBase {
136
192
  database(dbConfig = {}) {
137
193
  return (0, database_1.newDb)(this, dbConfig);
138
194
  }
195
+ ai() {
196
+ if (!this.aiInstance) {
197
+ this.aiInstance = (0, ai_1.createAI)(this);
198
+ }
199
+ return this.aiInstance;
200
+ }
139
201
  async callFunction(callFunctionOptions, opts) {
140
202
  return await (0, functions_1.callFunction)(this, callFunctionOptions, opts);
141
203
  }
@@ -240,7 +302,7 @@ function headersInitToRecord(headers) {
240
302
  });
241
303
  }
242
304
  else {
243
- Object.keys(headers).forEach(key => {
305
+ Object.keys(headers).forEach((key) => {
244
306
  ret[key] = headers[key];
245
307
  });
246
308
  }
@@ -254,3 +316,20 @@ function safeParseJSON(x) {
254
316
  return x;
255
317
  }
256
318
  }
319
+ function incomingHttpHeadersToHeadersInit(headers) {
320
+ const result = [];
321
+ for (const [key, value] of Object.entries(headers)) {
322
+ if (value === undefined) {
323
+ continue;
324
+ }
325
+ if (Array.isArray(value)) {
326
+ for (const v of value) {
327
+ result.push([key, v]);
328
+ }
329
+ }
330
+ else {
331
+ result.push([key, value]);
332
+ }
333
+ }
334
+ return result;
335
+ }
@@ -113,6 +113,10 @@ async function onResponse(res, { encoding, type = 'json' }) {
113
113
  if (type === 'stream') {
114
114
  return await Promise.resolve(undefined);
115
115
  }
116
+ // rawStream: 返回原始的 Node.js 流,用于真正的流式处理
117
+ if (type === 'rawStream') {
118
+ return await Promise.resolve(res);
119
+ }
116
120
  if (encoding) {
117
121
  res.setEncoding(encoding);
118
122
  }
@@ -80,7 +80,7 @@ class TcbOpenApiHttpCommonRequester {
80
80
  });
81
81
  }
82
82
  makeReqOpts() {
83
- var _a;
83
+ var _a, _b;
84
84
  const config = this.config;
85
85
  const args = this.args;
86
86
  const envId = args.config.envName === symbol_1.SYMBOL_CURRENT_ENV
@@ -88,7 +88,8 @@ class TcbOpenApiHttpCommonRequester {
88
88
  : args.config.envName;
89
89
  const url = args.url || (0, tcbopenapiendpoint_1.buildCommonOpenApiUrlWithPath)({
90
90
  envId,
91
- path: args.path
91
+ path: args.path,
92
+ region: config.region
92
93
  });
93
94
  const timeout = ((_a = this.args.opts) === null || _a === void 0 ? void 0 : _a.timeout) || this.config.timeout || this.defaultTimeout;
94
95
  const opts = {
@@ -107,14 +108,20 @@ class TcbOpenApiHttpCommonRequester {
107
108
  opts.keepalive = typeof config.keepalive === 'boolean' && config.keepalive;
108
109
  }
109
110
  if (args.data) {
110
- if (['post', 'put'].includes(args.method.toLowerCase())) {
111
+ if (['post', 'put', 'patch', 'delete'].includes(args.method.toLowerCase())) {
111
112
  if (args.isFormData) {
112
113
  opts.formData = args.data;
113
114
  opts.encoding = null;
114
115
  }
115
116
  else {
116
117
  opts.body = args.data;
117
- opts.json = true;
118
+ if (((_b = args.opts) === null || _b === void 0 ? void 0 : _b.stream) !== true) {
119
+ opts.json = true;
120
+ }
121
+ else {
122
+ // 使用 rawStream 类型返回原始的 Node.js 流,实现真正的流式响应
123
+ opts.type = 'rawStream';
124
+ }
118
125
  }
119
126
  }
120
127
  else {
@@ -1,14 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildCommonOpenApiUrlWithPath = exports.buildUrl = void 0;
4
+ const ZONE_CHINA = ['ap-shanghai', 'ap-guangzhou', 'ap-shenzhen-fsi', 'ap-shanghai-fsi', 'ap-nanjing', 'ap-beijing', 'ap-chengdu', 'ap-chongqing', 'ap-hongkong'];
4
5
  /* eslint-disable complexity */
5
6
  function buildUrl(options) {
6
- const endpoint = `https://${options.envId}.api.tcloudbasegateway.com/v1/cloudrun/${options.name}`;
7
+ const endpoint = `https://${getGatewayUrl(options)}/v1/cloudrun/${options.name}`;
7
8
  const path = options.path.startsWith('/') ? options.path : `/${options.path}`;
8
9
  return `${endpoint}${path}`;
9
10
  }
10
11
  exports.buildUrl = buildUrl;
11
12
  function buildCommonOpenApiUrlWithPath(options) {
12
- return `${options.protocol || 'https'}://${options.serviceUrl || `${options.envId}.api.tcloudbasegateway.com`}${options.path}`;
13
+ return `${options.protocol || 'https'}://${options.serviceUrl || getGatewayUrl(options)}${options.path}`;
13
14
  }
14
15
  exports.buildCommonOpenApiUrlWithPath = buildCommonOpenApiUrlWithPath;
16
+ function getGatewayUrl(options) {
17
+ const region = options.region || 'ap-shanghai';
18
+ let baseUrl = `${options.envId}.api.tcloudbasegateway.com`;
19
+ if (!ZONE_CHINA.includes(region)) {
20
+ baseUrl = `${options.envId}.api.intl.tcloudbasegateway.com`;
21
+ }
22
+ return baseUrl;
23
+ }
@@ -88,7 +88,7 @@ class TcbOpenApiHttpRequester {
88
88
  : args.config.envName;
89
89
  const url = (0, tcbopenapiendpoint_1.buildUrl)({
90
90
  envId,
91
- // region: this.config.region,
91
+ region: this.config.region,
92
92
  // protocol: this.config.protocol || 'https',
93
93
  // serviceUrl: this.config.serviceUrl,
94
94
  // seqId: this.tracingInfo.seqId,
@@ -125,6 +125,7 @@ class TcbOpenApiHttpRequester {
125
125
  }
126
126
  else {
127
127
  /* istanbul ignore next */
128
+ // 这里 qs 参数暂时并没使用
128
129
  opts.qs = args.data;
129
130
  }
130
131
  }
@@ -168,13 +169,18 @@ class TcbOpenApiHttpRequester {
168
169
  requiredHeaders['X-TCB-Region'] = region;
169
170
  }
170
171
  requiredHeaders = Object.assign(Object.assign(Object.assign({}, config.headers), args.headers), requiredHeaders);
172
+ let params = args.data || '';
173
+ // GET 方法不支持传 BODY
174
+ if (method.toLowerCase() === 'get') {
175
+ params = '';
176
+ }
171
177
  // TODO: 升级SDK版本,否则没传 args.data 时会签名失败
172
178
  const { authorization, timestamp } = (0, signature_nodejs_1.sign)({
173
179
  secretId,
174
180
  secretKey,
175
181
  method,
176
182
  url,
177
- params: args.data || '',
183
+ params,
178
184
  headers: requiredHeaders,
179
185
  timestamp: second() - 1,
180
186
  withSignedParams: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/node-sdk",
3
- "version": "3.15.0",
3
+ "version": "3.16.0",
4
4
  "description": "tencent cloud base server sdk for node.js",
5
5
  "main": "dist/index.js",
6
6
  "typings": "types/index.d.ts",
@@ -41,8 +41,11 @@
41
41
  "prepare": "husky install"
42
42
  },
43
43
  "dependencies": {
44
+ "@cloudbase/ai": "^2.23.0",
45
+ "@cloudbase/app": "^2.23.0",
44
46
  "@cloudbase/database": "1.4.2",
45
47
  "@cloudbase/signature-nodejs": "2.2.0",
48
+ "@cloudbase/types": "^2.23.0",
46
49
  "@cloudbase/wx-cloud-client-sdk": "1.7.1",
47
50
  "agentkeepalive": "^4.3.0",
48
51
  "axios": "0.27.2",
@@ -51,6 +54,7 @@
51
54
  "https-proxy-agent": "^5.0.1",
52
55
  "jsonwebtoken": "^9.0.2",
53
56
  "retry": "^0.13.1",
57
+ "web-streams-polyfill": "^4.2.0",
54
58
  "xml2js": "^0.6.2"
55
59
  },
56
60
  "devDependencies": {
@@ -0,0 +1,38 @@
1
+ import { AI } from '@cloudbase/ai'
2
+ import { CloudBase } from '../cloudbase'
3
+ import { AIRequestAdapter } from './request-adapter'
4
+ import { buildCommonOpenApiUrlWithPath } from '../utils/tcbopenapiendpoint'
5
+ import { SYMBOL_CURRENT_ENV } from '../const/symbol'
6
+ import * as openapicommonrequester from '../utils/tcbopenapicommonrequester'
7
+ import { LANGS } from '@cloudbase/app'
8
+
9
+ /**
10
+ * 创建 AI 实例
11
+ * @param cloudbase CloudBase 实例
12
+ * @returns AI 实例
13
+ */
14
+ export function createAI(cloudbase: CloudBase): AI {
15
+ const config = cloudbase.config
16
+
17
+ // 获取环境 ID
18
+ const envId = config.envName === SYMBOL_CURRENT_ENV ? openapicommonrequester.getEnvIdFromContext() : (config.envName as string)
19
+
20
+ // 构建 AI 基础 URL
21
+ const baseUrl = buildCommonOpenApiUrlWithPath({
22
+ serviceUrl: config.serviceUrl,
23
+ envId,
24
+ path: '/v1',
25
+ region: config.region
26
+ })
27
+
28
+ // 创建请求适配器
29
+ const requestAdapter = new AIRequestAdapter(config, async () => {
30
+ const credential = await cloudbase.auth().getClientCredential()
31
+ return credential.access_token
32
+ })
33
+
34
+ // 创建 AI 实例
35
+ const ai = new AI(requestAdapter, baseUrl, { t: (s) => s, lang: LANGS.ZH, LANG_HEADER_KEY: 'Accept-Language' })
36
+
37
+ return ai
38
+ }
@@ -0,0 +1,235 @@
1
+ import { SDKRequestInterface, IFetchOptions, ResponseObject } from '@cloudbase/adapter-interface'
2
+ import { ICloudBaseConfig } from '../../types'
3
+ import * as openapicommonrequester from '../utils/tcbopenapicommonrequester'
4
+ import { ReadableStream } from 'web-streams-polyfill'
5
+ import { E } from '../utils/utils'
6
+
7
+ export class AIRequestAdapter implements SDKRequestInterface {
8
+ constructor(private readonly config: ICloudBaseConfig, private readonly getAccessToken: () => Promise<string>) {}
9
+
10
+ async fetch(options: IFetchOptions): Promise<ResponseObject> {
11
+ const { url, stream = false, timeout, method = 'POST', headers, body } = options
12
+
13
+ const headersObj: Record<string, string> = {}
14
+ if (isHeaders(headers)) {
15
+ headers.forEach((value, key) => {
16
+ headersObj[key] = value
17
+ })
18
+ } else if (Array.isArray(headers)) {
19
+ headers.forEach(([k, v]) => (headersObj[k] = v))
20
+ } else {
21
+ Object.assign(headersObj, headers)
22
+ }
23
+
24
+ let parsedBody: any
25
+ if (typeof body === 'string') {
26
+ try {
27
+ parsedBody = JSON.parse(body)
28
+ } catch {
29
+ parsedBody = body
30
+ }
31
+ } else {
32
+ parsedBody = body
33
+ }
34
+
35
+ const token = await this.getAccessToken()
36
+
37
+ const result = await openapicommonrequester.request({
38
+ config: this.config,
39
+ data: parsedBody,
40
+ method: method?.toUpperCase() || 'POST',
41
+ url,
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ ...headersObj
45
+ },
46
+ token,
47
+ opts: {
48
+ timeout: timeout || this.config.timeout,
49
+ stream
50
+ }
51
+ })
52
+
53
+ const { body: bodyData, headers: responseHeaders, statusCode } = result
54
+
55
+ if (statusCode < 200 || statusCode >= 300) {
56
+ let errorMessage = `Request failed with status code ${statusCode}`
57
+ let errorCode = `${statusCode}`
58
+ let requestId = ''
59
+ let errorBody: string | null = null
60
+
61
+ if (typeof bodyData === 'string') {
62
+ errorBody = bodyData
63
+ } else if (Buffer.isBuffer(bodyData)) {
64
+ errorBody = bodyData.toString('utf-8')
65
+ } else if (bodyData && typeof bodyData === 'object' && typeof (bodyData as any).on === 'function') {
66
+ errorBody = await readStreamToString(bodyData as unknown as NodeJS.ReadableStream)
67
+ }
68
+
69
+ if (errorBody) {
70
+ try {
71
+ const errorData = JSON.parse(errorBody)
72
+ if (errorData.error?.message) {
73
+ errorMessage = errorData.error.message
74
+ } else if (errorData.message) {
75
+ errorMessage = errorData.message
76
+ }
77
+ if (errorData.error?.code) {
78
+ errorCode = errorData.error.code
79
+ } else if (errorData.code) {
80
+ errorCode = errorData.code
81
+ }
82
+ if (errorData.requestId) {
83
+ requestId = errorData.requestId
84
+ }
85
+ } catch {
86
+ errorMessage = errorBody || errorMessage
87
+ }
88
+ }
89
+
90
+ // 从响应头中获取 requestId
91
+ if (!requestId && responseHeaders) {
92
+ const headerRequestId = responseHeaders['x-cloudbase-request-id'] || responseHeaders['x-request-id'] || ''
93
+ requestId = Array.isArray(headerRequestId) ? headerRequestId[0] : headerRequestId
94
+ }
95
+
96
+ throw E({
97
+ code: errorCode,
98
+ message: errorMessage,
99
+ requestId
100
+ })
101
+ }
102
+
103
+ if (stream) {
104
+ // 对于流式响应,将 Node.js 原生流转换为 Web ReadableStream
105
+ let readableStream: ReadableStream<Uint8Array>
106
+
107
+ if (bodyData && typeof bodyData === 'object' && 'on' in bodyData && typeof bodyData.on === 'function') {
108
+ const nodeStream = bodyData
109
+ // Node 12 兼容: 使用标志位追踪 stream 状态,避免重复 close 导致异常
110
+ let streamClosed = false
111
+ readableStream = new ReadableStream({
112
+ start(controller) {
113
+ nodeStream.on('data', (chunk: Buffer) => {
114
+ if (streamClosed) return
115
+ controller.enqueue(new Uint8Array(chunk))
116
+ })
117
+ nodeStream.on('end', () => {
118
+ if (streamClosed) return
119
+ streamClosed = true
120
+ controller.close()
121
+ })
122
+ nodeStream.on('error', (err) => {
123
+ if (streamClosed) return
124
+ streamClosed = true
125
+ controller.error(err)
126
+ })
127
+ },
128
+ cancel() {
129
+ streamClosed = true
130
+ nodeStream.destroy()
131
+ }
132
+ })
133
+ } else if (bodyData instanceof Buffer) {
134
+ readableStream = new ReadableStream({
135
+ start(controller) {
136
+ controller.enqueue(new Uint8Array(bodyData))
137
+ controller.close()
138
+ }
139
+ })
140
+ } else if (typeof bodyData === 'string') {
141
+ const encoder = new TextEncoder()
142
+ readableStream = new ReadableStream({
143
+ start(controller) {
144
+ controller.enqueue(encoder.encode(bodyData))
145
+ controller.close()
146
+ }
147
+ })
148
+ } else {
149
+ readableStream = new ReadableStream({
150
+ start(controller) {
151
+ controller.close()
152
+ }
153
+ })
154
+ }
155
+
156
+ return {
157
+ data: readableStream,
158
+ statusCode,
159
+ header: responseHeaders
160
+ }
161
+ }
162
+
163
+ let responseData: any
164
+ if (typeof bodyData === 'string') {
165
+ try {
166
+ responseData = JSON.parse(bodyData)
167
+ } catch {
168
+ responseData = bodyData
169
+ }
170
+ } else if (bodyData instanceof Buffer) {
171
+ const bodyString = bodyData.toString('utf-8')
172
+ try {
173
+ responseData = JSON.parse(bodyString)
174
+ } catch {
175
+ responseData = bodyString
176
+ }
177
+ } else {
178
+ responseData = bodyData
179
+ }
180
+
181
+ return {
182
+ data: Promise.resolve(responseData),
183
+ statusCode,
184
+ header: responseHeaders
185
+ }
186
+ }
187
+
188
+ /**
189
+ * post 方法 - AI 模块可能不使用,但需要实现接口
190
+ */
191
+ async post() {
192
+ throw new Error('post method is not supported in AI module')
193
+ }
194
+
195
+ /**
196
+ * upload 方法 - AI 模块可能不使用,但需要实现接口
197
+ */
198
+ async upload() {
199
+ throw new Error('upload method is not supported in AI module')
200
+ }
201
+
202
+ /**
203
+ * download 方法 - AI 模块可能不使用,但需要实现接口
204
+ */
205
+ async download() {
206
+ throw new Error('download method is not supported in AI module')
207
+ }
208
+ }
209
+
210
+ function isHeaders(h: HeadersInit): h is Headers {
211
+ try {
212
+ // Node.js 低版本可能没有 Headers
213
+ return h instanceof Headers
214
+ } catch (_) {
215
+ return false
216
+ }
217
+ }
218
+
219
+ /**
220
+ * 从 Node.js 流中读取完整内容为字符串
221
+ */
222
+ async function readStreamToString(stream: NodeJS.ReadableStream): Promise<string> {
223
+ return await new Promise((resolve, reject) => {
224
+ const chunks: Buffer[] = []
225
+ stream.on('data', (chunk: Buffer) => {
226
+ chunks.push(chunk)
227
+ })
228
+ stream.on('end', () => {
229
+ resolve(Buffer.concat(chunks).toString('utf-8'))
230
+ })
231
+ stream.on('error', (err) => {
232
+ reject(err)
233
+ })
234
+ })
235
+ }
package/src/cloudbase.ts CHANGED
@@ -46,6 +46,8 @@ import { newDb } from './database'
46
46
  import { uploadFile, deleteFile, getTempFileURL, getFileInfo, downloadFile, getUploadMetadata, getFileAuthority, copyFile } from './storage'
47
47
  import { callWxOpenApi, callCompatibleWxOpenApi, callWxPayApi, wxCallContainerApi } from './wx'
48
48
  import { analytics } from './analytics'
49
+ import { createAI } from './ai'
50
+ import { AI } from '@cloudbase/ai'
49
51
 
50
52
  import { Logger, logger } from './logger'
51
53
 
@@ -59,6 +61,7 @@ import * as openapicommonrequester from './utils/tcbopenapicommonrequester'
59
61
  import { IFetchOptions } from '@cloudbase/adapter-interface'
60
62
  import { buildCommonOpenApiUrlWithPath } from './utils/tcbopenapiendpoint'
61
63
  import { SYMBOL_CURRENT_ENV } from './const/symbol'
64
+ import { IncomingHttpHeaders } from 'http'
62
65
 
63
66
  export class CloudBase {
64
67
  public static scfContext: ISCFContext
@@ -79,6 +82,8 @@ export class CloudBase {
79
82
 
80
83
  private extensionMap: Map<string, Extension>
81
84
 
85
+ private aiInstance: AI
86
+
82
87
  public models: OrmClient & OrmRawQueryClient
83
88
 
84
89
  public mysql: IMySqlClient
@@ -94,19 +99,9 @@ export class CloudBase {
94
99
  /* eslint-disable-next-line */
95
100
  preflightRuntimeCloudPlatform()
96
101
 
97
- const {
98
- debug,
99
- secretId,
100
- secretKey,
101
- sessionToken,
102
- env,
103
- timeout,
104
- headers = {},
105
- ...restConfig
106
- } = config
102
+ const { debug, secretId, secretKey, sessionToken, env, timeout, headers = {}, ...restConfig } = config
107
103
 
108
- if (('secretId' in config && !('secretKey' in config))
109
- || (!('secretId' in config) && 'secretKey' in config)) {
104
+ if (('secretId' in config && !('secretKey' in config)) || (!('secretId' in config) && 'secretKey' in config)) {
110
105
  throw utils.E({
111
106
  ...ERROR.INVALID_PARAM,
112
107
  message: 'secretId and secretKey must be a pair'
@@ -220,13 +215,10 @@ export class CloudBase {
220
215
  const res = {
221
216
  ok: result?.statusCode >= 200 && result?.statusCode < 300,
222
217
  status: result?.statusCode || 200,
223
- statusText: result?.statusMessage || 'OK',
218
+ statusText: (result as unknown as { statusMessage: string })?.statusMessage || 'OK',
224
219
  json: async () => await Promise.resolve(data || {}),
225
- text: async () =>
226
- await Promise.resolve(
227
- typeof data === 'string' ? data : JSON.stringify(data || {})
228
- ),
229
- headers: new Headers(result?.headers || {})
220
+ text: async () => await Promise.resolve(typeof data === 'string' ? data : JSON.stringify(data || {})),
221
+ headers: new Headers(incomingHttpHeadersToHeadersInit(result?.headers || {}))
230
222
  }
231
223
 
232
224
  return res as Response
@@ -262,6 +254,13 @@ export class CloudBase {
262
254
  return newDb(this, dbConfig)
263
255
  }
264
256
 
257
+ public ai(): AI {
258
+ if (!this.aiInstance) {
259
+ this.aiInstance = createAI(this)
260
+ }
261
+ return this.aiInstance
262
+ }
263
+
265
264
  public async callFunction<ParaT, ResultT>(callFunctionOptions: ICallFunctionOptions<ParaT>, opts?: ICustomReqOpts) {
266
265
  return await callFunction<ParaT, ResultT>(this, callFunctionOptions, opts)
267
266
  }
@@ -270,8 +269,8 @@ export class CloudBase {
270
269
  return await callContainer<ParaT, ResultT>(this, callContainerOptions, opts)
271
270
  }
272
271
 
273
- public async callApis<ParaT, ResultT>(callApiOptions: ICallApisOptions<ParaT>, opts?: ICustomReqOpts) {
274
- return await callApis<ParaT, ResultT>(this, callApiOptions, opts)
272
+ public async callApis<ParaT>(callApiOptions: ICallApisOptions<ParaT>, opts?: ICustomReqOpts) {
273
+ return await callApis<ParaT>(this, callApiOptions, opts)
275
274
  }
276
275
 
277
276
  public async callWxOpenApi(wxOpenApiOptions: ICallWxOpenApiOptions, opts?: ICustomReqOpts): Promise<ICallWxOpenApiResult> {
@@ -384,7 +383,7 @@ function headersInitToRecord(headers: HeadersInit): Record<string, string> {
384
383
  ret[key] = value
385
384
  })
386
385
  } else {
387
- Object.keys(headers).forEach(key => {
386
+ Object.keys(headers).forEach((key) => {
388
387
  ret[key] = headers[key]
389
388
  })
390
389
  }
@@ -398,3 +397,20 @@ function safeParseJSON(x: unknown) {
398
397
  return x
399
398
  }
400
399
  }
400
+
401
+ function incomingHttpHeadersToHeadersInit(headers: IncomingHttpHeaders): HeadersInit {
402
+ const result: Array<[string, string]> = []
403
+ for (const [key, value] of Object.entries(headers)) {
404
+ if (value === undefined) {
405
+ continue
406
+ }
407
+ if (Array.isArray(value)) {
408
+ for (const v of value) {
409
+ result.push([key, v])
410
+ }
411
+ } else {
412
+ result.push([key, value])
413
+ }
414
+ }
415
+ return result
416
+ }
@@ -76,7 +76,7 @@ export async function callFunction<ParaT, ResultT>(cloudbase: CloudBase, callFun
76
76
  * @param opts
77
77
  * @returns
78
78
  */
79
- export async function callApis<ParaT, ResultT>(cloudbase: CloudBase, callApiOptions: ICallApisOptions<ParaT>, opts?: ICustomReqOpts): Promise<ResultT> {
79
+ export async function callApis<ParaT>(cloudbase: CloudBase, callApiOptions: ICallApisOptions<ParaT>, opts?: ICustomReqOpts) {
80
80
  let { name, body, path = '', method = 'POST', header = {}, token = '' } = callApiOptions
81
81
 
82
82
  if (!name) {
@@ -101,12 +101,17 @@ async function onResponse(
101
101
  {
102
102
  encoding,
103
103
  type = 'json'
104
- }: { encoding?: string, type?: 'stream' | 'raw' | 'json' }
105
- ): Promise<string | Buffer | undefined> {
104
+ }: { encoding?: string, type?: 'stream' | 'raw' | 'json' | 'rawStream' }
105
+ ): Promise<string | Buffer | http.IncomingMessage | undefined> {
106
106
  if (type === 'stream') {
107
107
  return await Promise.resolve(undefined)
108
108
  }
109
109
 
110
+ // rawStream: 返回原始的 Node.js 流,用于真正的流式处理
111
+ if (type === 'rawStream') {
112
+ return await Promise.resolve(res)
113
+ }
114
+
110
115
  if (encoding) {
111
116
  res.setEncoding(encoding)
112
117
  }
@@ -171,7 +176,13 @@ function onTimeout(req: http.ClientRequest, cb: RequestCB) {
171
176
 
172
177
  export type RequestCB = (err: Error, res?: http.IncomingMessage, body?: string | Buffer) => void
173
178
 
174
- export function request(opts: IReqOpts, cb: RequestCB): http.ClientRequest {
179
+ // 用于 rawStream 类型的回调,body 参数实际上是 IncomingMessage
180
+ export type RequestCBWithStream = (err: Error, res?: http.IncomingMessage, body?: string | Buffer | http.IncomingMessage) => void
181
+
182
+ // 函数重载:支持普通回调和流式回调
183
+ export function request(opts: IReqOpts, cb: RequestCB): http.ClientRequest
184
+ export function request(opts: IReqOpts & { type: 'rawStream' }, cb: RequestCBWithStream): http.ClientRequest
185
+ export function request(opts: IReqOpts, cb: RequestCB | RequestCBWithStream): http.ClientRequest {
175
186
  const times = opts.times || 1
176
187
 
177
188
  const options: http.ClientRequestArgs = {
@@ -200,7 +211,7 @@ export function request(opts: IReqOpts, cb: RequestCB): http.ClientRequest {
200
211
  type: opts.json ? 'json' : opts.type
201
212
  })
202
213
  .then((body) => {
203
- cb(null, res, body)
214
+ (cb as RequestCBWithStream)(null, res, body)
204
215
  })
205
216
  .catch((err) => {
206
217
  cb(err)
@@ -67,7 +67,7 @@ interface IExtraRequestOptions {
67
67
  interface IResponse {
68
68
  statusCode: number
69
69
  headers: http.IncomingHttpHeaders
70
- body: string | Buffer
70
+ body: string | Buffer | http.IncomingMessage
71
71
  }
72
72
 
73
73
  /* istanbul ignore next */
@@ -99,7 +99,7 @@ export async function requestWithTimingsMeasure(opts: IReqOpts, extraOptions?: I
99
99
  })
100
100
 
101
101
  ; (function r() {
102
- const cRequest = request(opts, (err: Error, res: http.IncomingMessage, body: string | Buffer) => {
102
+ const cRequest = request(opts, (err: Error, res: http.IncomingMessage, body) => {
103
103
  if (err) {
104
104
  reject(err)
105
105
  } else {
@@ -53,7 +53,7 @@ export class TcbOpenApiHttpCommonRequester {
53
53
  this.tracingInfo = generateTracingInfo(args.config?.context?.eventID)
54
54
  }
55
55
 
56
- public async request(): Promise<any> {
56
+ public async request() {
57
57
  await this.prepareCredentials()
58
58
 
59
59
  const opts = this.makeReqOpts()
@@ -75,7 +75,7 @@ export class TcbOpenApiHttpCommonRequester {
75
75
  seqId: this.tracingInfo.seqId,
76
76
  retryOptions,
77
77
  timingsMeasurerOptions: config.timingsMeasurerOptions || {}
78
- }).then((response: any) => {
78
+ }).then((response) => {
79
79
  this.slowWarnTimer && clearTimeout(this.slowWarnTimer)
80
80
  return response
81
81
  })
@@ -120,7 +120,12 @@ export class TcbOpenApiHttpCommonRequester {
120
120
  opts.encoding = null
121
121
  } else {
122
122
  opts.body = args.data
123
- opts.json = true
123
+ if (args.opts?.stream !== true) {
124
+ opts.json = true
125
+ } else {
126
+ // 使用 rawStream 类型返回原始的 Node.js 流,实现真正的流式响应
127
+ opts.type = 'rawStream'
128
+ }
124
129
  }
125
130
  } else {
126
131
  /* istanbul ignore next */
@@ -201,7 +206,7 @@ export class TcbOpenApiHttpCommonRequester {
201
206
  }
202
207
  }
203
208
 
204
- export async function request(args: ITcbOpenApiHttpCommonRequestInfo): Promise<any> {
209
+ export async function request(args: ITcbOpenApiHttpCommonRequestInfo) {
205
210
  if (typeof args.isInternal === 'undefined') {
206
211
  args.isInternal = await checkIsInternalAsync()
207
212
  }
package/types/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import fs from 'fs'
2
2
 
3
3
  import { Db } from '@cloudbase/database'
4
4
  import { OrmClient, OrmRawQueryClient } from '@cloudbase/wx-cloud-client-sdk'
5
+ import { AI } from '@cloudbase/ai'
5
6
 
6
7
  type IKeyValue = Record<string, any>
7
8
 
@@ -570,6 +571,11 @@ export declare class CloudBase {
570
571
  wxCallContainerApi(wxOpenApiOptions: ICallWxOpenApiOptions, opts?: ICustomReqOpts): Promise<any>
571
572
  callCompatibleWxOpenApi(wxOpenApiOptions: ICallWxOpenApiOptions, opts?: ICustomReqOpts): Promise<any>
572
573
 
574
+ /**
575
+ * ai - 获取 AI 实例
576
+ */
577
+ ai(): AI
578
+
573
579
  registerExtension(ext: Extension): void
574
580
  invokeExtension<T>(name: string, opts: T)
575
581
  }
@@ -39,7 +39,7 @@ export interface IReqOpts {
39
39
  timeout?: number
40
40
  headers?: IHeaderOpts
41
41
  json?: boolean
42
- type?: 'stream' | 'raw' | 'json'
42
+ type?: 'stream' | 'raw' | 'json' | 'rawStream'
43
43
  debug?: boolean
44
44
  times?: number
45
45
  noBody?: boolean