Initial lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
Generated
+2104
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 };
|
||||
@@ -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}`);
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user