1. 文档概述

1.1 文档目的

本文档旨在定义控制台智能体菜单和页面的技术架构,包括系统架构、模块划分、核心功能实现、数据结构与模型、接口设计等,为开发团队提供清晰的技术实现指南。

1.2 适用范围

适用于控制台智能体模块的所有相关页面和功能开发。

1.3 术语定义

  • 智能体:具备特定能力和个性的AI实体,可与用户进行对话并执行任务
  • 智能体广场:展示和分享智能体的公共平台
  • DSL:领域特定语言,用于智能体的配置和导出
  • MCP:模型调用协议,用于智能体与外部服务的交互

2. 技术栈

2.1 前端技术栈

  • 框架:Vue 3 + TypeScript + Nuxt 3
  • UI组件库:@nuxt/ui
  • 状态管理:Pinia
  • 路由:Nuxt Router
  • HTTP客户端:Axios

2.2 后端技术栈

  • 框架:NestJS + TypeScript
  • ORM:TypeORM
  • 数据库:PostgreSQL
  • 权限:基于角色的权限控制(RBAC)

3. 系统架构

3.1 整体架构

┌─────────────────────────┐     ┌─────────────────────────┐     ┌─────────────────────────┐
│      前端页面层         │     │      后端服务层         │     │      数据存储层         │
├─────────────────────────┤     ├─────────────────────────┤     ├─────────────────────────┤
│ 智能体列表页面         │     │ 智能体管理服务         │     │ 智能体实体表          │
│ 智能体创建/编辑页面     │     │ 智能体配置服务         │     │ 智能体配置表          │
│ 智能体详情页面         │     │ 智能体发布服务         │     │ 智能体对话记录表       │
│ 智能体配置页面         │     │ 智能体统计服务         │     │ 智能体发布历史表       │
│ 智能体发布页面         │     │ 智能体标签服务         │     │ 标签表               │
└─────────────────────────┘     └─────────────────────────┘     └─────────────────────────┘

3.2 前后端交互流程

  1. 前端页面通过API调用后端服务
  2. 后端服务处理请求,进行业务逻辑处理
  3. 后端服务与数据库交互,获取或存储数据
  4. 后端服务将处理结果返回给前端页面
  5. 前端页面根据返回结果更新UI

4. 模块划分

4.1 前端模块

4.1.1 页面模块
  • 智能体列表页面:展示所有智能体,支持搜索、过滤和批量操作
  • 智能体创建/编辑页面:用于创建和编辑智能体的基本信息和配置
  • 智能体详情页面:展示智能体的详细信息和配置
  • 智能体配置页面:管理智能体的配置
  • 智能体发布页面:发布智能体和管理发布历史
4.1.2 组件模块
  • 智能体卡片组件:展示智能体的基本信息和操作按钮
  • 智能体模态框组件:用于创建和编辑智能体
  • 智能体DSL导入组件:用于导入智能体的DSL配置
  • 智能体标签组件:用于管理智能体的标签
  • 智能体预览聊天组件:用于预览智能体的聊天功能

4.2 后端模块

4.2.1 控制器模块
  • 控制台控制器:处理控制台端的API请求
  • Web控制器:处理Web端的API请求
4.2.2 服务模块
  • 智能体管理服务:处理智能体的创建、编辑、删除等操作
  • 智能体配置服务:处理智能体的配置管理
  • 智能体发布服务:处理智能体的发布和撤销发布
  • 智能体统计服务:处理智能体的统计信息
  • 智能体标签服务:处理智能体的标签管理
4.2.3 数据访问模块
  • 智能体实体:定义智能体的数据结构
  • 智能体配置实体:定义智能体配置的数据结构
  • 智能体对话记录实体:定义智能体对话记录的数据结构
  • 智能体发布历史实体:定义智能体发布历史的数据结构

5. 核心功能实现

5.1 智能体列表页面

5.1.1 功能说明

展示所有智能体,支持搜索、过滤和批量操作。

5.1.2 技术实现
  • 使用Nuxt 3的页面组件实现
  • 使用@buildingai/service/consoleapi/ai-agent提供的API获取智能体列表
  • 使用BdInfiniteScroll实现无限滚动加载
  • 使用AgentCard组件展示智能体卡片
