feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -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';

View File

@@ -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',
}

View File

@@ -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',
}

View File

@@ -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);
});
};
}

View File

@@ -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;
}

View File

@@ -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,
};
};

View File

@@ -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',
}

View File

@@ -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,
});
}
}

View File

@@ -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);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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);
},
});
};
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 messagestashed 不应存入全量 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);
},
);
});
}
}

View File

@@ -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,
}

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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: 补充刷新机制
}

View File

@@ -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,
};
};
}

View 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' />

View 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';

View File

@@ -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;
}
}

View File

@@ -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',
}

View File

@@ -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';

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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_type1-预设任务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; // 回复 idquery的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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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,
};
};

View File

@@ -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;

View File

@@ -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();

View File

@@ -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',
},
},
});

View File

@@ -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;

View File

@@ -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';

View File

@@ -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);
}
});

View File

@@ -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('-');

View File

@@ -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,
};
}
};

View 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';