server session initial

This commit is contained in:
Wenjie Zhang 2025-04-03 16:02:14 +08:00
parent 988426a5f5
commit fbbefe0a63
8 changed files with 610 additions and 22 deletions

24
src/models/token_model.py Normal file
View File

@ -0,0 +1,24 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func
Base = declarative_base()
class AgentToken(Base):
"""智能体访问令牌模型"""
__tablename__ = 'agent_tokens'
id = Column(Integer, primary_key=True, autoincrement=True)
agent_id = Column(String, nullable=False, index=True) # 智能体ID
name = Column(String, nullable=False) # 令牌名称
token = Column(String, nullable=False, unique=True) # 令牌值
created_at = Column(DateTime, default=func.now()) # 创建时间
def to_dict(self):
return {
"id": self.id,
"agent_id": self.agent_id,
"name": self.name,
"token": self.token,
"created_at": self.created_at.isoformat() if self.created_at else None
}

View File

@ -3,9 +3,11 @@ from src.routers.chat_router import chat
from src.routers.data_router import data
from src.routers.base_router import base
from src.routers.tool_router import tool
from src.routers.admin_router import admin
router = APIRouter()
router.include_router(base)
router.include_router(chat)
router.include_router(data)
router.include_router(tool)
router.include_router(admin)

View File

@ -0,0 +1,83 @@
import secrets
import string
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from typing import List, Optional
from sqlalchemy.orm import Session
from src.utils.db_manager import db_manager
from src.models.token_model import AgentToken
admin = APIRouter(prefix="/admin", tags=["admin"])
# 依赖项:获取数据库会话
def get_db():
db = db_manager.get_session()
try:
yield db
finally:
db.close()
# 请求和响应模型
class TokenCreate(BaseModel):
agent_id: str
name: str
class TokenResponse(BaseModel):
id: int
agent_id: str
name: str
token: str
created_at: str
# 生成随机token
def generate_token(length=32):
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(length))
@admin.get("/tokens", response_model=List[TokenResponse])
async def get_agent_tokens(
agent_id: Optional[str] = Query(None),
db: Session = Depends(get_db)
):
"""获取智能体的token列表"""
query = db.query(AgentToken)
if agent_id:
query = query.filter(AgentToken.agent_id == agent_id)
tokens = query.all()
return [token.to_dict() for token in tokens]
@admin.post("/tokens", response_model=TokenResponse)
async def create_token(
token_data: TokenCreate,
db: Session = Depends(get_db)
):
"""创建新的token"""
# 生成随机token
token_value = generate_token()
# 创建token记录
new_token = AgentToken(
agent_id=token_data.agent_id,
name=token_data.name,
token=token_value
)
# 保存到数据库
db.add(new_token)
db.commit()
db.refresh(new_token)
return new_token.to_dict()
@admin.delete("/tokens/{token_id}", response_model=dict)
async def delete_token(token_id: int, db: Session = Depends(get_db)):
"""删除token"""
token = db.query(AgentToken).filter(AgentToken.id == token_id).first()
if not token:
raise HTTPException(status_code=404, detail="Token not found")
db.delete(token)
db.commit()
return {"success": True, "message": "Token deleted"}

View File

@ -3,19 +3,49 @@ import json
import asyncio
import traceback
import uuid
from fastapi import APIRouter, Body
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.responses import StreamingResponse
from langchain_core.messages import AIMessageChunk
from pydantic import BaseModel
from sqlalchemy.orm import Session
from src import executor, config, retriever
from src.core import HistoryManager
from src.agents import agent_manager
from src.models import select_model
from src.utils.logging_config import logger
from src.utils.db_manager import db_manager
from src.models.token_model import AgentToken
chat = APIRouter(prefix="/chat")
# 依赖项:获取数据库会话
def get_db():
db = db_manager.get_session()
try:
yield db
finally:
db.close()
class TokenVerify(BaseModel):
agent_id: str
token: str
@chat.post("/verify_token")
async def verify_agent_token(
token_data: TokenVerify,
db: Session = Depends(get_db)
):
"""验证智能体访问令牌"""
token = db.query(AgentToken).filter(
AgentToken.agent_id == token_data.agent_id,
AgentToken.token == token_data.token
).first()
if not token:
raise HTTPException(status_code=401, detail="Invalid token")
return {"success": True, "message": "Token verified"}
@chat.get("/")
async def chat_get():

