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

@@ -34,6 +34,8 @@ import {useAgentDesignerState} from './composables/useAgentDesignerState';
const route = useRoute();
const router = useRouter();
const AGENT_TAB_PAGE_KEY = '/ai/agents';
const DEFAULT_AGENT_TITLE = '未命名智能体';
const {
state,
addKnowledgeNode,
@@ -52,6 +54,7 @@ const {
const pageLoading = ref(false);
const saveLoading = ref(false);
const offlineLoading = ref(false);
const publishLoading = ref(false);
const issues = ref<AgentValidationIssue[]>([]);
const categories = ref<AgentOption[]>([]);
@@ -62,14 +65,6 @@ const pluginTools = ref<AgentOption[]>([]);
const isNew = computed(() => String(route.params.id || '') === 'new');
const publishText = computed(() => {
if (
canAiResourceOffline(
state.agent.displayPublishStatus,
state.agent.publishStatus,
)
) {
return '下线';
}
if (
canAiResourceRepublish(
state.agent.displayPublishStatus,
@@ -81,6 +76,13 @@ const publishText = computed(() => {
return '发布';
});
const offlineVisible = computed(() =>
canAiResourceOffline(
state.agent.displayPublishStatus,
state.agent.publishStatus,
),
);
const publishDisabled = computed(() => {
if (!state.agent.id) return true;
if (
@@ -99,14 +101,15 @@ const publishDisabled = computed(() => {
canAiResourceRepublish(
state.agent.displayPublishStatus,
state.agent.publishStatus,
) ||
canAiResourceOffline(
state.agent.displayPublishStatus,
state.agent.publishStatus,
)
);
});
const offlineDisabled = computed(() => {
if (!state.agent.id) return true;
return !offlineVisible.value;
});
onMounted(async () => {
pageLoading.value = true;
try {
@@ -119,14 +122,55 @@ onMounted(async () => {
async function loadAgent() {
if (isNew.value) {
reset();
syncNavTitle(DEFAULT_AGENT_TITLE, { force: true });
return;
}
const [, res] = await tryit(getAgentDetail)(String(route.params.id));
if (res?.errorCode === 0) {
reset(res.data);
syncNavTitle(resolveAgentTitle(res.data), { force: !hasNavTitle() });
}
}
function hasNavTitle() {
const navTitle = Array.isArray(route.query.navTitle)
? route.query.navTitle[0]
: route.query.navTitle;
return typeof navTitle === 'string' && navTitle.trim();
}
function resolveAgentTitle(agent = state.agent) {
return String(agent.name || '').trim() || DEFAULT_AGENT_TITLE;
}
function syncNavTitle(title: string, options: { force?: boolean } = {}) {
const normalizedTitle = String(title || '').trim() || DEFAULT_AGENT_TITLE;
const query = route.query as Record<string, any>;
const currentNavTitle = Array.isArray(query.navTitle)
? query.navTitle[0]
: query.navTitle;
const currentPageKey = Array.isArray(query.pageKey)
? query.pageKey[0]
: query.pageKey;
if (
!options.force &&
currentNavTitle === normalizedTitle &&
currentPageKey === AGENT_TAB_PAGE_KEY
) {
return;
}
router.replace({
path: route.path,
query: {
...query,
pageKey: AGENT_TAB_PAGE_KEY,
navTitle: normalizedTitle,
},
});
}
async function loadOptions() {
const [categoryRes, modelRes, knowledgeRes, workflowRes, pluginRes] =
await Promise.all([
@@ -245,8 +289,18 @@ async function handleSave(showMessage = true) {
id,
};
state.dirty = false;
const title = resolveAgentTitle();
if (isNew.value) {
await router.replace(`/ai/agents/designer/${id}`);
await router.replace({
path: `/ai/agents/designer/${id}`,
query: {
...route.query,
pageKey: AGENT_TAB_PAGE_KEY,
navTitle: title,
},
});
} else {
syncNavTitle(title, { force: true });
}
if (showMessage) {
ElMessage.success('已保存');
@@ -262,29 +316,19 @@ async function handlePublish() {
const saved = await handleSave(false);
if (!saved) return;
const offline = canAiResourceOffline(
state.agent.displayPublishStatus,
state.agent.publishStatus,
);
try {
await ElMessageBox.confirm(
offline ? '确认提交下线审批?' : '确认提交发布审批?',
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: offline ? 'warning' : 'info',
},
);
await ElMessageBox.confirm('确认提交发布审批?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'info',
});
} catch {
return;
}
publishLoading.value = true;
try {
const res = offline
? await submitAgentOfflineApproval(String(state.agent.id))
: await submitAgentPublishApproval(String(state.agent.id));
const res = await submitAgentPublishApproval(String(state.agent.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || '已提交');
await loadAgent();
@@ -294,6 +338,33 @@ async function handlePublish() {
}
}
async function handleOffline() {
if (!state.agent.id) return;
const saved = await handleSave(false);
if (!saved) return;
try {
await ElMessageBox.confirm('确认提交下线审批?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
});
} catch {
return;
}
offlineLoading.value = true;
try {
const res = await submitAgentOfflineApproval(String(state.agent.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || '已提交');
await loadAgent();
}
} finally {
offlineLoading.value = false;
}
}
function handleTryout() {
if (!runValidation()) return;
openTryout();
@@ -330,8 +401,12 @@ function handleCloseTryout() {
:publish-loading="publishLoading"
:publish-disabled="publishDisabled"
:publish-text="publishText"
:offline-disabled="offlineDisabled"
:offline-loading="offlineLoading"
:offline-visible="offlineVisible"
@add="handleAdd"
@save="handleSave()"
@offline="handleOffline"
@publish="handlePublish"
@tryout="handleTryout"
/>

View File

@@ -36,6 +36,8 @@ import {
const router = useRouter();
const pageDataRef = ref();
const sideList = ref<any[]>([]);
const AGENT_TAB_PAGE_KEY = '/ai/agents';
const DEFAULT_AGENT_TITLE = '未命名智能体';
const headerButtons = [
{
@@ -53,7 +55,13 @@ const primaryAction: CardPrimaryAction = {
text: '编排',
permission: '/api/v1/agent/update',
onClick(row: AgentInfo) {
router.push(`/ai/agents/designer/${row.id}`);
router.push({
path: `/ai/agents/designer/${row.id}`,
query: {
pageKey: AGENT_TAB_PAGE_KEY,
navTitle: resolveNavTitle(row),
},
});
},
};
@@ -106,10 +114,20 @@ function handleSearch(keyword: string) {
function handleButtonClick(payload: any) {
if (payload?.key === 'create' || payload?.data?.action === 'create') {
router.push('/ai/agents/designer/new');
router.push({
path: '/ai/agents/designer/new',
query: {
pageKey: AGENT_TAB_PAGE_KEY,
navTitle: DEFAULT_AGENT_TITLE,
},
});
}
}
function resolveNavTitle(row: AgentInfo) {
return String(row.name || '').trim() || DEFAULT_AGENT_TITLE;
}
function changeCategory(category: any) {
pageDataRef.value?.setQuery({ categoryId: category.id });
}

View File

@@ -1,19 +1,14 @@
<script setup lang="ts">
import type {AgentCapabilityKind} from '../types';
import {onBeforeUnmount, onMounted, ref} from 'vue';
import {computed, onBeforeUnmount, onMounted, ref} from 'vue';
import {
Connection,
Files,
Loading,
Plus,
Promotion,
Share,
VideoPlay,
} from '@element-plus/icons-vue';
import {Connection, Files, Loading, Plus, Share, VideoPlay,} from '@element-plus/icons-vue';
defineProps<{
const props = defineProps<{
offlineDisabled?: boolean;
offlineLoading?: boolean;
offlineVisible?: boolean;
publishDisabled?: boolean;
publishLoading?: boolean;
publishText: string;
@@ -21,8 +16,11 @@ defineProps<{
tryoutDisabled?: boolean;
}>();
const isRepublish = computed(() => props.publishText === '重新发布');
const emit = defineEmits<{
add: [kind: AgentCapabilityKind];
offline: [];
publish: [];
save: [];
tryout: [];
@@ -120,13 +118,23 @@ onBeforeUnmount(() => {
</button>
<div class="agent-command-bar__divider"></div>
<button
v-if="offlineVisible"
class="agent-command-bar__button agent-command-bar__button--ghost"
type="button"
:disabled="offlineDisabled || offlineLoading || publishLoading"
@click="emit('offline')"
>
<Loading v-if="offlineLoading" class="is-loading" />
<span>下线</span>
</button>
<button
class="agent-command-bar__button agent-command-bar__button--ghost"
:class="{ 'agent-command-bar__button--republish': isRepublish }"
type="button"
:disabled="publishDisabled || publishLoading"
@click="emit('publish')"
>
<Loading v-if="publishLoading" class="is-loading" />
<Promotion v-else />
<span>{{ publishText }}</span>
</button>
</div>
@@ -280,11 +288,21 @@ onBeforeUnmount(() => {
color: var(--el-text-color-secondary);
}
.agent-command-bar__button--republish {
color: var(--el-color-warning);
background: var(--el-color-warning-light-9);
}
.agent-command-bar__button:hover:not(:disabled) {
color: var(--el-text-color-primary);
background: var(--el-fill-color-light);
}
.agent-command-bar__button--republish:hover:not(:disabled) {
color: var(--el-color-warning);
background: var(--el-color-warning-light-8);
}
.agent-command-bar__button--primary:hover:not(:disabled) {
color: var(--el-color-primary);
background: var(--el-color-primary-light-8);

View File

@@ -98,6 +98,37 @@ describe('useAgentTryoutRawRounds', () => {
]);
});
it('AgentScope context_reload 工具事件不进入页面时间线', () => {
const store = useAgentTryoutRawRounds({
mode: 'draft',
sessionId: 'session-raw-context-reload',
});
const roundId = store.createRound('展开第一层');
store.recordEvent(roundId, {
domain: 'TOOL',
payload: {
input: { working_context_offload_uuid: 'context-id' },
toolCallId: 'context-reload-1',
toolName: 'context_reload',
},
type: 'TOOL_CALL',
});
store.recordEvent(roundId, {
domain: 'TOOL',
payload: {
output: 'context',
toolCallId: 'context-reload-1',
toolName: 'context_reload',
},
type: 'TOOL_RESULT',
});
expect(store.buildTimelineItems().map((item) => item.type)).toEqual([
'message',
]);
});
it('刷新后能从 raw rounds 恢复 timeline', () => {
const first = useAgentTryoutRawRounds({
mode: 'draft',

View File

@@ -75,7 +75,11 @@ function normalizeToolName(value: unknown) {
function isHiddenToolName(value: unknown) {
const normalizedName = normalizeToolName(value);
return normalizedName === 'retrieve_knowledge' || normalizedName === '__fragment__';
return (
normalizedName === 'retrieve_knowledge' ||
normalizedName === 'context_reload' ||
normalizedName === '__fragment__'
);
}
function clone<T>(value: T): T {