feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const FONTIER_LOGID_PLACEHOLDER = 'FONTIER_LOGID_PLACEHOLDER';
|
||||
export const ABORT_HTTP_CHUNK_MESSAGE = 'ABORT_FETCH';
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum HttpChunkEvents {
|
||||
// 收到消息
|
||||
MESSAGE_RECEIVED = 'http_chunk_message_received',
|
||||
// 收到消息异常
|
||||
MESSAGE_RECEIVED_INVALID = 'http_chunk_message_received_invalid',
|
||||
// 整体拉流超时
|
||||
TOTAL_FETCH_TIMEOUT = 'http_chunk_total_fetch_timeout',
|
||||
// 包间超时
|
||||
BETWEEN_CHUNK_TIMEOUT = 'http_chunk_between_chunk_timeout',
|
||||
// 开始 fetch
|
||||
FETCH_START = 'http_chunk_fetch_start',
|
||||
// fetch 请求成功
|
||||
FETCH_SUCCESS = 'http_chunk_fetch_success',
|
||||
// fetch 请求异常
|
||||
FETCH_ERROR = 'http_chunk_fetch_error',
|
||||
// 无效的消息格式
|
||||
INVALID_MESSAGE = 'http_chunk_invalid_message',
|
||||
// 拉流开始
|
||||
READ_STREAM_START = 'http_chunk_read_stream_start',
|
||||
// 拉流异常
|
||||
READ_STREAM_ERROR = 'http_chunk_read_stream_error',
|
||||
// 从 fetch 到 read stream 完整成功
|
||||
ALL_SUCCESS = 'http_chunk_all_success',
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* http chunk 的 slardar 自定义事件
|
||||
*/
|
||||
export enum SlardarEvents {
|
||||
// 调用 controller.abort 的代码发生的报错 不在预期之内
|
||||
HTTP_CHUNK_UNEXPECTED_ABORT_ERROR = 'http_chunk_unexpected_abort_error',
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FetchStreamErrorCode, fetchStream } from '@coze-arch/fetch-stream';
|
||||
|
||||
import { ABORT_HTTP_CHUNK_MESSAGE } from '../constant';
|
||||
import { RequestScene } from '../../request-manager/types';
|
||||
import { type RequestManager } from '../../request-manager';
|
||||
import { type ReportLog } from '../../report-log';
|
||||
import { type SendMessage } from '../../message/types';
|
||||
import { ChatCoreError } from '../../custom-error';
|
||||
import { type TokenManager } from '../../credential';
|
||||
import {
|
||||
CustomEventEmitter,
|
||||
FetchDataHelper,
|
||||
getDataHelperPlaceholder,
|
||||
getMessageLifecycleCallbackParam,
|
||||
inValidChunkRaw,
|
||||
type RetryCounterConfig,
|
||||
streamParser,
|
||||
ChunkEvent,
|
||||
} from './utils';
|
||||
import {
|
||||
type ParsedEvent,
|
||||
type HandleErrorParams,
|
||||
type HandleMessageParams,
|
||||
type HandleMessageSuccessParams,
|
||||
type HandleMessageTimerEndParams,
|
||||
type SendMessageConfig,
|
||||
} from './types';
|
||||
import { SlardarEvents } from './events/slardar-events';
|
||||
import { HttpChunkEvents } from './events/http-chunk-events';
|
||||
|
||||
interface HttpChunkAdaptorConfig {
|
||||
retryCounterConfig?: RetryCounterConfig;
|
||||
requestManager: RequestManager;
|
||||
tokenManager?: TokenManager;
|
||||
reportLogWithScope: ReportLog;
|
||||
}
|
||||
|
||||
const MAX_DATA_HELPERS = 100;
|
||||
|
||||
export class HttpChunk extends CustomEventEmitter {
|
||||
readonly retryCounterConfig?: RetryCounterConfig;
|
||||
private fetchDataHelperMap: Map<string, FetchDataHelper>;
|
||||
private requestManager: RequestManager;
|
||||
private tokenManager?: TokenManager;
|
||||
private reportLogWithScope: ReportLog;
|
||||
constructor({
|
||||
retryCounterConfig,
|
||||
requestManager,
|
||||
tokenManager,
|
||||
reportLogWithScope,
|
||||
}: HttpChunkAdaptorConfig) {
|
||||
super();
|
||||
this.retryCounterConfig = retryCounterConfig;
|
||||
this.fetchDataHelperMap = new Map();
|
||||
this.requestManager = requestManager;
|
||||
this.tokenManager = tokenManager;
|
||||
this.reportLogWithScope = reportLogWithScope;
|
||||
}
|
||||
|
||||
private handleMessageSuccess = ({
|
||||
fetchDataHelper = getDataHelperPlaceholder(),
|
||||
}: HandleMessageSuccessParams) => {
|
||||
const { localMessageID } = fetchDataHelper;
|
||||
|
||||
this.fetchDataHelperMap.delete(localMessageID);
|
||||
|
||||
this.customEmit(
|
||||
HttpChunkEvents.ALL_SUCCESS,
|
||||
getMessageLifecycleCallbackParam(fetchDataHelper),
|
||||
);
|
||||
};
|
||||
|
||||
private handleMessage = ({
|
||||
message: { data },
|
||||
fetchDataHelper = getDataHelperPlaceholder(),
|
||||
}: HandleMessageParams) => {
|
||||
const { logID, replyID } = fetchDataHelper;
|
||||
|
||||
// 从 fetch 中抛出的数据类型由断言得到 这里运行时保卫一下类型
|
||||
if (!inValidChunkRaw(data)) {
|
||||
this.customEmit(HttpChunkEvents.INVALID_MESSAGE, {
|
||||
logID,
|
||||
replyID,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
const validChunk = data;
|
||||
fetchDataHelper.setReplyID(validChunk.message.reply_id);
|
||||
this.customEmit(HttpChunkEvents.MESSAGE_RECEIVED, {
|
||||
chunk: validChunk,
|
||||
logID,
|
||||
});
|
||||
};
|
||||
|
||||
private pullMessage = async ({
|
||||
value,
|
||||
// TODO: 本期没有做消息重试
|
||||
isRePullMessage: _isRePullMessage,
|
||||
fetchDataHelper,
|
||||
fetchUrl,
|
||||
scene,
|
||||
}: {
|
||||
value: Record<string, unknown>; // 短链入参:直接透传body不做特化处理,在外层处理业务逻辑
|
||||
isRePullMessage: boolean;
|
||||
fetchDataHelper: FetchDataHelper;
|
||||
fetchUrl: string; // 短链链接
|
||||
scene: RequestScene;
|
||||
}) => {
|
||||
// TODO: hzf,不使用三元表达式
|
||||
const headers: [string, string][] = [
|
||||
['content-type', 'application/json'],
|
||||
...((this.tokenManager?.getApiKeyAuthorizationValue()
|
||||
? [['Authorization', this.tokenManager.getApiKeyAuthorizationValue()]]
|
||||
: []) as [string, string][]),
|
||||
...(fetchDataHelper.headers
|
||||
? Array.isArray(fetchDataHelper.headers)
|
||||
? fetchDataHelper.headers
|
||||
: Object.entries(fetchDataHelper.headers)
|
||||
: []),
|
||||
];
|
||||
const { hooks } = this.requestManager.getSceneConfig?.(scene) || {};
|
||||
const { onBeforeSendMessage = [], onGetMessageStreamParser } = hooks || {};
|
||||
|
||||
// 如下参数可做修改
|
||||
let channelFetchInfo = {
|
||||
url: fetchUrl,
|
||||
body: JSON.stringify(value),
|
||||
headers,
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
for (const hook of onBeforeSendMessage) {
|
||||
channelFetchInfo = await hook(channelFetchInfo);
|
||||
}
|
||||
await fetchStream<ParsedEvent, FetchDataHelper>(channelFetchInfo.url, {
|
||||
onStart: response => {
|
||||
fetchDataHelper.setLogID(response.headers.get('x-tt-logid'));
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
onFetchStart: localeData => {
|
||||
this.customEmit(
|
||||
HttpChunkEvents.FETCH_START,
|
||||
getMessageLifecycleCallbackParam(localeData),
|
||||
);
|
||||
},
|
||||
onFetchSuccess: localeData => {
|
||||
this.customEmit(
|
||||
HttpChunkEvents.FETCH_SUCCESS,
|
||||
getMessageLifecycleCallbackParam(localeData),
|
||||
);
|
||||
},
|
||||
onStartReadStream: localeData => {
|
||||
this.customEmit(
|
||||
HttpChunkEvents.READ_STREAM_START,
|
||||
getMessageLifecycleCallbackParam(localeData),
|
||||
);
|
||||
},
|
||||
onError: ({ fetchStreamError, dataClump: localeData }) =>
|
||||
this.handleError({
|
||||
errorInfo: {
|
||||
...fetchStreamError,
|
||||
ext: getMessageLifecycleCallbackParam(localeData),
|
||||
},
|
||||
fetchDataHelper: localeData,
|
||||
}),
|
||||
onAllSuccess: localClump =>
|
||||
this.handleMessageSuccess({ fetchDataHelper: localClump }),
|
||||
validateMessage: ({ message }) => {
|
||||
if (message.event !== ChunkEvent.ERROR) {
|
||||
return {
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
return {
|
||||
error: new Error(String(message.data)),
|
||||
status: 'error',
|
||||
};
|
||||
},
|
||||
onMessage: ({ message, dataClump }) =>
|
||||
this.handleMessage({ message, fetchDataHelper: dataClump }),
|
||||
streamParser: onGetMessageStreamParser?.(value) || streamParser,
|
||||
dataClump: fetchDataHelper,
|
||||
|
||||
body: channelFetchInfo.body,
|
||||
headers: channelFetchInfo.headers,
|
||||
method: channelFetchInfo.method,
|
||||
|
||||
signal: fetchDataHelper.abortSignal.signal,
|
||||
totalFetchTimeout: fetchDataHelper.totalFetchTimeout,
|
||||
onTotalFetchTimeout: dataClump =>
|
||||
this.handleTotalFetchTimeout({ fetchDataHelper: dataClump }),
|
||||
betweenChunkTimeout: fetchDataHelper.betweenChunkTimeout,
|
||||
onBetweenChunkTimeout: dataClump =>
|
||||
this.handleBetweenChunkTimeout({ fetchDataHelper: dataClump }),
|
||||
});
|
||||
};
|
||||
|
||||
private handleBetweenChunkTimeout = ({
|
||||
fetchDataHelper = getDataHelperPlaceholder(),
|
||||
}: HandleMessageTimerEndParams) => {
|
||||
this.customEmit(
|
||||
HttpChunkEvents.BETWEEN_CHUNK_TIMEOUT,
|
||||
getMessageLifecycleCallbackParam(fetchDataHelper),
|
||||
);
|
||||
};
|
||||
|
||||
private handleTotalFetchTimeout = ({
|
||||
fetchDataHelper = getDataHelperPlaceholder(),
|
||||
}: HandleMessageTimerEndParams) => {
|
||||
this.customEmit(
|
||||
HttpChunkEvents.TOTAL_FETCH_TIMEOUT,
|
||||
getMessageLifecycleCallbackParam(fetchDataHelper),
|
||||
);
|
||||
};
|
||||
|
||||
private handleError = ({ errorInfo }: HandleErrorParams) => {
|
||||
if (errorInfo.code === FetchStreamErrorCode.FetchException) {
|
||||
this.customEmit(HttpChunkEvents.FETCH_ERROR, errorInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
this.customEmit(HttpChunkEvents.READ_STREAM_ERROR, errorInfo);
|
||||
|
||||
return;
|
||||
|
||||
// TODO: 下面应该是重新拉取 message 的逻辑 本期服务端没有来得及做
|
||||
// if (dataClump.retryCounter.matchMaxRetryAttempts()) {
|
||||
// this.customOnError?.(errorInfo);
|
||||
// // 放弃尝试重试
|
||||
// this.handleFinish();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// dataClump.retryCounter.add();
|
||||
};
|
||||
|
||||
// 调用chat、resume接口发送消息,已经在上层抹平差异
|
||||
sendMessage = (value: SendMessage, config?: SendMessageConfig) => {
|
||||
const localMessageID = value.local_message_id;
|
||||
|
||||
if (!localMessageID) {
|
||||
// TODO: 用同一的异常类
|
||||
this.customEmit(HttpChunkEvents.FETCH_ERROR, {
|
||||
code: FetchStreamErrorCode.FetchException,
|
||||
msg: 'SendMessageError: SendMessage is Invalid',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchDataHelper = new FetchDataHelper({
|
||||
localMessageID,
|
||||
retryCounterConfig: this.retryCounterConfig,
|
||||
betweenChunkTimeout: config?.betweenChunkTimeout,
|
||||
totalFetchTimeout: config?.totalFetchTimeout,
|
||||
headers: config?.headers,
|
||||
});
|
||||
|
||||
if (this.fetchDataHelperMap.size >= MAX_DATA_HELPERS) {
|
||||
this.fetchDataHelperMap.clear();
|
||||
}
|
||||
|
||||
this.fetchDataHelperMap.set(localMessageID, fetchDataHelper);
|
||||
|
||||
const scene = config?.requestScene || RequestScene.SendMessage;
|
||||
// 获取消息短链请求链接
|
||||
const { url, baseURL } = this.requestManager.getSceneConfig?.(scene) || {};
|
||||
|
||||
const fetchUrl = baseURL ? `${baseURL}${url}` : url;
|
||||
this.pullMessage({
|
||||
value,
|
||||
isRePullMessage: false,
|
||||
fetchDataHelper,
|
||||
fetchUrl,
|
||||
scene,
|
||||
});
|
||||
};
|
||||
|
||||
abort = (localMessageID: string) => {
|
||||
const targetFetchDataHelper = this.fetchDataHelperMap.get(localMessageID);
|
||||
this.fetchDataHelperMap.delete(localMessageID);
|
||||
|
||||
if (targetFetchDataHelper?.abortSignal.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
targetFetchDataHelper?.abortSignal.abort?.(ABORT_HTTP_CHUNK_MESSAGE);
|
||||
|
||||
this.reportLogWithScope.slardarSuccessEvent({
|
||||
eventName: SlardarEvents.HTTP_CHUNK_UNEXPECTED_ABORT_ERROR,
|
||||
});
|
||||
} catch (rawError) {
|
||||
const error = new ChatCoreError(
|
||||
'An error occurred in calling abort in synchronous code',
|
||||
{ rawError },
|
||||
);
|
||||
this.reportLogWithScope.slardarErrorEvent({
|
||||
eventName: SlardarEvents.HTTP_CHUNK_UNEXPECTED_ABORT_ERROR,
|
||||
error,
|
||||
meta: error.flatten(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
drop = () => {
|
||||
this.fetchDataHelperMap.forEach(clump => {
|
||||
this.abort(clump.localMessageID);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type FetchSteamConfig,
|
||||
FetchStreamErrorCode,
|
||||
} from '@coze-arch/fetch-stream';
|
||||
|
||||
import { type FetchDataHelper } from '../utils';
|
||||
import { type HttpChunkEvents } from '../events/http-chunk-events';
|
||||
import { type RequestScene } from '../../../request-manager/types';
|
||||
import { type ChunkRaw } from '../../../message/types';
|
||||
|
||||
export { FetchStreamErrorCode };
|
||||
|
||||
export interface HttpChunkMessageInvalidErrorInfo {
|
||||
replyID?: string;
|
||||
logID?: string;
|
||||
}
|
||||
|
||||
export interface ErrorInfo {
|
||||
msg: string;
|
||||
code: FetchStreamErrorCode | number;
|
||||
error?: unknown;
|
||||
ext?: {
|
||||
localMessageID?: string;
|
||||
logID?: string;
|
||||
replyID?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OnMessageCallbackParams {
|
||||
chunk: ChunkRaw;
|
||||
logID?: string;
|
||||
replyID?: string;
|
||||
}
|
||||
|
||||
export interface MessageLifecycleCallbackParams
|
||||
extends Pick<OnMessageCallbackParams, 'logID' | 'replyID'> {
|
||||
localMessageID: string;
|
||||
}
|
||||
|
||||
export type OnMessageCallback = (messageEvent: OnMessageCallbackParams) => void;
|
||||
/**
|
||||
* 收到的消息结构体异常响应此错误
|
||||
*/
|
||||
export type OnMessageInvalidCallback = (
|
||||
error: HttpChunkMessageInvalidErrorInfo,
|
||||
) => void;
|
||||
export type OnMessageSuccessCallback = (
|
||||
params: MessageLifecycleCallbackParams,
|
||||
) => void;
|
||||
export type OnMessageStartCallback = (
|
||||
params: MessageLifecycleCallbackParams,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* HttpChunk 未能建连、意外断开、Abort
|
||||
*/
|
||||
export type OnErrorCallback = (errorInfo: ErrorInfo) => void;
|
||||
|
||||
export type OnMessageTimeoutCallback = (
|
||||
params: MessageLifecycleCallbackParams,
|
||||
) => void;
|
||||
|
||||
export type SendMessageConfig = Pick<
|
||||
FetchSteamConfig,
|
||||
'betweenChunkTimeout' | 'totalFetchTimeout' | 'headers'
|
||||
> & { requestScene?: RequestScene };
|
||||
|
||||
export interface ChannelEventMap {
|
||||
[HttpChunkEvents.MESSAGE_RECEIVED]: OnMessageCallback;
|
||||
[HttpChunkEvents.BETWEEN_CHUNK_TIMEOUT]: OnMessageTimeoutCallback;
|
||||
[HttpChunkEvents.TOTAL_FETCH_TIMEOUT]: OnMessageTimeoutCallback;
|
||||
[HttpChunkEvents.FETCH_SUCCESS]: OnMessageSuccessCallback;
|
||||
[HttpChunkEvents.FETCH_START]: OnMessageStartCallback;
|
||||
[HttpChunkEvents.FETCH_ERROR]: OnErrorCallback;
|
||||
[HttpChunkEvents.INVALID_MESSAGE]: OnMessageInvalidCallback;
|
||||
[HttpChunkEvents.READ_STREAM_START]: OnMessageStartCallback;
|
||||
[HttpChunkEvents.READ_STREAM_ERROR]: OnErrorCallback;
|
||||
[HttpChunkEvents.ALL_SUCCESS]: OnMessageSuccessCallback;
|
||||
}
|
||||
|
||||
export interface HandleMessageParams {
|
||||
message: ParsedEvent;
|
||||
fetchDataHelper?: FetchDataHelper;
|
||||
}
|
||||
|
||||
export interface HandleErrorParams {
|
||||
errorInfo: ErrorInfo;
|
||||
fetchDataHelper?: FetchDataHelper;
|
||||
}
|
||||
|
||||
export interface HandleMessageSuccessParams {
|
||||
fetchDataHelper?: FetchDataHelper;
|
||||
}
|
||||
|
||||
export interface HandleMessageTimerEndParams {
|
||||
fetchDataHelper?: FetchDataHelper;
|
||||
}
|
||||
|
||||
export interface HandleMessageTimerEndParams {
|
||||
fetchDataHelper?: FetchDataHelper;
|
||||
}
|
||||
|
||||
export interface ParsedEvent {
|
||||
event: string;
|
||||
data: ChunkRaw | string | undefined;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { type FetchSteamConfig } from '@coze-arch/fetch-stream';
|
||||
|
||||
import { safeJSONParse } from '../../shared/utils/safe-json-parse';
|
||||
import { type ChunkRaw } from '../../message/types';
|
||||
import {
|
||||
type ParsedEvent,
|
||||
type ChannelEventMap,
|
||||
type MessageLifecycleCallbackParams,
|
||||
} from './types';
|
||||
|
||||
export interface RetryCounterConfig {
|
||||
maxRetryAttempts?: number;
|
||||
}
|
||||
|
||||
const defaultMaxRetryAttempts = 3;
|
||||
|
||||
export class RetryCounter {
|
||||
private attempts = 0;
|
||||
private maxRetryAttempts = 0;
|
||||
|
||||
constructor(config?: RetryCounterConfig) {
|
||||
this.maxRetryAttempts = config?.maxRetryAttempts || defaultMaxRetryAttempts;
|
||||
}
|
||||
|
||||
add = () => {
|
||||
this.attempts++;
|
||||
};
|
||||
reset = () => {
|
||||
this.attempts = 0;
|
||||
};
|
||||
|
||||
matchMaxRetryAttempts = () => this.attempts >= this.maxRetryAttempts;
|
||||
}
|
||||
|
||||
interface FetchDataHelperConstructor {
|
||||
localMessageID: string;
|
||||
retryCounterConfig?: RetryCounterConfig;
|
||||
totalFetchTimeout?: number;
|
||||
betweenChunkTimeout?: number;
|
||||
headers?: HeadersInit;
|
||||
}
|
||||
|
||||
export class FetchDataHelper {
|
||||
abortSignal: AbortController;
|
||||
seqID?: number;
|
||||
retryCounter: RetryCounter;
|
||||
localMessageID: string;
|
||||
replyID?: string;
|
||||
logID?: string;
|
||||
totalFetchTimeout?: number;
|
||||
betweenChunkTimeout?: number;
|
||||
headers?: HeadersInit;
|
||||
|
||||
constructor({
|
||||
localMessageID,
|
||||
retryCounterConfig,
|
||||
betweenChunkTimeout,
|
||||
totalFetchTimeout,
|
||||
headers,
|
||||
}: FetchDataHelperConstructor) {
|
||||
this.localMessageID = localMessageID;
|
||||
this.retryCounter = new RetryCounter(retryCounterConfig);
|
||||
this.abortSignal = new AbortController();
|
||||
this.betweenChunkTimeout = betweenChunkTimeout;
|
||||
this.totalFetchTimeout = totalFetchTimeout;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
setReplyID = (id: string) => {
|
||||
this.replyID = id;
|
||||
};
|
||||
setSeqID = (id: number) => {
|
||||
this.seqID = id;
|
||||
};
|
||||
setLogID = (id?: string | null) => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
this.logID = id;
|
||||
};
|
||||
}
|
||||
export enum ChunkEvent {
|
||||
ERROR = 'error',
|
||||
DONE = 'done',
|
||||
MESSAGE = 'message',
|
||||
}
|
||||
|
||||
export const streamParser: FetchSteamConfig<
|
||||
ParsedEvent,
|
||||
FetchDataHelper
|
||||
>['streamParser'] = (parseEvent, { terminate }) => {
|
||||
const { type } = parseEvent;
|
||||
|
||||
if (type === 'event') {
|
||||
const { data, event } = parseEvent;
|
||||
switch (event) {
|
||||
case ChunkEvent.MESSAGE:
|
||||
return {
|
||||
event,
|
||||
data: safeJSONParse<ChunkRaw>(data, null).value || undefined,
|
||||
};
|
||||
case ChunkEvent.DONE:
|
||||
terminate();
|
||||
return;
|
||||
// 对话过程中出现异常,例如:token 消耗完了
|
||||
case ChunkEvent.ERROR:
|
||||
return { event, data };
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getDataHelperPlaceholder = () =>
|
||||
new FetchDataHelper({
|
||||
localMessageID:
|
||||
'DataClamp placeholder, please check your HttpChunk Instance',
|
||||
});
|
||||
|
||||
export function inValidChunkRaw(value: unknown): value is ChunkRaw {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
'seq_id' in value &&
|
||||
'message' in value &&
|
||||
'is_finish' in value &&
|
||||
'seq_id' in value
|
||||
);
|
||||
}
|
||||
|
||||
export class CustomEventEmitter extends EventEmitter {
|
||||
public customEmit<K extends keyof ChannelEventMap>(
|
||||
event: K,
|
||||
...args: Parameters<ChannelEventMap[K]>
|
||||
) {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export const getMessageLifecycleCallbackParam = (
|
||||
dataClump: FetchDataHelper | undefined,
|
||||
): MessageLifecycleCallbackParams => {
|
||||
const { localMessageID = '', replyID, logID } = dataClump ?? {};
|
||||
return {
|
||||
localMessageID,
|
||||
replyID,
|
||||
logID,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum SdkEventsEnum {
|
||||
MESSAGE_RECEIVED_AND_UPDATE = 'message_received_and_update',
|
||||
/**
|
||||
* 监测拉取回复消息状态变化
|
||||
*/
|
||||
MESSAGE_PULLING_STATUS = 'message_pulling_status',
|
||||
ERROR = 'error',
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReportLog, type Tracer } from '../../report-log';
|
||||
import type { ChatCoreError } from '../../custom-error';
|
||||
|
||||
/**
|
||||
* 承接所有 sdk 的 slardar 自定义事件
|
||||
*/
|
||||
export enum SlardarEvents {
|
||||
// sdk初始化,用于数据统计
|
||||
SDK_INIT = 'chat_sdk_init',
|
||||
// 上传失败
|
||||
SDK_MESSAGE_UPLOAD_FAIL = 'chat_sdk_message_upload_fail',
|
||||
// 打断消息
|
||||
SDK_BREAK_MESSAGE = 'chat_sdk_break_message',
|
||||
// 消息发送链路监控
|
||||
SDK_MESSAGE_SEND_TRACER = 'chat_sdk_message_send_tracer',
|
||||
// 拉流链路耗时监控
|
||||
SDK_PULL_STREAM_TRACER = 'chat_sdk_pull_stream_tracer',
|
||||
}
|
||||
|
||||
/**
|
||||
* slardar事件追踪
|
||||
*/
|
||||
export class ReportEventsTracer {
|
||||
private reporter: ReportLog;
|
||||
|
||||
private eventTracers = new Map<
|
||||
string,
|
||||
{
|
||||
trace: Tracer;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
>();
|
||||
|
||||
constructor(reporter: ReportLog) {
|
||||
this.reporter = reporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息发送事件追踪
|
||||
*/
|
||||
sendMessageTracer = {
|
||||
start: (local_message_id: string, meta?: Record<string, unknown>) => {
|
||||
const { trace } = this.createTracer(
|
||||
SlardarEvents.SDK_MESSAGE_SEND_TRACER,
|
||||
);
|
||||
this.setTracer(local_message_id, SlardarEvents.SDK_MESSAGE_SEND_TRACER, {
|
||||
trace,
|
||||
});
|
||||
trace?.('start', {
|
||||
meta,
|
||||
});
|
||||
},
|
||||
success: (local_message_id: string, meta?: Record<string, unknown>) => {
|
||||
const { trace } = this.getTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_MESSAGE_SEND_TRACER,
|
||||
);
|
||||
trace?.('success', {
|
||||
meta,
|
||||
});
|
||||
this.deleteTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_MESSAGE_SEND_TRACER,
|
||||
);
|
||||
},
|
||||
error: (chatCoreError: ChatCoreError) => {
|
||||
const { local_message_id } = chatCoreError.ext;
|
||||
if (!local_message_id) {
|
||||
return;
|
||||
}
|
||||
const { trace } = this.getTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_MESSAGE_SEND_TRACER,
|
||||
);
|
||||
trace?.('error', {
|
||||
meta: chatCoreError.flatten(),
|
||||
error: chatCoreError,
|
||||
});
|
||||
this.deleteTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_MESSAGE_SEND_TRACER,
|
||||
);
|
||||
},
|
||||
timeout: (local_message_id: string) => {
|
||||
const { trace } = this.getTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_MESSAGE_SEND_TRACER,
|
||||
);
|
||||
trace?.('timeout');
|
||||
this.deleteTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_MESSAGE_SEND_TRACER,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
* 拉取流事件追踪
|
||||
*/
|
||||
pullStreamTracer = {
|
||||
start: (local_message_id: string, meta?: Record<string, unknown>) => {
|
||||
const { trace } = this.createTracer(SlardarEvents.SDK_PULL_STREAM_TRACER);
|
||||
this.setTracer(local_message_id, SlardarEvents.SDK_PULL_STREAM_TRACER, {
|
||||
trace,
|
||||
meta,
|
||||
});
|
||||
trace?.('start', {
|
||||
meta,
|
||||
});
|
||||
},
|
||||
success: (local_message_id: string, meta?: Record<string, unknown>) => {
|
||||
const { trace } = this.getTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_PULL_STREAM_TRACER,
|
||||
);
|
||||
trace?.('success', {
|
||||
meta,
|
||||
});
|
||||
this.deleteTracer(local_message_id, SlardarEvents.SDK_PULL_STREAM_TRACER);
|
||||
},
|
||||
break: (local_message_id: string, meta?: Record<string, unknown>) => {
|
||||
const { trace } = this.getTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_PULL_STREAM_TRACER,
|
||||
);
|
||||
// 打断算成功
|
||||
trace?.('success', {
|
||||
meta,
|
||||
});
|
||||
this.deleteTracer(local_message_id, SlardarEvents.SDK_PULL_STREAM_TRACER);
|
||||
},
|
||||
error: (chatCoreError: ChatCoreError, meta?: Record<string, unknown>) => {
|
||||
const { local_message_id } = chatCoreError.ext;
|
||||
if (!local_message_id) {
|
||||
return;
|
||||
}
|
||||
const { trace } = this.getTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_PULL_STREAM_TRACER,
|
||||
);
|
||||
trace?.('error', {
|
||||
meta: {
|
||||
...chatCoreError.flatten(),
|
||||
...meta,
|
||||
},
|
||||
error: chatCoreError,
|
||||
});
|
||||
this.deleteTracer(local_message_id, SlardarEvents.SDK_PULL_STREAM_TRACER);
|
||||
},
|
||||
timeout: (chatCoreError: ChatCoreError) => {
|
||||
const { local_message_id } = chatCoreError.ext;
|
||||
if (!local_message_id) {
|
||||
return;
|
||||
}
|
||||
const { trace } = this.getTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_PULL_STREAM_TRACER,
|
||||
);
|
||||
trace?.('timeout', {
|
||||
meta: chatCoreError.flatten(),
|
||||
error: chatCoreError,
|
||||
});
|
||||
this.deleteTracer(local_message_id, SlardarEvents.SDK_PULL_STREAM_TRACER);
|
||||
},
|
||||
receiveAck: (local_message_id: string, meta?: Record<string, unknown>) => {
|
||||
const { trace } = this.getTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_PULL_STREAM_TRACER,
|
||||
);
|
||||
trace?.('ack', {
|
||||
meta,
|
||||
});
|
||||
},
|
||||
receiveFirstAnsChunk: (
|
||||
local_message_id: string,
|
||||
meta?: Record<string, unknown>,
|
||||
) => {
|
||||
const { trace } = this.getTracer(
|
||||
local_message_id,
|
||||
SlardarEvents.SDK_PULL_STREAM_TRACER,
|
||||
);
|
||||
trace?.('first_ans_chunk', {
|
||||
meta,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 组装获取唯一key
|
||||
*/
|
||||
static getUniqueKey(local_message_id: string, event: SlardarEvents): string {
|
||||
return `${local_message_id}_${event}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据local_message_id、event获取trace
|
||||
*/
|
||||
getTracer(local_message_id: string, event: SlardarEvents) {
|
||||
return (
|
||||
this.eventTracers.get(
|
||||
ReportEventsTracer.getUniqueKey(local_message_id, event),
|
||||
) || {
|
||||
trace: undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据local_message_id、event新增trace
|
||||
*/
|
||||
setTracer(
|
||||
local_message_id: string,
|
||||
event: SlardarEvents,
|
||||
traceInfo: {
|
||||
trace: Tracer;
|
||||
meta?: Record<string, unknown>;
|
||||
},
|
||||
) {
|
||||
const { trace, meta } = traceInfo;
|
||||
this.eventTracers.set(
|
||||
ReportEventsTracer.getUniqueKey(local_message_id, event),
|
||||
{
|
||||
trace,
|
||||
meta,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除trace
|
||||
*/
|
||||
deleteTracer(local_message_id: string, event: SlardarEvents) {
|
||||
this.eventTracers.delete(
|
||||
ReportEventsTracer.getUniqueKey(local_message_id, event),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建trace
|
||||
*/
|
||||
createTracer(eventName: SlardarEvents) {
|
||||
return this.reporter.slardarTracer({
|
||||
eventName,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module @coze-common/chat-core
|
||||
* 暴露所有对外接口
|
||||
*/
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { type InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
import { type DeployVersion, type ENV } from '@/shared/const';
|
||||
import { type RequestManagerOptions } from '@/request-manager/types';
|
||||
import { RequestManager } from '@/request-manager';
|
||||
import { ReportLog } from '@/report-log';
|
||||
import { type EventPayloadMaps } from '@/plugins/upload-plugin/types/plugin-upload';
|
||||
import {
|
||||
type GetHistoryMessageResponse,
|
||||
type ClearMessageContextParams,
|
||||
} from '@/message/types/message-manager';
|
||||
import {
|
||||
type CreateMessageOptions,
|
||||
type FileMessageProps,
|
||||
type ImageMessageProps,
|
||||
type NormalizedMessageProps,
|
||||
type SendMessageOptions,
|
||||
} from '@/message/types';
|
||||
import { PreSendLocalMessageEventsManager } from '@/message/presend-local-message/presend-local-message-events-manager';
|
||||
import { MessageManager } from '@/message/message-manager';
|
||||
import { ChunkProcessor, PreSendLocalMessageFactory } from '@/message';
|
||||
import { HttpChunk } from '@/channel/http-chunk';
|
||||
|
||||
import { type TokenManager } from '../credential';
|
||||
import {
|
||||
type ChatASRParams,
|
||||
type BreakMessageParams,
|
||||
type DeleteMessageParams,
|
||||
type GetHistoryMessageParams,
|
||||
type ReportMessageParams,
|
||||
} from './types/services/message-manager-service';
|
||||
import { isPresetBotUnique, SdkEventsEnum } from './types/interface';
|
||||
import type {
|
||||
Biz,
|
||||
BotUnique,
|
||||
CreateProps,
|
||||
LogLevel,
|
||||
Message,
|
||||
PluginKey,
|
||||
PluginValue,
|
||||
PresetBot,
|
||||
Scene,
|
||||
SdkEventsCallbackMap,
|
||||
ContentType,
|
||||
} from './types/interface';
|
||||
import { SendMessageService } from './services/send-message-service';
|
||||
import { PluginsService } from './services/plugins-service';
|
||||
import { MessageManagerService } from './services/message-manager-service';
|
||||
import { HttpChunkService } from './services/http-chunk-service';
|
||||
import { CreateMessageService } from './services/create-message-service';
|
||||
import { ReportEventsTracer, SlardarEvents } from './events/slardar-events';
|
||||
|
||||
export default class ChatSDK {
|
||||
private static instances: Map<string, ChatSDK> = new Map();
|
||||
/**
|
||||
* 预发送消息工厂: 创建预发送消息用于上屏
|
||||
*/
|
||||
private preSendLocalMessageFactory!: PreSendLocalMessageFactory;
|
||||
|
||||
/**
|
||||
* 处理接收到的 chunk 消息,处理成统一 Message 格式
|
||||
*/
|
||||
private chunkProcessor!: ChunkProcessor;
|
||||
|
||||
private messageManager!: MessageManager;
|
||||
|
||||
/**
|
||||
* stream拉流
|
||||
*/
|
||||
private httpChunk!: HttpChunk;
|
||||
|
||||
private reportLog!: ReportLog;
|
||||
|
||||
private reportLogWithScope!: ReportLog;
|
||||
|
||||
private requestManager!: RequestManager;
|
||||
|
||||
/**
|
||||
* 维护本地消息的发送事件:
|
||||
*/
|
||||
|
||||
private preSendLocalMessageEventsManager!: PreSendLocalMessageEventsManager;
|
||||
|
||||
static EVENTS = SdkEventsEnum;
|
||||
|
||||
biz!: Biz;
|
||||
|
||||
bot_id!: string;
|
||||
|
||||
space_id?: string;
|
||||
|
||||
preset_bot!: PresetBot;
|
||||
|
||||
/**
|
||||
* 目前chat-core生命周期内以一个 bot_id、user 为维度
|
||||
* 后续如果支持一个 user对应多个 conversation_id,需要调整
|
||||
*/
|
||||
user!: string;
|
||||
|
||||
scene?: Scene;
|
||||
|
||||
/**
|
||||
* 使用环境
|
||||
*/
|
||||
env!: ENV;
|
||||
|
||||
deployVersion!: DeployVersion;
|
||||
|
||||
bot_version?: string;
|
||||
|
||||
draft_mode?: boolean;
|
||||
|
||||
conversation_id!: string;
|
||||
|
||||
enableDebug?: boolean;
|
||||
|
||||
logLevel?: LogLevel;
|
||||
|
||||
tokenManager?: TokenManager;
|
||||
|
||||
private requestManagerOptions?: RequestManagerOptions;
|
||||
|
||||
private eventBus: EventEmitter<SdkEventsEnum> = new EventEmitter();
|
||||
|
||||
private reportEventsTracer!: ReportEventsTracer;
|
||||
|
||||
private sendMessageService!: SendMessageService;
|
||||
|
||||
private createMessageService!: CreateMessageService;
|
||||
|
||||
private messageManagerService!: MessageManagerService;
|
||||
|
||||
private httpChunkService!: HttpChunkService;
|
||||
|
||||
private pluginsService!: PluginsService;
|
||||
|
||||
constructor(props: CreateProps) {
|
||||
/** 初始化构造参数 */
|
||||
this.initProps(props);
|
||||
this.initModules(props);
|
||||
this.initServices();
|
||||
this.onEvents();
|
||||
this.reportLogWithScope.slardarEvent({
|
||||
eventName: SlardarEvents.SDK_INIT,
|
||||
meta: props,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建chatBot实例
|
||||
*1. 对于同一个 bot_id/preset_bot, 重复调用,sdk 只会创建一个实例
|
||||
*2. 多个 bot_id/presetBot,对应多个 sdk 实例,维护自己的events
|
||||
*/
|
||||
static create(props: CreateProps) {
|
||||
const { unique_key } = ChatSDK.getUniqueKey(props);
|
||||
// 对于同一个 bot_id/preset_bot, 重复调用,create只会创建一个实例
|
||||
if (ChatSDK.instances.has(unique_key)) {
|
||||
console.error('duplicate chat core instance error');
|
||||
return ChatSDK.instances.get(unique_key);
|
||||
}
|
||||
|
||||
const instance = new ChatSDK(props);
|
||||
ChatSDK.instances.set(unique_key, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 sdk 唯一键 preset_bot > bot_id
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
static getUniqueKey(props: BotUnique): {
|
||||
unique_key: string;
|
||||
bot_id: string;
|
||||
preset_bot: PresetBot;
|
||||
} {
|
||||
if (isPresetBotUnique(props)) {
|
||||
return {
|
||||
unique_key: props.preset_bot,
|
||||
bot_id: '',
|
||||
preset_bot: props.preset_bot,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
unique_key: props.bot_id,
|
||||
bot_id: props.bot_id,
|
||||
preset_bot: '',
|
||||
};
|
||||
}
|
||||
|
||||
private initProps(props: CreateProps) {
|
||||
const { bot_id, preset_bot } = ChatSDK.getUniqueKey(props);
|
||||
const {
|
||||
enableDebug,
|
||||
logLevel,
|
||||
conversation_id,
|
||||
biz,
|
||||
user,
|
||||
env,
|
||||
deployVersion,
|
||||
scene,
|
||||
bot_version,
|
||||
draft_mode,
|
||||
space_id,
|
||||
} = props;
|
||||
this.bot_id = bot_id;
|
||||
this.space_id = space_id;
|
||||
this.preset_bot = preset_bot;
|
||||
this.conversation_id = conversation_id;
|
||||
this.biz = biz;
|
||||
this.enableDebug = enableDebug || false;
|
||||
this.logLevel = logLevel || 'error';
|
||||
this.user = user || '';
|
||||
this.env = env;
|
||||
this.deployVersion = deployVersion;
|
||||
this.scene = scene;
|
||||
this.bot_version = bot_version;
|
||||
this.draft_mode = draft_mode;
|
||||
}
|
||||
|
||||
/** 初始化依赖Module实例 */
|
||||
private initModules(props: CreateProps) {
|
||||
this.initReportLog();
|
||||
this.reportEventsTracer = new ReportEventsTracer(this.reportLogWithScope);
|
||||
this.initRequestManager(props);
|
||||
this.initTokenManager(this.requestManager, props);
|
||||
this.preSendLocalMessageEventsManager =
|
||||
new PreSendLocalMessageEventsManager({
|
||||
reportLog: this.reportLog,
|
||||
});
|
||||
/** 初始化预发送消息工厂 */
|
||||
this.preSendLocalMessageFactory = new PreSendLocalMessageFactory({
|
||||
bot_id: this.bot_id,
|
||||
preset_bot: this.preset_bot,
|
||||
conversation_id: this.conversation_id,
|
||||
user: this.user,
|
||||
scene: this.scene,
|
||||
bot_version: this.bot_version,
|
||||
draft_mode: this.draft_mode,
|
||||
});
|
||||
/** 初始化处理接收到的 chunk 消息,处理成统一 Message 格式 */
|
||||
this.chunkProcessor = new ChunkProcessor({
|
||||
bot_id: this.bot_id,
|
||||
preset_bot: this.preset_bot,
|
||||
enableDebug: this.enableDebug,
|
||||
});
|
||||
this.httpChunk = new HttpChunk({
|
||||
tokenManager: props.tokenManager,
|
||||
requestManager: this.requestManager,
|
||||
reportLogWithScope: this.reportLogWithScope,
|
||||
});
|
||||
/**
|
||||
* 初始化消息管理器:消息删除/历史等
|
||||
*/
|
||||
this.messageManager = new MessageManager({
|
||||
reportLog: this.reportLog,
|
||||
requestManager: this.requestManager,
|
||||
});
|
||||
}
|
||||
|
||||
private onEvents() {
|
||||
this.httpChunkService.onHttpChunkEvents();
|
||||
}
|
||||
|
||||
private initServices() {
|
||||
this.pluginsService = new PluginsService();
|
||||
this.createMessageService = new CreateMessageService({
|
||||
preSendLocalMessageFactory: this.preSendLocalMessageFactory,
|
||||
preSendLocalMessageEventsManager: this.preSendLocalMessageEventsManager,
|
||||
reportLogWithScope: this.reportLogWithScope,
|
||||
pluginsService: this.pluginsService,
|
||||
});
|
||||
this.sendMessageService = new SendMessageService({
|
||||
preSendLocalMessageFactory: this.preSendLocalMessageFactory,
|
||||
httpChunk: this.httpChunk,
|
||||
preSendLocalMessageEventsManager: this.preSendLocalMessageEventsManager,
|
||||
reportLogWithScope: this.reportLogWithScope,
|
||||
reportEventsTracer: this.reportEventsTracer,
|
||||
});
|
||||
this.messageManagerService = new MessageManagerService({
|
||||
messageManager: this.messageManager,
|
||||
conversation_id: this.conversation_id,
|
||||
scene: this.scene,
|
||||
bot_id: this.bot_id,
|
||||
preset_bot: this.preset_bot,
|
||||
draft_mode: this.draft_mode,
|
||||
httpChunk: this.httpChunk,
|
||||
chunkProcessor: this.chunkProcessor,
|
||||
reportEventsTracer: this.reportEventsTracer,
|
||||
reportLogWithScope: this.reportLogWithScope,
|
||||
});
|
||||
this.httpChunkService = new HttpChunkService({
|
||||
httpChunk: this.httpChunk,
|
||||
reportLogWithScope: this.reportLogWithScope,
|
||||
chunkProcessor: this.chunkProcessor,
|
||||
preSendLocalMessageEventsManager: this.preSendLocalMessageEventsManager,
|
||||
chatSdkEventEmit: this.emit.bind(this),
|
||||
chatSdkEventBus: this.eventBus,
|
||||
reportEventsTracer: this.reportEventsTracer,
|
||||
});
|
||||
}
|
||||
|
||||
private initReportLog() {
|
||||
this.reportLog = new ReportLog({
|
||||
logLevel: this.logLevel,
|
||||
env: this.env,
|
||||
deployVersion: this.deployVersion,
|
||||
meta: {
|
||||
biz: this.biz,
|
||||
chatCoreVersion: '1.1.0',
|
||||
},
|
||||
});
|
||||
this.reportLog.init();
|
||||
this.reportLogWithScope = this.reportLog.createLoggerWith({
|
||||
scope: 'chat-sdk',
|
||||
});
|
||||
}
|
||||
|
||||
private initTokenManager(requestManager: RequestManager, props: CreateProps) {
|
||||
this.tokenManager = props.tokenManager;
|
||||
|
||||
if (!this.tokenManager) {
|
||||
return;
|
||||
}
|
||||
const tokenManagerRequestHook = (config: InternalAxiosRequestConfig) => {
|
||||
if (!this.tokenManager) {
|
||||
return config;
|
||||
}
|
||||
const apiKeyAuthValue = this.tokenManager.getApiKeyAuthorizationValue();
|
||||
if (apiKeyAuthValue) {
|
||||
config.headers.set('Authorization', apiKeyAuthValue);
|
||||
}
|
||||
return config;
|
||||
};
|
||||
const options: RequestManagerOptions = {
|
||||
hooks: {
|
||||
onBeforeRequest: [tokenManagerRequestHook],
|
||||
},
|
||||
};
|
||||
requestManager.appendRequestOptions(options);
|
||||
}
|
||||
|
||||
private initRequestManager(props: CreateProps) {
|
||||
this.requestManagerOptions = props.requestManagerOptions;
|
||||
this.requestManager = new RequestManager({
|
||||
options: this.requestManagerOptions,
|
||||
reportLog: this.reportLog,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁 SDK 实例。
|
||||
* 清空所有监听事件
|
||||
*/
|
||||
destroy() {
|
||||
// 清空所有httpChunk监听事件
|
||||
this.httpChunk.drop();
|
||||
// 清空sdk时间
|
||||
this.eventBus.removeAllListeners();
|
||||
// 清空所有缓存的 chunk 消息
|
||||
this.chunkProcessor.streamBuffer.clearMessageBuffer();
|
||||
// 清除对应的实例
|
||||
const { unique_key } = ChatSDK.getUniqueKey({
|
||||
bot_id: this.bot_id,
|
||||
preset_bot: this.preset_bot,
|
||||
});
|
||||
ChatSDK.instances.delete(unique_key);
|
||||
// 清除预发送消息缓存
|
||||
this.preSendLocalMessageEventsManager.destroy();
|
||||
this.reportLogWithScope.info({
|
||||
message: 'SDK销毁',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听sdk事件
|
||||
*/
|
||||
on<T extends SdkEventsEnum>(event: T, fn: SdkEventsCallbackMap[T]) {
|
||||
// 重复监听,错误提示
|
||||
if (this.eventBus.eventNames().includes(event)) {
|
||||
this.reportLogWithScope.slardarError({
|
||||
message: '重复监听事件',
|
||||
error: new Error('重复监听'),
|
||||
meta: {
|
||||
event,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.eventBus.on(event, fn);
|
||||
|
||||
return () => {
|
||||
this.eventBus.off(event, fn);
|
||||
};
|
||||
}
|
||||
|
||||
off<T extends SdkEventsEnum>(event: T, fn: SdkEventsCallbackMap[T]) {
|
||||
this.eventBus.off(event, fn);
|
||||
}
|
||||
|
||||
private emit<T extends SdkEventsEnum>(
|
||||
event: T,
|
||||
...args: Parameters<SdkEventsCallbackMap[T]>
|
||||
) {
|
||||
this.eventBus.emit(event, ...args);
|
||||
}
|
||||
|
||||
createTextMessage(
|
||||
...args: Parameters<CreateMessageService['createTextMessage']>
|
||||
) {
|
||||
return this.createMessageService.createTextMessage(...args);
|
||||
}
|
||||
|
||||
createImageMessage<M extends EventPayloadMaps = EventPayloadMaps>(
|
||||
props: ImageMessageProps<M>,
|
||||
options?: CreateMessageOptions,
|
||||
) {
|
||||
return this.createMessageService.createImageMessage(props, options);
|
||||
}
|
||||
|
||||
createFileMessage<M extends EventPayloadMaps = EventPayloadMaps>(
|
||||
props: FileMessageProps<M>,
|
||||
options?: CreateMessageOptions,
|
||||
) {
|
||||
return this.createMessageService.createFileMessage<M>(props, options);
|
||||
}
|
||||
|
||||
createTextAndFileMixMessage(
|
||||
...args: Parameters<CreateMessageService['createTextAndFileMixMessage']>
|
||||
) {
|
||||
return this.createMessageService.createTextAndFileMixMessage(...args);
|
||||
}
|
||||
|
||||
createNormalizedPayloadMessage<T extends ContentType>(
|
||||
props: NormalizedMessageProps<T>,
|
||||
options?: CreateMessageOptions,
|
||||
): Message<T> {
|
||||
return this.createMessageService.createNormalizedPayloadMessage(
|
||||
props,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
resumeMessage(message: Message<ContentType>, options?: SendMessageOptions) {
|
||||
return this.sendMessageService.resumeMessage(message, options);
|
||||
}
|
||||
|
||||
sendMessage(
|
||||
message: Message<ContentType>,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<Message<ContentType>> {
|
||||
return this.sendMessageService.sendMessage(message, options);
|
||||
}
|
||||
|
||||
registerPlugin<T extends PluginKey, P extends Record<string, unknown>>(
|
||||
key: T,
|
||||
plugin: PluginValue<T, P>,
|
||||
constructorOptions?: P,
|
||||
) {
|
||||
this.pluginsService.registerPlugin(key, plugin, constructorOptions);
|
||||
}
|
||||
|
||||
checkPluginIsRegistered(key: PluginKey): boolean {
|
||||
return this.pluginsService.checkPluginIsRegistered(key);
|
||||
}
|
||||
|
||||
getRegisteredPlugin(key: PluginKey) {
|
||||
return this.pluginsService.getRegisteredPlugin(key);
|
||||
}
|
||||
|
||||
getHistoryMessage(params: GetHistoryMessageParams) {
|
||||
return this.messageManagerService.getHistoryMessage(params);
|
||||
}
|
||||
|
||||
static convertMessageList = (
|
||||
data: GetHistoryMessageResponse['message_list'],
|
||||
) => MessageManager.convertMessageList(data);
|
||||
|
||||
clearMessageContext(params: ClearMessageContextParams) {
|
||||
return this.messageManagerService.clearMessageContext(params);
|
||||
}
|
||||
|
||||
clearHistory() {
|
||||
return this.messageManagerService.clearHistory();
|
||||
}
|
||||
|
||||
deleteMessage(params: DeleteMessageParams) {
|
||||
return this.messageManagerService.deleteMessage(params);
|
||||
}
|
||||
|
||||
reportMessage(params: ReportMessageParams) {
|
||||
return this.messageManagerService.reportMessage(params);
|
||||
}
|
||||
|
||||
breakMessage(params: BreakMessageParams) {
|
||||
return this.messageManagerService.breakMessage(params);
|
||||
}
|
||||
|
||||
chatASR(params: ChatASRParams) {
|
||||
if (this.space_id) {
|
||||
params.append('space_id', this.space_id);
|
||||
}
|
||||
return this.messageManagerService.chatASR(params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReportLog } from '@/report-log';
|
||||
import type { EventPayloadMaps } from '@/plugins/upload-plugin/types/plugin-upload';
|
||||
import type {
|
||||
ContentType,
|
||||
CreateMessageOptions,
|
||||
FileMessageProps,
|
||||
ImageMessageProps,
|
||||
Message,
|
||||
NormalizedMessageProps,
|
||||
TextAndFileMixMessageProps,
|
||||
TextMessageProps,
|
||||
} from '@/message/types';
|
||||
import { type PreSendLocalMessageEventsManager } from '@/message/presend-local-message/presend-local-message-events-manager';
|
||||
import { type PreSendLocalMessageFactory } from '@/message';
|
||||
|
||||
import { type PluginsService } from './plugins-service';
|
||||
|
||||
export interface CreateMessageServicesProps {
|
||||
preSendLocalMessageFactory: PreSendLocalMessageFactory;
|
||||
preSendLocalMessageEventsManager: PreSendLocalMessageEventsManager;
|
||||
reportLogWithScope: ReportLog;
|
||||
pluginsService: PluginsService;
|
||||
}
|
||||
|
||||
export class CreateMessageService {
|
||||
preSendLocalMessageFactory: PreSendLocalMessageFactory;
|
||||
preSendLocalMessageEventsManager: PreSendLocalMessageEventsManager;
|
||||
reportLogWithScope: ReportLog;
|
||||
pluginsService: PluginsService;
|
||||
constructor({
|
||||
preSendLocalMessageFactory,
|
||||
preSendLocalMessageEventsManager,
|
||||
reportLogWithScope,
|
||||
pluginsService,
|
||||
}: CreateMessageServicesProps) {
|
||||
this.preSendLocalMessageFactory = preSendLocalMessageFactory;
|
||||
this.preSendLocalMessageEventsManager = preSendLocalMessageEventsManager;
|
||||
this.reportLogWithScope = reportLogWithScope;
|
||||
this.pluginsService = pluginsService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文本消息
|
||||
*/
|
||||
createTextMessage(
|
||||
props: TextMessageProps,
|
||||
options?: CreateMessageOptions,
|
||||
): Message<ContentType.Text> {
|
||||
return this.preSendLocalMessageFactory.createTextMessage(
|
||||
props,
|
||||
this.preSendLocalMessageEventsManager,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图片消息
|
||||
*/
|
||||
createImageMessage<M extends EventPayloadMaps = EventPayloadMaps>(
|
||||
props: ImageMessageProps<M>,
|
||||
options?: CreateMessageOptions,
|
||||
): Message<ContentType.Image> {
|
||||
const { UploadPlugin, uploadPluginConstructorOptions } =
|
||||
this.pluginsService;
|
||||
if (!UploadPlugin) {
|
||||
this.reportLogWithScope.info({
|
||||
message: '请先注册上传插件',
|
||||
});
|
||||
throw new Error('请先注册上传插件');
|
||||
}
|
||||
return this.preSendLocalMessageFactory.createImageMessage({
|
||||
messageProps: props,
|
||||
UploadPlugin,
|
||||
uploadPluginConstructorOptions,
|
||||
messageEventsManager: this.preSendLocalMessageEventsManager,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件消息
|
||||
*/
|
||||
createFileMessage<M extends EventPayloadMaps = EventPayloadMaps>(
|
||||
props: FileMessageProps<M>,
|
||||
options?: CreateMessageOptions,
|
||||
): Message<ContentType.File> {
|
||||
const { UploadPlugin, uploadPluginConstructorOptions } =
|
||||
this.pluginsService;
|
||||
if (!UploadPlugin) {
|
||||
this.reportLogWithScope.info({
|
||||
message: '请先注册上传插件',
|
||||
});
|
||||
throw new Error('请先注册上传插件');
|
||||
}
|
||||
return this.preSendLocalMessageFactory.createFileMessage({
|
||||
messageProps: props,
|
||||
UploadPlugin,
|
||||
uploadPluginConstructorOptions,
|
||||
messageEventsManager: this.preSendLocalMessageEventsManager,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图文混合消息
|
||||
*/
|
||||
createTextAndFileMixMessage(
|
||||
props: TextAndFileMixMessageProps,
|
||||
options?: CreateMessageOptions,
|
||||
): Message<ContentType.Mix> {
|
||||
return this.preSendLocalMessageFactory.createTextAndFileMixMessage(
|
||||
props,
|
||||
this.preSendLocalMessageEventsManager,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标准化消息,已经处理好payload content结构的消息
|
||||
*/
|
||||
createNormalizedPayloadMessage<T extends ContentType>(
|
||||
props: NormalizedMessageProps<T>,
|
||||
options?: CreateMessageOptions,
|
||||
): Message<T> {
|
||||
return this.preSendLocalMessageFactory.createNormalizedMessage<T>(
|
||||
props,
|
||||
this.preSendLocalMessageEventsManager,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type EventEmitter from 'eventemitter3';
|
||||
|
||||
import { type ReportLog } from '@/report-log';
|
||||
import { PreSendLocalMessageEventsEnum } from '@/message/types';
|
||||
import { type PreSendLocalMessageEventsManager } from '@/message/presend-local-message/presend-local-message-events-manager';
|
||||
import { type ChunkProcessor } from '@/message';
|
||||
import { ChatCoreError } from '@/custom-error';
|
||||
import type {
|
||||
ErrorInfo,
|
||||
MessageLifecycleCallbackParams,
|
||||
OnMessageCallbackParams,
|
||||
} from '@/channel/http-chunk/types';
|
||||
import { HttpChunkEvents } from '@/channel/http-chunk/events/http-chunk-events';
|
||||
import { type HttpChunk } from '@/channel/http-chunk';
|
||||
|
||||
import {
|
||||
type PullingStatus,
|
||||
type SdkEventsCallbackMap,
|
||||
SdkEventsEnum,
|
||||
} from '../types/interface';
|
||||
import { type ReportEventsTracer } from '../events/slardar-events';
|
||||
|
||||
export interface HttpChunkServiceProps {
|
||||
httpChunk: HttpChunk;
|
||||
reportLogWithScope: ReportLog;
|
||||
chunkProcessor: ChunkProcessor;
|
||||
preSendLocalMessageEventsManager: PreSendLocalMessageEventsManager;
|
||||
chatSdkEventEmit: <T extends SdkEventsEnum>(
|
||||
event: T,
|
||||
...args: Parameters<SdkEventsCallbackMap[T]>
|
||||
) => void;
|
||||
chatSdkEventBus: EventEmitter<SdkEventsEnum>;
|
||||
reportEventsTracer: ReportEventsTracer;
|
||||
}
|
||||
export class HttpChunkService {
|
||||
httpChunk: HttpChunk;
|
||||
reportLogWithScope: ReportLog;
|
||||
chunkProcessor: ChunkProcessor;
|
||||
preSendLocalMessageEventsManager: PreSendLocalMessageEventsManager;
|
||||
chatSdkEventEmit: <T extends SdkEventsEnum>(
|
||||
event: T,
|
||||
...args: Parameters<SdkEventsCallbackMap[T]>
|
||||
) => void;
|
||||
chatSdkEventBus: EventEmitter<SdkEventsEnum>;
|
||||
reportEventsTracer: ReportEventsTracer;
|
||||
constructor({
|
||||
httpChunk,
|
||||
reportLogWithScope,
|
||||
chunkProcessor,
|
||||
preSendLocalMessageEventsManager,
|
||||
chatSdkEventEmit,
|
||||
chatSdkEventBus,
|
||||
reportEventsTracer,
|
||||
}: HttpChunkServiceProps) {
|
||||
this.httpChunk = httpChunk;
|
||||
this.reportLogWithScope = reportLogWithScope;
|
||||
this.chunkProcessor = chunkProcessor;
|
||||
this.preSendLocalMessageEventsManager = preSendLocalMessageEventsManager;
|
||||
this.chatSdkEventEmit = chatSdkEventEmit;
|
||||
this.reportEventsTracer = reportEventsTracer;
|
||||
this.chatSdkEventBus = chatSdkEventBus;
|
||||
}
|
||||
/**
|
||||
* 处理channel监听到的事件
|
||||
*/
|
||||
onHttpChunkEvents() {
|
||||
this.httpChunk.on(
|
||||
HttpChunkEvents.FETCH_START,
|
||||
this.handleHttpChunkFetchStart,
|
||||
);
|
||||
// 读取流
|
||||
this.httpChunk.on(
|
||||
HttpChunkEvents.MESSAGE_RECEIVED,
|
||||
this.handleHttpChunkMessageReceived,
|
||||
);
|
||||
// 整体拉流成功
|
||||
this.httpChunk.on(
|
||||
HttpChunkEvents.ALL_SUCCESS,
|
||||
this.handleHttpChunkStreamSuccess,
|
||||
);
|
||||
// 开始读取流
|
||||
this.httpChunk.on(
|
||||
HttpChunkEvents.READ_STREAM_START,
|
||||
this.handleHttpChunkReadStreamStart,
|
||||
);
|
||||
// fetch阶段异常, 还没到读流阶段
|
||||
this.httpChunk.on(
|
||||
HttpChunkEvents.FETCH_ERROR,
|
||||
this.handleHttpChunkFetchError,
|
||||
);
|
||||
this.httpChunk.on(
|
||||
HttpChunkEvents.READ_STREAM_ERROR,
|
||||
this.handleReadStreamError,
|
||||
);
|
||||
// 包间超时
|
||||
this.httpChunk.on(
|
||||
HttpChunkEvents.BETWEEN_CHUNK_TIMEOUT,
|
||||
this.handleHttpChunkTimeout,
|
||||
);
|
||||
}
|
||||
|
||||
private handleHttpChunkFetchStart = ({
|
||||
localMessageID,
|
||||
}: MessageLifecycleCallbackParams) => {
|
||||
this.reportEventsTracer?.pullStreamTracer?.start(localMessageID);
|
||||
};
|
||||
|
||||
private handleHttpChunkMessageReceived = (
|
||||
receiveMessage: OnMessageCallbackParams,
|
||||
) => {
|
||||
const { chunk, logID } = receiveMessage;
|
||||
const ackMessage = this.chunkProcessor.getAckMessageByChunk(chunk);
|
||||
const { local_message_id = '' } =
|
||||
ackMessage?.extra_info || receiveMessage.chunk.message.extra_info;
|
||||
|
||||
let pullingStatus: PullingStatus = 'pulling';
|
||||
// 判断是否是final answer
|
||||
if (this.chunkProcessor.isMessageAnswerEnd(chunk)) {
|
||||
pullingStatus = 'answerEnd';
|
||||
}
|
||||
|
||||
this.chatSdkEventEmit(SdkEventsEnum.MESSAGE_PULLING_STATUS, {
|
||||
name: SdkEventsEnum.MESSAGE_PULLING_STATUS,
|
||||
data: {
|
||||
pullingStatus,
|
||||
local_message_id,
|
||||
reply_id: receiveMessage.chunk.message.reply_id || '',
|
||||
},
|
||||
});
|
||||
|
||||
const hasOnMessage = this.chatSdkEventBus
|
||||
.eventNames()
|
||||
.includes(SdkEventsEnum.MESSAGE_RECEIVED_AND_UPDATE);
|
||||
|
||||
// 判断接收到的消息是否已经存在
|
||||
if (this.chunkProcessor.isFirstReplyMessage(chunk)) {
|
||||
this.reportEventsTracer?.pullStreamTracer?.receiveFirstAnsChunk(
|
||||
local_message_id,
|
||||
{
|
||||
logId: logID,
|
||||
},
|
||||
);
|
||||
}
|
||||
this.chunkProcessor.addChunkAndProcess(chunk, {
|
||||
logId: logID,
|
||||
});
|
||||
const processedMessage =
|
||||
this.chunkProcessor.getProcessedMessageByChunk(chunk);
|
||||
|
||||
hasOnMessage &&
|
||||
this.reportLogWithScope.info({
|
||||
message: '消息接收&更新',
|
||||
meta: {
|
||||
logMessageWithDebugInfo:
|
||||
this.chunkProcessor.appendDebugMessage(processedMessage),
|
||||
},
|
||||
});
|
||||
|
||||
if (chunk.message.type === 'ack') {
|
||||
this.reportEventsTracer?.pullStreamTracer?.receiveAck(local_message_id, {
|
||||
logId: logID,
|
||||
});
|
||||
this.preSendLocalMessageEventsManager.emit(
|
||||
PreSendLocalMessageEventsEnum.MESSAGE_SEND_SUCCESS,
|
||||
processedMessage,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatSdkEventEmit(SdkEventsEnum.MESSAGE_RECEIVED_AND_UPDATE, {
|
||||
name: SdkEventsEnum.MESSAGE_RECEIVED_AND_UPDATE,
|
||||
data: [processedMessage],
|
||||
});
|
||||
};
|
||||
|
||||
// 读取流异常
|
||||
private handleReadStreamError = (errorInfo: ErrorInfo) => {
|
||||
const {
|
||||
ext: {
|
||||
localMessageID: local_message_id = '',
|
||||
replyID: reply_id = '',
|
||||
logID: logId = '',
|
||||
} = {},
|
||||
code,
|
||||
msg,
|
||||
} = errorInfo;
|
||||
|
||||
const chatCoreError = new ChatCoreError(msg, {
|
||||
code,
|
||||
local_message_id,
|
||||
logId,
|
||||
reply_id,
|
||||
rawError: errorInfo,
|
||||
});
|
||||
|
||||
const stashedAckMessage =
|
||||
this.chunkProcessor.getAckMessageByLocalMessageId(local_message_id);
|
||||
|
||||
// 读取流异常,要区分在首包接受到了么
|
||||
if (!stashedAckMessage) {
|
||||
this.preSendLocalMessageEventsManager.emit(
|
||||
PreSendLocalMessageEventsEnum.MESSAGE_SEND_FAIL,
|
||||
chatCoreError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是发送消息成功,则表示是拉流阶段失败
|
||||
if (stashedAckMessage) {
|
||||
this.chatSdkEventEmit(SdkEventsEnum.MESSAGE_PULLING_STATUS, {
|
||||
name: SdkEventsEnum.MESSAGE_PULLING_STATUS,
|
||||
data: {
|
||||
pullingStatus: 'error',
|
||||
local_message_id,
|
||||
reply_id,
|
||||
},
|
||||
error: chatCoreError,
|
||||
});
|
||||
}
|
||||
const contentLength =
|
||||
this.chunkProcessor.getReplyMessagesLengthByReplyId(reply_id);
|
||||
this.reportEventsTracer?.pullStreamTracer?.error(chatCoreError, {
|
||||
contentLength,
|
||||
});
|
||||
};
|
||||
|
||||
// fetch异常,还没到拉流阶段
|
||||
private handleHttpChunkFetchError = (errorInfo: ErrorInfo) => {
|
||||
const {
|
||||
ext: {
|
||||
localMessageID: local_message_id = '',
|
||||
replyID: reply_id = '',
|
||||
logID: logId = '',
|
||||
} = {},
|
||||
code,
|
||||
msg,
|
||||
} = errorInfo;
|
||||
|
||||
const chatCoreError = new ChatCoreError(msg, {
|
||||
code,
|
||||
local_message_id,
|
||||
logId,
|
||||
reply_id,
|
||||
rawError: errorInfo,
|
||||
});
|
||||
this.preSendLocalMessageEventsManager.emit(
|
||||
PreSendLocalMessageEventsEnum.MESSAGE_SEND_FAIL,
|
||||
chatCoreError,
|
||||
);
|
||||
};
|
||||
|
||||
private handleHttpChunkStreamSuccess = ({
|
||||
localMessageID,
|
||||
replyID,
|
||||
}: MessageLifecycleCallbackParams) => {
|
||||
this.chatSdkEventEmit(SdkEventsEnum.MESSAGE_PULLING_STATUS, {
|
||||
name: SdkEventsEnum.MESSAGE_PULLING_STATUS,
|
||||
data: {
|
||||
pullingStatus: 'success',
|
||||
local_message_id: localMessageID,
|
||||
reply_id: replyID || '',
|
||||
},
|
||||
});
|
||||
const contentLength =
|
||||
replyID && this.chunkProcessor.getReplyMessagesLengthByReplyId(replyID);
|
||||
this.reportEventsTracer?.pullStreamTracer?.success(localMessageID, {
|
||||
contentLength,
|
||||
});
|
||||
this.reportLogWithScope.info({
|
||||
message: '拉取回复完成',
|
||||
meta: {
|
||||
local_message_id: localMessageID,
|
||||
reply_id: replyID || '',
|
||||
streamBuffer: this.chunkProcessor.streamBuffer,
|
||||
},
|
||||
});
|
||||
replyID &&
|
||||
this.chunkProcessor.streamBuffer.clearMessageBufferByReplyId(replyID);
|
||||
};
|
||||
|
||||
private handleHttpChunkReadStreamStart = ({
|
||||
localMessageID,
|
||||
replyID,
|
||||
logID,
|
||||
}: MessageLifecycleCallbackParams) => {
|
||||
this.reportLogWithScope.info({
|
||||
message: '开始拉取回复',
|
||||
meta: {
|
||||
local_message_id: localMessageID,
|
||||
reply_id: replyID || '',
|
||||
logID,
|
||||
},
|
||||
});
|
||||
|
||||
this.chatSdkEventEmit(SdkEventsEnum.MESSAGE_PULLING_STATUS, {
|
||||
name: SdkEventsEnum.MESSAGE_PULLING_STATUS,
|
||||
data: {
|
||||
pullingStatus: 'start',
|
||||
local_message_id: localMessageID,
|
||||
reply_id: replyID || '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private handleHttpChunkTimeout = (
|
||||
rawError: MessageLifecycleCallbackParams,
|
||||
) => {
|
||||
const { localMessageID, replyID, logID } = rawError;
|
||||
|
||||
const chatCoreError = new ChatCoreError('拉取回复超时', {
|
||||
local_message_id: localMessageID,
|
||||
reply_id: replyID || '',
|
||||
logId: logID,
|
||||
});
|
||||
|
||||
this.reportLogWithScope.info({
|
||||
message: '拉取回复超时',
|
||||
meta: {
|
||||
chatCoreError,
|
||||
},
|
||||
});
|
||||
|
||||
this.chatSdkEventEmit(SdkEventsEnum.MESSAGE_PULLING_STATUS, {
|
||||
name: SdkEventsEnum.MESSAGE_PULLING_STATUS,
|
||||
data: {
|
||||
pullingStatus: 'timeout',
|
||||
local_message_id: localMessageID,
|
||||
reply_id: replyID || '',
|
||||
},
|
||||
error: chatCoreError,
|
||||
abort: () => {
|
||||
this.httpChunk.abort(localMessageID);
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { filterEmptyField } from '@/shared/utils/data-handler';
|
||||
import { type ReportLog } from '@/report-log';
|
||||
import type {
|
||||
ClearMessageContextParams,
|
||||
GetHistoryMessageResponse,
|
||||
} from '@/message/types/message-manager';
|
||||
import { MessageManager } from '@/message/message-manager';
|
||||
import { type ChunkProcessor } from '@/message';
|
||||
import { type HttpChunk } from '@/channel/http-chunk';
|
||||
|
||||
import {
|
||||
type ChatASRParams,
|
||||
type BreakMessageParams,
|
||||
type DeleteMessageParams,
|
||||
type GetHistoryMessageParams,
|
||||
type ReportMessageParams,
|
||||
} from '../types/services/message-manager-service';
|
||||
import type { Scene } from '../types/interface';
|
||||
import {
|
||||
type ReportEventsTracer,
|
||||
SlardarEvents,
|
||||
} from '../events/slardar-events';
|
||||
|
||||
export interface MessageManagerServiceProps {
|
||||
messageManager: MessageManager;
|
||||
conversation_id: string;
|
||||
scene?: Scene;
|
||||
bot_id: string;
|
||||
preset_bot: string;
|
||||
draft_mode?: boolean;
|
||||
httpChunk: HttpChunk;
|
||||
chunkProcessor: ChunkProcessor;
|
||||
reportEventsTracer: ReportEventsTracer;
|
||||
reportLogWithScope: ReportLog;
|
||||
}
|
||||
|
||||
export class MessageManagerService {
|
||||
messageManager: MessageManager;
|
||||
conversation_id: string;
|
||||
scene?: Scene;
|
||||
bot_id: string;
|
||||
preset_bot: string;
|
||||
draft_mode?: boolean;
|
||||
httpChunk: HttpChunk;
|
||||
chunkProcessor: ChunkProcessor;
|
||||
reportEventsTracer: ReportEventsTracer;
|
||||
reportLogWithScope: ReportLog;
|
||||
constructor({
|
||||
messageManager,
|
||||
conversation_id,
|
||||
scene,
|
||||
bot_id,
|
||||
preset_bot,
|
||||
draft_mode,
|
||||
httpChunk,
|
||||
chunkProcessor,
|
||||
reportEventsTracer,
|
||||
reportLogWithScope,
|
||||
}: MessageManagerServiceProps) {
|
||||
this.messageManager = messageManager;
|
||||
this.conversation_id = conversation_id;
|
||||
this.scene = scene;
|
||||
this.bot_id = bot_id;
|
||||
this.preset_bot = preset_bot;
|
||||
this.draft_mode = draft_mode;
|
||||
this.httpChunk = httpChunk;
|
||||
this.chunkProcessor = chunkProcessor;
|
||||
this.reportEventsTracer = reportEventsTracer;
|
||||
this.reportLogWithScope = reportLogWithScope;
|
||||
}
|
||||
/**
|
||||
* 获取历史消息
|
||||
*/
|
||||
async getHistoryMessage(props: GetHistoryMessageParams) {
|
||||
const params = filterEmptyField({
|
||||
conversation_id: this.conversation_id,
|
||||
scene: this.scene,
|
||||
bot_id: this.bot_id,
|
||||
preset_bot: this.preset_bot,
|
||||
draft_mode: this.draft_mode,
|
||||
...props,
|
||||
});
|
||||
return await this.messageManager.getHistoryMessage(params);
|
||||
}
|
||||
|
||||
convertMessageList = (data: GetHistoryMessageResponse['message_list']) =>
|
||||
MessageManager.convertMessageList(data);
|
||||
|
||||
/**
|
||||
* 清空对话上下文
|
||||
*/
|
||||
async clearMessageContext(params: ClearMessageContextParams) {
|
||||
return await this.messageManager.clearMessageContextUrl({
|
||||
conversation_id: this.conversation_id,
|
||||
scene: this.scene,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空历史
|
||||
*/
|
||||
async clearHistory() {
|
||||
return await this.messageManager.clearHistory({
|
||||
bot_id: this.bot_id,
|
||||
conversation_id: this.conversation_id,
|
||||
scene: this.scene,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除消息
|
||||
*/
|
||||
async deleteMessage(params: DeleteMessageParams) {
|
||||
return await this.messageManager.deleteMessage({
|
||||
bot_id: this.bot_id,
|
||||
conversation_id: this.conversation_id,
|
||||
scene: this.scene,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞/点踩消息
|
||||
*/
|
||||
async reportMessage(params: ReportMessageParams) {
|
||||
return await this.messageManager.reportMessage({
|
||||
bot_id: this.bot_id,
|
||||
biz_conversation_id: this.conversation_id,
|
||||
scene: this.scene,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打断消息
|
||||
*/
|
||||
async breakMessage(params: BreakMessageParams) {
|
||||
this.httpChunk.abort(params.local_message_id);
|
||||
const contentLength = this.chunkProcessor.getReplyMessagesLengthByReplyId(
|
||||
params.query_message_id,
|
||||
);
|
||||
this.reportEventsTracer?.pullStreamTracer.break(params.local_message_id, {
|
||||
contentLength,
|
||||
});
|
||||
this.reportLogWithScope.slardarEvent({
|
||||
eventName: SlardarEvents.SDK_BREAK_MESSAGE,
|
||||
meta: {
|
||||
...params,
|
||||
},
|
||||
});
|
||||
return await this.messageManager.breakMessage({
|
||||
conversation_id: this.conversation_id,
|
||||
scene: this.scene,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ASR 语音转文字
|
||||
*/
|
||||
async chatASR(params: ChatASRParams) {
|
||||
return await this.messageManager.chatASR(params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { exhaustiveCheckSimple } from '@coze-common/chat-area-utils';
|
||||
|
||||
import type { UploadPluginConstructor } from '@/plugins/upload-plugin/types/plugin-upload';
|
||||
|
||||
import type { PluginKey, PluginValue } from '../types/interface';
|
||||
|
||||
export class PluginsService {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any -- 暂时没想到合适的类型体操, 先用 any,
|
||||
UploadPlugin: UploadPluginConstructor<any> | null = null;
|
||||
uploadPluginConstructorOptions: Record<string, unknown> = {};
|
||||
|
||||
/**
|
||||
* 注册插件
|
||||
*/
|
||||
registerPlugin<T extends PluginKey, P extends Record<string, unknown>>(
|
||||
key: T,
|
||||
plugin: PluginValue<T, P>,
|
||||
constructorOptions?: P,
|
||||
) {
|
||||
if (key === 'upload-plugin') {
|
||||
this.UploadPlugin = plugin;
|
||||
this.uploadPluginConstructorOptions = constructorOptions || {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件是否已经注册过
|
||||
*/
|
||||
checkPluginIsRegistered(key: PluginKey): boolean {
|
||||
if (key === 'upload-plugin') {
|
||||
return !!this.UploadPlugin;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getRegisteredPlugin(key: PluginKey) {
|
||||
if (key === 'upload-plugin') {
|
||||
return this.UploadPlugin;
|
||||
}
|
||||
exhaustiveCheckSimple(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BETWEEN_CHUNK_TIMEOUT, SEND_MESSAGE_TIMEOUT } from '@/shared/const';
|
||||
import { RequestScene } from '@/request-manager/types';
|
||||
import { type ReportLog } from '@/report-log';
|
||||
import {
|
||||
PreSendLocalMessageEventsEnum,
|
||||
type SendMessage,
|
||||
type SendMessageMergedOptions,
|
||||
type SendMessageOptions,
|
||||
ContentType,
|
||||
type Message,
|
||||
} from '@/message/types';
|
||||
import { type PreSendLocalMessageEventsManager } from '@/message/presend-local-message/presend-local-message-events-manager';
|
||||
import {
|
||||
type PreSendLocalMessage,
|
||||
type PreSendLocalMessageFactory,
|
||||
} from '@/message';
|
||||
import { ChatCoreError } from '@/custom-error';
|
||||
import { type HttpChunk } from '@/channel/http-chunk';
|
||||
|
||||
import {
|
||||
type ReportEventsTracer,
|
||||
SlardarEvents,
|
||||
} from '../events/slardar-events';
|
||||
|
||||
export interface SendMessageServicesProps {
|
||||
preSendLocalMessageFactory: PreSendLocalMessageFactory;
|
||||
httpChunk: HttpChunk;
|
||||
preSendLocalMessageEventsManager: PreSendLocalMessageEventsManager;
|
||||
reportLogWithScope: ReportLog;
|
||||
reportEventsTracer: ReportEventsTracer;
|
||||
}
|
||||
export class SendMessageService {
|
||||
preSendLocalMessageFactory: PreSendLocalMessageFactory;
|
||||
httpChunk: HttpChunk;
|
||||
preSendLocalMessageEventsManager: PreSendLocalMessageEventsManager;
|
||||
reportLogWithScope: ReportLog;
|
||||
reportEventsTracer: ReportEventsTracer;
|
||||
constructor({
|
||||
preSendLocalMessageFactory,
|
||||
httpChunk,
|
||||
preSendLocalMessageEventsManager,
|
||||
reportLogWithScope,
|
||||
reportEventsTracer,
|
||||
}: SendMessageServicesProps) {
|
||||
this.preSendLocalMessageFactory = preSendLocalMessageFactory;
|
||||
this.httpChunk = httpChunk;
|
||||
this.preSendLocalMessageEventsManager = preSendLocalMessageEventsManager;
|
||||
this.reportLogWithScope = reportLogWithScope;
|
||||
this.reportEventsTracer = reportEventsTracer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送resume消息
|
||||
*/
|
||||
resumeMessage(message: Message<ContentType>, options?: SendMessageOptions) {
|
||||
const mergedOptions: SendMessageMergedOptions = {
|
||||
sendTimeout: SEND_MESSAGE_TIMEOUT,
|
||||
betweenChunkTimeout: BETWEEN_CHUNK_TIMEOUT,
|
||||
stream: true,
|
||||
chatHistory: [],
|
||||
isRegenMessage: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
const exposedMessage =
|
||||
this.preSendLocalMessageFactory.getSendMessageStructure(
|
||||
message,
|
||||
mergedOptions,
|
||||
);
|
||||
|
||||
this.httpChunk.sendMessage(exposedMessage, {
|
||||
betweenChunkTimeout: options?.betweenChunkTimeout,
|
||||
headers: options?.headers,
|
||||
requestScene: RequestScene.ResumeMessage,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async sendMessage(
|
||||
message: Message<ContentType>,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<Message<ContentType>> {
|
||||
const mergedOptions: SendMessageMergedOptions = {
|
||||
sendTimeout: SEND_MESSAGE_TIMEOUT,
|
||||
betweenChunkTimeout: BETWEEN_CHUNK_TIMEOUT,
|
||||
stream: true,
|
||||
chatHistory: [],
|
||||
isRegenMessage: false,
|
||||
...options,
|
||||
};
|
||||
this.reportLogWithScope.info({
|
||||
message: '开始发送消息',
|
||||
meta: {
|
||||
message,
|
||||
},
|
||||
});
|
||||
|
||||
if (message.content_type === ContentType.Image) {
|
||||
return await this.sendImageMessage(
|
||||
message as PreSendLocalMessage<ContentType.Image>,
|
||||
mergedOptions,
|
||||
);
|
||||
}
|
||||
|
||||
if (message.content_type === ContentType.File) {
|
||||
return await this.sendFileMessage(
|
||||
message as PreSendLocalMessage<ContentType.File>,
|
||||
mergedOptions,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.sendTextMessage(
|
||||
message as PreSendLocalMessage<ContentType.Text>,
|
||||
mergedOptions,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送图片消息
|
||||
*/
|
||||
private async sendImageMessage(
|
||||
message: PreSendLocalMessage<ContentType.Image>,
|
||||
options: SendMessageMergedOptions,
|
||||
) {
|
||||
const uploadMessage = await this.onUploadEventFinish(message, options);
|
||||
const exposedMessage =
|
||||
this.preSendLocalMessageFactory.getSendMessageStructure(
|
||||
uploadMessage,
|
||||
options,
|
||||
);
|
||||
return await this.sendChannelMessage(exposedMessage, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送文件消息
|
||||
* @param message
|
||||
* @param options
|
||||
* @private
|
||||
*/
|
||||
private async sendFileMessage(
|
||||
message: PreSendLocalMessage<ContentType.File>,
|
||||
options: SendMessageMergedOptions,
|
||||
) {
|
||||
const uploadMessage = await this.onUploadEventFinish(message, options);
|
||||
const exposedMessage =
|
||||
this.preSendLocalMessageFactory.getSendMessageStructure(
|
||||
uploadMessage,
|
||||
options,
|
||||
);
|
||||
return await this.sendChannelMessage(exposedMessage, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送文本消息
|
||||
*/
|
||||
private async sendTextMessage(
|
||||
message: PreSendLocalMessage<ContentType.Text>,
|
||||
options: SendMessageMergedOptions,
|
||||
) {
|
||||
const exposedMessage =
|
||||
this.preSendLocalMessageFactory.getSendMessageStructure(message, options);
|
||||
return await this.sendChannelMessage(exposedMessage, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片&文件上传事件完成
|
||||
*/
|
||||
private onUploadEventFinish<T extends ContentType.Image | ContentType.File>(
|
||||
message: PreSendLocalMessage<T>,
|
||||
sendMessageOptions?: SendMessageOptions,
|
||||
): Promise<PreSendLocalMessage<T>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 如果是重新生成消息,直接返回
|
||||
if (sendMessageOptions?.isRegenMessage) {
|
||||
resolve(message);
|
||||
return;
|
||||
}
|
||||
// 根据 message_id 查询是否已经上传完成
|
||||
const stashedLocalMessage =
|
||||
this.preSendLocalMessageEventsManager.getStashedLocalMessage(
|
||||
message.extra_info.local_message_id,
|
||||
) as PreSendLocalMessage<T>;
|
||||
if (stashedLocalMessage?.file_upload_result) {
|
||||
if (stashedLocalMessage?.file_upload_result === 'success') {
|
||||
// todo 改成直接 resolve message,stashed 不应存入全量 request,容易引起误会
|
||||
resolve(message);
|
||||
return;
|
||||
}
|
||||
this.reportLogWithScope.slardarEvent({
|
||||
eventName: SlardarEvents.SDK_MESSAGE_UPLOAD_FAIL,
|
||||
meta: {
|
||||
message: '图片上传失败',
|
||||
},
|
||||
});
|
||||
reject(new Error('图片上传失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 消息上传完成
|
||||
this.preSendLocalMessageEventsManager.on(
|
||||
PreSendLocalMessageEventsEnum.FILE_UPLOAD_STATUS_CHANGE,
|
||||
(preSendLocalMessage: Message<ContentType>) => {
|
||||
if (
|
||||
preSendLocalMessage.extra_info.local_message_id !==
|
||||
message.extra_info.local_message_id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (preSendLocalMessage.file_upload_result === 'success') {
|
||||
resolve(preSendLocalMessage as PreSendLocalMessage<T>);
|
||||
} else {
|
||||
this.reportLogWithScope.slardarEvent({
|
||||
eventName: SlardarEvents.SDK_MESSAGE_UPLOAD_FAIL,
|
||||
meta: {
|
||||
message: '图片上传失败-fail',
|
||||
},
|
||||
});
|
||||
reject(new Error('图片上传失败'));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* httpChunk 发送消息事件模式改为 await 模式
|
||||
* @param message 最终要发送给服务端的消息格式
|
||||
* @param options 发送消息配置
|
||||
*/
|
||||
private sendChannelMessage(
|
||||
message: SendMessage,
|
||||
options: SendMessageMergedOptions,
|
||||
): Promise<Message<ContentType>> {
|
||||
const { sendTimeout, betweenChunkTimeout, headers } = options;
|
||||
const { local_message_id } = message;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let isHandled = false;
|
||||
const timer = setTimeout(() => {
|
||||
if (isHandled) {
|
||||
return;
|
||||
}
|
||||
isHandled = true;
|
||||
this.preSendLocalMessageEventsManager.updateLocalMessageStatus(
|
||||
message.local_message_id,
|
||||
'send_timeout',
|
||||
);
|
||||
this.reportEventsTracer?.sendMessageTracer.timeout(local_message_id);
|
||||
this.preSendLocalMessageEventsManager.emit(
|
||||
PreSendLocalMessageEventsEnum.MESSAGE_SEND_TIMEOUT,
|
||||
new ChatCoreError('消息发送超时', {
|
||||
local_message_id: message.local_message_id,
|
||||
}),
|
||||
);
|
||||
reject(
|
||||
new ChatCoreError('消息发送超时', {
|
||||
local_message_id: message.local_message_id,
|
||||
}),
|
||||
);
|
||||
}, sendTimeout);
|
||||
this.reportEventsTracer?.sendMessageTracer.start(local_message_id);
|
||||
this.httpChunk.sendMessage(message, {
|
||||
betweenChunkTimeout,
|
||||
headers,
|
||||
requestScene: RequestScene.SendMessage,
|
||||
});
|
||||
// 监听消息发送成功
|
||||
this.preSendLocalMessageEventsManager.once(
|
||||
PreSendLocalMessageEventsEnum.MESSAGE_SEND_SUCCESS,
|
||||
(receiveMessage: Message<ContentType>) => {
|
||||
if (
|
||||
receiveMessage.extra_info.local_message_id !==
|
||||
message.local_message_id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (isHandled) {
|
||||
return;
|
||||
}
|
||||
isHandled = true;
|
||||
clearTimeout(timer);
|
||||
this.preSendLocalMessageEventsManager.updateLocalMessageStatus(
|
||||
receiveMessage.extra_info.local_message_id,
|
||||
'send_success',
|
||||
);
|
||||
this.reportEventsTracer?.sendMessageTracer.success(local_message_id, {
|
||||
logId: receiveMessage.logId,
|
||||
});
|
||||
resolve(receiveMessage);
|
||||
},
|
||||
);
|
||||
// 监听消息发送失败
|
||||
this.preSendLocalMessageEventsManager.once(
|
||||
PreSendLocalMessageEventsEnum.MESSAGE_SEND_FAIL,
|
||||
(error: ChatCoreError) => {
|
||||
if (error.ext.local_message_id !== message.local_message_id) {
|
||||
return;
|
||||
}
|
||||
if (isHandled) {
|
||||
return;
|
||||
}
|
||||
isHandled = true;
|
||||
clearTimeout(timer);
|
||||
this.preSendLocalMessageEventsManager.updateLocalMessageStatus(
|
||||
error.ext.local_message_id,
|
||||
'send_fail',
|
||||
);
|
||||
this.reportEventsTracer?.sendMessageTracer.error(error);
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SdkEventsEnum } from '../events/sdk-events';
|
||||
import { type DeployVersion, type ENV } from '../../shared/const';
|
||||
import { type RequestManagerOptions } from '../../request-manager/types';
|
||||
import { type UploadPluginConstructor } from '../../plugins/upload-plugin/types/plugin-upload';
|
||||
import type { Message, ImageMessageProps } from '../../message/types';
|
||||
export type {
|
||||
GetHistoryMessageProps,
|
||||
ClearHistoryProps,
|
||||
DeleteMessageProps,
|
||||
BreakMessageProps,
|
||||
} from '../../message/types/message-manager';
|
||||
import { ContentType } from '../../message/types';
|
||||
import { type ChatCoreError } from '../../custom-error';
|
||||
import type { TokenManager } from '../../credential';
|
||||
|
||||
export { Message, ContentType, ImageMessageProps };
|
||||
|
||||
export { SdkEventsEnum };
|
||||
|
||||
export type BotUnique =
|
||||
| {
|
||||
bot_id: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* 使用的bot模版 代替bot_id bot_version draft_mode参数
|
||||
* 非安全考虑,仅不想暴露bot_id的情况下使用
|
||||
* botId、presetBot必传其一
|
||||
*/
|
||||
preset_bot: PresetBot;
|
||||
};
|
||||
|
||||
export const isPresetBotUnique = <T extends BotUnique>(
|
||||
sth: T,
|
||||
): sth is { preset_bot: PresetBot } & Exclude<T, 'bot_id'> =>
|
||||
'preset_bot' in sth && !!sth.preset_bot;
|
||||
|
||||
export type CreateProps = BotUnique & {
|
||||
/**
|
||||
* 用于计算资源点消耗
|
||||
*/
|
||||
space_id?: string;
|
||||
/**
|
||||
* 业务方标识,用于埋点记录
|
||||
*/
|
||||
biz: Biz;
|
||||
/**
|
||||
* bot 版本号
|
||||
*/
|
||||
bot_version?: string;
|
||||
|
||||
/**
|
||||
* 草稿bot or 线上bot,
|
||||
*/
|
||||
draft_mode?: boolean;
|
||||
|
||||
/**
|
||||
* 会话 id
|
||||
*/
|
||||
conversation_id: string;
|
||||
|
||||
/**
|
||||
* 指定发送的唯一用户
|
||||
*/
|
||||
user?: string;
|
||||
|
||||
/**
|
||||
* 场景值,主要用于服务端鉴权, 默认 0 default
|
||||
*/
|
||||
scene?: Scene;
|
||||
|
||||
/**
|
||||
* 环境变量,区分测试环境和线上环境
|
||||
* 用于日志上报
|
||||
*/
|
||||
env: ENV;
|
||||
|
||||
/**
|
||||
* 区分部署版本
|
||||
*/
|
||||
|
||||
deployVersion: DeployVersion;
|
||||
|
||||
/**
|
||||
* 是否开启 debug模式,目前主要给 bot editor 使用
|
||||
* 开启后,每条回复消息新增debug_messages字段,包含channel 吐出的所有 chunk消息
|
||||
**/
|
||||
enableDebug?: boolean;
|
||||
/**
|
||||
* sdk 控制台日志等级,默认error, 后面层级会包含前面层级
|
||||
**/
|
||||
logLevel?: LogLevel;
|
||||
/**
|
||||
接口拦截器
|
||||
**/
|
||||
requestManagerOptions?: RequestManagerOptions;
|
||||
|
||||
/**
|
||||
* token 刷新机制
|
||||
*/
|
||||
tokenManager?: TokenManager;
|
||||
};
|
||||
|
||||
export type LogLevel = 'disable' | 'info' | 'error';
|
||||
|
||||
export interface SdkMessageEvent {
|
||||
name: SdkEventsEnum;
|
||||
data: Message<ContentType>[];
|
||||
}
|
||||
|
||||
export interface SdkErrorEvent {
|
||||
name: SdkEventsEnum;
|
||||
data: {
|
||||
error: Error;
|
||||
};
|
||||
}
|
||||
export type PullingStatus =
|
||||
| 'start'
|
||||
| 'pulling'
|
||||
| 'answerEnd'
|
||||
| 'success'
|
||||
| 'error'
|
||||
| 'timeout';
|
||||
|
||||
export interface SdkPullingStatusEvent {
|
||||
name: SdkEventsEnum;
|
||||
data: {
|
||||
/**
|
||||
* 拉取回复消息状态
|
||||
*/
|
||||
pullingStatus: PullingStatus;
|
||||
/**
|
||||
* query 的本地 id
|
||||
*/
|
||||
local_message_id: string;
|
||||
/**
|
||||
* query 的服务端 message_id
|
||||
*/
|
||||
reply_id: string;
|
||||
};
|
||||
|
||||
error?: ChatCoreError;
|
||||
|
||||
/**
|
||||
* timeout 状态下返回,用于终止拉取
|
||||
* @returns
|
||||
*/
|
||||
abort?: () => void;
|
||||
}
|
||||
|
||||
export interface SdkEventsCallbackMap {
|
||||
[SdkEventsEnum.MESSAGE_RECEIVED_AND_UPDATE]: (event: SdkMessageEvent) => void;
|
||||
[SdkEventsEnum.ERROR]: (event: SdkErrorEvent) => void;
|
||||
[SdkEventsEnum.MESSAGE_PULLING_STATUS]: (
|
||||
event: SdkPullingStatusEvent,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type PluginKey = 'upload-plugin';
|
||||
|
||||
export type PluginValue<
|
||||
T,
|
||||
P extends Record<string, unknown>,
|
||||
> = T extends 'upload-plugin' ? UploadPluginConstructor<P> : never;
|
||||
|
||||
/**
|
||||
* 接入的业务方向
|
||||
* 目前采用枚举接入,控制接入方向
|
||||
* third_part给open_api sdk使用,暴露给第三方
|
||||
*/
|
||||
export type Biz = 'coze_home' | 'bot_editor' | 'third_part';
|
||||
|
||||
export type PresetBot = 'coze_home' | 'prompt_optimize' | '';
|
||||
|
||||
/**
|
||||
* 接口也有这个定义 enum Scene.
|
||||
* 注意前端定义与接口完全对齐:
|
||||
* src/auto-generate/developer_api/namespaces/developer_api.ts
|
||||
*/
|
||||
export const enum Scene {
|
||||
Default = 0,
|
||||
Explore = 1,
|
||||
BotStore = 2,
|
||||
CozeHome = 3,
|
||||
// 调试区 命名和服务端对齐的
|
||||
Playground = 4,
|
||||
AgentAPP = 6,
|
||||
PromptOptimize = 7,
|
||||
/**
|
||||
* TODO: 前端单独增加,需要和后端对齐固定一个枚举
|
||||
*/
|
||||
OpenAipSdk = 1000,
|
||||
}
|
||||
|
||||
/**
|
||||
* 对齐 developer_api/namespaces/developer_api.ts
|
||||
*/
|
||||
export const enum LoadDirection {
|
||||
Unknown = 0,
|
||||
Prev = 1,
|
||||
Next = 2,
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ChatASRProps,
|
||||
type BreakMessageProps,
|
||||
type DeleteMessageProps,
|
||||
type GetHistoryMessageProps,
|
||||
type ReportMessageProps,
|
||||
} from '../../../message/types/message-manager';
|
||||
|
||||
export type GetHistoryMessageParams = Omit<
|
||||
GetHistoryMessageProps,
|
||||
'conversation_id' | 'scene' | 'bot_id' | 'preset_bot' | 'draft_mode'
|
||||
>;
|
||||
|
||||
export type DeleteMessageParams = Omit<
|
||||
DeleteMessageProps,
|
||||
'conversation_id' | 'bot_id'
|
||||
>;
|
||||
|
||||
export type ReportMessageParams = Omit<
|
||||
ReportMessageProps,
|
||||
'biz_conversation_id' | 'bot_id' | 'scene'
|
||||
>;
|
||||
|
||||
export type BreakMessageParams = Omit<BreakMessageProps, 'conversation_id'>;
|
||||
|
||||
export type ChatASRParams = ChatASRProps;
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import TokenManager from './token-manager';
|
||||
|
||||
export { TokenManager };
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 负责 token 验权,自动刷新 token
|
||||
* 暴露给业务方,由业务方决定是否单例还是多例鉴权
|
||||
*/
|
||||
export interface TokenManagerProps {
|
||||
token?: string;
|
||||
// Authorization: Bearer {sdk_verify_token}
|
||||
apiKey?: string;
|
||||
tokenRefresher?: () => Promise<string>;
|
||||
}
|
||||
export default class TokenManager {
|
||||
private token?: string;
|
||||
|
||||
private apiKey?: string;
|
||||
|
||||
private tokenRefresher?: () => Promise<string>;
|
||||
|
||||
constructor(props: TokenManagerProps) {
|
||||
const { token, tokenRefresher, apiKey } = props;
|
||||
this.token = token;
|
||||
this.apiKey = apiKey;
|
||||
this.tokenRefresher = tokenRefresher;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 token
|
||||
*/
|
||||
getToken() {
|
||||
// TODO: 没有 token,获取最新 token
|
||||
return this.token;
|
||||
}
|
||||
updateToken(token: string) {
|
||||
this.token = token;
|
||||
}
|
||||
updateApiKey(apiKey: string) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 apiKey
|
||||
*/
|
||||
getApiKey() {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 apiKey 组装成的 Authorization 值
|
||||
*/
|
||||
getApiKeyAuthorizationValue() {
|
||||
return `Bearer ${this?.getApiKey()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 apiKey
|
||||
*/
|
||||
refreshApiKey(apiKey: string) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
// TODO: 补充刷新机制
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface ExtErrorInfo {
|
||||
code?: number;
|
||||
local_message_id?: string;
|
||||
reply_id?: string;
|
||||
logId?: string;
|
||||
rawError?: unknown;
|
||||
}
|
||||
export class ChatCoreError extends Error {
|
||||
ext: ExtErrorInfo;
|
||||
constructor(message: string, ext?: ExtErrorInfo) {
|
||||
super(message);
|
||||
this.name = 'chatCoreError';
|
||||
this.ext = ext || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 扁平化错误信息,方便在slardar中筛选错误信息
|
||||
*/
|
||||
flatten = () => {
|
||||
const { message, ext } = this;
|
||||
return {
|
||||
message,
|
||||
...ext,
|
||||
};
|
||||
};
|
||||
}
|
||||
17
frontend/packages/common/chat-area/chat-core/src/global.d.ts
vendored
Normal file
17
frontend/packages/common/chat-area/chat-core/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
116
frontend/packages/common/chat-area/chat-core/src/index.ts
Normal file
116
frontend/packages/common/chat-area/chat-core/src/index.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ChatCore from './chat-sdk';
|
||||
export { TokenManager } from './credential';
|
||||
export {
|
||||
UploadPluginConstructor,
|
||||
UploadEventName,
|
||||
UploadResult,
|
||||
BaseEventInfo,
|
||||
CompleteEventInfo,
|
||||
ProgressEventInfo,
|
||||
EventPayloadMaps,
|
||||
UploadPluginInterface,
|
||||
} from './plugins/upload-plugin/types/plugin-upload';
|
||||
export {
|
||||
type MsgParticipantType,
|
||||
type ParticipantInfo,
|
||||
GetHistoryMessageResponse,
|
||||
} from './message/types/message-manager';
|
||||
export type {
|
||||
CreateProps as CreateChatCoreProps,
|
||||
SdkMessageEvent,
|
||||
SdkPullingStatusEvent,
|
||||
SdkErrorEvent,
|
||||
} from './chat-sdk/types/interface';
|
||||
export { SdkEventsEnum } from './chat-sdk/types/interface';
|
||||
|
||||
export default ChatCore;
|
||||
export { ChatCore };
|
||||
|
||||
export {
|
||||
Message,
|
||||
ContentType,
|
||||
VerboseContent,
|
||||
VerboseMsgType,
|
||||
AnswerFinishVerboseData,
|
||||
FinishReasonType,
|
||||
type MessageContent,
|
||||
type TextMixItem,
|
||||
type TextAndFileMixMessagePropsFilePayload,
|
||||
type TextAndFileMixMessagePropsImagePayload,
|
||||
type ImageModel,
|
||||
type ImageMixItem,
|
||||
type FileModel,
|
||||
type FileMixItem,
|
||||
messageSource,
|
||||
type MessageSource,
|
||||
type SendMessageOptions,
|
||||
type NormalizedMessageProps,
|
||||
type NormalizedMessagePropsPayload,
|
||||
type MessageMentionListFields,
|
||||
type TextAndFileMixMessageProps,
|
||||
type TextMessageProps,
|
||||
taskType,
|
||||
ChatMessageMetaType,
|
||||
type ChatMessageMetaInfo,
|
||||
type InterruptToolCallsType,
|
||||
} from './message/types';
|
||||
export { ChatCoreError } from './custom-error';
|
||||
export {
|
||||
MessageFeedbackDetailType,
|
||||
MessageFeedbackType,
|
||||
ReportMessageAction,
|
||||
type ReportMessageProps,
|
||||
type ClearMessageContextParams,
|
||||
type ClearMessageContextProps,
|
||||
} from './message/types/message-manager';
|
||||
|
||||
export { ChatCoreUploadPlugin } from './plugins/upload-plugin';
|
||||
export {
|
||||
RequestScene,
|
||||
type RequestManagerOptions,
|
||||
type SceneConfig,
|
||||
} from './request-manager/types';
|
||||
|
||||
export { ApiError } from './request-manager/api-error';
|
||||
|
||||
export {
|
||||
Scene,
|
||||
CreateProps,
|
||||
PresetBot,
|
||||
LoadDirection,
|
||||
PluginKey,
|
||||
} from './chat-sdk/types/interface';
|
||||
export { getFileInfo } from './shared/const';
|
||||
export { FILE_TYPE_CONFIG, FileTypeEnum } from './shared/const';
|
||||
|
||||
export { type FileType } from './plugins/upload-plugin/types/plugin-upload';
|
||||
|
||||
export { getSlardarEnv } from './shared/utils/env';
|
||||
|
||||
export {
|
||||
type ImageMessageContent,
|
||||
type FileMessageContent,
|
||||
type MixMessageContent,
|
||||
} from './message/types';
|
||||
export { TFileTypeConfig } from './shared/const';
|
||||
|
||||
export { MessageType } from './message/types';
|
||||
|
||||
export { Biz } from './chat-sdk/types/interface';
|
||||
export { ParsedEvent } from './channel/http-chunk/types';
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 处理接收到的Chunk消息
|
||||
* 1、预处理:反序列化
|
||||
* 2、增量消息拼接
|
||||
*/
|
||||
import { cloneDeep, flow } from 'lodash-es';
|
||||
|
||||
import { safeJSONParse } from '../shared/utils/safe-json-parse';
|
||||
import {
|
||||
type Message,
|
||||
ContentType,
|
||||
type ChunkRaw,
|
||||
type MessageContent,
|
||||
type VerboseContent,
|
||||
VerboseMsgType,
|
||||
type AnswerFinishVerboseData,
|
||||
FinishReasonType,
|
||||
} from './types';
|
||||
|
||||
export class StreamBufferHelper {
|
||||
// 一次流式拉取的message消息缓存
|
||||
streamMessageBuffer: Message<ContentType>[] = [];
|
||||
|
||||
// 一次流式拉取的Chunk消息缓存
|
||||
streamChunkBuffer: ChunkRaw[] = [];
|
||||
|
||||
/**
|
||||
* 新增Chunk消息缓存
|
||||
*/
|
||||
pushChunk(chunk: ChunkRaw) {
|
||||
this.streamChunkBuffer.push(chunk);
|
||||
}
|
||||
|
||||
concatContentAndUpdateMessage(message: Message<ContentType>) {
|
||||
const previousIndex = this.streamMessageBuffer.findIndex(
|
||||
item => item.message_id === message.message_id,
|
||||
);
|
||||
// 新增
|
||||
if (previousIndex === -1) {
|
||||
this.streamMessageBuffer.push(message);
|
||||
return;
|
||||
}
|
||||
// 更新
|
||||
const previousMessage = this.streamMessageBuffer.at(previousIndex);
|
||||
message.content = (previousMessage?.content || '') + message.content;
|
||||
message.reasoning_content =
|
||||
(previousMessage?.reasoning_content ?? '') +
|
||||
(message.reasoning_content ?? '');
|
||||
|
||||
message.content_obj = message.content;
|
||||
this.streamMessageBuffer.splice(previousIndex, 1, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空消息缓存
|
||||
*/
|
||||
clearMessageBuffer() {
|
||||
this.streamMessageBuffer = [];
|
||||
this.streamChunkBuffer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按reply_id清除相关消息缓存
|
||||
* 1、reply_id相等的回复
|
||||
* 2、reply_id为 message_id 的问题
|
||||
*/
|
||||
clearMessageBufferByReplyId(reply_id: string) {
|
||||
this.streamMessageBuffer = this.streamMessageBuffer.filter(
|
||||
message =>
|
||||
message.reply_id !== reply_id && message.message_id !== reply_id,
|
||||
);
|
||||
this.streamChunkBuffer = this.streamChunkBuffer.filter(
|
||||
chunk =>
|
||||
chunk.message.reply_id !== reply_id &&
|
||||
chunk.message.message_id !== reply_id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据message_id获取chunk buffer中的chunk
|
||||
*/
|
||||
getChunkByMessageId(message_id: string) {
|
||||
return this.streamChunkBuffer.filter(
|
||||
chunk => chunk.message.message_id === message_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface AddChunkAndProcessOptions {
|
||||
logId?: string;
|
||||
}
|
||||
export class ChunkProcessor {
|
||||
streamBuffer: StreamBufferHelper = new StreamBufferHelper();
|
||||
|
||||
bot_id?: string;
|
||||
|
||||
preset_bot?: string;
|
||||
|
||||
enableDebug?: boolean;
|
||||
|
||||
constructor(props: {
|
||||
bot_id?: string;
|
||||
preset_bot?: string;
|
||||
enableDebug?: boolean;
|
||||
}) {
|
||||
const { bot_id, preset_bot, enableDebug } = props;
|
||||
this.bot_id = bot_id;
|
||||
this.preset_bot = preset_bot;
|
||||
this.enableDebug = enableDebug;
|
||||
}
|
||||
/**
|
||||
* 新增chunk, 统一处理后的Chunk消息
|
||||
*/
|
||||
addChunkAndProcess(chunk: ChunkRaw, options?: AddChunkAndProcessOptions) {
|
||||
this.streamBuffer.pushChunk(chunk);
|
||||
flow(
|
||||
this.preProcessChunk.bind(this),
|
||||
this.concatChunkMessage.bind(this),
|
||||
this.assembleDebugMessage.bind(this),
|
||||
)(chunk, options) as Message<ContentType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据chunk获取处理后的消息
|
||||
*/
|
||||
getProcessedMessageByChunk(chunk: ChunkRaw) {
|
||||
return this.streamBuffer.streamMessageBuffer.find(
|
||||
message => message.message_id === chunk.message.message_id,
|
||||
) as Message<ContentType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据message_id获取处理后的消息
|
||||
*/
|
||||
getProcessedMessageByMessageId(message_id: string) {
|
||||
return this.streamBuffer.streamMessageBuffer.find(
|
||||
message => message.message_id === message_id,
|
||||
) as Message<ContentType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据local_message_id获取接收到的ack消息
|
||||
*/
|
||||
getAckMessageByLocalMessageId(local_message_id: string) {
|
||||
return this.streamBuffer.streamMessageBuffer.find(
|
||||
message =>
|
||||
message.extra_info.local_message_id === local_message_id &&
|
||||
message.type === 'ack',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据chunk获取到第一条回复
|
||||
*/
|
||||
getFirstReplyMessageByChunk(chunk: ChunkRaw) {
|
||||
const hasAck = this.streamBuffer.streamMessageBuffer.find(
|
||||
item => item.type === 'ack' && item.message_id === chunk.message.reply_id,
|
||||
);
|
||||
if (!hasAck) {
|
||||
return undefined;
|
||||
}
|
||||
return this.streamBuffer.streamMessageBuffer.find(
|
||||
item => item.type !== 'ack' && item.reply_id === chunk.message.reply_id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据chunk获取到ack
|
||||
*/
|
||||
getAckMessageByChunk(chunk: ChunkRaw) {
|
||||
return this.streamBuffer.streamMessageBuffer.find(
|
||||
item => item.type === 'ack' && item.message_id === chunk.message.reply_id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是第一条回复消息
|
||||
* 除ack外的第一条回复
|
||||
*/
|
||||
isFirstReplyMessage(chunk: ChunkRaw) {
|
||||
// 还没有ack,肯定没有第一条回复
|
||||
if (!this.getAckMessageByChunk(chunk)) {
|
||||
return false;
|
||||
}
|
||||
return !this.getFirstReplyMessageByChunk(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据reply_id获取所有回复消息
|
||||
*/
|
||||
getReplyMessagesByReplyId(reply_id: string) {
|
||||
return this.streamBuffer.streamMessageBuffer.filter(
|
||||
message => message.type !== 'ack' && message.reply_id === reply_id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有回复消息的长度
|
||||
*/
|
||||
getReplyMessagesLengthByReplyId(reply_id: string) {
|
||||
return `${this.getReplyMessagesByReplyId(reply_id).reduce(
|
||||
(acc, message) => acc + message.content.length,
|
||||
0,
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给本地日志使用
|
||||
* @param message
|
||||
* @returns
|
||||
*/
|
||||
appendDebugMessage(message: Message<ContentType>) {
|
||||
const cloneMessage = cloneDeep(message);
|
||||
cloneMessage.debug_messages = this.streamBuffer.getChunkByMessageId(
|
||||
message.message_id,
|
||||
);
|
||||
cloneMessage.stream_chunk_buffer = this.streamBuffer.streamChunkBuffer;
|
||||
cloneMessage.stream_message_buffer = this.streamBuffer.streamMessageBuffer;
|
||||
return cloneMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是否是final answer
|
||||
*/
|
||||
isMessageAnswerEnd(chunk: ChunkRaw): boolean {
|
||||
const { message } = chunk;
|
||||
// 找到对应的所有回复
|
||||
const replyMessages = this.getReplyMessagesByReplyId(message.reply_id);
|
||||
// 查找是否有verbose消息, 并且标识answer结束,并且过滤掉中断场景的finish
|
||||
const finalAnswerVerboseMessage = replyMessages.find(replyMessage => {
|
||||
const { type, content } = replyMessage;
|
||||
if (type !== 'verbose') {
|
||||
return false;
|
||||
}
|
||||
const { value: verboseContent } = safeJSONParse<VerboseContent>(
|
||||
content,
|
||||
null,
|
||||
);
|
||||
if (!verboseContent) {
|
||||
return false;
|
||||
}
|
||||
const { value: verboseContentData } =
|
||||
safeJSONParse<AnswerFinishVerboseData>(verboseContent.data, null);
|
||||
|
||||
// 目前一个group内可能会有finish包,需要通过finish_reason过滤掉中断场景的,拿到的就是回答全部结束的finish
|
||||
return (
|
||||
verboseContent.msg_type === VerboseMsgType.GENERATE_ANSWER_FINISH &&
|
||||
verboseContentData?.finish_reason !== FinishReasonType.INTERRUPT
|
||||
);
|
||||
});
|
||||
return Boolean(finalAnswerVerboseMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预处理消息
|
||||
* 1、反序列化
|
||||
* 2、添加 bot_id、is_finish、index, logId
|
||||
* @param chunk
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
private preProcessChunk(
|
||||
chunk: ChunkRaw,
|
||||
options?: AddChunkAndProcessOptions,
|
||||
): Message<ContentType> {
|
||||
const { message, is_finish, index } = chunk;
|
||||
const { logId } = options || {};
|
||||
|
||||
return {
|
||||
mention_list: [],
|
||||
...message,
|
||||
logId,
|
||||
bot_id: this.bot_id,
|
||||
preset_bot: this.preset_bot,
|
||||
is_finish,
|
||||
index,
|
||||
content_obj:
|
||||
message.content_type !== ContentType.Text
|
||||
? safeJSONParse<MessageContent<ContentType>>(message.content, null)
|
||||
.value
|
||||
: message.content,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量消息拼接
|
||||
* 1、对于增量消息,需要拼接上一次的消息
|
||||
*/
|
||||
private concatChunkMessage(
|
||||
message: Message<ContentType>,
|
||||
): Message<ContentType> {
|
||||
this.streamBuffer.concatContentAndUpdateMessage(message);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
// debug_message 逻辑
|
||||
private assembleDebugMessage(
|
||||
message: Message<ContentType>,
|
||||
): Message<ContentType> {
|
||||
if (!this.enableDebug) {
|
||||
return message;
|
||||
}
|
||||
// 一次 stream拉取的所有message_id一样的 chunk 消息一次返回
|
||||
message.debug_messages = this.streamBuffer.getChunkByMessageId(
|
||||
message.message_id,
|
||||
);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 承接所有 sdk 的 slardar 自定义事件
|
||||
*/
|
||||
export enum SlardarEvents {
|
||||
// 拉取历史异常
|
||||
MESSAGE_FETCH_HISTORY_ERROR = 'message_fetch_history_error',
|
||||
// 清空上下文异常
|
||||
MESSAGE_CLEAR_CONTEXT_ERROR = 'message_clear_context_error',
|
||||
// 清空历史异常
|
||||
MESSAGE_CLEAR_HISTORY_ERROR = 'message_clear_history_error',
|
||||
// 删除消息异常
|
||||
MESSAGE_DELETE_ERROR = 'message_delete_error',
|
||||
// 打断消息
|
||||
MESSAGE_INTERRUPT_ERROR = 'message_interrupt_error',
|
||||
// 点赞/点踩消息
|
||||
MESSAGE_REPORT_ERROR = 'message_report_error',
|
||||
// 语音转文字
|
||||
CHAT_ASR_ERROR = 'chat_asr_error',
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. 负责规范各种类型消息创建的入参出参,减少消息创建成本
|
||||
* 2. 对于接收到的消息,针对不同消息类型,吐出指定的消息格式
|
||||
*/
|
||||
|
||||
export { PreSendLocalMessageFactory } from './presend-local-message/presend-local-message-factory';
|
||||
|
||||
export { ChunkProcessor } from './chunk-processor';
|
||||
|
||||
export { PreSendLocalMessage } from './presend-local-message/presend-local-message';
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 消息管理相关
|
||||
* 1、获取历史消息
|
||||
* 2、清空对话上下文
|
||||
* 3、清空历史
|
||||
* 4、删除消息
|
||||
*/
|
||||
|
||||
import { safeJSONParse } from '../shared/utils/safe-json-parse';
|
||||
import { RequestScene } from '../request-manager/types';
|
||||
import { type RequestManager } from '../request-manager';
|
||||
import type { ReportLog } from '../report-log';
|
||||
import {
|
||||
type ReportMessageProps,
|
||||
type BreakMessageProps,
|
||||
type BreakMessageResponse,
|
||||
type ClearHistoryProps,
|
||||
type ClearHistoryResponse,
|
||||
type ClearMessageContextProps,
|
||||
type DeleteMessageProps,
|
||||
type DeleteMessageResponse,
|
||||
type GetHistoryMessageProps,
|
||||
type GetHistoryMessageResponse,
|
||||
type ReportMessageResponse,
|
||||
type ClearMessageContextResponse,
|
||||
type ChatASRProps,
|
||||
type ChatASRResponse,
|
||||
} from './types/message-manager';
|
||||
import { ContentType, type MessageContent } from './types';
|
||||
import { SlardarEvents } from './events/slardar-events';
|
||||
|
||||
export interface MessageManagerProps {
|
||||
reportLog: ReportLog;
|
||||
requestManager: RequestManager;
|
||||
}
|
||||
|
||||
export class MessageManager {
|
||||
reportLog: ReportLog;
|
||||
|
||||
reportLogWithScope: ReportLog;
|
||||
|
||||
requestManager: RequestManager;
|
||||
|
||||
request: RequestManager['request'];
|
||||
|
||||
constructor(props: MessageManagerProps) {
|
||||
const { reportLog, requestManager } = props;
|
||||
this.reportLog = reportLog;
|
||||
this.requestManager = requestManager;
|
||||
this.request = requestManager.request;
|
||||
this.reportLogWithScope = this.reportLog.createLoggerWith({
|
||||
scope: 'message',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将接口获取到的消息历史记录进行数据转换
|
||||
*/
|
||||
static convertMessageList = (
|
||||
messageList: GetHistoryMessageResponse['message_list'],
|
||||
) => {
|
||||
messageList.forEach(message => {
|
||||
message.content_obj =
|
||||
message.content_type === ContentType.Text
|
||||
? undefined
|
||||
: safeJSONParse<MessageContent<ContentType>>(message.content, null)
|
||||
.value;
|
||||
});
|
||||
return messageList;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取历史消息
|
||||
*/
|
||||
async getHistoryMessage(props: GetHistoryMessageProps) {
|
||||
try {
|
||||
const res = await this.request.post(
|
||||
this.requestManager.getSceneConfig(RequestScene.GetMessage).url,
|
||||
props,
|
||||
);
|
||||
const data = res.data as GetHistoryMessageResponse;
|
||||
data.message_list = MessageManager.convertMessageList(data.message_list);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.reportLogWithScope.slardarErrorEvent({
|
||||
eventName: SlardarEvents.MESSAGE_FETCH_HISTORY_ERROR,
|
||||
error: error as Error,
|
||||
});
|
||||
// 此处不应省略异常抛出,上游逻辑分支已检查,无风险
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空对话上下文
|
||||
*/
|
||||
async clearMessageContextUrl(props: ClearMessageContextProps) {
|
||||
try {
|
||||
const res = await this.request.post(
|
||||
this.requestManager.getSceneConfig(RequestScene.ClearMessageContext)
|
||||
.url,
|
||||
props,
|
||||
);
|
||||
return res.data as ClearMessageContextResponse;
|
||||
} catch (error) {
|
||||
this.reportLogWithScope.slardarErrorEvent({
|
||||
eventName: SlardarEvents.MESSAGE_CLEAR_CONTEXT_ERROR,
|
||||
error: error as Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空历史
|
||||
*/
|
||||
async clearHistory(props: ClearHistoryProps) {
|
||||
try {
|
||||
const res = await this.request.post(
|
||||
this.requestManager.getSceneConfig(RequestScene.ClearHistory).url,
|
||||
props,
|
||||
);
|
||||
return res.data as ClearHistoryResponse;
|
||||
} catch (error) {
|
||||
this.reportLogWithScope.slardarErrorEvent({
|
||||
eventName: SlardarEvents.MESSAGE_CLEAR_HISTORY_ERROR,
|
||||
error: error as Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除消息
|
||||
*/
|
||||
async deleteMessage(props: DeleteMessageProps) {
|
||||
try {
|
||||
const res = await this.request.post(
|
||||
this.requestManager.getSceneConfig(RequestScene.DeleteMessage).url,
|
||||
props,
|
||||
);
|
||||
return res.data as DeleteMessageResponse;
|
||||
} catch (error) {
|
||||
this.reportLogWithScope.slardarErrorEvent({
|
||||
eventName: SlardarEvents.MESSAGE_DELETE_ERROR,
|
||||
error: error as Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打断消息
|
||||
*/
|
||||
async breakMessage(props: BreakMessageProps) {
|
||||
try {
|
||||
const res = await this.request.post(
|
||||
this.requestManager.getSceneConfig(RequestScene.BreakMessage).url,
|
||||
props,
|
||||
);
|
||||
return res.data as BreakMessageResponse;
|
||||
} catch (error) {
|
||||
this.reportLogWithScope.slardarErrorEvent({
|
||||
eventName: SlardarEvents.MESSAGE_INTERRUPT_ERROR,
|
||||
error: error as Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞/点踩消息
|
||||
*/
|
||||
async reportMessage(props: ReportMessageProps) {
|
||||
try {
|
||||
const res = await this.request.post(
|
||||
this.requestManager.getSceneConfig(RequestScene.ReportMessage).url,
|
||||
props,
|
||||
);
|
||||
return res.data as ReportMessageResponse;
|
||||
} catch (error) {
|
||||
this.reportLogWithScope.slardarErrorEvent({
|
||||
eventName: SlardarEvents.MESSAGE_REPORT_ERROR,
|
||||
error: error as Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 语音转文字
|
||||
*/
|
||||
async chatASR(props: ChatASRProps) {
|
||||
try {
|
||||
const res = await this.request.post(
|
||||
this.requestManager.getSceneConfig(RequestScene.ChatASR).url,
|
||||
props,
|
||||
{
|
||||
headers: {
|
||||
/**
|
||||
* https://developer.mozilla.org/zh-CN/docs/Web/API/FormData
|
||||
* 如果送出时的编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。
|
||||
*/
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
},
|
||||
);
|
||||
return res.data as ChatASRResponse;
|
||||
} catch (error) {
|
||||
this.reportLogWithScope.slardarErrorEvent({
|
||||
eventName: SlardarEvents.CHAT_ASR_ERROR,
|
||||
error: error as Error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
import {
|
||||
type PreSendLocalMessageEventsMap,
|
||||
type ContentType,
|
||||
type Message,
|
||||
PreSendLocalMessageEventsEnum,
|
||||
type LocalMessageStatus,
|
||||
} from '../types';
|
||||
import { type ReportLog } from '../../report-log';
|
||||
import { type ChatCoreError } from '../../custom-error';
|
||||
import { type PreSendLocalMessage } from './presend-local-message';
|
||||
|
||||
export interface PreSendLocalMessageEventsManagerProps {
|
||||
reportLog: ReportLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主要处理预发送消息的状态管理
|
||||
*/
|
||||
export class PreSendLocalMessageEventsManager {
|
||||
private reportLog: ReportLog;
|
||||
|
||||
private reportLogWithScope: ReportLog;
|
||||
|
||||
constructor(props: PreSendLocalMessageEventsManagerProps) {
|
||||
this.reportLog = props.reportLog;
|
||||
this.reportLogWithScope = this.reportLog.createLoggerWith({
|
||||
scope: 'preSendLocalMessageEventsManager',
|
||||
});
|
||||
this.preSendLocalMessageEventsMap = new Map();
|
||||
}
|
||||
|
||||
private preSendLocalMessageEvents: EventEmitter<PreSendLocalMessageEventsEnum> =
|
||||
new EventEmitter();
|
||||
|
||||
private preSendLocalMessageEventsMap: Map<
|
||||
string,
|
||||
PreSendLocalMessage<ContentType>
|
||||
> = new Map();
|
||||
|
||||
// 新增需要缓存的本地消息
|
||||
add(message: Message<ContentType>) {
|
||||
this.preSendLocalMessageEventsMap.set(
|
||||
message.extra_info.local_message_id,
|
||||
message,
|
||||
);
|
||||
this.reportLogWithScope.info({
|
||||
message: '本地消息缓存-新增',
|
||||
meta: {
|
||||
buffer: this.preSendLocalMessageEventsMap,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateLocalMessageStatus(
|
||||
local_message_id: string,
|
||||
local_message_status: LocalMessageStatus,
|
||||
) {
|
||||
const message = this.preSendLocalMessageEventsMap.get(local_message_id);
|
||||
if (message) {
|
||||
message.local_message_status = local_message_status;
|
||||
this.preSendLocalMessageEventsMap.set(local_message_id, message);
|
||||
this.reportLogWithScope.info({
|
||||
message: '本地消息缓存-更新消息状态',
|
||||
meta: {
|
||||
buffer: this.preSendLocalMessageEventsMap,
|
||||
local_message_status,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取缓存的本地消息
|
||||
getStashedLocalMessage(local_message_id: string) {
|
||||
return this.preSendLocalMessageEventsMap.get(local_message_id);
|
||||
}
|
||||
|
||||
on<T extends PreSendLocalMessageEventsEnum>(
|
||||
event: T,
|
||||
callback: PreSendLocalMessageEventsMap[T],
|
||||
) {
|
||||
this.preSendLocalMessageEvents.on(event, callback);
|
||||
}
|
||||
|
||||
once<T extends PreSendLocalMessageEventsEnum>(
|
||||
event: T,
|
||||
callback: PreSendLocalMessageEventsMap[T],
|
||||
) {
|
||||
this.preSendLocalMessageEvents.once(event, callback);
|
||||
}
|
||||
|
||||
emit<T extends PreSendLocalMessageEventsEnum>(
|
||||
event: T,
|
||||
params: Parameters<PreSendLocalMessageEventsMap[T]>[0],
|
||||
) {
|
||||
this.preSendLocalMessageEvents.emit(event, params);
|
||||
// 发送成功, 清除
|
||||
if (event === PreSendLocalMessageEventsEnum.MESSAGE_SEND_SUCCESS) {
|
||||
const message = params as Message<ContentType>;
|
||||
this.preSendLocalMessageEventsMap.delete(
|
||||
message.extra_info.local_message_id,
|
||||
);
|
||||
this.reportLogWithScope.info({
|
||||
message: '本地消息缓存清除-发送成功',
|
||||
meta: {
|
||||
buffer: this.preSendLocalMessageEventsMap,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送失败/超时, 清除
|
||||
if (
|
||||
[
|
||||
PreSendLocalMessageEventsEnum.MESSAGE_SEND_FAIL,
|
||||
PreSendLocalMessageEventsEnum.MESSAGE_SEND_TIMEOUT,
|
||||
].includes(event)
|
||||
) {
|
||||
const { local_message_id } = (params as ChatCoreError).ext;
|
||||
local_message_id &&
|
||||
this.preSendLocalMessageEventsMap.delete(local_message_id);
|
||||
this.reportLogWithScope.info({
|
||||
message: '本地消息缓存清除-发送失败/超时',
|
||||
meta: {
|
||||
buffer: this.preSendLocalMessageEventsMap,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 上传状态修改
|
||||
if (event === PreSendLocalMessageEventsEnum.FILE_UPLOAD_STATUS_CHANGE) {
|
||||
const message = params as Message<ContentType>;
|
||||
this.preSendLocalMessageEventsMap.set(
|
||||
message.extra_info.local_message_id,
|
||||
message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
destroy() {
|
||||
this.preSendLocalMessageEvents.removeAllListeners();
|
||||
this.preSendLocalMessageEventsMap.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. 负责规范各种类型消息创建的入参出参,减少消息创建成本
|
||||
* 2. 对于接收到的消息,针对不同消息类型,吐出指定的消息格式
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { cloneDeep, merge } from 'lodash-es';
|
||||
|
||||
import {
|
||||
ContentType,
|
||||
type CreateMessageOptions,
|
||||
type ImageMessageProps,
|
||||
type Message,
|
||||
type MessageContent,
|
||||
type MixMessageContent,
|
||||
type NormalizedMessageProps,
|
||||
PreSendLocalMessageEventsEnum,
|
||||
type SendMessage,
|
||||
type SendMessageOptions,
|
||||
type TextAndFileMixMessageProps,
|
||||
type TextAndFileMixMessagePropsPayload,
|
||||
type TextMessageProps,
|
||||
} from '../types';
|
||||
import { filterEmptyField } from '../../shared/utils/data-handler';
|
||||
import { FileTypeEnum, getFileInfo } from '../../shared/const';
|
||||
import {
|
||||
type EventPayloadMaps,
|
||||
type UploadPluginConstructor,
|
||||
type UploadPluginInterface,
|
||||
type UploadResult,
|
||||
} from '../../plugins/upload-plugin/types/plugin-upload';
|
||||
import { ChatCoreError } from '../../custom-error';
|
||||
import { type Scene } from '../../chat-sdk/types/interface';
|
||||
import { type PreSendLocalMessageEventsManager } from './presend-local-message-events-manager';
|
||||
import { PreSendLocalMessage } from './presend-local-message';
|
||||
|
||||
/**
|
||||
* 创建预发送消息
|
||||
*/
|
||||
export interface PreSendLocalMessageFactoryProps {
|
||||
bot_id?: string;
|
||||
preset_bot?: string;
|
||||
conversation_id: string;
|
||||
user?: string;
|
||||
enableDebug?: false;
|
||||
scene?: Scene;
|
||||
bot_version?: string;
|
||||
draft_mode?: boolean;
|
||||
}
|
||||
|
||||
export class PreSendLocalMessageFactory {
|
||||
bot_id?: string;
|
||||
|
||||
preset_bot?: string;
|
||||
|
||||
conversation_id: string;
|
||||
|
||||
user?: string;
|
||||
|
||||
scene?: Scene;
|
||||
|
||||
bot_version?: string;
|
||||
|
||||
draft_mode?: boolean;
|
||||
|
||||
constructor(props: PreSendLocalMessageFactoryProps) {
|
||||
const {
|
||||
bot_id,
|
||||
conversation_id,
|
||||
preset_bot,
|
||||
user,
|
||||
scene,
|
||||
bot_version,
|
||||
draft_mode,
|
||||
} = props;
|
||||
this.bot_id = bot_id;
|
||||
this.preset_bot = preset_bot;
|
||||
this.conversation_id = conversation_id;
|
||||
this.user = user;
|
||||
this.scene = scene;
|
||||
this.bot_version = bot_version;
|
||||
this.draft_mode = draft_mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文本消息
|
||||
*/
|
||||
createTextMessage(
|
||||
props: TextMessageProps,
|
||||
messageEventsManager: PreSendLocalMessageEventsManager,
|
||||
options?: CreateMessageOptions,
|
||||
): Message<ContentType.Text> {
|
||||
const { payload } = props;
|
||||
const message = PreSendLocalMessage.create<ContentType.Text>(
|
||||
this.assembleMessageCommonProps({
|
||||
content: payload.text,
|
||||
content_obj: payload.text,
|
||||
content_type: ContentType.Text,
|
||||
section_id: options?.section_id || '',
|
||||
mention_list: props.payload.mention_list,
|
||||
}),
|
||||
);
|
||||
messageEventsManager.add(message);
|
||||
return cloneDeep(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图片消息
|
||||
*/
|
||||
createImageMessage<M extends EventPayloadMaps>(props: {
|
||||
messageProps: ImageMessageProps<M>;
|
||||
|
||||
UploadPlugin: UploadPluginConstructor;
|
||||
uploadPluginConstructorOptions: Record<string, unknown>;
|
||||
messageEventsManager: PreSendLocalMessageEventsManager;
|
||||
options?: CreateMessageOptions;
|
||||
}): PreSendLocalMessage<ContentType.Image> {
|
||||
const {
|
||||
payload: { file, mention_list },
|
||||
pluginUploadManager,
|
||||
} = props.messageProps;
|
||||
const {
|
||||
UploadPlugin,
|
||||
messageEventsManager,
|
||||
options,
|
||||
uploadPluginConstructorOptions,
|
||||
} = props;
|
||||
const message = PreSendLocalMessage.create(
|
||||
this.assembleMessageCommonProps<ContentType.Image>({
|
||||
content: JSON.stringify(this.assembleImageMessageContent(file)),
|
||||
content_obj: this.assembleImageMessageContent(file),
|
||||
content_type: ContentType.Image,
|
||||
section_id: options?.section_id || '',
|
||||
mention_list,
|
||||
}),
|
||||
);
|
||||
|
||||
// 预发送消息保存本地
|
||||
messageEventsManager.add(message);
|
||||
|
||||
const uploaderPluginInstance = new UploadPlugin({
|
||||
file,
|
||||
type: 'image',
|
||||
...uploadPluginConstructorOptions,
|
||||
}) as UploadPluginInterface<M>;
|
||||
|
||||
pluginUploadManager?.(uploaderPluginInstance);
|
||||
|
||||
uploaderPluginInstance.on('complete', info => {
|
||||
this.updateImageMessageContent(message, info.uploadResult);
|
||||
this.updateMessageUploadResult(message, 'success');
|
||||
messageEventsManager.emit(
|
||||
PreSendLocalMessageEventsEnum.FILE_UPLOAD_STATUS_CHANGE,
|
||||
message,
|
||||
);
|
||||
});
|
||||
|
||||
uploaderPluginInstance.on('error', () => {
|
||||
this.updateMessageUploadResult(message, 'fail');
|
||||
});
|
||||
|
||||
return cloneDeep(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件消息
|
||||
*/
|
||||
createFileMessage<M extends EventPayloadMaps>(props: {
|
||||
messageProps: ImageMessageProps<M>;
|
||||
|
||||
UploadPlugin: UploadPluginConstructor;
|
||||
uploadPluginConstructorOptions: Record<string, unknown>;
|
||||
messageEventsManager: PreSendLocalMessageEventsManager;
|
||||
options?: CreateMessageOptions;
|
||||
}): PreSendLocalMessage<ContentType.File> {
|
||||
const {
|
||||
payload: { file, mention_list },
|
||||
pluginUploadManager,
|
||||
} = props.messageProps;
|
||||
const {
|
||||
UploadPlugin,
|
||||
messageEventsManager,
|
||||
options,
|
||||
uploadPluginConstructorOptions,
|
||||
} = props;
|
||||
const message = PreSendLocalMessage.create(
|
||||
this.assembleMessageCommonProps<ContentType.File>({
|
||||
content: JSON.stringify(this.assembleFileMessageContent(file)),
|
||||
content_obj: this.assembleFileMessageContent(file),
|
||||
content_type: ContentType.File,
|
||||
section_id: options?.section_id || '',
|
||||
mention_list,
|
||||
}),
|
||||
);
|
||||
// 预发送文件消息保存本地
|
||||
messageEventsManager.add(message);
|
||||
|
||||
const uploaderPluginInstance = new UploadPlugin({
|
||||
file,
|
||||
type: 'object',
|
||||
...uploadPluginConstructorOptions,
|
||||
}) as UploadPluginInterface<M>;
|
||||
pluginUploadManager?.(uploaderPluginInstance);
|
||||
|
||||
uploaderPluginInstance.on('complete', info => {
|
||||
const { uploadResult, type } = info;
|
||||
if (type === 'success') {
|
||||
this.updateFileMessageContent(message, uploadResult);
|
||||
this.updateMessageUploadResult(message, 'success');
|
||||
messageEventsManager.emit(
|
||||
PreSendLocalMessageEventsEnum.FILE_UPLOAD_STATUS_CHANGE,
|
||||
message,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
uploaderPluginInstance.on('error', () => {
|
||||
this.updateMessageUploadResult(message, 'fail');
|
||||
messageEventsManager.emit(
|
||||
PreSendLocalMessageEventsEnum.FILE_UPLOAD_STATUS_CHANGE,
|
||||
message,
|
||||
);
|
||||
});
|
||||
return cloneDeep(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图文混合消息
|
||||
*/
|
||||
createTextAndFileMixMessage(
|
||||
props: TextAndFileMixMessageProps,
|
||||
messageEventsManager: PreSendLocalMessageEventsManager,
|
||||
options?: CreateMessageOptions,
|
||||
): Message<ContentType.Mix> {
|
||||
const {
|
||||
payload: { mixList, mention_list },
|
||||
} = props;
|
||||
const message = PreSendLocalMessage.create(
|
||||
this.assembleMessageCommonProps<ContentType.Mix>({
|
||||
content: JSON.stringify(
|
||||
this.assembleTextAndFileMixMessageContent(mixList),
|
||||
),
|
||||
content_obj: this.assembleTextAndFileMixMessageContent(mixList),
|
||||
content_type: ContentType.Mix,
|
||||
section_id: options?.section_id || '',
|
||||
mention_list,
|
||||
}),
|
||||
);
|
||||
messageEventsManager.add(message);
|
||||
return cloneDeep(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标准化过的消息
|
||||
*/
|
||||
createNormalizedMessage<T extends ContentType>(
|
||||
props: NormalizedMessageProps<T>,
|
||||
messageEventsManager: PreSendLocalMessageEventsManager,
|
||||
options?: CreateMessageOptions,
|
||||
): Message<T> {
|
||||
const {
|
||||
payload: { contentObj, contentType, mention_list },
|
||||
} = props;
|
||||
const message = PreSendLocalMessage.create(
|
||||
this.assembleMessageCommonProps({
|
||||
content: JSON.stringify(contentObj),
|
||||
content_obj: contentObj,
|
||||
content_type: contentType,
|
||||
section_id: options?.section_id || '',
|
||||
mention_list,
|
||||
file_upload_result: 'success',
|
||||
}),
|
||||
);
|
||||
messageEventsManager.add(message);
|
||||
return cloneDeep(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装图片消息content
|
||||
*/
|
||||
private assembleImageMessageContent(
|
||||
file: File,
|
||||
): MessageContent<ContentType.Image> {
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
return {
|
||||
image_list: [
|
||||
{
|
||||
key: '',
|
||||
image_thumb: {
|
||||
url: blobUrl,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
image_ori: {
|
||||
url: blobUrl,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
feedback: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图片消息content
|
||||
*/
|
||||
private updateImageMessageContent(
|
||||
message: PreSendLocalMessage<ContentType.Image>,
|
||||
uploadResult: UploadResult,
|
||||
): void {
|
||||
const {
|
||||
Uri = '',
|
||||
Url = '',
|
||||
ImageWidth = 0,
|
||||
ImageHeight = 0,
|
||||
} = uploadResult;
|
||||
message.content_obj.image_list[0] = {
|
||||
...message.content_obj.image_list[0],
|
||||
key: Uri,
|
||||
image_thumb: {
|
||||
...message.content_obj.image_list[0].image_thumb,
|
||||
width: ImageWidth,
|
||||
height: ImageHeight,
|
||||
url: Url,
|
||||
},
|
||||
image_ori: {
|
||||
...message.content_obj.image_list[0].image_ori,
|
||||
width: ImageWidth,
|
||||
height: ImageHeight,
|
||||
url: Url,
|
||||
},
|
||||
};
|
||||
|
||||
message.content = JSON.stringify(message.content_obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件消息content
|
||||
*/
|
||||
private updateFileMessageContent(
|
||||
message: PreSendLocalMessage<ContentType.File>,
|
||||
uploadResult: UploadResult,
|
||||
): void {
|
||||
const { Uri = '', Url = '' } = uploadResult;
|
||||
message.content_obj.file_list[0].file_key = Uri;
|
||||
message.content_obj.file_list[0].file_url = Url;
|
||||
message.content = JSON.stringify(message.content_obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图片/文件消息上传状态:success | fail
|
||||
*/
|
||||
private updateMessageUploadResult(
|
||||
message: PreSendLocalMessage<ContentType.Image | ContentType.File>,
|
||||
status: 'success' | 'fail',
|
||||
) {
|
||||
message.file_upload_result = status;
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装文件消息content
|
||||
*/
|
||||
private assembleFileMessageContent(
|
||||
file: File,
|
||||
): MessageContent<ContentType.File> {
|
||||
const fileType = getFileInfo(file)?.fileType;
|
||||
if (!fileType) {
|
||||
throw new ChatCoreError('文件类型不支持');
|
||||
}
|
||||
return {
|
||||
file_list: [
|
||||
{
|
||||
file_key: '',
|
||||
file_name: file.name,
|
||||
file_type: fileType,
|
||||
file_size: file.size,
|
||||
file_url: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装图文混合消息content
|
||||
*/
|
||||
private assembleTextAndFileMixMessageContent(
|
||||
mixList: TextAndFileMixMessagePropsPayload['mixList'],
|
||||
): MessageContent<ContentType.Mix> {
|
||||
const itemList = mixList.map(item => {
|
||||
const { type } = item;
|
||||
if (type === ContentType.Text) {
|
||||
return {
|
||||
type,
|
||||
text: item.text,
|
||||
};
|
||||
}
|
||||
if (type === ContentType.File) {
|
||||
const fileType =
|
||||
getFileInfo(item.file)?.fileType || FileTypeEnum.DEFAULT_UNKNOWN;
|
||||
|
||||
return {
|
||||
type,
|
||||
file: {
|
||||
file_key: item.uri,
|
||||
file_name: item.file.name,
|
||||
file_type: fileType,
|
||||
file_size: item.file.size,
|
||||
file_url: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (type === ContentType.Image) {
|
||||
const blobUrl = URL.createObjectURL(item.file);
|
||||
|
||||
return {
|
||||
type,
|
||||
image: {
|
||||
key: item.uri,
|
||||
image_thumb: {
|
||||
url: blobUrl,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
},
|
||||
image_ori: {
|
||||
url: blobUrl,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
},
|
||||
feedback: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
item_list: itemList as MixMessageContent['item_list'],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装消息通用字段
|
||||
*/
|
||||
private assembleMessageCommonProps<T extends ContentType>(
|
||||
props: Pick<
|
||||
PreSendLocalMessage<T>,
|
||||
| 'content'
|
||||
| 'content_type'
|
||||
| 'section_id'
|
||||
| 'content_obj'
|
||||
| 'mention_list'
|
||||
| 'file_upload_result'
|
||||
>,
|
||||
): Message<T> {
|
||||
const commonProps: Pick<
|
||||
Message<T>,
|
||||
'message_id' | 'reply_id' | 'is_finish' | 'extra_info' | 'role' | 'type'
|
||||
> = {
|
||||
message_id: '',
|
||||
reply_id: '',
|
||||
is_finish: true,
|
||||
// TODO: fix me
|
||||
// @ts-expect-error should be fixed
|
||||
extra_info: {
|
||||
local_message_id: nanoid(),
|
||||
input_tokens: '', // 用户 query 消耗的 token
|
||||
output_tokens: '', // llm 输出消耗的 token
|
||||
token: '', // 总的 token 消耗
|
||||
plugin_status: 'success', // "success" or "fail"
|
||||
time_cost: '', // 中间调用过程的时间
|
||||
workflow_tokens: '',
|
||||
bot_state: '', // { bot_id?: string;agent_id?: string;agent_name?: string; }
|
||||
plugin_request: '', // plugin 请求的参数
|
||||
tool_name: '', // 调用的 plugin 下具体的 api 名称
|
||||
plugin: '', // 调用的 plugin 名称
|
||||
},
|
||||
role: 'user',
|
||||
type: 'question',
|
||||
};
|
||||
return merge(
|
||||
commonProps,
|
||||
this.bot_id ? { bot_id: this.bot_id } : {},
|
||||
this.preset_bot ? { preset_bot: this.preset_bot } : {},
|
||||
this.user ? { user: this.user } : {},
|
||||
this.scene ? { scene: this.scene } : {},
|
||||
props,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理发送给服务端的消息结构
|
||||
*/
|
||||
getSendMessageStructure(
|
||||
message: PreSendLocalMessage<ContentType>,
|
||||
options: SendMessageOptions,
|
||||
): SendMessage {
|
||||
const {
|
||||
extra_info: { local_message_id },
|
||||
content_type,
|
||||
content,
|
||||
message_id,
|
||||
mention_list,
|
||||
} = message;
|
||||
const { user, bot_id, preset_bot, scene, bot_version, draft_mode } = this;
|
||||
const { stream, chatHistory, isRegenMessage, extendFiled } = options;
|
||||
const mergedStructure = merge(
|
||||
{
|
||||
bot_id,
|
||||
preset_bot,
|
||||
conversation_id: this.conversation_id,
|
||||
local_message_id,
|
||||
content_type,
|
||||
query: content,
|
||||
user,
|
||||
extra: {},
|
||||
scene,
|
||||
bot_version,
|
||||
draft_mode,
|
||||
stream,
|
||||
chat_history: chatHistory,
|
||||
regen_message_id: isRegenMessage ? message_id : undefined,
|
||||
mention_list,
|
||||
},
|
||||
extendFiled,
|
||||
);
|
||||
return filterEmptyField(mergedStructure);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 暴露预发送消息实例,预发送消息用于创建消息后上屏,消息格式和Message<T>一致
|
||||
*/
|
||||
|
||||
import {
|
||||
type Message,
|
||||
type MessageInfoRole,
|
||||
type ContentType,
|
||||
type MessageType,
|
||||
type MessageStatus,
|
||||
type MessageContent,
|
||||
type ChunkRaw,
|
||||
type LocalMessageStatus,
|
||||
} from '../types';
|
||||
|
||||
export class PreSendLocalMessage<T extends ContentType> implements Message<T> {
|
||||
bot_id?: string;
|
||||
preset_bot?: string;
|
||||
user?: string;
|
||||
// TODO: fix me
|
||||
// @ts-expect-error should be fixed
|
||||
extra_info: Message<T>['extra_info'] = {
|
||||
local_message_id: '',
|
||||
input_tokens: '', // 用户 query 消耗的 token
|
||||
output_tokens: '', // llm 输出消耗的 token
|
||||
token: '', // 总的 token 消耗
|
||||
plugin_status: 'success', // "success" or "fail"
|
||||
time_cost: '', // 中间调用过程的时间
|
||||
workflow_tokens: '',
|
||||
bot_state: '', // { bot_id?: string;agent_id?: string;agent_name?: string; }
|
||||
plugin_request: '', // plugin 请求的参数
|
||||
tool_name: '', // 调用的 plugin 下具体的 api 名称
|
||||
plugin: '', // 调用的 plugin 名称
|
||||
};
|
||||
index?: number; // message在一次 response 的排序
|
||||
is_finish?: boolean; // 消息状态
|
||||
section_id: string; // 消息属于的上下文id
|
||||
content_type: ContentType;
|
||||
debug_messages?: ChunkRaw[];
|
||||
content: string;
|
||||
content_obj: MessageContent<T>;
|
||||
file_upload_result?: 'success' | 'fail'; // 文件上传状态
|
||||
role: MessageInfoRole;
|
||||
type: MessageType;
|
||||
message_status?: MessageStatus;
|
||||
message_id: string;
|
||||
reply_id: string;
|
||||
local_message_status?: LocalMessageStatus;
|
||||
mention_list: { id: string }[];
|
||||
|
||||
constructor(props: Message<T>) {
|
||||
const {
|
||||
bot_id,
|
||||
preset_bot,
|
||||
extra_info: { local_message_id },
|
||||
content_type,
|
||||
content,
|
||||
content_obj,
|
||||
role,
|
||||
type,
|
||||
message_status,
|
||||
message_id,
|
||||
reply_id,
|
||||
user,
|
||||
section_id,
|
||||
local_message_status,
|
||||
mention_list,
|
||||
file_upload_result,
|
||||
} = props;
|
||||
this.bot_id = bot_id;
|
||||
this.preset_bot = preset_bot;
|
||||
this.user = user;
|
||||
this.extra_info.local_message_id = local_message_id;
|
||||
this.content_type = content_type;
|
||||
this.content = content;
|
||||
this.content_obj = content_obj;
|
||||
this.file_upload_result = undefined;
|
||||
this.role = role;
|
||||
this.type = type;
|
||||
this.message_status = message_status;
|
||||
this.message_id = message_id;
|
||||
this.reply_id = reply_id;
|
||||
this.section_id = section_id;
|
||||
this.local_message_status = local_message_status || 'unsent';
|
||||
this.mention_list = mention_list;
|
||||
this.file_upload_result = file_upload_result;
|
||||
}
|
||||
|
||||
static create<T extends ContentType>(props: Message<T>) {
|
||||
return new PreSendLocalMessage(props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type RequiredAction } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { type PartiallyRequired } from '../../shared/utils/data-handler';
|
||||
import { type FileType } from '../../shared/const';
|
||||
import {
|
||||
type EventPayloadMaps,
|
||||
type UploadPluginInterface,
|
||||
} from '../../plugins/upload-plugin/types/plugin-upload';
|
||||
import { type ChatCoreError } from '../../custom-error';
|
||||
import { type Scene } from '../../chat-sdk/types/interface';
|
||||
|
||||
type JSONstring<T = object> = T extends object ? string : never;
|
||||
|
||||
/** follow copilot 定义的枚举 */
|
||||
export enum ChatMessageMetaType {
|
||||
/** Compatible value */
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
Default_0,
|
||||
/** 端侧直接替换 */
|
||||
Replaceable,
|
||||
/** 插入引用 */
|
||||
Insertable,
|
||||
/** 文档引用 */
|
||||
DocumentRef,
|
||||
/** 知识库引用卡片 */
|
||||
KnowledgeCard,
|
||||
/** 嵌入的多媒体信息,只是alice给端上用的,因为全链路复用这一个字段,所以在这儿改了 */
|
||||
EmbeddedMultimedia = 100,
|
||||
}
|
||||
|
||||
export interface ChatMessageMetaInfo {
|
||||
type?: ChatMessageMetaType;
|
||||
info?: JSONstring;
|
||||
}
|
||||
|
||||
// 服务端返回的chunk
|
||||
export interface ChunkRaw {
|
||||
index: number;
|
||||
seq_id: number;
|
||||
is_finish: boolean;
|
||||
message: MessageRaw;
|
||||
}
|
||||
|
||||
export interface MessageExtraInfo {
|
||||
local_message_id: string; // 前端消息 id, 用于预发送消息体更新
|
||||
input_tokens: string; // 用户 query 消耗的 token
|
||||
output_tokens: string; // llm 输出消耗的 token
|
||||
token: string; // 总的 token 消耗
|
||||
plugin_status: string; // "0" === success or "1" === fail
|
||||
time_cost: string; // 中间调用过程的时间
|
||||
workflow_tokens: string;
|
||||
bot_state: string; // { bot_id?: string;agent_id?: string;agent_name?: string; }
|
||||
plugin_request: string; // plugin 请求的参数
|
||||
tool_name: string; // 调用的 plugin 下具体的 api 名称
|
||||
plugin: string; // 调用的 plugin 名称
|
||||
log_id?: string; // chat 的 logId
|
||||
mock_hit_info?: string; // plugin 命中 mockset 信息
|
||||
execute_display_name: string; // 展示名称
|
||||
/** 流式plugin返回的用于替换tool response内容的标识,普通plugin没有 */
|
||||
stream_plugin_running?: string;
|
||||
/** 目前仅有中间消息返回*/
|
||||
message_title?: string;
|
||||
remove_query_id?: string; // 有此字段代表触发擦除用户 qeury 安全策略, 值为需要擦出的用户消息的 message_id
|
||||
new_section_id?: string; // 有此字段代表触发清除上下文安全策略
|
||||
/** 对应定时任务task_type,1-预设任务,2-用户任务,3-Plugin后台任务 */
|
||||
task_type?: string;
|
||||
call_id?: string; // function_call和tool_response匹配的id
|
||||
}
|
||||
|
||||
// 服务端返回的原始Message结构
|
||||
export interface MessageRaw {
|
||||
role: MessageInfoRole; // 信息发出方的角色
|
||||
type: MessageType; // 主要用于区分role=assistant的bot返回信息类型
|
||||
section_id: string;
|
||||
content_type: ContentType;
|
||||
content: string;
|
||||
reasoning_content?: string;
|
||||
content_time?: number; // 消息发送时间,服务端是 Int64,接口拿到需要转一下
|
||||
user?: string; // 用户唯一标识
|
||||
/**
|
||||
* 仅拉取历史返回
|
||||
*/
|
||||
message_status?: MessageStatus;
|
||||
message_id: string; // 后端消息 id, 可能有多条回复
|
||||
reply_id: string; // 回复 id,query的messageId
|
||||
broken_pos?: number; // 打断位置,仅对type = 'answer'生效
|
||||
/** 拉流 ack 有, 后续没有 */
|
||||
mention_list?: MessageMentionListFields['mention_list'];
|
||||
/** 发送者 id */
|
||||
sender_id?: string;
|
||||
extra_info: MessageExtraInfo;
|
||||
source?: MessageSource;
|
||||
reply_message?: Message<ContentType>;
|
||||
meta_infos?: Array<ChatMessageMetaInfo>;
|
||||
/** 中断消息服务端透传中台的参数,获取中断场景、续聊id */
|
||||
required_action?: RequiredAction;
|
||||
/** 卡片状态 */
|
||||
card_status?: Record<string, string>;
|
||||
}
|
||||
|
||||
export const messageSource = {
|
||||
/** 普通聊天消息 */
|
||||
Chat: 0,
|
||||
/** 定时任务 */
|
||||
TaskManualTrigger: 1,
|
||||
/** 通知 */
|
||||
Notice: 2,
|
||||
/** 异步结果 */
|
||||
AsyncResult: 3,
|
||||
} as const;
|
||||
|
||||
export const taskType = {
|
||||
/** 预设任务 */
|
||||
PresetTask: '1',
|
||||
/** 用户任务 */
|
||||
CreatedByUserTask: '2',
|
||||
/** Plugin 后台任务 */
|
||||
PluginTask: '3',
|
||||
} as const;
|
||||
|
||||
export type MessageSource = (typeof messageSource)[keyof typeof messageSource];
|
||||
|
||||
// 经过Processor处理后的chunk
|
||||
export interface Chunk<T extends ContentType> {
|
||||
index: number;
|
||||
seq_id: number;
|
||||
is_finish: boolean;
|
||||
message: Message<T>;
|
||||
}
|
||||
|
||||
// 经过Processor处理后的Message
|
||||
export type Message<T extends ContentType, V = unknown> = MessageRaw &
|
||||
MessageMentionListFields & {
|
||||
bot_id?: string;
|
||||
preset_bot?: string;
|
||||
index?: number; // 临时状态,message在一次 response 的排序
|
||||
is_finish?: boolean; // 临时状态,标识消息是否拉取完成
|
||||
/**
|
||||
* content 经过反序列化
|
||||
*/
|
||||
content_obj: MessageContent<T, V>;
|
||||
/**
|
||||
* sdk开启了enableDebug模式,每条回复消息新增debug_messages字段,包含channel 吐出的所有 chunk消息
|
||||
*/
|
||||
debug_messages?: ChunkRaw[];
|
||||
stream_chunk_buffer?: ChunkRaw[];
|
||||
stream_message_buffer?: Message<ContentType>[];
|
||||
/**
|
||||
* 旧接口未必有该字段;仅 query、answer、notice 等常见显示类型会进行计数。
|
||||
* - int64 类型,计数从 "1" 开始。
|
||||
* - 尽管我不认为单聊会有超过 Number.MAX_SAFE_INTEGER 的数值,但是还是用了 big-integer 库来进行处理。
|
||||
* - 不刷旧数据,因此 ① 旧数据 ② 非常规消息 的值取 "0"
|
||||
*/
|
||||
message_index?: string;
|
||||
/**
|
||||
* 仅预发送消息返回
|
||||
*/
|
||||
file_upload_result?: 'success' | 'fail'; // 文件上传状态
|
||||
/**
|
||||
* 本地消息状态, 仅预发送消息返回
|
||||
*/
|
||||
local_message_status?: LocalMessageStatus;
|
||||
/**
|
||||
* 仅即使消息返回,拉取历史消息无
|
||||
*/
|
||||
logId?: string; // chat 的 logId
|
||||
};
|
||||
|
||||
// 发送给服务端的消息结构
|
||||
export interface SendMessage
|
||||
extends MessageMentionListFields,
|
||||
ResumeMessage,
|
||||
Record<string, unknown> {
|
||||
bot_id?: string;
|
||||
preset_bot?: string;
|
||||
conversation_id: string;
|
||||
stream?: boolean;
|
||||
user?: string;
|
||||
query: string;
|
||||
extra: Record<string, string>;
|
||||
draft_mode?: boolean; // 草稿bot or 线上bot
|
||||
content_type?: string; // 文件 file 图片 image 等
|
||||
regen_message_id?: string; // 重试消息id
|
||||
local_message_id: string; // 前端本地的message_id 在extra_info 里面透传返回
|
||||
chat_history?: Message<ContentType>[]; // 指定聊天上下文, 服务端不落库
|
||||
}
|
||||
// 发送给服务端的resume结构体,chat类型本身有缺失,本期只补充resume相关
|
||||
export interface ResumeMessage {
|
||||
conversation_id: string;
|
||||
scene?: Scene; // 场景值
|
||||
resume_message_id?: string; // 续聊场景服务端所需id,即reply_id
|
||||
interrupt_message_id?: string; // 中断的verbose消息id
|
||||
tool_outputs?: {
|
||||
tool_call_id?: string; // 透传中断verbose消息中的tool_call.id
|
||||
output?: string; // 地理位置授权场景传经纬度
|
||||
}[];
|
||||
}
|
||||
|
||||
export type LocalMessageStatus =
|
||||
| 'unsent'
|
||||
| 'send_success'
|
||||
| 'send_fail'
|
||||
| 'send_timeout';
|
||||
|
||||
export enum ContentType {
|
||||
Text = 'text',
|
||||
Link = 'link',
|
||||
Music = 'music',
|
||||
Video = 'video',
|
||||
Card = 'card',
|
||||
Image = 'image',
|
||||
File = 'file',
|
||||
Tako = 'tako',
|
||||
Custom = 'custom',
|
||||
Mix = 'mix',
|
||||
}
|
||||
|
||||
export type MessageInfoRole = 'user' | 'assistant';
|
||||
|
||||
export type MessageType =
|
||||
| 'answer'
|
||||
| 'function_call'
|
||||
| 'tool_response'
|
||||
| 'follow_up'
|
||||
| 'ack'
|
||||
| 'question'
|
||||
| 'knowledge'
|
||||
| 'verbose'
|
||||
| 'task_manual_trigger'
|
||||
| '';
|
||||
|
||||
export type MessageStatus = 'available' | 'broken';
|
||||
|
||||
export type ResponseStatus = 'responding' | 'endResponse' | 'interrupt';
|
||||
|
||||
export enum PreSendLocalMessageEventsEnum {
|
||||
FILE_UPLOAD_STATUS_CHANGE = 'file_upload_status_change',
|
||||
MESSAGE_SEND_SUCCESS = 'message_send_success',
|
||||
MESSAGE_SEND_FAIL = 'message_send_fail',
|
||||
MESSAGE_SEND_TIMEOUT = 'message_send_timeout',
|
||||
}
|
||||
|
||||
export interface PreSendLocalMessageEventsMap {
|
||||
[PreSendLocalMessageEventsEnum.FILE_UPLOAD_STATUS_CHANGE]: (
|
||||
message: Message<ContentType>,
|
||||
) => void;
|
||||
[PreSendLocalMessageEventsEnum.MESSAGE_SEND_SUCCESS]: (
|
||||
message: Message<ContentType>,
|
||||
) => void;
|
||||
[PreSendLocalMessageEventsEnum.MESSAGE_SEND_FAIL]: (
|
||||
chatCoreError: ChatCoreError,
|
||||
) => void;
|
||||
[PreSendLocalMessageEventsEnum.MESSAGE_SEND_TIMEOUT]: (
|
||||
chatCoreError: ChatCoreError,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type TextMessageContent = string;
|
||||
|
||||
export interface ImageModel {
|
||||
key: string;
|
||||
image_thumb: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
image_ori: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
feedback?: null;
|
||||
}
|
||||
|
||||
export interface FileModel {
|
||||
file_key: string;
|
||||
file_name: string;
|
||||
file_type: FileType;
|
||||
file_size: number;
|
||||
file_url: string;
|
||||
}
|
||||
|
||||
export interface ImageMessageContent {
|
||||
image_list: ImageModel[];
|
||||
}
|
||||
|
||||
export interface FileMessageContent {
|
||||
file_list: FileModel[];
|
||||
}
|
||||
|
||||
export interface MixMessageContent {
|
||||
item_list: (ImageMixItem | TextMixItem | FileMixItem | ReferMixItem)[];
|
||||
}
|
||||
|
||||
export interface ContentTypeMap {
|
||||
[ContentType.Text]: TextMessageContent;
|
||||
[ContentType.Image]: ImageMessageContent;
|
||||
[ContentType.File]: FileMessageContent;
|
||||
[ContentType.Mix]: MixMessageContent;
|
||||
}
|
||||
|
||||
export type MessageContent<
|
||||
T extends ContentType,
|
||||
V = unknown,
|
||||
> = T extends keyof ContentTypeMap ? ContentTypeMap[T] : V;
|
||||
|
||||
export interface MessageExtProps {
|
||||
input_price: string;
|
||||
input_tokens: string;
|
||||
is_finish: string;
|
||||
output_price: string;
|
||||
output_tokens: string;
|
||||
token: string;
|
||||
total_price: string;
|
||||
has_suggest: string;
|
||||
time_cost: string;
|
||||
}
|
||||
|
||||
export interface MessageMentionListFields {
|
||||
/** \@bot 功能,在输入时提及的 bot id */
|
||||
mention_list: { id: string }[];
|
||||
}
|
||||
|
||||
interface TextMessagePropsPayload extends MessageMentionListFields {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface FileMessagePropsPayload extends MessageMentionListFields {
|
||||
file: File;
|
||||
}
|
||||
|
||||
export interface TextAndFileMixMessagePropsTextPayload {
|
||||
type: ContentType.Text;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface TextAndFileMixMessagePropsFilePayload {
|
||||
type: ContentType.File;
|
||||
file: File;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export interface TextAndFileMixMessagePropsImagePayload {
|
||||
type: ContentType.Image;
|
||||
file: File;
|
||||
uri: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface TextAndFileMixMessagePropsPayload
|
||||
extends MessageMentionListFields {
|
||||
mixList: (
|
||||
| TextAndFileMixMessagePropsTextPayload
|
||||
| TextAndFileMixMessagePropsFilePayload
|
||||
| TextAndFileMixMessagePropsImagePayload
|
||||
)[];
|
||||
}
|
||||
|
||||
export interface TextMessageProps {
|
||||
payload: TextMessagePropsPayload;
|
||||
}
|
||||
|
||||
export interface ImageMessageProps<U extends EventPayloadMaps> {
|
||||
payload: FileMessagePropsPayload;
|
||||
pluginUploadManager?: (uploadPlugin: UploadPluginInterface<U>) => void;
|
||||
}
|
||||
|
||||
export interface FileMessageProps<U extends EventPayloadMaps> {
|
||||
payload: FileMessagePropsPayload;
|
||||
pluginUploadManager?: (uploadPlugin: UploadPluginInterface<U>) => void;
|
||||
}
|
||||
|
||||
export interface TextMixItem {
|
||||
type: ContentType.Text;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface FileMixItem {
|
||||
type: ContentType.File;
|
||||
file: FileModel;
|
||||
}
|
||||
|
||||
export interface ImageMixItem {
|
||||
type: ContentType.Image;
|
||||
image: ImageModel;
|
||||
}
|
||||
|
||||
export interface ReferMixItem {
|
||||
type: ContentType.Text;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface TextAndFileMixMessageProps {
|
||||
payload: TextAndFileMixMessagePropsPayload;
|
||||
}
|
||||
|
||||
export interface NormalizedMessagePropsPayload<T extends ContentType>
|
||||
extends MessageMentionListFields {
|
||||
contentType: T;
|
||||
contentObj: MessageContent<T>;
|
||||
}
|
||||
|
||||
export interface NormalizedMessageProps<T extends ContentType> {
|
||||
payload: NormalizedMessagePropsPayload<T>;
|
||||
}
|
||||
|
||||
export interface SendMessageOptions {
|
||||
sendTimeout?: number;
|
||||
betweenChunkTimeout?: number;
|
||||
stream?: boolean;
|
||||
chatHistory?: Message<ContentType>[];
|
||||
// 参数会透传给 chat 接口
|
||||
extendFiled?: Record<string, unknown>;
|
||||
// 透传的header
|
||||
headers?: HeadersInit;
|
||||
// 是否为重新生成消息, 默认false
|
||||
isRegenMessage?: boolean;
|
||||
}
|
||||
|
||||
export type SendMessageMergedOptions = PartiallyRequired<
|
||||
SendMessageOptions,
|
||||
'sendTimeout' | 'betweenChunkTimeout'
|
||||
>;
|
||||
|
||||
export interface CreateMessageOptions {
|
||||
section_id?: string;
|
||||
}
|
||||
|
||||
export enum VerboseMsgType {
|
||||
/** 跳转节点 */
|
||||
JUMP_TO = 'multi_agents_jump_to_agent',
|
||||
/** 回溯节点 */
|
||||
BACK_WORD = 'multi_agents_backwards',
|
||||
/** 长期记忆节点 */
|
||||
LONG_TERM_MEMORY = 'time_capsule_recall',
|
||||
/** finish answer*/
|
||||
GENERATE_ANSWER_FINISH = 'generate_answer_finish',
|
||||
/** 流式插件调用状态 */
|
||||
STREAM_PLUGIN_FINISH = 'stream_plugin_finish',
|
||||
/** 知识库召回 */
|
||||
KNOWLEDGE_RECALL = 'knowledge_recall',
|
||||
/** 中断消息:目前用于地理位置授权 / workflow question 待回复 */
|
||||
INTERRUPT = 'interrupt',
|
||||
/** hooks调用 */
|
||||
HOOK_CALL = 'hook_call',
|
||||
}
|
||||
|
||||
export enum InterruptToolCallsType {
|
||||
FunctionType = 'function', // tool 结果上报
|
||||
RequireInfoType = 'require_info', // 需要信息,如地理位置
|
||||
ReplyMessage = 'reply_message', // question 节点
|
||||
}
|
||||
|
||||
export interface VerboseContent {
|
||||
msg_type: VerboseMsgType;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export enum FinishReasonType {
|
||||
/** 正常回答全部结束 */
|
||||
ALL_FINISH = 0,
|
||||
/** 中断结束 */
|
||||
INTERRUPT = 1,
|
||||
}
|
||||
|
||||
export interface AnswerFinishVerboseData {
|
||||
finish_reason?: FinishReasonType;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type Message,
|
||||
type ContentType,
|
||||
type Scene,
|
||||
type LoadDirection,
|
||||
} from '../../chat-sdk/types/interface';
|
||||
|
||||
export interface GetHistoryMessageProps {
|
||||
conversation_id: string;
|
||||
cursor: string;
|
||||
count: number;
|
||||
scene?: Scene;
|
||||
bot_id?: string;
|
||||
draft_mode?: boolean;
|
||||
preset_bot?: string;
|
||||
load_direction?: LoadDirection;
|
||||
}
|
||||
|
||||
export enum MsgParticipantType {
|
||||
Bot = 1,
|
||||
User = 2,
|
||||
}
|
||||
|
||||
export interface ParticipantInfo {
|
||||
id: string;
|
||||
type?: MsgParticipantType;
|
||||
name: string;
|
||||
desc?: string;
|
||||
avatar_url: string;
|
||||
space_id?: string;
|
||||
user_id?: string;
|
||||
user_name?: string;
|
||||
allow_mention: boolean;
|
||||
access_path: string | undefined;
|
||||
/** 是否允许被分享 */
|
||||
allow_share?: boolean;
|
||||
}
|
||||
|
||||
export interface GetHistoryMessageResponse {
|
||||
message_list: Message<ContentType>[];
|
||||
cursor: string;
|
||||
hasmore: boolean;
|
||||
next_has_more?: boolean;
|
||||
next_cursor?: string;
|
||||
read_message_index?: string;
|
||||
code?: number;
|
||||
msg?: string;
|
||||
participant_info_map?: Record<string, Partial<ParticipantInfo>>;
|
||||
last_section_id: string;
|
||||
}
|
||||
|
||||
export interface ClearHistoryProps {
|
||||
bot_id: string;
|
||||
conversation_id: string;
|
||||
scene?: Scene;
|
||||
}
|
||||
|
||||
export interface ClearHistoryResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
new_section_id: string;
|
||||
new_section_message_list: Message<ContentType>[];
|
||||
}
|
||||
|
||||
export interface ClearMessageContextProps {
|
||||
conversation_id: string;
|
||||
insert_history_message_list: string[];
|
||||
scene?: Scene;
|
||||
}
|
||||
|
||||
export interface ClearMessageContextResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
new_section_id: string;
|
||||
new_section_message_list: Message<ContentType>[];
|
||||
}
|
||||
|
||||
export interface DeleteMessageProps {
|
||||
bot_id: string;
|
||||
conversation_id: string;
|
||||
message_id: string;
|
||||
scene?: Scene;
|
||||
}
|
||||
|
||||
export interface DeleteMessageResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface BreakMessageProps {
|
||||
conversation_id: string;
|
||||
/**
|
||||
* 被打断问题的 local_message_id
|
||||
*/
|
||||
local_message_id: string;
|
||||
/**
|
||||
* 被打断的问题id
|
||||
*/
|
||||
query_message_id: string;
|
||||
/**
|
||||
* 当前问题下哪一条回复被打断了
|
||||
* 仅但被打断的消息 type = 'answer' 时传递
|
||||
*/
|
||||
answer_message_id?: string;
|
||||
/**
|
||||
* 打断位置
|
||||
* 仅但被打断的消息 type = 'answer' 时传递
|
||||
*/
|
||||
broken_pos?: number;
|
||||
|
||||
scene?: Scene;
|
||||
}
|
||||
|
||||
export interface BreakMessageResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export type ClearMessageContextParams = Pick<
|
||||
ClearMessageContextProps,
|
||||
'insert_history_message_list'
|
||||
>;
|
||||
|
||||
/*
|
||||
消息点赞/点踩接口类型定义
|
||||
*/
|
||||
export enum MessageFeedbackType {
|
||||
Default = 0,
|
||||
Like = 1,
|
||||
Unlike = 2,
|
||||
}
|
||||
|
||||
export enum MessageFeedbackDetailType {
|
||||
UnlikeDefault = 0,
|
||||
UnlikeHarmful = 1, //有害信息
|
||||
UnlikeIncorrect = 2, //信息有误
|
||||
UnlikeNotFollowInstructions = 3, //未遵循指令
|
||||
UnlikeOthers = 4, //其他
|
||||
}
|
||||
|
||||
export interface MessageFeedback {
|
||||
feedback_type?: MessageFeedbackType; //反馈类型
|
||||
detail_types?: MessageFeedbackDetailType[]; //细分类型
|
||||
detail_content?: string; //负反馈自定义内容,对应用户选择Others
|
||||
}
|
||||
|
||||
export enum ReportMessageAction {
|
||||
Feedback = 0,
|
||||
Delete = 1,
|
||||
UpdataCard = 2,
|
||||
}
|
||||
|
||||
export interface ReportMessageProps {
|
||||
bot_id?: string; //bot_id
|
||||
biz_conversation_id: string; //会话ID
|
||||
message_id: string; //消息ID
|
||||
scene?: Scene; //当前会话所处场景
|
||||
action: ReportMessageAction; //动作
|
||||
message_feedback?: MessageFeedback;
|
||||
// 卡片状态
|
||||
attributes?: {
|
||||
card_status?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReportMessageResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
/* 消息点赞/点踩接口类型定义:end */
|
||||
|
||||
export type ChatASRProps = FormData;
|
||||
|
||||
export interface ChatASRResponse {
|
||||
code: number;
|
||||
data?: { text?: string };
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import {
|
||||
getUploader,
|
||||
type CozeUploader,
|
||||
type Config,
|
||||
} from '@coze-studio/uploader-adapter';
|
||||
import { safeAsyncThrow } from '@coze-common/chat-area-utils';
|
||||
|
||||
import { requestInstance } from '../../request-manager';
|
||||
import {
|
||||
type UploadPluginProps,
|
||||
type EventPayloadMaps,
|
||||
type FileType,
|
||||
type UploadPluginInterface,
|
||||
type GetUploadAuthTokenData,
|
||||
} from './types/plugin-upload';
|
||||
|
||||
const GET_AUTH_URL = '/api/playground/upload/auth_token';
|
||||
|
||||
type ChatCoreUploadPluginProps = Config & UploadPluginProps;
|
||||
|
||||
export class ChatCoreUploadPlugin implements UploadPluginInterface {
|
||||
private uploader: CozeUploader | null = null;
|
||||
|
||||
private eventBus = new EventEmitter();
|
||||
|
||||
private dataAuth: GetUploadAuthTokenData = {};
|
||||
|
||||
private uploaderConfig: ChatCoreUploadPluginProps;
|
||||
|
||||
constructor(props: ChatCoreUploadPluginProps) {
|
||||
this.uploaderConfig = props;
|
||||
this.initUploader();
|
||||
}
|
||||
|
||||
private async initUploader() {
|
||||
try {
|
||||
const dataAuth = await requestInstance.post(GET_AUTH_URL, {
|
||||
data: {
|
||||
// TODO: 确认参数是否要支持传入配置
|
||||
scene: 'bot_task',
|
||||
},
|
||||
});
|
||||
this.dataAuth = dataAuth.data || {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { service_id, upload_host, schema } = (this.dataAuth || {}) as any;
|
||||
this.uploader = getUploader(
|
||||
{
|
||||
schema,
|
||||
useFileExtension: true,
|
||||
// cp-disable-next-line
|
||||
imageHost: `https://${upload_host}`, //imageX上传必填
|
||||
imageConfig: {
|
||||
serviceId: service_id || '', // 在视频云中申请的服务id
|
||||
},
|
||||
objectConfig: {
|
||||
serviceId: service_id || '',
|
||||
},
|
||||
...this.uploaderConfig,
|
||||
},
|
||||
IS_OVERSEA,
|
||||
);
|
||||
|
||||
this.addFile(this.uploaderConfig.file, this.uploaderConfig.type);
|
||||
this.uploader.on('complete', info => {
|
||||
this.eventBus.emit('complete', info);
|
||||
});
|
||||
this.uploader.on('progress', info => {
|
||||
this.eventBus.emit('progress', info);
|
||||
});
|
||||
this.uploader.on('error', info => {
|
||||
this.eventBus.emit('error', info);
|
||||
});
|
||||
this.uploader.on('complete', info => {
|
||||
this.eventBus.emit('complete', info);
|
||||
});
|
||||
this.uploader.start();
|
||||
} catch (e) {
|
||||
safeAsyncThrow(
|
||||
`upload-plugin error: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private addFile(file: File, type: FileType) {
|
||||
this.uploader?.addFile({
|
||||
file,
|
||||
stsToken: {
|
||||
CurrentTime: this.dataAuth?.auth?.current_time || '',
|
||||
ExpiredTime: this.dataAuth?.auth?.expired_time || '',
|
||||
SessionToken: this.dataAuth?.auth?.session_token || '',
|
||||
AccessKeyId: this.dataAuth?.auth?.access_key_id || '',
|
||||
SecretAccessKey: this.dataAuth?.auth?.secret_access_key || '',
|
||||
},
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
on<T extends keyof EventPayloadMaps>(
|
||||
eventName: T,
|
||||
callback: (info: EventPayloadMaps[T]) => void,
|
||||
) {
|
||||
this.eventBus.on(eventName, callback);
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.uploader?.pause();
|
||||
return;
|
||||
}
|
||||
cancel() {
|
||||
this.uploader?.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type UploadEventName = 'complete' | 'error' | 'progress';
|
||||
export interface UploadResult {
|
||||
// 图片 & 文件 uri资源标识,用于换取url·
|
||||
|
||||
Uri?: string;
|
||||
// 图片 & 文件 url
|
||||
|
||||
Url?: string;
|
||||
// 图片宽度
|
||||
|
||||
ImageWidth?: number;
|
||||
// 图片高度
|
||||
|
||||
ImageHeight?: number;
|
||||
}
|
||||
|
||||
export type FileType = 'object' | 'image' | undefined;
|
||||
|
||||
export interface BaseEventInfo {
|
||||
type: 'success' | 'error'; // 当前任务状态,成功/失败
|
||||
// 当前状态的描述(随着生命周期不断变化)
|
||||
extra: {
|
||||
error?: unknown;
|
||||
errorCode?: number;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
export interface CompleteEventInfo extends BaseEventInfo {
|
||||
// 上传结果,注意对于不同 type 来说结构不一样,
|
||||
uploadResult: UploadResult;
|
||||
}
|
||||
export interface ProgressEventInfo extends BaseEventInfo {
|
||||
// 当前上传总体进度百分比(%)
|
||||
percent: number;
|
||||
}
|
||||
export interface EventPayloadMaps {
|
||||
complete: CompleteEventInfo;
|
||||
progress: ProgressEventInfo;
|
||||
error: BaseEventInfo;
|
||||
}
|
||||
|
||||
export interface UploadConfig {
|
||||
file: File;
|
||||
fileType: 'object' | 'image' | undefined;
|
||||
}
|
||||
|
||||
export interface UploadPluginProps {
|
||||
file: File;
|
||||
type: FileType;
|
||||
}
|
||||
|
||||
// 上传插件构造函数
|
||||
export interface UploadPluginConstructor<
|
||||
P extends Record<string, unknown> = Record<string, unknown>,
|
||||
> {
|
||||
new (props: UploadPluginProps & P): UploadPluginInterface;
|
||||
}
|
||||
|
||||
// 上传插件接口实现
|
||||
export interface UploadPluginInterface<
|
||||
M extends EventPayloadMaps = EventPayloadMaps,
|
||||
> {
|
||||
pause: () => void;
|
||||
cancel: () => void;
|
||||
on: <T extends keyof M>(eventName: T, callback: (info: M[T]) => void) => void;
|
||||
}
|
||||
export interface UploadAuthTokenInfo {
|
||||
access_key_id?: string;
|
||||
secret_access_key?: string;
|
||||
session_token?: string;
|
||||
expired_time?: string;
|
||||
current_time?: string;
|
||||
}
|
||||
export interface GetUploadAuthTokenData {
|
||||
service_id?: string;
|
||||
upload_path_prefix?: string;
|
||||
auth?: UploadAuthTokenInfo;
|
||||
upload_host?: string;
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
logger,
|
||||
reporter,
|
||||
type Logger,
|
||||
type Reporter,
|
||||
} from '@coze-arch/logger';
|
||||
|
||||
import { getSlardarEnv } from '../shared/utils/env';
|
||||
import { type DeployVersion, type ENV } from '../shared/const';
|
||||
import { slardarInstance, createSlardarConfig } from './slardar';
|
||||
import { LogOptionsHelper } from './log-options-helper';
|
||||
|
||||
// 获取 ReportLog 类的实例类型
|
||||
type ReportLogInstance = InstanceType<typeof ReportLog>;
|
||||
|
||||
// 获取 slardarTracer 方法的返回类型
|
||||
type SlardarTracerReturnType = ReturnType<ReportLogInstance['slardarTracer']>;
|
||||
|
||||
// 获取 trace 方法的类型
|
||||
export type Tracer = SlardarTracerReturnType['trace'];
|
||||
|
||||
export type ReportLogType = 'error' | 'info';
|
||||
/**
|
||||
* 日志上报
|
||||
*/
|
||||
|
||||
export interface ReportLogProps {
|
||||
// 后面等级上报,包含前面等级日志
|
||||
logLevel?: 'disable' | 'error' | 'info';
|
||||
env?: ENV;
|
||||
namespace?: string;
|
||||
scope?: string;
|
||||
meta?: Record<string, unknown>;
|
||||
deployVersion?: DeployVersion;
|
||||
}
|
||||
|
||||
const defaultReportLogProps: {
|
||||
env: ENV;
|
||||
logLevel: 'disable' | 'error' | 'info';
|
||||
deployVersion: DeployVersion;
|
||||
} = {
|
||||
env: 'production',
|
||||
deployVersion: 'release',
|
||||
logLevel: 'error',
|
||||
};
|
||||
|
||||
/**
|
||||
* namespace不可覆盖
|
||||
*/
|
||||
const unChangeProps = {
|
||||
namespace: 'chat-core',
|
||||
meta: {},
|
||||
};
|
||||
|
||||
export class ReportLog {
|
||||
ctx: LogOptionsHelper<ReportLogProps>;
|
||||
|
||||
private hasSlardarInitd = false;
|
||||
|
||||
private loggerWithBaseInfo!: Logger;
|
||||
|
||||
private reportLogWithBaseInfo!: Reporter;
|
||||
|
||||
constructor(props?: ReportLogProps) {
|
||||
const options = LogOptionsHelper.merge(props || {}, unChangeProps);
|
||||
this.ctx = new LogOptionsHelper(options);
|
||||
this.initLog(options);
|
||||
this.initReport(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实例初始化,所有 scope 也只初始化一次
|
||||
*/
|
||||
init() {
|
||||
console.log('debugger slardar instance init', this.hasSlardarInitd);
|
||||
if (this.hasSlardarInitd) {
|
||||
return;
|
||||
}
|
||||
this.hasSlardarInitd = true;
|
||||
const options = this.ctx.get();
|
||||
slardarInstance.init(
|
||||
createSlardarConfig({
|
||||
env: getSlardarEnv({
|
||||
env: options?.env || defaultReportLogProps.env,
|
||||
deployVersion:
|
||||
options?.deployVersion || defaultReportLogProps.deployVersion,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
slardarInstance.start();
|
||||
}
|
||||
|
||||
createLoggerWith(options?: ReportLogProps) {
|
||||
return new ReportLog(this.resolveCloneParams(options || {}));
|
||||
}
|
||||
|
||||
private resolveCloneParams(props: ReportLogProps) {
|
||||
return LogOptionsHelper.merge(this.ctx.get(), props);
|
||||
}
|
||||
|
||||
/**
|
||||
* slardar 初始化
|
||||
*/
|
||||
private initReport(options?: ReportLogProps) {
|
||||
this.reportLogWithBaseInfo = reporter.createReporterWithPreset(
|
||||
this.resolveCloneParams(options || {}),
|
||||
);
|
||||
this.reportLogWithBaseInfo.init(slardarInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制台日志初始化
|
||||
* @param options
|
||||
*/
|
||||
private initLog(options?: ReportLogProps) {
|
||||
this.loggerWithBaseInfo = logger.createLoggerWith({
|
||||
ctx: this.resolveCloneParams(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
// 判断是否需要上报
|
||||
private isNeedReport(logType: ReportLogType) {
|
||||
const { logLevel } = this.ctx.get();
|
||||
if (logLevel === 'disable') {
|
||||
return false;
|
||||
}
|
||||
if (logLevel === 'error') {
|
||||
return logType === 'error';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
info(...args: Parameters<typeof logger.info>) {
|
||||
if (!this.isNeedReport('info')) {
|
||||
return;
|
||||
}
|
||||
this.loggerWithBaseInfo.info(...args);
|
||||
}
|
||||
|
||||
error(...args: Parameters<typeof logger.error>) {
|
||||
if (!this.isNeedReport('error')) {
|
||||
return;
|
||||
}
|
||||
this.loggerWithBaseInfo.error(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* slardar日志 info层级
|
||||
*/
|
||||
slardarInfo(...args: Parameters<typeof reporter.info>) {
|
||||
this.reportLogWithBaseInfo.info(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* slardar日志 success级别
|
||||
* @param args
|
||||
*/
|
||||
slardarSuccess(...args: Parameters<typeof reporter.success>) {
|
||||
this.reportLogWithBaseInfo.success(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* slardar日志, error级别
|
||||
*/
|
||||
slardarError(...args: Parameters<typeof reporter.error>) {
|
||||
this.reportLogWithBaseInfo.error(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* slardar 自定义事件,用于事件统计
|
||||
*/
|
||||
slardarEvent(...args: Parameters<typeof reporter.event>) {
|
||||
this.reportLogWithBaseInfo.event(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* slardar 自定义成功事件,用于事件统计
|
||||
*/
|
||||
slardarSuccessEvent(...args: Parameters<typeof reporter.event>) {
|
||||
this.reportLogWithBaseInfo.successEvent(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* slardar 自定义错误事件,用于事件统计, 有错误堆栈信息
|
||||
*/
|
||||
slardarErrorEvent(...args: Parameters<typeof reporter.errorEvent>) {
|
||||
this.reportLogWithBaseInfo.errorEvent(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件追踪, 用于链路性能统计
|
||||
*/
|
||||
slardarTracer(...args: Parameters<typeof reporter.tracer>) {
|
||||
return this.reportLogWithBaseInfo.tracer(...args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import { type ReportLogProps } from './index';
|
||||
|
||||
function mergeLogOption<T extends ReportLogProps, P extends ReportLogProps>(
|
||||
source1: T,
|
||||
source2: P,
|
||||
) {
|
||||
const { meta: meta1, ...rest1 } = source1;
|
||||
const { meta: meta2, ...rest2 } = source2;
|
||||
|
||||
const meta = {
|
||||
...meta1,
|
||||
...meta2,
|
||||
};
|
||||
|
||||
const mergedOptions = {
|
||||
...rest1,
|
||||
...rest2,
|
||||
...(isEmpty(meta) ? {} : { meta }),
|
||||
};
|
||||
|
||||
return mergedOptions as T & P;
|
||||
}
|
||||
export class LogOptionsHelper<T extends ReportLogProps = ReportLogProps> {
|
||||
static merge<T extends ReportLogProps>(...list: ReportLogProps[]) {
|
||||
return list.filter(Boolean).reduce((r, c) => mergeLogOption(r, c), {}) as T;
|
||||
}
|
||||
|
||||
options: T;
|
||||
|
||||
constructor(options: T) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
createMinimalBrowserClient,
|
||||
jsErrorPlugin,
|
||||
customPlugin,
|
||||
} from '@coze-studio/slardar-adapter';
|
||||
|
||||
import { CHAT_CORE_VERSION } from '../../shared/const';
|
||||
interface SlardarConfig {
|
||||
env: string;
|
||||
}
|
||||
|
||||
export const slardarInstance = createMinimalBrowserClient();
|
||||
|
||||
export const createSlardarConfig = (defaultConfig: SlardarConfig): any => {
|
||||
const { env } = defaultConfig;
|
||||
return {
|
||||
bid: 'bot_studio_sdk',
|
||||
release: CHAT_CORE_VERSION,
|
||||
env,
|
||||
integrations: [jsErrorPlugin(), customPlugin()] as any,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosError, type AxiosResponse } from 'axios';
|
||||
|
||||
export class ApiError extends AxiosError {
|
||||
public raw?: unknown;
|
||||
type: string;
|
||||
|
||||
logId: string;
|
||||
|
||||
constructor(
|
||||
public code: string,
|
||||
public msg: string | undefined,
|
||||
response: AxiosResponse,
|
||||
) {
|
||||
super(msg, code, response.config, response.request, response);
|
||||
this.name = 'ApiError';
|
||||
this.type = 'Api Response Error';
|
||||
this.raw = response.data;
|
||||
this.logId = response.headers?.['x-tt-logid'];
|
||||
}
|
||||
}
|
||||
|
||||
export const isApiError = (error: unknown): error is ApiError =>
|
||||
error instanceof ApiError;
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { merge } from 'lodash-es';
|
||||
import axios, {
|
||||
type AxiosInstance,
|
||||
type AxiosResponse,
|
||||
type InternalAxiosRequestConfig,
|
||||
} from 'axios';
|
||||
|
||||
import {
|
||||
muteMergeWithArray,
|
||||
type PartiallyRequired,
|
||||
} from '../shared/utils/data-handler';
|
||||
import { type ReportLog } from '../report-log';
|
||||
import {
|
||||
type DefaultRequestManagerOptions,
|
||||
type RequestManagerOptions,
|
||||
type RequestScene,
|
||||
type SceneConfig,
|
||||
} from './types';
|
||||
import { getDefaultSceneConfig } from './request-config';
|
||||
|
||||
export interface RequestManagerProps {
|
||||
options?: RequestManagerOptions;
|
||||
reportLog: ReportLog;
|
||||
}
|
||||
export class RequestManager {
|
||||
private mergedBaseOptions: DefaultRequestManagerOptions;
|
||||
|
||||
private reportLog: ReportLog;
|
||||
|
||||
private reportLogWithScope: ReportLog;
|
||||
|
||||
request!: AxiosInstance;
|
||||
|
||||
constructor({ options, reportLog }: RequestManagerProps) {
|
||||
this.mergedBaseOptions = muteMergeWithArray(
|
||||
getDefaultSceneConfig(),
|
||||
options,
|
||||
);
|
||||
this.reportLog = reportLog;
|
||||
this.reportLogWithScope = this.reportLog.createLoggerWith({
|
||||
scope: 'RequestManager',
|
||||
});
|
||||
this.createRequest();
|
||||
}
|
||||
|
||||
createRequest() {
|
||||
this.reportLogWithScope.info({
|
||||
message: 'RequestManager is initialized',
|
||||
meta: {
|
||||
...this.mergedBaseOptions,
|
||||
},
|
||||
});
|
||||
const { baseURL, timeout, headers } = this.mergedBaseOptions;
|
||||
this.request = axios.create({
|
||||
baseURL,
|
||||
timeout,
|
||||
headers,
|
||||
});
|
||||
this.useRequestInterceptor();
|
||||
this.useResponseInterceptor();
|
||||
}
|
||||
|
||||
appendRequestOptions(options: RequestManagerOptions) {
|
||||
this.mergedBaseOptions = muteMergeWithArray(
|
||||
this.mergedBaseOptions,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 传入的request hooks, 可以对每个scene单独做拦截
|
||||
*/
|
||||
private useRequestInterceptor() {
|
||||
// 执行传入的统一的hooks
|
||||
const onCommonBeforeRequest = async (
|
||||
config: InternalAxiosRequestConfig,
|
||||
) => {
|
||||
const { hooks, scenes, ...rest } = this.mergedBaseOptions;
|
||||
if (!hooks) {
|
||||
return merge(config, rest);
|
||||
}
|
||||
const { onBeforeRequest = [] } = hooks;
|
||||
for (const hook of onBeforeRequest) {
|
||||
config = await hook(config);
|
||||
}
|
||||
return merge(config, rest);
|
||||
};
|
||||
// 执行每个scene的hooks
|
||||
const onSceneBeforeRequest = async (config: InternalAxiosRequestConfig) => {
|
||||
const { scenes } = this.mergedBaseOptions;
|
||||
if (!scenes) {
|
||||
return config;
|
||||
}
|
||||
const { url } = config;
|
||||
const targetScene = Object.values(scenes).find(v => v.url === url);
|
||||
if (!targetScene) {
|
||||
return config;
|
||||
}
|
||||
const { hooks, ...rest } = targetScene;
|
||||
if (!hooks) {
|
||||
return merge(config, rest);
|
||||
}
|
||||
const { onBeforeRequest = [] } = hooks;
|
||||
for (const hook of onBeforeRequest) {
|
||||
config = await hook(config);
|
||||
}
|
||||
|
||||
return merge({ ...rest }, config);
|
||||
};
|
||||
this.request.interceptors.request.use(async config => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 临时变量, 挺正常的
|
||||
const _config = await onCommonBeforeRequest(config);
|
||||
return await onSceneBeforeRequest(_config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 传入的response hooks, 可以对每个scene单独做拦截
|
||||
*/
|
||||
private useResponseInterceptor() {
|
||||
// 执行传入的统一的hooks
|
||||
const onCommonAfterResponse = async (
|
||||
response: AxiosResponse,
|
||||
hooksName: 'onAfterResponse' | 'onErrrorResponse' = 'onAfterResponse',
|
||||
): Promise<AxiosResponse> => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 临时变量, 挺正常的
|
||||
let _response: AxiosResponse | Promise<AxiosResponse> = response;
|
||||
const { hooks } = this.mergedBaseOptions;
|
||||
if (!hooks) {
|
||||
return response;
|
||||
}
|
||||
const onAfterResponse = hooks[hooksName] || [];
|
||||
for (const hook of onAfterResponse) {
|
||||
_response = await hook(response);
|
||||
}
|
||||
return _response;
|
||||
};
|
||||
// 执行每个scene的hooks
|
||||
const onSceneAfterResponse = async (
|
||||
response: AxiosResponse,
|
||||
hooksName: 'onAfterResponse' | 'onErrrorResponse' = 'onAfterResponse',
|
||||
): Promise<AxiosResponse> => {
|
||||
const { scenes } = this.mergedBaseOptions;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 临时变量, 挺正常的
|
||||
let _response: AxiosResponse | Promise<AxiosResponse> = response;
|
||||
if (!scenes) {
|
||||
return response;
|
||||
}
|
||||
const { url } = response.config;
|
||||
const targetScene = Object.values(scenes).find(v => v.url === url);
|
||||
if (!targetScene) {
|
||||
return response;
|
||||
}
|
||||
const { hooks } = targetScene;
|
||||
if (!hooks) {
|
||||
return response;
|
||||
}
|
||||
const onAfterResponse = hooks[hooksName] || [];
|
||||
for (const hook of onAfterResponse) {
|
||||
_response = await hook(response);
|
||||
}
|
||||
|
||||
return _response;
|
||||
};
|
||||
this.request.interceptors.response.use(
|
||||
async response => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 临时变量, 挺正常的
|
||||
const _response = await onCommonAfterResponse(response);
|
||||
return await onSceneAfterResponse(_response);
|
||||
},
|
||||
async response => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 临时变量, 挺正常的
|
||||
const _response = await onCommonAfterResponse(
|
||||
response,
|
||||
'onErrrorResponse',
|
||||
);
|
||||
return await onSceneAfterResponse(_response, 'onErrrorResponse');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置信息
|
||||
*/
|
||||
getSceneConfig(scene: RequestScene): PartiallyRequired<SceneConfig, 'url'> {
|
||||
const { hooks, scenes, ...rest } = this.mergedBaseOptions;
|
||||
return merge(rest, scenes[scene]);
|
||||
}
|
||||
}
|
||||
|
||||
export const requestInstance = axios.create();
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type AxiosResponse, type InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
import { type DefaultRequestManagerOptions, RequestScene } from './types';
|
||||
import { ApiError } from './api-error';
|
||||
|
||||
const useApiErrorResponseHook = (response: AxiosResponse) => {
|
||||
const { data = {} } = response;
|
||||
const { code, msg } = data;
|
||||
if (code !== 0) {
|
||||
const apiError = new ApiError(String(code), msg, response);
|
||||
|
||||
return Promise.reject(apiError);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const useCsrfRequestHook = (config: InternalAxiosRequestConfig) => {
|
||||
config.headers.set('x-requested-with', 'XMLHttpRequest');
|
||||
if (
|
||||
config.method?.toLowerCase() === 'post' &&
|
||||
!config.headers.get('content-type')
|
||||
) {
|
||||
// 新的 csrf 防护需要 post 请求全部带上这个 header
|
||||
config.headers.set('content-type', 'application/json');
|
||||
if (!config.data) {
|
||||
// axios 会自动在 data 为空时清除 content-type,所以需要设置一个空对象
|
||||
config.data = {};
|
||||
}
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
export const getDefaultSceneConfig = (): DefaultRequestManagerOptions => ({
|
||||
hooks: {
|
||||
onBeforeRequest: [useCsrfRequestHook],
|
||||
onAfterResponse: [useApiErrorResponseHook],
|
||||
},
|
||||
scenes: {
|
||||
[RequestScene.SendMessage]: {
|
||||
url: '/api/conversation/chat',
|
||||
method: 'POST',
|
||||
},
|
||||
[RequestScene.ResumeMessage]: {
|
||||
url: '/api/conversation/resume_chat',
|
||||
method: 'POST',
|
||||
},
|
||||
[RequestScene.GetMessage]: {
|
||||
url: '/api/conversation/get_message_list',
|
||||
method: 'POST',
|
||||
},
|
||||
[RequestScene.ClearHistory]: {
|
||||
url: '/api/conversation/clear_message',
|
||||
method: 'POST',
|
||||
},
|
||||
[RequestScene.ClearMessageContext]: {
|
||||
url: '/api/conversation/create_section',
|
||||
method: 'POST',
|
||||
},
|
||||
[RequestScene.DeleteMessage]: {
|
||||
url: '/api/conversation/delete_message',
|
||||
method: 'POST',
|
||||
},
|
||||
[RequestScene.BreakMessage]: {
|
||||
url: '/api/conversation/break_message',
|
||||
method: 'POST',
|
||||
},
|
||||
[RequestScene.ReportMessage]: {
|
||||
url: '/api/conversation/message/report',
|
||||
method: 'POST',
|
||||
},
|
||||
[RequestScene.ChatASR]: {
|
||||
url: '/api/audio/transcriptions',
|
||||
method: 'POST',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type InternalAxiosRequestConfig,
|
||||
type AxiosResponse,
|
||||
type AxiosRequestConfig,
|
||||
} from 'axios';
|
||||
import { type FetchSteamConfig } from '@coze-arch/fetch-stream';
|
||||
|
||||
import { type ParsedEvent } from '@/channel/http-chunk/types';
|
||||
|
||||
import { type PartiallyRequired } from '../shared/utils/data-handler';
|
||||
|
||||
export type RequestManagerOptions = {
|
||||
scenes?: {
|
||||
[key in RequestScene]?: SceneConfig;
|
||||
};
|
||||
hooks?: Hooks;
|
||||
} & AxiosRequestConfig;
|
||||
|
||||
export type DefaultRequestManagerOptions = {
|
||||
scenes: {
|
||||
[key in RequestScene]: PartiallyRequired<SceneConfig, 'url'>;
|
||||
};
|
||||
hooks: Hooks;
|
||||
} & AxiosRequestConfig;
|
||||
|
||||
interface InternalChannelSendMessageConfig {
|
||||
url: string;
|
||||
method: string;
|
||||
headers: [string, string][];
|
||||
body: string;
|
||||
}
|
||||
|
||||
interface Hooks {
|
||||
onBeforeRequest?: Array<
|
||||
(
|
||||
requestConfig: InternalAxiosRequestConfig,
|
||||
) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>
|
||||
>;
|
||||
onAfterResponse?: Array<
|
||||
(response: AxiosResponse) => AxiosResponse | Promise<AxiosResponse>
|
||||
>;
|
||||
onBeforeSendMessage?: Array<
|
||||
(
|
||||
sendMessageConfig: InternalChannelSendMessageConfig,
|
||||
) =>
|
||||
| InternalChannelSendMessageConfig
|
||||
| Promise<InternalChannelSendMessageConfig>
|
||||
>;
|
||||
//为何这样,是由于OpenSdk 与 CozeSdk消息差异过大,缺少了Ack消息,需要构造出来。
|
||||
onGetMessageStreamParser?: (
|
||||
requestMessageRawBody: Record<string, unknown>,
|
||||
) => FetchSteamConfig<ParsedEvent>['streamParser'];
|
||||
onErrrorResponse?: Array<(response: AxiosResponse) => Promise<AxiosResponse>>;
|
||||
}
|
||||
|
||||
export enum RequestScene {
|
||||
SendMessage = 'sendMessage',
|
||||
ResumeMessage = 'resumeMessage',
|
||||
GetMessage = 'getMessage',
|
||||
ClearHistory = 'clearHistory',
|
||||
ClearMessageContext = 'clearMessageContext',
|
||||
DeleteMessage = 'deleteMessage',
|
||||
BreakMessage = 'breakMessage',
|
||||
ReportMessage = 'reportMessage',
|
||||
ChatASR = 'chatASR',
|
||||
}
|
||||
|
||||
export type SceneConfig = {
|
||||
hooks?: Hooks;
|
||||
} & AxiosRequestConfig;
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* sdk版本号
|
||||
*/
|
||||
export const CHAT_CORE_VERSION = '1.1.0';
|
||||
|
||||
/**
|
||||
* 使用环境
|
||||
*/
|
||||
export type ENV = 'local' | 'boe' | 'production' | 'thirdPart';
|
||||
|
||||
/**
|
||||
* 部署版本
|
||||
* release: 正式版
|
||||
* inhouse: 内部测试版本
|
||||
*/
|
||||
|
||||
export type DeployVersion = 'release' | 'inhouse';
|
||||
|
||||
// 1min -> 60s
|
||||
export const SECONDS_PER_MINUTE = 60;
|
||||
|
||||
// 1s -> 1000ms
|
||||
export const SECONDS_PER_SECOND = 1000;
|
||||
|
||||
// 1min -> 60*1000ms
|
||||
export const MILLISECONDS_PER_MINUTE = SECONDS_PER_MINUTE * SECONDS_PER_SECOND;
|
||||
|
||||
// 拉流超时
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- 5min更语义化
|
||||
export const BETWEEN_CHUNK_TIMEOUT = 5 * MILLISECONDS_PER_MINUTE;
|
||||
|
||||
// 发送消息超时
|
||||
export const SEND_MESSAGE_TIMEOUT = MILLISECONDS_PER_MINUTE;
|
||||
|
||||
const MAX_RANDOM_NUMBER = 0x10000000;
|
||||
|
||||
function getRandomDeviceID() {
|
||||
return Math.abs(Date.now() ^ (Math.random() * MAX_RANDOM_NUMBER));
|
||||
}
|
||||
|
||||
export const randomDeviceID = getRandomDeviceID();
|
||||
|
||||
// ws 最大重试次数
|
||||
export const WS_MAX_RETRY_COUNT = 10;
|
||||
|
||||
export {
|
||||
FileTypeEnum,
|
||||
FileType,
|
||||
TFileTypeConfig,
|
||||
FILE_TYPE_CONFIG,
|
||||
getFileInfo,
|
||||
} from '@coze-studio/file-kit/logic';
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { pickBy, type merge, mergeWith, isArray } from 'lodash-es';
|
||||
|
||||
export const filterEmptyField = <T extends Record<string, unknown>>(
|
||||
obj: T,
|
||||
): T =>
|
||||
pickBy(
|
||||
obj,
|
||||
value => value !== undefined && value !== null && value !== '',
|
||||
) as T;
|
||||
|
||||
export type PartiallyRequired<T, K extends keyof T> = Omit<T, K> &
|
||||
Required<Pick<T, K>>;
|
||||
|
||||
// enum转换为联合类型
|
||||
export type EnumToUnion<T extends Record<string, string>> = T[keyof T];
|
||||
|
||||
export const muteMergeWithArray = (...args: Parameters<typeof merge>) =>
|
||||
mergeWith(...args, (objValue: unknown, srcValue: unknown) => {
|
||||
if (isArray(objValue)) {
|
||||
return objValue.concat(srcValue);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ENV, type DeployVersion } from '../const';
|
||||
|
||||
/**
|
||||
* 获取 slardar 上报环境
|
||||
* 不同环境之间数据隔离
|
||||
* @returns
|
||||
*/
|
||||
export const getSlardarEnv = ({
|
||||
env,
|
||||
deployVersion,
|
||||
}: {
|
||||
env: ENV;
|
||||
deployVersion: DeployVersion;
|
||||
}) => [deployVersion, env].join('-');
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const safeJSONParse = <T = unknown>(
|
||||
value: string,
|
||||
fallback: T | null,
|
||||
):
|
||||
| {
|
||||
parseSuccess: true;
|
||||
useFallback: false;
|
||||
value: T;
|
||||
}
|
||||
| {
|
||||
parseSuccess: false;
|
||||
useFallback: true;
|
||||
value: T;
|
||||
}
|
||||
| {
|
||||
parseSuccess: false;
|
||||
useFallback: false;
|
||||
value: null;
|
||||
} => {
|
||||
try {
|
||||
return {
|
||||
parseSuccess: true,
|
||||
value: JSON.parse(value),
|
||||
useFallback: false,
|
||||
};
|
||||
} catch (error) {
|
||||
if (fallback !== null) {
|
||||
return {
|
||||
parseSuccess: false,
|
||||
useFallback: true,
|
||||
value: fallback,
|
||||
};
|
||||
}
|
||||
return {
|
||||
parseSuccess: false,
|
||||
useFallback: false,
|
||||
value: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
17
frontend/packages/common/chat-area/chat-core/src/typing/type.d.ts
vendored
Normal file
17
frontend/packages/common/chat-area/chat-core/src/typing/type.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare module '@coze-arch/vitest-config';
|
||||
Reference in New Issue
Block a user