5.1.3 关键代码
<template>
    <div class="flex w-full flex-col items-center justify-center">
        <div class="bg-background sticky top-0 z-10 flex w-full flex-wrap justify-between gap-4 pb-2">
            <div class="flex items-center gap-4">
                <UInput
                    v-model="searchForm.keyword"
                    :placeholder="$t('ai-agent.backend.search.placeholder')"
                    class="w-80"
                    @change="getLists"
                />

                <TagSelect v-model="searchForm.tagIds" @change="getLists" />

                <USelect
                    v-model="searchForm.isPublic"
                    :items="[
                        { label: $t('ai-agent.backend.search.allStatus'), value: undefined },
                        { label: $t('ai-agent.backend.configuration.public'), value: true },
                        { label: $t('ai-agent.backend.configuration.private'), value: false },
                    ]"
                    :placeholder="$t('ai-agent.backend.search.filterByStatus')"
                    label-key="label"
                    value-key="value"
                    class="w-48"
                    @change="getLists"
                />
            </div>

            <div>
                <UButton
                    color="primary"
                    variant="ghost"
                    icon="i-lucide-package"
                    :label="$t('decorate.openDecorateSettings')"
                    @click.stop="handleAgentDecorate"
                />
            </div>
        </div>

        <BdScrollArea class="h-[calc(100vh-9rem)] min-h-0 w-full">
            <BdInfiniteScroll
                :loading="loading"
                :has-more="hasMore"
                :threshold="200"
                :loading-text="$t('common.loading')"
                :no-more-text="searchForm.page !== 1 ? $t('common.noMoreData') : ' '"
                @load-more="loadMore"
            >
                <div class="grid grid-cols-1 gap-6 pt-2 pb-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
                    <div
                        class="group border-default relative cursor-pointer rounded-lg border border-dashed p-4 transition-all duration-200 hover:shadow-lg"
                        @click="handleCreate"
                    >
                        <div class="group-hover:text-primary text-foreground mb-3 flex items-center gap-3">
                            <div class="border-default flex size-10 flex-none items-center justify-center rounded-lg border border-dashed">
                                <UIcon name="i-lucide-plus" class="h-6 w-6" />
                            </div>

                            <h3 class="truncate text-sm font-medium">
                                {{ $t('ai-agent.backend.create.title') }}
                            </h3>
                        </div>

                        <div class="text-muted-foreground mb-6 pr-8 text-xs">
                            <p class="line-clamp-2 overflow-hidden">
                                {{ $t('ai-agent.backend.create.desc') }}
                            </p>
                        </div>

                        <div class="flex items-center gap-2">
                            <UButton
                                color="primary"
                                variant="ghost"
                                class="w-full"
                                icon="i-lucide-package"
                                size="sm"
                                :label="$t('ai-agent.backend.create.fromTemplate')"
                                @click.stop="handleCreateFromTemplate"
                            />
                            <UButton
                                color="neutral"
                                variant="ghost"
                                class="w-full"
                                icon="i-lucide-file-up"
                                size="sm"
                                :label="$t('ai-agent.backend.dslImport.import')"
                                @click.stop="handleImportAgent"
                            />
                        </div>
                    </div>

                    <!-- 智能体卡片 -->
                    <AgentCard
                        v-for="agent in agents"
                        :key="agent.id"
                        :agent="agent"
                        @delete="handleDelete"
                        @edit="handleEdit"
                        @export-dsl="handleExportDsl"
                        @update-tags="handleUpdateTags"
                    />
                </div>
            </BdInfiniteScroll>
        </BdScrollArea>
    </div>
</template>

5.2 智能体创建/编辑页面

5.2.1 功能说明

用于创建和编辑智能体的基本信息和配置。

5.2.2 技术实现
  • 使用Nuxt 3的页面组件实现
  • 使用表单组件收集智能体的基本信息和配置
  • 使用API调用后端服务创建或编辑智能体
  • 支持从模板创建智能体
  • 支持DSL配置导入
5.2.3 关键代码
<script lang="ts" setup>
import type { Agent, QueryAgentParams } from "@buildingai/service/consoleapi/ai-agent";
import { apiAddAgentTags, apiDeleteAgent, apiExportAgentDsl, apiGetAgentList, apiRemoveAgentTags } from "@buildingai/service/consoleapi/ai-agent";

// ... 省略部分代码

const handleCreateFromTemplate = async () => {
    const drawer = overlay.create(TemplateDrawer);
    const instance = drawer.open();
    const shouldRefresh = await instance.result;
    if (shouldRefresh) {
        searchForm.page = 1;
        searchForm.pageSize = 15;
        await getLists();
    }
};

const handleImportAgent = async () => {
    const modal = overlay.create(AgentDslImport);
    const instance = modal.open();
    const shouldRefresh = await instance.result;
    if (shouldRefresh) {
        searchForm.page = 1;
        searchForm.pageSize = 15;
        await getLists();
    }
};

// ... 省略部分代码
</script>

