Files
EasyFlow/easyflow-ui-usercenter/packages/utils/src/helpers/chat-time.ts
陈子默 72df00f25b feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能
- 增加智能体聊天,并对接持久化
2026-05-25 11:42:48 +08:00

1112 lines
29 KiB
TypeScript

import type {
ChatTimeAssistantItem,
ChatTimeHistoryRecord,
ChatTimeRoundMeta,
ChatTimeThinkingStatus,
ChatTimeTimelineItem,
ChatTimeTimelineItemBase,
ChatTimeToolItem,
ChatTimeToolMutationPayload,
ChatTimeToolStatus,
} from '../../../types/src/chat-time';
import {uuid} from './uuid';
type ChatTimeToolMeta = {
arguments?: string;
name?: string;
};
/**
* 聊天时间线实时构建器。
*/
class ChatTimeTimelineBuilder {
/**
* 追加用户消息。
*/
static appendUserMessage(
items: ChatTimeTimelineItem[],
payload: {
content?: string;
created?: number | string;
id?: string;
messageKind?: string;
roundId?: number | string;
roundNo?: number;
senderName?: string;
},
) {
const item: ChatTimeTimelineItem = {
content: normalizePlainText(payload.content),
created: normalizeTimestamp(payload.created),
id: payload.id || uuid(),
placement: 'end',
role: 'user',
senderName: payload.senderName,
};
applyRoundMeta(item, payload);
items.push(item);
}
/**
* 将最新一条待绑定的用户消息补齐到当前轮次。
*/
static bindLatestPendingUserMessage(
items: ChatTimeTimelineItem[],
meta?: ChatTimeRoundMeta,
) {
const roundId = normalizeRoundId(meta?.roundId);
if (!roundId) {
return;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item) {
continue;
}
if (item.role !== 'user') {
continue;
}
if (item.roundId) {
return;
}
applyRoundMeta(item, {
roundId,
roundNo: meta?.roundNo,
});
return;
}
}
/**
* 更新指定轮次的可切换状态。
*/
static setRoundSwitchable(
items: ChatTimeTimelineItem[],
roundId: number | string | undefined,
switchable: boolean,
) {
const normalizedRoundId = normalizeRoundId(roundId);
if (!normalizedRoundId) {
return;
}
for (const item of items) {
if (item.roundId === normalizedRoundId && item.role !== 'user') {
item.switchable = switchable;
}
}
}
/**
* 追加思考增量。
*/
static appendThinkingDelta(
items: ChatTimeTimelineItem[],
delta?: string,
created?: number | string,
meta?: ChatTimeRoundMeta,
) {
const normalizedDelta = normalizePlainText(delta);
if (!normalizedDelta) {
return;
}
prepareRoundVariant(items, meta);
const assistant = ensureAssistantTail(items, created, meta);
const tail = assistant.segments[assistant.segments.length - 1];
if (tail?.type === 'thinking' && tail.status === 'thinking') {
tail.content += normalizedDelta;
} else {
assistant.segments.push({
content: normalizedDelta,
expanded: false,
id: uuid(),
status: 'thinking',
type: 'thinking',
});
}
assistant.loading = true;
assistant.typing = true;
}
/**
* 追加回答增量。
*/
static appendMessageDelta(
items: ChatTimeTimelineItem[],
delta?: string,
created?: number | string,
meta?: ChatTimeRoundMeta,
) {
const normalizedDelta = normalizeAssistantText(delta);
if (!normalizedDelta) {
return;
}
prepareRoundVariant(items, meta);
const assistant = ensureAssistantTail(items, created, meta);
stopThinkingForAssistant(assistant);
const tail = assistant.segments[assistant.segments.length - 1];
if (tail?.type === 'text') {
tail.content += normalizedDelta;
} else {
assistant.segments.push({
content: normalizedDelta,
id: uuid(),
type: 'text',
});
}
assistant.content += normalizedDelta;
assistant.loading = false;
assistant.typing = true;
}
/**
* 用最终完整回答替换当前 assistant 文本。
*/
static replaceMessageContent(
items: ChatTimeTimelineItem[],
content?: string,
created?: number | string,
meta?: ChatTimeRoundMeta,
) {
const normalizedContent = normalizeAssistantText(content);
if (!normalizedContent) {
return;
}
prepareRoundVariant(items, meta);
const assistant = ensureAssistantTail(items, created, meta);
stopThinkingForAssistant(assistant);
assistant.content = normalizedContent;
assistant.segments = [
...assistant.segments.filter((segment) => segment.type !== 'text'),
{
content: normalizedContent,
id: uuid(),
type: 'text' as const,
},
];
assistant.loading = false;
assistant.typing = false;
}
/**
* 停止当前 assistant 的思考态。
*/
static stopThinking(items: ChatTimeTimelineItem[]) {
const last = items[items.length - 1];
if (!isAssistantItem(last)) {
return;
}
stopThinkingForAssistant(last);
}
/**
* 更新工具调用状态。
*/
static upsertToolCall(
items: ChatTimeTimelineItem[],
payload: ChatTimeToolMutationPayload,
) {
prepareRoundVariant(items, payload);
this.stopThinking(items);
const toolItem = ensureToolItem(
items,
payload.toolCallId,
payload.created,
payload.name,
payload,
);
toolItem.arguments = normalizePayloadValue(payload.value);
toolItem.content = '';
toolItem.status = 'TOOL_CALL';
}
/**
* 更新工具结果状态。
*/
static upsertToolResult(
items: ChatTimeTimelineItem[],
payload: ChatTimeToolMutationPayload,
) {
prepareRoundVariant(items, payload);
const toolItem = ensureToolItem(
items,
payload.toolCallId,
payload.created,
payload.name,
payload,
);
toolItem.result = normalizePayloadValue(payload.result);
toolItem.content = toolItem.result;
toolItem.status = 'TOOL_RESULT';
}
/**
* 追加系统错误。
*/
static applySystemError(
items: ChatTimeTimelineItem[],
message?: string,
created?: number | string,
) {
const errorMessage = normalizePlainText(message);
if (!errorMessage) {
return;
}
const last = items[items.length - 1];
if (isAssistantItem(last)) {
stopThinkingForAssistant(last);
appendAssistantText(last, errorMessage);
last.loading = false;
last.typing = false;
return;
}
const assistant = createAssistantItem(created);
appendAssistantText(assistant, errorMessage);
assistant.loading = false;
assistant.typing = false;
items.push(assistant);
}
/**
* 结束当前轮的 assistant 状态。
*/
static finalize(items: ChatTimeTimelineItem[]) {
const last = findLastAssistant(items);
if (!isAssistantItem(last)) {
return;
}
stopThinkingForAssistant(last);
last.loading = false;
last.typing = false;
}
/**
* 按轮次替换当前主线可见的 assistant/tool 片段。
*/
static replaceRoundMessages(
items: ChatTimeTimelineItem[],
roundId: number | string | undefined,
nextMessages: ChatTimeTimelineItem[],
) {
const normalizedRoundId = normalizeRoundId(roundId);
if (!normalizedRoundId) {
return;
}
const range = resolveRoundReplaceRange(items, normalizedRoundId);
if (range) {
items.splice(range.start, range.deleteCount, ...nextMessages);
return;
}
items.splice(resolveRoundInsertIndex(items, normalizedRoundId), 0, ...nextMessages);
}
}
/**
* 聊天时间线历史映射器。
*/
class ChatTimeHistoryMapper {
/**
* 从聊天历史记录恢复时间线。
*/
static fromHistoryRecords(records: ChatTimeHistoryRecord[]) {
return normalizeVisibleHistoryRecords(records).flatMap((record) =>
this.fromHistoryRecord(record),
);
}
/**
* 从旧版消息列表恢复时间线。
*/
static fromLegacyMessages(records: ChatTimeHistoryRecord[]) {
return records.flatMap((record) => this.fromLegacyRecord(record));
}
private static fromHistoryRecord(record: ChatTimeHistoryRecord) {
const role = normalizeRole(record.senderRole || record.role);
if (role === 'user') {
return [createUserItem(record)];
}
if (role === 'tool') {
return [createToolItemFromTopLevelRecord(record)];
}
if (role !== 'assistant') {
return [];
}
const payload = toObjectRecord(record.contentPayload);
const messageChain = toObjectArray(payload.messageChain);
if (messageChain.length > 0) {
const structuredItems = this.fromStructuredAssistantRecord(
record,
messageChain,
);
if (structuredItems.length > 0) {
return structuredItems;
}
}
return this.fromLegacyRecord(record);
}
private static fromLegacyRecord(record: ChatTimeHistoryRecord) {
const role = normalizeRole(record.senderRole || record.role);
if (role === 'user') {
return [createUserItem(record)];
}
if (role === 'tool') {
return [createToolItemFromTopLevelRecord(record)];
}
if (role !== 'assistant') {
return [];
}
const content = normalizeAssistantText(record.contentText || record.content);
const chains = extractDisplayChains(record);
const assistant = createAssistantItem(record.created, {
id: record.id == null ? undefined : String(record.id),
loading: record.loading,
messageKind: record.messageKind,
roundId: normalizeRoundId(record.roundId),
roundNo: record.roundNo,
selectedVariantIndex: record.selectedVariantIndex,
senderName: record.senderName,
switchable: record.switchable,
typing: record.typing,
variantCount: record.variantCount,
variantIndex: record.variantIndex,
});
const tools: ChatTimeTimelineItem[] = [];
for (const rawChain of chains) {
const reasoning = normalizePlainText(rawChain.reasoning_content);
if (reasoning) {
assistant.segments.push({
content: reasoning,
expanded: Boolean(rawChain.thinkingExpanded),
id: uuid(),
status: normalizeThinkingStatus(rawChain.thinkingStatus),
type: 'thinking',
});
continue;
}
const toolItem = createToolItemFromChain(rawChain, record.created, record);
if (toolItem) {
tools.push(toolItem);
}
}
if (content) {
appendAssistantText(assistant, content);
}
const results: ChatTimeTimelineItem[] = [];
if (assistant.segments.length > 0) {
assistant.loading = false;
assistant.typing = false;
results.push(assistant);
}
return [...results, ...tools];
}
private static fromStructuredAssistantRecord(
record: ChatTimeHistoryRecord,
messageChain: Array<Record<string, any>>,
) {
const toolMetaMap = new Map<string, ChatTimeToolMeta>();
const items: ChatTimeTimelineItem[] = [];
let assistantIndex = 0;
for (const rawMessage of messageChain) {
const role = normalizeRole(rawMessage.role);
if (role === 'assistant') {
collectToolMeta(rawMessage, toolMetaMap);
const assistant = createAssistantItemFromStructuredMessage(
rawMessage,
record,
assistantIndex,
);
if (assistant) {
items.push(assistant);
assistantIndex += 1;
}
continue;
}
if (role === 'tool') {
items.push(
createToolItemFromStructuredMessage(
rawMessage,
toolMetaMap,
record.created,
record,
),
);
}
}
return items;
}
}
function normalizeVisibleHistoryRecords(records: ChatTimeHistoryRecord[]) {
const dedupedRecords = dedupeHistoryRecords(records);
const userSelectedVariantByRound = new Map<string, number>();
const assistantSelectedVariantByRound = new Map<string, number>();
const fallbackVariantByRound = new Map<string, number>();
for (const record of dedupedRecords) {
const roundId = normalizeRoundId(record.roundId);
if (!roundId) {
continue;
}
const selectedVariantIndex = normalizePositiveInteger(
record.selectedVariantIndex,
);
if (selectedVariantIndex) {
if (isUserHistoryRecord(record)) {
userSelectedVariantByRound.set(roundId, selectedVariantIndex);
} else {
assistantSelectedVariantByRound.set(roundId, selectedVariantIndex);
}
}
const variantIndex = normalizePositiveInteger(record.variantIndex);
if (!isUserHistoryRecord(record) && variantIndex) {
fallbackVariantByRound.set(roundId, variantIndex);
}
}
return dedupedRecords.filter((record) => {
const roundId = normalizeRoundId(record.roundId);
if (!roundId || isUserHistoryRecord(record)) {
return true;
}
const variantIndex = normalizePositiveInteger(record.variantIndex);
if (!variantIndex) {
return true;
}
const selectedVariantIndex =
userSelectedVariantByRound.get(roundId) ||
assistantSelectedVariantByRound.get(roundId) ||
fallbackVariantByRound.get(roundId);
return !selectedVariantIndex || variantIndex === selectedVariantIndex;
});
}
function dedupeHistoryRecords(records: ChatTimeHistoryRecord[]) {
const seen = new Set<string>();
const result: ChatTimeHistoryRecord[] = [];
for (const record of records) {
const key = resolveHistoryRecordKey(record);
if (seen.has(key)) {
continue;
}
seen.add(key);
result.push(record);
}
return result;
}
function resolveHistoryRecordKey(record: ChatTimeHistoryRecord) {
if (record.id != null) {
return `id:${String(record.id)}`;
}
return [
'fallback',
normalizeRoundId(record.roundId) || '',
normalizeRole(record.senderRole || record.role),
normalizePositiveInteger(record.variantIndex) || '',
normalizePlainText(record.contentText || record.content),
].join(':');
}
function isUserHistoryRecord(record: ChatTimeHistoryRecord) {
return normalizeRole(record.senderRole || record.role) === 'user';
}
function createAssistantItem(
created?: number | string,
patch?: Omit<Partial<ChatTimeAssistantItem>, 'roundId'> & ChatTimeRoundMeta,
): ChatTimeAssistantItem {
const item: ChatTimeAssistantItem = {
content: patch?.content || '',
created: normalizeTimestamp(created),
id: patch?.id || uuid(),
loading: patch?.loading,
placement: 'start',
role: 'assistant',
segments: patch?.segments ? [...patch.segments] : [],
senderName: patch?.senderName,
typing: patch?.typing,
};
applyRoundMeta(item, patch);
return item;
}
function createAssistantItemFromStructuredMessage(
rawMessage: Record<string, any>,
record: ChatTimeHistoryRecord,
assistantIndex: number,
) {
const content = normalizeAssistantText(rawMessage.content);
const reasoning = normalizePlainText(
rawMessage.reasoningContent ?? rawMessage.reasoning_content,
);
if (!content && !reasoning) {
return null;
}
const assistant = createAssistantItem(record.created, {
id:
record.id == null
? undefined
: `${String(record.id)}-assistant-${assistantIndex}`,
loading: false,
messageKind: record.messageKind,
roundId: normalizeRoundId(record.roundId),
roundNo: record.roundNo,
selectedVariantIndex: record.selectedVariantIndex,
senderName: record.senderName,
switchable: record.switchable,
typing: false,
variantCount: record.variantCount,
variantIndex: record.variantIndex,
});
if (reasoning) {
assistant.segments.push({
content: reasoning,
expanded: false,
id: uuid(),
status: 'end',
type: 'thinking',
});
}
if (content) {
appendAssistantText(assistant, content);
}
return assistant;
}
function createToolItemFromChain(
rawChain: Record<string, any>,
created?: number | string,
record?: ChatTimeHistoryRecord,
) {
const toolCallId = normalizePlainText(rawChain.id);
const name = normalizePlainText(rawChain.name);
const argumentsValue = normalizePayloadValue(rawChain.arguments ?? rawChain.result);
const status = normalizeToolStatus(rawChain.status);
if (!toolCallId && !name && !argumentsValue) {
return null;
}
return createToolItem({
arguments: status === 'TOOL_CALL' ? argumentsValue : undefined,
created,
id: toolCallId || uuid(),
messageKind: record?.messageKind,
name,
roundId: record?.roundId,
roundNo: record?.roundNo,
result: status === 'TOOL_RESULT' ? argumentsValue : undefined,
selectedVariantIndex: record?.selectedVariantIndex,
status,
switchable: record?.switchable,
toolCallId,
variantCount: record?.variantCount,
variantIndex: record?.variantIndex,
});
}
function createToolItemFromStructuredMessage(
rawMessage: Record<string, any>,
toolMetaMap: Map<string, ChatTimeToolMeta>,
created?: number | string,
record?: ChatTimeHistoryRecord,
) {
const toolCallId = normalizePlainText(
rawMessage.toolCallId ?? rawMessage.tool_call_id,
);
const toolMeta = toolMetaMap.get(toolCallId);
const result = normalizePayloadValue(rawMessage.content);
return createToolItem({
arguments: toolMeta?.arguments,
created,
id: toolCallId || uuid(),
messageKind: record?.messageKind,
name: toolMeta?.name,
roundId: record?.roundId,
roundNo: record?.roundNo,
result,
selectedVariantIndex: record?.selectedVariantIndex,
status: 'TOOL_RESULT',
switchable: record?.switchable,
toolCallId,
variantCount: record?.variantCount,
variantIndex: record?.variantIndex,
});
}
function createToolItemFromTopLevelRecord(record: ChatTimeHistoryRecord) {
const payload = toObjectRecord(record.contentPayload);
const toolCallId = normalizePlainText(
payload.toolCallId ?? payload.tool_call_id ?? record.id,
);
return createToolItem({
created: record.created,
id: record.id == null ? toolCallId || uuid() : String(record.id),
messageKind: record.messageKind,
name: normalizePlainText(payload.name),
roundId: record.roundId,
roundNo: record.roundNo,
result: normalizePayloadValue(
payload.content ?? payload.result ?? record.contentText ?? record.content,
),
selectedVariantIndex: record.selectedVariantIndex,
status: 'TOOL_RESULT',
switchable: record.switchable,
toolCallId,
variantCount: record.variantCount,
variantIndex: record.variantIndex,
});
}
function createToolItem(payload: {
arguments?: string;
created?: number | string;
id?: string;
messageKind?: string;
name?: string;
roundId?: number | string;
roundNo?: number;
result?: string;
selectedVariantIndex?: number;
status: ChatTimeToolStatus;
switchable?: boolean;
toolCallId?: string;
variantCount?: number;
variantIndex?: number;
}): ChatTimeToolItem {
const item: ChatTimeToolItem = {
arguments: payload.arguments,
content: payload.result || '',
created: normalizeTimestamp(payload.created),
id: payload.id || payload.toolCallId || uuid(),
name: payload.name || '',
placement: 'start',
result: payload.result,
role: 'tool',
status: payload.status,
toolCallId: payload.toolCallId || payload.id || uuid(),
};
applyRoundMeta(item, payload);
return item;
}
function createUserItem(record: ChatTimeHistoryRecord): ChatTimeTimelineItem {
const item: ChatTimeTimelineItem = {
content: normalizePlainText(record.contentText || record.content),
created: normalizeTimestamp(record.created),
id: record.id == null ? uuid() : String(record.id),
loading: record.loading,
placement: record.placement || 'end',
role: 'user',
senderName: record.senderName,
typing: record.typing,
};
applyRoundMeta(item, record);
return item;
}
function appendAssistantText(item: ChatTimeAssistantItem, content: string) {
const normalized = normalizeAssistantText(content);
if (!normalized) {
return;
}
item.content += normalized;
item.segments.push({
content: normalized,
id: uuid(),
type: 'text',
});
}
function collectToolMeta(
rawMessage: Record<string, any>,
toolMetaMap: Map<string, ChatTimeToolMeta>,
) {
const toolCalls = toObjectArray(rawMessage.toolCalls ?? rawMessage.tool_calls);
for (const toolCall of toolCalls) {
const toolCallId = normalizePlainText(toolCall.id);
if (!toolCallId) {
continue;
}
toolMetaMap.set(toolCallId, {
arguments: normalizePayloadValue(toolCall.arguments),
name: normalizePlainText(toolCall.name),
});
}
}
function ensureAssistantTail(
items: ChatTimeTimelineItem[],
created?: number | string,
meta?: ChatTimeRoundMeta,
) {
const last = items[items.length - 1];
if (isAssistantItem(last) && isSameRoundVariant(last, meta)) {
applyRoundMeta(last, meta);
return last;
}
const assistant = createAssistantItem(created, {
loading: true,
typing: true,
...normalizeRoundMeta(meta),
});
items.push(assistant);
return assistant;
}
function ensureToolItem(
items: ChatTimeTimelineItem[],
toolCallId?: string,
created?: number | string,
name?: string,
meta?: ChatTimeRoundMeta,
) {
const normalizedToolCallId = normalizePlainText(toolCallId);
const found = findToolItem(items, normalizedToolCallId, meta);
if (found) {
if (name) {
found.name = name;
}
applyRoundMeta(found, meta);
return found;
}
const toolItem = createToolItem({
created,
id: normalizedToolCallId || uuid(),
messageKind: meta?.messageKind,
name,
roundId: meta?.roundId,
roundNo: meta?.roundNo,
selectedVariantIndex: meta?.selectedVariantIndex,
status: 'TOOL_CALL',
switchable: meta?.switchable,
toolCallId: normalizedToolCallId,
variantCount: meta?.variantCount,
variantIndex: meta?.variantIndex,
});
items.push(toolItem);
return toolItem;
}
function findToolItem(
items: ChatTimeTimelineItem[],
toolCallId?: string,
meta?: ChatTimeRoundMeta,
) {
const normalizedRoundId = normalizeRoundId(meta?.roundId);
const normalizedVariantIndex = normalizePositiveInteger(meta?.variantIndex);
if (toolCallId) {
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item) {
continue;
}
if (
isToolItem(item) &&
item.toolCallId === toolCallId &&
matchesRoundVariant(item, normalizedRoundId, normalizedVariantIndex)
) {
return item;
}
}
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item) {
continue;
}
if (
isToolItem(item) &&
item.status === 'TOOL_CALL' &&
matchesRoundVariant(item, normalizedRoundId, normalizedVariantIndex)
) {
return item;
}
}
return null;
}
function extractDisplayChains(record: ChatTimeHistoryRecord) {
if (Array.isArray(record.chains)) {
return record.chains.map((item) => toObjectRecord(item));
}
const payload = toObjectRecord(record.contentPayload);
if (Array.isArray(payload.chains)) {
return payload.chains.map((item: unknown) => toObjectRecord(item));
}
return [];
}
function isAssistantItem(
item?: ChatTimeTimelineItem,
): item is ChatTimeAssistantItem {
return item?.role === 'assistant';
}
function isToolItem(item?: ChatTimeTimelineItem): item is ChatTimeToolItem {
return item?.role === 'tool';
}
function findLastAssistant(items: ChatTimeTimelineItem[]) {
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (isAssistantItem(item)) {
return item;
}
}
return undefined;
}
function prepareRoundVariant(
items: ChatTimeTimelineItem[],
meta?: ChatTimeRoundMeta,
) {
const normalizedRoundId = normalizeRoundId(meta?.roundId);
const normalizedVariantIndex = normalizePositiveInteger(meta?.variantIndex);
if (!normalizedRoundId || !normalizedVariantIndex) {
return;
}
const assistant = items.find(
(item) => item.role === 'assistant' && item.roundId === normalizedRoundId,
);
if (assistant?.variantIndex === normalizedVariantIndex) {
return;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item) {
continue;
}
if (item.roundId === normalizedRoundId && item.role !== 'user') {
items.splice(index, 1);
}
}
}
function resolveRoundInsertIndex(
items: ChatTimeTimelineItem[],
roundId: string,
) {
const firstRoundItemIndex = items.findIndex(
(item) => item.roundId === roundId && item.role !== 'user',
);
if (firstRoundItemIndex >= 0) {
return firstRoundItemIndex;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item) {
continue;
}
if (item.roundId === roundId && item.role === 'user') {
return index + 1;
}
}
return items.length;
}
function resolveRoundReplaceRange(
items: ChatTimeTimelineItem[],
roundId: string,
) {
let start = -1;
let end = -1;
for (let index = 0; index < items.length; index += 1) {
const item = items[index];
if (item?.roundId === roundId && item.role !== 'user') {
if (start < 0) {
start = index;
}
end = index;
} else if (start >= 0) {
break;
}
}
if (start < 0) {
return null;
}
return {
deleteCount: end - start + 1,
start,
};
}
function matchesRoundVariant(
item: ChatTimeTimelineItem,
roundId?: string,
variantIndex?: number,
) {
if (roundId && item.roundId !== roundId) {
return false;
}
if (variantIndex && item.variantIndex && item.variantIndex !== variantIndex) {
return false;
}
return true;
}
function isSameRoundVariant(
item: ChatTimeTimelineItem,
meta?: ChatTimeRoundMeta,
) {
const normalizedRoundId = normalizeRoundId(meta?.roundId);
const normalizedVariantIndex = normalizePositiveInteger(meta?.variantIndex);
if (!normalizedRoundId || !normalizedVariantIndex) {
return true;
}
return (
item.roundId === normalizedRoundId &&
normalizePositiveInteger(item.variantIndex) === normalizedVariantIndex
);
}
function applyRoundMeta(
target: Partial<ChatTimeTimelineItemBase>,
source?: ChatTimeRoundMeta | null,
) {
if (!source) {
return;
}
const roundId = normalizeRoundId(source.roundId);
if (roundId) {
target.roundId = roundId;
}
const roundNo = normalizePositiveInteger(source.roundNo);
if (roundNo) {
target.roundNo = roundNo;
}
const variantIndex = normalizePositiveInteger(source.variantIndex);
if (variantIndex) {
target.variantIndex = variantIndex;
}
const variantCount = normalizePositiveInteger(source.variantCount);
if (variantCount) {
target.variantCount = variantCount;
}
const selectedVariantIndex = normalizePositiveInteger(
source.selectedVariantIndex,
);
if (selectedVariantIndex) {
target.selectedVariantIndex = selectedVariantIndex;
}
if (typeof source.switchable === 'boolean') {
target.switchable = source.switchable;
}
const messageKind = normalizePlainText(source.messageKind).trim();
if (messageKind) {
target.messageKind = messageKind;
}
}
function normalizeRoundMeta(meta?: ChatTimeRoundMeta): ChatTimeRoundMeta {
return {
messageKind: meta?.messageKind,
roundId: normalizeRoundId(meta?.roundId),
roundNo: meta?.roundNo,
selectedVariantIndex: meta?.selectedVariantIndex,
switchable: meta?.switchable,
variantCount: meta?.variantCount,
variantIndex: meta?.variantIndex,
};
}
function normalizeRoundId(value: any) {
const normalized = normalizePlainText(value).trim();
return normalized || undefined;
}
function normalizePositiveInteger(value: any) {
if (value == null || value === '') {
return undefined;
}
const parsed = Number.parseInt(String(value), 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}
function normalizeAssistantText(value: any) {
return normalizePlainText(value);
}
function normalizePayloadValue(value: any) {
if (value == null) {
return '';
}
if (typeof value === 'string') {
return value;
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function normalizePlainText(value: any) {
return typeof value === 'string' ? value : value == null ? '' : String(value);
}
function normalizeRole(value: any) {
return normalizePlainText(value).trim().toLowerCase();
}
function normalizeThinkingStatus(value: any): ChatTimeThinkingStatus {
return normalizeRole(value) === 'thinking' ? 'thinking' : 'end';
}
function normalizeTimestamp(value?: number | string) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string' && value.trim()) {
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? value : parsed;
}
return Date.now();
}
function normalizeToolStatus(value: any): ChatTimeToolStatus {
return normalizePlainText(value).toUpperCase() === 'TOOL_CALL'
? 'TOOL_CALL'
: 'TOOL_RESULT';
}
function stopThinkingForAssistant(item: ChatTimeAssistantItem) {
item.segments = item.segments.map(
(segment: ChatTimeAssistantItem['segments'][number]) => {
if (segment.type !== 'thinking' || segment.status !== 'thinking') {
return segment;
}
return {
...segment,
status: 'end' as ChatTimeThinkingStatus,
};
},
);
}
function toObjectArray(value: any): Array<Record<string, any>> {
if (!Array.isArray(value)) {
return [];
}
return value
.map((item: unknown) => toObjectRecord(item))
.filter((item) => Object.keys(item).length > 0);
}
function toObjectRecord(value: any): Record<string, any> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {};
}
return value as Record<string, any>;
}
export { ChatTimeHistoryMapper, ChatTimeTimelineBuilder };