40
src/utils/db_manager.py Normal file
View File

@ -0,0 +1,40 @@
import os
import sqlite3
import pathlib
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from src.models.token_model import Base, AgentToken
class DBManager:
"""数据库管理器"""
def __init__(self):
self.db_path = os.path.join("saves", "data", "server.db")
self.ensure_db_dir()
# 创建SQLAlchemy引擎
self.engine = create_engine(f"sqlite:///{self.db_path}")
# 创建会话工厂
self.Session = sessionmaker(bind=self.engine)
# 确保表存在
self.create_tables()
def ensure_db_dir(self):
"""确保数据库目录存在"""
db_dir = os.path.dirname(self.db_path)
pathlib.Path(db_dir).mkdir(parents=True, exist_ok=True)
def create_tables(self):
"""创建数据库表"""
Base.metadata.create_all(self.engine)
def get_session(self):
"""获取数据库会话"""
return self.Session()
# 创建全局数据库管理器实例
db_manager = DBManager()

View File

@ -0,0 +1,253 @@
<template>
<div class="token-manager">
<h2 class="manager-title">访问令牌管理</h2>
<div class="token-tools">
<a-button type="primary" size="small" @click="showAddTokenModal">
<PlusOutlined /> 创建令牌
</a-button>
</div>
<!-- 令牌列表 -->
<div class="token-list" v-if="tokens.length > 0">
<a-spin :spinning="loading">
<a-list size="small">
<a-list-item v-for="token in tokens" :key="token.id">
<div class="token-item">
<div class="token-info">
<div class="token-name">{{ token.name }}</div>
<div class="token-value">
<code>{{ token.token }}</code>
<a-button type="link" size="small" @click="copyToken(token.token)">
<CopyOutlined />
</a-button>
</div>
<div class="token-time">创建时间: {{ formatDate(token.created_at) }}</div>
</div>
<div class="token-actions">
<a-popconfirm
title="确定要删除这个令牌吗?"
ok-text="确定"
cancel-text="取消"
@confirm="deleteToken(token.id)"
>
<a-button type="text" danger size="small">
<DeleteOutlined />
</a-button>
</a-popconfirm>
</div>
</div>
</a-list-item>
</a-list>
</a-spin>
</div>
<a-empty v-else description="暂无访问令牌" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<!-- 添加令牌弹窗 -->
<a-modal
v-model:visible="addTokenModalVisible"
title="添加访问令牌"
ok-text="创建"
cancel-text="取消"
@ok="createToken"
>
<a-form :model="newToken" layout="vertical">
<a-form-item label="令牌名称" name="name">
<a-input v-model:value="newToken.name" placeholder="请输入令牌名称" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { message, Empty } from 'ant-design-vue';
import { PlusOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons-vue';
const props = defineProps({
agentId: {
type: String,
required: true
}
});
//
const tokens = ref([]);
const loading = ref(false);
const addTokenModalVisible = ref(false);
const newToken = ref({
name: ''
});
//
const fetchTokens = async () => {
loading.value = true;
try {
const response = await fetch(`/api/admin/tokens?agent_id=${props.agentId}`);
if (response.ok) {
const data = await response.json();
tokens.value = data;
} else {
message.error('获取令牌列表失败');
}
} catch (error) {
console.error('获取令牌列表出错:', error);
message.error('获取令牌列表出错');
} finally {
loading.value = false;
}
};
//
const createToken = async () => {
if (!newToken.value.name.trim()) {
message.warning('请输入令牌名称');
return;
}
try {
const response = await fetch('/api/admin/tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
agent_id: props.agentId,
name: newToken.value.name
})
});
if (response.ok) {
const data = await response.json();
tokens.value.push(data);
message.success('令牌创建成功');
addTokenModalVisible.value = false;
newToken.value.name = '';
} else {
message.error('创建令牌失败');
}
} catch (error) {
console.error('创建令牌出错:', error);
message.error('创建令牌出错');
}
};
//
const deleteToken = async (tokenId) => {
try {
const response = await fetch(`/api/admin/tokens/${tokenId}`, {
method: 'DELETE'
});
if (response.ok) {
tokens.value = tokens.value.filter(token => token.id !== tokenId);
message.success('令牌已删除');
} else {
message.error('删除令牌失败');
}
} catch (error) {
console.error('删除令牌出错:', error);
message.error('删除令牌出错');
}
};
//
const copyToken = (token) => {
navigator.clipboard.writeText(token).then(() => {
message.success('令牌已复制到剪贴板');
});
};
//
const showAddTokenModal = () => {
newToken.value.name = '';
addTokenModalVisible.value = true;
};
//
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString();
};
// agentId
watch(() => props.agentId, (newAgentId) => {
if (newAgentId) {
fetchTokens();
} else {
tokens.value = [];
}
}, { immediate: true });
//
onMounted(() => {
if (props.agentId) {
fetchTokens();
}
});
</script>
<style lang="less" scoped>
.token-manager {
margin-top: 1rem;
padding: 0 0.5rem;
}
.manager-title {
font-size: 1rem;
margin-bottom: 1rem;
}
.token-tools {
margin-bottom: 1rem;
display: flex;
justify-content: flex-end;
}
.token-list {
max-height: calc(100vh - 400px);
overflow-y: auto;
}
.token-item {
display: flex;
justify-content: space-between;
width: 100%;
}
.token-info {
flex: 1;
}
.token-name {
font-weight: 500;
margin-bottom: 0.25rem;
}
.token-value {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
background-color: var(--main-light-4);
padding: 0.25rem 0.5rem;
border-radius: 4px;
margin-bottom: 0.25rem;
overflow-x: auto;
user-select: all;
code {
flex: 1;
}
}
.token-time {
font-size: 0.75rem;
color: var(--gray-500);
}
.token-actions {
display: flex;
align-items: center;
}
</style>