<template>
    <div class="grid grid-cols-1 gap-6 pt-2 pb-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
        <div
            class="group border-default relative cursor-pointer rounded-lg border border-dashed p-4 transition-all duration-200 hover:shadow-lg"
            @click="handleCreate"
        >
            <div class="group-hover:text-primary text-foreground mb-3 flex items-center gap-3">
                <div class="border-default flex size-10 flex-none items-center justify-center rounded-lg border border-dashed">
                    <UIcon name="i-lucide-plus" class="h-6 w-6" />
                </div>

                <h3 class="truncate text-sm font-medium">
                    {{ $t('ai-agent.backend.create.title') }}
                </h3>
            </div>

            <div class="text-muted-foreground mb-6 pr-8 text-xs">
                <p class="line-clamp-2 overflow-hidden">
                    {{ $t('ai-agent.backend.create.desc') }}
                </p>
            </div>

            <div class="flex items-center gap-2">
                <UButton
                    color="primary"
                    variant="ghost"
                    class="w-full"
                    icon="i-lucide-package"
                    size="sm"
                    :label="$t('ai-agent.backend.create.fromTemplate')"
                    @click.stop="handleCreateFromTemplate"
                />
                <UButton
                    color="neutral"
                    variant="ghost"
                    class="w-full"
                    icon="i-lucide-file-up"
                    size="sm"
                    :label="$t('ai-agent.backend.dslImport.import')"
                    @click.stop="handleImportAgent"
                />
            </div>
        </div>

        <!-- 智能体卡片 -->
        <AgentCard
            v-for="agent in agents"
            :key="agent.id"
            :agent="agent"
            @delete="handleDelete"
            @edit="handleEdit"
            @export-dsl="handleExportDsl"
            @update-tags="handleUpdateTags"
        />
    </div>
</template>

5.3 智能体详情页面

5.3.1 功能说明

展示智能体的详细信息和配置。

5.3.2 技术实现
  • 使用Nuxt 3的页面组件实现
  • 使用嵌套路由展示智能体的不同配置页面
  • 使用导航菜单切换不同的配置页面
5.3.3 关键代码
<script setup lang="ts">
import { apiGetAgentDetail } from "@buildingai/service/consoleapi/ai-agent";
import type { NavigationMenuItem } from "@nuxt/ui";

const AgentModal = defineAsyncComponent(() => import("./components/agent-modal.vue"));

const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const overlay = useOverlay();
const { hasAccessByCodes } = useAccessControl();
const isMobile = useMediaQuery("(max-width: 1380px)");
const collapsed = shallowRef<boolean>(false);
const agentId = computed(() => (route.params as Record<string, string>).id);

const { data: agent, refresh: refreshAgent } = await useAsyncData(
    `agent-detail-${agentId.value}`,
    () => apiGetAgentDetail(agentId.value as string),
);

provide("agents", agent);

const mountAgentModal = async (id: string) => {
    const modal = overlay.create(AgentModal);

    const instance = modal.open({ id: id });
    const shouldRefresh = await instance.result;
    if (shouldRefresh) refreshAgent();
};

const handleEdit = () => {
    mountAgentModal(agentId.value as string);
};

const navigationItems = computed(() => {
    return [
        hasAccessByCodes(["ai-agent:detail"]) ? {
            label: t("ai-agent.backend.menu.arrange"),
            icon: "i-lucide-radar",
            to: useRoutePath("ai-agent:detail", {
                id: agentId.value as string,
            }),
        } : null,
        hasAccessByCodes(["ai-agent:publish"]) ? {
            label: t("ai-agent.backend.menu.publish"),
            icon: "i-lucide-radio-tower",
            to: useRoutePath("ai-agent:publish", {
                id: agentId.value as string,
            }),
        } : null,
        hasAccessByCodes(["ai-agent-chat-record:list"]) ? {
            label: t("ai-agent.backend.menu.chatRecord"),
            icon: "i-lucide-file-text",
            to: useRoutePath("ai-agent-chat-record:list", {
                id: agentId.value as string,
            }),
        } : null,
        hasAccessByCodes(["ai-agent:statistics"]) ? {
            label: t("ai-agent.backend.menu.statistics"),
            icon: "i-lucide-pie-chart",
            to: useRoutePath("ai-agent:statistics", {
                id: agentId.value as string,
            }),
        } : null,
    ].filter(Boolean) as NavigationMenuItem[];
});
</script>

<template>
    <div class="flex h-full min-h-0 w-full">
        <div
            class="bg-muted flex h-full w-50 flex-none flex-col rounded-tr-xl rounded-br-xl p-2"
            :class="{ 'w-18!': collapsed }"
        >
            <!-- 智能体信息和导航菜单 -->
            <!-- ... 省略部分代码 ... -->
        </div>
        <!-- 内容区域 -->
        <NuxtPage />
    </div>
</template>

5.4 智能体发布页面

5.4.1 功能说明

发布智能体和管理发布历史。

5.4.2 技术实现
  • 使用Nuxt 3的页面组件实现
  • 使用表单组件收集发布信息
  • 使用API调用后端服务发布智能体
  • 支持版本控制
  • 支持发布范围设置

5.5 后端服务实现

5.5.1 智能体服务

import { BaseService } from "@buildingai/base";
import { InjectRepository } from "@buildingai/db/@nestjs/typeorm";
import { Agent } from "@buildingai/db/entities/ai-agent.entity";
import { AgentChatRecord } from "@buildingai/db/entities/ai-agent-chat-record.entity";
import { AgentChatMessage } from "@buildingai/db/entities/ai-agent-chat-message.entity";
import { AgentAnnotation } from "@buildingai/db/entities/ai-agent-annotation.entity";
import { Tag } from "@buildingai/db/entities/tag.entity";
import { AiModel } from "@buildingai/db/entities/ai-model.entity";
import { AiProvider } from "@buildingai/db/entities/ai-provider.entity";
import { Repository } from "@buildingai/db/typeorm";
import { HttpErrorFactory } from "@buildingai/errors";
import { Injectable } from "@nestjs/common";
import { randomBytes } from "crypto";

