@blueking/chat-helper 0.0.12-beta.16 → 0.0.12-beta.18

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.
@@ -79,6 +79,30 @@ function _object_spread(target) {
79
79
  }
80
80
  return target;
81
81
  }
82
+ function ownKeys(object, enumerableOnly) {
83
+ var keys = Object.keys(object);
84
+ if (Object.getOwnPropertySymbols) {
85
+ var symbols = Object.getOwnPropertySymbols(object);
86
+ if (enumerableOnly) {
87
+ symbols = symbols.filter(function(sym) {
88
+ return Object.getOwnPropertyDescriptor(object, sym).enumerable;
89
+ });
90
+ }
91
+ keys.push.apply(keys, symbols);
92
+ }
93
+ return keys;
94
+ }
95
+ function _object_spread_props(target, source) {
96
+ source = source != null ? source : {};
97
+ if (Object.getOwnPropertyDescriptors) {
98
+ Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
99
+ } else {
100
+ ownKeys(Object(source)).forEach(function(key) {
101
+ Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
102
+ });
103
+ }
104
+ return target;
105
+ }
82
106
  import { ref } from 'vue';
83
107
  import { AGUIProtocol, ApprovalInterruptTicketStatus, EventType, ResumeStatus, RunFinishedOutcomeType } from '../event/index.ts.js';
84
108
  import { MessageRole, MessageStatus, UserOperation } from '../message/index.ts.js';
@@ -129,6 +153,7 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
129
153
  /** 用户主动中止(abortChat),禁止自动重连 */ let manualAbort = false;
130
154
  /** 当前轮次已静默重连次数 */ let reconnectAttempt = 0;
131
155
  /** 本轮流是否已收到 RUN_FINISHED */ let runFinished = false;
156
+ /** 流世代号:新开流时递增,用于忽略已被替换的旧 SSE 回调 */ let activeStreamId = 0;
132
157
  let reconnectTimer = null;
133
158
  let reconnectWaitResolve = null;
134
159
  let activeStreamContext = null;
@@ -245,11 +270,13 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
245
270
  };
