ibc-ai-web-sdk 2.1.0 → 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 +51 -13
- package/dist/index.cjs.js +56 -10
- package/dist/index.esm.js +56 -10
- package/dist/index.umd.js +56 -10
- package/dist/index.umd.min.js +1 -1
- package/dist-ts/AIChatDialog.d.ts +1 -0
- package/dist-ts/config/defaultConfig.d.ts +1 -1
- package/dist-ts/types.d.ts +15 -6
- package/package.json +4 -7
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
|
-
|
|
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: '
|
|
94
|
-
tokenExpiresAt: Date.now() +
|
|
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
|
-
|
|
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({
|
|
106
|
+
body: JSON.stringify({ userId })
|
|
102
107
|
});
|
|
103
108
|
const data = await res.json();
|
|
104
109
|
return {
|
|
105
|
-
token: data.
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
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}
|
|
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
|
|
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.
|
|
203
|
+
this.config.onAuthExpired?.({
|
|
204
|
+
reason,
|
|
205
|
+
message: authErr.message,
|
|
206
|
+
noRefreshFn: true,
|
|
207
|
+
error: authErr
|
|
208
|
+
});
|
|
202
209
|
}
|
|
203
|
-
throw
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
3625
|
+
onAuthExpired: null,
|
|
3602
3626
|
authErrorCodes: [],
|
|
3603
3627
|
title: 'AI 助手',
|
|
3604
3628
|
welcomeText: 'Hi,我是智能助手 ~\n欢迎随时提问',
|
|
@@ -3634,6 +3658,8 @@ class AIChatDialog extends HTMLElement {
|
|
|
3634
3658
|
debug: false,
|
|
3635
3659
|
// P4: 自定义消息操作按钮 — 返回数组 [{label, className?, onClick}] 在 AI 消息下方渲染
|
|
3636
3660
|
onMessageActions: null,
|
|
3661
|
+
// P5: 浮窗按钮显隐控制 — false 时浮窗按钮隐藏,对话框只能通过 API 打开(登录页等场景)
|
|
3662
|
+
floatButton: true,
|
|
3637
3663
|
suggestions: [{
|
|
3638
3664
|
label: '帮我购买一台轻量应用服务器用于部署应用',
|
|
3639
3665
|
value: '帮我购买一台轻量应用服务器用于部署应用'
|
|
@@ -3647,6 +3673,7 @@ class AIChatDialog extends HTMLElement {
|
|
|
3647
3673
|
this._chatClient = new AIChatClient(this._config);
|
|
3648
3674
|
if (this._conversationId) this._chatClient.setConversationId(this._conversationId);
|
|
3649
3675
|
this.applyConfig();
|
|
3676
|
+
this.setFloatButtonVisible(this._config.floatButton !== false);
|
|
3650
3677
|
// P1-2 fix: 在 _config 就绪后初始化拖拽,以正确读取 enableDrag
|
|
3651
3678
|
if (this._config?.enableDrag !== false && this.getAttribute('mode') !== 'inline' && !this._dragInitialized) {
|
|
3652
3679
|
this.initDraggable();
|
|
@@ -3683,6 +3710,21 @@ class AIChatDialog extends HTMLElement {
|
|
|
3683
3710
|
if (this._chatClient) this._chatClient.dispose();
|
|
3684
3711
|
if (this.parentNode) this.parentNode.removeChild(this);
|
|
3685
3712
|
}
|
|
3713
|
+
// P5: 运行时动态控制浮窗按钮显隐(SPA 路由切换场景:登录页隐藏,业务页显示)
|
|
3714
|
+
setFloatButtonVisible(show) {
|
|
3715
|
+
if (!this._config) this.init({
|
|
3716
|
+
floatButton: show
|
|
3717
|
+
});else this._config.floatButton = show;
|
|
3718
|
+
const btn = this.shadowRoot?.querySelector('.float-button');
|
|
3719
|
+
if (btn) {
|
|
3720
|
+
const dialog = this.shadowRoot?.querySelector('.ai-chat-dialog');
|
|
3721
|
+
const isOpen = dialog?.classList.contains('dialog-visible');
|
|
3722
|
+
if (!isOpen) {
|
|
3723
|
+
btn.style.display = show ? 'flex' : 'none';
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
return this;
|
|
3727
|
+
}
|
|
3686
3728
|
addMessage(message) {
|
|
3687
3729
|
if (!message || !message.content) return this;
|
|
3688
3730
|
const msg = {
|
|
@@ -3725,6 +3767,7 @@ class AIChatDialog extends HTMLElement {
|
|
|
3725
3767
|
if (!this._config.appId && this._config.agentAppId) this._config.appId = this._config.agentAppId;
|
|
3726
3768
|
if (this._chatClient) this._chatClient.updateConfig(this._config);
|
|
3727
3769
|
this.loadAppDetail();
|
|
3770
|
+
if ('floatButton' in config) this.setFloatButtonVisible(this._config.floatButton !== false);
|
|
3728
3771
|
}
|
|
3729
3772
|
this.applyConfig();
|
|
3730
3773
|
return this;
|
|
@@ -3785,7 +3828,7 @@ class AIChatDialog extends HTMLElement {
|
|
|
3785
3828
|
const dialog = this.shadowRoot.querySelector('.ai-chat-dialog');
|
|
3786
3829
|
const btn = this.shadowRoot.querySelector('.float-button');
|
|
3787
3830
|
if (dialog) dialog.classList.remove('dialog-visible');
|
|
3788
|
-
if (btn) btn.style.display = 'flex';
|
|
3831
|
+
if (btn) btn.style.display = this._config?.floatButton !== false ? 'flex' : 'none';
|
|
3789
3832
|
}
|
|
3790
3833
|
// ==================== 渲染(完整CSS对齐theme.css) ====================
|
|
3791
3834
|
render() {
|
|
@@ -6045,7 +6088,7 @@ class AIChatDialog extends HTMLElement {
|
|
|
6045
6088
|
if (!text || typeof text !== 'string') return false;
|
|
6046
6089
|
if (text === '\n' || text === '\n\n') return false;
|
|
6047
6090
|
const p = text.trim();
|
|
6048
|
-
if (!p) return
|
|
6091
|
+
if (!p) return false;
|
|
6049
6092
|
return RUNTIME_PLACEHOLDERS.some(k => p.includes(k));
|
|
6050
6093
|
}
|
|
6051
6094
|
_isEventLike(text) {
|
|
@@ -6281,8 +6324,11 @@ class AIChatDialog extends HTMLElement {
|
|
|
6281
6324
|
handleError(err) {
|
|
6282
6325
|
let displayMsg = '抱歉,网络请求失败,请稍后重试';
|
|
6283
6326
|
const errMsg = err?.message || '';
|
|
6327
|
+
const errCode = err?.code;
|
|
6284
6328
|
// 按错误类型分类
|
|
6285
|
-
if (
|
|
6329
|
+
if (err?._authExpired || errCode === 40301) {
|
|
6330
|
+
displayMsg = '登录已过期,请重新登录后再试';
|
|
6331
|
+
} else if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('网络')) {
|
|
6286
6332
|
displayMsg = '网络连接失败,请检查网络后重试';
|
|
6287
6333
|
} else if (errMsg.includes('AbortError') || errMsg.includes('超时') || errMsg.includes('timeout')) {
|
|
6288
6334
|
displayMsg = '请求超时,请稍后重试';
|
|
@@ -6660,7 +6706,7 @@ function installVuePlugin(Vue) {
|
|
|
6660
6706
|
}
|
|
6661
6707
|
function useAIChat(options = {}) {
|
|
6662
6708
|
if (typeof React === 'undefined') {
|
|
6663
|
-
console.warn('[AI Web SDK] React is not loaded
|
|
6709
|
+
console.warn('[AI Web SDK] React is not loaded globally — useAIChat requires React on window (CDN mode) or use the imperative createAIChatDialog() API directly');
|
|
6664
6710
|
return {
|
|
6665
6711
|
open: () => {},
|
|
6666
6712
|
close: () => {},
|
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
|
|
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.
|
|
199
|
+
this.config.onAuthExpired?.({
|
|
200
|
+
reason,
|
|
201
|
+
message: authErr.message,
|
|
202
|
+
noRefreshFn: true,
|
|
203
|
+
error: authErr
|
|
204
|
+
});
|
|
198
205
|
}
|
|
199
|
-
throw
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
3621
|
+
onAuthExpired: null,
|
|
3598
3622
|
authErrorCodes: [],
|
|
3599
3623
|
title: 'AI 助手',
|
|
3600
3624
|
welcomeText: 'Hi,我是智能助手 ~\n欢迎随时提问',
|
|
@@ -3630,6 +3654,8 @@ class AIChatDialog extends HTMLElement {
|
|
|
3630
3654
|
debug: false,
|
|
3631
3655
|
// P4: 自定义消息操作按钮 — 返回数组 [{label, className?, onClick}] 在 AI 消息下方渲染
|
|
3632
3656
|
onMessageActions: null,
|
|
3657
|
+
// P5: 浮窗按钮显隐控制 — false 时浮窗按钮隐藏,对话框只能通过 API 打开(登录页等场景)
|
|
3658
|
+
floatButton: true,
|
|
3633
3659
|
suggestions: [{
|
|
3634
3660
|
label: '帮我购买一台轻量应用服务器用于部署应用',
|
|
3635
3661
|
value: '帮我购买一台轻量应用服务器用于部署应用'
|
|
@@ -3643,6 +3669,7 @@ class AIChatDialog extends HTMLElement {
|
|
|
3643
3669
|
this._chatClient = new AIChatClient(this._config);
|
|
3644
3670
|
if (this._conversationId) this._chatClient.setConversationId(this._conversationId);
|
|
3645
3671
|
this.applyConfig();
|
|
3672
|
+
this.setFloatButtonVisible(this._config.floatButton !== false);
|
|
3646
3673
|
// P1-2 fix: 在 _config 就绪后初始化拖拽,以正确读取 enableDrag
|
|
3647
3674
|
if (this._config?.enableDrag !== false && this.getAttribute('mode') !== 'inline' && !this._dragInitialized) {
|
|
3648
3675
|
this.initDraggable();
|
|
@@ -3679,6 +3706,21 @@ class AIChatDialog extends HTMLElement {
|
|
|
3679
3706
|
if (this._chatClient) this._chatClient.dispose();
|
|
3680
3707
|
if (this.parentNode) this.parentNode.removeChild(this);
|
|
3681
3708
|
}
|
|
3709
|
+
// P5: 运行时动态控制浮窗按钮显隐(SPA 路由切换场景:登录页隐藏,业务页显示)
|
|
3710
|
+
setFloatButtonVisible(show) {
|
|
3711
|
+
if (!this._config) this.init({
|
|
3712
|
+
floatButton: show
|
|
3713
|
+
});else this._config.floatButton = show;
|
|
3714
|
+
const btn = this.shadowRoot?.querySelector('.float-button');
|
|
3715
|
+
if (btn) {
|
|
3716
|
+
const dialog = this.shadowRoot?.querySelector('.ai-chat-dialog');
|
|
3717
|
+
const isOpen = dialog?.classList.contains('dialog-visible');
|
|
3718
|
+
if (!isOpen) {
|
|
3719
|
+
btn.style.display = show ? 'flex' : 'none';
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
return this;
|
|
3723
|
+
}
|
|
3682
3724
|
addMessage(message) {
|
|
3683
3725
|
if (!message || !message.content) return this;
|
|
3684
3726
|
const msg = {
|
|
@@ -3721,6 +3763,7 @@ class AIChatDialog extends HTMLElement {
|
|
|
3721
3763
|
if (!this._config.appId && this._config.agentAppId) this._config.appId = this._config.agentAppId;
|
|
3722
3764
|
if (this._chatClient) this._chatClient.updateConfig(this._config);
|
|
3723
3765
|
this.loadAppDetail();
|
|
3766
|
+
if ('floatButton' in config) this.setFloatButtonVisible(this._config.floatButton !== false);
|
|
3724
3767
|
}
|
|
3725
3768
|
this.applyConfig();
|
|
3726
3769
|
return this;
|
|
@@ -3781,7 +3824,7 @@ class AIChatDialog extends HTMLElement {
|
|
|
3781
3824
|
const dialog = this.shadowRoot.querySelector('.ai-chat-dialog');
|
|
3782
3825
|
const btn = this.shadowRoot.querySelector('.float-button');
|
|
3783
3826
|
if (dialog) dialog.classList.remove('dialog-visible');
|
|
3784
|
-
if (btn) btn.style.display = 'flex';
|
|
3827
|
+
if (btn) btn.style.display = this._config?.floatButton !== false ? 'flex' : 'none';
|
|
3785
3828
|
}
|
|
3786
3829
|
// ==================== 渲染(完整CSS对齐theme.css) ====================
|
|
3787
3830
|
render() {
|
|
@@ -6041,7 +6084,7 @@ class AIChatDialog extends HTMLElement {
|
|
|
6041
6084
|
if (!text || typeof text !== 'string') return false;
|
|
6042
6085
|
if (text === '\n' || text === '\n\n') return false;
|
|
6043
6086
|
const p = text.trim();
|
|
6044
|
-
if (!p) return
|
|
6087
|
+
if (!p) return false;
|
|
6045
6088
|
return RUNTIME_PLACEHOLDERS.some(k => p.includes(k));
|
|
6046
6089
|
}
|
|
6047
6090
|
_isEventLike(text) {
|
|
@@ -6277,8 +6320,11 @@ class AIChatDialog extends HTMLElement {
|
|
|
6277
6320
|
handleError(err) {
|
|
6278
6321
|
let displayMsg = '抱歉,网络请求失败,请稍后重试';
|
|
6279
6322
|
const errMsg = err?.message || '';
|
|
6323
|
+
const errCode = err?.code;
|
|
6280
6324
|
// 按错误类型分类
|
|
6281
|
-
if (
|
|
6325
|
+
if (err?._authExpired || errCode === 40301) {
|
|
6326
|
+
displayMsg = '登录已过期,请重新登录后再试';
|
|
6327
|
+
} else if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('网络')) {
|
|
6282
6328
|
displayMsg = '网络连接失败,请检查网络后重试';
|
|
6283
6329
|
} else if (errMsg.includes('AbortError') || errMsg.includes('超时') || errMsg.includes('timeout')) {
|
|
6284
6330
|
displayMsg = '请求超时,请稍后重试';
|
|
@@ -6656,7 +6702,7 @@ function installVuePlugin(Vue) {
|
|
|
6656
6702
|
}
|
|
6657
6703
|
function useAIChat(options = {}) {
|
|
6658
6704
|
if (typeof React === 'undefined') {
|
|
6659
|
-
console.warn('[AI Web SDK] React is not loaded
|
|
6705
|
+
console.warn('[AI Web SDK] React is not loaded globally — useAIChat requires React on window (CDN mode) or use the imperative createAIChatDialog() API directly');
|
|
6660
6706
|
return {
|
|
6661
6707
|
open: () => {},
|
|
6662
6708
|
close: () => {},
|
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
|
|
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.
|
|
205
|
+
this.config.onAuthExpired?.({
|
|
206
|
+
reason,
|
|
207
|
+
message: authErr.message,
|
|
208
|
+
noRefreshFn: true,
|
|
209
|
+
error: authErr
|
|
210
|
+
});
|
|
204
211
|
}
|
|
205
|
-
throw
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
3627
|
+
onAuthExpired: null,
|
|
3604
3628
|
authErrorCodes: [],
|
|
3605
3629
|
title: 'AI 助手',
|
|
3606
3630
|
welcomeText: 'Hi,我是智能助手 ~\n欢迎随时提问',
|
|
@@ -3636,6 +3660,8 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
|
|
|
3636
3660
|
debug: false,
|
|
3637
3661
|
// P4: 自定义消息操作按钮 — 返回数组 [{label, className?, onClick}] 在 AI 消息下方渲染
|
|
3638
3662
|
onMessageActions: null,
|
|
3663
|
+
// P5: 浮窗按钮显隐控制 — false 时浮窗按钮隐藏,对话框只能通过 API 打开(登录页等场景)
|
|
3664
|
+
floatButton: true,
|
|
3639
3665
|
suggestions: [{
|
|
3640
3666
|
label: '帮我购买一台轻量应用服务器用于部署应用',
|
|
3641
3667
|
value: '帮我购买一台轻量应用服务器用于部署应用'
|
|
@@ -3649,6 +3675,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
|
|
|
3649
3675
|
this._chatClient = new AIChatClient(this._config);
|
|
3650
3676
|
if (this._conversationId) this._chatClient.setConversationId(this._conversationId);
|
|
3651
3677
|
this.applyConfig();
|
|
3678
|
+
this.setFloatButtonVisible(this._config.floatButton !== false);
|
|
3652
3679
|
// P1-2 fix: 在 _config 就绪后初始化拖拽,以正确读取 enableDrag
|
|
3653
3680
|
if (this._config?.enableDrag !== false && this.getAttribute('mode') !== 'inline' && !this._dragInitialized) {
|
|
3654
3681
|
this.initDraggable();
|
|
@@ -3685,6 +3712,21 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
|
|
|
3685
3712
|
if (this._chatClient) this._chatClient.dispose();
|
|
3686
3713
|
if (this.parentNode) this.parentNode.removeChild(this);
|
|
3687
3714
|
}
|
|
3715
|
+
// P5: 运行时动态控制浮窗按钮显隐(SPA 路由切换场景:登录页隐藏,业务页显示)
|
|
3716
|
+
setFloatButtonVisible(show) {
|
|
3717
|
+
if (!this._config) this.init({
|
|
3718
|
+
floatButton: show
|
|
3719
|
+
});else this._config.floatButton = show;
|
|
3720
|
+
const btn = this.shadowRoot?.querySelector('.float-button');
|
|
3721
|
+
if (btn) {
|
|
3722
|
+
const dialog = this.shadowRoot?.querySelector('.ai-chat-dialog');
|
|
3723
|
+
const isOpen = dialog?.classList.contains('dialog-visible');
|
|
3724
|
+
if (!isOpen) {
|
|
3725
|
+
btn.style.display = show ? 'flex' : 'none';
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
return this;
|
|
3729
|
+
}
|
|
3688
3730
|
addMessage(message) {
|
|
3689
3731
|
if (!message || !message.content) return this;
|
|
3690
3732
|
const msg = {
|
|
@@ -3727,6 +3769,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
|
|
|
3727
3769
|
if (!this._config.appId && this._config.agentAppId) this._config.appId = this._config.agentAppId;
|
|
3728
3770
|
if (this._chatClient) this._chatClient.updateConfig(this._config);
|
|
3729
3771
|
this.loadAppDetail();
|
|
3772
|
+
if ('floatButton' in config) this.setFloatButtonVisible(this._config.floatButton !== false);
|
|
3730
3773
|
}
|
|
3731
3774
|
this.applyConfig();
|
|
3732
3775
|
return this;
|
|
@@ -3787,7 +3830,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
|
|
|
3787
3830
|
const dialog = this.shadowRoot.querySelector('.ai-chat-dialog');
|
|
3788
3831
|
const btn = this.shadowRoot.querySelector('.float-button');
|
|
3789
3832
|
if (dialog) dialog.classList.remove('dialog-visible');
|
|
3790
|
-
if (btn) btn.style.display = 'flex';
|
|
3833
|
+
if (btn) btn.style.display = this._config?.floatButton !== false ? 'flex' : 'none';
|
|
3791
3834
|
}
|
|
3792
3835
|
// ==================== 渲染(完整CSS对齐theme.css) ====================
|
|
3793
3836
|
render() {
|
|
@@ -6047,7 +6090,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
|
|
|
6047
6090
|
if (!text || typeof text !== 'string') return false;
|
|
6048
6091
|
if (text === '\n' || text === '\n\n') return false;
|
|
6049
6092
|
const p = text.trim();
|
|
6050
|
-
if (!p) return
|
|
6093
|
+
if (!p) return false;
|
|
6051
6094
|
return RUNTIME_PLACEHOLDERS.some(k => p.includes(k));
|
|
6052
6095
|
}
|
|
6053
6096
|
_isEventLike(text) {
|
|
@@ -6283,8 +6326,11 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
|
|
|
6283
6326
|
handleError(err) {
|
|
6284
6327
|
let displayMsg = '抱歉,网络请求失败,请稍后重试';
|
|
6285
6328
|
const errMsg = err?.message || '';
|
|
6329
|
+
const errCode = err?.code;
|
|
6286
6330
|
// 按错误类型分类
|
|
6287
|
-
if (
|
|
6331
|
+
if (err?._authExpired || errCode === 40301) {
|
|
6332
|
+
displayMsg = '登录已过期,请重新登录后再试';
|
|
6333
|
+
} else if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('网络')) {
|
|
6288
6334
|
displayMsg = '网络连接失败,请检查网络后重试';
|
|
6289
6335
|
} else if (errMsg.includes('AbortError') || errMsg.includes('超时') || errMsg.includes('timeout')) {
|
|
6290
6336
|
displayMsg = '请求超时,请稍后重试';
|
|
@@ -6662,7 +6708,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error
|
|
|
6662
6708
|
}
|
|
6663
6709
|
function useAIChat(options = {}) {
|
|
6664
6710
|
if (typeof React === 'undefined') {
|
|
6665
|
-
console.warn('[AI Web SDK] React is not loaded
|
|
6711
|
+
console.warn('[AI Web SDK] React is not loaded globally — useAIChat requires React on window (CDN mode) or use the imperative createAIChatDialog() API directly');
|
|
6666
6712
|
return {
|
|
6667
6713
|
open: () => {},
|
|
6668
6714
|
close: () => {},
|