import { CreateAgentDto, PublishAgentDto, QueryAgentDto, QueryAgentStatisticsDto, UpdateAgentConfigDto } from "../dto/agent";

@Injectable()
export class AiAgentService extends BaseService<Agent> {
    private readonly defaultAvatar = "/static/images/agent.png";

    constructor(
        @InjectRepository(Agent) private readonly agentRepository: Repository<Agent>,
        @InjectRepository(AgentChatRecord) private readonly chatRecordRepository: Repository<AgentChatRecord>,
        @InjectRepository(AgentChatMessage) private readonly chatMessageRepository: Repository<AgentChatMessage>,
        @InjectRepository(AgentAnnotation) private readonly annotationRepository: Repository<AgentAnnotation>,
        @InjectRepository(Tag) private readonly tagRepository: Repository<Tag>,
        @InjectRepository(AiModel) private readonly aiModelRepository: Repository<AiModel>,
        @InjectRepository(AiProvider) private readonly aiProviderRepository: Repository<AiProvider>,
    ) {
        super(agentRepository);
    }

    // 创建新智能体
    async createAgent(dto: CreateAgentDto, user: UserPlayground): Promise<Agent> {
        const { name, description, avatar, createMode = "direct", thirdPartyIntegration = {}, tagIds } = dto;

        await this.checkNameUniqueness(name);

        return this.withErrorHandling(async () => {
            const agent = await this.create({
                name,
                description,
                avatar: avatar || this.defaultAvatar,
                showContext: true,
                createMode,
                thirdPartyIntegration: thirdPartyIntegration || {},
                showReference: true,
                enableFeedback: false,
                enableWebSearch: false,
                userCount: 0,
                isPublic: false,
                createBy: user.id,
            });

            if (tagIds && tagIds.length > 0) {
                await this.syncAgentTags(agent.id, tagIds);
            }

            return agent;
        });
    }

    // 发布智能体
    async publishAgent(agentId: string, dto: PublishAgentDto): Promise<Agent> {
        const { version, releaseNotes, publishScope } = dto;

        return this.withErrorHandling(async () => {
            const agent = await this.getById(agentId);

            if (!agent) {
                throw HttpErrorFactory.createNotFoundError("Agent not found");
            }

            const updatedAgent = await this.updateById(agentId, {
                isPublished: true,
                publishConfig: { ...agent.publishConfig, version, releaseNotes, publishScope },
            });

            // 记录发布历史
            await this.recordPublishHistory(agentId, version, releaseNotes, publishScope);

            return updatedAgent;
        });
    }

    // ... 省略其他方法 ...
}

5.6 智能体配置页面

5.6.1 功能说明

配置智能体的模型、数据集、表单字段等参数。

5.6.2 技术实现
  • 使用Nuxt 3的页面组件实现
  • 支持多标签页切换配置不同参数
  • 使用表单组件收集配置信息
  • 使用API调用后端服务保存配置
  • 支持实时预览配置效果
5.6.3 关键代码
<script setup lang="ts">
import { apiGetAgentDetail, apiUpdateAgentConfig } from "@buildingai/service/consoleapi/ai-agent";
import type { Agent, ModelConfig, DatasetConfig } from "@buildingai/service/consoleapi/ai-agent";

const route = useRoute();
const { t } = useI18n();
const toast = useToast();

const agentId = computed(() => (route.params as Record<string, string>).id);
const { data: agent, refresh: refreshAgent } = await useAsyncData(
    `agent-detail-${agentId.value}`,
    () => apiGetAgentDetail(agentId.value as string),
);

const modelConfig = ref<ModelConfig | undefined>(agent.value?.modelConfig);
const datasetConfig = ref<DatasetConfig | undefined>(agent.value?.datasetConfig);

const updateConfig = async () => {
    try {
        await apiUpdateAgentConfig(agentId.value as string, {
            modelConfig: modelConfig.value,
            datasetConfig: datasetConfig.value,
        });
        toast.add({ title: t("common.saveSuccess"), color: "primary" });
        await refreshAgent();
    } catch (error) {
        toast.add({ title: t("common.saveFailed"), color: "negative" });
    }
};
</script>

