feat: 新增语音转文字(ASR)功能
- 后端:新增 /api/audio/transcribe 接口,调用 Ollama whisper 进行语音识别 - 前端:新增 AudioTranscribePage.vue 页面,支持 MP3/WAV/M4A/OGG/FLAC 等格式 - 注册路由、工具卡片、智能助手欢迎语更新 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Config 全局配置,从环境变量读取(带默认值)。
|
||||
// system_config 表中的值在运行时覆盖对应字段(见 service)。
|
||||
type Config struct {
|
||||
Port string
|
||||
DBPath string // SQLite 文件路径
|
||||
JWTSecret string
|
||||
JWTExpireMin int
|
||||
|
||||
LLMBaseURL string
|
||||
LLMAPIKey string
|
||||
LLMModel string
|
||||
EmbedModel string
|
||||
|
||||
KnowledgeServiceURL string
|
||||
KnowledgeIndexDir string
|
||||
|
||||
KBDataDir string
|
||||
KnowledgeSourceDir string
|
||||
|
||||
FileMaxDoc int64
|
||||
FileMaxVideo int64
|
||||
ChunkThreshold int64
|
||||
|
||||
LibreOfficeBin string
|
||||
PdftotextBin string
|
||||
|
||||
KingdeeBaseURL string
|
||||
KingdeeAccountID string
|
||||
KingdeeUsername string
|
||||
KingdeePassword string
|
||||
KingdeeLCID int
|
||||
KingdeeTimeoutSec int
|
||||
}
|
||||
|
||||
// Load 读取环境变量构造配置。缺失的用默认值(部署时通过 .env / systemd 注入)。
|
||||
func Load() *Config {
|
||||
baseDir := backendBaseDir()
|
||||
|
||||
return &Config{
|
||||
Port: getenv("PORT", "8080"),
|
||||
DBPath: getenv("DB_PATH", filepath.Join(baseDir, "data", "eai_agentplatform.db")),
|
||||
JWTSecret: getenv("JWT_SECRET", "change-this-to-a-strong-secret-in-production"),
|
||||
JWTExpireMin: getenvInt("JWT_EXPIRE_MINUTES", 480),
|
||||
|
||||
LLMBaseURL: getenv("LLM_BASE_URL", "http://127.0.0.1:11434/v1"),
|
||||
LLMAPIKey: getenv("LLM_API_KEY", "ollama"),
|
||||
LLMModel: getenv("LLM_MODEL", "qwen2.5:7b"),
|
||||
EmbedModel: getenv("EMBED_MODEL", "bge-m3"),
|
||||
|
||||
KnowledgeServiceURL: getenv("KNOWLEDGE_SERVICE_URL", "http://127.0.0.1:10233"),
|
||||
KnowledgeIndexDir: getenv("KNOWLEDGE_INDEX_DIR", filepath.Join(baseDir, "data", "faiss")),
|
||||
|
||||
KBDataDir: getenv("KB_DATA_DIR", filepath.Join(baseDir, "data", "kb_data")),
|
||||
KnowledgeSourceDir: getenv("KNOWLEDGE_SOURCE_DIR", filepath.Join(baseDir, "knowledge_source")),
|
||||
|
||||
FileMaxDoc: getenvInt64("FILE_MAX_SIZE_DOC", 209715200),
|
||||
FileMaxVideo: getenvInt64("FILE_MAX_SIZE_VIDEO", 2147483648),
|
||||
ChunkThreshold: getenvInt64("CHUNK_THRESHOLD", 104857600),
|
||||
|
||||
LibreOfficeBin: getenv("LIBREOFFICE_BIN", "libreoffice"),
|
||||
PdftotextBin: getenv("PDFTOTEXT_BIN", "pdftotext"),
|
||||
|
||||
KingdeeBaseURL: getenv("KINGDEE_BASE_URL", ""),
|
||||
KingdeeAccountID: getenv("KINGDEE_ACCOUNT_ID", ""),
|
||||
KingdeeUsername: getenv("KINGDEE_USERNAME", ""),
|
||||
KingdeePassword: getenv("KINGDEE_PASSWORD", ""),
|
||||
KingdeeLCID: getenvInt("KINGDEE_LCID", 2052),
|
||||
KingdeeTimeoutSec: getenvInt("KINGDEE_TIMEOUT_SEC", 15),
|
||||
}
|
||||
}
|
||||
|
||||
func backendBaseDir() string {
|
||||
if exe, err := os.Executable(); err == nil && exe != "" {
|
||||
return filepath.Clean(filepath.Join(filepath.Dir(exe), ".."))
|
||||
}
|
||||
if wd, err := os.Getwd(); err == nil && wd != "" {
|
||||
return wd
|
||||
}
|
||||
return "."
|
||||
}
|
||||
|
||||
func getenv(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getenvInt(key string, def int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getenvInt64(key string, def int64) int64 {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 核心类型
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// RouteInfo 单条路由定义(JSON 文件中的原始数据)
|
||||
type RouteInfo struct {
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// RouteConfig 运行时完整路由配置(合并 secrets 后)
|
||||
type RouteConfig struct {
|
||||
RouteID string
|
||||
Provider string
|
||||
Model string
|
||||
BaseURL string // 完整 base_url(已从 secrets 注入)
|
||||
Endpoint string // 相对路径(如 /chat/completions)
|
||||
FullURL string // BaseURL + Endpoint
|
||||
APIKey string
|
||||
MaxTokens int
|
||||
Temperature float64
|
||||
Category string // chat / embed / image
|
||||
Description string
|
||||
}
|
||||
|
||||
// AIConfig 顶层结构(支持分类路由)
|
||||
type AIConfig struct {
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
DefaultRoute string `json:"default_route"`
|
||||
DefaultEmbedRoute string `json:"default_embed_route"`
|
||||
AgentRoutes map[string]string `json:"agent_routes"`
|
||||
ChatRoutes map[string]RouteInfo `json:"chat_routes"`
|
||||
EmbedRoutes map[string]RouteInfo `json:"embed_routes"`
|
||||
ImageRoutes map[string]RouteInfo `json:"image_routes"`
|
||||
FallbackRoutes map[string][]string `json:"fallback_routes"`
|
||||
// 兼容旧版平铺 routes(若有则回退)
|
||||
Routes map[string]RouteInfo `json:"routes,omitempty"`
|
||||
}
|
||||
|
||||
// AISecrets AI 密钥文件结构(从 pj034 直接复制的格式)
|
||||
type AISecrets struct {
|
||||
VECTORENGINE_API_KEY string `json:"VECTORENGINE_API_KEY,omitempty"`
|
||||
OPENROUTER_API_KEY string `json:"OPENROUTER_API_KEY,omitempty"`
|
||||
VOLCES_API_KEY string `json:"VOLCES_API_KEY,omitempty"`
|
||||
LMUAI_API_KEY string `json:"LMUAI_API_KEY,omitempty"`
|
||||
ALIYUN_API_KEY string `json:"ALIYUN_API_KEY,omitempty"`
|
||||
DEEPSEEK_API_KEY string `json:"DEEPSEEK_API_KEY,omitempty"`
|
||||
REMOVE_BG_API_KEY string `json:"REMOVE_BG_API_KEY,omitempty"`
|
||||
ANTHROPIC_API_KEY string `json:"ANTHROPIC_API_KEY,omitempty"`
|
||||
OPENAI_API_KEY string `json:"OPENAI_API_KEY,omitempty"`
|
||||
BRAVE_SEARCH_API_KEY string `json:"BRAVE_SEARCH_API_KEY,omitempty"`
|
||||
}
|
||||
|
||||
// PlatformConfig 平台静态配置
|
||||
type PlatformConfig struct {
|
||||
Name string `json:"name"`
|
||||
ShortName string `json:"short_name"`
|
||||
Version string `json:"version"`
|
||||
BuildTime string `json:"build_time"`
|
||||
Company string `json:"company"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// PROVIDER_SECRET_KEY 映射(参考 pj034 ai_config.py)
|
||||
// provider 名称 → secrets JSON 中的字段名
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
var ProviderSecretKey = map[string]string{
|
||||
"vectorengine": "VECTORENGINE_API_KEY",
|
||||
"openrouter": "OPENROUTER_API_KEY",
|
||||
"volces": "VOLCES_API_KEY",
|
||||
"lmuai": "LMUAI_API_KEY",
|
||||
"aliyun": "ALIYUN_API_KEY",
|
||||
"pic_copilot": "ALIYUN_API_KEY",
|
||||
"deepseek": "DEEPSEEK_API_KEY",
|
||||
"remove_bg": "REMOVE_BG_API_KEY",
|
||||
"anthropic": "ANTHROPIC_API_KEY",
|
||||
"openai": "OPENAI_API_KEY",
|
||||
}
|
||||
|
||||
// ProviderDefaultBaseURL provider 默认 base_url(当 secrets 未提供时)
|
||||
var ProviderDefaultBaseURL = map[string]string{
|
||||
"ollama": "http://127.0.0.1:11434/v1",
|
||||
"openrouter": "https://openrouter.ai/api/v1",
|
||||
"openai": "https://api.openai.com/v1",
|
||||
"vectorengine": "https://api.vectorengine.ai/v1",
|
||||
"volces": "https://ark.cn-beijing.volces.com/api/v3",
|
||||
"lmuai": "https://api.lmuai.com/v1",
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 文件路径解析
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
cachedAICfg *AIConfig
|
||||
cachedSecrets *AISecrets
|
||||
cachedPlatform *PlatformConfig
|
||||
)
|
||||
|
||||
// configDir 寻找 config/ 目录
|
||||
func configDir() string {
|
||||
// 优先使用可执行文件所在后端目录下的 config/
|
||||
exe, _ := os.Executable()
|
||||
if exe != "" {
|
||||
exeDir := filepath.Dir(exe)
|
||||
candidates := []string{
|
||||
filepath.Join(exeDir, "config"),
|
||||
filepath.Join(exeDir, "..", "config"),
|
||||
}
|
||||
for _, d := range candidates {
|
||||
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
|
||||
return filepath.Clean(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 回退 CWD 下的 config/
|
||||
if fi, err := os.Stat("config"); err == nil && fi.IsDir() {
|
||||
return filepath.Clean("config")
|
||||
}
|
||||
// 再回退 deploy/
|
||||
if fi, err := os.Stat("deploy"); err == nil && fi.IsDir() {
|
||||
return filepath.Clean("deploy")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveConfigPath(name string) string {
|
||||
if d := configDir(); d != "" {
|
||||
return filepath.Join(d, name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func loadJSON(path string, dst any) error {
|
||||
p := resolveConfigPath(path)
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 %s 失败: %w", p, err)
|
||||
}
|
||||
if err := json.Unmarshal(data, dst); err != nil {
|
||||
return fmt.Errorf("解析 %s 失败: %w", p, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 公开加载函数
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
func LoadAIConfig(forceReload ...bool) (*AIConfig, error) {
|
||||
fr := len(forceReload) > 0 && forceReload[0]
|
||||
if !fr {
|
||||
mu.RLock()
|
||||
if cachedAICfg != nil {
|
||||
ptr := cachedAICfg
|
||||
mu.RUnlock()
|
||||
return ptr, nil
|
||||
}
|
||||
mu.RUnlock()
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if !fr && cachedAICfg != nil {
|
||||
return cachedAICfg, nil
|
||||
}
|
||||
|
||||
var cfg AIConfig
|
||||
if err := loadJSON("ai_config.json", &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cachedAICfg = &cfg
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func LoadAISecrets(forceReload ...bool) (*AISecrets, error) {
|
||||
fr := len(forceReload) > 0 && forceReload[0]
|
||||
if !fr {
|
||||
mu.RLock()
|
||||
if cachedSecrets != nil {
|
||||
ptr := cachedSecrets
|
||||
mu.RUnlock()
|
||||
return ptr, nil
|
||||
}
|
||||
mu.RUnlock()
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if !fr && cachedSecrets != nil {
|
||||
return cachedSecrets, nil
|
||||
}
|
||||
|
||||
var s AISecrets
|
||||
if err := loadJSON("ai_secrets.json", &s); err != nil {
|
||||
// 文件不存在时不报错,返回空结构
|
||||
return &AISecrets{}, nil
|
||||
}
|
||||
cachedSecrets = &s
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func LoadPlatformConfig(forceReload ...bool) (*PlatformConfig, error) {
|
||||
fr := len(forceReload) > 0 && forceReload[0]
|
||||
if !fr {
|
||||
mu.RLock()
|
||||
if cachedPlatform != nil {
|
||||
ptr := cachedPlatform
|
||||
mu.RUnlock()
|
||||
return ptr, nil
|
||||
}
|
||||
mu.RUnlock()
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if !fr && cachedPlatform != nil {
|
||||
return cachedPlatform, nil
|
||||
}
|
||||
|
||||
var p PlatformConfig
|
||||
if err := loadJSON("platform.json", &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cachedPlatform = &p
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// GetRoute — 核心路由解析(参考 pj034 get_route())
|
||||
// 输入 agent 名或 route_id,返回完整 RouteConfig(已注入 base_url / api_key)
|
||||
// 查找路径:agent_routes → 分类 routes(chats/embeds/images) → 兼容旧版 routes → default
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
func GetRoute(agentOrRouteID string) (*RouteConfig, error) {
|
||||
aiCfg, err := LoadAIConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AI 配置加载失败: %w", err)
|
||||
}
|
||||
|
||||
// 1) 先当 agent 名查
|
||||
routeID := agentOrRouteID
|
||||
if r, ok := aiCfg.AgentRoutes[agentOrRouteID]; ok {
|
||||
routeID = r
|
||||
}
|
||||
|
||||
// 2) 在分类路由中查找
|
||||
info, category, found := findRoute(aiCfg, routeID)
|
||||
if !found {
|
||||
// 3) 回退默认
|
||||
if aiCfg.DefaultRoute != "" {
|
||||
routeID = aiCfg.DefaultRoute
|
||||
info, category, found = findRoute(aiCfg, routeID)
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("路由 %q 未在 ai_config.json 中定义,且无有效默认路由", agentOrRouteID)
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 从 secrets 注入 base_url / api_key
|
||||
baseURL := ""
|
||||
apiKey := ""
|
||||
secrets, _ := LoadAISecrets()
|
||||
|
||||
// 先查 provider 对应的 secrets key
|
||||
if secretField, ok := ProviderSecretKey[info.Provider]; ok && secrets != nil {
|
||||
// 通过反射取 struct 字段值
|
||||
apiKey = getSecretByField(secrets, secretField)
|
||||
}
|
||||
|
||||
// base_url: 先从 ProviderDefaultBaseURL 取
|
||||
if url, ok := ProviderDefaultBaseURL[info.Provider]; ok {
|
||||
baseURL = url
|
||||
}
|
||||
|
||||
// 组装
|
||||
fullURL := strings.TrimRight(baseURL, "/") + info.Endpoint
|
||||
|
||||
rc := &RouteConfig{
|
||||
RouteID: routeID,
|
||||
Provider: info.Provider,
|
||||
Model: info.Model,
|
||||
BaseURL: baseURL,
|
||||
Endpoint: info.Endpoint,
|
||||
FullURL: fullURL,
|
||||
APIKey: apiKey,
|
||||
MaxTokens: info.MaxTokens,
|
||||
Temperature: info.Temperature,
|
||||
Category: category,
|
||||
Description: info.Description,
|
||||
}
|
||||
if rc.MaxTokens <= 0 {
|
||||
rc.MaxTokens = 2048
|
||||
}
|
||||
if rc.Temperature <= 0 {
|
||||
rc.Temperature = 0.7
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// findRoute 在 chat_routes / embed_routes / image_routes / 兼容 routes 中查找
|
||||
func findRoute(cfg *AIConfig, routeID string) (RouteInfo, string, bool) {
|
||||
if cfg.ChatRoutes != nil {
|
||||
if info, ok := cfg.ChatRoutes[routeID]; ok {
|
||||
return info, "chat", true
|
||||
}
|
||||
}
|
||||
if cfg.EmbedRoutes != nil {
|
||||
if info, ok := cfg.EmbedRoutes[routeID]; ok {
|
||||
return info, "embed", true
|
||||
}
|
||||
}
|
||||
if cfg.ImageRoutes != nil {
|
||||
if info, ok := cfg.ImageRoutes[routeID]; ok {
|
||||
return info, "image", true
|
||||
}
|
||||
}
|
||||
// 兼容旧版平铺 routes
|
||||
if cfg.Routes != nil {
|
||||
if info, ok := cfg.Routes[routeID]; ok {
|
||||
return info, info.Category, true
|
||||
}
|
||||
}
|
||||
return RouteInfo{}, "", false
|
||||
}
|
||||
|
||||
// getSecretByField 从 AISecrets 按字段名取值
|
||||
func getSecretByField(s *AISecrets, field string) string {
|
||||
switch field {
|
||||
case "VECTORENGINE_API_KEY":
|
||||
return s.VECTORENGINE_API_KEY
|
||||
case "OPENROUTER_API_KEY":
|
||||
return s.OPENROUTER_API_KEY
|
||||
case "VOLCES_API_KEY":
|
||||
return s.VOLCES_API_KEY
|
||||
case "LMUAI_API_KEY":
|
||||
return s.LMUAI_API_KEY
|
||||
case "ALIYUN_API_KEY":
|
||||
return s.ALIYUN_API_KEY
|
||||
case "DEEPSEEK_API_KEY":
|
||||
return s.DEEPSEEK_API_KEY
|
||||
case "REMOVE_BG_API_KEY":
|
||||
return s.REMOVE_BG_API_KEY
|
||||
case "ANTHROPIC_API_KEY":
|
||||
return s.ANTHROPIC_API_KEY
|
||||
case "OPENAI_API_KEY":
|
||||
return s.OPENAI_API_KEY
|
||||
case "BRAVE_SEARCH_API_KEY":
|
||||
return s.BRAVE_SEARCH_API_KEY
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 辅助函数
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// GetFallbackRoutes 获取回退路由链
|
||||
func GetFallbackRoutes(primaryRouteID string) ([]*RouteConfig, error) {
|
||||
aiCfg, err := LoadAIConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fallbackIDs := aiCfg.FallbackRoutes[primaryRouteID]
|
||||
if len(fallbackIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var result []*RouteConfig
|
||||
for _, fid := range fallbackIDs {
|
||||
r, err := GetRoute(fid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetAllRoutes 返回所有定义的路由(跨分类)
|
||||
func GetAllRoutes() ([]*RouteConfig, error) {
|
||||
aiCfg, err := LoadAIConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []*RouteConfig
|
||||
seen := make(map[string]bool)
|
||||
|
||||
addRoutes := func(routes map[string]RouteInfo, category string) {
|
||||
for rid := range routes {
|
||||
if seen[rid] {
|
||||
continue
|
||||
}
|
||||
seen[rid] = true
|
||||
r, err := GetRoute(rid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
r.Category = category
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
|
||||
addRoutes(aiCfg.ChatRoutes, "chat")
|
||||
addRoutes(aiCfg.EmbedRoutes, "embed")
|
||||
addRoutes(aiCfg.ImageRoutes, "image")
|
||||
// 兼容旧版
|
||||
if aiCfg.Routes != nil {
|
||||
for rid, info := range aiCfg.Routes {
|
||||
if seen[rid] {
|
||||
continue
|
||||
}
|
||||
seen[rid] = true
|
||||
cat := info.Category
|
||||
if cat == "" {
|
||||
cat = "general"
|
||||
}
|
||||
r, err := GetRoute(rid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
r.Category = cat
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetRoutesByCategory 按分类返回路由
|
||||
func GetRoutesByCategory(category string) ([]*RouteConfig, error) {
|
||||
aiCfg, err := LoadAIConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var routeMap map[string]RouteInfo
|
||||
switch category {
|
||||
case "chat":
|
||||
routeMap = aiCfg.ChatRoutes
|
||||
case "embed":
|
||||
routeMap = aiCfg.EmbedRoutes
|
||||
case "image":
|
||||
routeMap = aiCfg.ImageRoutes
|
||||
default:
|
||||
return nil, fmt.Errorf("未知路由分类: %s", category)
|
||||
}
|
||||
|
||||
var result []*RouteConfig
|
||||
for rid := range routeMap {
|
||||
r, err := GetRoute(rid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetProviderAPIKey 直接获取指定 provider 的 API key
|
||||
func GetProviderAPIKey(provider string) string {
|
||||
secrets, _ := LoadAISecrets()
|
||||
if secrets == nil {
|
||||
return ""
|
||||
}
|
||||
if field, ok := ProviderSecretKey[provider]; ok {
|
||||
return getSecretByField(secrets, field)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetProviderBaseURL 获取指定 provider 的默认 base_url
|
||||
func GetProviderBaseURL(provider string) string {
|
||||
if url, ok := ProviderDefaultBaseURL[provider]; ok {
|
||||
return url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// AI 配置管理(管理员:读/写/热重载/密钥状态)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// GetAIConfigRaw 读取 ai_config.json 原始内容(供前端 JSON 编辑器)
|
||||
func GetAIConfigRaw() ([]byte, error) {
|
||||
return os.ReadFile(resolveConfigPath("ai_config.json"))
|
||||
}
|
||||
|
||||
// SaveAIConfig 校验并原子写回 ai_config.json,随后清缓存热生效
|
||||
func SaveAIConfig(raw []byte) error {
|
||||
// 先校验合法性(能反序列化为 AIConfig 且有 version)
|
||||
var cfg AIConfig
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
return fmt.Errorf("ai_config.json 不是合法 JSON: %w", err)
|
||||
}
|
||||
if cfg.Version == "" {
|
||||
return fmt.Errorf("ai_config.json 缺少 version 字段")
|
||||
}
|
||||
|
||||
path := resolveConfigPath("ai_config.json")
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, raw, 0o644); err != nil {
|
||||
return fmt.Errorf("写入临时文件失败: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
return fmt.Errorf("替换配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
cachedAICfg = nil
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetCache 清空全部配置缓存(热重载,无需重启)
|
||||
func ResetCache() {
|
||||
mu.Lock()
|
||||
cachedAICfg = nil
|
||||
cachedSecrets = nil
|
||||
cachedPlatform = nil
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
// SecretsStatus 返回各 provider 密钥是否已配置(不回显明文)
|
||||
func SecretsStatus() map[string]bool {
|
||||
secrets, _ := LoadAISecrets()
|
||||
status := make(map[string]bool)
|
||||
if secrets == nil {
|
||||
return status
|
||||
}
|
||||
for provider, field := range ProviderSecretKey {
|
||||
status[provider] = strings.TrimSpace(getSecretByField(secrets, field)) != ""
|
||||
}
|
||||
return status
|
||||
}
|
||||
Reference in New Issue
Block a user