ibc-ai-web-sdk 2.0.4 → 2.0.5

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/dist/index.esm.js CHANGED
@@ -380,8 +380,25 @@ class AIChatClient {
380
380
  if (conversationId) payload.conversationId = conversationId;
381
381
  const attachmentIds = options.attachmentIds || this.normalizeAttachmentIds(options.attachments);
382
382
  if (attachmentIds.length > 0) payload.attachmentIds = attachmentIds;
383
- const bizParams = options.bizParams || options.extendInfo || this.config.extendInfo;
384
- if (bizParams) payload.bizParams = typeof bizParams === 'string' ? bizParams : JSON.stringify(bizParams);
383
+ let bizParams = options.bizParams || options.extendInfo || this.config.extendInfo;
384
+ if (bizParams) {
385
+ if (typeof bizParams === 'string') {
386
+ try {
387
+ bizParams = JSON.parse(bizParams);
388
+ } catch (e) {
389
+ bizParams = {};
390
+ }
391
+ }
392
+ } else {
393
+ bizParams = {};
394
+ }
395
+ // P3: 将页面上下文合并到 bizParams(contextMode='bizParams' 时由 dialog 传入)
396
+ if (options.context) {
397
+ Object.assign(bizParams, options.context);
398
+ }
399
+ if (Object.keys(bizParams).length > 0) {
400
+ payload.bizParams = JSON.stringify(bizParams);
401
+ }
385
402
  return payload;
386
403
  }
