@cloudbase/ai 3.3.14-alpha.2 → 3.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/ai",
3
- "version": "3.3.14-alpha.2",
3
+ "version": "3.6.2",
4
4
  "description": "cloudbase js sdk ai module",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -30,8 +30,8 @@
30
30
  "author": "",
31
31
  "license": "Apache-2.0",
32
32
  "dependencies": {
33
- "@cloudbase/types": "3.3.14-alpha.2",
34
- "@cloudbase/utilities": "3.3.14-alpha.2",
33
+ "@cloudbase/types": "3.6.2",
34
+ "@cloudbase/utilities": "3.6.2",
35
35
  "@mattiasbuelens/web-streams-adapter": "^0.1.0",
36
36
  "text-encoding-shim": "^1.0.5",
37
37
  "web-streams-polyfill": "^4.0.0"
@@ -39,5 +39,5 @@
39
39
  "devDependencies": {
40
40
  "@cloudbase/adapter-interface": "^0.7.1"
41
41
  },
42
- "gitHead": "1490b9b59961d394f8952c55335b7a1a63377789"
42
+ "gitHead": "859557576897d4778857c12fb8d7fe8ca1f3a052"
43
43
  }
package/src/AI.ts CHANGED
@@ -11,19 +11,28 @@ import { langEvent } from '@cloudbase/utilities'
11
11
  const { MODELS } = models
12
12
 
