feat: 先进智能体功能上线

- 基于 agent-runtime 打造,默认 ReAct agent
- 支持 agent 能力对接,已对接工作流、插件、知识库等 tool 能力
- 全新 agent 编排界面,支持可视化便捷配置 agent
- 全新 agent 聊天界面,支持快捷操作、额外知识库选择等
This commit is contained in:
2026-05-28 11:29:18 +08:00
parent 11e595b088
commit 1c205c3720
39 changed files with 3546 additions and 217 deletions

View File

@@ -15,14 +15,21 @@ import {
getAgentSession,
getAgentSessions,
getPublishedAgents,
getPublishedKnowledges,
rejectAgentRun,
renameAgentSession,
saveAgentSessionExtraKnowledges,
} from './api';
import type {
ChatInputTriggerGroup,
ChatInputTriggerItem,
} from '#/components/chat-workspace/input-triggers/types';
import {computed, onBeforeUnmount, onMounted, ref} from 'vue';
import {useRoute, useRouter} from 'vue-router';
import {Delete, EditPen, MoreFilled, Plus, Promotion,} from '@element-plus/icons-vue';
import {Delete, EditPen, MoreFilled, Plus, Promotion,} from '@element-plus/icons-vue';
import {
ElButton,
ElDropdown,
@@ -35,25 +42,49 @@ import {
ElOption,
ElSelect,
} from 'element-plus';
import ChatCapabilityMenu from '#/components/chat-workspace/ChatCapabilityMenu.vue';
import ChatInputTriggerPanel from '#/components/chat-workspace/ChatInputTriggerPanel.vue';
import {useChatInputTrigger} from '#/components/chat-workspace/input-triggers/useChatInputTrigger';
import {recordsToTimelineItems} from './adapters/agentTimelineAdapter';
import {agentChatRuntimeManager} from './agentChatRuntimeManager';
import AgentChatWelcomeState from './components/AgentChatWelcomeState.vue';
const route = useRoute();
const router = useRouter();
const WELCOME_TITLES = [
'我们应该做些什么',
'让协作发生',
'今天想推进什么',
'把想法变成行动',
'让智能体开始工作',
'从一个问题开始',
'一起把事情理清楚',
'把下一步交给协作',
];
const agents = ref<AgentInfo[]>([]);
const sessions = ref<AgentChatSessionView[]>([]);
const timelineItems = ref<ChatTimelineItem[]>([]);
const selectedAgentId = ref('');
const currentSessionId = ref('');
const promptText = ref('');
const promptInputRef = ref();
const loadingAgents = ref(false);
const loadingSessions = ref(false);
const loadingConversation = ref(false);
const loadingKnowledges = ref(false);
const savingExtraKnowledges = ref(false);
const sending = ref(false);
const runtimeRunning = ref(false);
const approvalLoadingKey = ref('');
const knowledgeOptions = ref<{ label: string; value: string }[]>([]);
const knowledgeMap = ref(new Map<string, { id: string; title: string }>());
const extraKnowledgeIds = ref<string[]>([]);
const runtimeSendingState = new Map<string, boolean>();
const MAX_EXTRA_KNOWLEDGE_COUNT = 3;
let runtimeUnsubscribe: (() => void) | undefined;
const selectedAgent = computed(() =>
@@ -75,13 +106,66 @@ const canSend = computed(
const composerPlaceholder = computed(() =>
selectedAgent.value ? '输入消息' : '请选择智能体',
);
const agentSelectWidth = computed(() => {
const name = selectedAgent.value?.name || '选择智能体';
const textWidth = Array.from(name).reduce(
(total, char) => total + (/[\u4E00-\u9FFF]/.test(char) ? 14 : 8),
const selectedExtraKnowledges = computed(() => {
const knowledges: { id: string; title: string }[] = [];
for (const id of extraKnowledgeIds.value) {
const knowledge = knowledgeMap.value.get(String(id));
if (knowledge) {
knowledges.push(knowledge);
}
}
return knowledges;
});
const capabilityDisabled = computed(
() =>
sending.value ||
runtimeRunning.value ||
savingExtraKnowledges.value ||
!selectedAgentId.value,
);
const isWelcomeState = computed(
() =>
!loadingConversation.value &&
!currentSessionId.value &&
timelineItems.value.length === 0,
);
const welcomeTitle = computed(() => {
const agentKey = selectedAgentId.value || selectedAgent.value?.name || '';
const index = [...agentKey].reduce(
(total, char) => total + char.charCodeAt(0),
0,
);
return `${Math.min(Math.max(textWidth + 36, 92), 240)}px`;
return WELCOME_TITLES[index % WELCOME_TITLES.length] || '我们应该做些什么';
});
const agentSelectWidth = computed(() => {
const name = selectedAgent.value?.name || '选择智能体';
const textWidth = [...name].reduce(
(total, char) => total + (/[\u4E00-\u9FFF]/.test(char) ? 15 : 8),
0,
);
return `${Math.min(Math.max(textWidth + 36, 116), 320)}px`;
});
const triggerGroups = computed<ChatInputTriggerGroup[]>(() => [
{
items: knowledgeOptions.value.map((item) => {
const selected = extraKnowledgeIds.value.includes(String(item.value));
return {
disabled:
!selected &&
extraKnowledgeIds.value.length >= MAX_EXTRA_KNOWLEDGE_COUNT,
id: item.value,
label: item.label,
};
}),
label: '知识库',
symbol: '@',
},
]);
const chatInputTrigger = useChatInputTrigger({
disabled: capabilityDisabled,
groups: triggerGroups,
inputRef: promptInputRef,
text: promptText,
});
function formatDate(value?: string) {
@@ -171,8 +255,42 @@ async function loadSessions() {
}
}
async function loadKnowledges() {
loadingKnowledges.value = true;
try {
const res = await getPublishedKnowledges();
if (res.errorCode !== 0) {
throw new Error(res.message || '知识库加载失败');
}
const records = Array.isArray(res.data) ? res.data : [];
knowledgeOptions.value = records
.filter((item) => item?.id)
.map((item) => ({
label: item.title || item.alias || String(item.id),
value: String(item.id),
}));
knowledgeMap.value = new Map(
records
.filter((item) => item?.id)
.map((item) => [
String(item.id),
{
id: String(item.id),
title: item.title || item.alias || String(item.id),
},
]),
);
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '知识库加载失败');
} finally {
loadingKnowledges.value = false;
}
}
function resolveSessionSortTime(session: AgentChatSessionView) {
const time = new Date(session.lastMessageAt || session.accessAt || '').getTime();
const time = new Date(
session.lastMessageAt || session.accessAt || '',
).getTime();
return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
}
@@ -195,14 +313,14 @@ function upsertSessionRecord(session: AgentChatSessionView) {
const currentIndex = next.findIndex(
(item) => String(item.sessionId) === sessionId,
);
if (currentIndex >= 0) {
next.splice(currentIndex, 1, {
...next[currentIndex],
if (currentIndex === -1) {
next.push({
...session,
sessionId,
});
} else {
next.push({
next.splice(currentIndex, 1, {
...next[currentIndex],
...session,
sessionId,
});
@@ -318,7 +436,8 @@ async function loadConversation(sessionId: string) {
try {
const detailRes = await getAgentSession(sessionId);
const res = await getAgentConversation(sessionId);
const latestRuntimeSnapshot = agentChatRuntimeManager.getSnapshot(sessionId);
const latestRuntimeSnapshot =
agentChatRuntimeManager.getSnapshot(sessionId);
if (latestRuntimeSnapshot?.sending) {
syncRuntimeSnapshot(sessionId);
await syncSessionRoute(sessionId);
@@ -335,6 +454,24 @@ async function loadConversation(sessionId: string) {
if (session?.assistantId) {
selectedAgentId.value = String(session.assistantId);
}
if (detailRes.errorCode === 0 && detailRes.data) {
extraKnowledgeIds.value = (detailRes.data.extraKnowledges || [])
.map((item) => String(item.id || ''))
.filter(Boolean);
for (const item of detailRes.data.extraKnowledges || []) {
if (!item.id) {
continue;
}
knowledgeMap.value.set(String(item.id), {
id: String(item.id),
title: item.title || item.alias || String(item.id),
});
}
if ((detailRes.data.removedExtraKnowledgeNames || []).length > 0) {
const removedNames = detailRes.data.removedExtraKnowledgeNames || [];
ElMessage.warning(`以下知识库已失效并移除:${removedNames.join('、')}`);
}
}
currentSessionId.value = sessionId;
sending.value = false;
await syncSessionRoute(sessionId);
@@ -349,6 +486,7 @@ async function createNewSession() {
currentSessionId.value = '';
timelineItems.value = [];
promptText.value = '';
extraKnowledgeIds.value = [];
sending.value = false;
await syncSessionRoute();
}
@@ -362,20 +500,77 @@ async function bindCreatedSession(sessionId: string, prompt: string) {
(session) => String(session.sessionId) === sessionId,
);
const nextSession = buildOptimisticSession(sessionId, prompt);
if (existingIndex >= 0) {
upsertSessionRecord(nextSession);
} else {
if (existingIndex === -1) {
sessions.value = [nextSession, ...sessions.value];
} else {
upsertSessionRecord(nextSession);
}
await syncSessionRoute(sessionId);
}
function handleAgentChange() {
extraKnowledgeIds.value = [];
if (timelineItems.value.length > 0 || currentSessionId.value) {
void createNewSession();
}
}
async function handleExtraKnowledgeIdsChange(value: string[]) {
const previousIds = [...extraKnowledgeIds.value];
const nextIds = value.map(String);
extraKnowledgeIds.value = nextIds;
if (!currentSessionId.value) {
return;
}
savingExtraKnowledges.value = true;
try {
const res = await saveAgentSessionExtraKnowledges(
currentSessionId.value,
nextIds,
);
if (res.errorCode !== 0 || !res.data) {
throw new Error(res.message || '知识库保存失败');
}
extraKnowledgeIds.value = (res.data.extraKnowledges || [])
.map((item) => String(item.id || ''))
.filter(Boolean);
upsertSessionRecord({
...res.data,
sessionId: currentSessionId.value,
});
} catch (error) {
extraKnowledgeIds.value = previousIds;
ElMessage.error(error instanceof Error ? error.message : '知识库保存失败');
} finally {
savingExtraKnowledges.value = false;
}
}
async function handleTriggerSelect(item: ChatInputTriggerItem) {
if (item.disabled) {
return;
}
if (chatInputTrigger.activePanel.value?.symbol !== '@') {
await chatInputTrigger.replaceTriggerText('');
return;
}
const nextIds = extraKnowledgeIds.value.map(String);
if (!nextIds.includes(String(item.id))) {
nextIds.push(String(item.id));
await handleExtraKnowledgeIdsChange(nextIds);
}
await chatInputTrigger.replaceTriggerText('');
}
function buildCapabilities() {
return [
{
resourceIds: [...extraKnowledgeIds.value],
type: 'KNOWLEDGE' as const,
},
];
}
async function handleSend() {
const content = promptText.value.trim();
if (!content || !selectedAgentId.value || sending.value) {
@@ -392,6 +587,7 @@ async function handleSend() {
agentId: selectedAgentId.value,
agentName: selectedAgent.value?.name,
baseItems: timelineItems.value,
capabilities: buildCapabilities(),
prompt: content,
sessionId: currentSessionId.value,
});
@@ -406,6 +602,55 @@ async function handleSend() {
}
}
function handlePromptInput() {
chatInputTrigger.sync();
}
function handlePromptKeyup() {
chatInputTrigger.sync();
}
function handlePromptClick() {
chatInputTrigger.sync();
}
function handlePromptKeydown(event: Event | KeyboardEvent) {
if (!(event instanceof KeyboardEvent)) {
return;
}
if (chatInputTrigger.activePanel.value) {
if (event.key === 'ArrowDown') {
event.preventDefault();
chatInputTrigger.move(1);
return;
}
if (event.key === 'ArrowUp') {
event.preventDefault();
chatInputTrigger.move(-1);
return;
}
if (event.key === 'Escape') {
event.preventDefault();
chatInputTrigger.close();
return;
}
if (event.key === 'Enter' && !event.shiftKey) {
const item =
chatInputTrigger.visibleItems.value[chatInputTrigger.activeIndex.value];
if (item) {
event.preventDefault();
void handleTriggerSelect(item);
return;
}
}
}
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
void handleSend();
}
}
function handleStop() {
if (!canStopRuntime.value) {
return;
@@ -539,7 +784,7 @@ async function handleReject(payload: ChatTimelineToolApprovalPayload) {
}
async function bootstrap() {
await Promise.all([loadAgents(), loadSessions()]);
await Promise.all([loadAgents(), loadSessions(), loadKnowledges()]);
const routeSessionId = String(route.query.sessionId || '');
if (routeSessionId) {
await loadConversation(routeSessionId);
@@ -645,10 +890,17 @@ onBeforeUnmount(() => {
</div>
</header>
<div class="agent-chat__timeline-wrap">
<div
class="agent-chat__timeline-wrap"
:class="{ 'is-welcome': isWelcomeState }"
>
<div v-if="loadingConversation" class="agent-chat__state is-center">
加载中
</div>
<AgentChatWelcomeState
v-else-if="isWelcomeState"
:title="welcomeTitle"
/>
<ChatTimeline
v-else
:items="timelineItems"
@@ -663,8 +915,31 @@ onBeforeUnmount(() => {
/>
</div>
<div class="agent-chat__composer">
<div
class="agent-chat__composer"
:class="{ 'is-welcome': isWelcomeState }"
>
<ChatCapabilityMenu
:disabled="capabilityDisabled"
:extra-knowledge-ids="extraKnowledgeIds"
:knowledge-options="knowledgeOptions"
:loading="loadingKnowledges"
:selected-knowledges="selectedExtraKnowledges"
:show-trigger="false"
@update:extra-knowledge-ids="handleExtraKnowledgeIdsChange"
/>
<ChatInputTriggerPanel
v-if="chatInputTrigger.activePanel.value"
class="agent-chat__trigger-panel"
:active-index="chatInputTrigger.activeIndex.value"
:group-label="chatInputTrigger.activePanel.value.groupLabel"
:items="chatInputTrigger.visibleItems.value"
:keyword="chatInputTrigger.activePanel.value.keyword"
@select="handleTriggerSelect"
@set-active="chatInputTrigger.setActiveIndex"
/>
<ElInput
ref="promptInputRef"
v-model="promptText"
class="agent-chat__composer-input"
type="textarea"
@@ -672,27 +947,41 @@ onBeforeUnmount(() => {
resize="none"
:placeholder="composerPlaceholder"
:disabled="sending || runtimeRunning || !selectedAgentId"
@keydown.enter.exact.prevent="handleSend"
@click="handlePromptClick"
@input="handlePromptInput"
@keydown="handlePromptKeydown"
@keyup="handlePromptKeyup"
/>
<div class="agent-chat__composer-footer">
<ElSelect
v-model="selectedAgentId"
:loading="loadingAgents"
placeholder="选择智能体"
class="agent-chat__agent-select"
:style="{ width: agentSelectWidth }"
@change="handleAgentChange"
>
<ElOption
v-for="agent in agents"
:key="String(agent.id)"
:label="agent.name || String(agent.id)"
:value="String(agent.id)"
<div class="agent-chat__composer-tools">
<ChatCapabilityMenu
class="agent-chat__capability-entry"
:disabled="capabilityDisabled"
:extra-knowledge-ids="extraKnowledgeIds"
:knowledge-options="knowledgeOptions"
:loading="loadingKnowledges"
:selected-knowledges="[]"
@update:extra-knowledge-ids="handleExtraKnowledgeIdsChange"
/>
</ElSelect>
<ElSelect
v-model="selectedAgentId"
:loading="loadingAgents"
placeholder="选择智能体"
class="agent-chat__agent-select"
:style="{ width: agentSelectWidth }"
@change="handleAgentChange"
>
<ElOption
v-for="agent in agents"
:key="String(agent.id)"
:label="agent.name || String(agent.id)"
:value="String(agent.id)"
/>
</ElSelect>
</div>
<div class="agent-chat__composer-actions">
<ElButton
v-if="canStopRuntime"
v-if="canStopRuntime"
type="primary"
circle
aria-label="中止"
@@ -700,7 +989,7 @@ onBeforeUnmount(() => {
class="agent-chat__send-button is-stop"
@click="handleStop"
>
<span class="agent-chat__stop-glyph" />
<span class="agent-chat__stop-glyph"></span>
</ElButton>
<ElButton
v-else
@@ -863,6 +1152,11 @@ onBeforeUnmount(() => {
box-sizing: border-box;
}
.agent-chat__timeline-wrap.is-welcome {
justify-content: center;
padding: 0 min(8vw, 96px) 252px;
}
.agent-chat__timeline-wrap :deep(.chat-timeline) {
height: 100%;
min-height: 0;
@@ -886,6 +1180,20 @@ onBeforeUnmount(() => {
box-shadow: var(--el-box-shadow-light);
}
.agent-chat__composer.is-welcome {
top: calc(50% + 40px);
bottom: auto;
transform: translateY(-50%);
}
.agent-chat__trigger-panel {
position: absolute;
bottom: calc(100% + 10px);
left: 0;
z-index: 5;
width: 100%;
}
.agent-chat__composer-input :deep(.el-textarea__inner) {
min-height: 48px !important;
padding: 0;
@@ -902,13 +1210,25 @@ onBeforeUnmount(() => {
justify-content: space-between;
}
.agent-chat__composer-tools {
display: inline-flex;
align-items: center;
min-width: 0;
gap: 4px;
max-width: calc(100% - 64px);
}
.agent-chat__capability-entry {
flex: none;
}
.agent-chat__agent-select {
max-width: min(240px, 58%);
max-width: min(320px, calc(100vw - 240px));
}
.agent-chat__agent-select :deep(.el-select__wrapper) {
min-height: 36px;
padding: 0;
padding: 0 4px 0 0;
background: transparent;
border: 0;
box-shadow: none;
@@ -921,7 +1241,7 @@ onBeforeUnmount(() => {
.agent-chat__agent-select :deep(.el-select__placeholder),
.agent-chat__agent-select :deep(.el-select__selected-item) {
min-width: 0;
max-width: 184px;
max-width: none;
overflow: hidden;
font-size: 14px;
font-weight: 400;
@@ -932,6 +1252,7 @@ onBeforeUnmount(() => {
.agent-chat__agent-select :deep(.el-select__caret) {
color: var(--el-color-primary);
margin-left: 6px;
}
.agent-chat__composer-actions {
@@ -1002,6 +1323,10 @@ onBeforeUnmount(() => {
padding-bottom: 184px;
}
.agent-chat__timeline-wrap.is-welcome {
padding: 0 16px 244px;
}
.agent-chat__timeline-wrap :deep(.chat-timeline) {
padding: 16px;
}
@@ -1012,12 +1337,18 @@ onBeforeUnmount(() => {
left: 16px;
}
.agent-chat__composer.is-welcome {
top: calc(50% + 52px);
bottom: auto;
}
.agent-chat__composer-footer {
align-items: flex-end;
}
.agent-chat__agent-select {
width: min(220px, calc(100% - 58px));
width: min(280px, calc(100% - 58px));
max-width: calc(100vw - 128px);
}
.agent-chat__composer-actions {