feat: 全新智能体功能

- 基于先进智能体框架,增加智能体编排功能
- 增加智能体聊天,并对接持久化
This commit is contained in:
2026-05-25 11:42:48 +08:00
parent 6c3d98eaac
commit 72df00f25b
168 changed files with 22045 additions and 400 deletions

View File

@@ -1,9 +1,6 @@
import { describe, expect, it } from 'vitest';
import {describe, expect, it} from 'vitest';
import {
ChatTimeHistoryMapper,
ChatTimeTimelineBuilder,
} from '../chat-time';
import {ChatTimeHistoryMapper, ChatTimeTimelineBuilder,} from '../chat-time';
describe('chat-time timeline builder', () => {
it('builds assistant thinking and message in the same assistant item', () => {
@@ -29,6 +26,37 @@ describe('chat-time timeline builder', () => {
]);
});
it('appends markdown deltas without altering repeated symbols', () => {
const items: any[] = [];
ChatTimeTimelineBuilder.appendThinkingDelta(items, '先想一下', 1);
ChatTimeTimelineBuilder.appendMessageDelta(items, '## 标题\n', 2);
ChatTimeTimelineBuilder.appendMessageDelta(items, '| 模型 | 说明 |\n', 3);
ChatTimeTimelineBuilder.appendMessageDelta(items, '| --- | --- |\n', 4);
ChatTimeTimelineBuilder.appendMessageDelta(items, '| ACL | 访问控制列表 |\n', 5);
ChatTimeTimelineBuilder.appendMessageDelta(
items,
'Final Answer: ```echartsoption',
6,
);
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({
content:
'## 标题\n| 模型 | 说明 |\n| --- | --- |\n| ACL | 访问控制列表 |\nFinal Answer: ```echartsoption',
role: 'assistant',
typing: true,
});
expect(items[0].segments).toMatchObject([
{ content: '先想一下', status: 'end', type: 'thinking' },
{
content:
'## 标题\n| 模型 | 说明 |\n| --- | --- |\n| ACL | 访问控制列表 |\nFinal Answer: ```echartsoption',
type: 'text',
},
]);
});
it('creates a new assistant item after tool result', () => {
const items: any[] = [];
@@ -61,6 +89,62 @@ describe('chat-time timeline builder', () => {
{ content: '第二段回答', type: 'text' },
]);
});
it('does not render built-in knowledge retrieval tools as normal tool cards', () => {
const items: any[] = [];
ChatTimeTimelineBuilder.appendMessageDelta(items, '第一段回答', 1);
ChatTimeTimelineBuilder.upsertToolCall(items, {
name: 'retrieve_knowledge',
toolCallId: 'knowledge-1',
value: '{"query":"请假安排"}',
});
ChatTimeTimelineBuilder.upsertToolResult(items, {
name: 'retrieve_knowledge',
result: '{"hits":1}',
toolCallId: 'knowledge-1',
});
ChatTimeTimelineBuilder.upsertToolCall(items, {
name: 'search_docs',
toolCallId: 'tool-1',
value: '{"query":"java"}',
});
expect(items).toHaveLength(2);
expect(items[0]).toMatchObject({ content: '第一段回答', role: 'assistant' });
expect(items[1]).toMatchObject({
name: 'search_docs',
role: 'tool',
status: 'TOOL_CALL',
toolCallId: 'tool-1',
});
});
it('does not render anonymous internal tool calls as normal tool cards', () => {
const items: any[] = [];
ChatTimeTimelineBuilder.appendMessageDelta(items, '第一段回答', 1);
ChatTimeTimelineBuilder.upsertToolCall(items, {
toolCallId: 'fragment-1',
value: '{"arguments":"partial"}',
});
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({ content: '第一段回答', role: 'assistant' });
});
it('does not render anonymous orphan tool results as normal tool cards', () => {
const items: any[] = [];
ChatTimeTimelineBuilder.appendMessageDelta(items, '第一段回答', 1);
ChatTimeTimelineBuilder.upsertToolResult(items, {
result: '{"ok":true}',
toolCallId: 'fragment-1',
});
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({ content: '第一段回答', role: 'assistant' });
});
});
describe('chat-time history mapper', () => {
@@ -118,6 +202,58 @@ describe('chat-time history mapper', () => {
expect(items[0]?.id).not.toBe(items[2]?.id);
});
it('skips built-in knowledge retrieval tools when restoring structured history', () => {
const items = ChatTimeHistoryMapper.fromHistoryRecords([
{
contentPayload: {
messageChain: [
{
content: '先回答一点',
role: 'assistant',
toolCalls: [
{
arguments: '{"query":"请假安排"}',
id: 'knowledge-1',
toolName: 'retrieve_knowledge',
},
{
arguments: '{"query":"java"}',
id: 'tool-1',
name: 'search_docs',
},
],
},
{
content: '{"hits":1}',
role: 'tool',
toolCallId: 'knowledge-1',
},
{
content: '{"hits":2}',
role: 'tool',
toolCallId: 'tool-1',
},
],
},
created: 100,
id: 'assistant-record',
senderRole: 'assistant',
},
]);
expect(items).toHaveLength(2);
expect(items[0]).toMatchObject({
content: '先回答一点',
role: 'assistant',
});
expect(items[1]).toMatchObject({
name: 'search_docs',
result: '{"hits":2}',
role: 'tool',
toolCallId: 'tool-1',
});
});
it('falls back to legacy chains when messageChain is unavailable', () => {
const items = ChatTimeHistoryMapper.fromLegacyMessages([
{

View File

@@ -10,13 +10,22 @@ import type {
ChatTimeToolStatus,
} from '../../../types/src/chat-time';
import { uuid } from './uuid';
import {uuid} from './uuid';
type ChatTimeToolMeta = {
arguments?: string;
name?: string;
};
function isHiddenToolName(value?: string) {
const normalized = normalizePlainText(value).trim().toLowerCase();
return normalized === 'retrieve_knowledge' || normalized === '__fragment__';
}
function isBlankToolName(value?: string) {
return !normalizePlainText(value).trim();
}
/**
* 聊天时间线实时构建器。
*/
@@ -159,6 +168,35 @@ class ChatTimeTimelineBuilder {
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 的思考态。
*/
@@ -177,6 +215,9 @@ class ChatTimeTimelineBuilder {
items: ChatTimeTimelineItem[],
payload: ChatTimeToolMutationPayload,
) {
if (isHiddenToolName(payload.name) || isBlankToolName(payload.name)) {
return;
}
prepareRoundVariant(items, payload);
this.stopThinking(items);
const toolItem = ensureToolItem(
@@ -198,6 +239,16 @@ class ChatTimeTimelineBuilder {
items: ChatTimeTimelineItem[],
payload: ChatTimeToolMutationPayload,
) {
if (isHiddenToolName(payload.name)) {
return;
}
const normalizedToolCallId = normalizePlainText(payload.toolCallId);
if (
isBlankToolName(payload.name) &&
!findToolItem(items, normalizedToolCallId, payload)
) {
return;
}
prepareRoundVariant(items, payload);
const toolItem = ensureToolItem(
items,
@@ -298,7 +349,8 @@ class ChatTimeHistoryMapper {
return [createUserItem(record)];
}
if (role === 'tool') {
return [createToolItemFromTopLevelRecord(record)];
const toolItem = createToolItemFromTopLevelRecord(record);
return toolItem ? [toolItem] : [];
}
if (role !== 'assistant') {
return [];
@@ -324,7 +376,8 @@ class ChatTimeHistoryMapper {
return [createUserItem(record)];
}
if (role === 'tool') {
return [createToolItemFromTopLevelRecord(record)];
const toolItem = createToolItemFromTopLevelRecord(record);
return toolItem ? [toolItem] : [];
}
if (role !== 'assistant') {
return [];
@@ -404,14 +457,15 @@ class ChatTimeHistoryMapper {
}
if (role === 'tool') {
items.push(
createToolItemFromStructuredMessage(
rawMessage,
toolMetaMap,
record.created,
record,
),
const toolItem = createToolItemFromStructuredMessage(
rawMessage,
toolMetaMap,
record.created,
record,
);
if (toolItem) {
items.push(toolItem);
}
}
}
@@ -560,7 +614,10 @@ function createToolItemFromChain(
record?: ChatTimeHistoryRecord,
) {
const toolCallId = normalizePlainText(rawChain.id);
const name = normalizePlainText(rawChain.name);
const name = normalizePlainText(rawChain.name ?? rawChain.toolName);
if (isHiddenToolName(name)) {
return null;
}
const argumentsValue = normalizePayloadValue(rawChain.arguments ?? rawChain.result);
const status = normalizeToolStatus(rawChain.status);
if (!toolCallId && !name && !argumentsValue) {
@@ -594,13 +651,17 @@ function createToolItemFromStructuredMessage(
rawMessage.toolCallId ?? rawMessage.tool_call_id,
);
const toolMeta = toolMetaMap.get(toolCallId);
const toolName = normalizePlainText(rawMessage.name ?? rawMessage.toolName);
if (isHiddenToolName(toolMeta?.name || toolName)) {
return null;
}
const result = normalizePayloadValue(rawMessage.content);
return createToolItem({
arguments: toolMeta?.arguments,
created,
id: toolCallId || uuid(),
messageKind: record?.messageKind,
name: toolMeta?.name,
name: toolMeta?.name || toolName,
roundId: record?.roundId,
roundNo: record?.roundNo,
result,
@@ -615,6 +676,10 @@ function createToolItemFromStructuredMessage(
function createToolItemFromTopLevelRecord(record: ChatTimeHistoryRecord) {
const payload = toObjectRecord(record.contentPayload);
const name = normalizePlainText(payload.name ?? payload.toolName);
if (isHiddenToolName(name)) {
return null;
}
const toolCallId = normalizePlainText(
payload.toolCallId ?? payload.tool_call_id ?? record.id,
);
@@ -622,7 +687,7 @@ function createToolItemFromTopLevelRecord(record: ChatTimeHistoryRecord) {
created: record.created,
id: record.id == null ? toolCallId || uuid() : String(record.id),
messageKind: record.messageKind,
name: normalizePlainText(payload.name),
name,
roundId: record.roundId,
roundNo: record.roundNo,
result: normalizePayloadValue(
@@ -709,7 +774,7 @@ function collectToolMeta(
}
toolMetaMap.set(toolCallId, {
arguments: normalizePayloadValue(toolCall.arguments),
name: normalizePlainText(toolCall.name),
name: normalizePlainText(toolCall.name ?? toolCall.toolName),
});
}
}
@@ -1000,9 +1065,7 @@ function normalizePositiveInteger(value: any) {
}
function normalizeAssistantText(value: any) {
return normalizePlainText(value)
.replace(/^Final Answer:\s*/i, '')
.replaceAll('```echartsoption', '```echarts\noption');
return normalizePlainText(value);
}
function normalizePayloadValue(value: any) {