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

@@ -31,13 +31,14 @@
"@easyflow/icons": "workspace:*",
"@easyflow/locales": "workspace:*",
"@easyflow/types": "workspace:*",
"@incremark/theme": "1.0.2",
"@incremark/vue": "1.0.2",
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"json-bigint": "catalog:",
"qrcode": "catalog:",
"tippy.js": "catalog:",
"vue": "catalog:",
"vue-element-plus-x": "catalog:",
"vue-json-viewer": "catalog:",
"vue-router": "catalog:",
"vue-tippy": "catalog:"

View File

@@ -1,31 +1,61 @@
<script setup lang="ts">
import { computed } from 'vue';
import { XMarkdown as ElXMarkdown } from 'vue-element-plus-x';
import {computed, ref, watch} from 'vue';
import {IncremarkContent, ThemeProvider} from '@incremark/vue';
import '@incremark/theme/styles.css';
import { usePreferences } from '@easyflow-core/preferences';
import {usePreferences} from '@easyflow-core/preferences';
interface Props {
content?: string;
streaming?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
content: '',
streaming: false,
});
const { isDark } = usePreferences();
const normalizedContent = computed(() => String(props.content || ''));
const markdownContent = computed(() => props.content || '');
const isFinished = computed(() => !props.streaming);
const incremarkOptions = {
breaks: true,
containers: false,
gfm: true,
htmlTree: false,
math: true,
};
const previousContent = ref('');
watch(
() => [props.content || '', props.streaming] as const,
([content, streaming]) => {
const previous = previousContent.value;
if (import.meta.env.DEV && streaming) {
const startsWithPrevious = content.startsWith(previous);
console.debug('[ChatTimeMarkdown] streaming update', {
deltaLength: startsWithPrevious ? content.length - previous.length : null,
length: content.length,
previousLength: previous.length,
preview: content.slice(-160).replaceAll('\n', '\\n'),
startsWithPrevious,
});
}
previousContent.value = content;
},
{ immediate: true },
);
</script>
<template>
<ElXMarkdown
class="chat-time-markdown"
:allow-html="false"
:enable-breaks="true"
:enable-code-line-number="false"
:enable-latex="true"
:default-theme-mode="isDark ? 'dark' : 'light'"
:markdown="normalizedContent"
:need-view-code-btn="false"
/>
<div class="chat-time-markdown">
<ThemeProvider :theme="isDark ? 'dark' : 'default'">
<IncremarkContent
:content="markdownContent"
:incremark-options="incremarkOptions"
:is-finished="isFinished"
/>
</ThemeProvider>
</div>
</template>
<style scoped>
@@ -39,9 +69,8 @@ const normalizedContent = computed(() => String(props.content || ''));
word-break: break-word;
}
.chat-time-markdown :deep(.elx-xmarkdown-container),
.chat-time-markdown :deep(.elx-xmarkdown-provider),
.chat-time-markdown :deep(.markdown-body) {
.chat-time-markdown :deep(.incremark-theme-provider),
.chat-time-markdown :deep(.incremark) {
width: 100%;
max-width: 100%;
color: inherit;
@@ -50,15 +79,15 @@ const normalizedContent = computed(() => String(props.content || ''));
background: transparent;
}
.chat-time-markdown :deep(.markdown-body) {
.chat-time-markdown :deep(.incremark) {
overflow-wrap: anywhere;
}
.chat-time-markdown :deep(.markdown-body > :first-child) {
.chat-time-markdown :deep(.incremark > .incremark-block:first-child > *) {
margin-top: 0;
}
.chat-time-markdown :deep(.markdown-body > :last-child) {
.chat-time-markdown :deep(.incremark > .incremark-block:last-child > *) {
margin-bottom: 0;
}
@@ -260,6 +289,34 @@ const normalizedContent = computed(() => String(props.content || ''));
word-break: normal;
}
.chat-time-markdown :deep(.incremark-code) {
max-width: 100%;
margin: 1em 0;
overflow: hidden;
color: hsl(var(--text-strong));
background: hsl(var(--surface-subtle));
border: 1px solid hsl(var(--divider-faint) / 0.82);
border-radius: 12px;
}
.chat-time-markdown :deep(.incremark-code .code-header) {
padding: 8px 12px;
color: hsl(var(--text-muted));
background: hsl(var(--surface-subtle) / 0.72);
border-bottom: 1px solid hsl(var(--divider-faint) / 0.72);
}
.chat-time-markdown :deep(.incremark-code .code-content) {
overflow: auto;
}
.chat-time-markdown :deep(.incremark-code pre) {
margin: 0;
background: transparent;
border: 0;
border-radius: 0;
}
.chat-time-markdown :deep(.shiki),
.chat-time-markdown :deep(.shiki code) {
background: transparent !important;
@@ -302,7 +359,8 @@ const normalizedContent = computed(() => String(props.content || ''));
}
:global(.dark) .chat-time-markdown :deep(code),
:global(.dark) .chat-time-markdown :deep(pre) {
:global(.dark) .chat-time-markdown :deep(pre),
:global(.dark) .chat-time-markdown :deep(.incremark-code) {
background: hsl(var(--surface-subtle) / 0.78);
}
</style>

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[] = [];

View File

@@ -10,7 +10,7 @@ import type {
ChatTimeToolStatus,
} from '../../../types/src/chat-time';
import { uuid } from './uuid';
import {uuid} from './uuid';
type ChatTimeToolMeta = {
arguments?: string;
@@ -159,6 +159,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 的思考态。
*/
@@ -1003,9 +1032,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) {