fix: 完成系统向智能体数据链路切换

- 切换工作台、聊天历史、资源候选与公共调用到 Agent

- 加固资源绑定、删除保护及发布运行并发控制

- 隔离旧 Bot 专属服务和组件并保留兼容入口
This commit is contained in:
2026-07-31 14:24:15 +08:00
parent f0aba1eddd
commit f872eac1f9
114 changed files with 5997 additions and 874 deletions

View File

@@ -15,7 +15,7 @@ export interface DashboardUserRankQuery extends DashboardOverviewQuery {
export interface DashboardSummary {
activeUserTotal: number;
activeAssistantTotal: number;
botTotal: number;
agentTotal: number;
chatActiveUserTotal: number;
chatMessageTotal: number;
chatSessionTotal: number;
@@ -55,7 +55,6 @@ export interface DashboardDistributionItem {
assistantId?: number | string;
avgMessagePerSession?: number;
avgSessionPerUser?: number;
botTotal: number;
key: string;
knowledgeBaseTotal: number;
label: string;

View File

@@ -96,11 +96,11 @@
"submitRepublishApprovalConfirm": "Republish the current workflow now?",
"submitOfflineApprovalConfirm": "Take the current workflow offline?",
"submitDeleteApprovalConfirm": "Delete the current workflow?",
"offlineImpactBoundBotsIntro": "This workflow is currently bound to the following bots:",
"offlineImpactBoundBotsFooter": "After the workflow goes offline, the system will automatically remove it from these bots.",
"offlineImpactBoundAgentsIntro": "This workflow is currently bound to the following agents:",
"offlineImpactBoundAgentsFooter": "After the workflow goes offline, the system will automatically remove it from these agents.",
"offlineImpactBoundPluginsIntro": "This workflow is currently bound to the following plugins:",
"offlineImpactBoundPluginsFooter": "After offline approval succeeds, these plugins will automatically become unavailable and show the reason in plugin management.",
"offlineImpactBoundMixedFooter": "After offline approval succeeds, the system will remove the workflow from bots and mark the related plugins as unavailable.",
"offlineImpactBoundMixedFooter": "After offline approval succeeds, the system will remove the workflow from agents and mark the related plugins as unavailable.",
"publishPendingHint": "There is already an approval in progress for this workflow.",
"deletePendingHint": "There is already an approval in progress for this workflow.",
"check": "Check",

View File

@@ -7,6 +7,7 @@
"initiated": "Initiated"
},
"resource": {
"agent": "Agent",
"bot": "Chat Assistant",
"workflow": "Workflow",
"knowledge": "Knowledge Base"
@@ -144,6 +145,8 @@
"knowledgeBasic": "Basic Info",
"knowledgeConfig": "Retrieval Config",
"botOverview": "Assistant Overview",
"agentModelConfig": "Model Config",
"agentBindings": "Capability Bindings",
"botModelConfig": "Model Config",
"botBindings": "Capability Bindings",
"systemPrompt": "System Prompt",
@@ -168,6 +171,7 @@
"notConfigured": "Not configured",
"noBindings": "No bindings",
"untitledKnowledge": "Untitled knowledge base",
"untitledAgent": "Untitled agent",
"untitledBot": "Untitled assistant",
"unnamedKnowledge": "Unnamed knowledge base",
"unnamedWorkflow": "Unnamed workflow",

View File

@@ -47,8 +47,8 @@
"submitRepublishApprovalConfirm": "Republish the current knowledge base now?",
"submitOfflineApprovalConfirm": "Take the current knowledge base offline?",
"submitDeleteApprovalConfirm": "Delete the current knowledge base?",
"offlineImpactBoundBotsIntro": "This knowledge base is currently bound to the following bots:",
"offlineImpactBoundBotsFooter": "After the knowledge base goes offline, the system will automatically remove it from these bots.",
"offlineImpactBoundAgentsIntro": "This knowledge base is currently bound to the following agents:",
"offlineImpactBoundAgentsFooter": "After the knowledge base goes offline, the system will automatically remove it from these agents.",
"offlineImpactWorkflowBlockedIntro": "This knowledge base is still used by the following workflows:",
"offlineImpactWorkflowBlockedFooter": "Please update those workflow nodes before taking the knowledge base offline.",
"publishPendingHint": "There is already an approval in progress for this knowledge base.",

View File

@@ -96,11 +96,11 @@
"submitRepublishApprovalConfirm": "确认重新发布当前工作流吗?",
"submitOfflineApprovalConfirm": "确认下线当前工作流吗?",
"submitDeleteApprovalConfirm": "确认删除当前工作流吗?",
"offlineImpactBoundBotsIntro": "当前工作流被以下聊天助手绑定:",
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该工作流。",
"offlineImpactBoundAgentsIntro": "当前工作流被以下智能体绑定:",
"offlineImpactBoundAgentsFooter": "下线成功后,系统会自动从这些智能体中解绑该工作流。",
"offlineImpactBoundPluginsIntro": "当前工作流被以下插件绑定:",
"offlineImpactBoundPluginsFooter": "下线审批通过后,这些插件会自动变为不可用,并在插件页展示对应原因。",
"offlineImpactBoundMixedFooter": "下线审批通过后,系统会自动从聊天助手中解绑该工作流,同时让相关插件进入不可用状态。",
"offlineImpactBoundMixedFooter": "下线审批通过后,系统会自动从智能体中解绑该工作流,同时让相关插件进入不可用状态。",
"publishPendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
"deletePendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
"check": "检查",

View File

@@ -7,6 +7,7 @@
"initiated": "我发起"
},
"resource": {
"agent": "智能体",
"bot": "聊天助手",
"workflow": "工作流",
"knowledge": "知识库"
@@ -144,6 +145,8 @@
"knowledgeBasic": "基础信息",
"knowledgeConfig": "检索配置",
"botOverview": "助手概览",
"agentModelConfig": "模型配置",
"agentBindings": "能力绑定",
"botModelConfig": "模型配置",
"botBindings": "能力绑定",
"systemPrompt": "系统提示词",
@@ -168,6 +171,7 @@
"notConfigured": "未配置",
"noBindings": "未绑定任何能力",
"untitledKnowledge": "未命名知识库",
"untitledAgent": "未命名智能体",
"untitledBot": "未命名聊天助手",
"unnamedKnowledge": "未命名知识库",
"unnamedWorkflow": "未命名工作流",

View File

@@ -47,8 +47,8 @@
"submitRepublishApprovalConfirm": "确认重新发布当前知识库吗?",
"submitOfflineApprovalConfirm": "确认下线当前知识库吗?",
"submitDeleteApprovalConfirm": "确认删除当前知识库吗?",
"offlineImpactBoundBotsIntro": "当前知识库被以下聊天助手绑定:",
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该知识库。",
"offlineImpactBoundAgentsIntro": "当前知识库被以下智能体绑定:",
"offlineImpactBoundAgentsFooter": "下线成功后,系统会自动从这些智能体中解绑该知识库。",
"offlineImpactWorkflowBlockedIntro": "当前知识库仍被以下工作流使用:",
"offlineImpactWorkflowBlockedFooter": "请先在工作流中调整相关知识库节点后再下线。",
"publishPendingHint": "当前知识库已有进行中的审批,请等待处理完成。",

View File

@@ -78,7 +78,7 @@ export interface AgentChatCapabilityPayload {
}
export function getPublishedAgents() {
return api.get<RequestResult<AgentInfo[]>>('/api/v1/agent/list', {
return api.get<RequestResult<AgentInfo[]>>('/api/v1/agent/options', {
params: { publishedOnly: true },
});
}
@@ -95,10 +95,7 @@ export function getAgentSession(sessionId: number | string) {
export function getPublishedKnowledges() {
return api.get<RequestResult<AgentChatKnowledgeView[]>>(
'/api/v1/documentCollection/list',
{
params: { publishedOnly: true },
},
'/api/v1/agent/knowledgeOptions',
);
}

View File

@@ -23,10 +23,8 @@ import {
import {
getAgentDetail,
getAgentModels,
getMcpPage,
getMcpTools,
getPublishedKnowledgeList,
getAgentMcpToolOptions,
getAgentResourceOptions,
saveAgent,
submitAgentOfflineApproval,
submitAgentPublishApproval,
@@ -76,7 +74,7 @@ const workflows = ref<AgentOption[]>([]);
const pluginTools = ref<AgentOption[]>([]);
const mcps = ref<AgentOption[]>([]);
const fetchMcpToolResource = createMcpToolLoader(async (id) => {
const res = await getMcpTools(id);
const res = await getAgentMcpToolOptions(id);
return res.errorCode === 0 ? res.data : undefined;
});
@@ -128,7 +126,6 @@ const offlineDisabled = computed(() => {
});
onMounted(async () => {
void loadDeferredOptions();
try {
await Promise.all([loadCriticalOptions(), loadAgent()]);
} finally {
@@ -213,11 +210,11 @@ function syncNavTitle(title: string, options: { force?: boolean } = {}) {
}
async function loadCriticalOptions() {
const [categoryResult, modelResult] = await Promise.allSettled([
const [categoryResult, resourceResult] = await Promise.allSettled([
api.get('/api/v1/agentCategory/visibleList', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
getAgentModels(),
getAgentResourceOptions(),
]);
if (categoryResult.status === 'fulfilled') {
@@ -227,71 +224,33 @@ async function loadCriticalOptions() {
raw: item,
}));
}
if (modelResult.status === 'fulfilled') {
models.value = (modelResult.value.data || []).map((item: any) => ({
if (resourceResult.status === 'fulfilled') {
const resources = resourceResult.value.data;
if (resourceResult.value.errorCode !== 0 || !resources) {
return;
}
models.value = (resources.models || []).map((item: any) => ({
label: item.title || item.name,
value: String(item.id),
raw: item,
}));
}
}
async function loadDeferredOptions() {
const [knowledgeResult, workflowResult, pluginResult, mcpResult] =
await Promise.allSettled([
getPublishedKnowledgeList(),
api.get('/api/v1/workflow/page', {
params: { pageNumber: 1, pageSize: 200 },
}),
api.get('/api/v1/plugin/pageByCategory', {
params: { pageNumber: 1, pageSize: 200, category: 0 },
}),
getMcpPage(),
]);
if (knowledgeResult.status === 'fulfilled') {
knowledges.value = (knowledgeResult.value.data || []).map((item: any) => ({
knowledges.value = (resources.knowledges || []).map((item: any) => ({
label: item.title || item.name,
value: String(item.id),
raw: item,
}));
}
if (workflowResult.status === 'fulfilled') {
workflows.value = (
(workflowResult.value.data?.records ||
workflowResult.value.data ||
[]) as any[]
).map((item) => ({
workflows.value = (resources.workflows || []).map((item: any) => ({
label: item.title || item.name,
value: String(item.id),
raw: item,
}));
pluginTools.value = (resources.pluginTools || []).map((item: any) => ({
label: item.name || item.title,
value: String(item.id),
raw: item,
}));
mcps.value = mapMcpOptions(resources.mcps || []);
}
if (pluginResult.status === 'fulfilled') {
pluginTools.value = flattenPluginTools(
pluginResult.value.data?.records || pluginResult.value.data || [],
);
}
if (mcpResult.status === 'fulfilled') {
mcps.value = mapMcpOptions(
mcpResult.value.data?.records || mcpResult.value.data || [],
);
}
}
function flattenPluginTools(list: any[]): AgentOption[] {
const result: AgentOption[] = [];
list.forEach((plugin) => {
const tools = Array.isArray(plugin.tools) ? plugin.tools : [];
tools.forEach((tool: any) => {
result.push({
label: tool.name || tool.title,
value: String(tool.id),
raw: { ...tool, pluginName: plugin.name || plugin.title },
});
});
});
return result;
}
function mapMcpOptions(list: any[]): AgentOption[] {
@@ -325,8 +284,7 @@ async function loadMcpToolsForOption(id: number | string) {
const currentOption = mcps.value.find((item) => String(item.value) === key);
const mergedResource = {
...currentOption?.raw,
...resource,
tools: Array.isArray(resource.tools) ? resource.tools : [],
tools: Array.isArray(resource) ? resource : [],
};
if (currentOption) {
mcps.value = mcps.value.map((item) =>
@@ -334,7 +292,7 @@ async function loadMcpToolsForOption(id: number | string) {
? {
...item,
label:
resource.title || resource.name || currentOption.label || 'MCP',
currentOption.label || 'MCP',
raw: mergedResource,
}
: item,

View File

@@ -12,7 +12,7 @@ import {Delete, Edit, Plus, Promotion} from '@element-plus/icons-vue';
import {ElMessage, ElMessageBox, ElTag} from 'element-plus';
import {tryit} from 'radash';
import defaultAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
import defaultAgentAvatar from '#/assets/defaultUserAvatar.png';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
@@ -262,7 +262,7 @@ async function handleDeleteAction(row: AgentInfo) {
<CardList
title-field="name"
icon-field="avatar"
:default-icon="defaultAvatar"
:default-icon="defaultAgentAvatar"
:data="pageList"
:primary-action="primaryAction"
:actions="actions"

View File

@@ -1,6 +1,10 @@
import type {AgentInfo, AgentKnowledgeBinding, AgentToolBinding,} from './types';
import type {
AgentInfo,
AgentKnowledgeBinding,
AgentToolBinding,
} from './types';
import {api} from '#/api/request';
import { api } from '#/api/request';
export interface RequestResult<T = any> {
data: T;
@@ -22,10 +26,6 @@ export function updateAgent(agent: AgentInfo) {
return api.post<RequestResult<AgentInfo>>('/api/v1/agent/update', agent);
}
export function removeAgent(id: number | string) {
return api.post<RequestResult>('/api/v1/agent/remove', { id });
}
export function updateAgentToolBindings(
agentId: number | string,
bindings: AgentToolBinding[],
@@ -98,24 +98,22 @@ export function getAgentCategories() {
});
}
export function getAgentModels() {
return api.get<RequestResult<any[]>>('/api/v1/model/list', {
params: { modelType: 'chatModel', added: true },
});
export interface AgentResourceOptions {
knowledges: any[];
mcps: any[];
models: any[];
pluginTools: any[];
workflows: any[];
}
export function getPublishedKnowledgeList() {
return api.get<RequestResult<any[]>>('/api/v1/documentCollection/list', {
params: { publishedOnly: true },
});
export function getAgentResourceOptions() {
return api.get<RequestResult<AgentResourceOptions>>(
'/api/v1/agent/resourceOptions',
);
}
export function getMcpPage() {
return api.get<RequestResult<any>>('/api/v1/mcp/page', {
params: { pageNumber: 1, pageSize: 200, status: 1 },
export function getAgentMcpToolOptions(id: number | string) {
return api.get<RequestResult<any[]>>('/api/v1/agent/mcpToolOptions', {
params: { id },
});
}
export function getMcpTools(id: number | string) {
return api.post<RequestResult<any>>('/api/v1/mcp/getMcpTools', { id });
}

View File

@@ -6,7 +6,7 @@ import pageSource from './index.vue?raw';
describe('管理端智能体聊天历史契约', () => {
it('使用智能体候选接口并保留 assistantId 查询参数', () => {
expect(pageSource).toContain("'/api/v1/agent/list'");
expect(pageSource).toContain("'/api/v1/agent/options'");
expect(pageSource).not.toContain('/api/v1/bot/list');
expect(pageSource).toContain('label: item.name');
expect(pageSource).toContain('assistantId: query.value.assistantId');

View File

@@ -98,7 +98,7 @@ onMounted(async () => {
async function fetchAgents() {
agentLoading.value = true;
const [error, res] = await tryit(api.get)('/api/v1/agent/list');
const [error, res] = await tryit(api.get)('/api/v1/agent/options');
agentLoading.value = false;
if (error || res?.errorCode !== 0) {
agentOptions.value = [];

View File

@@ -360,11 +360,11 @@ const submitOfflineAction = async (item: any) => {
}
try {
await ElMessageBox.confirm(
impactRes.data?.hasBotBindings
impactRes.data?.hasAgentBindings
? buildOfflineImpactMessage(
$t('documentCollection.offlineImpactBoundBotsIntro'),
impactRes.data.botBindings,
$t('documentCollection.offlineImpactBoundBotsFooter'),
$t('documentCollection.offlineImpactBoundAgentsIntro'),
impactRes.data.agentBindings,
$t('documentCollection.offlineImpactBoundAgentsFooter'),
)
: $t('documentCollection.submitOfflineApprovalConfirm'),
$t('message.noticeTitle'),

View File

@@ -7,10 +7,10 @@ export interface OfflineImpactBinding {
export interface OfflineImpactCheck {
canProceed: boolean;
hasBotBindings: boolean;
hasAgentBindings: boolean;
hasPluginBindings: boolean;
hasWorkflowUsages: boolean;
botBindings: OfflineImpactBinding[];
agentBindings: OfflineImpactBinding[];
pluginBindings: OfflineImpactBinding[];
workflowUsages: OfflineImpactBinding[];
message?: string;
@@ -33,7 +33,13 @@ export function buildOfflineImpactMessage(
h('p', intro),
h(
'ul',
items.map((item) => h('li', { key: String(item.id || item.title || '') }, resolveTitle(item))),
items.map((item) =>
h(
'li',
{ key: String(item.id || item.title || '') },
resolveTitle(item),
),
),
),
footer ? h('p', footer) : null,
]);

View File

@@ -797,15 +797,15 @@ async function submitOfflineAction(row: any) {
}
try {
const sections = [];
let offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundBotsFooter');
if (impactRes.data?.hasBotBindings) {
let offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundAgentsFooter');
if (impactRes.data?.hasAgentBindings) {
sections.push(
buildOfflineImpactMessage(
$t('aiWorkflow.offlineImpactBoundBotsIntro'),
impactRes.data.botBindings,
$t('aiWorkflow.offlineImpactBoundAgentsIntro'),
impactRes.data.agentBindings,
impactRes.data?.hasPluginBindings
? undefined
: $t('aiWorkflow.offlineImpactBoundBotsFooter'),
: $t('aiWorkflow.offlineImpactBoundAgentsFooter'),
),
);
}
@@ -814,13 +814,13 @@ async function submitOfflineAction(row: any) {
buildOfflineImpactMessage(
$t('aiWorkflow.offlineImpactBoundPluginsIntro'),
impactRes.data.pluginBindings,
impactRes.data?.hasBotBindings
impactRes.data?.hasAgentBindings
? undefined
: $t('aiWorkflow.offlineImpactBoundPluginsFooter'),
),
);
}
if (impactRes.data?.hasBotBindings && impactRes.data?.hasPluginBindings) {
if (impactRes.data?.hasAgentBindings && impactRes.data?.hasPluginBindings) {
offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundMixedFooter');
} else if (impactRes.data?.hasPluginBindings) {
offlineImpactFooter = $t('aiWorkflow.offlineImpactBoundPluginsFooter');

View File

@@ -136,8 +136,11 @@ function createDefaultEntity(row: Partial<Entity> = {}): Entity {
}
function renderPermissionLabel(item: ResourcePermission) {
if (item.requestInterface === '/public-api/bot/chat') {
return '聊天助手调用';
if (
item.requestInterface === '/public-api/agent/chat' ||
item.requestInterface === '/public-api/bot/chat'
) {
return '智能体调用';
}
if (
item.requestInterface === '/v1/chat/completions' ||

View File

@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import pageSource from './index.vue?raw';
describe('管理端工作台智能体数据契约', () => {
it('使用 Agent 候选接口和名称字段', () => {
expect(pageSource).toContain("'/api/v1/agent/options'");
expect(pageSource).not.toContain('/api/v1/bot/list');
expect(pageSource).toContain('label: item.name');
expect(pageSource).not.toContain('publishedOnly');
});
it('使用 Agent 总数字段', () => {
expect(pageSource).toContain('summary.value.agentTotal');
expect(pageSource).not.toContain('summary.value.botTotal');
});
});

View File

@@ -114,7 +114,7 @@ const trendModeOptions: Array<{ label: string; value: DashboardTrendMode }> = [
const emptySummary: DashboardSummary = {
activeAssistantTotal: 0,
activeUserTotal: 0,
botTotal: 0,
agentTotal: 0,
chatActiveUserTotal: 0,
chatMessageTotal: 0,
chatSessionTotal: 0,
@@ -178,7 +178,7 @@ const summaryCards = computed<SummaryCardItem[]>(() => [
chatAvailable.value,
),
},
{ label: '智能体总数', value: formatCount(summary.value.botTotal) },
{ label: '智能体总数', value: formatCount(summary.value.agentTotal) },
{
label: '知识库总数',
value: formatCount(summary.value.knowledgeBaseTotal),
@@ -330,15 +330,14 @@ async function loadOverview() {
async function loadAssistantOptions() {
assistantOptionsLoading.value = true;
try {
const bots = await requestClient.get<
Array<{ id?: number | string; title?: string }>
>('/api/v1/bot/list', {
params: { status: 1 },
});
const agents =
await requestClient.get<Array<{ id?: number | string; name?: string }>>(
'/api/v1/agent/options',
);
const nextOptions: AssistantOptionItem[] = [
{ label: '全部智能体', value: '' },
...(bots || []).map((item) => ({
label: item.title?.trim() || '未命名智能体',
...(agents || []).map((item) => ({
label: item.name?.trim() || '未命名智能体',
value: item.id === undefined || item.id === null ? '' : String(item.id),
})),
];

View File

@@ -21,6 +21,7 @@ import { hasPermission } from '#/api/common/hasPermission';
import { api } from '#/api/request';
import { $t } from '#/locales';
import { router } from '#/router';
import AgentApprovalSnapshotPreview from '#/views/system/approval/components/AgentApprovalSnapshotPreview.vue';
import BotApprovalSnapshotPreview from '#/views/system/approval/components/BotApprovalSnapshotPreview.vue';
import KnowledgeApprovalSnapshotPreview from '#/views/system/approval/components/KnowledgeApprovalSnapshotPreview.vue';
import WorkflowApprovalSnapshotPreview from '#/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue';
@@ -33,6 +34,7 @@ const detail = ref<any>(null);
const approvalActionLoading = ref<'approve' | 'reject' | 'revoke' | null>(null);
const resourceLabelMap: Record<string, string> = {
AGENT: $t('approval.resource.agent'),
BOT: $t('approval.resource.bot'),
KNOWLEDGE: $t('approval.resource.knowledge'),
WORKFLOW: $t('approval.resource.workflow'),
@@ -91,6 +93,13 @@ const botSnapshot = computed(() => {
return detail.value?.snapshotJson?.resourceSnapshot || null;
});
const agentSnapshot = computed(() => {
if (detail.value?.resourceType !== 'AGENT') {
return null;
}
return detail.value?.snapshotJson?.resourceSnapshot || null;
});
onMounted(() => {
void loadDetail();
});
@@ -498,8 +507,12 @@ function formatEventInfo(row: Record<string, any>) {
<div class="approval-detail__panel-header">
<h3>{{ $t('approval.section.snapshot') }}</h3>
</div>
<AgentApprovalSnapshotPreview
v-if="agentSnapshot"
:snapshot="agentSnapshot"
/>
<WorkflowApprovalSnapshotPreview
v-if="workflowSnapshot"
v-else-if="workflowSnapshot"
:title="workflowSnapshot.title"
:description="workflowSnapshot.description"
:content="workflowSnapshot.content"

View File

@@ -41,7 +41,7 @@ defineExpose({
openDialog,
});
type ResourceType = '' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
type ResourceType = '' | 'AGENT' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
type ActionType = '' | 'DELETE' | 'OFFLINE' | 'PUBLISH';
type ScopeType = 'CATEGORY' | 'DEPT';
type FlowStatus = 'DISABLED' | 'ENABLED';
@@ -88,7 +88,7 @@ interface FlowFormModel {
}
const RESOURCE_OPTIONS = [
{ label: $t('approval.resource.bot'), value: 'BOT' },
{ label: $t('approval.resource.agent'), value: 'AGENT' },
{ label: $t('approval.resource.workflow'), value: 'WORKFLOW' },
{ label: $t('approval.resource.knowledge'), value: 'KNOWLEDGE' },
];
@@ -128,6 +128,7 @@ const deptTreeOptions = ref<any[]>([]);
const roleOptions = ref<SelectOption[]>([]);
const accountOptions = ref<SelectOption[]>([]);
const categoryOptions = ref<Record<Exclude<ResourceType, ''>, SelectOption[]>>({
AGENT: [],
BOT: [],
KNOWLEDGE: [],
WORKFLOW: [],
@@ -188,7 +189,7 @@ function buildDefaultForm(): FlowFormModel {
name: '',
priority: 100,
remark: '',
resourceType: 'BOT',
resourceType: 'AGENT',
scopes: [],
status: 'ENABLED',
steps: [buildDefaultStep()],
@@ -236,7 +237,7 @@ async function openDialog(row: any = {}) {
name: res.data?.name || '',
priority: Number(res.data?.priority || 100),
remark: res.data?.remark || '',
resourceType: res.data?.resourceType || 'BOT',
resourceType: res.data?.resourceType || 'AGENT',
scopes: (res.data?.scopes || []).map((item: any) => ({
id: item.id,
includeChildren: item.includeChildren === 1,
@@ -279,8 +280,8 @@ async function ensureCategoryOptions() {
if (categoryLoaded.value) {
return;
}
const [botRes, workflowRes, knowledgeRes] = await Promise.all([
api.get('/api/v1/botCategory/list', {
const [agentRes, workflowRes, knowledgeRes] = await Promise.all([
api.get('/api/v1/agentCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
api.get('/api/v1/workflowCategory/list', {
@@ -291,7 +292,8 @@ async function ensureCategoryOptions() {
}),
]);
categoryOptions.value = {
BOT: normalizeCategoryOptions(botRes.data, 'categoryName'),
AGENT: normalizeCategoryOptions(agentRes.data, 'categoryName'),
BOT: [],
KNOWLEDGE: normalizeCategoryOptions(knowledgeRes.data, 'categoryName'),
WORKFLOW: normalizeCategoryOptions(workflowRes.data, 'categoryName'),
};

View File

@@ -35,6 +35,7 @@ import ApprovalFlowModal from './ApprovalFlowModal.vue';
type ApprovalTabName = 'flow' | 'initiated' | 'pending' | 'processed';
const RESOURCE_OPTIONS = [
{ label: $t('approval.resource.agent'), value: 'AGENT' },
{ label: $t('approval.resource.bot'), value: 'BOT' },
{ label: $t('approval.resource.workflow'), value: 'WORKFLOW' },
{ label: $t('approval.resource.knowledge'), value: 'KNOWLEDGE' },

View File

@@ -0,0 +1,297 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { ElButton, ElTag } from 'element-plus';
import { $t } from '#/locales';
const props = defineProps<{
snapshot?: null | Record<string, any>;
}>();
const agent = computed(() => props.snapshot || {});
const expandedPrompt = ref(false);
const modelItems = computed(() => {
const model = agent.value.modelSummary || {};
return [
{
label: $t('approval.snapshot.modelName'),
value: model.title || model.modelName,
},
];
});
const promptPreview = computed(
() =>
agent.value.promptSummary?.systemPrompt ||
agent.value.promptConfigJson?.systemPrompt ||
'',
);
const bindingGroups = computed(() => [
{
items: Array.isArray(agent.value.knowledgeSummaries)
? agent.value.knowledgeSummaries
: [],
key: 'knowledge',
label: $t('approval.snapshot.knowledgeBindings'),
},
{
items: Array.isArray(agent.value.toolSummaries)
? agent.value.toolSummaries
: [],
key: 'tool',
label: $t('approval.snapshot.agentBindings'),
},
]);
function formatValue(value?: null | number | string) {
if (value === null || value === undefined || value === '') {
return $t('approval.snapshot.notConfigured');
}
return String(value);
}
function getBindingName(item: Record<string, any>) {
return (
item.title ||
item.toolName ||
item.name ||
$t('approval.snapshot.notConfigured')
);
}
</script>
<template>
<div class="agent-snapshot">
<div class="agent-snapshot__hero">
<div class="agent-snapshot__avatar">
<img
v-if="agent.avatar"
:src="agent.avatar"
:alt="agent.name || $t('approval.resource.agent')"
/>
<span v-else>{{ $t('approval.resource.agent').slice(0, 1) }}</span>
</div>
<div class="agent-snapshot__hero-copy">
<div class="agent-snapshot__title">
{{ agent.name || $t('approval.snapshot.untitledAgent') }}
</div>
<div v-if="agent.description" class="agent-snapshot__description">
{{ agent.description }}
</div>
</div>
</div>
<section class="agent-snapshot__section">
<div class="agent-snapshot__section-title">
{{ $t('approval.snapshot.agentModelConfig') }}
</div>
<div class="agent-snapshot__info-grid">
<div
v-for="item in modelItems"
:key="item.label"
class="agent-snapshot__info-item"
>
<span>{{ item.label }}</span>
<strong>{{ formatValue(item.value) }}</strong>
</div>
</div>
</section>
<section class="agent-snapshot__section">
<div class="agent-snapshot__section-head">
<div class="agent-snapshot__section-title">
{{ $t('approval.snapshot.systemPrompt') }}
</div>
<ElButton
v-if="promptPreview"
link
type="primary"
@click="expandedPrompt = !expandedPrompt"
>
{{
expandedPrompt
? $t('approval.snapshot.collapsePrompt')
: $t('approval.snapshot.expandPrompt')
}}
</ElButton>
</div>
<div
v-if="promptPreview"
class="agent-snapshot__prompt"
:class="{ 'is-expanded': expandedPrompt }"
>
{{ promptPreview }}
</div>
<div v-else class="agent-snapshot__empty">
{{ $t('approval.snapshot.notConfigured') }}
</div>
</section>
<section class="agent-snapshot__section">
<div class="agent-snapshot__section-title">
{{ $t('approval.snapshot.agentBindings') }}
</div>
<div class="agent-snapshot__binding-grid">
<div
v-for="group in bindingGroups"
:key="group.key"
class="agent-snapshot__binding-group"
>
<div class="agent-snapshot__binding-head">
<span>{{ group.label }}</span>
<ElTag round effect="plain" type="info">
{{ group.items.length }}
</ElTag>
</div>
<div v-if="group.items.length" class="agent-snapshot__binding-list">
<div
v-for="item in group.items"
:key="`${group.key}-${item.bindingId || item.id || item.targetId || item.knowledgeId}`"
class="agent-snapshot__binding-item"
>
<span>{{ getBindingName(item) }}</span>
<ElTag
v-if="item.toolType || item.retrievalMode"
round
effect="plain"
type="primary"
>
{{ item.toolType || item.retrievalMode }}
</ElTag>
</div>
</div>
<div v-else class="agent-snapshot__empty">
{{ $t('approval.snapshot.noBindings') }}
</div>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.agent-snapshot {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.agent-snapshot__hero,
.agent-snapshot__section {
padding: var(--space-4);
border: 1px solid hsl(var(--divider-faint) / 0.64);
border-radius: var(--radius-panel);
background: hsl(var(--surface-panel) / 0.88);
}
.agent-snapshot__hero,
.agent-snapshot__section-head,
.agent-snapshot__binding-head,
.agent-snapshot__binding-item {
display: flex;
align-items: center;
}
.agent-snapshot__hero {
gap: var(--space-3);
}
.agent-snapshot__avatar {
display: grid;
width: 48px;
height: 48px;
flex: none;
place-items: center;
overflow: hidden;
border-radius: var(--radius-control);
background: hsl(var(--primary) / 0.1);
color: hsl(var(--primary));
font-weight: 600;
}
.agent-snapshot__avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.agent-snapshot__hero-copy,
.agent-snapshot__section,
.agent-snapshot__binding-group,
.agent-snapshot__binding-list {
display: flex;
min-width: 0;
flex-direction: column;
gap: var(--space-2);
}
.agent-snapshot__title,
.agent-snapshot__section-title {
color: hsl(var(--foreground));
font-weight: 600;
}
.agent-snapshot__description,
.agent-snapshot__empty {
color: hsl(var(--text-muted));
font-size: 13px;
}
.agent-snapshot__section-head,
.agent-snapshot__binding-head,
.agent-snapshot__binding-item {
justify-content: space-between;
gap: var(--space-3);
}
.agent-snapshot__info-grid,
.agent-snapshot__binding-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--space-3);
}
.agent-snapshot__info-item,
.agent-snapshot__binding-group {
padding: var(--space-3);
border-radius: var(--radius-control);
background: hsl(var(--surface-contrast-soft));
}
.agent-snapshot__info-item {
display: flex;
flex-direction: column;
gap: var(--space-1);
color: hsl(var(--text-muted));
font-size: 13px;
}
.agent-snapshot__info-item strong {
color: hsl(var(--foreground));
font-weight: 500;
}
.agent-snapshot__prompt {
display: -webkit-box;
overflow: hidden;
color: hsl(var(--foreground));
font-size: 13px;
line-height: 1.6;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
white-space: pre-wrap;
}
.agent-snapshot__prompt.is-expanded {
display: block;
}
.agent-snapshot__binding-item {
min-height: 32px;
color: hsl(var(--foreground));
font-size: 13px;
}
</style>

View File

@@ -17,7 +17,13 @@ defineExpose({
openDialog,
});
type ResourceType = 'BOT' | 'KNOWLEDGE' | 'PLUGIN' | 'RESOURCE' | 'WORKFLOW';
type ResourceType =
| 'AGENT'
| 'BOT'
| 'KNOWLEDGE'
| 'PLUGIN'
| 'RESOURCE'
| 'WORKFLOW';
interface CategoryScopeItem {
categoryIds: Array<number | string>;
@@ -40,6 +46,7 @@ const RESOURCE_SCOPE_GROUPS: Array<{
label: string;
resourceType: ResourceType;
}> = [
{ resourceType: 'AGENT', label: $t('menus.ai.agents') },
{ resourceType: 'BOT', label: $t('bot.chatAssistant') },
{ resourceType: 'PLUGIN', label: $t('menus.ai.plugin') },
{ resourceType: 'WORKFLOW', label: $t('menus.ai.workflow') },
@@ -54,6 +61,7 @@ const entity = ref<any>(buildDefaultEntity());
const categoryScopeLoaded = ref(false);
const categoryScopeEditable = ref(false);
const categoryOptions = ref<Record<ResourceType, CategoryOption[]>>({
AGENT: [],
BOT: [],
KNOWLEDGE: [],
PLUGIN: [],
@@ -186,6 +194,9 @@ async function ensureCategoryOptions() {
return;
}
const requests = [
api.get('/api/v1/agentCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
api.get('/api/v1/botCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
@@ -201,9 +212,10 @@ async function ensureCategoryOptions() {
}),
];
const [botRes, pluginRes, workflowRes, knowledgeRes, resourceRes] =
const [agentRes, botRes, pluginRes, workflowRes, knowledgeRes, resourceRes] =
await Promise.all(requests);
categoryOptions.value = {
AGENT: normalizeCategoryOptions(agentRes.data, 'categoryName'),
BOT: normalizeCategoryOptions(botRes.data, 'categoryName'),
KNOWLEDGE: normalizeCategoryOptions(knowledgeRes.data, 'categoryName'),
PLUGIN: normalizeCategoryOptions(pluginRes.data, 'name'),