@blueking/chat-helper 0.0.11 → 0.0.12-beta.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.
@@ -123,8 +123,7 @@ import { SessionStatus } from '../session/type.ts.js';
123
123
  };
124
124
  const streamRequest = function() {
125
125
  var _ref = _async_to_generator(function*(sessionCode, url, config) {
126
- var // 发起聊天
127
- _mediator_http;
126
+ var _mediator_http;
128
127
  // ag-ui 协议需要注入消息模块
129
128
  if (usedProtocol instanceof AGUIProtocol) {
130
129
  usedProtocol.injectMessageModule(mediator.message);
@@ -151,7 +150,8 @@ import { SessionStatus } from '../session/type.ts.js';
151
150
  };
152
151
  // 创建 AbortController
153
152
  abortController = new AbortController();
154
- (_mediator_http = mediator.http) === null || _mediator_http === void 0 ? void 0 : _mediator_http.fetchClient.streamRequest(_object_spread({
153
+ // 发起聊天
154
+ void ((_mediator_http = mediator.http) === null || _mediator_http === void 0 ? void 0 : _mediator_http.fetchClient.streamRequest(_object_spread({
155
155
  url: url || 'chat_completion/',
156
156
  method: 'POST',
157
157
  data: {
@@ -165,7 +165,9 @@ import { SessionStatus } from '../session/type.ts.js';
165
165
  onError,
166
166
  onMessage,
167
167
  onStart
168
- }, config));
168
+ }, config)).catch(()=>{
169
+ // 非 abort 错误已通过 onError 回调处理;abort 为正常结束
170
+ }));
169
171
  });
170
172
  return function streamRequest(sessionCode, url, config) {
171
173
  return _ref.apply(this, arguments);
@@ -397,10 +397,10 @@ import { CustomEventName, EventType, FlowTaskState } from './type.ts.js';
397
397
  (_this_onDoneCallback = (_this = this).onDoneCallback) === null || _this_onDoneCallback === void 0 ? void 0 : _this_onDoneCallback.call(_this);
398
398
  }
399
399
  onError(error) {
400
- var // 回调给上层
400
+ var // 在聊天区域显示错误消息
401
+ _this_messageModule, // 回调给上层
401
402
  _this_onErrorCallback, _this;
402
- // 创建一个错误消息
403
- this.messageModule.plusMessage({
403
+ (_this_messageModule = this.messageModule) === null || _this_messageModule === void 0 ? void 0 : _this_messageModule.plusMessage({
404
404
  role: MessageRole.Assistant,
405
405
  content: error.message,
406
406
  status: MessageStatus.Error
@@ -45,6 +45,13 @@ export interface ISSEProtocol {
45
45
  onMessage?: (event: unknown) => void;
46
46
  onStart?: () => void;
47
47
  }
48
+ /** 全局错误处理器类型 */
49
+ export type GlobalErrorHandler = (error: IRequestError) => void;
50
+ /** 错误处理器配置 */
51
+ export interface ErrorHandlerOptions {
52
+ /** 忽略的 URL 模式(字符串包含匹配或正则) */
53
+ ignoreErrors?: Array<string | RegExp>;
54
+ }
48
55
  interface IInterceptor<T> {
49
56
  fulfilled?: (value: T) => T;
50
57
  rejected?: (error: unknown) => unknown;
@@ -62,7 +69,20 @@ export declare class FetchClient {
62
69
  request: InterceptorManager<IRequestConfig>;
63
70
  response: InterceptorManager<IResponse>;
64
71
  };
72
+ /** 全局错误处理器 */
73
+ private globalErrorHandler?;
74
+ /** 忽略的错误 URL 模式 */
75
+ private ignoreErrorPatterns;
65
76
  constructor(config?: IRequestConfig);
77
+ /**
78
+ * 注册全局错误处理器
79
+ * 所有 HTTP 错误(request 和 streamRequest)都会经过此处理器
80
+ */
81
+ onError(handler: GlobalErrorHandler, options?: ErrorHandlerOptions): void;
82
+ /** 判断是否应忽略该错误 */
83
+ private shouldIgnoreError;
84
+ /** 统一错误分发 */
85
+ private dispatchError;
66
86
  applyResponseErrorInterceptors(error: unknown): unknown;
67
87
  create(config?: IRequestConfig): FetchClient;
68
88
  delete<T = unknown>(url: string, params?: Record<string, unknown>, config?: IRequestConfig): Promise<T>;
@@ -132,6 +132,35 @@ class InterceptorManager {
132
132
  }
133
133
  }
134
134
  export class FetchClient {
135
+ /**
136
+ * 注册全局错误处理器
137
+ * 所有 HTTP 错误(request 和 streamRequest)都会经过此处理器
138
+ */ onError(handler, options) {
139
+ this.globalErrorHandler = handler;
140
+ if (options === null || options === void 0 ? void 0 : options.ignoreErrors) {
141
+ this.ignoreErrorPatterns = options.ignoreErrors;
142
+ }
143
+ }
144
+ /** 判断是否应忽略该错误 */ shouldIgnoreError(error) {
145
+ var _error_config, _error_config1;
146
+ // 同时匹配相对路径和完整 URL
147
+ const url = ((_error_config = error.config) === null || _error_config === void 0 ? void 0 : _error_config.url) || '';
148
+ const baseURL = ((_error_config1 = error.config) === null || _error_config1 === void 0 ? void 0 : _error_config1.baseURL) || '';
149
+ const fullURL = baseURL && !url.startsWith('http') ? baseURL + url : url;
150
+ return this.ignoreErrorPatterns.some((pattern)=>{
151
+ if (typeof pattern === 'string') {
152
+ // 字符串模式:同时检查相对路径和完整 URL
153
+ return url.includes(pattern) || fullURL.includes(pattern);
154
+ }
155
+ // 正则模式:同时检查相对路径和完整 URL
156
+ return pattern.test(url) || pattern.test(fullURL);
157
+ });
158
+ }
159
+ /** 统一错误分发 */ dispatchError(error) {
160
+ if (this.globalErrorHandler && !this.shouldIgnoreError(error)) {
161
+ this.globalErrorHandler(error);
162
+ }
163
+ }
135
164
  applyResponseErrorInterceptors(error) {
136
165
  let rejectedError = error;
137
166
  this.interceptors.response.forEach((interceptor)=>{
@@ -325,9 +354,11 @@ export class FetchClient {
325
354
  }
326
355
  if (error instanceof Error && error.name === 'AbortError') {
327
356
  const requestError = createError('Request timeout', requestConfig, 'ECONNABORTED', undefined);
357
+ _this.dispatchError(requestError);
328
358
  throw _this.applyResponseErrorInterceptors(requestError);
329
359
  }
330
360
  const requestError = error.isAxiosError === true ? error : createError(error.message, requestConfig, error.code, undefined);
361
+ _this.dispatchError(requestError);
331
362
  throw _this.applyResponseErrorInterceptors(requestError);
332
363
  }
333
364
  })();
@@ -340,13 +371,31 @@ export class FetchClient {
340
371
  streamRequest(config) {
341
372
  var _this = this;
342
373
  return _async_to_generator(function*() {
343
- const { url, fetchConfig, requestConfig } = _this.prepareRequest(config, true);
374
+ // 错误去重:标记是否已调用过全局错误处理器
375
+ let errorDispatched = false;
376
+ // 包装 onError 回调,统一调用全局错误处理器
377
+ const originalOnError = config.onError;
378
+ const wrappedOnError = (error)=>{
379
+ if (!errorDispatched) {
380
+ // 将 requestConfig 附加到错误对象,以便 shouldIgnoreError 能匹配 URL
381
+ const errorWithConfig = error;
382
+ if (!errorWithConfig.config) {
383
+ errorWithConfig.config = requestConfig;
384
+ }
385
+ _this.dispatchError(errorWithConfig);
386
+ errorDispatched = true;
387
+ }
388
+ originalOnError === null || originalOnError === void 0 ? void 0 : originalOnError(error);
389
+ };
390
+ const wrappedConfig = _object_spread_props(_object_spread({}, config), {
391
+ onError: wrappedOnError
392
+ });
393
+ const { url, fetchConfig, requestConfig } = _this.prepareRequest(wrappedConfig, true);
344
394
  try {
345
- var _config_onStart, _fetchResponse_body;
395
+ var _wrappedConfig_onStart, _fetchResponse_body;
346
396
  const fetchResponse = yield fetch(url, fetchConfig);
347
397
  const validateStatus = requestConfig.validateStatus || _this.defaults.validateStatus;
348
398
  if (!validateStatus(fetchResponse.status)) {
349
- var _config_onError;
350
399
  let message = `Request failed with status code ${fetchResponse.status}`;
351
400
  try {
352
401
  var _errorData_error;
@@ -358,15 +407,14 @@ export class FetchClient {
358
407
  message = `Request failed with status code ${fetchResponse.status}`;
359
408
  }
360
409
  const error = createError(message, requestConfig, `ERR_BAD_RESPONSE`, undefined);
361
- (_config_onError = config.onError) === null || _config_onError === void 0 ? void 0 : _config_onError.call(config, error);
410
+ wrappedOnError(error);
362
411
  return;
363
412
  }
364
- (_config_onStart = config.onStart) === null || _config_onStart === void 0 ? void 0 : _config_onStart.call(config);
413
+ (_wrappedConfig_onStart = wrappedConfig.onStart) === null || _wrappedConfig_onStart === void 0 ? void 0 : _wrappedConfig_onStart.call(wrappedConfig);
365
414
  const reader = (_fetchResponse_body = fetchResponse.body) === null || _fetchResponse_body === void 0 ? void 0 : _fetchResponse_body.pipeThrough(new window.TextDecoderStream()).getReader();
366
415
  if (!reader) {
367
- var _config_onError1;
368
416
  const error = new Error('IResponse body is not readable');
369
- (_config_onError1 = config.onError) === null || _config_onError1 === void 0 ? void 0 : _config_onError1.call(config, error);
417
+ wrappedOnError(error);
370
418
  return;
371
419
  }
372
420
  // 缓存跨行分片数据
@@ -383,17 +431,17 @@ export class FetchClient {
383
431
  while(true){
384
432
  const { value, done } = yield reader.read();
385
433
  if (done) {
386
- var _config_onDone;
387
- (_config_onDone = config.onDone) === null || _config_onDone === void 0 ? void 0 : _config_onDone.call(config);
434
+ var _wrappedConfig_onDone;
435
+ (_wrappedConfig_onDone = wrappedConfig.onDone) === null || _wrappedConfig_onDone === void 0 ? void 0 : _wrappedConfig_onDone.call(wrappedConfig);
388
436
  break;
389
437
  }
390
438
  const values = (temp + value.toString()).split('\n');
391
439
  values.forEach((value)=>{
392
440
  const item = value.replace('data:', '').trim();
393
441
  if (isJson(item)) {
394
- var _config_onMessage;
442
+ var _wrappedConfig_onMessage;
395
443
  const json = JSON.parse(item);
396
- (_config_onMessage = config.onMessage) === null || _config_onMessage === void 0 ? void 0 : _config_onMessage.call(config, json);
444
+ (_wrappedConfig_onMessage = wrappedConfig.onMessage) === null || _wrappedConfig_onMessage === void 0 ? void 0 : _wrappedConfig_onMessage.call(wrappedConfig, json);
397
445
  temp = '';
398
446
  } else if (item) {
399
447
  temp = item;
@@ -403,12 +451,11 @@ export class FetchClient {
403
451
  } catch (error) {
404
452
  if (error instanceof Error) {
405
453
  if (error.name === 'AbortError') {
406
- var _config_onDone1;
407
- (_config_onDone1 = config.onDone) === null || _config_onDone1 === void 0 ? void 0 : _config_onDone1.call(config);
408
- } else {
409
- var _config_onError2;
410
- (_config_onError2 = config.onError) === null || _config_onError2 === void 0 ? void 0 : _config_onError2.call(config, error);
454
+ var _wrappedConfig_onDone1;
455
+ (_wrappedConfig_onDone1 = wrappedConfig.onDone) === null || _wrappedConfig_onDone1 === void 0 ? void 0 : _wrappedConfig_onDone1.call(wrappedConfig);
456
+ return;
411
457
  }
458
+ wrappedOnError(error);
412
459
  }
413
460
  throw error;
414
461
  }
@@ -417,6 +464,8 @@ export class FetchClient {
417
464
  constructor(config = {}){
418
465
  _define_property(this, "defaults", void 0);
419
466
  _define_property(this, "interceptors", void 0);
467
+ /** 全局错误处理器 */ _define_property(this, "globalErrorHandler", void 0);
468
+ /** 忽略的错误 URL 模式 */ _define_property(this, "ignoreErrorPatterns", []);
420
469
  this.defaults = mergeConfig({
421
470
  method: 'GET',
422
471
  headers: {
@@ -8,6 +8,8 @@ export * from './transform';
8
8
  */
9
9
  export declare const useHttp: (options: IUseChatHelperOptions) => {
10
10
  reset: (newOptions: IUseChatHelperOptions) => void;
11
+ /** 注册全局错误处理器 */
12
+ onError: (handler: import("./fetch").GlobalErrorHandler, options?: import("./fetch").ErrorHandlerOptions) => void;
11
13
  agent: {
12
14
  getAgentInfo: (config?: import("./fetch").IRequestConfig) => Promise<import("..").IAgentInfo>;
13
15
  };
@@ -88,6 +88,7 @@ export * from './transform/index.ts.js';
88
88
  // 创建 http 模块
89
89
  const http = useModule(fetchClient);
90
90
  return _object_spread_props(_object_spread({}, http), {
91
- reset
91
+ reset,
92
+ /** 注册全局错误处理器 */ onError: fetchClient.onError.bind(fetchClient)
92
93
  });
93
94
  };
@@ -75,6 +75,27 @@ function _object_spread_props(target, source) {
75
75
  return target;
76
76
  }
77
77
  import { transferSession2SessionApi, transferSessionApi2Session, transferSessionFeedback2SessionFeedbackApi, transferSessionFeedbackApi2SessionFeedback } from '../transform/session.ts.js';
78
+ import { resolveRequestValue } from '../fetch/index.ts.js';
79
+ /** 上传接口传输用文件名前缀,与原始展示名解耦 */ const UPLOAD_FILE_NAME_PREFIX = 'upload_file';
80
+ /** 从原始文件名提取 ASCII 安全扩展名,用于保留文件类型 */ const getSafeFileExtension = (fileName)=>{
81
+ const extension = fileName.split('.').pop();
82
+ if (!extension || extension === fileName) {
83
+ return '';
84
+ }
85
+ const safeExtension = extension.replace(/[^A-Za-z0-9]/g, '').slice(0, 20).toLowerCase();
86
+ return safeExtension ? `.${safeExtension}` : '';
87
+ };
88
+ /**
89
+ * 生成上传接口使用的 ASCII 安全文件名。
90
+ * 中文等非 ASCII 原名不能用于 URL path / Content-Disposition(浏览器与后端均可能编码失败),
91
+ * 原始 file.name 仍保留在 File 对象上,由 chat-x 发送消息时作为展示名使用。
92
+ */ const buildSafeUploadFileName = (file)=>{
93
+ const suffix = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
94
+ return `${UPLOAD_FILE_NAME_PREFIX}_${suffix}${getSafeFileExtension(file.name)}`;
95
+ };
96
+ /** Content-Disposition 仅允许 ASCII,须与 upload path 中的 file_name 一致 */ const buildUploadContentDisposition = (fileName)=>{
97
+ return `attachment; filename="${fileName}"`;
98
+ };
78
99
  /**
79
100
  * session 相关 http 接口
80
101
  * @param fetchClient - 请求客户端
@@ -104,13 +125,16 @@ import { transferSession2SessionApi, transferSessionApi2Session, transferSession
104
125
  }, config);
105
126
  // 会话重命名
106
127
  const renameSession = (sessionCode, config)=>fetchClient.post(`session/${sessionCode}/ai_rename/`, undefined, config).then((res)=>transferSessionApi2Session(res));
107
- // 上传文件到会话
128
+ // 上传文件到会话(传输名与展示名分离,见 buildSafeUploadFileName)
108
129
  const uploadFile = (sessionCode, file, config)=>{
109
- const fileName = encodeURIComponent(file.name);
130
+ const safeFileName = buildSafeUploadFileName(file);
131
+ const fileName = encodeURIComponent(safeFileName);
132
+ var _resolveRequestValue;
133
+ const headers = (_resolveRequestValue = resolveRequestValue(config === null || config === void 0 ? void 0 : config.headers)) !== null && _resolveRequestValue !== void 0 ? _resolveRequestValue : {};
110
134
  return file.arrayBuffer().then((content)=>fetchClient.post(`session/${sessionCode}/upload/${fileName}/`, content, _object_spread_props(_object_spread({}, config), {
111
- headers: {
112
- 'Content-Disposition': `attachment; filename="${file.name}"`
113
- }
135
+ headers: _object_spread_props(_object_spread({}, headers), {
136
+ 'Content-Disposition': buildUploadContentDisposition(safeFileName)
137
+ })
114
138
  })));
115
139
  };
116
140
  return {
package/dist/index.d.ts CHANGED
@@ -3725,6 +3725,7 @@ export declare const useChatHelper: (options: IUseChatHelperOptions) => {
3725
3725
  };
3726
3726
  http: {
3727
3727
  reset: (newOptions: IUseChatHelperOptions) => void;
3728
+ onError: (handler: import("./http").GlobalErrorHandler, options?: import("./http").ErrorHandlerOptions) => void;
3728
3729
  agent: {
3729
3730
  getAgentInfo: (config?: import("./http").IRequestConfig) => Promise<import("./agent").IAgentInfo>;
3730
3731
  };
@@ -3763,4 +3764,6 @@ export declare const useChatHelper: (options: IUseChatHelperOptions) => {
3763
3764
  fetchClient: import("./http").FetchClient;
3764
3765
  };
3765
3766
  reset: (options: IUseChatHelperOptions) => void;
3767
+ /** 注册全局错误处理器,简化调用链 */
3768
+ onError: (handler: import("./http").GlobalErrorHandler, options?: import("./http").ErrorHandlerOptions) => void;
3766
3769
  };
package/dist/index.ts.js CHANGED
@@ -62,6 +62,7 @@ export * from './type.ts.js';
62
62
  session,
63
63
  message,
64
64
  http,
65
- reset
65
+ reset,
66
+ /** 注册全局错误处理器,简化调用链 */ onError: http.onError
66
67
  };
67
68
  };
@@ -126,10 +126,17 @@ import { ref } from 'vue';
126
126
  var // 中止当前聊天
127
127
  _mediator_agent;
128
128
  (_mediator_agent = mediator.agent) === null || _mediator_agent === void 0 ? void 0 : _mediator_agent.abortChat();
129
- // 从接口更新当前会话信息
130
- yield getSession(sessionCode);
129
+ const shouldLoadMessages = (options === null || options === void 0 ? void 0 : options.loadMessages) !== false;
130
+ const localSession = list.value.find((item)=>item.sessionCode === sessionCode);
131
+ // 新会话或明确空会话已在本地列表中,无需额外 GET 会话详情。
132
+ if (!shouldLoadMessages && localSession) {
133
+ current.value = localSession;
134
+ } else {
135
+ // 从接口更新当前会话信息
136
+ yield getSession(sessionCode);
137
+ }
131
138
  // 默认加载消息,但允许跳过(新会话消息必定为空,无需加载)
132
- if ((options === null || options === void 0 ? void 0 : options.loadMessages) !== false) {
139
+ if (shouldLoadMessages) {
133
140
  var _mediator_message, // 继续聊天
134
141
  _mediator_agent1;
135
142
  // 获取会话内容
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueking/chat-helper",
3
- "version": "0.0.11",
3
+ "version": "0.0.12-beta.2",
4
4
  "description": "",
5
5
  "main": "./dist/index.ts.js",
6
6
  "types": "./dist/index.d.ts",