feat: 重构聊天时间线与附件上传交互
- 管理端和用户中心统一切换到 chatTime 时间线模型,按真实 assistant/tool 时序渲染 - 收紧工具气泡与 JSON 展示样式,保留同气泡内的工具状态更新 - 回形针直接触发文件选择,附件列表上移到输入框上方并补充共享 helper 测试
This commit is contained in:
96
easyflow-ui-usercenter/packages/types/src/chat-time.ts
Normal file
96
easyflow-ui-usercenter/packages/types/src/chat-time.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
type ChatTimeTimelineRole = 'assistant' | 'tool' | 'user';
|
||||
type ChatTimeToolStatus = 'TOOL_CALL' | 'TOOL_RESULT';
|
||||
type ChatTimeThinkingStatus = 'end' | 'thinking';
|
||||
|
||||
interface ChatTimeTimelineItemBase {
|
||||
created: number | string;
|
||||
id: string;
|
||||
loading?: boolean;
|
||||
placement: 'end' | 'start';
|
||||
role: ChatTimeTimelineRole;
|
||||
senderName?: string;
|
||||
typing?: boolean;
|
||||
}
|
||||
|
||||
interface ChatTimeAssistantThinkingSegment {
|
||||
content: string;
|
||||
expanded: boolean;
|
||||
id: string;
|
||||
status: ChatTimeThinkingStatus;
|
||||
type: 'thinking';
|
||||
}
|
||||
|
||||
interface ChatTimeAssistantTextSegment {
|
||||
content: string;
|
||||
id: string;
|
||||
type: 'text';
|
||||
}
|
||||
|
||||
type ChatTimeAssistantSegment =
|
||||
| ChatTimeAssistantTextSegment
|
||||
| ChatTimeAssistantThinkingSegment;
|
||||
|
||||
interface ChatTimeAssistantItem extends ChatTimeTimelineItemBase {
|
||||
content: string;
|
||||
role: 'assistant';
|
||||
segments: ChatTimeAssistantSegment[];
|
||||
}
|
||||
|
||||
interface ChatTimeToolItem extends ChatTimeTimelineItemBase {
|
||||
arguments?: string;
|
||||
content: string;
|
||||
name: string;
|
||||
result?: string;
|
||||
role: 'tool';
|
||||
status: ChatTimeToolStatus;
|
||||
toolCallId: string;
|
||||
}
|
||||
|
||||
interface ChatTimeUserItem extends ChatTimeTimelineItemBase {
|
||||
content: string;
|
||||
role: 'user';
|
||||
}
|
||||
|
||||
type ChatTimeTimelineItem =
|
||||
| ChatTimeAssistantItem
|
||||
| ChatTimeToolItem
|
||||
| ChatTimeUserItem;
|
||||
|
||||
interface ChatTimeHistoryRecord {
|
||||
chains?: Array<Record<string, any>>;
|
||||
content?: string;
|
||||
contentPayload?: null | Record<string, any>;
|
||||
contentText?: string;
|
||||
created?: number | string;
|
||||
id?: number | string;
|
||||
loading?: boolean;
|
||||
placement?: 'end' | 'start';
|
||||
role?: string;
|
||||
senderName?: string;
|
||||
senderRole?: string;
|
||||
typing?: boolean;
|
||||
}
|
||||
|
||||
interface ChatTimeToolMutationPayload {
|
||||
created?: number | string;
|
||||
name?: string;
|
||||
result?: any;
|
||||
toolCallId?: string;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export type {
|
||||
ChatTimeAssistantItem,
|
||||
ChatTimeAssistantSegment,
|
||||
ChatTimeAssistantTextSegment,
|
||||
ChatTimeAssistantThinkingSegment,
|
||||
ChatTimeHistoryRecord,
|
||||
ChatTimeThinkingStatus,
|
||||
ChatTimeTimelineItem,
|
||||
ChatTimeTimelineItemBase,
|
||||
ChatTimeTimelineRole,
|
||||
ChatTimeToolItem,
|
||||
ChatTimeToolMutationPayload,
|
||||
ChatTimeToolStatus,
|
||||
ChatTimeUserItem,
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export type * from './bot';
|
||||
export type * from './chat-time';
|
||||
export type * from './user';
|
||||
export type * from '@easyflow-core/typings';
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@easyflow-core/shared": "workspace:*",
|
||||
"@easyflow-core/typings": "workspace:*",
|
||||
"@easyflow/types": "workspace:*",
|
||||
"vue-router": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
ChatTimeHistoryMapper,
|
||||
ChatTimeTimelineBuilder,
|
||||
} from '../chat-time';
|
||||
|
||||
describe('chat-time timeline builder', () => {
|
||||
it('builds assistant thinking and message in the same assistant item', () => {
|
||||
const items: any[] = [];
|
||||
|
||||
ChatTimeTimelineBuilder.appendUserMessage(items, {
|
||||
content: '你好',
|
||||
created: 1,
|
||||
id: 'user-1',
|
||||
});
|
||||
ChatTimeTimelineBuilder.appendThinkingDelta(items, '先想一下', 2);
|
||||
ChatTimeTimelineBuilder.appendMessageDelta(items, '最终回答', 3);
|
||||
ChatTimeTimelineBuilder.finalize(items);
|
||||
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[1]).toMatchObject({
|
||||
content: '最终回答',
|
||||
role: 'assistant',
|
||||
});
|
||||
expect(items[1].segments).toMatchObject([
|
||||
{ content: '先想一下', status: 'end', type: 'thinking' },
|
||||
{ content: '最终回答', type: 'text' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates a new assistant item after tool result', () => {
|
||||
const items: any[] = [];
|
||||
|
||||
ChatTimeTimelineBuilder.appendMessageDelta(items, '第一段回答', 1);
|
||||
ChatTimeTimelineBuilder.upsertToolCall(items, {
|
||||
name: 'search_docs',
|
||||
toolCallId: 'tool-1',
|
||||
value: '{"query":"java"}',
|
||||
});
|
||||
ChatTimeTimelineBuilder.upsertToolResult(items, {
|
||||
result: '{"hits":1}',
|
||||
toolCallId: 'tool-1',
|
||||
});
|
||||
ChatTimeTimelineBuilder.appendThinkingDelta(items, '继续思考', 2);
|
||||
ChatTimeTimelineBuilder.appendMessageDelta(items, '第二段回答', 3);
|
||||
ChatTimeTimelineBuilder.finalize(items);
|
||||
|
||||
expect(items).toHaveLength(3);
|
||||
expect(items[0]).toMatchObject({ content: '第一段回答', role: 'assistant' });
|
||||
expect(items[1]).toMatchObject({
|
||||
name: 'search_docs',
|
||||
result: '{"hits":1}',
|
||||
role: 'tool',
|
||||
status: 'TOOL_RESULT',
|
||||
toolCallId: 'tool-1',
|
||||
});
|
||||
expect(items[2]).toMatchObject({ content: '第二段回答', role: 'assistant' });
|
||||
expect(items[2].segments).toMatchObject([
|
||||
{ content: '继续思考', status: 'end', type: 'thinking' },
|
||||
{ content: '第二段回答', type: 'text' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chat-time history mapper', () => {
|
||||
it('expands structured messageChain into assistant and tool timeline items', () => {
|
||||
const items = ChatTimeHistoryMapper.fromHistoryRecords([
|
||||
{
|
||||
contentPayload: {
|
||||
messageChain: [
|
||||
{
|
||||
content: '先回答一点',
|
||||
reasoningContent: '先思考',
|
||||
role: 'assistant',
|
||||
toolCalls: [
|
||||
{
|
||||
arguments: '{"query":"java"}',
|
||||
id: 'tool-1',
|
||||
name: 'search_docs',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
content: '{"hits":1}',
|
||||
role: 'tool',
|
||||
toolCallId: 'tool-1',
|
||||
},
|
||||
{
|
||||
content: '最后总结',
|
||||
reasoningContent: '继续思考',
|
||||
role: 'assistant',
|
||||
},
|
||||
],
|
||||
},
|
||||
created: 100,
|
||||
id: 'assistant-record',
|
||||
senderRole: 'assistant',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(items).toHaveLength(3);
|
||||
expect(items[0]).toMatchObject({
|
||||
content: '先回答一点',
|
||||
role: 'assistant',
|
||||
});
|
||||
expect(items[1]).toMatchObject({
|
||||
arguments: '{"query":"java"}',
|
||||
name: 'search_docs',
|
||||
result: '{"hits":1}',
|
||||
role: 'tool',
|
||||
toolCallId: 'tool-1',
|
||||
});
|
||||
expect(items[2]).toMatchObject({
|
||||
content: '最后总结',
|
||||
role: 'assistant',
|
||||
});
|
||||
expect(items[0]?.id).not.toBe(items[2]?.id);
|
||||
});
|
||||
|
||||
it('falls back to legacy chains when messageChain is unavailable', () => {
|
||||
const items = ChatTimeHistoryMapper.fromLegacyMessages([
|
||||
{
|
||||
chains: [
|
||||
{
|
||||
reasoning_content: '旧思考',
|
||||
thinkingStatus: 'end',
|
||||
},
|
||||
{
|
||||
id: 'tool-2',
|
||||
name: 'legacy_tool',
|
||||
result: '{"ok":true}',
|
||||
status: 'TOOL_RESULT',
|
||||
},
|
||||
],
|
||||
content: '旧回答',
|
||||
created: '2026-05-11 10:00:00',
|
||||
id: 'legacy-1',
|
||||
role: 'assistant',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[0]).toMatchObject({
|
||||
content: '旧回答',
|
||||
role: 'assistant',
|
||||
});
|
||||
expect(items[0].segments).toMatchObject([
|
||||
{ content: '旧思考', status: 'end', type: 'thinking' },
|
||||
{ content: '旧回答', type: 'text' },
|
||||
]);
|
||||
expect(items[1]).toMatchObject({
|
||||
name: 'legacy_tool',
|
||||
result: '{"ok":true}',
|
||||
role: 'tool',
|
||||
toolCallId: 'tool-2',
|
||||
});
|
||||
});
|
||||
});
|
||||
666
easyflow-ui-usercenter/packages/utils/src/helpers/chat-time.ts
Normal file
666
easyflow-ui-usercenter/packages/utils/src/helpers/chat-time.ts
Normal file
@@ -0,0 +1,666 @@
|
||||
import type {
|
||||
ChatTimeAssistantItem,
|
||||
ChatTimeHistoryRecord,
|
||||
ChatTimeThinkingStatus,
|
||||
ChatTimeTimelineItem,
|
||||
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;
|
||||
senderName?: string;
|
||||
},
|
||||
) {
|
||||
items.push({
|
||||
content: normalizePlainText(payload.content),
|
||||
created: normalizeTimestamp(payload.created),
|
||||
id: payload.id || uuid(),
|
||||
placement: 'end',
|
||||
role: 'user',
|
||||
senderName: payload.senderName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加思考增量。
|
||||
*/
|
||||
static appendThinkingDelta(
|
||||
items: ChatTimeTimelineItem[],
|
||||
delta?: string,
|
||||
created?: number | string,
|
||||
) {
|
||||
const normalizedDelta = normalizePlainText(delta);
|
||||
if (!normalizedDelta) {
|
||||
return;
|
||||
}
|
||||
const assistant = ensureAssistantTail(items, created);
|
||||
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,
|
||||
) {
|
||||
const normalizedDelta = normalizeAssistantText(delta);
|
||||
if (!normalizedDelta) {
|
||||
return;
|
||||
}
|
||||
const assistant = ensureAssistantTail(items, created);
|
||||
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 stopThinking(items: ChatTimeTimelineItem[]) {
|
||||
const last = items[items.length - 1];
|
||||
if (!isAssistantItem(last)) {
|
||||
return;
|
||||
}
|
||||
stopThinkingForAssistant(last);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工具调用状态。
|
||||
*/
|
||||
static upsertToolCall(
|
||||
items: ChatTimeTimelineItem[],
|
||||
payload: ChatTimeToolMutationPayload,
|
||||
) {
|
||||
this.stopThinking(items);
|
||||
const toolItem = ensureToolItem(
|
||||
items,
|
||||
payload.toolCallId,
|
||||
payload.created,
|
||||
payload.name,
|
||||
);
|
||||
toolItem.arguments = normalizePayloadValue(payload.value);
|
||||
toolItem.content = '';
|
||||
toolItem.status = 'TOOL_CALL';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工具结果状态。
|
||||
*/
|
||||
static upsertToolResult(
|
||||
items: ChatTimeTimelineItem[],
|
||||
payload: ChatTimeToolMutationPayload,
|
||||
) {
|
||||
const toolItem = ensureToolItem(
|
||||
items,
|
||||
payload.toolCallId,
|
||||
payload.created,
|
||||
payload.name,
|
||||
);
|
||||
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 = items[items.length - 1];
|
||||
if (!isAssistantItem(last)) {
|
||||
return;
|
||||
}
|
||||
stopThinkingForAssistant(last);
|
||||
last.loading = false;
|
||||
last.typing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天时间线历史映射器。
|
||||
*/
|
||||
class ChatTimeHistoryMapper {
|
||||
/**
|
||||
* 从聊天历史记录恢复时间线。
|
||||
*/
|
||||
static fromHistoryRecords(records: ChatTimeHistoryRecord[]) {
|
||||
return 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,
|
||||
senderName: record.senderName,
|
||||
typing: record.typing,
|
||||
});
|
||||
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);
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
function createAssistantItem(
|
||||
created?: number | string,
|
||||
patch?: Partial<ChatTimeAssistantItem>,
|
||||
): ChatTimeAssistantItem {
|
||||
return {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
senderName: record.senderName,
|
||||
typing: false,
|
||||
});
|
||||
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,
|
||||
) {
|
||||
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(),
|
||||
name,
|
||||
result: status === 'TOOL_RESULT' ? argumentsValue : undefined,
|
||||
status,
|
||||
toolCallId,
|
||||
});
|
||||
}
|
||||
|
||||
function createToolItemFromStructuredMessage(
|
||||
rawMessage: Record<string, any>,
|
||||
toolMetaMap: Map<string, ChatTimeToolMeta>,
|
||||
created?: number | string,
|
||||
) {
|
||||
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(),
|
||||
name: toolMeta?.name,
|
||||
result,
|
||||
status: 'TOOL_RESULT',
|
||||
toolCallId,
|
||||
});
|
||||
}
|
||||
|
||||
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),
|
||||
name: normalizePlainText(payload.name),
|
||||
result: normalizePayloadValue(
|
||||
payload.content ?? payload.result ?? record.contentText ?? record.content,
|
||||
),
|
||||
status: 'TOOL_RESULT',
|
||||
toolCallId,
|
||||
});
|
||||
}
|
||||
|
||||
function createToolItem(payload: {
|
||||
arguments?: string;
|
||||
created?: number | string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
result?: string;
|
||||
status: ChatTimeToolStatus;
|
||||
toolCallId?: string;
|
||||
}): ChatTimeToolItem {
|
||||
return {
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
function createUserItem(record: ChatTimeHistoryRecord): ChatTimeTimelineItem {
|
||||
return {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
const last = items[items.length - 1];
|
||||
if (isAssistantItem(last)) {
|
||||
return last;
|
||||
}
|
||||
const assistant = createAssistantItem(created, {
|
||||
loading: true,
|
||||
typing: true,
|
||||
});
|
||||
items.push(assistant);
|
||||
return assistant;
|
||||
}
|
||||
|
||||
function ensureToolItem(
|
||||
items: ChatTimeTimelineItem[],
|
||||
toolCallId?: string,
|
||||
created?: number | string,
|
||||
name?: string,
|
||||
) {
|
||||
const normalizedToolCallId = normalizePlainText(toolCallId);
|
||||
const found = findToolItem(items, normalizedToolCallId);
|
||||
if (found) {
|
||||
if (name) {
|
||||
found.name = name;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
const toolItem = createToolItem({
|
||||
created,
|
||||
id: normalizedToolCallId || uuid(),
|
||||
name,
|
||||
status: 'TOOL_CALL',
|
||||
toolCallId: normalizedToolCallId,
|
||||
});
|
||||
items.push(toolItem);
|
||||
return toolItem;
|
||||
}
|
||||
|
||||
function findToolItem(items: ChatTimeTimelineItem[], toolCallId?: string) {
|
||||
if (toolCallId) {
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (isToolItem(item) && item.toolCallId === toolCallId) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (isToolItem(item) && item.status === 'TOOL_CALL') {
|
||||
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 normalizeAssistantText(value: any) {
|
||||
return normalizePlainText(value)
|
||||
.replace(/^Final Answer:\s*/i, '')
|
||||
.replaceAll('```echartsoption', '```echarts\noption');
|
||||
}
|
||||
|
||||
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 };
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './chat-time';
|
||||
export * from './clipboard';
|
||||
export * from './find-menu-by-path';
|
||||
export * from './generate-menus';
|
||||
|
||||
Reference in New Issue
Block a user