<template>
    <div class="space-y-6">
        <div class="flex items-center justify-between">
            <h3 class="text-lg font-medium">{{ t("ai-agent.backend.menu.configuration") }}</h3>
            <UButton color="primary" size="sm" :label="t('common.save')" @click="updateConfig" />
        </div>

        <UTabs v-model="activeTab">
            <UTab title="{{ t('ai-agent.backend.configuration.model') }}">
                <!-- 模型配置 -->
                <ModelConfigPanel v-model="modelConfig" />
            </UTab>
            <UTab title="{{ t('ai-agent.backend.configuration.dataset') }}">
                <!-- 数据集配置 -->
                <DatasetConfigPanel v-model="datasetConfig" />
            </UTab>
            <UTab title="{{ t('ai-agent.backend.configuration.form') }}">
                <!-- 表单字段配置 -->
                <FormConfigPanel v-model="formConfig" />
            </UTab>
            <UTab title="{{ t('ai-agent.backend.configuration.thirdParty') }}">
                <!-- 第三方集成配置 -->
                <ThirdPartyConfigPanel v-model="thirdPartyConfig" />
            </UTab>
        </UTabs>
    </div>
</template>

5.7 智能体日志页面

5.7.1 功能说明

查看智能体的运行日志和调试信息。

5.7.2 技术实现
  • 使用Nuxt 3的页面组件实现
  • 支持日志过滤和搜索
  • 支持分页加载日志
  • 支持日志级别筛选
  • 使用WebSocket实现实时日志推送
5.7.3 关键代码
<script setup lang="ts">
import { apiGetAgentLogs, apiClearAgentLogs } from "@buildingai/service/consoleapi/ai-agent";
import type { AgentLog, QueryAgentLogsDto } from "@buildingai/service/consoleapi/ai-agent";

const route = useRoute();
const { t } = useI18n();
const toast = useToast();

const agentId = computed(() => (route.params as Record<string, string>).id);
const logs = ref<AgentLog[]>([]);
const total = ref(0);
const loading = ref(false);

const queryParams = reactive<QueryAgentLogsDto>({
    page: 1,
    pageSize: 50,
    level: "all",
    keyword: "",
});

const getLogs = async () => {
    loading.value = true;
    try {
        const result = await apiGetAgentLogs(agentId.value as string, queryParams);
        logs.value = result.data;
        total.value = result.total;
    } catch (error) {
        toast.add({ title: t("ai-agent.backend.logs.loadFailed"), color: "negative" });
    } finally {
        loading.value = false;
    }
};

const clearLogs = async () => {
    try {
        await apiClearAgentLogs(agentId.value as string);
        toast.add({ title: t("ai-agent.backend.logs.clearSuccess"), color: "primary" });
        await getLogs();
    } catch (error) {
        toast.add({ title: t("ai-agent.backend.logs.clearFailed"), color: "negative" });
    }
};

// WebSocket 实时日志推送
const socket = ref<WebSocket | null>(null);

onMounted(() => {
    socket.value = new WebSocket(`ws://localhost:3000/agent/${agentId.value}/logs`);
    socket.value.onmessage = (event) => {
        const log: AgentLog = JSON.parse(event.data);
        logs.value.unshift(log);
        if (logs.value.length > 100) logs.value.pop();
    };
    await getLogs();
});

onUnmounted(() => {
    if (socket.value) socket.value.close();
});
</script>

<template>
    <div class="space-y-6">
        <div class="flex items-center justify-between">
            <h3 class="text-lg font-medium">{{ t("ai-agent.backend.menu.logs") }}</h3>
            <UButton color="negative" size="sm" :label="t('ai-agent.backend.logs.clear')" @click="clearLogs" />
        </div>

        <!-- 日志筛选 -->
        <div class="flex flex-wrap gap-4">
            <USelect v-model="queryParams.level" :options="logLevels" placeholder="{{ t('ai-agent.backend.logs.level') }}" size="sm" />
            <UInput v-model="queryParams.keyword" placeholder="{{ t('ai-agent.backend.logs.search') }}" size="sm" />
            <UButton color="primary" size="sm" :label="t('common.search')" @click="getLogs" />
        </div>

        <!-- 日志列表 -->
        <div class="bg-card rounded-lg border p-4 max-h-[600px] overflow-y-auto">
            <div v-if="loading" class="flex justify-center py-4">{{ t('common.loading') }}</div>
            <div v-else-if="logs.length === 0" class="text-center py-4 text-muted-foreground">
                {{ t('ai-agent.backend.logs.empty') }}
            </div>
            <div v-else>
                <div
                    v-for="log in logs"
                    :key="log.id"
                    class="border-l-4 pl-3 py-2 mb-2"
                    :class="{
                        'border-red-500': log.level === 'error',
                        'border-yellow-500': log.level === 'warn',
                        'border-blue-500': log.level === 'info',
                        'border-gray-500': log.level === 'debug',
                    }"
                >
                    <div class="text-xs text-muted-foreground flex items-center gap-2">
                        <span>{{ log.timestamp }}</span>
                        <span class="font-medium" :class="{
                            'text-red-500': log.level === 'error',
                            'text-yellow-500': log.level === 'warn',
                            'text-blue-500': log.level === 'info',
                            'text-gray-500': log.level === 'debug',
                        }">
                            {{ log.level.toUpperCase() }}
                        </span>
                    </div>
                    <div class="text-sm whitespace-pre-wrap mt-1">{{ log.message }}</div>
                    <div v-if="log.stacktrace" class="text-xs text-red-500 mt-1 whitespace-pre-wrap">
                        {{ log.stacktrace }}
                    </div>
                </div>
            </div>
        </div>

        <!-- 分页 -->
        <div v-if="total > 0" class="flex items-center justify-end">
            <UPagination v-model="queryParams.page" :total="total" :page-size="queryParams.pageSize" @change="getLogs" />
        </div>
    </div>
