mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 00:33:28 +08:00
- 新增共享六语言词典、前端 i18n 运行时与语言设置入口 - 推进连接、驱动、数据网格、查询、AI、Redis、表设计等模块文案本地化 - 补充 raw 边界、SQL/驱动/更新场景测试与 i18n 扫描工具
272 lines
7.8 KiB
Go
272 lines
7.8 KiB
Go
package aiservice
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"GoNavi-Wails/internal/ai"
|
|
"GoNavi-Wails/internal/dailysecret"
|
|
"GoNavi-Wails/internal/secretstore"
|
|
"GoNavi-Wails/shared/i18n"
|
|
)
|
|
|
|
const providerSecretKind = "ai-provider"
|
|
|
|
type providerSecretBundle struct {
|
|
APIKey string `json:"apiKey,omitempty"`
|
|
SensitiveHeaders map[string]string `json:"sensitiveHeaders,omitempty"`
|
|
}
|
|
|
|
func (b providerSecretBundle) hasAny() bool {
|
|
return strings.TrimSpace(b.APIKey) != "" || len(b.SensitiveHeaders) > 0
|
|
}
|
|
|
|
func mergeProviderSecretBundles(base, overlay providerSecretBundle) providerSecretBundle {
|
|
merged := providerSecretBundle{
|
|
APIKey: base.APIKey,
|
|
SensitiveHeaders: cloneStringMap(base.SensitiveHeaders),
|
|
}
|
|
if strings.TrimSpace(overlay.APIKey) != "" {
|
|
merged.APIKey = overlay.APIKey
|
|
}
|
|
for key, value := range overlay.SensitiveHeaders {
|
|
if merged.SensitiveHeaders == nil {
|
|
merged.SensitiveHeaders = make(map[string]string, len(overlay.SensitiveHeaders))
|
|
}
|
|
merged.SensitiveHeaders[key] = value
|
|
}
|
|
if len(merged.SensitiveHeaders) == 0 {
|
|
merged.SensitiveHeaders = nil
|
|
}
|
|
return merged
|
|
}
|
|
|
|
func splitProviderSecrets(cfg ai.ProviderConfig) (ai.ProviderConfig, providerSecretBundle) {
|
|
meta := cfg
|
|
meta.APIKey = ""
|
|
|
|
bundle := providerSecretBundle{}
|
|
if apiKey := strings.TrimSpace(cfg.APIKey); apiKey != "" {
|
|
bundle.APIKey = apiKey
|
|
}
|
|
|
|
if len(cfg.Headers) > 0 {
|
|
safeHeaders := make(map[string]string, len(cfg.Headers))
|
|
sensitiveHeaders := make(map[string]string)
|
|
for key, value := range cfg.Headers {
|
|
if isSensitiveProviderHeader(key) {
|
|
if strings.TrimSpace(value) != "" {
|
|
sensitiveHeaders[key] = value
|
|
}
|
|
continue
|
|
}
|
|
safeHeaders[key] = value
|
|
}
|
|
if len(safeHeaders) > 0 {
|
|
meta.Headers = safeHeaders
|
|
} else {
|
|
meta.Headers = nil
|
|
}
|
|
if len(sensitiveHeaders) > 0 {
|
|
bundle.SensitiveHeaders = sensitiveHeaders
|
|
}
|
|
} else {
|
|
meta.Headers = nil
|
|
}
|
|
|
|
meta.HasSecret = cfg.HasSecret || bundle.hasAny()
|
|
meta.SecretRef = strings.TrimSpace(cfg.SecretRef)
|
|
if !meta.HasSecret {
|
|
meta.SecretRef = ""
|
|
}
|
|
|
|
return meta, bundle
|
|
}
|
|
|
|
func mergeProviderSecrets(cfg ai.ProviderConfig, bundle providerSecretBundle) ai.ProviderConfig {
|
|
merged := cfg
|
|
merged.APIKey = bundle.APIKey
|
|
|
|
headers := cloneStringMap(cfg.Headers)
|
|
if len(bundle.SensitiveHeaders) > 0 {
|
|
if headers == nil {
|
|
headers = make(map[string]string, len(bundle.SensitiveHeaders))
|
|
}
|
|
for key, value := range bundle.SensitiveHeaders {
|
|
headers[key] = value
|
|
}
|
|
}
|
|
if len(headers) > 0 {
|
|
merged.Headers = headers
|
|
} else {
|
|
merged.Headers = nil
|
|
}
|
|
|
|
merged.HasSecret = cfg.HasSecret || bundle.hasAny()
|
|
merged.SecretRef = ""
|
|
if !merged.HasSecret {
|
|
merged.SecretRef = ""
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
func toDailyProviderBundle(bundle providerSecretBundle) dailysecret.ProviderBundle {
|
|
return dailysecret.ProviderBundle{
|
|
APIKey: bundle.APIKey,
|
|
SensitiveHeaders: cloneStringMap(bundle.SensitiveHeaders),
|
|
}
|
|
}
|
|
|
|
func fromDailyProviderBundle(bundle dailysecret.ProviderBundle) providerSecretBundle {
|
|
return providerSecretBundle{
|
|
APIKey: bundle.APIKey,
|
|
SensitiveHeaders: cloneStringMap(bundle.SensitiveHeaders),
|
|
}
|
|
}
|
|
|
|
func persistProviderSecretBundle(store *dailysecret.Store, meta ai.ProviderConfig, bundle providerSecretBundle) (ai.ProviderConfig, error) {
|
|
return persistProviderSecretBundleWithLocalizer(store, meta, bundle, nil)
|
|
}
|
|
|
|
func persistProviderSecretBundleWithLocalizer(store *dailysecret.Store, meta ai.ProviderConfig, bundle providerSecretBundle, localizer *i18n.Localizer) (ai.ProviderConfig, error) {
|
|
meta, _ = splitProviderSecrets(meta)
|
|
if !bundle.hasAny() {
|
|
meta.HasSecret = false
|
|
meta.SecretRef = ""
|
|
if store == nil {
|
|
return meta, nil
|
|
}
|
|
return meta, store.DeleteAIProvider(meta.ID)
|
|
}
|
|
if store == nil {
|
|
return meta, serviceErrorFromLocalizer(localizer, "ai_service.backend.error.daily_secret_store_unavailable", nil, fmt.Errorf("daily secret store unavailable"))
|
|
}
|
|
if err := store.PutAIProvider(meta.ID, toDailyProviderBundle(bundle)); err != nil {
|
|
return meta, err
|
|
}
|
|
meta.SecretRef = ""
|
|
meta.HasSecret = true
|
|
return meta, nil
|
|
}
|
|
|
|
func resolveProviderConfigSecrets(store *dailysecret.Store, cfg ai.ProviderConfig) (ai.ProviderConfig, error) {
|
|
return resolveProviderConfigSecretsWithLocalizer(store, cfg, nil)
|
|
}
|
|
|
|
func resolveProviderConfigSecretsWithLocalizer(store *dailysecret.Store, cfg ai.ProviderConfig, localizer *i18n.Localizer) (ai.ProviderConfig, error) {
|
|
cfg = normalizeProviderConfig(cfg)
|
|
meta, bundle := splitProviderSecrets(cfg)
|
|
if bundle.hasAny() {
|
|
return mergeProviderSecrets(meta, bundle), nil
|
|
}
|
|
if !meta.HasSecret {
|
|
return meta, nil
|
|
}
|
|
if store == nil {
|
|
return meta, serviceErrorFromLocalizer(localizer, "ai_service.backend.error.daily_secret_store_unavailable", nil, fmt.Errorf("daily secret store unavailable"))
|
|
}
|
|
stored, ok, err := store.GetAIProvider(meta.ID)
|
|
if err != nil {
|
|
return meta, err
|
|
}
|
|
if !ok {
|
|
return meta, os.ErrNotExist
|
|
}
|
|
meta.SecretRef = ""
|
|
return mergeProviderSecrets(meta, fromDailyProviderBundle(stored)), nil
|
|
}
|
|
|
|
func (s *Service) persistProviderSecretBundle(meta ai.ProviderConfig, bundle providerSecretBundle) (ai.ProviderConfig, error) {
|
|
return persistProviderSecretBundleWithLocalizer(s.dailySecretStore(), meta, bundle, s.serviceLocalizerForLanguageLocked())
|
|
}
|
|
|
|
func (s *Service) resolveProviderConfigSecrets(cfg ai.ProviderConfig) (ai.ProviderConfig, error) {
|
|
return resolveProviderConfigSecretsWithLocalizer(s.dailySecretStore(), cfg, s.serviceLocalizerForLanguage())
|
|
}
|
|
|
|
func (s *Service) resolveProviderConfigSecretsLocked(cfg ai.ProviderConfig) (ai.ProviderConfig, error) {
|
|
return resolveProviderConfigSecretsWithLocalizer(s.dailySecretStore(), cfg, s.serviceLocalizerForLanguageLocked())
|
|
}
|
|
|
|
func resolveProviderConfigSecretsFromStore(store secretstore.SecretStore, cfg ai.ProviderConfig) (ai.ProviderConfig, error) {
|
|
return resolveProviderConfigSecretsFromStoreWithLocalizer(store, cfg, nil)
|
|
}
|
|
|
|
func resolveProviderConfigSecretsFromStoreWithLocalizer(store secretstore.SecretStore, cfg ai.ProviderConfig, localizer *i18n.Localizer) (ai.ProviderConfig, error) {
|
|
cfg = normalizeProviderConfig(cfg)
|
|
meta, bundle := splitProviderSecrets(cfg)
|
|
if bundle.hasAny() {
|
|
return mergeProviderSecrets(meta, bundle), nil
|
|
}
|
|
if !meta.HasSecret {
|
|
return meta, nil
|
|
}
|
|
if store == nil {
|
|
return meta, serviceErrorFromLocalizer(localizer, "ai_service.backend.error.secret_store_unavailable", nil, fmt.Errorf("secret store unavailable"))
|
|
}
|
|
|
|
ref := strings.TrimSpace(meta.SecretRef)
|
|
if ref == "" {
|
|
var err error
|
|
ref, err = secretstore.BuildRef(providerSecretKind, meta.ID)
|
|
if err != nil {
|
|
return meta, err
|
|
}
|
|
meta.SecretRef = ref
|
|
}
|
|
|
|
payload, err := store.Get(ref)
|
|
if err != nil {
|
|
return meta, err
|
|
}
|
|
|
|
var stored providerSecretBundle
|
|
if err := json.Unmarshal(payload, &stored); err != nil {
|
|
return meta, serviceErrorFromLocalizer(localizer, "ai_service.backend.error.provider_secret_bundle_parse_failed", nil, err)
|
|
}
|
|
return mergeProviderSecrets(meta, stored), nil
|
|
}
|
|
|
|
func providerMetadataView(cfg ai.ProviderConfig) ai.ProviderConfig {
|
|
meta, _ := splitProviderSecrets(normalizeProviderConfig(cfg))
|
|
return meta
|
|
}
|
|
|
|
func isSensitiveProviderHeader(name string) bool {
|
|
normalized := strings.TrimSpace(strings.ToLower(name))
|
|
switch normalized {
|
|
case "authorization", "proxy-authorization", "x-api-key", "api-key":
|
|
return true
|
|
}
|
|
|
|
for _, token := range providerHeaderTokens(normalized) {
|
|
switch token {
|
|
case "auth", "authorization", "token", "secret", "key", "apikey":
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func providerHeaderTokens(name string) []string {
|
|
return strings.FieldsFunc(name, func(r rune) bool {
|
|
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
|
|
})
|
|
}
|
|
|
|
func cloneStringMap(input map[string]string) map[string]string {
|
|
if len(input) == 0 {
|
|
return nil
|
|
}
|
|
cloned := make(map[string]string, len(input))
|
|
for key, value := range input {
|
|
cloned[key] = value
|
|
}
|
|
return cloned
|
|
}
|