View File

@ -1,16 +1,106 @@
<template>
<div class="agent-single-view">
<AgentChatComponent :agent-id="agentId" />
<!-- Token验证弹窗 -->
<a-modal
v-model:visible="tokenModalVisible"
title="访问验证"
:closable="false"
:maskClosable="false"
:keyboard="false"
:footer="null"
>
<div class="token-verify-form">
<p>需要输入访问令牌才能使用该智能体</p>
<a-input-password
v-model:value="tokenInput"
placeholder="请输入访问令牌"
@pressEnter="verifyToken"
/>
<div class="error-message" v-if="errorMessage">{{ errorMessage }}</div>
<div class="token-actions">
<a-button type="primary" :loading="verifying" @click="verifyToken">验证</a-button>
</div>
</div>
</a-modal>
<!-- 智能体聊天界面 -->
<AgentChatComponent v-if="isVerified" :agent-id="agentId" />
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import AgentChatComponent from '@/components/AgentChatComponent.vue';
import { message } from 'ant-design-vue';
const route = useRoute();
const router = useRouter();
const agentId = computed(() => route.params.agent_id);
// Token
const tokenModalVisible = ref(false);
const tokenInput = ref('');
const isVerified = ref(false);
const verifying = ref(false);
const errorMessage = ref('');
// Token
const verifyToken = async () => {
if (!tokenInput.value) {
errorMessage.value = '请输入访问令牌';
return;
}
verifying.value = true;
errorMessage.value = '';
try {
const response = await fetch('/api/chat/verify_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
agent_id: agentId.value,
token: tokenInput.value
})
});
if (response.ok) {
//
isVerified.value = true;
tokenModalVisible.value = false;
// localStorage
localStorage.setItem(`agent-token-verified-${agentId.value}`, 'true');
} else {
//
const data = await response.json();
errorMessage.value = data.detail || '令牌验证失败';
}
} catch (error) {
console.error('验证令牌出错:', error);
errorMessage.value = '验证令牌时发生错误';
} finally {
verifying.value = false;
}
};
//
const checkVerification = () => {
const verified = localStorage.getItem(`agent-token-verified-${agentId.value}`);
if (verified === 'true') {
isVerified.value = true;
} else {
tokenModalVisible.value = true;
}
};
//
onMounted(() => {
checkVerification();
});
</script>
<style lang="less" scoped>
@ -19,6 +109,23 @@ const agentId = computed(() => route.params.agent_id);
height: 100vh;
overflow: hidden;
}
.token-verify-form {
display: flex;
flex-direction: column;
gap: 1rem;
.error-message {
color: #ff4d4f;
font-size: 0.85rem;
}
.token-actions {
display: flex;
justify-content: flex-end;
margin-top: 0.5rem;
}
}
</style>