</template>

5.8 智能体统计页面

5.8.1 功能说明

查看智能体的使用统计和性能指标。

5.8.2 技术实现
  • 使用Nuxt 3的页面组件实现
  • 使用ECharts或其他图表库展示统计数据
  • 支持时间范围筛选
  • 支持数据导出
  • 实时更新统计数据
5.8.3 关键代码
<script setup lang="ts">
import { apiGetAgentStatistics, apiExportAgentStatistics } from "@buildingai/service/consoleapi/ai-agent";
import type { AgentStatistics, QueryAgentStatisticsDto } from "@buildingai/service/consoleapi/ai-agent";
import { createChart } from "@buildingai/echarts";

const route = useRoute();
const { t } = useI18n();
const toast = useToast();

const agentId = computed(() => (route.params as Record<string, string>).id);
const statistics = ref<AgentStatistics | null>(null);
const loading = ref(false);

const queryParams = reactive<QueryAgentStatisticsDto>({
    timeRange: "7d",
    metrics: ["usage", "responseTime", "successRate"],
});

const chartRef = ref<HTMLElement | null>(null);
let chartInstance: any = null;

const getStatistics = async () => {
    loading.value = true;
    try {
        const result = await apiGetAgentStatistics(agentId.value as string, queryParams);
        statistics.value = result;
        updateChart();
    } catch (error) {
        toast.add({ title: t("ai-agent.backend.statistics.loadFailed"), color: "negative" });
    } finally {
        loading.value = false;
    }
};

const updateChart = () => {
    if (!chartRef.value || !statistics.value) return;

    if (!chartInstance) {
        chartInstance = createChart(chartRef.value, {
            xAxis: {
                type: "category",
                data: statistics.value.timeSeries,
            },
            yAxis: {
                type: "value",
            },
            series: [
                {
                    name: t("ai-agent.backend.statistics.usage"),
                    type: "line",
                    data: statistics.value.usage,
                    smooth: true,
                },
                {
                    name: t("ai-agent.backend.statistics.responseTime"),
                    type: "line",
                    data: statistics.value.responseTime,
                    smooth: true,
                },
                {
                    name: t("ai-agent.backend.statistics.successRate"),
                    type: "line",
                    data: statistics.value.successRate,
                    smooth: true,
                },
            ],
        });
    } else {
        chartInstance.setOption({
            xAxis: {
                data: statistics.value.timeSeries,
            },
            series: [
                {
                    data: statistics.value.usage,
                },
                {
                    data: statistics.value.responseTime,
                },
                {
                    data: statistics.value.successRate,
                },
            ],
        });
    }
};

onMounted(() => {
    getStatistics();
});

onUnmounted(() => {
    if (chartInstance) chartInstance.dispose();
});
</script>

<template>
    <div class="space-y-6">
        <div class="flex items-center justify-between">
            <h3 class="text-lg font-medium">{{ t("ai-agent.backend.menu.statistics") }}</h3>
            <UButton color="primary" size="sm" :label="t('ai-agent.backend.statistics.export')" @click="exportStatistics" />
        </div>

        <!-- 统计筛选 -->
        <div class="flex flex-wrap gap-4">
            <USelect v-model="queryParams.timeRange" :options="timeRanges" placeholder="{{ t('ai-agent.backend.statistics.timeRange') }}" size="sm" />
            <UTagInput
                v-model="queryParams.metrics"
                :tags="metricsOptions"
                placeholder="{{ t('ai-agent.backend.statistics.metrics') }}"
                size="sm"
            />
            <UButton color="primary" size="sm" :label="t('common.search')" @click="getStatistics" />
        </div>

        <!-- 统计图表 -->
        <div v-if="loading" class="flex justify-center py-8">{{ t('common.loading') }}</div>
        <div v-else-if="!statistics" class="text-center py-8 text-muted-foreground">
            {{ t('ai-agent.backend.statistics.empty') }}
        </div>
        <div v-else>
            <div ref="chartRef" class="w-full h-[400px] mb-4"></div>

            <!-- 统计卡片 -->
            <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
                <UCard variant="outlined">
                    <div class="text-sm text-muted-foreground">{{ t('ai-agent.backend.statistics.totalUsage') }}</div>
                    <div class="text-2xl font-bold mt-1">{{ statistics.totalUsage }}</div>
                </UCard>
                <UCard variant="outlined">
                    <div class="text-sm text-muted-foreground">{{ t('ai-agent.backend.statistics.averageResponseTime') }}</div>
                    <div class="text-2xl font-bold mt-1">{{ statistics.averageResponseTime }}ms</div>
                </UCard>
                <UCard variant="outlined">
                    <div class="text-sm text-muted-foreground">{{ t('ai-agent.backend.statistics.successRate') }}</div>
                    <div class="text-2xl font-bold mt-1">{{ (statistics.successRate * 100).toFixed(2) }}%</div>
                </UCard>
            </div>
        </div>
    </div>
