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.cjs.js CHANGED
@@ -384,8 +384,25 @@ class AIChatClient {
384
384
  if (conversationId) payload.conversationId = conversationId;
385
385
  const attachmentIds = options.attachmentIds || this.normalizeAttachmentIds(options.attachments);
386
386
  if (attachmentIds.length > 0) payload.attachmentIds = attachmentIds;
387
- const bizParams = options.bizParams || options.extendInfo || this.config.extendInfo;
388
- if (bizParams) payload.bizParams = typeof bizParams === 'string' ? bizParams : JSON.stringify(bizParams);
387
+ let bizParams = options.bizParams || options.extendInfo || this.config.extendInfo;
388
+ if (bizParams) {
389
+ if (typeof bizParams === 'string') {
390
+ try {
391
+ bizParams = JSON.parse(bizParams);
392
+ } catch (e) {
393
+ bizParams = {};
394
+ }
395
+ }
396
+ } else {
397
+ bizParams = {};
398
+ }
399
+ // P3: 将页面上下文合并到 bizParams(contextMode='bizParams' 时由 dialog 传入)
400
+ if (options.context) {
401
+ Object.assign(bizParams, options.context);
402
+ }
403
+ if (Object.keys(bizParams).length > 0) {
404
+ payload.bizParams = JSON.stringify(bizParams);
405
+ }
389
406
  return payload;
390
407
  }
