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.data_router import data
from src.routers.base_router import base from src.routers.base_router import base
from src.routers.tool_router import tool from src.routers.tool_router import tool
from src.routers.admin_router import admin
router = APIRouter() router = APIRouter()
router.include_router(base) router.include_router(base)
router.include_router(chat) router.include_router(chat)
router.include_router(data) router.include_router(data)
router.include_router(tool) 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 asyncio
import traceback import traceback
import uuid import uuid
from fastapi import APIRouter, Body from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from langchain_core.messages import AIMessageChunk from langchain_core.messages import AIMessageChunk
from pydantic import BaseModel
from sqlalchemy.orm import Session
from src import executor, config, retriever from src import executor, config, retriever
from src.core import HistoryManager from src.core import HistoryManager
from src.agents import agent_manager from src.agents import agent_manager
from src.models import select_model from src.models import select_model
from src.utils.logging_config import logger 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") 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("/") @chat.get("/")
async def 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> <template>
<div class="agent-single-view"> <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> </div>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import AgentChatComponent from '@/components/AgentChatComponent.vue'; import AgentChatComponent from '@/components/AgentChatComponent.vue';
import { message } from 'ant-design-vue';
const route = useRoute(); const route = useRoute();
const router = useRouter();
const agentId = computed(() => route.params.agent_id); 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> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -19,6 +109,23 @@ const agentId = computed(() => route.params.agent_id);
height: 100vh; height: 100vh;
overflow: hidden; 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> </style>

View File

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