</template>

6. 数据结构与模型

6.1 智能体实体

interface Agent {
    id: string;
    name: string;
    description?: string;
    createMode: string;
    avatar?: string;
    chatAvatar?: string;
    rolePrompt?: string;
    showContext: boolean;
    showReference: boolean;
    enableFeedback: boolean;
    enableWebSearch: boolean;
    userCount: number;
    modelConfig?: ModelConfig;
    billingConfig?: ModelBillingConfig;
    datasetIds?: string[];
    openingStatement?: string;
    openingQuestions?: string[];
    quickCommands?: QuickCommandConfig[];
    autoQuestions?: AutoQuestionsConfig;
    formFields?: FormFieldConfig[];
    formFieldsInputs?: Record<string, any>;
    mcpServerIds?: string[];
    isPublished: boolean;
    isPublic: boolean;
    publishToken?: string;
    apiKey?: string;
    createBy: string;
    publishConfig?: {
        allowOrigins?: string[];
        rateLimitPerMinute?: number;
        showBranding?: boolean;
        allowDownloadHistory?: boolean;
    };
    thirdPartyIntegration?: ThirdPartyIntegrationConfig;
    tags?: Tag[];
}
### 6.2 智能体日志实体
```typescript
interface AgentLog {
    id: string;
    agentId: string;
    level: "debug" | "info" | "warn" | "error";
    message: string;
    stacktrace?: string;
    context?: Record<string, any>;
    timestamp: string;
    createBy: string;
}

6.3 智能体统计实体

interface AgentStatistics {
    agentId: string;
    timeSeries: string[];
    usage: number[];
    responseTime: number[];
    successRate: number[];
    totalUsage: number;
    averageResponseTime: number;
    totalRequests: number;
    successRequests: number;
    failureRequests: number;
}

7. 性能与安全

7.1 权限体系

  • 基于角色的访问控制(RBAC)
  • 支持资源级别的权限控制
  • 支持数据级别的权限控制
  • 支持动态权限分配

7.2 权限实现

// 权限控制工具函数
export function useAccessControl() {
    const user = useUser();

    // 检查用户是否有指定权限码
    function hasAccessByCodes(codes: string[]): boolean {
        if (!user.value || !user.value.roles || user.value.roles.length === 0) return false;
        if (user.value.isSuperAdmin) return true;

        const userPermissions = new Set<string>();
        user.value.roles.forEach((role) => {
            role.permissions.forEach((permission) => {
                userPermissions.add(permission.code);
            });
        });

        return codes.some((code) => userPermissions.has(code));
    }

    // 检查用户是否有指定角色
    function hasRole(roleCodes: string[]): boolean {
        if (!user.value || !user.value.roles || user.value.roles.length === 0) return false;
        if (user.value.isSuperAdmin) return true;

        const userRoleCodes = user.value.roles.map((role) => role.code);
        return roleCodes.some((code) => userRoleCodes.includes(code));
    }

    return {
        hasAccessByCodes,
        hasRole,
    };
}

// 在页面中使用权限控制
<script setup lang="ts">
const { hasAccessByCodes } = useAccessControl();

const navigationItems = computed(() => {
    return [
        hasAccessByCodes(["ai-agent:detail"]) ? {
            label: t("ai-agent.backend.menu.arrange"),
            icon: "i-lucide-radar",
            to: useRoutePath("ai-agent:detail", {
                id: agentId.value as string,
            }),
        } : null,
        hasAccessByCodes(["ai-agent:publish"]) ? {
            label: t("ai-agent.backend.menu.publish"),
            icon: "i-lucide-radio-tower",
            to: useRoutePath("ai-agent:publish", {
                id: agentId.value as string,
            }),
        } : null,
        // ... 其他菜单项
    ].filter(Boolean);
});
</script>

8. RBAC权限控制

8.1 智能体管理接口

8.1.1 获取智能体列表
GET /api/console/ai/agent/list

请求参数:

interface QueryAgentDto {
    page: number;
    pageSize: number;
    keyword?: string;
    tags?: string[];
    isPublished?: boolean;
    isPublic?: boolean;
}

响应参数:

interface AgentListResponse {
    list: Agent[];
    total: number;
    page: number;
    pageSize: number;
}
8.1.2 创建智能体
POST /api/console/ai/agent

请求参数:

interface CreateAgentDto {
    name: string;
    description?: string;
    createMode: string;
    avatar?: string;
    chatAvatar?: string;
    rolePrompt?: string;
    showContext: boolean;
    showReference: boolean;
    enableFeedback: boolean;
    enableWebSearch: boolean;
    datasetIds?: string[];
    openingStatement?: string;
    openingQuestions?: string[];
    formFields?: FormFieldConfig[];
    tagIds?: string[];
}

响应参数:

interface AgentResponse {
    agent: Agent;
}
8.1.3 更新智能体配置
PUT /api/console/ai/agent/{id}/config

请求参数:

interface UpdateAgentConfigDto {
    modelConfig?: ModelConfig;
    datasetConfig?: DatasetConfig;
    formConfig?: FormConfig;
    thirdPartyConfig?: ThirdPartyIntegrationConfig;
}

响应参数:

interface AgentResponse {
    agent: Agent;
}
8.1.4 发布智能体
POST /api/console/ai/agent/{id}/publish

请求参数:

interface PublishAgentDto {
    version: string;
    releaseNotes?: string;
    publishScope?: "private" | "public";
    allowOrigins?: string[];
    rateLimitPerMinute?: number;
    showBranding?: boolean;
}

响应参数:

interface AgentResponse {
    agent: Agent;
}

8.2 智能体日志接口

8.2.1 获取智能体日志
GET /api/console/ai/agent/{id}/logs

请求参数:

interface QueryAgentLogsDto {
    page: number;
    pageSize: number;
    level?: "debug" | "info" | "warn" | "error" | "all";
    keyword?: string;
    startTime?: string;
    endTime?: string;
}

响应参数:

interface AgentLogResponse {
    list: AgentLog[];
    total: number;
    page: number;
    pageSize: number;
}

8.3 智能体统计接口

8.3.1 获取智能体统计
GET /api/console/ai/agent/{id}/statistics

请求参数:

interface QueryAgentStatisticsDto {
    timeRange: "24h" | "7d" | "30d" | "90d" | "custom";
    metrics?: ("usage" | "responseTime" | "successRate" | "requestCount")[];
    startTime?: string;
    endTime?: string;
}

响应参数:

interface AgentStatisticsResponse {
    statistics: AgentStatistics;
}

6.2 智能体配置实体

interface AgentConfig {
    id: string;
    name: string;
    type: string;
    content: any;
    createdAt: Date;
    updatedAt: Date;
}

6.3 智能体对话记录实体

interface AgentChatRecord {
    id: string;
    agentId: string;
    userId: string;
    sessionId: string;
    messages: AgentChatMessage[];
    createdAt: Date;
    updatedAt: Date;
}

10. 部署与维护

7.1 智能体管理接口

接口名称 方法 路径 功能描述
智能体列表 GET /api/v1/ai/agent 获取智能体列表
创建智能体 POST /api/v1/ai/agent 创建新智能体
智能体详情 GET /api/v1/ai/agent/{id} 获取智能体详情
更新智能体 PUT /api/v1/ai/agent/{id} 更新智能体信息
删除智能体 DELETE /api/v1/ai/agent/{id} 删除智能体
批量删除智能体 POST /api/v1/ai/agent/batch-delete 批量删除智能体

7.2 智能体配置接口

接口名称 方法 路径 功能描述
配置列表 GET /api/v1/ai/agent/config 获取配置列表
创建配置 POST /api/v1/ai/agent/config 创建新配置
配置详情 GET /api/v1/ai/agent/config/{id} 获取配置详情
更新配置 PUT /api/v1/ai/agent/config/{id} 更新配置信息
删除配置 DELETE /api/v1/ai/agent/config/{id} 删除配置

7.3 智能体发布接口

接口名称 方法 路径 功能描述
发布智能体 POST /api/v1/ai/agent/{id}/publish 发布智能体
撤销发布 POST /api/v1/ai/agent/{id}/unpublish 撤销智能体发布
发布历史 GET /api/v1/ai/agent/{id}/publish-history 获取发布历史

8. 性能与安全

8.1 性能优化

  • 使用无限滚动加载智能体列表,减少初始加载时间
  • 使用缓存机制缓存智能体的配置信息
  • 使用异步加载组件,提高页面加载速度
  • 使用CDN加速静态资源的加载

8.2 安全措施

  • 使用JWT进行身份认证
  • 使用RBAC进行权限控制
  • 使用HTTPS加密数据传输
  • 对敏感数据进行加密存储
  • 对API请求进行限流和防刷

9. API接口文档

9.1 部署方式

  • 使用Docker容器化部署
  • 使用Kubernetes进行容器编排
  • 使用CI/CD工具自动化部署

9.2 维护措施

  • 定期备份数据库
  • 定期更新系统和依赖包
  • 监控系统的性能和可用性
  • 日志记录和分析
  • 故障排查和恢复

10. 总结

本文档详细介绍了控制台智能体菜单和页面的技术架构,包括系统架构、模块划分、核心功能实现、数据结构与模型、接口设计、性能与安全、部署与维护等方面。通过本文档,开发团队可以清晰地了解控制台智能体菜单和页面的技术实现,为后续的开发和维护提供指导。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