ibc-ai-web-sdk 2.1.1 → 2.1.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/README.md CHANGED
@@ -84,39 +84,75 @@ AIWebSDK.create({
84
84
  | `headers` | object | `{}` | | 附加请求头 |
85
85
  | `extendInfo` | object | `{}` | | 随请求发送的扩展信息 |
86
86
 
87
- ### Token 刷新
87
+ ### Token 管理
88
88
 
89
- 提供 `refreshTokenFn` 即可,SDK 会自动处理过期检测和刷新。
89
+ SDK Token 生命周期**应与应用层保持一致**,建议应用层为 SDK 签发的 Token TTL 与自身登录态 TTL 相同。
90
+
91
+ #### 正常刷新
92
+
93
+ 提供 `refreshTokenFn`,SDK 会在 Token 即将过期时自动刷新(默认提前 60 秒)。
90
94
 
91
95
  ```js
92
96
  createAIChatDialog({
93
- token: 'initial-access-token',
94
- tokenExpiresAt: Date.now() + 30 * 60 * 1000, // Token 过期时间戳
95
- refreshToken: 'initial-refresh-token', // 可选
97
+ token: 'st-xxxx', // 由应用后端调 /v1/token/create 签发
98
+ tokenExpiresAt: Date.now() + 7200 * 1000, // SDK Token 过期时间戳(毫秒),建议与用户登录态一致
99
+ refreshToken: 'initial-refresh-token', // 可选:服务端刷新凭证
96
100
 
97
101
  refreshTokenFn: async ({ token, refreshToken, userId }) => {
98
- const res = await fetch('/api/auth/refresh', {
102
+ // 调自己的后端接口,后端用 API-Key /v1/token/create 拿新 st- Token
103
+ const res = await fetch('/api/auth/refresh-sdk-token', {
99
104
  method: 'POST',
100
105
  headers: { 'Content-Type': 'application/json' },
101
- body: JSON.stringify({ token, refreshToken, userId })
106
+ body: JSON.stringify({ userId })
102
107
  });
103
108
  const data = await res.json();
104
109
  return {
105
- token: data.accessToken,
110
+ token: data.token,
106
111
  expiresAt: data.expiresAt * 1000
107
112
  };
108
113
  }
109
114
  });
110
115
  ```
111
116
 
117
+ #### Token 彻底失效
118
+
119
+ 当 Token 过期且刷新失败(如刷新接口返回 401),或未配置 `refreshTokenFn` 时,SDK 会触发 `onAuthExpired` 回调。**应用层应在此回调中引导用户重新登录。**
120
+
121
+ ```js
122
+ createAIChatDialog({
123
+ token: 'st-xxxx',
124
+ tokenExpiresAt: Date.now() + 7200 * 1000,
125
+
126
+ onAuthExpired: ({ reason, message }) => {
127
+ console.log('SDK Token 已失效:', reason, message);
128
+ dialog.destroy(); // 销毁 SDK 实例
129
+ router.push('/login'); // 引导用户重新登录
130
+ }
131
+ });
132
+
133
+ // 登录成功后:重新拿 Token,重新初始化 SDK
134
+ async function afterLogin(userId) {
135
+ // 1. 调后端通过 API-Key 签新 st- Token
136
+ const { token, expiresAt } = await fetchNewSdkToken(userId);
137
+ // 2. 重新初始化 SDK
138
+ dialog = createAIChatDialog({
139
+ token,
140
+ tokenExpiresAt: expiresAt * 1000,
141
+ onAuthExpired: () => { /* 同上 */ }
142
+ });
143
+ }
144
+ ```
145
+
112
146
  | 参数 | 类型 | 说明 |
113
147
  |------|------|------|
114
- | `token` | string | 认证 Token |
115
- | `tokenExpiresAt` | number | Token 过期时间戳(毫秒) |
116
- | `refreshToken` | string | refresh token |
148
+ | `token` | string | 认证 Token(由后端用 API-Key 调 `/v1/token/create` 签发) |
149
+ | `tokenExpiresAt` | number | Token 过期时间戳(毫秒)。建议与应用登录态 TTL 一致 |
150
+ | `tokenRefreshBufferMs` | number | 过期前多久提前刷新(毫秒),默认 60000 |
151
+ | `refreshToken` | string | 后端刷新凭证(可选) |
117
152
  | `refreshTokenFn` | function | 刷新函数,返回新 token 信息 |
118
153
  | `onTokenRefreshed` | function | 刷新成功回调 |
119
- | `onTokenExpired` | function | 刷新失败回调 |
154
+ | `onAuthExpired` | function | 认证彻底失效回调。**在此处引导用户重新登录** |
155
+
120
156
  ```
121
157
 
122
158
  ### 高级配置
@@ -431,7 +467,9 @@ data: {"conversationId":"conv_xxx","tokens":{"input":10,"output":15}}
431
467
 
432
468
  ### 认证
433
469
 
434
- 所有请求自动携带 `Authorization: Bearer {token}`。提供 `refreshTokenFn` Token 过期会自动刷新,无需手动处理。
470
+ 所有请求自动携带 `Authorization: Bearer {token}`。Token 由应用后端通过 API-Key 调用 `/v1/token/create` 签发,建议 TTL 与应用登录态保持一致。
471
+
472
+ Token 过期后 SDK 触发 `onAuthExpired` 回调,应用层应引导用户重新登录。登录成功后调用 `updateConfig({ token, tokenExpiresAt, ... })` 或重新初始化 SDK 传入新 Token。
435
473
 
436
474
  ---
437
475
 
package/dist/index.cjs.js CHANGED
@@ -195,12 +195,19 @@ class AIChatClient {
195
195
  silent = false
196
196
  } = {}) {
197
197
  if (typeof this.config.refreshTokenFn !== 'function') {
198
- const error = new Error('Token 已过期且未配置 refreshTokenFn');
198
+ const authErr = new Error('Token 已过期且未配置 refreshTokenFn');
199
+ authErr._authExpired = true;
200
+ authErr._authReason = reason;
199
201
  if (!silent) {
200
202
  this.stopBackgroundRefresh();
201
- this.config.onTokenExpired?.(error);
203
+ this.config.onAuthExpired?.({
204
+ reason,
205
+ message: authErr.message,
206
+ noRefreshFn: true,
207
+ error: authErr
208
+ });
202
209
  }
203
- throw error;
210
+ throw authErr;
204
211
  }
205
212
  if (!this.refreshTokenPromise) {
206
213
  this.refreshTokenPromise = Promise.resolve().then(() => this.config.refreshTokenFn({
@@ -227,8 +234,17 @@ class AIChatClient {
227
234
  return this.config.token || '';
228
235
  }
229
236
  this.stopBackgroundRefresh();
230
- this.config.onTokenExpired?.(error);
231
- throw error;
237
+ const errMsg = error && (error.message || String(error)) || 'Token 刷新失败,请重新登录';
238
+ this.config.onAuthExpired?.({
239
+ reason,
240
+ message: errMsg,
241
+ noRefreshFn: false,
242
+ error
243
+ });
244
+ const authErr = new Error(errMsg);
245
+ authErr._authExpired = true;
246
+ authErr._authReason = reason;
247
+ throw authErr;
232
248
  }).finally(() => {
233
249
  this.refreshTokenPromise = null;
234
250
  });
@@ -360,6 +376,9 @@ class AIChatClient {
360
376
  }
361
377
  return result;
362
378
  } catch (error) {
379
+ if (error && error._authExpired) {
380
+ return this.createError(ERROR_CODES.AUTH_FAILED, error.message || '登录已过期,请重新登录');
381
+ }
363
382
  if (error && error.name === 'AbortError') {
364
383
  return this.createError(ERROR_CODES.TIMEOUT_ERROR, '请求超时,请稍后重试');
365
384
  }
@@ -454,6 +473,11 @@ class AIChatClient {
454
473
  try {
455
474
  return await this.executeMessageStream(prompt, handlers, options, false);
456
475
  } catch (error) {
476
+ if (error && error._authExpired) {
477
+ const message = error.message || '登录已过期,请重新登录';
478
+ handlers.onError?.(message, error);
479
+ return this.createError(ERROR_CODES.AUTH_FAILED, message);
480
+ }
457
481
  if (error && error.name === 'AbortError') {
458
482
  const message = '请求超时或已取消';
459
483
  handlers.onError?.(message, error.sseData || error);
@@ -3598,7 +3622,7 @@ class AIChatDialog extends HTMLElement {
3598
3622
  tokenRefreshBufferMs: 60000,
3599
3623
  refreshTokenFn: null,
3600
3624
  onTokenRefreshed: null,
3601
- onTokenExpired: null,
3625
+ onAuthExpired: null,
3602
3626
  authErrorCodes: [],
3603
3627
  title: 'AI 助手',
3604
3628
  welcomeText: 'Hi,我是智能助手 ~\n欢迎随时提问',
@@ -6064,7 +6088,7 @@ class AIChatDialog extends HTMLElement {
6064
6088
  if (!text || typeof text !== 'string') return false;
6065
6089
  if (text === '\n' || text === '\n\n') return false;
6066
6090
  const p = text.trim();
6067
- if (!p) return true;
6091
+ if (!p) return false;
6068
6092
  return RUNTIME_PLACEHOLDERS.some(k => p.includes(k));
6069
6093
  }
6070
6094
  _isEventLike(text) {
@@ -6300,8 +6324,11 @@ class AIChatDialog extends HTMLElement {
6300
6324
  handleError(err) {
6301
6325
  let displayMsg = '抱歉,网络请求失败,请稍后重试';
6302
6326
  const errMsg = err?.message || '';
6327
+ const errCode = err?.code;
6303
6328
  // 按错误类型分类
6304
- if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('网络')) {
6329
+ if (err?._authExpired || errCode === 40301) {
6330
+ displayMsg = '登录已过期,请重新登录后再试';
6331
+ } else if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('网络')) {
6305
6332
  displayMsg = '网络连接失败,请检查网络后重试';
6306
6333
  } else if (errMsg.includes('AbortError') || errMsg.includes('超时') || errMsg.includes('timeout')) {
6307
6334
  displayMsg = '请求超时,请稍后重试';
package/dist/index.esm.js CHANGED
@@ -191,12 +191,19 @@ class AIChatClient {
191
191
  silent = false
192
192
  } = {}) {
193
193
  if (typeof this.config.refreshTokenFn !== 'function') {
194
- const error = new Error('Token 已过期且未配置 refreshTokenFn');
194
+ const authErr = new Error('Token 已过期且未配置 refreshTokenFn');
195
+ authErr._authExpired = true;
196
+ authErr._authReason = reason;
195
197
  if (!silent) {
196
198
  this.stopBackgroundRefresh();
197
- this.config.onTokenExpired?.(error);
199
+ this.config.onAuthExpired?.({
200
+ reason,
201
+ message: authErr.message,
202
+ noRefreshFn: true,
203
+ error: authErr
204
+ });
198
205
  }
199
- throw error;
206
+ throw authErr;
200
207
  }
201
208
  if (!this.refreshTokenPromise) {
202
209
  this.refreshTokenPromise = Promise.resolve().then(() => this.config.refreshTokenFn({
@@ -223,8 +230,17 @@ class AIChatClient {
223
230
  return this.config.token || '';
224
231
  }
225
232
  this.stopBackgroundRefresh();
226
- this.config.onTokenExpired?.(error);
227
- throw error;
233
+ const errMsg = error && (error.message || String(error)) || 'Token 刷新失败,请重新登录';
234
+ this.config.onAuthExpired?.({
235
+ reason,
236
+ message: errMsg,
237
+ noRefreshFn: false,
238
+ error
239
+ });
240
+ const authErr = new Error(errMsg);
241
+ authErr._authExpired = true;
242
+ authErr._authReason = reason;
243
+ throw authErr;
228
244
  }).finally(() => {
229
245
  this.refreshTokenPromise = null;
230
246
  });
@@ -356,6 +372,9 @@ class AIChatClient {
356
372
  }
357
373
  return result;
358
374
  } catch (error) {
375
+ if (error && error._authExpired) {
376
+ return this.createError(ERROR_CODES.AUTH_FAILED, error.message || '登录已过期,请重新登录');
377
+ }
359
378
  if (error && error.name === 'AbortError') {
360
379
  return this.createError(ERROR_CODES.TIMEOUT_ERROR, '请求超时,请稍后重试');
361
380
  }
@@ -450,6 +469,11 @@ class AIChatClient {
450
469
  try {
451
470
  return await this.executeMessageStream(prompt, handlers, options, false);
452
471
  } catch (error) {
472
+ if (error && error._authExpired) {
473
+ const message = error.message || '登录已过期,请重新登录';
474
+ handlers.onError?.(message, error);
475
+ return this.createError(ERROR_CODES.AUTH_FAILED, message);
476
+ }
453
477
  if (error && error.name === 'AbortError') {
454
478
  const message = '请求超时或已取消';
455
479
  handlers.onError?.(message, error.sseData || error);
@@ -3594,7 +3618,7 @@ class AIChatDialog extends HTMLElement {
3594
3618
  tokenRefreshBufferMs: 60000,
3595
3619
  refreshTokenFn: null,
3596
3620
  onTokenRefreshed: null,
3597
- onTokenExpired: null,
3621
+ onAuthExpired: null,
3598
3622
  authErrorCodes: [],
3599
3623
  title: 'AI 助手',
3600
3624
  welcomeText: 'Hi,我是智能助手 ~\n欢迎随时提问',
@@ -6060,7 +6084,7 @@ class AIChatDialog extends HTMLElement {
6060
6084
  if (!text || typeof text !== 'string') return false;
6061
6085
  if (text === '\n' || text === '\n\n') return false;
6062
6086
  const p = text.trim();
6063
- if (!p) return true;
6087
+ if (!p) return false;
6064
6088
  return RUNTIME_PLACEHOLDERS.some(k => p.includes(k));
6065
6089
  }
6066
6090
  _isEventLike(text) {
@@ -6296,8 +6320,11 @@ class AIChatDialog extends HTMLElement {
6296
6320
  handleError(err) {
6297
6321
  let displayMsg = '抱歉,网络请求失败,请稍后重试';
6298
6322
  const errMsg = err?.message || '';
6323
+ const errCode = err?.code;
6299
6324
  // 按错误类型分类
6300
- if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('网络')) {
6325
+ if (err?._authExpired || errCode === 40301) {
6326
+ displayMsg = '登录已过期,请重新登录后再试';
6327
+ } else if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('网络')) {
6301
6328
  displayMsg = '网络连接失败,请检查网络后重试';
6302
6329
  } else if (errMsg.includes('AbortError') || errMsg.includes('超时') || errMsg.includes('timeout')) {
6303
6330
  displayMsg = '请求超时,请稍后重试';
package/dist/index.umd.js CHANGED
@@ -197,12 +197,19 @@
197
197
  silent = false
198
198
  } = {}) {
199
199
  if (typeof this.config.refreshTokenFn !== 'function') {
200
- const error = new Error('Token 已过期且未配置 refreshTokenFn');
200
+ const authErr = new Error('Token 已过期且未配置 refreshTokenFn');
201
+ authErr._authExpired = true;
202
+ authErr._authReason = reason;
201
203
  if (!silent) {
202
204
  this.stopBackgroundRefresh();
203
- this.config.onTokenExpired?.(error);
205
+ this.config.onAuthExpired?.({
206
+ reason,
207
+ message: authErr.message,
208
+ noRefreshFn: true,
209
+ error: authErr
210
+ });
204
211
  }
205
- throw error;
212
+ throw authErr;
206
213
  }
207
214
  if (!this.refreshTokenPromise) {
208
215
  this.refreshTokenPromise = Promise.resolve().then(() => this.config.refreshTokenFn({
@@ -229,8 +236,17 @@
229
236
  return this.config.token || '';
230
237
  }
231
238
  this.stopBackgroundRefresh();
232
- this.config.onTokenExpired?.(error);
233
- throw error;
239
+ const errMsg = error && (error.message || String(error)) || 'Token 刷新失败,请重新登录';
240
+ this.config.onAuthExpired?.({
241
+ reason,
242
+ message: errMsg,
243
+ noRefreshFn: false,
244
+ error
245
+ });
246
+ const authErr = new Error(errMsg);
247
+ authErr._authExpired = true;
248
+ authErr._authReason = reason;
249
+ throw authErr;
234
250
  }).finally(() => {
235
251
  this.refreshTokenPromise = null;
236
252
  });
@@ -362,6 +378,9 @@
362
378
  }
363
379
  return result;
364
380
  } catch (error) {
381
+ if (error && error._authExpired) {
382
+ return this.createError(ERROR_CODES.AUTH_FAILED, error.message || '登录已过期,请重新登录');
383
+ }
365
384
  if (error && error.name === 'AbortError') {
366
385
  return this.createError(ERROR_CODES.TIMEOUT_ERROR, '请求超时,请稍后重试');
367
386
  }
@@ -456,6 +475,11 @@
456
475
  try {
457
476
  return await this.executeMessageStream(prompt, handlers, options, false);
458
477
  } catch (error) {
478
+ if (error && error._authExpired) {
479
+ const message = error.message || '登录已过期,请重新登录';
480
+ handlers.onError?.(message, error);
481
+ return this.createError(ERROR_CODES.AUTH_FAILED, message);
482
+ }
459
483
  if (error && error.name === 'AbortError') {
460
484
  const message = '请求超时或已取消';
461
485
  handlers.onError?.(message, error.sseData || error);
@@ -3600,7 +3624,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
3600
3624
  tokenRefreshBufferMs: 60000,
3601
3625
  refreshTokenFn: null,
3602
3626
  onTokenRefreshed: null,
3603
- onTokenExpired: null,
3627
+ onAuthExpired: null,
3604
3628
  authErrorCodes: [],
3605
3629
  title: 'AI 助手',
3606
3630
  welcomeText: 'Hi,我是智能助手 ~\n欢迎随时提问',
@@ -6066,7 +6090,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
6066
6090
  if (!text || typeof text !== 'string') return false;
6067
6091
  if (text === '\n' || text === '\n\n') return false;
6068
6092
  const p = text.trim();
6069
- if (!p) return true;
6093
+ if (!p) return false;
6070
6094
  return RUNTIME_PLACEHOLDERS.some(k => p.includes(k));
6071
6095
  }
6072
6096
  _isEventLike(text) {
@@ -6302,8 +6326,11 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
6302
6326
  handleError(err) {
6303
6327
  let displayMsg = '抱歉,网络请求失败,请稍后重试';
6304
6328
  const errMsg = err?.message || '';
6329
+ const errCode = err?.code;
6305
6330
  // 按错误类型分类
6306
- if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('网络')) {
6331
+ if (err?._authExpired || errCode === 40301) {
6332
+ displayMsg = '登录已过期,请重新登录后再试';
6333
+ } else if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('网络')) {
6307
6334
  displayMsg = '网络连接失败,请检查网络后重试';
6308
6335
  } else if (errMsg.includes('AbortError') || errMsg.includes('超时') || errMsg.includes('timeout')) {
6309
6336
  displayMsg = '请求超时,请稍后重试';