246
271
  /**
247
272
  * 尝试静默重连。
273
+ * @param streamId - 发起重连的流世代;任一 await 后若已被新流/abort 取代则立即退出,避免 abort 掉更新流
248
274
  * @returns reconnected | finished(服务端已结束)| failed(需对外报错或中止)
249
275
  */ const attemptSilentReconnect = function() {
250
- var _ref = _async_to_generator(function*(sessionCode) {
276
+ var _ref = _async_to_generator(function*(sessionCode, streamId) {
251
277
  var _mediator_session_current_value, _mediator_session, _mediator_session_current_value1, _mediator_session1, _mediator_message_list_value_at, _mediator_message;
252
- if (manualAbort || !isSameActiveSession(sessionCode)) {
278
+ const isOwnerStream = ()=>streamId === activeStreamId && !manualAbort;
279
+ if (!isOwnerStream() || !isSameActiveSession(sessionCode)) {
253
280
  return 'failed';
254
281
  }
255
282
  try {
@@ -258,7 +285,7 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
258
285
  } catch (e) {
259
286
  // 状态刷新失败时沿用本地 session.status
260
287
  }
261
- if (manualAbort || !isSameActiveSession(sessionCode)) {
288
+ if (!isOwnerStream() || !isSameActiveSession(sessionCode)) {
262
289
  return 'failed';
263
290
  }
264
291
  if (((_mediator_session = mediator.session) === null || _mediator_session === void 0 ? void 0 : (_mediator_session_current_value = _mediator_session.current.value) === null || _mediator_session_current_value === void 0 ? void 0 : _mediator_session_current_value.status) !== SessionStatus.Running) {
@@ -272,12 +299,16 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
272
299
  isChatting.value = true;
273
300
  const delayMs = getReconnectDelayMs(reconnectAttempt);
274
301
  const waited = yield waitForReconnectDelay(delayMs);
275
- if (!waited || manualAbort || !isSameActiveSession(sessionCode)) {
302
+ if (!waited || !isOwnerStream() || !isSameActiveSession(sessionCode)) {
276
303
  return 'failed';
277
304
  }
278
305
  if (((_mediator_session1 = mediator.session) === null || _mediator_session1 === void 0 ? void 0 : (_mediator_session_current_value1 = _mediator_session1.current.value) === null || _mediator_session_current_value1 === void 0 ? void 0 : _mediator_session_current_value1.status) !== SessionStatus.Running) {
279
306
  return 'finished';
280
307
  }
308
+ // 发起新流前再校验一次:防止 await 后窗口期被新用户请求抢走 activeStreamId
309
+ if (!isOwnerStream()) {
310
+ return 'failed';
311
+ }
281
312
  pruneTrailingIncompleteMessages();
282
313
  const lastMessageId = (_mediator_message = mediator.message) === null || _mediator_message === void 0 ? void 0 : (_mediator_message_list_value_at = _mediator_message.list.value.at(-1)) === null || _mediator_message_list_value_at === void 0 ? void 0 : _mediator_message_list_value_at.id;
283
314
  const ctx = activeStreamContext;
@@ -291,13 +322,13 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
291
322
  });
292
323
  return 'reconnected';
293
324
  });
294
- return function attemptSilentReconnect(sessionCode) {
325
+ return function attemptSilentReconnect(sessionCode, streamId) {
295
326
  return _ref.apply(this, arguments);
296
327
  };
297
328
  }();
298
329
  const streamRequest = function() {
299
330
  var _ref = _async_to_generator(function*({ sessionCode, model, url, config, resume, input, lastMessageId, isReconnect = false }) {
300
- var _mediator_http;
331
+ var _previousController_abort, _mediator_http;
301
332
  if (!isReconnect) {
302
333
  resetStreamReconnectState();
303
334
  }
@@ -307,6 +338,12 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
307
338
  url,
308
339
  config
309
340
  };
341
+ // 先递增世代再 abort 旧连接,避免旧流 onDone 误触发二次重连
342
+ const streamId = ++activeStreamId;
343
+ const previousController = chatAbortController;
344
+ chatAbortController = new AbortController();
345
+ previousController === null || previousController === void 0 ? void 0 : (_previousController_abort = previousController.abort) === null || _previousController_abort === void 0 ? void 0 : _previousController_abort.call(previousController);
346
+ const isActiveStream = ()=>streamId === activeStreamId;
310
347
  // ag-ui 协议需要注入消息模块
311
348
  if (usedProtocol instanceof AGUIProtocol) {
312
349
  usedProtocol.injectMessageModule(mediator.message);
@@ -323,6 +360,9 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
323
360
  }
324
361
  const finishSuccessfully = ()=>{
325
362
  var _usedProtocol_onDone;
363
+ if (!isActiveStream()) {
364
+ return;
365
+ }
326
366
  isChatting.value = false;
327
367
  (_usedProtocol_onDone = usedProtocol.onDone) === null || _usedProtocol_onDone === void 0 ? void 0 : _usedProtocol_onDone.call(usedProtocol);
328
368
  if (input) {
@@ -342,18 +382,30 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
342
382
  };
343
383
  const failWithError = (error)=>{
344
384
  var _usedProtocol_onError;
385
+ if (!isActiveStream()) {
386
+ return;
387
+ }
345
388
  isChatting.value = false;
346
389
  (_usedProtocol_onError = usedProtocol.onError) === null || _usedProtocol_onError === void 0 ? void 0 : _usedProtocol_onError.call(usedProtocol, error);
347
390
  };
348
391
  // 事件代理
349
392
  const onDone = ()=>{
350
- // 用户 abort:FetchClient 将 AbortError 转为 onDone,不重连
351
- if (manualAbort || runFinished) {
393
+ if (!isActiveStream()) {
394
+ return;
395
+ }
396
+ // 用户 abort:本地状态已在 abortChat 结算,勿走 completion/poll
397
+ if (manualAbort) {
398
+ return;
399
+ }
400
+ if (runFinished) {
352
401
  finishSuccessfully();
353
402
  return;
354
403
  }
355
404
  // 连接被干净掐断且未收到 RUN_FINISHED:尝试静默重连
356
- void attemptSilentReconnect(sessionCode).then((result)=>{
405
+ void attemptSilentReconnect(sessionCode, streamId).then((result)=>{
406
+ if (!isActiveStream()) {
407
+ return;
408
+ }
357
409
  if (result === 'reconnected') {
358
410
  // 保持 isChatting=true,不展示错误
359
411
  return;
@@ -371,14 +423,17 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
371
423
  });
372
424
  };
373
425
  const onError = (error)=>{
374
- if (manualAbort) {
426
+ if (!isActiveStream() || manualAbort) {
375
427
  return;
376
428
  }
377
429
  if (!isRecoverableStreamError(error)) {
378
430
  failWithError(error);
379
431
  return;
380
432
  }
381
- void attemptSilentReconnect(sessionCode).then((result)=>{
433
+ void attemptSilentReconnect(sessionCode, streamId).then((result)=>{
434
+ if (!isActiveStream()) {
435
+ return;
436
+ }
382
437
  if (result === 'reconnected') {
383
438
  return;
384
439
  }
@@ -395,6 +450,9 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
395
450
  };
396
451
  const onMessage = (event)=>{
397
452
  var _usedProtocol_onMessage;
453
+ if (!isActiveStream()) {
454
+ return;
455
+ }
398
456
  const typedEvent = event;
399
457
  if ((typedEvent === null || typedEvent === void 0 ? void 0 : typedEvent.type) === EventType.RunFinished) {
400
458
  runFinished = true;
@@ -403,13 +461,14 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
403
461
  };
404
462
  const onStart = ()=>{
405
463
  var _usedProtocol_onStart;
464
+ if (!isActiveStream()) {
465
+ return;
466
+ }
406
467
  isChatting.value = true;
407
468
  (_usedProtocol_onStart = usedProtocol.onStart) === null || _usedProtocol_onStart === void 0 ? void 0 : _usedProtocol_onStart.call(usedProtocol);
408
469
  };
409
- // 创建 AbortController
410
- chatAbortController = new AbortController();
411
- // 发起聊天
412
- void ((_mediator_http = mediator.http) === null || _mediator_http === void 0 ? void 0 : _mediator_http.fetchClient.streamRequest(_object_spread({
470
+ // 发起聊天(controller / 回调必须在 ...config 之后,防止被覆盖)
471
+ void ((_mediator_http = mediator.http) === null || _mediator_http === void 0 ? void 0 : _mediator_http.fetchClient.streamRequest(_object_spread_props(_object_spread({
413
472
  url: url || 'chat_completion/',
414
473
  method: 'POST',
415
474
  data: {
@@ -422,13 +481,14 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
422
481
  last_message_id: lastMessageId,
423
482
  resume
424
483
  }
425
- },
484
+ }
485
+ }, config), {
426
486
  controller: chatAbortController,
427
487
  onDone,
428
488
  onError,
429
489
  onMessage,
430
490
  onStart
431
- }, config)).catch(()=>{
491
+ })).catch(()=>{
432
492
  // 非 abort 错误已通过 onError 回调处理;abort 为正常结束
433
493
  }));
434
494
  });
@@ -546,10 +606,15 @@ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.p
546
606
  };
547
607
  /**
548
608
  * 中止聊天(纯前端中止,后端继续处理)
609
+ * 会使当前流世代失效,避免 abort 后的 onDone 误触发 finishSuccessfully / pollResumeSession
549
610
  */ const abortChat = ()=>{
550
611
  var _chatAbortController_abort;
551
612
  manualAbort = true;
613
+ // 先失效世代,再 abort:FetchClient 同步触发的 onDone 会因 isActiveStream=false 直接返回
614
+ activeStreamId += 1;
552
615
  clearReconnectTimer(false);
616
+ clearLongPollTimer();
617
+ isChatting.value = false;
553
618
  chatAbortController === null || chatAbortController === void 0 ? void 0 : (_chatAbortController_abort = chatAbortController.abort) === null || _chatAbortController_abort === void 0 ? void 0 : _chatAbortController_abort.call(chatAbortController);
554
619
  chatAbortController = null;
555
620
  };
@@ -399,7 +399,12 @@ export class FetchClient {
399
399
  const wrappedConfig = _object_spread_props(_object_spread({}, config), {
400
400
  onError: wrappedOnError
401
401
  });
402
- const { url, fetchConfig, requestConfig } = _this.prepareRequest(wrappedConfig, true);
402
+ const { url, fetchConfig, requestConfig, controller } = _this.prepareRequest(wrappedConfig, true);
403
+ let reader;
404
+ const cancelReaderOnAbort = ()=>{
405
+ // pipeThrough(TextDecoderStream) 后,仅 abort fetch signal 在部分环境下不会立刻打断 reader.read()
406
+ void (reader === null || reader === void 0 ? void 0 : reader.cancel().catch(()=>{}));
407
+ };
403
408
  try {
404
409
  var _wrappedConfig_onStart, _fetchResponse_body;
405
410
  const fetchResponse = yield fetch(url, fetchConfig);
@@ -417,12 +422,21 @@ export class FetchClient {
417
422
  return;
418
423
  }
419
424
  (_wrappedConfig_onStart = wrappedConfig.onStart) === null || _wrappedConfig_onStart === void 0 ? void 0 : _wrappedConfig_onStart.call(wrappedConfig);
420
- const reader = (_fetchResponse_body = fetchResponse.body) === null || _fetchResponse_body === void 0 ? void 0 : _fetchResponse_body.pipeThrough(new window.TextDecoderStream()).getReader();
425
+ reader = (_fetchResponse_body = fetchResponse.body) === null || _fetchResponse_body === void 0 ? void 0 : _fetchResponse_body.pipeThrough(new window.TextDecoderStream()).getReader();
421
426
  if (!reader) {
422
427
  const error = new Error('IResponse body is not readable');
423
428
  wrappedOnError(error);
424
429
  return;
425
430
  }
431
+ controller.signal.addEventListener('abort', cancelReaderOnAbort, {
432
+ once: true
433
+ });
434
+ if (controller.signal.aborted) {
435
+ var _wrappedConfig_onDone;
436
+ cancelReaderOnAbort();
437
+ (_wrappedConfig_onDone = wrappedConfig.onDone) === null || _wrappedConfig_onDone === void 0 ? void 0 : _wrappedConfig_onDone.call(wrappedConfig);
438
+ return;
439
+ }
426
440
  // 缓存跨行分片数据
427
441
  let temp = '';
428
442
  const isJson = (str)=>{
@@ -437,8 +451,8 @@ export class FetchClient {
437
451
  while(true){
438
452
  const { value, done } = yield reader.read();
439
453
  if (done) {
440
- var _wrappedConfig_onDone;
441
- (_wrappedConfig_onDone = wrappedConfig.onDone) === null || _wrappedConfig_onDone === void 0 ? void 0 : _wrappedConfig_onDone.call(wrappedConfig);
454
+ var _wrappedConfig_onDone1;
455
+ (_wrappedConfig_onDone1 = wrappedConfig.onDone) === null || _wrappedConfig_onDone1 === void 0 ? void 0 : _wrappedConfig_onDone1.call(wrappedConfig);
442
456
  break;
443
457
  }
444
458
  const values = (temp + value.toString()).split('\n');
@@ -457,13 +471,15 @@ export class FetchClient {
457
471
  } catch (error) {
458
472
  if (error instanceof Error) {
459
473
  if (error.name === 'AbortError') {
460
- var _wrappedConfig_onDone1;
461
- (_wrappedConfig_onDone1 = wrappedConfig.onDone) === null || _wrappedConfig_onDone1 === void 0 ? void 0 : _wrappedConfig_onDone1.call(wrappedConfig);
474
+ var _wrappedConfig_onDone2;
475
+ (_wrappedConfig_onDone2 = wrappedConfig.onDone) === null || _wrappedConfig_onDone2 === void 0 ? void 0 : _wrappedConfig_onDone2.call(wrappedConfig);
462
476
  return;
463
477
  }
464
478
  wrappedOnError(error);
465
479
  }
466
480
  throw error;
481
+ } finally{
482
+ controller.signal.removeEventListener('abort', cancelReaderOnAbort);
467
483
  }
468
484
  })();
469
485
  }
package/dist/index.d.ts CHANGED
@@ -9616,7 +9616,7 @@ export declare const useChatHelper: (options: IUseChatHelperOptions) => {
9616
9616
  labels: string[];
9617
9617
  }>;
9618
9618
  getSessionFeedbackReasons: (rate: number) => Promise<string[]>;
9619
- renameSession: (sessionCode: string) => Promise<void>;
9619
+ renameSession: (sessionCode: string) => Promise<import("./session").ISession<unknown, unknown>>;
9620
9620
  uploadFile: (sessionCode: string, file: File) => Promise<{
9621
9621
  download_url?: string;
9622
9622
  }>;
@@ -7555,7 +7555,7 @@ export declare const useSession: (mediator: IMediatorModule) => {
7555
7555
  labels: string[];
7556
7556
  }>;
7557
7557
  getSessionFeedbackReasons: (rate: number) => Promise<string[]>;
7558
- renameSession: (sessionCode: string) => Promise<void>;
7558
+ renameSession: (sessionCode: string) => Promise<ISession<unknown, unknown>>;
7559
7559
  uploadFile: (sessionCode: string, file: File) => Promise<{
7560
7560
  download_url?: string;
7561
7561
  }>;
@@ -107,10 +107,10 @@ import { computed, ref } from 'vue';
107
107
  var _current_value;
108
108
  // 先更新 list 中对应的 session
109
109
  list.value = list.value.map((item)=>item.sessionCode === session.sessionCode ? _object_spread({}, item, session) : item);
110
- // 如果 current 是被更新的 session,则让 current 重新指向 list 中更新后的对象
110
+ // 同步 current:优先指向 list 中更新后的对象;若不在 list(分页外 / getSession 单独写入),直接合并字段
111
111
  if (((_current_value = current.value) === null || _current_value === void 0 ? void 0 : _current_value.sessionCode) === session.sessionCode) {
112
112
  var _list_value_find;
113
- current.value = (_list_value_find = list.value.find((item)=>item.sessionCode === session.sessionCode)) !== null && _list_value_find !== void 0 ? _list_value_find : current.value;
113
+ current.value = (_list_value_find = list.value.find((item)=>item.sessionCode === session.sessionCode)) !== null && _list_value_find !== void 0 ? _list_value_find : _object_spread({}, current.value, session);
114
114
  }
115
115
  };
116
116
  /**
@@ -312,7 +312,7 @@ import { computed, ref } from 'vue';
312
312
  var _mediator_http;
313
313
  return (_mediator_http = mediator.http) === null || _mediator_http === void 0 ? void 0 : _mediator_http.session.getSessionFeedbackReasons(rate);
314
314
  };
315
- // 会话重命名
315
+ // 会话重命名(返回接口最新 session,供上层事件直接使用新名称)
316
316
  const renameSession = (sessionCode)=>{
317
317
  var _mediator_http;
318
318
  isRenameLoading.value = true;
@@ -321,6 +321,7 @@ import { computed, ref } from 'vue';
321
321
  sessionCode: res.sessionCode,
322
322
  sessionName: res.sessionName
323
323
  });
324
+ return res;
324
325
  }).finally(()=>{
325
326
  isRenameLoading.value = false;
326
327
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueking/chat-helper",
3
- "version": "0.0.12-beta.16",
3
+ "version": "0.0.12-beta.18",
4
4
  "description": "",
5
5
  "main": "./dist/index.ts.js",
6
6
  "types": "./dist/index.d.ts",