387
404
  normalizeAttachmentIds(attachments) {
@@ -3564,7 +3581,7 @@ const PRESET_THEMES = {
3564
3581
  * 样式与功能完全对齐 Vue 版本(1:1 复刻)
3565
3582
  *
3566
3583
  * @author IBC AI Team
3567
- * @version 2.1.0
3584
+ * @version 2.0.5
3568
3585
  */
3569
3586
 
3570
3587
 
@@ -3655,6 +3672,8 @@ class AIChatDialog extends HTMLElement {
3655
3672
  this._runtimeEventsContainerEl = null;
3656
3673
  this._runtimeStatusEl = null;
3657
3674
  this._runtimeCountEl = null;
3675
+ // P3: 页面上下文数据(宿主页面选中的数据)
3676
+ this._context = {};
3658
3677
 
3659
3678
  // ====== 默认主题配置(与 themes.js DEFAULT_THEME 完全一致) ======
3660
3679
  this._themeVars = {
@@ -3741,6 +3760,8 @@ class AIChatDialog extends HTMLElement {
3741
3760
  if (this._handleOffline) window.removeEventListener('offline', this._handleOffline);
3742
3761
  // P2-17: 清理resize监听
3743
3762
  if (this._handleResize) window.removeEventListener('resize', this._handleResize);
3763
+ // P0: 清理拖拽事件监听
3764
+ this._cleanupDragListeners();
3744
3765
  // 暂停后台Token刷新定时器(detach时停止,re-attach时由connectedCallback恢复)
3745
3766
  if (this._chatClient) {
3746
3767
  this._chatClient.stopBackgroundRefresh();
@@ -3892,6 +3913,14 @@ class AIChatDialog extends HTMLElement {
3892
3913
  authFn: null,
3893
3914
  onLoad: null,
3894
3915
  onError: null,
3916
+ // P3: 发送前回调 - 每发消息前调用,可返回最新页面上下文(与 setContext 合并,此回调结果优先)
3917
+ onBeforeSend: null,
3918
+ // P3: 上下文注入方式 — 'prompt'=拼到用户消息前面(默认), 'bizParams'=注入到 bizParams JSON 中
3919
+ contextMode: 'prompt',
3920
+ // P4: 调试模式 - 开启后 console 打印请求/响应/SSE/上下文合并全过程
3921
+ debug: false,
3922
+ // P4: 自定义消息操作按钮 — 返回数组 [{label, className?, onClick}] 在 AI 消息下方渲染
3923
+ onMessageActions: null,
3895
3924
  suggestions: [{
3896
3925
  label: '帮我购买一台轻量应用服务器用于部署应用',
3897
3926
  value: '帮我购买一台轻量应用服务器用于部署应用'
@@ -3905,6 +3934,11 @@ class AIChatDialog extends HTMLElement {
3905
3934
  this._chatClient = new AIChatClient(this._config);
3906
3935
  if (this._conversationId) this._chatClient.setConversationId(this._conversationId);
3907
3936
  this.applyConfig();
3937
+ // P1-2 fix: 在 _config 就绪后初始化拖拽,以正确读取 enableDrag
3938
+ if (this._config?.enableDrag !== false && this.getAttribute('mode') !== 'inline' && !this._dragInitialized) {
3939
+ this.initDraggable();
3940
+ }
3941
+ this._debugLog('初始化完成', 'appId=' + this._config.appId, 'userId=' + this._config.userId, 'baseUrl=' + this._config.apiBaseUrl, 'debug=' + !!this._config.debug, 'contextMode=' + (this._config.contextMode || 'prompt'));
3908
3942
  this.loadAppDetail();
3909
3943
  // 触发 onLoad 回调(与Vue版一致)
3910
3944
  if (this._config?.onLoad) this._config.onLoad(this);
@@ -3991,6 +4025,44 @@ class AIChatDialog extends HTMLElement {
3991
4025
  return this;
3992
4026
  }
3993
4027
 
4028
+ /**
4029
+ * 设置页面上下文数据(宿主页面选中的数据,随每次请求发送)
4030
+ * @param {Object} context - 上下文数据对象,传 null 清空
4031
+ * @returns {this}
4032
+ */
4033
+ setContext(context) {
4034
+ this._context = context && typeof context === 'object' ? {
4035
+ ...context
4036
+ } : {};
4037
+ return this;
4038
+ }
4039
+
4040
+ /**
4041
+ * 获取当前上下文数据
4042
+ * @returns {Object}
4043
+ */
4044
+ getContext() {
4045
+ return {
4046
+ ...this._context
4047
+ };
4048
+ }
4049
+
4050
+ /**
4051
+ * 外部触发表态发送(业务层主动调用,如点击外部按钮触发 AI 请求)
4052
+ * @param {string} text - 要发送的消息内容
4053
+ * @returns {this}
4054
+ */
4055
+ send(text) {
4056
+ const content = String(text || '').trim();
4057
+ if (!content) return this;
4058
+ if (this._input) this._input.value = content;
4059
+ this._autoResizeInput();
4060
+ this._updateSendButtonState();
4061
+ this._lastSendTime = 0; // 跳过防抖,外部触发视为独立操作
4062
+ this.handleSend();
4063
+ return this;
4064
+ }
4065
+
3994
4066
  // ==================== 显示/隐藏 ====================
3995
4067
 
3996
4068
  showDialog() {
@@ -4626,6 +4698,19 @@ class AIChatDialog extends HTMLElement {
4626
4698
  color: var(--ai-success);
4627
4699
  background: rgba(16, 185, 129, 0.08);
4628
4700
  }
4701
+ /* P4: 自定义操作按钮(文本标签型,宽度自适应) */
4702
+ .action-icon.custom-action-btn {
4703
+ width: auto;
4704
+ padding: 4px 10px;
4705
+ font-size: 12px;
4706
+ font-weight: 500;
4707
+ white-space: nowrap;
4708
+ }
4709
+ .action-icon.custom-action-btn:hover {
4710
+ color: #fff;
4711
+ background: var(--ai-primary);
4712
+ transform: translateY(-1px);
4713
+ }
4629
4714
 
4630
4715
  /* ========== 底部输入框(与Vue .dialog-footer 一致) ========== */
4631
4716
  .dialog-footer {
@@ -5206,8 +5291,8 @@ class AIChatDialog extends HTMLElement {
5206
5291
  });
5207
5292
  }
5208
5293
 
5209
- // 拖拽(内嵌模式禁用)
5210
- if (this._dialog && this._config?.enableDrag !== false && this.getAttribute('mode') !== 'inline') this.initDraggable();
5294
+ // 拖拽(内嵌模式禁用)— 由 init() 在 _config 就绪后触发
5295
+ // initDraggable 延迟到 init() 中执行以免 _config 为空
5211
5296
  });
5212
5297
  }
5213
5298
 
@@ -5260,59 +5345,97 @@ class AIChatDialog extends HTMLElement {
5260
5345
  initDraggable() {
5261
5346
  const header = this.shadowRoot?.querySelector('.dialog-header');
5262
5347
  if (!header) return;
5348
+
5349
+ // 清理上一次遗留的 document 级监听(SPA 路由切换场景)
5350
+ this._cleanupDragListeners();
5351
+ const self = this;
5263
5352
  const startDrag = (clientX, clientY) => {
5264
5353
  // P2-17: 移动端禁用拖拽(与Vue版 startTouchDrag: if(isExpanded || isMobile) return 一致)
5265
- if (this._isExpanded || this._isMobile) return;
5266
- this._isDragging = true;
5267
- this._dialog.classList.add('dragging');
5268
- const rect = this._dialog.getBoundingClientRect();
5269
- this._dragStartX = clientX - rect.left;
5270
- this._dragStartY = clientY - rect.top;
5354
+ if (self._isExpanded || self._isMobile) return;
5355
+ self._isDragging = true;
5356
+ self._dialog.classList.add('dragging');
5357
+ const rect = self._dialog.getBoundingClientRect();
5358
+ self._dragStartX = clientX - rect.left;
5359
+ self._dragStartY = clientY - rect.top;
5271
5360
  };
5272
5361
  const onDrag = (clientX, clientY) => {
5273
- if (!this._isDragging) return;
5274
- this._dialogX = clientX - this._dragStartX;
5275
- this._dialogY = clientY - this._dragStartY;
5276
- this._dialog.style.left = this._dialogX + 'px';
5277
- this._dialog.style.right = 'auto';
5278
- this._dialog.style.top = this._dialogY + 'px';
5279
- this._dialog.style.bottom = 'auto';
5362
+ if (!self._isDragging) return;
5363
+ self._dialogX = clientX - self._dragStartX;
5364
+ self._dialogY = clientY - self._dragStartY;
5365
+ self._dialog.style.left = self._dialogX + 'px';
5366
+ self._dialog.style.right = 'auto';
5367
+ self._dialog.style.top = self._dialogY + 'px';
5368
+ self._dialog.style.bottom = 'auto';
5280
5369
  };
5281
5370
  const stopDrag = () => {
5282
- if (this._isDragging) {
5283
- this._isDragging = false;
5284
- this._dialog.classList.remove('dragging');
5371
+ if (self._isDragging) {
5372
+ self._isDragging = false;
5373
+ self._dialog.classList.remove('dragging');
5285
5374
  }
5286
5375
  };
5287
5376
 
5288
- // 鼠标事件(桌面端)
5289
- header.addEventListener('mousedown', e => {
5377
+ // 鼠标事件(桌面端)— 保存引用以便清理
5378
+ this._dragMouseDownHandler = e => {
5290
5379
  if (e.target.closest('button, .header-icon')) return;
5291
5380
  startDrag(e.clientX, e.clientY);
5292
5381
  e.preventDefault();
5293
- });
5294
- document.addEventListener('mousemove', e => {
5382
+ };
5383
+ this._dragMouseMoveHandler = e => {
5295
5384
  onDrag(e.clientX, e.clientY);
5296
- });
5297
- document.addEventListener('mouseup', stopDrag);
5385
+ };
5386
+ this._dragMouseUpHandler = stopDrag;
5387
+ header.addEventListener('mousedown', this._dragMouseDownHandler);
5388
+ document.addEventListener('mousemove', this._dragMouseMoveHandler);
5389
+ document.addEventListener('mouseup', this._dragMouseUpHandler);
5298
5390
 
5299
5391
  // P1-7: 触摸事件(移动端,与Vue版一致)
5300
- header.addEventListener('touchstart', e => {
5301
- if (this._isExpanded) return;
5392
+ this._dragTouchStartHandler = e => {
5393
+ if (self._isExpanded) return;
5302
5394
  if (e.target.closest('button, .header-icon')) return;
5303
5395
  const touch = e.touches[0];
5304
5396
  startDrag(touch.clientX, touch.clientY);
5305
- }, {
5306
- passive: true
5307
- });
5308
- document.addEventListener('touchmove', e => {
5309
- if (!this._isDragging) return;
5397
+ };
5398
+ this._dragTouchMoveHandler = e => {
5399
+ if (!self._isDragging) return;
5310
5400
  const touch = e.touches[0];
5311
5401
  onDrag(touch.clientX, touch.clientY);
5312
- }, {
5402
+ };
5403
+ this._dragTouchEndHandler = stopDrag;
5404
+ header.addEventListener('touchstart', this._dragTouchStartHandler, {
5313
5405
  passive: true
5314
5406
  });
5315
- document.addEventListener('touchend', stopDrag);
5407
+ document.addEventListener('touchmove', this._dragTouchMoveHandler, {
5408
+ passive: true
5409
+ });
5410
+ document.addEventListener('touchend', this._dragTouchEndHandler);
5411
+ this._dragInitialized = true;
5412
+ }
5413
+
5414
+ // P0: 清理拖拽事件监听(SPA disconnectedCallback / re-init 调用)
5415
+ _cleanupDragListeners() {
5416
+ if (this._dragMouseDownHandler) {
5417
+ const header = this.shadowRoot?.querySelector('.dialog-header');
5418
+ if (header) {
5419
+ header.removeEventListener('mousedown', this._dragMouseDownHandler);
5420
+ header.removeEventListener('touchstart', this._dragTouchStartHandler, {
5421
+ passive: true
5422
+ });
5423
+ }
5424
+ this._dragMouseDownHandler = null;
5425
+ this._dragTouchStartHandler = null;
5426
+ }
5427
+ if (this._dragMouseMoveHandler) {
5428
+ document.removeEventListener('mousemove', this._dragMouseMoveHandler);
5429
+ document.removeEventListener('touchmove', this._dragTouchMoveHandler, {
5430
+ passive: true
5431
+ });
5432
+ document.removeEventListener('mouseup', this._dragMouseUpHandler);
5433
+ document.removeEventListener('touchend', this._dragTouchEndHandler);
5434
+ this._dragMouseMoveHandler = null;
5435
+ this._dragTouchMoveHandler = null;
5436
+ this._dragMouseUpHandler = null;
5437
+ this._dragTouchEndHandler = null;
5438
+ }
5316
5439
  }
5317
5440
  applyConfig() {
5318
5441
  if (!this._config) return;
@@ -5477,11 +5600,12 @@ class AIChatDialog extends HTMLElement {
5477
5600
  <div class="message-content">
5478
5601
  ${hasRuntimeEvents ? this._buildRuntimePanel(msg, idx) : ''}
5479
5602
  ${bubbleContent ? `<div class="${bubbleClass}">${bubbleContent}</div>` : ''}
5480
- ${!msg._isStreaming ? `<div class="message-actions-bar">
5481
- ${msg.type === 'ai' ? `<div class="action-icons">
5482
- <span class="action-icon copy-btn${msg.copied ? ' copied' : ''}" data-idx="${idx}" title="${msg.copied ? '已复制' : '复制'}">${msg.copied ? '<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="m20 6-11 11-5-5"/></svg>' : '<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>'}</span>
5483
- <span class="action-icon regenerate-btn" data-idx="${idx}" title="重新回答"><svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg></span>
5484
- </div>` : ''}
5603
+ ${!msg._isStreaming ? `<div class="message-actions-bar">
5604
+ ${msg.type === 'ai' ? `<div class="action-icons">
5605
+ <span class="action-icon copy-btn${msg.copied ? ' copied' : ''}" data-idx="${idx}" title="${msg.copied ? '已复制' : '复制'}">${msg.copied ? '<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="m20 6-11 11-5-5"/></svg>' : '<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>'}</span>
5606
+ <span class="action-icon regenerate-btn" data-idx="${idx}" title="重新回答"><svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg></span>
5607
+ ${this._buildCustomActions(msg, idx)}
5608
+ </div>` : ''}
5485
5609
  </div>` : ''}
5486
5610
  </div>
5487
5611
  </div>`;
@@ -5511,6 +5635,24 @@ class AIChatDialog extends HTMLElement {
5511
5635
  });
5512
5636
  });
5513
5637
 
5638
+ // P4: 绑定自定义操作按钮
5639
+ this._body.querySelectorAll('.custom-action-btn').forEach(btn => {
5640
+ btn.addEventListener('click', () => {
5641
+ const idx = parseInt(btn.dataset.idx);
5642
+ const actionIdx = parseInt(btn.dataset.action);
5643
+ const msg = this._messages[idx];
5644
+ if (!msg) return;
5645
+ try {
5646
+ const actions = this._getCustomActions(msg, idx);
5647
+ if (actions && actions[actionIdx]) {
5648
+ actions[actionIdx].onClick(msg.content);
5649
+ }
5650
+ } catch (e) {
5651
+ console.warn('[AIChatDialog] 自定义操作按钮执行失败:', e);
5652
+ }
5653
+ });
5654
+ });
5655
+
5514
5656
  // 绑定执行过程面板折叠/展开(Shadow DOM 下 inline onclick 不可靠)
5515
5657
  this._body.querySelectorAll('.runtime-header').forEach(header => {
5516
5658
  header.addEventListener('click', () => {
@@ -5763,7 +5905,7 @@ class AIChatDialog extends HTMLElement {
5763
5905
  this.showToast(this._config?.emptyMessageError || '请输入消息内容');
5764
5906
  return;
5765
5907
  }
5766
- if (this._isLoading) return;
5908
+ if (this._isLoading || this._streaming) return;
5767
5909
 
5768
5910
  // P1-11: 字数限制检查
5769
5911
  const maxLen = this._config?.maxLength ?? 500;
@@ -5819,8 +5961,47 @@ class AIChatDialog extends HTMLElement {
5819
5961
  attachmentIds.push(res.data.attachmentId);
5820
5962
  }
5821
5963
  }
5822
- await this.sendMessageToAI(content || attachmentText, {
5823
- attachmentIds
5964
+
5965
+ // P3: 收集页面上下文(setContext + onBeforeSend)
5966
+ let context = {
5967
+ ...(this._context || {})
5968
+ };
5969
+ if (typeof this._config?.onBeforeSend === 'function') {
5970
+ try {
5971
+ const dynamicCtx = await this._config.onBeforeSend();
5972
+ if (dynamicCtx && typeof dynamicCtx === 'object') {
5973
+ context = {
5974
+ ...context,
5975
+ ...dynamicCtx
5976
+ };
5977
+ }
5978
+ } catch (e) {
5979
+ console.warn('[AIChatDialog] onBeforeSend 执行失败:', e);
5980
+ }
5981
+ }
5982
+ const hasContext = context && Object.keys(context).length > 0;
5983
+ const rawMode = this._config?.contextMode;
5984
+ const VALID_CONTEXT_MODES = ['prompt', 'bizParams'];
5985
+ const contextMode = VALID_CONTEXT_MODES.includes(rawMode) ? rawMode : rawMode ? (console.warn('[AIChatDialog] 无效的 contextMode: "' + rawMode + '",已回退为 "prompt"(有效值: ' + VALID_CONTEXT_MODES.join(', ') + ')'), 'prompt') : 'prompt';
5986
+ let finalPrompt = content || attachmentText;
5987
+ let contextForBizParams = null;
5988
+ if (hasContext) {
5989
+ this._debugLog('上下文合并', 'keys=', Object.keys(context), 'mode=', contextMode);
5990
+ if (contextMode === 'bizParams') {
5991
+ // 注入到 bizParams
5992
+ contextForBizParams = context;
5993
+ } else {
5994
+ // 默认:拼到 prompt 前面
5995
+ const ctxText = this._formatContextText(context);
5996
+ if (ctxText) {
5997
+ finalPrompt = ctxText + '\n---\n' + finalPrompt;
5998
+ }
5999
+ }
6000
+ }
6001
+ this._debugLog('发送', 'prompt=' + (finalPrompt || '').substring(0, 150) + (finalPrompt && finalPrompt.length > 150 ? '...' : ''), 'ctxKeys=' + Object.keys(contextForBizParams || context || {}).join(','), 'mode=' + contextMode);
6002
+ await this.sendMessageToAI(finalPrompt, {
6003
+ attachmentIds,
6004
+ context: contextForBizParams
5824
6005
  });
5825
6006
  } catch (err) {
5826
6007
  console.error('[AIChatDialog] Error:', err);
@@ -5854,6 +6035,7 @@ class AIChatDialog extends HTMLElement {
5854
6035
  ...(options.streamOptions || {})
5855
6036
  };
5856
6037
  if (options.attachmentIds?.length) streamOpts.attachmentIds = options.attachmentIds;
6038
+ if (options.context) streamOpts.context = options.context;
5857
6039
  await this._directApiCallStream(msgId, content, streamOpts);
5858
6040
  }
5859
6041
  }
@@ -5911,6 +6093,10 @@ class AIChatDialog extends HTMLElement {
5911
6093
  if (!this._chatClient) {
5912
6094
  this._chatClient = new AIChatClient(this._config || {});
5913
6095
  }
6096
+ if (this._config?.debug) {
6097
+ this._chatClient.config.debug = true;
6098
+ }
6099
+ this._debugLog('Stream开始', 'msgId=' + msgId, 'content=' + (content || '').substring(0, 80), 'opts=' + JSON.stringify(requestOptions));
5914
6100
  if (Object.prototype.hasOwnProperty.call(requestOptions, 'conversationId')) {
5915
6101
  this._chatClient.setConversationId(requestOptions.conversationId || null);
5916
6102
  } else if (this._conversationId) {
@@ -6029,6 +6215,7 @@ class AIChatDialog extends HTMLElement {
6029
6215
  }
6030
6216
  }, requestOptions);
6031
6217
  if (response?.success) {
6218
+ this._debugLog('Stream完成', 'ok, conversationId=' + (response.data?.conversationId || this._conversationId), 'textLen=' + streamText.length);
6032
6219
  if (response.data?.conversationId) {
6033
6220
  this._conversationId = response.data.conversationId;
6034
6221
  } else if (this._chatClient.conversationId) {
@@ -6041,6 +6228,7 @@ class AIChatDialog extends HTMLElement {
6041
6228
  streamText = result;
6042
6229
  }
6043
6230
  } else {
6231
+ this._finalizeMsg(msgId, streamText || '(无回复)');
6044
6232
  return;
6045
6233
  }
6046
6234
  this._finalizeMsg(msgId, streamText || '(无回复)');
@@ -6051,12 +6239,14 @@ class AIChatDialog extends HTMLElement {
6051
6239
  }));
6052
6240
  if (this._config?.onMessageReceived) this._config.onMessageReceived(streamText);
6053
6241
  } catch (e) {
6242
+ this._debugWarn('Stream异常', e.name + ': ' + (e.message || ''));
6054
6243
  if (e.name === 'AbortError') {
6055
6244
  this._updateMsg(msgId, '(请求超时或已取消)');
6056
6245
  } else {
6057
6246
  console.error('[AIChatDialog] 流式失败:', e);
6058
6247
  this._updateMsg(msgId, '网络错误: ' + (e.message || '请检查连接'));
6059
6248
  }
6249
+ this._finalizeMsg(msgId, this._messages.find(m => m.id === msgId)?.content || '(无回复)');
6060
6250
  }
6061
6251
  }
6062
6252
  _getStreamTextEl() {
@@ -6180,6 +6370,27 @@ class AIChatDialog extends HTMLElement {
6180
6370
  return this._ERROR_CONTENT_PATTERNS.some(p => content.startsWith(p));
6181
6371
  }
6182
6372
 
6373
+ // P3: 将上下文对象格式化为可读文本(拼入 prompt 前缀)
6374
+ _formatContextText(context) {
6375
+ if (!context || typeof context !== 'object') return '';
6376
+ const lines = ['[页面上下文]'];
6377
+ for (const key in context) {
6378
+ if (!Object.prototype.hasOwnProperty.call(context, key)) continue;
6379
+ const val = context[key];
6380
+ if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object') {
6381
+ lines.push(key + ':');
6382
+ for (const item of val) {
6383
+ lines.push(' - ' + Object.values(item).join(' | '));
6384
+ }
6385
+ } else if (typeof val === 'object' && val !== null) {
6386
+ lines.push(key + ': ' + JSON.stringify(val));
6387
+ } else {
6388
+ lines.push(key + ': ' + val);
6389
+ }
6390
+ }
6391
+ return lines.join('\n');
6392
+ }
6393
+
6183
6394
  // 运行时事件管理 – incremental DOM, no renderMessages
6184
6395
  _addRuntimeEvent(msgId, event) {
6185
6396
  const idx = this._messages.findIndex(m => m.id === msgId);
@@ -6274,6 +6485,30 @@ class AIChatDialog extends HTMLElement {
6274
6485
  panel.appendChild(eventsContainer);
6275
6486
  return panel;
6276
6487
  }
6488
+
6489
+ // P4: 获取自定义操作按钮列表(缓存结果避免重复调用 onMessageActions)
6490
+ _getCustomActions(msg, idx) {
6491
+ const key = '_customActions_' + idx;
6492
+ if (!msg[key] && typeof this._config?.onMessageActions === 'function') {
6493
+ try {
6494
+ msg[key] = this._config.onMessageActions(msg, idx) || [];
6495
+ } catch (e) {
6496
+ console.warn('[AIChatDialog] onMessageActions 执行失败:', e);
6497
+ msg[key] = [];
6498
+ }
6499
+ }
6500
+ return msg[key] || [];
6501
+ }
6502
+
6503
+ // P4: 生成自定义操作按钮 HTML
6504
+ _buildCustomActions(msg, idx) {
6505
+ const actions = this._getCustomActions(msg, idx);
6506
+ if (!actions.length) return '';
6507
+ return actions.map((a, i) => {
6508
+ const cls = a.className || '';
6509
+ return '<span class="action-icon custom-action-btn' + (cls ? ' ' + this.escapeAttr(cls) : '') + '" data-idx="' + idx + '" data-action="' + i + '" title="' + this.escapeAttr(a.label) + '">' + this.escapeHtml(a.label) + '</span>';
6510
+ }).join('');
6511
+ }
6277
6512
  _buildRuntimePanel(msg, msgIdx) {
6278
6513
  const events = msg._runtimeEvents || [];
6279
6514
  if (events.length === 0) return '';
@@ -6310,7 +6545,7 @@ class AIChatDialog extends HTMLElement {
6310
6545
  // ==================== 重新生成(与Vue regenerate 一致) ====================
6311
6546
 
6312
6547
  async regenerate(index) {
6313
- if (this._isLoading) return;
6548
+ if (this._isLoading || this._streaming) return;
6314
6549
  const aiMessage = this._messages[index];
6315
6550
  if (!aiMessage || aiMessage.type !== 'ai') return;
6316
6551
  let userQuestion = '';
@@ -6498,6 +6733,20 @@ class AIChatDialog extends HTMLElement {
6498
6733
  const n = new Date();
6499
6734
  return `${String(n.getHours()).padStart(2, '0')}:${String(n.getMinutes()).padStart(2, '0')}`;
6500
6735
  }
6736
+
6737
+ /**
6738
+ * 调试日志(仅在 config.debug === true 时输出)
6739
+ */
6740
+ _debugLog(tag, ...args) {
6741
+ if (this._config?.debug) {
6742
+ console.log('%c[AI-SDK]%c ' + tag, 'color:#2563eb;font-weight:600', 'color:inherit', ...args);
6743
+ }
6744
+ }
6745
+ _debugWarn(tag, ...args) {
6746
+ if (this._config?.debug) {
6747
+ console.warn('%c[AI-SDK]%c ' + tag, 'color:#f59e0b;font-weight:600', 'color:inherit', ...args);
6748
+ }
6749
+ }
6501
6750
  escapeHtml(s) {
6502
6751
  if (!s) return '';
6503
6752
  const d = document.createElement('div');
@@ -6588,7 +6837,7 @@ if (typeof window !== 'undefined') {
6588
6837
  * 支持任意前端框架:Vue, React, Angular, 原生HTML等
6589
6838
  *
6590
6839
  * @author IBC AI Team
6591
- * @version 2.0.4
6840
+ * @version 2.0.5
6592
6841
  */
6593
6842
 
6594
6843
 
@@ -6805,12 +7054,10 @@ function useAIChat(options = {}) {
6805
7054
  },
6806
7055
  sendMessage: content => {
6807
7056
  if (dialogRef.current) {
6808
- const input = dialogRef.current.shadowRoot?.querySelector('.ai-textarea');
7057
+ const input = dialogRef.current.shadowRoot?.querySelector('.message-input');
6809
7058
  if (input) {
6810
- input.value = content;
6811
- input.dispatchEvent(new Event('input'));
7059
+ dialogRef.current.send?.(content) || (input.value = content, input.dispatchEvent(new Event('input')), dialogRef.current.shadowRoot?.querySelector('.send-btn')?.click());
6812
7060
  }
6813
- dialogRef.current.handleSend?.() || dialogRef.current.shadowRoot?.querySelector('.ai-send-btn')?.click();
6814
7061
  }
6815
7062
  },
6816
7063
  clearMessages: () => {
@@ -6831,14 +7078,14 @@ var index = {
6831
7078
  createAIChatDialog,
6832
7079
  installVuePlugin,
6833
7080
  useAIChat,
6834
- version: '2.0.4'
7081
+ version: '2.0.5'
6835
7082
  };
6836
7083
 
6837
7084
  // 全局暴露(UMD/浏览器环境)
6838
7085
  if (typeof window !== 'undefined') {
6839
7086
  window.AICreateChatDialog = createAIChatDialog;
6840
7087
  window.AIWebSDK = {
6841
- version: '2.0.4',
7088
+ version: '2.0.5',
6842
7089
  create: createAIChatDialog,
6843
7090
  AIChatDialog,
6844
7091
  AIChatClient,