View File

@ -79,10 +79,38 @@
<CloseOutlined class="iconfont icon-20" />
</div>
</h2>
<div v-if="selectedAgentId && configSchema" class="config-form">
<div v-if="selectedAgentId" class="config-form">
<!-- 打开配置弹窗的按钮 -->
<a-button
type="primary"
block
@click="openConfigModal"
>
打开智能体配置
</a-button>
<!-- 添加Token管理组件 -->
<div class="token-section">
<TokenManagerComponent :agent-id="selectedAgentId" />
</div>
</div>
<div v-else class="no-agent-selected">
请先选择一个智能体
</div>
</div>
<!-- 配置弹窗 -->
<a-modal
v-model:visible="state.configModalVisible"
title="智能体配置"
width="650px"
:footer="null"
@cancel="closeConfigModal"
>
<div v-if="selectedAgentId && configSchema" class="config-modal-content">
<!-- 配置表单 -->
<a-form :model="agentConfig" layout="vertical">
<!-- 系统提示词 -->
<div class="empty-config" v-if="state.isEmptyConfig">
<a-alert type="warning" message="该智能体没有配置项" show-icon/>
</div>
@ -93,8 +121,6 @@
:name="key"
class="config-item"
>
<!-- <p>{{ value }}</p> -->
<!-- 根据值的类型使用不同的输入控件 -->
<p v-if="value.description" class="description">{{ value.description }}</p>
<a-switch
v-if="typeof agentConfig[key] === 'boolean'"
@ -120,17 +146,15 @@
</a-form-item>
</template>
<!-- 保存和重置按钮 -->
<!-- 弹窗底部按钮 -->
<div class="form-actions" v-if="!state.isEmptyConfig">
<a-button type="primary" @click="saveConfig">保存配置</a-button>
<a-button @click="resetConfig">重置</a-button>
<a-button @click="closeConfigModal">取消</a-button>
</div>
</a-form>
</div>
<div v-else class="no-agent-selected">
请先选择一个智能体
</div>
</div>
</a-modal>
</div>
</template>
@ -145,6 +169,7 @@ import {
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import AgentChatComponent from '@/components/AgentChatComponent.vue';
import TokenManagerComponent from '@/components/TokenManagerComponent.vue';
//
const agents = ref({});
@ -153,6 +178,7 @@ const state = reactive({
debug_mode: false,
isSidebarOpen: JSON.parse(localStorage.getItem('agent-sidebar-open') || 'true'),
isConfigSidebarOpen: false,
configModalVisible: false,
isEmptyConfig: computed(() =>
!selectedAgentId.value ||
Object.keys(configurableItems.value).length === 0
@ -169,6 +195,16 @@ const toggleDebugMode = () => {
state.debug_mode = !state.debug_mode;
};
//
const openConfigModal = () => {
state.configModalVisible = true;
};
//
const closeConfigModal = () => {
state.configModalVisible = false;
};
//
const loadAgentConfig = () => {
// BUG:
@ -222,6 +258,7 @@ const saveConfig = () => {
//
message.success('配置已保存');
closeConfigModal();
};
//
@ -379,18 +416,13 @@ const getPlaceholder = (key, value) => {
overflow-y: auto;
max-height: calc(100vh - 100px);
.description {
font-size: 12px;
color: var(--gray-700);
.token-section {
margin-top: 1.5rem;
border-top: 1px solid var(--main-light-3);
padding-top: 1rem;
}
}
.form-actions {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.no-agent-selected {
padding: 16px;
color: var(--gray-500);
@ -548,6 +580,23 @@ const getPlaceholder = (key, value) => {
}
}
}
.config-modal-content {
max-height: 70vh;
overflow-y: auto;
.description {
font-size: 12px;
color: var(--gray-700);
}
.form-actions {
display: flex;
justify-content: space-between;
margin-top: 20px;
gap: 10px;
}
}
</style>