feat: 添加注释
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
// Package service 负责管理用户的本地 AI 配置(API Key、模型选择等)。
|
||||
//
|
||||
// 所有的配置都持久化存储在 settings.db 中。
|
||||
// 这里的配置与 config.yaml 的职责有本质区别:
|
||||
// - config.yaml 是"应用的公共回退配置"(开发者提供);
|
||||
// - settings.db 是"用户自己配置的 API Key"(数据主权归用户)。
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"AI-Expert-Sidebar/internal/config"
|
||||
"AI-Expert-Sidebar/internal/crypto"
|
||||
@@ -9,7 +16,8 @@ import (
|
||||
"AI-Expert-Sidebar/internal/models"
|
||||
)
|
||||
|
||||
// SettingsDTO is what the frontend reads and writes.
|
||||
// SettingsDTO 是在前后端之间传递配置信息的"数据传输对象"。
|
||||
// 它将散落在 settings.db KV 表里的各条记录聚合为一个结构体,方便前端 React 渲染表单。
|
||||
type SettingsDTO struct {
|
||||
AIProvider string `json:"ai_provider"`
|
||||
BaseURL string `json:"base_url"`
|
||||
@@ -17,15 +25,19 @@ type SettingsDTO struct {
|
||||
Model string `json:"model"`
|
||||
SystemPrompt string `json:"system_prompt"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
UsePublicKey bool `json:"use_public_key"`
|
||||
// UsePublicKey: true 表示用户不想用自己的 Key,而是使用软件内置的 Key。
|
||||
UsePublicKey bool `json:"use_public_key"`
|
||||
}
|
||||
|
||||
// GetSettings reads AI config from settings.db key-value store.
|
||||
// GetSettings 从 settings.db 读取所有 AI 配置,并组装为 DTO 返回给前端。
|
||||
// 如果发现 api_key 是被加密过的,会在此处进行解密。
|
||||
func GetSettings() *SettingsDTO {
|
||||
sdb := database.GetSettings()
|
||||
if sdb == nil {
|
||||
return defaultDTO()
|
||||
}
|
||||
|
||||
// 将 KV 表一次性读取到 map 中
|
||||
var rows []models.AppSetting
|
||||
sdb.Find(&rows)
|
||||
m := make(map[string]string, len(rows))
|
||||
@@ -33,42 +45,57 @@ func GetSettings() *SettingsDTO {
|
||||
m[r.Key] = r.Value
|
||||
}
|
||||
|
||||
// 尝试解密 API Key,若尚未配置或密文损坏,则返回空字符串
|
||||
apiKey, _ := crypto.DecryptAPIKey(m["api_key_encrypted"])
|
||||
|
||||
maxTokens := 1024
|
||||
fmt.Sscanf(m["max_tokens"], "%d", &maxTokens)
|
||||
if maxTokens <= 0 {
|
||||
maxTokens = 1024
|
||||
if mt, err := strconv.Atoi(m["max_tokens"]); err == nil && mt > 0 {
|
||||
maxTokens = mt
|
||||
}
|
||||
|
||||
return &SettingsDTO{
|
||||
AIProvider: strOr(m["ai_provider"], "deepseek"),
|
||||
AIProvider: strOr(m["ai_provider"], "deepseek"), // 默认厂商为 deepseek
|
||||
BaseURL: m["base_url"],
|
||||
APIKey: apiKey,
|
||||
APIKey: apiKey, // 传递给前端的是明文
|
||||
Model: strOr(m["model"], "deepseek-chat"),
|
||||
SystemPrompt: m["system_prompt"],
|
||||
MaxTokens: maxTokens,
|
||||
UsePublicKey: m["use_public_key"] != "false",
|
||||
UsePublicKey: m["use_public_key"] != "false", // 默认 true
|
||||
}
|
||||
}
|
||||
|
||||
// SaveSettings persists AI config into settings.db.
|
||||
// SaveSettings 接收前端传来的 DTO 并持久化到 settings.db。
|
||||
//
|
||||
// # 数据安全
|
||||
// 为了保护用户的 API Key,APIKey 字段在入库前会被强制进行 AES-256 加密,
|
||||
// 所以数据库里只会写入 api_key_encrypted。
|
||||
func SaveSettings(dto SettingsDTO) error {
|
||||
sdb := database.GetSettings()
|
||||
if sdb == nil {
|
||||
return fmt.Errorf("settings DB not ready")
|
||||
}
|
||||
|
||||
// 定义 UPSERT 闭包:使用 GORM 的原生 SQL,如果 key 已存在则更新 value,不存在则插入
|
||||
upsert := func(k, v string) {
|
||||
sdb.Exec("INSERT INTO app_settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", k, v)
|
||||
}
|
||||
|
||||
upsert("ai_provider", dto.AIProvider)
|
||||
upsert("base_url", dto.BaseURL)
|
||||
upsert("model", dto.Model)
|
||||
upsert("system_prompt", dto.SystemPrompt)
|
||||
upsert("max_tokens", fmt.Sprintf("%d", dto.MaxTokens))
|
||||
|
||||
usePublic := "true"
|
||||
if !dto.UsePublicKey {
|
||||
usePublic = "false"
|
||||
}
|
||||
upsert("use_public_key", usePublic)
|
||||
|
||||
// 如果用户选择使用私有 Key,并且确实输入了 Key,则对其加密后存储。
|
||||
// 如果前端传来了空字符串(可能是用户想清空),这里不会主动覆盖旧密文,
|
||||
// 需要增强逻辑:本系统目前只有覆盖更新,不提供独立删除 Key 按钮,
|
||||
// 留空即代表不更新原本缓存的 Key 密文。
|
||||
if !dto.UsePublicKey && dto.APIKey != "" {
|
||||
enc, err := crypto.EncryptAPIKey(dto.APIKey)
|
||||
if err != nil {
|
||||
@@ -79,24 +106,39 @@ func SaveSettings(dto SettingsDTO) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveAIConfig returns the effective AI call config (local settings or global fallback).
|
||||
// ResolveAIConfig 是 AI 调用的前置拦截器,决定最终到底使用哪个 API Key 和地址。
|
||||
//
|
||||
// # 动态回退策略
|
||||
// 1. 如果用户勾选了 "使用公共线路" 或是没填过自己的 API Key:
|
||||
// 直接短路,返回 config.yaml 里打包的公共 Key 和端点;
|
||||
// 2. 如果用户提供了私有 Key,优先使用用户的 Key;
|
||||
// 3. 对 BaseURL 进行缺省补全:如果选择了特定厂商(如 deepseek)但没填 URL,
|
||||
// 代码会自动填充标准官方 URL,极大简化了用户的配置门槛。
|
||||
func ResolveAIConfig() AICallConfig {
|
||||
// 默认配置回滚:来源于 config.yaml
|
||||
base := AICallConfig{
|
||||
BaseURL: "https://api.deepseek.com/chat/completions",
|
||||
APIKey: config.Global.DeepSeek.APIKey,
|
||||
Model: config.Global.DeepSeek.Model,
|
||||
MaxTokens: config.Global.DeepSeek.MaxTokens,
|
||||
}
|
||||
|
||||
dto := GetSettings()
|
||||
if dto == nil {
|
||||
return base
|
||||
}
|
||||
|
||||
// 无论使用什么 Key,用户的自定义系统提示词均生效
|
||||
base.SystemPrompt = dto.SystemPrompt
|
||||
|
||||
// 若用户指定使用公共线路,或用户的私钥实际上为空,触发降级
|
||||
if dto.UsePublicKey || dto.APIKey == "" {
|
||||
return base
|
||||
}
|
||||
|
||||
providerURL := dto.BaseURL
|
||||
if providerURL == "" {
|
||||
// 常见厂商的默认 API 路由表补全
|
||||
switch dto.AIProvider {
|
||||
case "deepseek":
|
||||
providerURL = "https://api.deepseek.com/chat/completions"
|
||||
@@ -106,10 +148,13 @@ func ResolveAIConfig() AICallConfig {
|
||||
providerURL = "https://api.x.ai/v1/chat/completions"
|
||||
}
|
||||
}
|
||||
|
||||
maxTok := dto.MaxTokens
|
||||
if maxTok <= 0 {
|
||||
maxTok = 1024
|
||||
}
|
||||
|
||||
// 返回拼接好的终端可直接消费的 AI 配置
|
||||
return AICallConfig{
|
||||
BaseURL: strOr(providerURL, base.BaseURL),
|
||||
APIKey: dto.APIKey,
|
||||
@@ -119,6 +164,7 @@ func ResolveAIConfig() AICallConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// strOr 是一个简单的 fallback 辅助函数。
|
||||
func strOr(v, def string) string {
|
||||
if v == "" {
|
||||
return def
|
||||
|
||||
Reference in New Issue
Block a user