feat: 先进智能体功能上线
- 基于 agent-runtime 打造,默认 ReAct agent - 支持 agent 能力对接,已对接工作流、插件、知识库等 tool 能力 - 全新 agent 编排界面,支持可视化便捷配置 agent - 全新 agent 聊天界面,支持快捷操作、额外知识库选择等
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
"@vueuse/core": "catalog:",
|
||||
"@vueuse/integrations": "catalog:",
|
||||
"json-bigint": "catalog:",
|
||||
"mermaid": "^11.15.0",
|
||||
"qrcode": "catalog:",
|
||||
"tippy.js": "catalog:",
|
||||
"vue": "catalog:",
|
||||
|
||||
@@ -24,6 +24,11 @@ const incremarkOptions = {
|
||||
htmlTree: false,
|
||||
math: true,
|
||||
};
|
||||
const codeBlockConfigs = {
|
||||
mermaid: {
|
||||
takeOver: true,
|
||||
},
|
||||
};
|
||||
const previousContent = ref('');
|
||||
|
||||
watch(
|
||||
@@ -33,7 +38,9 @@ watch(
|
||||
if (import.meta.env.DEV && streaming) {
|
||||
const startsWithPrevious = content.startsWith(previous);
|
||||
console.debug('[ChatTimeMarkdown] streaming update', {
|
||||
deltaLength: startsWithPrevious ? content.length - previous.length : null,
|
||||
deltaLength: startsWithPrevious
|
||||
? content.length - previous.length
|
||||
: null,
|
||||
length: content.length,
|
||||
previousLength: previous.length,
|
||||
preview: content.slice(-160).replaceAll('\n', '\\n'),
|
||||
@@ -50,6 +57,7 @@ watch(
|
||||
<div class="chat-time-markdown">
|
||||
<ThemeProvider :theme="isDark ? 'dark' : 'default'">
|
||||
<IncremarkContent
|
||||
:code-block-configs="codeBlockConfigs"
|
||||
:content="markdownContent"
|
||||
:incremark-options="incremarkOptions"
|
||||
:is-finished="isFinished"
|
||||
@@ -290,6 +298,7 @@ watch(
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-code) {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
margin: 1em 0;
|
||||
overflow: hidden;
|
||||
@@ -300,10 +309,52 @@ watch(
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-code .code-header) {
|
||||
padding: 8px 12px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
z-index: 1;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-code .code-header .language) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-code .code-btn) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: hsl(var(--text-muted));
|
||||
background: hsl(var(--surface-subtle) / 0.72);
|
||||
border-bottom: 1px solid hsl(var(--divider-faint) / 0.72);
|
||||
background: hsl(var(--surface-subtle) / 0.68);
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
opacity: 0.64;
|
||||
transition:
|
||||
background-color 0.16s ease,
|
||||
color 0.16s ease,
|
||||
opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-code .code-btn:hover:not(:disabled)) {
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--surface-hover, var(--surface-subtle)) / 0.82);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-code .code-btn svg),
|
||||
.chat-time-markdown :deep(.incremark-mermaid .code-btn svg) {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-code .code-btn:focus-visible) {
|
||||
outline: 2px solid hsl(var(--primary) / 0.42);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-code .code-content) {
|
||||
@@ -317,6 +368,10 @@ watch(
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-code .code-content pre) {
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.shiki),
|
||||
.chat-time-markdown :deep(.shiki code) {
|
||||
background: transparent !important;
|
||||
@@ -342,13 +397,121 @@ watch(
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.mermaid),
|
||||
.chat-time-markdown :deep([class*='mermaid']) {
|
||||
.chat-time-markdown :deep(svg.mermaid),
|
||||
.chat-time-markdown
|
||||
:deep(
|
||||
.mermaid:not(
|
||||
.incremark-mermaid,
|
||||
.mermaid-header,
|
||||
.mermaid-actions,
|
||||
.mermaid-content,
|
||||
.mermaid-loading,
|
||||
.mermaid-source-code,
|
||||
.mermaid-svg
|
||||
)
|
||||
) {
|
||||
max-width: 100%;
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-mermaid) {
|
||||
position: relative;
|
||||
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(.mermaid-header) {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
z-index: 1;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.mermaid-header .language) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.mermaid-actions) {
|
||||
gap: 2px;
|
||||
padding: 1px;
|
||||
background: hsl(var(--surface-subtle) / 0.68);
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
opacity: 0.68;
|
||||
transition:
|
||||
background-color 0.16s ease,
|
||||
opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.mermaid-actions:hover) {
|
||||
background: hsl(var(--surface-hover, var(--surface-subtle)) / 0.82);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-mermaid .code-btn) {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: hsl(var(--text-muted));
|
||||
border-radius: 4px;
|
||||
transition:
|
||||
background-color 0.16s ease,
|
||||
color 0.16s ease,
|
||||
opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-mermaid .code-btn:hover:not(:disabled)) {
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--surface-hover, var(--surface-subtle)) / 0.86);
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.incremark-mermaid .code-btn:focus-visible) {
|
||||
outline: 2px solid hsl(var(--primary) / 0.42);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.mermaid-content) {
|
||||
min-height: 96px;
|
||||
margin: 0;
|
||||
padding: 0 16px 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.mermaid-loading) {
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.mermaid-source-code) {
|
||||
margin: 0;
|
||||
padding: 0 52px 0 0;
|
||||
overflow: auto;
|
||||
color: hsl(var(--text-strong));
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.mermaid-svg) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(.mermaid-svg svg) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.chat-time-markdown :deep(svg) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import {mount} from '@vue/test-utils';
|
||||
import {nextTick} from 'vue';
|
||||
|
||||
import {describe, expect, it, vi} from 'vitest';
|
||||
|
||||
import ChatTimeMarkdown from '../ChatTimeMarkdown.vue';
|
||||
|
||||
vi.mock('@easyflow-core/preferences', () => ({
|
||||
usePreferences: () => ({
|
||||
isDark: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ChatTimeMarkdown', () => {
|
||||
it('renders mermaid code fences with the shared mermaid block', async () => {
|
||||
const wrapper = mount(ChatTimeMarkdown, {
|
||||
props: {
|
||||
content: [
|
||||
'```mermaid',
|
||||
'flowchart TD',
|
||||
'A[开始] --> B[结束]',
|
||||
'```',
|
||||
].join('\n'),
|
||||
},
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find('.incremark-mermaid').exists()).toBe(true);
|
||||
expect(wrapper.find('.mermaid-header').text().toLowerCase()).toContain(
|
||||
'mermaid',
|
||||
);
|
||||
expect(wrapper.find('.mermaid-source-code').text()).toContain(
|
||||
'flowchart TD',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -156,25 +156,24 @@ describe('chat timeline builder', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('shows no compression needed when memory compression produced no compressed event', () => {
|
||||
it('removes memory compression status when compression produced no compressed event', () => {
|
||||
const items: ChatTimelineItem[] = [];
|
||||
|
||||
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
|
||||
label: '正在整理上下文',
|
||||
phase: 'started',
|
||||
status: 'running',
|
||||
statusKey: 'memory-compression',
|
||||
});
|
||||
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
|
||||
compressed: false,
|
||||
label: '已整理上下文',
|
||||
label: '无需压缩上下文',
|
||||
phase: 'completed',
|
||||
status: 'done',
|
||||
statusKey: 'memory-compression',
|
||||
});
|
||||
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0]?.type).toBe('status');
|
||||
if (items[0]?.type === 'status') {
|
||||
expect(items[0].label).toBe('无需压缩上下文');
|
||||
expect(items[0].presentation).toBe('separator');
|
||||
expect(items[0].status).toBe('done');
|
||||
expect(items[0].statusKey).toBe('memory-compression');
|
||||
}
|
||||
expect(items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ends current thinking before showing knowledge retrieval status', () => {
|
||||
@@ -227,6 +226,24 @@ describe('chat timeline builder', () => {
|
||||
expect(items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('keeps AgentScope context reload hidden without showing a tool card', () => {
|
||||
const items: ChatTimelineItem[] = [];
|
||||
|
||||
ChatTimelineBuilder.upsertToolCall(items, {
|
||||
toolCallId: 'call-context-reload',
|
||||
toolName: 'context_reload',
|
||||
input: { working_context_offload_uuid: 'context-id' },
|
||||
});
|
||||
ChatTimelineBuilder.upsertToolCall(items, {
|
||||
toolCallId: 'call-context-reload',
|
||||
toolName: 'context_reload',
|
||||
output: { result: 'ok' },
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
expect(items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ignores anonymous tool call events instead of rendering a fallback card', () => {
|
||||
const items: ChatTimelineItem[] = [];
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ function normalizeToolName(value?: string) {
|
||||
function isHiddenToolName(toolName?: string) {
|
||||
const normalizedName = normalizeToolName(toolName);
|
||||
return (
|
||||
normalizedName === 'retrieve_knowledge' || normalizedName === '__fragment__'
|
||||
normalizedName === 'retrieve_knowledge' ||
|
||||
normalizedName === 'context_reload' ||
|
||||
normalizedName === '__fragment__'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,6 +180,15 @@ function findStatusItem(items: ChatTimelineItem[], statusKey: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function removeStatusItem(items: ChatTimelineItem[], statusKey: string) {
|
||||
const index = items.findIndex(
|
||||
(item) => item.type === 'status' && item.statusKey === statusKey,
|
||||
);
|
||||
if (index >= 0) {
|
||||
items.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function doneStatusLabel(item: ChatTimelineStatusItem) {
|
||||
if (item.statusKey === 'knowledge-retrieval') {
|
||||
return '已检索知识库';
|
||||
@@ -453,17 +464,20 @@ export const ChatTimelineBuilder = {
|
||||
payload?.status === 'done' || payload?.phase === 'completed'
|
||||
? 'done'
|
||||
: 'running';
|
||||
const statusKey = payload?.statusKey || 'memory-compression';
|
||||
finishAssistantMessage(items, false);
|
||||
if (status === 'done' && payload?.compressed === false) {
|
||||
removeStatusItem(items, statusKey);
|
||||
return;
|
||||
}
|
||||
const label =
|
||||
status === 'running'
|
||||
? payload?.label || '正在整理上下文'
|
||||
: payload?.compressed === false
|
||||
? '无需压缩上下文'
|
||||
: payload?.label || '已整理上下文';
|
||||
: payload?.label || '已整理上下文';
|
||||
upsertStatus(items, {
|
||||
label,
|
||||
status,
|
||||
statusKey: payload?.statusKey || 'memory-compression',
|
||||
statusKey,
|
||||
presentation: 'separator',
|
||||
tone: 'muted',
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import {computed} from 'vue';
|
||||
import {useRoute} from 'vue-router';
|
||||
|
||||
import { useContentMaximize, useTabs } from '@easyflow/hooks';
|
||||
import { preferences } from '@easyflow/preferences';
|
||||
import { useTabbarStore } from '@easyflow/stores';
|
||||
import {useContentMaximize, useTabs} from '@easyflow/hooks';
|
||||
import {preferences} from '@easyflow/preferences';
|
||||
import {useTabbarStore} from '@easyflow/stores';
|
||||
|
||||
import { TabsToolMore, TabsToolScreen, TabsView } from '@easyflow-core/tabs-ui';
|
||||
import {TabsToolMore, TabsToolScreen, TabsView} from '@easyflow-core/tabs-ui';
|
||||
|
||||
import { useTabbar } from './use-tabbar';
|
||||
import {useTabbar} from './use-tabbar';
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutTabbar',
|
||||
@@ -30,13 +30,17 @@ const {
|
||||
} = useTabbar();
|
||||
|
||||
const menus = computed(() => {
|
||||
const tab = tabbarStore.getTabByKey(currentActive.value);
|
||||
const menus = createContextMenus(tab);
|
||||
return menus.map((item) => {
|
||||
return (currentTabs.value || []).map((tab) => {
|
||||
const key = tab.key as string;
|
||||
const title =
|
||||
(tab.meta?.newTabTitle || tab.meta?.title || tab.name || tab.path) as
|
||||
| string
|
||||
| undefined;
|
||||
return {
|
||||
...item,
|
||||
label: item.text,
|
||||
value: item.key,
|
||||
active: key === currentActive.value,
|
||||
handler: () => handleClick(key),
|
||||
label: title || key,
|
||||
value: key,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { RouteLocationNormalizedGeneric } from 'vue-router';
|
||||
import type {TabDefinition} from '@easyflow/types';
|
||||
|
||||
import type { TabDefinition } from '@easyflow/types';
|
||||
import type {IContextMenuItem} from '@easyflow-core/tabs-ui';
|
||||
|
||||
import type { IContextMenuItem } from '@easyflow-core/tabs-ui';
|
||||
import {computed, ref, watch} from 'vue';
|
||||
import {useRoute, useRouter} from 'vue-router';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useContentMaximize, useTabs } from '@easyflow/hooks';
|
||||
import {useContentMaximize, useTabs} from '@easyflow/hooks';
|
||||
import {
|
||||
ArrowLeftToLine,
|
||||
ArrowRightLeft,
|
||||
@@ -21,9 +19,9 @@ import {
|
||||
RotateCw,
|
||||
X,
|
||||
} from '@easyflow/icons';
|
||||
import { $t, useI18n } from '@easyflow/locales';
|
||||
import { getTabKey, useAccessStore, useTabbarStore } from '@easyflow/stores';
|
||||
import { filterTree } from '@easyflow/utils';
|
||||
import {$t, useI18n} from '@easyflow/locales';
|
||||
import {getTabKey, useAccessStore, useTabbarStore} from '@easyflow/stores';
|
||||
import {filterTree} from '@easyflow/utils';
|
||||
|
||||
export function useTabbar() {
|
||||
const router = useRouter();
|
||||
@@ -52,7 +50,7 @@ export function useTabbar() {
|
||||
});
|
||||
|
||||
const { locale } = useI18n();
|
||||
const currentTabs = ref<RouteLocationNormalizedGeneric[]>();
|
||||
const currentTabs = ref<TabDefinition[]>();
|
||||
watch(
|
||||
[
|
||||
() => tabbarStore.getTabs,
|
||||
@@ -99,7 +97,7 @@ export function useTabbar() {
|
||||
}
|
||||
}
|
||||
|
||||
function wrapperTabLocale(tab: RouteLocationNormalizedGeneric) {
|
||||
function wrapperTabLocale(tab: TabDefinition) {
|
||||
const navTitle = tab?.meta?.navTitle as string | undefined;
|
||||
return {
|
||||
...tab,
|
||||
|
||||
Reference in New Issue
Block a user