@blueking/chat-helper 0.0.12-beta.10 → 0.0.12-beta.11

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.
@@ -1,4 +1,4 @@
1
- import { ApprovalInterruptTicketStatus, ResumeStatus, RunFinishedOutcomeType, type IResume } from '../event';
1
+ import { ApprovalInterruptTicketStatus, EventType, ResumeStatus, RunFinishedOutcomeType, type IResume } from '../event';
2
2
  import { MessageRole, MessageStatus, UserOperation } from '../message';
3
3
  import type { IRequestConfig, ISSEProtocol } from '../http';
4
4
  import type { IMediatorModule } from '../mediator';
@@ -154,7 +154,7 @@ export declare const useAgent: (mediator: IMediatorModule, protocol: ISSEProtoco
154
154
  };
155
155
  runId: number;
156
156
  threadId: string;
157
- type: import("../event").EventType.RunFinished;
157
+ type: EventType.RunFinished;
158
158
  outcome?: {
159
159
  type: RunFinishedOutcomeType.Success;
160
160
  } | {
@@ -1090,7 +1090,7 @@ export declare const useAgent: (mediator: IMediatorModule, protocol: ISSEProtoco
1090
1090
  };
1091
1091
  runId: number;
1092
1092
  threadId: string;
1093
- type: import("../event").EventType.RunFinished;
1093
+ type: EventType.RunFinished;
1094
1094
  outcome?: {
1095
1095
  type: RunFinishedOutcomeType.Success;
1096
1096
  } | {
@@ -1896,13 +1896,15 @@ export declare const useAgent: (mediator: IMediatorModule, protocol: ISSEProtoco
1896
1896
  pollResumeSession: (sessionCode: string) => void;
1897
1897
  clearLongPollTimer: () => void;
1898
1898
  userOperationStreamRequest: (sessionCode: string, operation: UserOperation, payload: IUserOperationPayload, config?: IRequestConfig) => Promise<void>;
1899
- streamRequest: ({ sessionCode, url, config, resume, input, lastMessageId, }: {
1899
+ streamRequest: ({ sessionCode, url, config, resume, input, lastMessageId, isReconnect, }: {
1900
1900
  sessionCode: string;
1901
1901
  url?: string;
1902
1902
  config?: IRequestConfig;
1903
1903
  resume?: IResume;
1904
1904
  input?: string;
1905
1905
  lastMessageId?: string;
1906
+ /** 内部静默重连,不重置重连计数 */
1907
+ isReconnect?: boolean;
1906
1908
  }) => Promise<void>;
1907
1909
  };
1908
1910
  export type IAgentModule = ReturnType<typeof useAgent>;
@@ -80,9 +80,38 @@ function _object_spread(target) {
80
80
  return target;
81
81
  }
82
82
  import { ref } from 'vue';
83
- import { AGUIProtocol, ApprovalInterruptTicketStatus, ResumeStatus, RunFinishedOutcomeType } from '../event/index.ts.js';
83
+ import { AGUIProtocol, ApprovalInterruptTicketStatus, EventType, ResumeStatus, RunFinishedOutcomeType } from '../event/index.ts.js';
84
84
  import { MessageRole, MessageStatus, UserOperation } from '../message/index.ts.js';
85
85
  import { SessionStatus } from '../session/type.ts.js';
86
+ /** SSE 静默重连最大次数(退避总时长约 23s,落在后端 orphan grace ~30s 内) */ const MAX_RECONNECT_ATTEMPTS = 5;
87
+ const RECONNECT_BASE_DELAY_MS = 1000;
88
+ const RECONNECT_MAX_DELAY_MS = 8000;
89
+ const getReconnectDelayMs = (attempt)=>Math.min(RECONNECT_BASE_DELAY_MS * Math.pow(2, attempt - 1), RECONNECT_MAX_DELAY_MS);
90
+ /**
91
+ * 判断流式错误是否可静默重连。
92
+ * 不重试:Abort、401/403 等业务 4xx(408/429 除外);可重试:网络错误、5xx、无状态码的中断。
93
+ */ const isRecoverableStreamError = (error)=>{
94
+ var _requestError_response;
95
+ if (error.name === 'AbortError') {
96
+ return false;
97
+ }
98
+ const requestError = error;
99
+ const statusFromResponse = (_requestError_response = requestError.response) === null || _requestError_response === void 0 ? void 0 : _requestError_response.status;
100
+ const statusFromMessage = error.message.match(/status code (\d+)/i);
101
+ const status = statusFromResponse !== null && statusFromResponse !== void 0 ? statusFromResponse : statusFromMessage ? Number(statusFromMessage[1]) : undefined;
102
+ if (status !== undefined) {
103
+ if (status === 408 || status === 429) {
104
+ return true;
105
+ }
106
+ if (status >= 400 && status < 500) {
107
+ return false;
108
+ }
109
+ if (status >= 500) {
110
+ return true;
111
+ }
112
+ }
113
+ return true;
114
+ };
86
115
  /**
87
116
  * Agent 模块
88
117
  * @param options - 配置选项
@@ -95,6 +124,12 @@ import { SessionStatus } from '../session/type.ts.js';
95
124
  let chatAbortController = null;
96
125
  let resumeAbortController = null;
97
126
  let longPollTimer = null;
127
+ /** 用户主动中止(abortChat),禁止自动重连 */ let manualAbort = false;
128
+ /** 当前轮次已静默重连次数 */ let reconnectAttempt = 0;
129
+ /** 本轮流是否已收到 RUN_FINISHED */ let runFinished = false;
130
+ let reconnectTimer = null;
131
+ let reconnectWaitResolve = null;
132
+ let activeStreamContext = null;
98
133
  const getAgentInfo = ()=>{
99
134
  var _mediator_http;
100
135
  isInfoLoading.value = true;
@@ -137,9 +172,115 @@ import { SessionStatus } from '../session/type.ts.js';
137
172
  }
138
173
  });
139
174
  };
175
+ const clearReconnectTimer = (continued = false)=>{
176
+ if (reconnectTimer) {
177
+ clearTimeout(reconnectTimer);
178
+ reconnectTimer = null;
179
+ }
180
+ if (reconnectWaitResolve) {
181
+ const resolve = reconnectWaitResolve;
182
+ reconnectWaitResolve = null;
183
+ resolve(continued);
184
+ }
185
+ };
186
+ const waitForReconnectDelay = (ms)=>new Promise((resolve)=>{
187
+ reconnectWaitResolve = resolve;
188
+ reconnectTimer = setTimeout(()=>{
189
+ reconnectTimer = null;
190
+ reconnectWaitResolve = null;
191
+ resolve(true);
192
+ }, ms);
193
+ });
194
+ const resetStreamReconnectState = ()=>{
195
+ manualAbort = false;
196
+ reconnectAttempt = 0;
197
+ runFinished = false;
198
+ clearReconnectTimer(false);
199
+ };
200
+ /** 裁掉尾部尚未完成的非用户消息,避免 DLQ 重放时重复气泡 */ const pruneTrailingIncompleteMessages = ()=>{
201
+ var _mediator_message;
202
+ const list = (_mediator_message = mediator.message) === null || _mediator_message === void 0 ? void 0 : _mediator_message.list.value;
203
+ if (!(list === null || list === void 0 ? void 0 : list.length)) {
204
+ return;
205
+ }
206
+ while(list.length > 0){
207
+ const last = list[list.length - 1];
208
+ if (last.role === MessageRole.User) {
209
+ break;
210
+ }
211
+ if (last.status === MessageStatus.Streaming || last.status === MessageStatus.Pending) {
212
+ list.pop();
213
+ continue;
214
+ }
215
+ break;
216
+ }
217
+ };
218
+ const isSameActiveSession = (sessionCode)=>{
219
+ var _mediator_session_current_value, _mediator_session_current, _mediator_session;
220
+ return sessionCode === ((_mediator_session = mediator.session) === null || _mediator_session === void 0 ? void 0 : (_mediator_session_current = _mediator_session.current) === null || _mediator_session_current === 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.sessionCode);
221
+ };
222
+ /**
223
+ * 尝试静默重连。
224
+ * @returns reconnected | finished(服务端已结束)| failed(需对外报错或中止)
225
+ */ const attemptSilentReconnect = function() {
226
+ var _ref = _async_to_generator(function*(sessionCode) {
227
+ var _mediator_session_current_value, _mediator_session, _mediator_session_current_value1, _mediator_session1, _mediator_message_list_value_at, _mediator_message;
228
+ if (manualAbort || !isSameActiveSession(sessionCode)) {
229
+ return 'failed';
230
+ }
231
+ try {
232
+ var _mediator_session2;
233
+ yield (_mediator_session2 = mediator.session) === null || _mediator_session2 === void 0 ? void 0 : _mediator_session2.getSession(sessionCode);
234
+ } catch (e) {
235
+ // 状态刷新失败时沿用本地 session.status
236
+ }
237
+ if (manualAbort || !isSameActiveSession(sessionCode)) {
238
+ return 'failed';
239
+ }
240
+ 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) {
241
+ return 'finished';
242
+ }
243
+ if (reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
244
+ return 'failed';
245
+ }
246
+ reconnectAttempt += 1;
247
+ // 静默重连期间保持「生成中」态
248
+ isChatting.value = true;
249
+ const delayMs = getReconnectDelayMs(reconnectAttempt);
250
+ const waited = yield waitForReconnectDelay(delayMs);
251
+ if (!waited || manualAbort || !isSameActiveSession(sessionCode)) {
252
+ return 'failed';
253
+ }
254
+ 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) {
255
+ return 'finished';
256
+ }
257
+ pruneTrailingIncompleteMessages();
258
+ 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;
259
+ const ctx = activeStreamContext;
260
+ streamRequest({
261
+ sessionCode,
262
+ url: ctx === null || ctx === void 0 ? void 0 : ctx.url,
263
+ config: ctx === null || ctx === void 0 ? void 0 : ctx.config,
264
+ lastMessageId: lastMessageId !== undefined ? String(lastMessageId) : undefined,
265
+ isReconnect: true
266
+ });
267
+ return 'reconnected';
268
+ });
269
+ return function attemptSilentReconnect(sessionCode) {
270
+ return _ref.apply(this, arguments);
271
+ };
272
+ }();
140
273
  const streamRequest = function() {
141
- var _ref = _async_to_generator(function*({ sessionCode, url, config, resume, input, lastMessageId }) {
274
+ var _ref = _async_to_generator(function*({ sessionCode, url, config, resume, input, lastMessageId, isReconnect = false }) {
142
275
  var _mediator_http;
276
+ if (!isReconnect) {
277
+ resetStreamReconnectState();
278
+ }
279
+ activeStreamContext = {
280
+ sessionCode,
281
+ url,
282
+ config
283
+ };
143
284
  // ag-ui 协议需要注入消息模块
144
285
  if (usedProtocol instanceof AGUIProtocol) {
145
286
  usedProtocol.injectMessageModule(mediator.message);
@@ -154,8 +295,7 @@ import { SessionStatus } from '../session/type.ts.js';
154
295
  sessionCode
155
296
  });
156
297
  }
157
- // 事件代理
158
- const onDone = ()=>{
298
+ const finishSuccessfully = ()=>{
159
299
  var _usedProtocol_onDone;
160
300
  isChatting.value = false;
161
301
  (_usedProtocol_onDone = usedProtocol.onDone) === null || _usedProtocol_onDone === void 0 ? void 0 : _usedProtocol_onDone.call(usedProtocol);
@@ -166,19 +306,73 @@ import { SessionStatus } from '../session/type.ts.js';
166
306
  var _mediator_message;
167
307
  const lastUserMessage = (_mediator_message = mediator.message) === null || _mediator_message === void 0 ? void 0 : _mediator_message.list.value.findLast((item)=>item.role === MessageRole.User);
168
308
  const lastApiUserMessage = res.findLast((item)=>item.role === MessageRole.User);
169
- lastUserMessage.id = lastApiUserMessage.id;
309
+ if (lastUserMessage && lastApiUserMessage) {
310
+ lastUserMessage.id = lastApiUserMessage.id;
311
+ }
170
312
  });
171
313
  }
172
314
  // 轮询接口,判断是否可以继续聊天
173
315
  pollResumeSession(sessionCode);
174
316
  };
175
- const onError = (error)=>{
317
+ const failWithError = (error)=>{
176
318
  var _usedProtocol_onError;
177
319
  isChatting.value = false;
178
320
  (_usedProtocol_onError = usedProtocol.onError) === null || _usedProtocol_onError === void 0 ? void 0 : _usedProtocol_onError.call(usedProtocol, error);
179
321
  };
322
+ // 事件代理
323
+ const onDone = ()=>{
324
+ // 用户 abort:FetchClient 将 AbortError 转为 onDone,不重连
325
+ if (manualAbort || runFinished) {
326
+ finishSuccessfully();
327
+ return;
328
+ }
329
+ // 连接被干净掐断且未收到 RUN_FINISHED:尝试静默重连
330
+ void attemptSilentReconnect(sessionCode).then((result)=>{
331
+ if (result === 'reconnected') {
332
+ // 保持 isChatting=true,不展示错误
333
+ return;
334
+ }
335
+ if (result === 'finished') {
336
+ finishSuccessfully();
337
+ return;
338
+ }
339
+ // abort / 切会话导致的失败:静默结束,不展示错误气泡
340
+ if (manualAbort || !isSameActiveSession(sessionCode)) {
341
+ isChatting.value = false;
342
+ return;
343
+ }
344
+ failWithError(new Error('Connection lost, please try again'));
345
+ });
346
+ };
347
+ const onError = (error)=>{
348
+ if (manualAbort) {
349
+ return;
350
+ }
351
+ if (!isRecoverableStreamError(error)) {
352
+ failWithError(error);
353
+ return;
354
+ }
355
+ void attemptSilentReconnect(sessionCode).then((result)=>{
356
+ if (result === 'reconnected') {
357
+ return;
358
+ }
359
+ if (result === 'finished') {
360
+ finishSuccessfully();
361
+ return;
362
+ }
363
+ if (manualAbort || !isSameActiveSession(sessionCode)) {
364
+ isChatting.value = false;
365
+ return;
366
+ }
367
+ failWithError(error);
368
+ });
369
+ };
180
370
  const onMessage = (event)=>{
181
371
  var _usedProtocol_onMessage;
372
+ const typedEvent = event;
373
+ if ((typedEvent === null || typedEvent === void 0 ? void 0 : typedEvent.type) === EventType.RunFinished) {
374
+ runFinished = true;
375
+ }
182
376
  (_usedProtocol_onMessage = usedProtocol.onMessage) === null || _usedProtocol_onMessage === void 0 ? void 0 : _usedProtocol_onMessage.call(usedProtocol, event);
183
377
  };
184
378
  const onStart = ()=>{
@@ -322,6 +516,8 @@ import { SessionStatus } from '../session/type.ts.js';
322
516
  * 中止聊天(纯前端中止,后端继续处理)
323
517
  */ const abortChat = ()=>{
324
518
  var _chatAbortController_abort;
519
+ manualAbort = true;
520
+ clearReconnectTimer(false);
325
521
  chatAbortController === null || chatAbortController === void 0 ? void 0 : (_chatAbortController_abort = chatAbortController.abort) === null || _chatAbortController_abort === void 0 ? void 0 : _chatAbortController_abort.call(chatAbortController);
326
522
  chatAbortController = null;
327
523
  };
@@ -396,10 +592,15 @@ import { SessionStatus } from '../session/type.ts.js';
396
592
  };
397
593
  }();
398
594
  const reset = (protocol)=>{
399
- // 重置状态
595
+ abortChat();
596
+ // 重置状态(保留 manualAbort=true,避免 abort 异步 onDone 误触发重连;下次 streamRequest 会复位)
400
597
  usedProtocol = protocol || new AGUIProtocol();
401
598
  info.value = null;
402
599
  isInfoLoading.value = false;
600
+ isChatting.value = false;
601
+ activeStreamContext = null;
602
+ reconnectAttempt = 0;
603
+ runFinished = false;
403
604
  };
404
605
  return {
405
606
  info,
@@ -131,6 +131,7 @@ export declare class AGUIProtocol implements ISSEProtocol {
131
131
  handleTextMessageEndEvent(event: ITextMessageEndEvent): void;
132
132
  /**
133
133
  * 处理文本消息开始事件
134
+ * 断线续传时 DLQ 会重放事件:同 messageId 已存在则重置内容,避免重复气泡/文本翻倍
134
135
  */
135
136
  handleTextMessageStartEvent(event: ITextMessageStartEvent): void;
136
137
  /**
@@ -349,7 +349,14 @@ import { CustomEventName, EventType, FlowTaskState, RunFinishedOutcomeType } fro
349
349
  }
350
350
  /**
351
351
  * 处理文本消息开始事件
352
+ * 断线续传时 DLQ 会重放事件:同 messageId 已存在则重置内容,避免重复气泡/文本翻倍
352
353
  */ handleTextMessageStartEvent(event) {
354
+ const existing = this.messageModule.getMessageByMessageId(event.messageId);
355
+ if (existing) {
356
+ existing.content = '';
357
+ existing.status = MessageStatus.Streaming;
358
+ return;
359
+ }
353
360
  this.messageModule.plusMessage({
354
361
  role: event.role,
355
362
  content: '',
package/dist/index.d.ts CHANGED
@@ -1898,13 +1898,14 @@ export declare const useChatHelper: (options: IUseChatHelperOptions) => {
1898
1898
  pollResumeSession: (sessionCode: string) => void;
1899
1899
  clearLongPollTimer: () => void;
1900
1900
  userOperationStreamRequest: (sessionCode: string, operation: import("./message").UserOperation, payload: import("./message").IUserOperationPayload, config?: import("./http").IRequestConfig) => Promise<void>;
1901
- streamRequest: ({ sessionCode, url, config, resume, input, lastMessageId, }: {
1901
+ streamRequest: ({ sessionCode, url, config, resume, input, lastMessageId, isReconnect, }: {
1902
1902
  sessionCode: string;
1903
1903
  url?: string;
1904
1904
  config?: import("./http").IRequestConfig;
1905
1905
  resume?: import("./event").IResume;
1906
1906
  input?: string;
1907
1907
  lastMessageId?: string;
1908
+ isReconnect?: boolean;
1908
1909
  }) => Promise<void>;
1909
1910
  };
1910
1911
  session: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueking/chat-helper",
3
- "version": "0.0.12-beta.10",
3
+ "version": "0.0.12-beta.11",
4
4
  "description": "",
5
5
  "main": "./dist/index.ts.js",
6
6
  "types": "./dist/index.d.ts",