Initial lightweight AI agent admin

This commit is contained in:
Hermes Agent
2026-06-08 15:05:57 +08:00
commit 0e485492cf
49 changed files with 4520 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
+2104
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "ai-agent-admin-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 5178",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview --host 0.0.0.0 --port 4178"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.9",
"element-plus": "^2.9.3",
"pinia": "^2.3.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "^5.7.2",
"vite": "^6.0.5",
"vue-tsc": "^2.2.0"
}
}
+80
View File
@@ -0,0 +1,80 @@
<template>
<router-view v-if="isLoginPage" />
<el-container v-else class="app-shell">
<el-aside width="248px" class="sidebar">
<div class="brand">
<span class="brand-mark">AI</span>
<div>
<strong>Agent Admin</strong>
<small>轻量智能体后台</small>
</div>
</div>
<el-menu router :default-active="$route.path" class="menu">
<el-menu-item index="/">
<el-icon><Monitor /></el-icon>
<span>工作台</span>
</el-menu-item>
<el-sub-menu index="system">
<template #title>
<el-icon><Setting /></el-icon>
<span>系统管理</span>
</template>
<el-menu-item index="/system/users">用户管理</el-menu-item>
<el-menu-item index="/system/roles">角色管理</el-menu-item>
<el-menu-item index="/system/menus">菜单管理</el-menu-item>
<el-menu-item index="/system/permissions">权限管理</el-menu-item>
<el-menu-item index="/system/announcements">公告管理</el-menu-item>
</el-sub-menu>
<el-sub-menu index="ai">
<template #title>
<el-icon><Connection /></el-icon>
<span>AI 管理</span>
</template>
<el-menu-item index="/ai/providers">Provider</el-menu-item>
<el-menu-item index="/ai/models">Model</el-menu-item>
<el-menu-item index="/ai/agents">Agent</el-menu-item>
<el-menu-item index="/ai/chat">Agent Chat</el-menu-item>
<el-menu-item index="/ai/workflows">Workflow</el-menu-item>
<el-menu-item index="/ai/workflow-runs">Workflow Runs</el-menu-item>
<el-menu-item index="/ai/knowledge">Knowledge Base</el-menu-item>
<el-menu-item index="/ai/teams">Agent Team</el-menu-item>
<el-menu-item index="/ai/collaboration-runs">Collaboration Runs</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<el-container>
<el-header class="topbar">
<div>
<strong>{{ pageTitle }}</strong>
</div>
<div class="user">
<span>{{ auth.user?.nickname || auth.user?.username }}</span>
<el-button text @click="logout">退出</el-button>
</div>
</el-header>
<el-main class="main">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { Connection, Monitor, Setting } from '@element-plus/icons-vue';
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAuthStore } from './stores/auth';
const auth = useAuthStore();
const route = useRoute();
const router = useRouter();
const isLoginPage = computed(() => route.path === '/login');
const pageTitle = computed(() => route.meta.title || 'AI Agent Admin');
function logout() {
auth.logout();
router.push('/login');
}
</script>
+25
View File
@@ -0,0 +1,25 @@
import axios from 'axios';
const api = axios.create({
baseURL: '/ai-agent-admin/basic-api/api',
timeout: 60000,
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem('ai-agent-admin-token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('ai-agent-admin-token');
location.href = '/ai-agent-admin/login';
}
return Promise.reject(error);
},
);
export { api };
+163
View File
@@ -0,0 +1,163 @@
import { api } from './http';
export interface ResourceConfig {
title: string;
path: string;
fields: FieldConfig[];
}
export interface FieldConfig {
key: string;
label: string;
type?: 'text' | 'textarea' | 'number' | 'boolean' | 'json';
required?: boolean;
}
export const resources: Record<string, ResourceConfig> = {
users: {
title: '用户管理',
path: '/core/users',
fields: [
{ key: 'email', label: '邮箱', required: true },
{ key: 'username', label: '用户名', required: true },
{ key: 'nickname', label: '昵称' },
{ key: 'password', label: '密码' },
{ key: 'status', label: '状态' },
{ key: 'is_superuser', label: '超级管理员', type: 'boolean' },
],
},
roles: {
title: '角色管理',
path: '/core/roles',
fields: [
{ key: 'name', label: '名称', required: true },
{ key: 'code', label: '编码', required: true },
{ key: 'description', label: '描述', type: 'textarea' },
{ key: 'status', label: '状态' },
],
},
permissions: {
title: '权限管理',
path: '/core/permissions',
fields: [
{ key: 'name', label: '名称', required: true },
{ key: 'code', label: '编码', required: true },
{ key: 'resource', label: '资源' },
{ key: 'action', label: '动作' },
],
},
menus: {
title: '菜单管理',
path: '/core/menus',
fields: [
{ key: 'parent_id', label: '父级 ID' },
{ key: 'title', label: '标题', required: true },
{ key: 'path', label: '路径' },
{ key: 'icon', label: '图标' },
{ key: 'permission_code', label: '权限编码' },
{ key: 'visible', label: '显示', type: 'boolean' },
],
},
announcements: {
title: '公告管理',
path: '/core/announcements',
fields: [
{ key: 'title', label: '标题', required: true },
{ key: 'content', label: '内容', type: 'textarea' },
{ key: 'status', label: '状态' },
],
},
providers: {
title: 'LLM Provider',
path: '/ai/providers',
fields: [
{ key: 'name', label: '名称', required: true },
{ key: 'code', label: '编码', required: true },
{ key: 'provider_type', label: '类型' },
{ key: 'base_url', label: 'Base URL' },
{ key: 'api_key', label: 'API Key' },
{ key: 'status', label: '状态' },
],
},
models: {
title: 'LLM Model',
path: '/ai/models',
fields: [
{ key: 'provider_id', label: 'Provider ID', required: true },
{ key: 'name', label: '模型名', required: true },
{ key: 'display_name', label: '显示名' },
{ key: 'context_length', label: '上下文长度', type: 'number' },
{ key: 'supports_streaming', label: '流式', type: 'boolean' },
{ key: 'supports_function_call', label: 'Function Call', type: 'boolean' },
{ key: 'default_temperature', label: '默认温度', type: 'number' },
{ key: 'default_max_tokens', label: '最大 Token', type: 'number' },
{ key: 'status', label: '状态' },
],
},
agents: {
title: 'Agent 管理',
path: '/ai/agents',
fields: [
{ key: 'name', label: '名称', required: true },
{ key: 'code', label: '编码', required: true },
{ key: 'description', label: '描述', type: 'textarea' },
{ key: 'avatar', label: '头像' },
{ key: 'status', label: '状态' },
{ key: 'system_prompt', label: '系统提示词', type: 'textarea' },
{ key: 'model_id', label: 'Model ID' },
{ key: 'temperature', label: '温度', type: 'number' },
{ key: 'max_tokens', label: '最大 Token', type: 'number' },
{ key: 'tools', label: '工具 JSON', type: 'json' },
{ key: 'knowledge_base_ids', label: '知识库 ID JSON', type: 'json' },
{ key: 'enable_memory', label: '记忆', type: 'boolean' },
{ key: 'memory_window', label: '记忆窗口', type: 'number' },
],
},
workflows: {
title: 'Workflow',
path: '/ai/workflows',
fields: [
{ key: 'name', label: '名称', required: true },
{ key: 'code', label: '编码', required: true },
{ key: 'description', label: '描述', type: 'textarea' },
{ key: 'status', label: '状态' },
{ key: 'definition', label: '定义 JSON', type: 'json' },
{ key: 'input_variables', label: '输入变量 JSON', type: 'json' },
{ key: 'output_variables', label: '输出变量 JSON', type: 'json' },
],
},
knowledge: {
title: 'Knowledge Base',
path: '/ai/knowledge-bases',
fields: [
{ key: 'name', label: '名称', required: true },
{ key: 'code', label: '编码', required: true },
{ key: 'description', label: '描述', type: 'textarea' },
{ key: 'status', label: '状态' },
],
},
teams: {
title: 'Agent Team',
path: '/ai/teams',
fields: [
{ key: 'name', label: '名称', required: true },
{ key: 'code', label: '编码', required: true },
{ key: 'description', label: '描述', type: 'textarea' },
{ key: 'mode', label: '模式' },
{ key: 'members', label: '成员 JSON', type: 'json' },
{ key: 'status', label: '状态' },
],
},
};
export function listResource(key: string, params?: Record<string, unknown>) {
return api.get(resources[key].path, { params });
}
export function saveResource(key: string, payload: Record<string, unknown>, id?: string) {
return id ? api.put(`${resources[key].path}/${id}`, payload) : api.post(resources[key].path, payload);
}
export function deleteResource(key: string, id: string) {
return api.delete(`${resources[key].path}/${id}`);
}
+5
View File
@@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
+11
View File
@@ -0,0 +1,11 @@
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import { createPinia } from 'pinia';
import { createApp } from 'vue';
import App from './App.vue';
import { router } from './router';
import './styles.css';
createApp(App).use(createPinia()).use(router).use(ElementPlus).mount('#app');
+24
View File
@@ -0,0 +1,24 @@
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from './stores/auth';
export const router = createRouter({
history: createWebHistory('/ai-agent-admin/'),
routes: [
{ path: '/login', component: () => import('./views/LoginView.vue') },
{ path: '/', component: () => import('./views/DashboardView.vue') },
{ path: '/system/:resource', component: () => import('./views/ResourceView.vue') },
{ path: '/ai/chat', component: () => import('./views/AgentChatView.vue') },
{ path: '/ai/workflow-runs', component: () => import('./views/RunsView.vue'), props: { type: 'workflow' } },
{ path: '/ai/collaboration-runs', component: () => import('./views/RunsView.vue'), props: { type: 'collaboration' } },
{ path: '/ai/:resource', component: () => import('./views/ResourceView.vue') },
],
});
router.beforeEach(async (to) => {
const auth = useAuthStore();
if (to.path === '/login') return true;
if (!auth.token) return '/login';
if (!auth.user) await auth.fetchMe();
return true;
});
+27
View File
@@ -0,0 +1,27 @@
import { defineStore } from 'pinia';
import { api } from '@/api/http';
export const useAuthStore = defineStore('auth', {
state: () => ({
token: localStorage.getItem('ai-agent-admin-token') || '',
user: null as any,
}),
actions: {
async login(username: string, password: string) {
const data: any = await api.post('/auth/login', { username, password });
this.token = data.access_token;
localStorage.setItem('ai-agent-admin-token', this.token);
await this.fetchMe();
},
async fetchMe() {
if (!this.token) return;
this.user = await api.get('/auth/me');
},
logout() {
this.token = '';
this.user = null;
localStorage.removeItem('ai-agent-admin-token');
},
},
});
+155
View File
@@ -0,0 +1,155 @@
:root {
color: #1f2937;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f5f7fb;
}
body {
margin: 0;
}
.app-shell {
min-height: 100vh;
}
.sidebar {
background: #101827;
color: #fff;
}
.brand {
align-items: center;
display: flex;
gap: 12px;
height: 64px;
padding: 0 18px;
}
.brand-mark {
align-items: center;
background: #2dd4bf;
border-radius: 6px;
color: #083344;
display: inline-flex;
font-weight: 800;
height: 34px;
justify-content: center;
width: 34px;
}
.brand small {
color: #9ca3af;
display: block;
font-size: 12px;
margin-top: 2px;
}
.menu {
border-right: 0;
}
.sidebar .el-menu {
background: transparent;
}
.sidebar .el-menu-item,
.sidebar .el-sub-menu__title {
color: #d1d5db;
}
.sidebar .el-menu-item.is-active {
background: #1f3a4a;
color: #67e8f9;
}
.topbar {
align-items: center;
background: #fff;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
}
.main {
padding: 20px;
}
.panel {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 18px;
}
.toolbar {
align-items: center;
display: flex;
gap: 12px;
justify-content: space-between;
margin-bottom: 14px;
}
.grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.metric {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 18px;
}
.metric strong {
display: block;
font-size: 28px;
margin-top: 8px;
}
.login-page {
align-items: center;
background: linear-gradient(135deg, #0f172a, #164e63);
display: flex;
min-height: 100vh;
justify-content: center;
}
.login-box {
background: #fff;
border-radius: 8px;
padding: 28px;
width: 360px;
}
.chat {
display: grid;
gap: 16px;
grid-template-columns: 320px 1fr;
}
.chat-log {
background: #f8fafc;
border: 1px solid #e5e7eb;
border-radius: 8px;
height: 460px;
overflow: auto;
padding: 12px;
}
.message {
border-radius: 8px;
margin-bottom: 10px;
padding: 10px;
white-space: pre-wrap;
}
.message.user {
background: #dbeafe;
}
.message.assistant {
background: #ecfdf5;
}
+94
View File
@@ -0,0 +1,94 @@
<template>
<div class="chat">
<div class="panel">
<div class="toolbar">
<h3>智能体</h3>
<el-button @click="loadAgents">刷新</el-button>
</div>
<el-table :data="agents" highlight-current-row @current-change="selectAgent">
<el-table-column prop="name" label="名称" />
<el-table-column prop="status" label="状态" width="100" />
</el-table>
</div>
<div class="panel">
<div class="toolbar">
<h3>{{ currentAgent?.name || 'Agent Chat' }}</h3>
</div>
<div class="chat-log">
<div v-for="(message, index) in messages" :key="index" class="message" :class="message.role">
<strong>{{ message.role === 'user' ? '我' : 'Agent' }}</strong>
<div>{{ message.content }}</div>
</div>
</div>
<el-input v-model="input" :rows="3" type="textarea" placeholder="输入任务或问题" style="margin-top: 12px" />
<el-button type="primary" :loading="sending" style="margin-top: 12px" @click="send">发送</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus';
import { onMounted, ref } from 'vue';
import { api } from '@/api/http';
const agents = ref<any[]>([]);
const currentAgent = ref<any>(null);
const input = ref('请给出这个系统下一步优化建议');
const messages = ref<{ role: string; content: string }[]>([]);
const sending = ref(false);
onMounted(loadAgents);
async function loadAgents() {
const data: any = await api.get('/ai/agents');
agents.value = data.items || [];
currentAgent.value ||= agents.value[0];
}
function selectAgent(row: any) {
currentAgent.value = row;
messages.value = [];
}
async function send() {
if (!currentAgent.value) return ElMessage.warning('请先选择智能体');
if (!input.value.trim()) return;
const userText = input.value;
input.value = '';
messages.value.push({ role: 'user', content: userText });
const assistant = { role: 'assistant', content: '' };
messages.value.push(assistant);
sending.value = true;
try {
const token = localStorage.getItem('ai-agent-admin-token');
const response = await fetch(`/ai-agent-admin/basic-api/api/ai/agents/${currentAgent.value.id}/chat`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: userText }),
});
if (!response.body) throw new Error('浏览器不支持流式读取');
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split('\n')) {
if (!line.startsWith('data: ')) continue;
const raw = line.slice(6);
if (raw === '[DONE]') continue;
const event = JSON.parse(raw);
if (event.type === 'chunk') assistant.content += event.content;
}
}
} catch (error: any) {
ElMessage.error(error.message || '发送失败');
} finally {
sending.value = false;
}
}
</script>
+34
View File
@@ -0,0 +1,34 @@
<template>
<div>
<div class="grid">
<div v-for="item in metrics" :key="item.label" class="metric">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
<div class="panel" style="margin-top: 16px">
<h3>当前交付边界</h3>
<el-table :data="modules" border>
<el-table-column prop="name" label="模块" />
<el-table-column prop="scope" label="范围" />
<el-table-column prop="status" label="状态" width="120" />
</el-table>
</div>
</div>
</template>
<script setup lang="ts">
const metrics = [
{ label: '基础 Admin', value: '5' },
{ label: 'AI 模块', value: '8' },
{ label: '前端架构', value: 'Vite' },
{ label: '构建模式', value: '单应用' },
];
const modules = [
{ name: '认证/RBAC', scope: '登录、用户、角色、菜单、权限、公告', status: '可用' },
{ name: 'AI Agent', scope: 'Provider、Model、Agent、Chat SSE', status: '可用' },
{ name: 'Workflow', scope: 'JSON 定义、发布、运行记录', status: '可用' },
{ name: 'Agent Team', scope: '顺序多 Agent 协作和运行记录', status: '可用' },
];
</script>
+42
View File
@@ -0,0 +1,42 @@
<template>
<div class="login-page">
<el-form class="login-box" @submit.prevent="submit">
<h2>AI Agent Admin</h2>
<el-form-item>
<el-input v-model="username" placeholder="用户名或邮箱" size="large" />
</el-form-item>
<el-form-item>
<el-input v-model="password" placeholder="密码" show-password size="large" />
</el-form-item>
<el-button type="primary" native-type="submit" size="large" :loading="loading" style="width: 100%">
登录
</el-button>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
const username = ref('admin');
const password = ref('admin123456');
const loading = ref(false);
const auth = useAuthStore();
const router = useRouter();
async function submit() {
loading.value = true;
try {
await auth.login(username.value, password.value);
router.push('/');
} catch (error: any) {
ElMessage.error(error.response?.data?.detail || '登录失败');
} finally {
loading.value = false;
}
}
</script>
+132
View File
@@ -0,0 +1,132 @@
<template>
<div class="panel">
<div class="toolbar">
<h3>{{ config.title }}</h3>
<div>
<el-button v-if="resource === 'workflows'" @click="runSelected">运行</el-button>
<el-button v-if="resource === 'teams'" @click="runTeam">协作运行</el-button>
<el-button type="primary" @click="openCreate">新增</el-button>
</div>
</div>
<el-table :data="items" border highlight-current-row @current-change="current = $event">
<el-table-column prop="id" label="ID" width="220" />
<el-table-column v-for="field in visibleFields" :key="field.key" :prop="field.key" :label="field.label" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="120" />
<el-table-column label="操作" width="150">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogVisible" :title="editing?.id ? '编辑' : '新增'" width="680px">
<el-form label-width="120px">
<el-form-item v-for="field in config.fields" :key="field.key" :label="field.label">
<el-switch v-if="field.type === 'boolean'" v-model="form[field.key]" />
<el-input-number v-else-if="field.type === 'number'" v-model="form[field.key]" style="width: 100%" />
<el-input v-else-if="field.type === 'textarea' || field.type === 'json'" v-model="form[field.key]" :rows="field.type === 'json' ? 8 : 3" type="textarea" />
<el-input v-else v-model="form[field.key]" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus';
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { api } from '@/api/http';
import { deleteResource, listResource, resources, saveResource } from '@/api/resources';
const route = useRoute();
const resource = computed(() => String(route.params.resource || 'users'));
const config = computed(() => resources[resource.value]);
const items = ref<any[]>([]);
const current = ref<any>(null);
const dialogVisible = ref(false);
const editing = ref<any>(null);
const form = reactive<Record<string, any>>({});
const visibleFields = computed(() => config.value.fields.filter((field) => !['api_key', 'password', 'tools', 'knowledge_base_ids', 'definition', 'input_variables', 'output_variables', 'members'].includes(field.key)).slice(0, 5));
watch(resource, load);
onMounted(load);
async function load() {
if (!config.value) return;
const data: any = await listResource(resource.value);
items.value = data.items || [];
}
function resetForm(row?: any) {
Object.keys(form).forEach((key) => delete form[key]);
for (const field of config.value.fields) {
const value = row?.[field.key];
if (field.type === 'json') {
form[field.key] = value === undefined ? '[]' : JSON.stringify(value, null, 2);
} else if (field.type === 'boolean') {
form[field.key] = value ?? false;
} else if (field.type === 'number') {
form[field.key] = value ?? 0;
} else {
form[field.key] = value ?? '';
}
}
}
function openCreate() {
editing.value = null;
resetForm();
dialogVisible.value = true;
}
function openEdit(row: any) {
editing.value = row;
resetForm(row);
dialogVisible.value = true;
}
async function save() {
const payload: Record<string, any> = {};
for (const field of config.value.fields) {
if (field.key === 'password' && !form[field.key]) continue;
if (field.type === 'json') {
payload[field.key] = form[field.key] ? JSON.parse(form[field.key]) : [];
} else {
payload[field.key] = form[field.key];
}
}
await saveResource(resource.value, payload, editing.value?.id);
dialogVisible.value = false;
ElMessage.success('已保存');
await load();
}
async function remove(row: any) {
await ElMessageBox.confirm(`确认删除 ${row.name || row.title || row.username || row.id}`);
await deleteResource(resource.value, row.id);
ElMessage.success('已删除');
await load();
}
async function runSelected() {
const row = current.value || items.value[0];
if (!row) return ElMessage.warning('请先选择工作流');
await api.post(`/ai/workflows/${row.id}/run`, { inputs: { task: '验证最小工作流运行' } });
ElMessage.success('工作流已运行');
}
async function runTeam() {
const row = current.value || items.value[0];
if (!row) return ElMessage.warning('请先选择团队');
await api.post(`/ai/teams/${row.id}/run`, { task: '请协作完成一个轻量后台建设验收建议' });
ElMessage.success('协作任务已运行');
}
</script>
+38
View File
@@ -0,0 +1,38 @@
<template>
<div class="panel">
<div class="toolbar">
<h3>{{ type === 'workflow' ? 'Workflow Runs' : 'Collaboration Runs' }}</h3>
<el-button @click="load">刷新</el-button>
</div>
<el-table :data="items" border>
<el-table-column prop="id" label="ID" width="220" />
<el-table-column v-if="type === 'workflow'" prop="workflow_id" label="Workflow ID" width="220" />
<el-table-column v-else prop="team_id" label="Team ID" width="220" />
<el-table-column prop="status" label="状态" width="120" />
<el-table-column prop="elapsed_time" label="耗时 ms" width="120" />
<el-table-column label="详情">
<template #default="{ row }">
<pre>{{ JSON.stringify(row.outputs || row.messages || row.final_answer, null, 2) }}</pre>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { api } from '@/api/http';
const props = defineProps<{ type: 'workflow' | 'collaboration' }>();
const items = ref<any[]>([]);
watch(() => props.type, load);
onMounted(load);
async function load() {
const path = props.type === 'workflow' ? '/ai/workflow-runs' : '/ai/collaboration-runs';
const data: any = await api.get(path);
items.value = data.items || [];
}
</script>
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.vue"]
}
+26
View File
@@ -0,0 +1,26 @@
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
base: '/ai-agent-admin/',
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
proxy: {
'/ai-agent-admin/basic-api': {
target: 'http://127.0.0.1:18083',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/ai-agent-admin\/basic-api/, ''),
},
},
},
build: {
sourcemap: false,
chunkSizeWarningLimit: 900,
},
});