up
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -12,12 +13,24 @@ import (
|
||||
func registerAdminLLMRoutes(r *gin.RouterGroup, store *store) {
|
||||
group := r.Group("/llm")
|
||||
{
|
||||
// LLM Message Queue
|
||||
group.GET("/messages", handleListLLMMessages(store))
|
||||
group.GET("/messages/:id", handleGetLLMMessage(store))
|
||||
group.PUT("/messages/:id/status", handleUpdateLLMMessageStatus(store))
|
||||
group.DELETE("/messages/:id", handleDeleteLLMMessage(store))
|
||||
group.DELETE("/bots/:bot_id/messages", handleDeleteLLMMessagesByBot(store))
|
||||
group.POST("/messages/cleanup", handleCleanupDeletedLLMMessages(store))
|
||||
|
||||
// LLM Providers
|
||||
group.GET("/providers", handleListLLMProviders(store))
|
||||
group.GET("/providers/:name", handleGetLLMProvider(store))
|
||||
group.POST("/providers", handleCreateLLMProvider(store))
|
||||
group.PUT("/providers/:name", handleUpdateLLMProvider(store))
|
||||
group.DELETE("/providers/:name", handleDeleteLLMProvider(store))
|
||||
|
||||
// LLM Tool Router
|
||||
group.GET("/tool-router", handleGetLLMToolRouter(store))
|
||||
group.PUT("/tool-router", handleUpdateLLMToolRouter(store))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,3 +197,301 @@ func handleCleanupDeletedLLMMessages(store *store) gin.HandlerFunc {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "deleted_count": count})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LLM Provider Handlers
|
||||
// ============================================
|
||||
|
||||
func handleListLLMProviders(store *store) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
includeInactive := c.Query("include_inactive") == "true"
|
||||
|
||||
rows, err := store.ListLLMProviders(includeInactive)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]map[string]any, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, llmProviderDTO(row))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetLLMProvider(store *store) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "provider name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
record, err := store.GetLLMProvider(name)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"item": llmProviderDTO(*record)})
|
||||
}
|
||||
}
|
||||
|
||||
func handleCreateLLMProvider(store *store) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
APIKey string `json:"api_key"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Model string `json:"model"`
|
||||
Timeout int `json:"timeout"`
|
||||
ContextWindowTokens int `json:"context_window_tokens"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
record := &llmProviderRecord{
|
||||
Name: req.Name,
|
||||
Active: req.Active,
|
||||
APIKey: req.APIKey,
|
||||
BaseURL: req.BaseURL,
|
||||
Model: req.Model,
|
||||
Timeout: req.Timeout,
|
||||
ContextWindowTokens: req.ContextWindowTokens,
|
||||
}
|
||||
|
||||
if err := store.CreateLLMProvider(record); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "item": llmProviderDTO(*record)})
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateLLMProvider(store *store) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "provider name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Active *bool `json:"active"`
|
||||
APIKey *string `json:"api_key"`
|
||||
BaseURL *string `json:"base_url"`
|
||||
Model *string `json:"model"`
|
||||
Timeout *int `json:"timeout"`
|
||||
ContextWindowTokens *int `json:"context_window_tokens"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]any)
|
||||
if req.Active != nil {
|
||||
updates["active"] = *req.Active
|
||||
}
|
||||
if req.APIKey != nil {
|
||||
updates["api_key"] = *req.APIKey
|
||||
}
|
||||
if req.BaseURL != nil {
|
||||
updates["base_url"] = *req.BaseURL
|
||||
}
|
||||
if req.Model != nil {
|
||||
updates["model"] = *req.Model
|
||||
}
|
||||
if req.Timeout != nil {
|
||||
updates["timeout"] = *req.Timeout
|
||||
}
|
||||
if req.ContextWindowTokens != nil {
|
||||
updates["context_window_tokens"] = *req.ContextWindowTokens
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.UpdateLLMProvider(name, updates); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
record, err := store.GetLLMProvider(name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "item": llmProviderDTO(*record)})
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteLLMProvider(store *store) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "provider name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.DeleteLLMProvider(name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func llmProviderDTO(row llmProviderRecord) map[string]any {
|
||||
return map[string]any{
|
||||
"name": row.Name,
|
||||
"active": row.Active,
|
||||
"base_url": row.BaseURL,
|
||||
"model": row.Model,
|
||||
"timeout": row.Timeout,
|
||||
"context_window_tokens": row.ContextWindowTokens,
|
||||
"created_at": row.CreatedAt,
|
||||
"updated_at": row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LLM Tool Router Handlers
|
||||
// ============================================
|
||||
|
||||
func handleGetLLMToolRouter(store *store) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
record, err := store.GetLLMToolRouter()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tool router config not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"item": llmToolRouterDTO(*record)})
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateLLMToolRouter(store *store) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
record, err := store.GetLLMToolRouter()
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
OpenAIName *string `json:"openai_name"`
|
||||
Timeout *int `json:"timeout"`
|
||||
MaxTokens *int `json:"max_tokens"`
|
||||
SystemPrompt *string `json:"system_prompt"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]any)
|
||||
if req.Enabled != nil {
|
||||
updates["enabled"] = *req.Enabled
|
||||
}
|
||||
if req.OpenAIName != nil {
|
||||
updates["openai_name"] = *req.OpenAIName
|
||||
}
|
||||
if req.Timeout != nil {
|
||||
updates["timeout"] = *req.Timeout
|
||||
}
|
||||
if req.MaxTokens != nil {
|
||||
updates["max_tokens"] = *req.MaxTokens
|
||||
}
|
||||
if req.SystemPrompt != nil {
|
||||
updates["system_prompt"] = *req.SystemPrompt
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
|
||||
return
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
// 创建新配置
|
||||
newRecord := &llmToolRouterRecord{
|
||||
Enabled: req.Enabled != nil && *req.Enabled,
|
||||
OpenAIName: "",
|
||||
Timeout: 30,
|
||||
MaxTokens: 512,
|
||||
SystemPrompt: "",
|
||||
}
|
||||
if req.OpenAIName != nil {
|
||||
newRecord.OpenAIName = *req.OpenAIName
|
||||
}
|
||||
if req.Timeout != nil {
|
||||
newRecord.Timeout = *req.Timeout
|
||||
}
|
||||
if req.MaxTokens != nil {
|
||||
newRecord.MaxTokens = *req.MaxTokens
|
||||
}
|
||||
if req.SystemPrompt != nil {
|
||||
newRecord.SystemPrompt = *req.SystemPrompt
|
||||
}
|
||||
if err := store.CreateLLMToolRouter(newRecord); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
record = newRecord
|
||||
} else {
|
||||
// 更新现有配置
|
||||
if err := store.UpdateLLMToolRouter(record.ID, updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
record, err = store.GetLLMToolRouter()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "item": llmToolRouterDTO(*record)})
|
||||
}
|
||||
}
|
||||
|
||||
func llmToolRouterDTO(row llmToolRouterRecord) map[string]any {
|
||||
return map[string]any{
|
||||
"id": row.ID,
|
||||
"enabled": row.Enabled,
|
||||
"openai_name": row.OpenAIName,
|
||||
"timeout": row.Timeout,
|
||||
"max_tokens": row.MaxTokens,
|
||||
"system_prompt": row.SystemPrompt,
|
||||
"created_at": row.CreatedAt,
|
||||
"updated_at": row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +460,39 @@ func (textMessageRecord) TableName() string {
|
||||
return "text_message"
|
||||
}
|
||||
|
||||
// llmProviderRecord 保存 LLM API 配置,支持多个 AI 提供商
|
||||
type llmProviderRecord struct {
|
||||
Name string `gorm:"column:name;primaryKey;size:64;not null"` // 配置名称,如 "default"、"openai"、"ark" 等
|
||||
Active bool `gorm:"column:active;not null;index"` // 是否启用此配置
|
||||
APIKey string `gorm:"column:api_key;type:text;not null"` // API 密钥
|
||||
BaseURL string `gorm:"column:base_url;type:text;not null"` // API 基础 URL
|
||||
Model string `gorm:"column:model;not null"` // 模型名称
|
||||
Timeout int `gorm:"column:timeout;not null;default:120"` // 超时时间(秒)
|
||||
ContextWindowTokens int `gorm:"column:context_window_tokens;not null;default:262144"` // 上下文窗口 token 数
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;index"`
|
||||
}
|
||||
|
||||
func (llmProviderRecord) TableName() string {
|
||||
return "llm_providers"
|
||||
}
|
||||
|
||||
// llmToolRouterRecord 保存工具路由的配置
|
||||
type llmToolRouterRecord struct {
|
||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||
Enabled bool `gorm:"column:enabled;not null;index"` // 是否启用工具路由
|
||||
OpenAIName string `gorm:"column:openai_name;size:64;not null"` // 使用的 LLM 提供商名称(关联 llm_providers.name)
|
||||
Timeout int `gorm:"column:timeout;not null;default:30"` // 工具调用超时时间(秒)
|
||||
MaxTokens int `gorm:"column:max_tokens;not null;default:512"` // 工具调用最大 token 数
|
||||
SystemPrompt string `gorm:"column:system_prompt;type:text;not null"` // 系统提示词
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;index"`
|
||||
}
|
||||
|
||||
func (llmToolRouterRecord) TableName() string {
|
||||
return "llm_tool_router"
|
||||
}
|
||||
|
||||
type positionRecord struct {
|
||||
AppendPacketFields `gorm:"embedded"`
|
||||
MQTTClientRecordFields `gorm:"embedded"`
|
||||
@@ -591,6 +624,8 @@ func (s *store) migrate() error {
|
||||
{label: "bot_messages", model: &botMessageRecord{}},
|
||||
{label: "bot_direct_messages", model: &botDirectMessageRecord{}},
|
||||
{label: "llm_message_queue", model: &llmMessageQueueRecord{}},
|
||||
{label: "llm_providers", model: &llmProviderRecord{}},
|
||||
{label: "llm_tool_router", model: &llmToolRouterRecord{}},
|
||||
{label: "nodeinfo", model: &nodeInfoRecord{}},
|
||||
{label: "map_report", model: &mapReportRecord{}},
|
||||
{label: "text_message", model: &textMessageRecord{}},
|
||||
@@ -627,7 +662,17 @@ func (s *store) migrate() error {
|
||||
if err := migrateMapTileSourceHash(tx, migrator, s.driver); err != nil {
|
||||
return err
|
||||
}
|
||||
return (&store{db: tx, driver: s.driver}).EnsureDefaultMapTileSource()
|
||||
txStore := &store{db: tx, driver: s.driver}
|
||||
if err := txStore.EnsureDefaultMapTileSource(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := txStore.EnsureDefaultLLMProvider(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := txStore.EnsureDefaultLLMToolRouter(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+128
@@ -9,6 +9,134 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// LLM Provider (llm_providers) - 多 AI API 配置
|
||||
// ============================================
|
||||
|
||||
// ListLLMProviders 列出所有 LLM Provider
|
||||
func (s *store) ListLLMProviders(includeInactive bool) ([]llmProviderRecord, error) {
|
||||
var rows []llmProviderRecord
|
||||
query := s.db.Model(&llmProviderRecord{})
|
||||
if !includeInactive {
|
||||
query = query.Where("active = ?", true)
|
||||
}
|
||||
if err := query.Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||
return nil, fmt.Errorf("list llm providers: %w", err)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// GetLLMProvider 获取单个 LLM Provider
|
||||
func (s *store) GetLLMProvider(name string) (*llmProviderRecord, error) {
|
||||
var record llmProviderRecord
|
||||
if err := s.db.Where("name = ?", name).Take(&record).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("get llm provider %s: %w", name, err)
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// CreateLLMProvider 创建 LLM Provider
|
||||
func (s *store) CreateLLMProvider(record *llmProviderRecord) error {
|
||||
if err := s.db.Create(record).Error; err != nil {
|
||||
return fmt.Errorf("create llm provider %s: %w", record.Name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLLMProvider 更新 LLM Provider
|
||||
func (s *store) UpdateLLMProvider(name string, updates map[string]any) error {
|
||||
if err := s.db.Model(&llmProviderRecord{}).Where("name = ?", name).Updates(updates).Error; err != nil {
|
||||
return fmt.Errorf("update llm provider %s: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteLLMProvider 删除 LLM Provider
|
||||
func (s *store) DeleteLLMProvider(name string) error {
|
||||
if err := s.db.Where("name = ?", name).Delete(&llmProviderRecord{}).Error; err != nil {
|
||||
return fmt.Errorf("delete llm provider %s: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureDefaultLLMProvider 确保存在默认 LLM Provider 配置
|
||||
func (s *store) EnsureDefaultLLMProvider() error {
|
||||
_, err := s.GetLLMProvider("default")
|
||||
if err == nil {
|
||||
return nil // 已存在
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
// 创建默认配置
|
||||
defaultConfig := &llmProviderRecord{
|
||||
Name: "default",
|
||||
Active: true,
|
||||
APIKey: "",
|
||||
BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
||||
Model: "",
|
||||
Timeout: 120,
|
||||
ContextWindowTokens: 262144,
|
||||
}
|
||||
return s.CreateLLMProvider(defaultConfig)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LLM Tool Router (llm_tool_router) - 工具路由配置
|
||||
// ============================================
|
||||
|
||||
// GetLLMToolRouter 获取当前激活的 Tool Router 配置
|
||||
func (s *store) GetLLMToolRouter() (*llmToolRouterRecord, error) {
|
||||
var record llmToolRouterRecord
|
||||
// 默认取第一条记录(ID 最小的),因为通常只需要一个配置
|
||||
if err := s.db.Order("id ASC").First(&record).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("get llm tool router: %w", err)
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// CreateLLMToolRouter 创建 Tool Router 配置
|
||||
func (s *store) CreateLLMToolRouter(record *llmToolRouterRecord) error {
|
||||
if err := s.db.Create(record).Error; err != nil {
|
||||
return fmt.Errorf("create llm tool router: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLLMToolRouter 更新 Tool Router 配置
|
||||
func (s *store) UpdateLLMToolRouter(id uint64, updates map[string]any) error {
|
||||
if err := s.db.Model(&llmToolRouterRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
return fmt.Errorf("update llm tool router %d: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureDefaultLLMToolRouter 确保存在默认 Tool Router 配置
|
||||
func (s *store) EnsureDefaultLLMToolRouter() error {
|
||||
_, err := s.GetLLMToolRouter()
|
||||
if err == nil {
|
||||
return nil // 已存在
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
// 创建默认配置
|
||||
defaultConfig := &llmToolRouterRecord{
|
||||
Enabled: true,
|
||||
OpenAIName: "",
|
||||
Timeout: 30,
|
||||
MaxTokens: 512,
|
||||
SystemPrompt: "你可以按需直接调用可用工具来回答用户问题。\n每个工具的 description 描述了它的适用场景和调用条件。\n工具结果优先于模型内置知识;工具失败时必须如实说明,不要编造结果。\n只调用确实必要的工具。",
|
||||
}
|
||||
return s.CreateLLMToolRouter(defaultConfig)
|
||||
}
|
||||
|
||||
// LLMMessageQueueInput 是添加 LLM 队列消息的输入
|
||||
type LLMMessageQueueInput struct {
|
||||
BotID uint64 // 0 表示频道消息
|
||||
|
||||
@@ -6,6 +6,7 @@ import AdminBot from './components/AdminBot.vue'
|
||||
import AdminBotDirect from './components/AdminBotDirect.vue'
|
||||
import AdminDashboard from './components/AdminDashboard.vue'
|
||||
import AdminLLM from './components/AdminLLM.vue'
|
||||
import AdminLLMApi from './components/AdminLLMApi.vue'
|
||||
import AdminDiscardDetails from './components/AdminDiscardDetails.vue'
|
||||
import AdminHelpEdit from './components/AdminHelpEdit.vue'
|
||||
import AdminLogin from './components/AdminLogin.vue'
|
||||
@@ -31,6 +32,7 @@ const isMqttForwardAdminPage = adminPath === '/admin/mqtt_forward' || adminPath
|
||||
const isBotAdminPage = adminPath === '/admin/bot' || adminPath === '/admin/bot/'
|
||||
const isBotDirectAdminPage = adminPath === '/admin/bot/direct' || adminPath === '/admin/bot/direct/'
|
||||
const isLLMAdminPage = adminPath === '/admin/llm' || adminPath === '/admin/llm/'
|
||||
const isLLMApiAdminPage = adminPath === '/admin/llm/api' || adminPath === '/admin/llm/api/'
|
||||
const isSignAdminPage = adminPath === '/admin/sign' || adminPath === '/admin/sign/'
|
||||
const detailMatch = currentPath.match(/^\/detailed\/(.+)$/)
|
||||
const detailedNodeId = detailMatch ? decodeURIComponent(detailMatch[1]) : ''
|
||||
@@ -553,6 +555,7 @@ onBeforeUnmount(() => {
|
||||
<a href="/admin/bot" :class="{ active: isBotAdminPage }">机器人</a>
|
||||
<a href="/admin/bot/direct" :class="{ active: isBotDirectAdminPage }">机器人私聊</a>
|
||||
<a href="/admin/llm" :class="{ active: isLLMAdminPage }">LLM消息队列</a>
|
||||
<a href="/admin/llm/api" :class="{ active: isLLMApiAdminPage }">LLM API配置</a>
|
||||
<a href="/admin/sign" :class="{ active: isSignAdminPage }">签到管理</a>
|
||||
<a href="/admin/map_source" :class="{ active: adminPath === '/admin/map_source' }">地图图源</a>
|
||||
<a href="/admin/help_edit" :class="{ active: adminPath === '/admin/help_edit' }">帮助编辑</a>
|
||||
@@ -602,6 +605,7 @@ onBeforeUnmount(() => {
|
||||
<AdminBot v-else-if="isBotAdminPage" />
|
||||
<AdminBotDirect v-else-if="isBotDirectAdminPage" />
|
||||
<AdminLLM v-else-if="isLLMAdminPage" />
|
||||
<AdminLLMApi v-else-if="isLLMApiAdminPage" />
|
||||
<AdminSignManagement v-else-if="isSignAdminPage" />
|
||||
<AdminMapSource v-else-if="adminPath === '/admin/map_source'" />
|
||||
<AdminHelpEdit v-else-if="adminPath === '/admin/help_edit'" />
|
||||
|
||||
@@ -49,6 +49,11 @@ import type {
|
||||
BotDirectConversationsResponse,
|
||||
LLMMessage,
|
||||
LLMMessageStatus,
|
||||
LLMProvider,
|
||||
LLMProviderPayload,
|
||||
LLMProviderResponse,
|
||||
LLMPlatformRouterPayload,
|
||||
LLMPlatformRouterResponse,
|
||||
} from './types'
|
||||
|
||||
async function requestJSON<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
@@ -464,5 +469,40 @@ export function cleanupDeletedLLMMessages(days = 7): Promise<{ status: string; d
|
||||
return postJSON<{ status: string; deleted_count: number }>('/api/admin/llm/messages/cleanup', { days })
|
||||
}
|
||||
|
||||
// LLM Provider API
|
||||
export function getLLMProviders(includeInactive = false): Promise<ListResponse<LLMProvider>> {
|
||||
const params = new URLSearchParams()
|
||||
if (includeInactive) {
|
||||
params.set('include_inactive', 'true')
|
||||
}
|
||||
const query = params.toString() ? `?${params.toString()}` : ''
|
||||
return getJSON<ListResponse<LLMProvider>>(`/api/admin/llm/providers${query}`)
|
||||
}
|
||||
|
||||
export function getLLMProvider(name: string): Promise<LLMProviderResponse> {
|
||||
return getJSON<LLMProviderResponse>(`/api/admin/llm/providers/${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
export function createLLMProvider(payload: LLMProviderPayload): Promise<LLMProviderResponse> {
|
||||
return postJSON<LLMProviderResponse>('/api/admin/llm/providers', payload)
|
||||
}
|
||||
|
||||
export function updateLLMProvider(name: string, payload: Partial<LLMProviderPayload>): Promise<LLMProviderResponse> {
|
||||
return putJSON<LLMProviderResponse>(`/api/admin/llm/providers/${encodeURIComponent(name)}`, payload)
|
||||
}
|
||||
|
||||
export function deleteLLMProvider(name: string): Promise<{ status: string }> {
|
||||
return deleteJSON<{ status: string }>(`/api/admin/llm/providers/${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
// LLM Tool Router API
|
||||
export function getLLMToolRouter(): Promise<LLMPlatformRouterResponse> {
|
||||
return getJSON<LLMPlatformRouterResponse>('/api/admin/llm/tool-router')
|
||||
}
|
||||
|
||||
export function updateLLMToolRouter(payload: Partial<LLMPlatformRouterPayload>): Promise<LLMPlatformRouterResponse> {
|
||||
return putJSON<LLMPlatformRouterResponse>('/api/admin/llm/tool-router', payload)
|
||||
}
|
||||
|
||||
// 静默使用未导出类型,避免 TS6133(未使用的导入)。
|
||||
export type { LLMMessage, LLMMessageStatus } from './types'
|
||||
export type { LLMMessage, LLMMessageStatus, LLMProvider, LLMPlatformRouter } from './types'
|
||||
|
||||
@@ -0,0 +1,827 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import {
|
||||
createLLMProvider,
|
||||
deleteLLMProvider,
|
||||
getLLMProviders,
|
||||
getLLMToolRouter,
|
||||
updateLLMProvider,
|
||||
updateLLMToolRouter,
|
||||
} from '../api'
|
||||
import type { LLMPlatformRouter, LLMProvider } from '../types'
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const success = ref('')
|
||||
|
||||
// LLM Provider 相关
|
||||
const providers = ref<LLMProvider[]>([])
|
||||
const editingProvider = ref<LLMProvider | null>(null)
|
||||
const showProviderForm = ref(false)
|
||||
const isCreatingProvider = ref(false)
|
||||
|
||||
const providerForm = ref({
|
||||
name: '',
|
||||
active: true,
|
||||
api_key: '',
|
||||
base_url: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
model: '',
|
||||
timeout: 120,
|
||||
context_window_tokens: 262144,
|
||||
})
|
||||
|
||||
// Tool Router 相关
|
||||
const toolRouter = ref<LLMPlatformRouter | null>(null)
|
||||
const editingToolRouter = ref(false)
|
||||
|
||||
const toolRouterForm = ref({
|
||||
enabled: true,
|
||||
openai_name: '',
|
||||
timeout: 30,
|
||||
max_tokens: 512,
|
||||
system_prompt: '',
|
||||
})
|
||||
|
||||
const activeProviders = computed(() => providers.value.filter((p) => p.active))
|
||||
|
||||
function clearSuccess() {
|
||||
setTimeout(() => {
|
||||
success.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
async function loadProviders() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const response = await getLLMProviders(true)
|
||||
providers.value = response.items
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadToolRouter() {
|
||||
try {
|
||||
const response = await getLLMToolRouter()
|
||||
toolRouter.value = response.item
|
||||
} catch (err) {
|
||||
// 如果不存在,使用默认值
|
||||
console.warn('Tool router config not found, using defaults')
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateProvider() {
|
||||
isCreatingProvider.value = true
|
||||
providerForm.value = {
|
||||
name: '',
|
||||
active: true,
|
||||
api_key: '',
|
||||
base_url: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
model: '',
|
||||
timeout: 120,
|
||||
context_window_tokens: 262144,
|
||||
}
|
||||
showProviderForm.value = true
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
function openEditProvider(provider: LLMProvider) {
|
||||
isCreatingProvider.value = false
|
||||
providerForm.value = {
|
||||
name: provider.name,
|
||||
active: provider.active,
|
||||
api_key: provider.api_key || '',
|
||||
base_url: provider.base_url,
|
||||
model: provider.model,
|
||||
timeout: provider.timeout,
|
||||
context_window_tokens: provider.context_window_tokens,
|
||||
}
|
||||
editingProvider.value = provider
|
||||
showProviderForm.value = true
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
function closeProviderForm() {
|
||||
showProviderForm.value = false
|
||||
editingProvider.value = null
|
||||
}
|
||||
|
||||
async function saveProvider() {
|
||||
if (!providerForm.value.name.trim()) {
|
||||
error.value = '请输入配置名称'
|
||||
return
|
||||
}
|
||||
if (!providerForm.value.base_url.trim()) {
|
||||
error.value = '请输入 API 地址'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (isCreatingProvider.value) {
|
||||
await createLLMProvider(providerForm.value)
|
||||
success.value = '创建成功'
|
||||
} else if (editingProvider.value) {
|
||||
await updateLLMProvider(editingProvider.value.name, providerForm.value)
|
||||
success.value = '更新成功'
|
||||
}
|
||||
clearSuccess()
|
||||
closeProviderForm()
|
||||
await loadProviders()
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDeleteProvider(name: string) {
|
||||
if (!confirm(`确定要删除配置 "${name}" 吗?此操作不可撤销。`)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteLLMProvider(name)
|
||||
success.value = '删除成功'
|
||||
clearSuccess()
|
||||
await loadProviders()
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
}
|
||||
|
||||
function openEditToolRouter() {
|
||||
if (toolRouter.value) {
|
||||
toolRouterForm.value = {
|
||||
enabled: toolRouter.value.enabled,
|
||||
openai_name: toolRouter.value.openai_name,
|
||||
timeout: toolRouter.value.timeout,
|
||||
max_tokens: toolRouter.value.max_tokens,
|
||||
system_prompt: toolRouter.value.system_prompt,
|
||||
}
|
||||
}
|
||||
editingToolRouter.value = true
|
||||
}
|
||||
|
||||
function closeToolRouterForm() {
|
||||
editingToolRouter.value = false
|
||||
}
|
||||
|
||||
async function saveToolRouter() {
|
||||
try {
|
||||
await updateLLMToolRouter(toolRouterForm.value)
|
||||
success.value = '更新成功'
|
||||
clearSuccess()
|
||||
closeToolRouterForm()
|
||||
await loadToolRouter()
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProviders()
|
||||
loadToolRouter()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-llm-api">
|
||||
<h2>LLM API 配置管理</h2>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-if="success" class="success">{{ success }}</p>
|
||||
|
||||
<!-- LLM Provider 列表 -->
|
||||
<div class="admin-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h3>AI 提供商配置</h3>
|
||||
<p class="section-desc">管理多个 LLM API 提供商配置,支持不同模型和服务。</p>
|
||||
</div>
|
||||
<button class="admin-button" @click="openCreateProvider">+ 添加配置</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="admin-loading">加载中...</div>
|
||||
|
||||
<div v-else class="provider-grid">
|
||||
<div v-for="provider in providers" :key="provider.name" class="provider-card">
|
||||
<div class="provider-header">
|
||||
<div class="provider-name">
|
||||
<span class="status-badge" :class="{ active: provider.active, inactive: !provider.active }">
|
||||
{{ provider.active ? '启用' : '停用' }}
|
||||
</span>
|
||||
<strong>{{ provider.name }}</strong>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
<button class="admin-button admin-button-small" @click="openEditProvider(provider)">编辑</button>
|
||||
<button class="admin-button admin-button-small admin-button-danger" @click="confirmDeleteProvider(provider.name)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-details">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">API 地址</span>
|
||||
<span class="detail-value">{{ provider.base_url }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">模型</span>
|
||||
<span class="detail-value">{{ provider.model || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">超时</span>
|
||||
<span class="detail-value">{{ provider.timeout }} 秒</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">上下文窗口</span>
|
||||
<span class="detail-value">{{ provider.context_window_tokens.toLocaleString() }} tokens</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">API Key</span>
|
||||
<span class="detail-value masked">{{ provider.api_key ? '••••••••••' : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="providers.length === 0" class="empty-state">
|
||||
<p>暂无配置,点击上方按钮添加第一个 AI 提供商配置。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tool Router 配置 -->
|
||||
<div class="admin-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h3>工具路由配置</h3>
|
||||
<p class="section-desc">配置 LLM 工具调用的路由设置,用于实现函数调用功能。</p>
|
||||
</div>
|
||||
<button v-if="!editingToolRouter" class="admin-button" @click="openEditToolRouter">编辑配置</button>
|
||||
</div>
|
||||
|
||||
<div v-if="editingToolRouter" class="tool-router-form">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" v-model="toolRouterForm.enabled" />
|
||||
启用工具路由
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>使用的 AI 配置</label>
|
||||
<select v-model="toolRouterForm.openai_name" class="form-input">
|
||||
<option value="">请选择</option>
|
||||
<option v-for="p in activeProviders" :key="p.name" :value="p.name">{{ p.name }}</option>
|
||||
</select>
|
||||
<p class="form-hint">选择用于工具调用的 AI 提供商配置</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>超时时间(秒)</label>
|
||||
<input type="number" v-model.number="toolRouterForm.timeout" class="form-input" min="1" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>最大 Token 数</label>
|
||||
<input type="number" v-model.number="toolRouterForm.max_tokens" class="form-input" min="1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>系统提示词</label>
|
||||
<textarea v-model="toolRouterForm.system_prompt" class="form-textarea" rows="6"></textarea>
|
||||
<p class="form-hint">用于指导模型如何使用工具的系统提示词</p>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="admin-button admin-button-secondary" @click="closeToolRouterForm">取消</button>
|
||||
<button class="admin-button" @click="saveToolRouter">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="toolRouter" class="tool-router-display">
|
||||
<div class="router-status">
|
||||
<span class="status-badge" :class="{ active: toolRouter.enabled, inactive: !toolRouter.enabled }">
|
||||
{{ toolRouter.enabled ? '已启用' : '已停用' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="router-details">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">使用的 AI 配置</span>
|
||||
<span class="detail-value">{{ toolRouter.openai_name || '未设置' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">超时时间</span>
|
||||
<span class="detail-value">{{ toolRouter.timeout }} 秒</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">最大 Token 数</span>
|
||||
<span class="detail-value">{{ toolRouter.max_tokens }}</span>
|
||||
</div>
|
||||
<div class="detail-row full-width">
|
||||
<span class="detail-label">系统提示词</span>
|
||||
<pre class="detail-value system-prompt">{{ toolRouter.system_prompt }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<p>暂无工具路由配置,点击上方按钮进行配置。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Provider 表单弹窗 -->
|
||||
<div v-if="showProviderForm" class="modal-overlay" @click.self="closeProviderForm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>{{ isCreatingProvider ? '添加 AI 提供商配置' : '编辑 AI 提供商配置' }}</h3>
|
||||
<button class="modal-close" @click="closeProviderForm">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>配置名称 *</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="providerForm.name"
|
||||
class="form-input"
|
||||
:disabled="!isCreatingProvider"
|
||||
placeholder="例如:volcengine-ark"
|
||||
/>
|
||||
<p class="form-hint">唯一标识此配置的名称,创建后不可修改</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" v-model="providerForm.active" />
|
||||
启用此配置
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>API 地址 *</label>
|
||||
<input type="text" v-model="providerForm.base_url" class="form-input" placeholder="https://api.example.com/v1" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>API Key *</label>
|
||||
<input type="password" v-model="providerForm.api_key" class="form-input" placeholder="sk-..." />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>模型名称</label>
|
||||
<input type="text" v-model="providerForm.model" class="form-input" placeholder="例如:doubao-pro-32k" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>超时时间(秒)</label>
|
||||
<input type="number" v-model.number="providerForm.timeout" class="form-input" min="1" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>上下文窗口(tokens)</label>
|
||||
<input type="number" v-model.number="providerForm.context_window_tokens" class="form-input" min="1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="admin-button admin-button-secondary" @click="closeProviderForm">取消</button>
|
||||
<button class="admin-button" @click="saveProvider">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-llm-api {
|
||||
padding: 1.5rem;
|
||||
max-width: 100%;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.admin-llm-api h2 {
|
||||
margin: 0 0 2rem;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.admin-section {
|
||||
background: white;
|
||||
padding: 1.75rem;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 4px 6px rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.provider-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
background: linear-gradient(135deg, #fafbfc 0%, #f8fafc 100%);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.provider-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.provider-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.provider-name strong {
|
||||
font-size: 1.05rem;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.provider-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.provider-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detail-row.full-width {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.9rem;
|
||||
color: #334155;
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.detail-value.masked {
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
|
||||
color: #166534;
|
||||
border: 1px solid #86efac;
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
||||
color: #991b1b;
|
||||
border: 1px solid #fca5a5;
|
||||
}
|
||||
|
||||
.tool-router-form,
|
||||
.tool-router-display {
|
||||
padding: 1.25rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.router-status {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.router-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.router-details .detail-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.router-details .detail-row .detail-value {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.system-prompt {
|
||||
width: 100%;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: white;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group label input[type='checkbox'] {
|
||||
margin-right: 0.5rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea,
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: #334155;
|
||||
background: white;
|
||||
transition: all 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-textarea:focus,
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
background: #f1f5f9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #991b1b;
|
||||
padding: 1rem 1.25rem;
|
||||
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1.25rem;
|
||||
border: 1px solid #fca5a5;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #166534;
|
||||
padding: 1rem 1.25rem;
|
||||
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1.25rem;
|
||||
border: 1px solid #86efac;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.admin-loading {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 3rem 2rem;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
border: 2px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 550px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 1.5rem 1rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
padding: 0.25rem 0.5rem;
|
||||
line-height: 1;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: #f1f5f9;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 1rem 1.5rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.admin-button {
|
||||
padding: 0.6rem 1.25rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.admin-button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.admin-button:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.admin-button-small {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-button-secondary {
|
||||
background: linear-gradient(135deg, #64748b 0%, #475569 100%);
|
||||
box-shadow: 0 2px 4px rgba(100, 116, 139, 0.2);
|
||||
}
|
||||
|
||||
.admin-button-secondary:hover:not(:disabled) {
|
||||
box-shadow: 0 4px 12px rgba(100, 116, 139, 0.3);
|
||||
}
|
||||
|
||||
.admin-button-danger {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.admin-button-danger:hover:not(:disabled) {
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -617,3 +617,54 @@ export interface LLMMessageStatusPayload {
|
||||
status: LLMMessageStatus
|
||||
error?: string
|
||||
}
|
||||
|
||||
// LLM Provider 相关类型
|
||||
export interface LLMProvider {
|
||||
name: string
|
||||
active: boolean
|
||||
api_key?: string
|
||||
base_url: string
|
||||
model: string
|
||||
timeout: number
|
||||
context_window_tokens: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface LLMProviderPayload {
|
||||
name: string
|
||||
active: boolean
|
||||
api_key: string
|
||||
base_url: string
|
||||
model: string
|
||||
timeout: number
|
||||
context_window_tokens: number
|
||||
}
|
||||
|
||||
export interface LLMProviderResponse {
|
||||
item: LLMProvider
|
||||
}
|
||||
|
||||
// LLM Tool Router 相关类型
|
||||
export interface LLMPlatformRouter {
|
||||
id: number
|
||||
enabled: boolean
|
||||
openai_name: string
|
||||
timeout: number
|
||||
max_tokens: number
|
||||
system_prompt: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface LLMPlatformRouterPayload {
|
||||
enabled: boolean
|
||||
openai_name: string
|
||||
timeout: number
|
||||
max_tokens: number
|
||||
system_prompt: string
|
||||
}
|
||||
|
||||
export interface LLMPlatformRouterResponse {
|
||||
item: LLMPlatformRouter
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user