first commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
es2021: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-recommended',
|
||||
'eslint:recommended',
|
||||
'@vue/typescript/recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SentClaw - AI 智能助手</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "sentclaw-web",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"pinia": "^2.1.7",
|
||||
"axios": "^1.6.2",
|
||||
"element-plus": "^2.5.0",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"nprogress": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"@vue/tsconfig": "^0.5.0",
|
||||
"typescript": "~5.3.0",
|
||||
"vite": "^5.0.0",
|
||||
"vue-tsc": "^1.8.27",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"prettier": "^3.1.1",
|
||||
"sass": "^1.69.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
import request from './request'
|
||||
import type { Agent } from '../types'
|
||||
|
||||
export interface AgentCreateData {
|
||||
name: string
|
||||
description?: string
|
||||
system_prompt?: string
|
||||
model_id?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
}
|
||||
|
||||
export interface AgentUpdateData {
|
||||
name?: string
|
||||
description?: string
|
||||
system_prompt?: string
|
||||
model_id?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
is_active?: boolean
|
||||
}
|
||||
|
||||
export const agentApi = {
|
||||
getAgents: (workspaceId: number) => request.get<Agent[]>(`/workspaces/${workspaceId}/agents`),
|
||||
createAgent: (workspaceId: number, data: AgentCreateData) => request.post<Agent>(`/workspaces/${workspaceId}/agents`, data),
|
||||
getAgent: (id: number) => request.get<Agent>(`/agents/${id}`),
|
||||
updateAgent: (id: number, data: AgentUpdateData) => request.put<Agent>(`/agents/${id}`, data),
|
||||
deleteAgent: (id: number) => request.delete(`/agents/${id}`)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import request from './request'
|
||||
import type { User } from '../types'
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
username: string
|
||||
password: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
message: string
|
||||
user: User
|
||||
access_token: string
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
login: (data: LoginRequest) => request.post<AuthResponse>('/auth/login', data),
|
||||
register: (data: RegisterRequest) => request.post<AuthResponse>('/auth/register', data),
|
||||
getCurrentUser: () => request.get<User>('/users/me'),
|
||||
updateUser: (data: Partial<User>) => request.put<User>('/users/me', data)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import request from './request'
|
||||
import type { Conversation } from '../types'
|
||||
|
||||
export interface ConversationCreateData {
|
||||
agent_id: number
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface ConversationUpdateData {
|
||||
title?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export const conversationApi = {
|
||||
getConversations: (workspaceId: number) => request.get<Conversation[]>(`/workspaces/${workspaceId}/conversations`),
|
||||
createConversation: (workspaceId: number, data: ConversationCreateData) => request.post<Conversation>(`/workspaces/${workspaceId}/conversations`, data),
|
||||
getConversation: (id: number) => request.get<Conversation>(`/conversations/${id}`),
|
||||
updateConversation: (id: number, data: ConversationUpdateData) => request.put<Conversation>(`/conversations/${id}`, data)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import request from './request'
|
||||
import type { Message } from '../types'
|
||||
|
||||
export interface MessageCreateData {
|
||||
role: string
|
||||
content: string
|
||||
tool_calls?: any[]
|
||||
metadata?: any
|
||||
}
|
||||
|
||||
export const messageApi = {
|
||||
getMessages: (conversationId: number) => request.get<Message[]>(`/conversations/${conversationId}/messages`),
|
||||
createMessage: (conversationId: number, data: MessageCreateData) => request.post<Message>(`/conversations/${conversationId}/messages`, data)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data
|
||||
},
|
||||
(error) => {
|
||||
const message = error.response?.data?.error || error.message || '请求失败'
|
||||
ElMessage.error(message)
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
@@ -0,0 +1,20 @@
|
||||
import request from './request'
|
||||
import type { Workspace } from '../types'
|
||||
|
||||
export interface WorkspaceCreateData {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface WorkspaceUpdateData {
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const workspaceApi = {
|
||||
getWorkspaces: () => request.get<Workspace[]>('/workspaces'),
|
||||
createWorkspace: (data: WorkspaceCreateData) => request.post<Workspace>('/workspaces', data),
|
||||
getWorkspace: (id: number) => request.get<Workspace>(`/workspaces/${id}`),
|
||||
updateWorkspace: (id: number, data: WorkspaceUpdateData) => request.put<Workspace>(`/workspaces/${id}`, data),
|
||||
deleteWorkspace: (id: number) => request.delete(`/workspaces/${id}`)
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div class="main-layout">
|
||||
<el-container>
|
||||
<el-aside width="240px">
|
||||
<div class="logo">
|
||||
<h1>SentClaw</h1>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
class="sidebar-menu"
|
||||
router
|
||||
>
|
||||
<el-menu-item index="/">
|
||||
<el-icon><HomeFilled /></el-icon>
|
||||
<span>首页</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/chat">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
<span>对话</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/agents">
|
||||
<el-icon><Avatar /></el-icon>
|
||||
<span>Agent</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/settings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>设置</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="header-content">
|
||||
<h2>{{ pageTitle }}</h2>
|
||||
<div class="user-info">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="user-dropdown">
|
||||
<el-avatar :size="32" :src="user?.avatar" />
|
||||
<span>{{ user?.username }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">个人资料</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { HomeFilled, ChatDotRound, Avatar, Setting, ArrowDown } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const user = computed(() => userStore.user)
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
const titles: Record<string, string> = {
|
||||
'/': '首页',
|
||||
'/chat': '对话',
|
||||
'/agents': 'Agent 管理',
|
||||
'/settings': '设置'
|
||||
}
|
||||
return titles[route.path] || 'SentClaw'
|
||||
})
|
||||
|
||||
const activeMenu = computed(() => route.path)
|
||||
|
||||
const handleCommand = (command: string) => {
|
||||
if (command === 'logout') {
|
||||
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
userStore.logout()
|
||||
ElMessage.success('已退出登录')
|
||||
router.push('/login')
|
||||
}).catch(() => {})
|
||||
} else if (command === 'profile') {
|
||||
router.push('/settings')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-layout {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.el-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-aside {
|
||||
background-color: #304156;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #2b3a4a;
|
||||
border-bottom: 1px solid #1f2d3d;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border: none;
|
||||
background-color: #304156;
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
color: #bfcbd9;
|
||||
|
||||
&:hover {
|
||||
background-color: #263445;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: #409eff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.el-main {
|
||||
background-color: #f0f2f5;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/main.scss'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,65 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/Login.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: () => import('../views/Register.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('../layouts/MainLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Home',
|
||||
component: () => import('../views/Home.vue')
|
||||
},
|
||||
{
|
||||
path: 'chat/:conversationId?',
|
||||
name: 'Chat',
|
||||
component: () => import('../views/Chat.vue')
|
||||
},
|
||||
{
|
||||
path: 'agents',
|
||||
name: 'Agents',
|
||||
component: () => import('../views/Agents.vue')
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Settings',
|
||||
component: () => import('../views/Settings.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
const requiresAuth = to.meta.requiresAuth !== false
|
||||
|
||||
if (requiresAuth && !userStore.token) {
|
||||
next('/login')
|
||||
} else if ((to.path === '/login' || to.path === '/register') && userStore.token) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,54 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { User } from '../types'
|
||||
import { authApi } from '../api/auth'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const user = ref<User | null>(null)
|
||||
const token = ref<string | null>(localStorage.getItem('token'))
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
const response = await authApi.login({ username, password })
|
||||
user.value = response.user
|
||||
token.value = response.access_token
|
||||
localStorage.setItem('token', response.access_token)
|
||||
localStorage.setItem('user', JSON.stringify(response.user))
|
||||
return response
|
||||
}
|
||||
|
||||
const register = async (username: string, password: string, email?: string) => {
|
||||
const response = await authApi.register({ username, password, email })
|
||||
user.value = response.user
|
||||
token.value = response.access_token
|
||||
localStorage.setItem('token', response.access_token)
|
||||
localStorage.setItem('user', JSON.stringify(response.user))
|
||||
return response
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
user.value = null
|
||||
token.value = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
const loadUser = async () => {
|
||||
if (!token.value) return
|
||||
try {
|
||||
const response = await authApi.getCurrentUser()
|
||||
user.value = response
|
||||
localStorage.setItem('user', JSON.stringify(response))
|
||||
} catch (error) {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
loadUser
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { Workspace } from '../types'
|
||||
import { workspaceApi } from '../api/workspace'
|
||||
|
||||
export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
const workspaces = ref<Workspace[]>([])
|
||||
const currentWorkspace = ref<Workspace | null>(null)
|
||||
|
||||
const loadWorkspaces = async () => {
|
||||
const response = await workspaceApi.getWorkspaces()
|
||||
workspaces.value = response
|
||||
if (!currentWorkspace.value && workspaces.value.length > 0) {
|
||||
currentWorkspace.value = workspaces.value[0]
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
const createWorkspace = async (name: string, description?: string) => {
|
||||
const response = await workspaceApi.createWorkspace({ name, description })
|
||||
workspaces.value.push(response)
|
||||
return response
|
||||
}
|
||||
|
||||
const updateWorkspace = async (id: number, data: any) => {
|
||||
const response = await workspaceApi.updateWorkspace(id, data)
|
||||
const index = workspaces.value.findIndex((w) => w.id === id)
|
||||
if (index !== -1) {
|
||||
workspaces.value[index] = response
|
||||
}
|
||||
if (currentWorkspace.value?.id === id) {
|
||||
currentWorkspace.value = response
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
const deleteWorkspace = async (id: number) => {
|
||||
await workspaceApi.deleteWorkspace(id)
|
||||
workspaces.value = workspaces.value.filter((w) => w.id !== id)
|
||||
if (currentWorkspace.value?.id === id) {
|
||||
currentWorkspace.value = workspaces.value[0] || null
|
||||
}
|
||||
}
|
||||
|
||||
const setCurrentWorkspace = (workspace: Workspace) => {
|
||||
currentWorkspace.value = workspace
|
||||
}
|
||||
|
||||
return {
|
||||
workspaces,
|
||||
currentWorkspace,
|
||||
loadWorkspaces,
|
||||
createWorkspace,
|
||||
updateWorkspace,
|
||||
deleteWorkspace,
|
||||
setCurrentWorkspace
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
email?: string
|
||||
avatar?: string
|
||||
is_active: boolean
|
||||
is_admin: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
id: number
|
||||
user_id: number
|
||||
name: string
|
||||
description?: string
|
||||
is_default: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
id: number
|
||||
workspace_id: number
|
||||
name: string
|
||||
description?: string
|
||||
system_prompt?: string
|
||||
model_id?: string
|
||||
temperature: number
|
||||
max_tokens: number
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
id: number
|
||||
workspace_id: number
|
||||
agent_id: number
|
||||
title?: string
|
||||
channel: string
|
||||
channel_user_id?: string
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: number
|
||||
conversation_id: number
|
||||
role: string
|
||||
content: string
|
||||
tokens?: number
|
||||
model?: string
|
||||
tool_calls?: any[]
|
||||
metadata?: any
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
config?: any
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Skill {
|
||||
id: number
|
||||
name: string
|
||||
version: string
|
||||
description?: string
|
||||
author?: string
|
||||
repository?: string
|
||||
config?: any
|
||||
is_installed: boolean
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Memory {
|
||||
id: number
|
||||
workspace_id: number
|
||||
agent_id?: number
|
||||
type: string
|
||||
content: string
|
||||
tags?: string[]
|
||||
importance: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
id: number
|
||||
provider: string
|
||||
name: string
|
||||
model_id: string
|
||||
api_key?: string
|
||||
base_url?: string
|
||||
is_default: boolean
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface CronJob {
|
||||
id: number
|
||||
agent_id: number
|
||||
name: string
|
||||
cron_expression: string
|
||||
prompt: string
|
||||
is_active: boolean
|
||||
last_run_at?: string
|
||||
next_run_at?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Channel {
|
||||
id: number
|
||||
type: string
|
||||
name: string
|
||||
config?: any
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="agents">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>Agent 管理</span>
|
||||
<el-button type="primary" size="small">创建 Agent</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-empty description="Agent 管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="chat">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>对话</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-empty description="对话功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card>
|
||||
<el-statistic title="工作空间" :value="stats.workspaces">
|
||||
<template #suffix>个</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card>
|
||||
<el-statistic title="Agent" :value="stats.agents">
|
||||
<template #suffix>个</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card>
|
||||
<el-statistic title="对话" :value="stats.conversations">
|
||||
<template #suffix>个</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card class="welcome-card" style="margin-top: 20px">
|
||||
<h2>欢迎使用 SentClaw</h2>
|
||||
<p>SentClaw 是一个基于 AI 的智能助手系统,支持多 Agent 编排、工具系统、记忆管理等功能。</p>
|
||||
<el-button type="primary" @click="$router.push('/chat')">
|
||||
开始对话
|
||||
</el-button>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const stats = reactive({
|
||||
workspaces: 0,
|
||||
agents: 0,
|
||||
conversations: 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home {
|
||||
h2 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 20px 0;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<el-card class="login-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>SentClaw</h2>
|
||||
<p>AI 智能助手</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" @submit.prevent="handleLogin">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名"
|
||||
size="large"
|
||||
prefix-icon="User"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
size="large"
|
||||
prefix-icon="Lock"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
style="width: 100%"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="register-link">
|
||||
还没有账号?<router-link to="/register">注册</router-link>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const loginFormRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
try {
|
||||
await loginFormRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
await userStore.login(loginForm.username, loginForm.password)
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} catch (error: any) {
|
||||
if (error.errors) {
|
||||
return
|
||||
}
|
||||
ElMessage.error(error.message || '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.register-link {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
color: #606266;
|
||||
|
||||
a {
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="register-container">
|
||||
<el-card class="register-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>注册账号</h2>
|
||||
<p>加入 SentClaw</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form ref="registerFormRef" :model="registerForm" :rules="rules" @submit.prevent="handleRegister">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="registerForm.username"
|
||||
placeholder="用户名"
|
||||
size="large"
|
||||
prefix-icon="User"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="email">
|
||||
<el-input
|
||||
v-model="registerForm.email"
|
||||
placeholder="邮箱(可选)"
|
||||
size="large"
|
||||
prefix-icon="Message"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
size="large"
|
||||
prefix-icon="Lock"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
placeholder="确认密码"
|
||||
size="large"
|
||||
prefix-icon="Lock"
|
||||
@keyup.enter="handleRegister"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleRegister"
|
||||
style="width: 100%"
|
||||
>
|
||||
注册
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="login-link">
|
||||
已有账号?<router-link to="/login">登录</router-link>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const registerFormRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const registerForm = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
|
||||
if (value !== registerForm.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认密码', trigger: 'blur' },
|
||||
{ validator: validateConfirmPassword, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!registerFormRef.value) return
|
||||
|
||||
try {
|
||||
await registerFormRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
await userStore.register(
|
||||
registerForm.username,
|
||||
registerForm.password,
|
||||
registerForm.email || undefined
|
||||
)
|
||||
|
||||
ElMessage.success('注册成功')
|
||||
router.push('/')
|
||||
} catch (error: any) {
|
||||
if (error.errors) {
|
||||
return
|
||||
}
|
||||
ElMessage.error(error.message || '注册失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.register-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.register-card {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.login-link {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
color: #606266;
|
||||
|
||||
a {
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="settings">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>设置</span>
|
||||
</template>
|
||||
|
||||
<el-empty description="设置功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"include": ["vite.config.ts"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'element-plus': ['element-plus'],
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user