391
408
  normalizeAttachmentIds(attachments) {
@@ -3568,7 +3585,7 @@ const PRESET_THEMES = {
3568
3585
  * 样式与功能完全对齐 Vue 版本(1:1 复刻)
3569
3586
  *
3570
3587
  * @author IBC AI Team
3571
- * @version 2.1.0
3588
+ * @version 2.0.5
3572
3589
  */
3573
3590
 
3574
3591
 
@@ -3659,6 +3676,8 @@ class AIChatDialog extends HTMLElement {
3659
3676
  this._runtimeEventsContainerEl = null;
3660
3677
  this._runtimeStatusEl = null;
3661
3678
  this._runtimeCountEl = null;
3679
+ // P3: 页面上下文数据(宿主页面选中的数据)
3680
+ this._context = {};
3662
3681
 
3663
3682
  // ====== 默认主题配置(与 themes.js DEFAULT_THEME 完全一致) ======
3664
3683
  this._themeVars = {
@@ -3745,6 +3764,8 @@ class AIChatDialog extends HTMLElement {
3745
3764
  if (this._handleOffline) window.removeEventListener('offline', this._handleOffline);
3746
3765
  // P2-17: 清理resize监听
3747
3766
  if (this._handleResize) window.removeEventListener('resize', this._handleResize);
3767
+ // P0: 清理拖拽事件监听
3768
+ this._cleanupDragListeners();
3748
3769
  // 暂停后台Token刷新定时器(detach时停止,re-attach时由connectedCallback恢复)
3749
3770
  if (this._chatClient) {
3750
3771
  this._chatClient.stopBackgroundRefresh();
@@ -3896,6 +3917,14 @@ class AIChatDialog extends HTMLElement {
3896
3917
  authFn: null,
3897
3918
  onLoad: null,
3898
3919
  onError: null,
3920
+ // P3: 发送前回调 - 每发消息前调用,可返回最新页面上下文(与 setContext 合并,此回调结果优先)
3921
+ onBeforeSend: null,
3922
+ // P3: 上下文注入方式 — 'prompt'=拼到用户消息前面(默认), 'bizParams'=注入到 bizParams JSON 中
3923
+ contextMode: 'prompt',
3924
+ // P4: 调试模式 - 开启后 console 打印请求/响应/SSE/上下文合并全过程
3925
+ debug: false,
3926
+ // P4: 自定义消息操作按钮 — 返回数组 [{label, className?, onClick}] 在 AI 消息下方渲染
3927
+ onMessageActions: null,
3899
3928
  suggestions: [{
3900
3929
  label: '帮我购买一台轻量应用服务器用于部署应用',
3901
3930
  value: '帮我购买一台轻量应用服务器用于部署应用'
@@ -3909,6 +3938,11 @@ class AIChatDialog extends HTMLElement {
3909
3938
  this._chatClient = new AIChatClient(this._config);
3910
3939
  if (this._conversationId) this._chatClient.setConversationId(this._conversationId);
3911
3940
  this.applyConfig();
3941
+ // P1-2 fix: 在 _config 就绪后初始化拖拽,以正确读取 enableDrag
3942
+ if (this._config?.enableDrag !== false && this.getAttribute('mode') !== 'inline' && !this._dragInitialized) {
3943
+ this.initDraggable();
3944
+ }
3945
+ this._debugLog('初始化完成', 'appId=' + this._config.appId, 'userId=' + this._config.userId, 'baseUrl=' + this._config.apiBaseUrl, 'debug=' + !!this._config.debug, 'contextMode=' + (this._config.contextMode || 'prompt'));
3912
3946
  this.loadAppDetail();
3913
3947
  // 触发 onLoad 回调(与Vue版一致)
3914
3948
  if (this._config?.onLoad) this._config.onLoad(this);
@@ -3995,6 +4029,44 @@ class AIChatDialog extends HTMLElement {
3995
4029
  return this;
3996
4030
  }
3997
4031
 
4032
+ /**
4033
+ * 设置页面上下文数据(宿主页面选中的数据,随每次请求发送)
4034
+ * @param {Object} context - 上下文数据对象,传 null 清空
4035
+ * @returns {this}
4036
+ */
4037
+ setContext(context) {
4038
+ this._context = context && typeof context === 'object' ? {
4039
+ ...context
4040
+ } : {};
4041
+ return this;
4042
+ }
4043
+
4044
+ /**
4045
+ * 获取当前上下文数据
4046
+ * @returns {Object}
4047
+ */
4048
+ getContext() {
4049
+ return {
4050
+ ...this._context
4051
+ };
4052
+ }
4053
+
4054
+ /**
4055
+ * 外部触发表态发送(业务层主动调用,如点击外部按钮触发 AI 请求)
4056
+ * @param {string} text - 要发送的消息内容
4057
+ * @returns {this}
4058
+ */
4059
+ send(text) {
4060
+ const content = String(text || '').trim();
4061
+ if (!content) return this;
4062
+ if (this._input) this._input.value = content;
4063
+ this._autoResizeInput();
4064
+ this._updateSendButtonState();
4065
+ this._lastSendTime = 0; // 跳过防抖,外部触发视为独立操作
4066
+ this.handleSend();
4067
+ return this;
4068
+ }
4069
+
3998
4070
  // ==================== 显示/隐藏 ====================
3999
4071
 
4000
4072
  showDialog() {
@@ -4630,6 +4702,19 @@ class AIChatDialog extends HTMLElement {
4630
4702
  color: var(--ai-success);
4631
4703
  background: rgba(16, 185, 129, 0.08);
4632
4704
  }
4705
+ /* P4: 自定义操作按钮(文本标签型,宽度自适应) */
4706
+ .action-icon.custom-action-btn {
4707
+ width: auto;
4708
+ padding: 4px 10px;
4709
+ font-size: 12px;
4710
+ font-weight: 500;
4711
+ white-space: nowrap;
4712
+ }
4713
+ .action-icon.custom-action-btn:hover {
4714
+ color: #fff;
4715
+ background: var(--ai-primary);
4716
+ transform: translateY(-1px);
4717
+ }
4633
4718
 
4634
4719
  /* ========== 底部输入框(与Vue .dialog-footer 一致) ========== */
4635
4720
  .dialog-footer {
@@ -5210,8 +5295,8 @@ class AIChatDialog extends HTMLElement {
5210
5295
  });
5211
5296
  }
5212
5297
 
5213
- // 拖拽(内嵌模式禁用)
5214
- if (this._dialog && this._config?.enableDrag !== false && this.getAttribute('mode') !== 'inline') this.initDraggable();
5298
+ // 拖拽(内嵌模式禁用)— 由 init() 在 _config 就绪后触发
5299
+ // initDraggable 延迟到 init() 中执行以免 _config 为空
5215
5300
  });
5216
5301
  }
5217
5302
 
@@ -5264,59 +5349,97 @@ class AIChatDialog extends HTMLElement {
5264
5349
  initDraggable() {
5265
5350
  const header = this.shadowRoot?.querySelector('.dialog-header');
5266
5351
  if (!header) return;
5352
+
5353
+ // 清理上一次遗留的 document 级监听(SPA 路由切换场景)
5354
+ this._cleanupDragListeners();
5355
+ const self = this;
5267
5356
  const startDrag = (clientX, clientY) => {
5268
5357
  // P2-17: 移动端禁用拖拽(与Vue版 startTouchDrag: if(isExpanded || isMobile) return 一致)
5269
- if (this._isExpanded || this._isMobile) return;
5270
- this._isDragging = true;
5271
- this._dialog.classList.add('dragging');
5272
- const rect = this._dialog.getBoundingClientRect();
5273
- this._dragStartX = clientX - rect.left;
5274
- this._dragStartY = clientY - rect.top;
5358
+ if (self._isExpanded || self._isMobile) return;
5359
+ self._isDragging = true;
5360
+ self._dialog.classList.add('dragging');
5361
+ const rect = self._dialog.getBoundingClientRect();
5362
+ self._dragStartX = clientX - rect.left;
5363
+ self._dragStartY = clientY - rect.top;
5275
5364
  };
5276
5365
  const onDrag = (clientX, clientY) => {
5277
- if (!this._isDragging) return;
5278
- this._dialogX = clientX - this._dragStartX;
5279
- this._dialogY = clientY - this._dragStartY;
5280
- this._dialog.style.left = this._dialogX + 'px';
5281
- this._dialog.style.right = 'auto';
5282
- this._dialog.style.top = this._dialogY + 'px';
5283
- this._dialog.style.bottom = 'auto';
5366
+ if (!self._isDragging) return;
5367
+ self._dialogX = clientX - self._dragStartX;
5368
+ self._dialogY = clientY - self._dragStartY;
5369
+ self._dialog.style.left = self._dialogX + 'px';
5370
+ self._dialog.style.right = 'auto';
5371
+ self._dialog.style.top = self._dialogY + 'px';
5372
+ self._dialog.style.bottom = 'auto';
5284
5373
  };
5285
5374
  const stopDrag = () => {
5286
- if (this._isDragging) {
5287
- this._isDragging = false;
5288
- this._dialog.classList.remove('dragging');
5375
+ if (self._isDragging) {
5376
+ self._isDragging = false;
5377
+ self._dialog.classList.remove('dragging');
5289
5378
  }
5290
5379
  };
5291
5380
 
5292
- // 鼠标事件(桌面端)
5293
- header.addEventListener('mousedown', e => {
5381
+ // 鼠标事件(桌面端)— 保存引用以便清理
5382
+ this._dragMouseDownHandler = e => {
5294
5383
  if (e.target.closest('button, .header-icon')) return;
5295
5384
  startDrag(e.clientX, e.clientY);
5296
5385
  e.preventDefault();
5297
- });
5298
- document.addEventListener('mousemove', e => {
5386
+ };
5387
+ this._dragMouseMoveHandler = e => {
5299
5388
  onDrag(e.clientX, e.clientY);
5300
- });
5301
- document.addEventListener('mouseup', stopDrag);
5389
+ };
5390
+ this._dragMouseUpHandler = stopDrag;
5391
+ header.addEventListener('mousedown', this._dragMouseDownHandler);
5392
+ document.addEventListener('mousemove', this._dragMouseMoveHandler);
5393
+ document.addEventListener('mouseup', this._dragMouseUpHandler);
5302
5394
 
5303
5395
  // P1-7: 触摸事件(移动端,与Vue版一致)
5304
- header.addEventListener('touchstart', e => {
5305
- if (this._isExpanded) return;
5396
+ this._dragTouchStartHandler = e => {
5397
+ if (self._isExpanded) return;
5306
5398
  if (e.target.closest('button, .header-icon')) return;
5307
5399
  const touch = e.touches[0];
5308
5400
  startDrag(touch.clientX, touch.clientY);
5309
- }, {
5310
- passive: true
5311
- });
5312
- document.addEventListener('touchmove', e => {
5313
- if (!this._isDragging) return;
5401
+ };
5402
+ this._dragTouchMoveHandler = e => {
5403
+ if (!self._isDragging) return;
5314
5404
  const touch = e.touches[0];
5315
5405
  onDrag(touch.clientX, touch.clientY);
5316
- }, {
5406
+ };
5407
+ this._dragTouchEndHandler = stopDrag;
5408
+ header.addEventListener('touchstart', this._dragTouchStartHandler, {
5317
5409
  passive: true
5318
5410
  });
5319
- document.addEventListener('touchend', stopDrag);
5411
+ document.addEventListener('touchmove', this._dragTouchMoveHandler, {
5412
+ passive: true
5413
+ });
5414
+ document.addEventListener('touchend', this._dragTouchEndHandler);
5415
+ this._dragInitialized = true;
5416
+ }
5417
+
5418
+ // P0: 清理拖拽事件监听(SPA disconnectedCallback / re-init 调用)
5419
+ _cleanupDragListeners() {
5420
+ if (this._dragMouseDownHandler) {
5421
+ const header = this.shadowRoot?.querySelector('.dialog-header');
5422
+ if (header) {
5423
+ header.removeEventListener('mousedown', this._dragMouseDownHandler);
5424
+ header.removeEventListener('touchstart', this._dragTouchStartHandler, {
5425
+ passive: true
5426
+ });
5427
+ }
5428
+ this._dragMouseDownHandler = null;
5429
+ this._dragTouchStartHandler = null;
5430
+ }
5431
+ if (this._dragMouseMoveHandler) {
5432
+ document.removeEventListener('mousemove', this._dragMouseMoveHandler);
5433
+ document.removeEventListener('touchmove', this._dragTouchMoveHandler, {
5434
+ passive: true
5435
+ });
5436
+ document.removeEventListener('mouseup', this._dragMouseUpHandler);
5437
+ document.removeEventListener('touchend', this._dragTouchEndHandler);
5438
+ this._dragMouseMoveHandler = null;
5439
+ this._dragTouchMoveHandler = null;
5440
+ this._dragMouseUpHandler = null;
5441
+ this._dragTouchEndHandler = null;
5442
+ }
5320
5443
  }
5321
5444
  applyConfig() {
5322
5445
  if (!this._config) return;
@@ -5481,11 +5604,12 @@ class AIChatDialog extends HTMLElement {
5481
5604
  <div class="message-content">
5482
5605
  ${hasRuntimeEvents ? this._buildRuntimePanel(msg, idx) : ''}
5483
5606
  ${bubbleContent ? `<div class="${bubbleClass}">${bubbleContent}</div>` : ''}
5484
- ${!msg._isStreaming ? `<div class="message-actions-bar">
5485
- ${msg.type === 'ai' ? `<div class="action-icons">
5486
- <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>
5487
- <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>
5488
- </div>` : ''}
5607
+ ${!msg._isStreaming ? `<div class="message-actions-bar">
5608
+ ${msg.type === 'ai' ? `<div class="action-icons">
5609
+ <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>
5610
+ <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>
5611
+ ${this._buildCustomActions(msg, idx)}
5612
+ </div>` : ''}
5489
5613
  </div>` : ''}
5490
5614
  </div>
5491
5615
  </div>`;
@@ -5515,6 +5639,24 @@ class AIChatDialog extends HTMLElement {
5515
5639
  });
5516
5640
  });
5517
5641
 
5642
+ // P4: 绑定自定义操作按钮
5643
+ this._body.querySelectorAll('.custom-action-btn').forEach(btn => {
5644
+ btn.addEventListener('click', () => {
5645
+ const idx = parseInt(btn.dataset.idx);
5646
+ const actionIdx = parseInt(btn.dataset.action);
5647
+ const msg = this._messages[idx];
5648
+ if (!msg) return;
5649
+ try {
5650
+ const actions = this._getCustomActions(msg, idx);
5651
+ if (actions && actions[actionIdx]) {
5652
+ actions[actionIdx].onClick(msg.content);
5653
+ }
5654
+ } catch (e) {
5655
+ console.warn('[AIChatDialog] 自定义操作按钮执行失败:', e);
5656
+ }
5657
+ });
5658
+ });
5659
+
5518
5660
  // 绑定执行过程面板折叠/展开(Shadow DOM 下 inline onclick 不可靠)
5519
5661
  this._body.querySelectorAll('.runtime-header').forEach(header => {
5520
5662
  header.addEventListener('click', () => {
@@ -5767,7 +5909,7 @@ class AIChatDialog extends HTMLElement {
5767
5909
  this.showToast(this._config?.emptyMessageError || '请输入消息内容');
5768
5910
  return;
5769
5911
  }
5770
- if (this._isLoading) return;
5912
+ if (this._isLoading || this._streaming) return;
5771
5913
 
5772
5914
  // P1-11: 字数限制检查
5773
5915
  const maxLen = this._config?.maxLength ?? 500;
@@ -5823,8 +5965,47 @@ class AIChatDialog extends HTMLElement {
5823
5965
  attachmentIds.push(res.data.attachmentId);
5824
5966
  }
5825
5967
  }
5826
- await this.sendMessageToAI(content || attachmentText, {
5827
- attachmentIds
5968
+
5969
+ // P3: 收集页面上下文(setContext + onBeforeSend)
5970
+ let context = {
5971
+ ...(this._context || {})
5972
+ };
5973
+ if (typeof this._config?.onBeforeSend === 'function') {
5974
+ try {
5975
+ const dynamicCtx = await this._config.onBeforeSend();
5976
+ if (dynamicCtx && typeof dynamicCtx === 'object') {
5977
+ context = {
5978
+ ...context,
5979
+ ...dynamicCtx
5980
+ };
5981
+ }
5982
+ } catch (e) {
5983
+ console.warn('[AIChatDialog] onBeforeSend 执行失败:', e);
5984
+ }
5985
+ }
5986
+ const hasContext = context && Object.keys(context).length > 0;
5987
+ const rawMode = this._config?.contextMode;
5988
+ const VALID_CONTEXT_MODES = ['prompt', 'bizParams'];
5989
+ const contextMode = VALID_CONTEXT_MODES.includes(rawMode) ? rawMode : rawMode ? (console.warn('[AIChatDialog] 无效的 contextMode: "' + rawMode + '",已回退为 "prompt"(有效值: ' + VALID_CONTEXT_MODES.join(', ') + ')'), 'prompt') : 'prompt';
5990
+ let finalPrompt = content || attachmentText;
5991
+ let contextForBizParams = null;
5992
+ if (hasContext) {
5993
+ this._debugLog('上下文合并', 'keys=', Object.keys(context), 'mode=', contextMode);
5994
+ if (contextMode === 'bizParams') {
5995
+ // 注入到 bizParams
5996
+ contextForBizParams = context;
5997
+ } else {
5998
+ // 默认:拼到 prompt 前面
5999
+ const ctxText = this._formatContextText(context);
6000
+ if (ctxText) {
6001
+ finalPrompt = ctxText + '\n---\n' + finalPrompt;
6002
+ }
6003
+ }
6004
+ }
6005
+ this._debugLog('发送', 'prompt=' + (finalPrompt || '').substring(0, 150) + (finalPrompt && finalPrompt.length > 150 ? '...' : ''), 'ctxKeys=' + Object.keys(contextForBizParams || context || {}).join(','), 'mode=' + contextMode);
6006
+ await this.sendMessageToAI(finalPrompt, {
6007
+ attachmentIds,
6008
+ context: contextForBizParams
5828
6009
  });
5829
6010
  } catch (err) {
5830
6011
  console.error('[AIChatDialog] Error:', err);
@@ -5858,6 +6039,7 @@ class AIChatDialog extends HTMLElement {
5858
6039
  ...(options.streamOptions || {})
5859
6040
  };
5860
6041
  if (options.attachmentIds?.length) streamOpts.attachmentIds = options.attachmentIds;
6042
+ if (options.context) streamOpts.context = options.context;
5861
6043
  await this._directApiCallStream(msgId, content, streamOpts);
5862
6044
  }
5863
6045
  }
@@ -5915,6 +6097,10 @@ class AIChatDialog extends HTMLElement {
5915
6097
  if (!this._chatClient) {
5916
6098
  this._chatClient = new AIChatClient(this._config || {});
5917
6099
  }
6100
+ if (this._config?.debug) {
6101
+ this._chatClient.config.debug = true;
6102
+ }
6103
+ this._debugLog('Stream开始', 'msgId=' + msgId, 'content=' + (content || '').substring(0, 80), 'opts=' + JSON.stringify(requestOptions));
5918
6104
  if (Object.prototype.hasOwnProperty.call(requestOptions, 'conversationId')) {
5919
6105
  this._chatClient.setConversationId(requestOptions.conversationId || null);
5920
6106
  } else if (this._conversationId) {
@@ -6033,6 +6219,7 @@ class AIChatDialog extends HTMLElement {
6033
6219
  }
6034
6220
  }, requestOptions);
6035
6221
  if (response?.success) {
6222
+ this._debugLog('Stream完成', 'ok, conversationId=' + (response.data?.conversationId || this._conversationId), 'textLen=' + streamText.length);
6036
6223
  if (response.data?.conversationId) {
6037
6224
  this._conversationId = response.data.conversationId;
6038
6225
  } else if (this._chatClient.conversationId) {
@@ -6045,6 +6232,7 @@ class AIChatDialog extends HTMLElement {
6045
6232
  streamText = result;
6046
6233
  }
6047
6234
  } else {
6235
+ this._finalizeMsg(msgId, streamText || '(无回复)');
6048
6236
  return;
6049
6237
  }
6050
6238
  this._finalizeMsg(msgId, streamText || '(无回复)');
@@ -6055,12 +6243,14 @@ class AIChatDialog extends HTMLElement {
6055
6243
  }));
6056
6244
  if (this._config?.onMessageReceived) this._config.onMessageReceived(streamText);
6057
6245
  } catch (e) {
6246
+ this._debugWarn('Stream异常', e.name + ': ' + (e.message || ''));
6058
6247
  if (e.name === 'AbortError') {
6059
6248
  this._updateMsg(msgId, '(请求超时或已取消)');
6060
6249
  } else {
6061
6250
  console.error('[AIChatDialog] 流式失败:', e);
6062
6251
  this._updateMsg(msgId, '网络错误: ' + (e.message || '请检查连接'));
6063
6252
  }
6253
+ this._finalizeMsg(msgId, this._messages.find(m => m.id === msgId)?.content || '(无回复)');
6064
6254
  }
6065
6255
  }
6066
6256
  _getStreamTextEl() {
@@ -6184,6 +6374,27 @@ class AIChatDialog extends HTMLElement {
6184
6374
  return this._ERROR_CONTENT_PATTERNS.some(p => content.startsWith(p));
6185
6375
  }
6186
6376
 
6377
+ // P3: 将上下文对象格式化为可读文本(拼入 prompt 前缀)
6378
+ _formatContextText(context) {
6379
+ if (!context || typeof context !== 'object') return '';
6380
+ const lines = ['[页面上下文]'];
6381
+ for (const key in context) {
6382
+ if (!Object.prototype.hasOwnProperty.call(context, key)) continue;
6383
+ const val = context[key];
6384
+ if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object') {
6385
+ lines.push(key + ':');
6386
+ for (const item of val) {
6387
+ lines.push(' - ' + Object.values(item).join(' | '));
6388
+ }
6389
+ } else if (typeof val === 'object' && val !== null) {
6390
+ lines.push(key + ': ' + JSON.stringify(val));
6391
+ } else {
6392
+ lines.push(key + ': ' + val);
6393
+ }
6394
+ }
6395
+ return lines.join('\n');
6396
+ }
6397
+
6187
6398
  // 运行时事件管理 – incremental DOM, no renderMessages
6188
6399
  _addRuntimeEvent(msgId, event) {
6189
6400
  const idx = this._messages.findIndex(m => m.id === msgId);
@@ -6278,6 +6489,30 @@ class AIChatDialog extends HTMLElement {
6278
6489
  panel.appendChild(eventsContainer);
6279
6490
  return panel;
6280
6491
  }
6492
+
6493
+ // P4: 获取自定义操作按钮列表(缓存结果避免重复调用 onMessageActions)
6494
+ _getCustomActions(msg, idx) {
6495
+ const key = '_customActions_' + idx;
6496
+ if (!msg[key] && typeof this._config?.onMessageActions === 'function') {
6497
+ try {
6498
+ msg[key] = this._config.onMessageActions(msg, idx) || [];
6499
+ } catch (e) {
6500
+ console.warn('[AIChatDialog] onMessageActions 执行失败:', e);
6501
+ msg[key] = [];
6502
+ }
6503
+ }
6504
+ return msg[key] || [];
6505
+ }
6506
+
6507
+ // P4: 生成自定义操作按钮 HTML
6508
+ _buildCustomActions(msg, idx) {
6509
+ const actions = this._getCustomActions(msg, idx);
6510
+ if (!actions.length) return '';
6511
+ return actions.map((a, i) => {
6512
+ const cls = a.className || '';
6513
+ 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>';
6514
+ }).join('');
6515
+ }
6281
6516
  _buildRuntimePanel(msg, msgIdx) {
6282
6517
  const events = msg._runtimeEvents || [];
6283
6518
  if (events.length === 0) return '';
@@ -6314,7 +6549,7 @@ class AIChatDialog extends HTMLElement {
6314
6549
  // ==================== 重新生成(与Vue regenerate 一致) ====================
6315
6550
 
6316
6551
  async regenerate(index) {
6317
- if (this._isLoading) return;
6552
+ if (this._isLoading || this._streaming) return;
6318
6553
  const aiMessage = this._messages[index];
6319
6554
  if (!aiMessage || aiMessage.type !== 'ai') return;
6320
6555
  let userQuestion = '';
@@ -6502,6 +6737,20 @@ class AIChatDialog extends HTMLElement {
6502
6737
  const n = new Date();
6503
6738
  return `${String(n.getHours()).padStart(2, '0')}:${String(n.getMinutes()).padStart(2, '0')}`;
6504
6739
  }
6740
+
6741
+ /**
6742
+ * 调试日志(仅在 config.debug === true 时输出)
6743
+ */
6744
+ _debugLog(tag, ...args) {
6745
+ if (this._config?.debug) {
6746
+ console.log('%c[AI-SDK]%c ' + tag, 'color:#2563eb;font-weight:600', 'color:inherit', ...args);
6747
+ }
6748
+ }
6749
+ _debugWarn(tag, ...args) {
6750
+ if (this._config?.debug) {
6751
+ console.warn('%c[AI-SDK]%c ' + tag, 'color:#f59e0b;font-weight:600', 'color:inherit', ...args);
6752
+ }
6753
+ }
6505
6754
  escapeHtml(s) {
6506
6755
  if (!s) return '';
6507
6756
  const d = document.createElement('div');
@@ -6592,7 +6841,7 @@ if (typeof window !== 'undefined') {
6592
6841
  * 支持任意前端框架:Vue, React, Angular, 原生HTML等
6593
6842
  *
6594
6843
  * @author IBC AI Team
6595
- * @version 2.0.4
6844
+ * @version 2.0.5
6596
6845
  */
6597
6846
 
6598
6847
 
@@ -6809,12 +7058,10 @@ function useAIChat(options = {}) {
6809
7058
  },
6810
7059
  sendMessage: content => {
6811
7060
  if (dialogRef.current) {
6812
- const input = dialogRef.current.shadowRoot?.querySelector('.ai-textarea');
7061
+ const input = dialogRef.current.shadowRoot?.querySelector('.message-input');
6813
7062
  if (input) {
6814
- input.value = content;
6815
- input.dispatchEvent(new Event('input'));
7063
+ dialogRef.current.send?.(content) || (input.value = content, input.dispatchEvent(new Event('input')), dialogRef.current.shadowRoot?.querySelector('.send-btn')?.click());
6816
7064
  }
6817
- dialogRef.current.handleSend?.() || dialogRef.current.shadowRoot?.querySelector('.ai-send-btn')?.click();
6818
7065
  }
6819
7066
  },
6820
7067
  clearMessages: () => {
@@ -6835,14 +7082,14 @@ var index = {
6835
7082
  createAIChatDialog,
6836
7083
  installVuePlugin,
6837
7084
  useAIChat,
6838
- version: '2.0.4'
7085
+ version: '2.0.5'
6839
7086
  };
6840
7087
 
6841
7088
  // 全局暴露(UMD/浏览器环境)
6842
7089
  if (typeof window !== 'undefined') {
6843
7090
  window.AICreateChatDialog = createAIChatDialog;
6844
7091
  window.AIWebSDK = {
6845
- version: '2.0.4',
7092
+ version: '2.0.5',
6846
7093
  create: createAIChatDialog,
6847
7094
  AIChatDialog,
6848
7095
  AIChatClient,