13
13
  class AI {
14
- public aiBaseUrl: string
15
- public aiBotBaseUrl: string
16
14
  public bot: Bot
17
15
  public i18n: ICloudbaseConfig['i18n']
18
16
  defaultHeaders: { [x: string]: any }
17
+ private _getBaseUrl?: () => string
18
+
19
+ /** 动态获取 aiBaseUrl,支持 registerEndPointWithKey 后端点变更生效 */
20
+ get aiBaseUrl(): string {
21
+ return `${this._getBaseUrl ? this._getBaseUrl() : this.baseUrl}/ai`
22
+ }
23
+
24
+ /** 动态获取 aiBotBaseUrl,支持 registerEndPointWithKey 后端点变更生效 */
25
+ get aiBotBaseUrl(): string {
26
+ return `${this._getBaseUrl ? this._getBaseUrl() : this.baseUrl}/aibot`
27
+ }
19
28
 
20
29
  constructor(
21
30
  private req: SDKRequestInterface,
22
31
  public baseUrl: string,
23
32
  i18n: ICloudbaseConfig['i18n'],
33
+ getBaseUrl?: () => string,
24
34
  ) {
25
- this.aiBaseUrl = `${baseUrl}/ai`
26
- this.aiBotBaseUrl = `${baseUrl}/aibot`
35
+ this._getBaseUrl = getBaseUrl
27
36
  this.bot = new Bot(this.botRequest, this.aiBotBaseUrl)
28
37
  this.i18n = i18n
29
38
 
@@ -35,25 +44,42 @@ class AI {
35
44
  async handleResponseData(responseData: Promise<unknown> | ReadableStream<Uint8Array>, header?: Headers) {
36
45
  const GO_TO_AI_TEXT = `${this.i18n.t('请检查调用方式,或前往云开发 AI+ 首页查看文档')}:https://tcb.cloud.tencent.com/dev#/ai`
37
46
 
38
- if (typeof responseData === 'object' && responseData && 'then' in responseData) {
39
- // 非流式请求,直接返回 json 数据
40
- const json = (await responseData) as Record<string, unknown>
41
- if (typeof json === 'object' && json && 'code' in json && json.code !== 'NORMAL') {
42
- throw new Error(`${this.i18n.t('AI+ 请求出错,错误码')}:${json.code},${this.i18n.t('错误信息')}:${
43
- json.message
47
+ const throwIfError = (json: unknown) => {
48
+ if (typeof json === 'object' && json && 'code' in json && (json as any).code !== 'NORMAL') {
49
+ throw new Error(`${this.i18n.t('AI+ 请求出错,错误码')}:${(json as any).code},${this.i18n.t('错误信息')}:${
50
+ (json as any).message
44
51
  }\n${GO_TO_AI_TEXT}\n${JSON.stringify(json, null, 2)}`,)
45
52
  }
53
+ }
46
54
 
55
+ // 非流式请求:
56
+ // - 小程序/Web 适配器返回 Promise(has 'then')
57
+ // - Node 适配器已将 body 解析为普通对象(无 'then'、无 getReader)
58
+ // 两种情况都按 JSON 校验错误码后原样返回
59
+ if (typeof responseData === 'object' && responseData && typeof (responseData as any).getReader !== 'function') {
60
+ const json = 'then' in responseData ? await responseData : responseData
61
+ throwIfError(json)
47
62
  return responseData
48
63
  }
49
- // 流式请求,如果接口出错,会变成传 json 格式的响应,通过 header 判断格式
50
- if (header?.get?.('content-type')?.includes('application/json')) {
51
- const json = await readableStream2JsonObject(responseData as ReadableStream<Uint8Array>)
64
+
65
+ // 判断 responseData 是否是 ReadableStream 实例(兼容 Node.js 适配器非流式场景已解析为 JSON 对象的情况)
66
+ const isStream = typeof (responseData as any)?.getReader === 'function'
67
+
68
+ if (!isStream) {
69
+ // responseData 已经是解析后的 JSON 对象(如 Node.js 适配器非流式返回),直接检查错误码
70
+ const json = responseData as unknown as Record<string, unknown>
52
71
  if (typeof json === 'object' && json && 'code' in json && json.code !== 'NORMAL') {
53
72
  throw new Error(`${this.i18n.t('AI+ 请求出错,错误码')}:${json.code},${this.i18n.t('错误信息')}:${
54
73
  json.message
55
74
  }\n${GO_TO_AI_TEXT}\n${JSON.stringify(json, null, 2)}`,)
56
75
  }
76
+ return responseData
77
+ }
78
+
79
+ // 流式请求,如果接口出错,会变成传 json 格式的响应,通过 header 判断格式
80
+ if (header?.get?.('content-type')?.includes('application/json')) {
81
+ const json = await readableStream2JsonObject(responseData as ReadableStream<Uint8Array>)
82
+ throwIfError(json)
57
83
  }
58
84
  return responseData
59
85
  }
@@ -102,7 +128,7 @@ class AI {
102
128
  method: 'post',
103
129
  headers: { [this.i18n?.LANG_HEADER_KEY]: this.i18n?.lang, ...fetchHeaders, ...headers },
104
130
  body: JSON.stringify(data),
105
- url,
131
+ url: this.resolveUrl(url),
106
132
  stream,
107
133
  timeout,
108
134
  })) as { data: Promise<unknown> | ReadableStream<Uint8Array>; header?: Headers }
@@ -111,8 +137,9 @@ class AI {
111
137
  }
112
138
 
113
139
  botRequest: types.BotReq = async ({ method, url, data = {}, headers, stream, timeout }) => {
140
+ const resolvedUrl = this.resolveUrl(url)
114
141
  if (method === 'get') {
115
- return this.handleResponseData((await this.req.fetch({ url: `${url}?${objectToParam(data)}`, method, headers, stream, timeout })).data,)
142
+ return this.handleResponseData((await this.req.fetch({ url: `${resolvedUrl}?${objectToParam(data)}`, method, headers, stream, timeout })).data,)
116
143
  }
117
144
 
118
145
  const fetchHeaders = {
@@ -122,7 +149,7 @@ class AI {
122
149
  stream && Object.assign(fetchHeaders, { Accept: 'text/event-stream' })
123
150
 
124
151
  const { data: responseData, header } = (await this.req.fetch({
125
- url,
152
+ url: resolvedUrl,
126
153
  body: JSON.stringify(data),
127
154
  headers: {
128
155
  [this.i18n?.LANG_HEADER_KEY]: this.i18n?.lang,
@@ -149,6 +176,16 @@ class AI {
149
176
  }
150
177
  models.toolMap.set(functionTool.name, functionTool.fn)
151
178
  }
179
+
180
+ /** 动态替换 URL 中的 origin 部分,支持 registerEndPointWithKey 后生效 */
181
+ private resolveUrl(url: string): string {
182
+ if (!this._getBaseUrl) return url
183
+ const currentBase = this._getBaseUrl()
184
+ if (currentBase !== this.baseUrl && url.startsWith(this.baseUrl)) {
185
+ return url.replace(this.baseUrl, currentBase)
186
+ }
187
+ return url
188
+ }
152
189
  }
153
190
 
154
191
  export { AI }
package/src/errors.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 参数校验错误类
3
+ * 用于统一参数校验错误的格式
4
+ */
5
+ export class InvalidParamsError extends Error {
6
+ public code: string
7
+ public field?: string
8
+
9
+ constructor(message: string, field?: string) {
10
+ super(message)
11
+ this.name = 'InvalidParamsError'
12
+ this.code = 'INVALID_PARAMS'
13
+ this.field = field
14
+ }
15
+
16
+ toJSON() {
17
+ return {
18
+ code: this.code,
19
+ message: this.message,
20
+ field: this.field,
21
+ }
22
+ }
23
+ }
package/src/index.ts CHANGED
@@ -58,6 +58,8 @@ interface AIInitOption {
58
58
  interface ICreateAI {
59
59
  req: SDKRequestInterface
60
60
  baseUrl?: string
61
+ /** 动态获取 baseUrl 的函数,若提供则优先使用,支持端点变更后请求自动跟随 */
62
+ getBaseUrl?: () => string
61
63
  env?: string
62
64
  getAccessToken?: () => Promise<{ accessToken: string }>
63
65
  handleReqInstance?: HandleRequestInstanceFunc
@@ -74,8 +76,11 @@ interface ICreateAI {
74
76
  *
75
77
  * 其他参数可按需传入覆盖函数逻辑。
76
78
  */
77
- function createAI({ env, baseUrl, req, getAccessToken, handleReqInstance, i18n }: ICreateAI) {
79
+ function createAI({ env, baseUrl, getBaseUrl: getBaseUrlFunc, req, getAccessToken, handleReqInstance, i18n }: ICreateAI) {
78
80
  const getBaseUrl = () => {
81
+ if (getBaseUrlFunc != null) {
82
+ return getBaseUrlFunc()
83
+ }
79
84
  if (baseUrl != null) {
80
85
  return baseUrl
81
86
  }
@@ -95,7 +100,7 @@ function createAI({ env, baseUrl, req, getAccessToken, handleReqInstance, i18n }
95
100
  return handleReqInstance({ req })
96
101
  }
97
102
 
98
- return new AI(getReq(), getBaseUrl(), i18n)
103
+ return new AI(getReq(), getBaseUrl(), i18n, getBaseUrlFunc)
99
104
  }
100
105
 
101
106
  // 兼容
@@ -130,9 +135,12 @@ function ai(
130
135
  return `${PROTOCOL}${BASE_URL}`
131
136
  }
132
137
 
133
- const baseUrl = options?.baseUrl ?? getUrlFromCloud()
138
+ // 若用户传了静态 baseUrl 则使用静态值,否则传入动态获取函数支持端点变更后生效
139
+ if (options?.baseUrl) {
140
+ return createAI({ req, baseUrl: options.baseUrl, handleReqInstance: ({ req }) => req, i18n: this.config.i18n })
141
+ }
134
142
 
135
- return createAI({ req, baseUrl, handleReqInstance: ({ req }) => req, i18n: this.config.i18n })
143
+ return createAI({ req, baseUrl: getUrlFromCloud(), getBaseUrl: getUrlFromCloud, handleReqInstance: ({ req }) => req, i18n: this.config.i18n })
136
144
  }
137
145
 
138
146
  const component: ICloudbaseComponent = {
@@ -150,6 +158,7 @@ function registerAi(app: ICloudbase) {
150
158
 
151
159
  export { AI, Bot, createAi, createAI, restModels as models, registerAi, utils }
152
160
  export * from './type'
161
+ export * from './errors'
153
162
 
154
163
  try {
155
164
  (window as any).registerAi = registerAi
@@ -8,6 +8,7 @@ import type {
8
8
  // HunyuanEditImageInput,
9
9
  // HunyuanEditImageOutput,
10
10
  } from '../type'
11
+ import { InvalidParamsError } from '../errors'
11
12
 
12
13
  interface HunyuanImageGenerateFn {
13
14
  (input: HunyuanARGenerateImageInput): Promise<HunyuanARGenerateImageOutput>
@@ -19,28 +20,53 @@ type GenerateImageFn<TProvider> = TProvider extends 'hunyuan-image'
19
20
  : (input: unknown) => Promise<unknown>
20
21
 
21
22
  export class DefaultImageModel<TProvider extends string> {
22
- public defaultGenerateImageSubUrl = 'images/generations'
23
+ public defaultGenerateImageSubUrl = 'images/ar/generations'
23
24
 
24
- public generateImageSubUrlConfig: Record<string, Record<string, string>> = {
25
- 'hunyuan-image': {
26
- 'hunyuan-image-v3.0-v1.0.4': 'images/ar/generations',
27
- 'hunyuan-image-v3.0-v1.0.1': 'images/ar/generations',
28
- },
25
+ public generateImageSubUrlConfig: Record<string, Array<[RegExp, string]>> = {
26
+ 'hunyuan-image': [
27
+ [/^hunyuan-image(-v[12](\..*)?)?$/, 'images/generations'],
28
+ ],
29
29
  }
30
30
 
31
31
  /** 生成图片 */
32
32
  public generateImage: GenerateImageFn<TProvider>
33
33
 
34
- constructor(private req: ModelReq, public baseUrl: string, public provider: TProvider) {
34
+ constructor(
35
+ private req: ModelReq,
36
+ public baseUrl: string,
37
+ public provider: TProvider,
38
+ ) {
35
39
  this.generateImage = this.generateImageImpl.bind(this) as GenerateImageFn<TProvider>
36
40
  }
37
41
 
38
42
  /** generateImage 的实际实现 */
39
43
  private async generateImageImpl(input: HunyuanARGenerateImageInput | HunyuanGenerateImageInput,): Promise<HunyuanARGenerateImageOutput | HunyuanGenerateImageOutput> {
44
+ // 当模型为 HY-Image-v3.0-I2I-ToB-v1.0.1 且 provider 为空或为 hunyuan-image 时校验参数
45
+ if (
46
+ typeof input === 'object' && input && 'model' in input && input.model === 'HY-Image-v3.0-I2I-ToB-v1.0.1' && (this.provider === '' || this.provider === 'hunyuan-image')
47
+ ) {
48
+ const typedInput = input
49
+ const imageUrls = typedInput.image_urls as string[] | undefined
50
+ const images = typedInput.images as string[] | undefined
51
+
52
+ if (imageUrls && imageUrls.length > 1) {
53
+ throw new InvalidParamsError(
54
+ '当模型为 HY-Image-v3.0-I2I-ToB-v1.0.1 时,image_urls 最多只能有 1 个元素',
55
+ 'image_urls',
56
+ )
57
+ }
58
+ if (images && images.length > 1) {
59
+ throw new InvalidParamsError(
60
+ '当模型为 HY-Image-v3.0-I2I-ToB-v1.0.1 时,images 最多只能有 1 个元素',
61
+ 'images',
62
+ )
63
+ }
64
+ }
65
+
40
66
  let subUrl = this.defaultGenerateImageSubUrl
41
67
 
42
68
  if (typeof input === 'object' && input && 'model' in input && typeof input.model === 'string') {
43
- const newSubUrl = this.generateImageSubUrlConfig[this.provider]?.[input.model]
69
+ const newSubUrl = this.getGenerateImageSubUrl(this.provider, input.model)
44
70
  if (newSubUrl) subUrl = newSubUrl
45
71
  }
46
72
 
@@ -56,4 +82,19 @@ export class DefaultImageModel<TProvider extends string> {
56
82
  private getGenerateUrl(subUrl: string) {
57
83
  return `${this.baseUrl}/${this.provider}/${subUrl}`
58
84
  }
85
+
86
+ /**
87
+ * 根据 provider 与 model 解析生成图片的子路径。
88
+ * 按配置顺序用 RegExp 匹配,命中即返回对应 subUrl。
89
+ */
90
+ private getGenerateImageSubUrl(provider: string, model: string): string | undefined {
91
+ const rules = this.generateImageSubUrlConfig[provider]
92
+ if (!rules) return undefined
93
+
94
+ for (const [pattern, subUrl] of rules) {
95
+ if (pattern.test(model)) return subUrl
96
+ }
97
+
98
+ return undefined
99
+ }
59
100
  }
package/src/type.ts CHANGED
@@ -231,12 +231,17 @@ export interface HunyuanGenerateImageInput {
231
231
 
232
232
  export interface HunyuanARGenerateImageInput {
233
233
  /** 模型名称:hunyuan-image-v3.0-v1.0.4(推荐)或 hunyuan-image-v3.0-v1.0.1 */
234
- model: 'hunyuan-image-v3.0-v1.0.4' | 'hunyuan-image-v3.0-v1.0.1'
234
+ model:
235
+ | 'HY-Image-3.0-Plus-4090-Tob-v1.0'
236
+ | 'HY-Image-v3.0-I2I-ToB-v1.0.1'
237
+ | 'hunyuan-image-v3.0-v1.0.4'
238
+ | 'hunyuan-image-v3.0-v1.0.1'
239
+ | 'hunyuan-image-v3.0-v1.0.5'
235
240
  /** 生成图片使用的文本,不超过 8192 字符 */
236
241
  prompt: string
237
242
  /**
238
243
  * 图片尺寸,格式 "${宽}x${高}",默认 "1024x1024"
239
- * hunyuan-image-v3.0-v1.0.4:宽高 [512, 2048],面积不超过 1024x1024
244
+ * HY-Image-3.0-Plus-4090-Tob-v1.0 hunyuan-image-v3.0-v1.0.4:宽高 [512, 2048],面积不超过 1024x1024
240
245
  * hunyuan-image-v3.0-v1.0.1:支持固定尺寸列表
241
246
  */
242
247
  size?: string
@@ -248,6 +253,14 @@ export interface HunyuanARGenerateImageInput {
248
253
  revise?: { value: boolean }
249
254
  /** 改写是否开启 thinking 模式,默认开启。开启后效果提升但耗时增加(最大 60s) */
250
255
  enable_thinking?: { value: boolean }
256
+ /**
257
+ * 垫图url列表,与image二选一,优先选image,大小不超过10MB,支持 jpg jpeg png 格式,最多3张图,image_urls和images必有一个非空
258
+ */
259
+ image_urls?: string[]
260
+ /**
261
+ * 垫图base64列表,与image_url二选一,优先选image,大小不超过10MB,支持 jpg jpeg png 格式,最多3张图
262
+ */
263
+ images?: string[]
251
264
  }
252
265
 
253
266
  export interface HunyuanARGenerateImageOutput {
package/src/utils.ts CHANGED
@@ -10,7 +10,15 @@ import type {
10
10
  } from './type'
11
11
  import { TextDecoder } from 'text-encoding-shim'
12
12
 
13
- export const ReadableStream = _ReadableStream as {
13
+ /**
14
+ * 检测当前环境是否原生支持完整的 Web Streams API(Node.js >= 18 或现代浏览器)
15
+ * 如果原生支持,则无需使用 polyfill,避免 web-streams-adapter 在高版本 Node.js 中的兼容问题
16
+ */
17
+ const hasNativeStreams = typeof globalThis.ReadableStream === 'function'
18
+ && typeof globalThis.TransformStream === 'function'
19
+ && typeof globalThis.ReadableStream.prototype.pipeThrough === 'function'
20
+
21
+ export const ReadableStream = (hasNativeStreams ? globalThis.ReadableStream : _ReadableStream) as {
14
22
  prototype: ReadableStream
15
23
 
16
24
  new (
@@ -23,7 +31,7 @@ export const ReadableStream = _ReadableStream as {
23
31
  new <R = any>(underlyingSource?: UnderlyingSource<R>, strategy?: QueuingStrategy<R>): ReadableStream<R>
24
32
  }
25
33
 
26
- export const TransformStream = _TransformStream as {
34
+ export const TransformStream = (hasNativeStreams ? globalThis.TransformStream : _TransformStream) as {
27
35
  prototype: TransformStream
28
36
 
29
37
  new <I = any, O = any>(
@@ -50,7 +58,14 @@ export const createEventSourceParserTransformStream = () => {
50
58
  })
51
59
  }
52
60
 
53
- export const toPolyfillReadable = createReadableStreamWrapper(ReadableStream)
61
+ /**
62
+ * 将任意 ReadableStream 转换为当前环境使用的 ReadableStream 实现
63
+ * - 在原生支持 Web Streams 的环境(Node.js >= 18、现代浏览器)中,直接返回原始 stream(无需转换)
64
+ * - 在不支持原生 Web Streams 的环境(旧浏览器、小程序)中,使用 web-streams-adapter 进行转换
65
+ */
66
+ export const toPolyfillReadable: (readable: any) => any = hasNativeStreams
67
+ ? readable => readable
68
+ : createReadableStreamWrapper(ReadableStream)
54
69
 
55
70
  export class TextDecoderStream {
56
71
  private handle: TextDecoder