mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-20 04:11:56 +08:00
# Conflicts: # frontend/package.json.md5 # frontend/src/App.tsx # frontend/src/components/AIChatPanel.message-boundary.test.tsx # frontend/src/components/AIChatPanel.tsx # frontend/src/components/AISettingsModal.tsx # frontend/src/components/ConnectionModal.tsx # frontend/src/components/DataGrid.ddl.test.tsx # frontend/src/components/DataGrid.layout.test.tsx # frontend/src/components/DataGrid.tsx # frontend/src/components/DataGridColumnTitle.test.tsx # frontend/src/components/DataGridLegacyCellContextMenu.tsx # frontend/src/components/DataGridSecondaryActions.tsx # frontend/src/components/DataGridToolbarFrame.tsx # frontend/src/components/DataSyncModal.tsx # frontend/src/components/DataViewer.tsx # frontend/src/components/DefinitionViewer.tsx # frontend/src/components/DriverManagerModal.tsx # frontend/src/components/QueryEditor.external-sql-save.test.tsx # frontend/src/components/QueryEditor.tsx # frontend/src/components/RedisViewer.tsx # frontend/src/components/Sidebar.locate-toolbar.test.tsx # frontend/src/components/Sidebar.tsx # frontend/src/components/TabManager.hover.test.tsx # frontend/src/components/TabManager.tsx # frontend/src/components/TableDesigner.tsx # frontend/src/components/V2TableContextMenu.tsx # frontend/src/components/ai/AIChatHeader.tsx # frontend/src/components/ai/AIHistoryDrawer.tsx # frontend/src/main.tsx # frontend/src/store.ts # frontend/src/utils/aiComposerNotice.test.ts # frontend/src/utils/aiComposerNotice.ts # frontend/src/utils/connectionModalPresentation.ts # frontend/src/utils/driverImportGuidance.ts # frontend/src/utils/externalSqlTree.test.ts # frontend/src/utils/externalSqlTree.ts # frontend/src/utils/sqlDialect.ts # internal/ai/service/service.go
296 lines
9.4 KiB
Go
296 lines
9.4 KiB
Go
package aiservice
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"GoNavi-Wails/internal/ai"
|
|
"GoNavi-Wails/internal/dailysecret"
|
|
"GoNavi-Wails/internal/secretstore"
|
|
"GoNavi-Wails/shared/i18n"
|
|
)
|
|
|
|
const (
|
|
aiConfigSchemaVersion = 4
|
|
aiConfigFileName = "ai_config.json"
|
|
)
|
|
|
|
type aiConfig struct {
|
|
SchemaVersion int `json:"schemaVersion,omitempty"`
|
|
Providers []ai.ProviderConfig `json:"providers"`
|
|
ActiveProvider string `json:"activeProvider"`
|
|
SafetyLevel string `json:"safetyLevel"`
|
|
ContextLevel string `json:"contextLevel"`
|
|
UserPromptSettings ai.UserPromptSettings `json:"userPromptSettings,omitempty"`
|
|
MCPServers []ai.MCPServerConfig `json:"mcpServers,omitempty"`
|
|
Skills []ai.SkillConfig `json:"skills,omitempty"`
|
|
}
|
|
|
|
type ProviderConfigStoreSnapshot struct {
|
|
Providers []ai.ProviderConfig
|
|
ActiveProvider string
|
|
SafetyLevel ai.SQLPermissionLevel
|
|
ContextLevel ai.ContextLevel
|
|
UserPromptSettings ai.UserPromptSettings
|
|
MCPServers []ai.MCPServerConfig
|
|
Skills []ai.SkillConfig
|
|
}
|
|
|
|
type ProviderConfigStoreInspection struct {
|
|
Snapshot ProviderConfigStoreSnapshot
|
|
ProvidersNeedingMigration []string
|
|
}
|
|
|
|
type ProviderConfigStore struct {
|
|
configDir string
|
|
secretStore secretstore.SecretStore
|
|
dailySecrets *dailysecret.Store
|
|
localizer *i18n.Localizer
|
|
}
|
|
|
|
func NewProviderConfigStore(configDir string, store secretstore.SecretStore) *ProviderConfigStore {
|
|
return NewProviderConfigStoreWithLanguage(configDir, store, i18n.LanguageEnUS)
|
|
}
|
|
|
|
func NewProviderConfigStoreWithLanguage(configDir string, store secretstore.SecretStore, language i18n.Language) *ProviderConfigStore {
|
|
if strings.TrimSpace(configDir) == "" {
|
|
configDir = resolveConfigDir()
|
|
}
|
|
if store == nil {
|
|
store = secretstore.NewUnavailableStore("secret store unavailable")
|
|
}
|
|
return &ProviderConfigStore{
|
|
configDir: configDir,
|
|
secretStore: store,
|
|
dailySecrets: dailysecret.NewStore(configDir),
|
|
localizer: newServiceLocalizerForLanguage(language),
|
|
}
|
|
}
|
|
|
|
func newProviderConfigStore(configDir string, store secretstore.SecretStore) *ProviderConfigStore {
|
|
return NewProviderConfigStore(configDir, store)
|
|
}
|
|
|
|
func (s *ProviderConfigStore) configPath() string {
|
|
return filepath.Join(s.configDir, aiConfigFileName)
|
|
}
|
|
|
|
func (s *ProviderConfigStore) storeError(key string, params map[string]any, cause error) error {
|
|
return serviceErrorFromLocalizer(s.localizer, key, params, cause)
|
|
}
|
|
|
|
func (s *ProviderConfigStore) Load() (ProviderConfigStoreSnapshot, error) {
|
|
cfg, snapshot, err := s.readStoredSnapshot()
|
|
if err != nil {
|
|
return snapshot, err
|
|
}
|
|
|
|
shouldRewrite := cfg.SchemaVersion != aiConfigSchemaVersion
|
|
providers := make([]ai.ProviderConfig, 0, len(snapshot.Providers))
|
|
for _, providerConfig := range snapshot.Providers {
|
|
runtimeConfig, rewritten, loadErr := s.loadStoredProviderConfig(providerConfig)
|
|
if loadErr != nil {
|
|
return snapshot, s.storeError("ai_service.backend.error.provider_secret_load_failed", map[string]any{
|
|
"provider": providerConfig.ID,
|
|
}, loadErr)
|
|
}
|
|
if rewritten {
|
|
shouldRewrite = true
|
|
}
|
|
providers = append(providers, runtimeConfig)
|
|
}
|
|
if providers == nil {
|
|
providers = []ai.ProviderConfig{}
|
|
}
|
|
snapshot.Providers = providers
|
|
|
|
if shouldRewrite {
|
|
if err := s.Save(snapshot); err != nil {
|
|
return snapshot, s.storeError("ai_service.backend.error.config_rewrite_failed", nil, err)
|
|
}
|
|
}
|
|
|
|
return snapshot, nil
|
|
}
|
|
|
|
func (s *ProviderConfigStore) LoadRuntime() (ProviderConfigStoreSnapshot, error) {
|
|
return s.Load()
|
|
}
|
|
|
|
func (s *ProviderConfigStore) Inspect() (ProviderConfigStoreInspection, error) {
|
|
_, snapshot, err := s.readStoredSnapshot()
|
|
inspection := ProviderConfigStoreInspection{
|
|
Snapshot: snapshot,
|
|
ProvidersNeedingMigration: []string{},
|
|
}
|
|
if err != nil {
|
|
return inspection, err
|
|
}
|
|
|
|
for _, providerConfig := range snapshot.Providers {
|
|
if providerNeedsMigration(providerConfig) {
|
|
inspection.ProvidersNeedingMigration = append(inspection.ProvidersNeedingMigration, providerConfig.ID)
|
|
}
|
|
}
|
|
|
|
return inspection, nil
|
|
}
|
|
|
|
func (s *ProviderConfigStore) Save(snapshot ProviderConfigStoreSnapshot) error {
|
|
providers := make([]ai.ProviderConfig, 0, len(snapshot.Providers))
|
|
for _, providerConfig := range snapshot.Providers {
|
|
runtimeConfig := normalizeProviderConfig(providerConfig)
|
|
meta, bundle := splitProviderSecrets(runtimeConfig)
|
|
if bundle.hasAny() {
|
|
storedMeta, err := persistProviderSecretBundleWithLocalizer(s.dailySecrets, meta, bundle, s.localizer)
|
|
if err != nil {
|
|
return s.storeError("ai_service.backend.error.provider_secret_save_failed", nil, err)
|
|
}
|
|
meta = storedMeta
|
|
} else if meta.HasSecret {
|
|
resolved, _, err := s.loadStoredProviderConfig(meta)
|
|
if err != nil {
|
|
return s.storeError("ai_service.backend.error.provider_secret_save_failed", nil, err)
|
|
}
|
|
meta = providerMetadataView(resolved)
|
|
}
|
|
providers = append(providers, providerMetadataView(meta))
|
|
}
|
|
if providers == nil {
|
|
providers = []ai.ProviderConfig{}
|
|
}
|
|
|
|
cfg := aiConfig{
|
|
SchemaVersion: aiConfigSchemaVersion,
|
|
Providers: providers,
|
|
ActiveProvider: snapshot.ActiveProvider,
|
|
SafetyLevel: string(snapshot.SafetyLevel),
|
|
ContextLevel: string(snapshot.ContextLevel),
|
|
UserPromptSettings: snapshot.UserPromptSettings,
|
|
MCPServers: snapshot.MCPServers,
|
|
Skills: snapshot.Skills,
|
|
}
|
|
|
|
data, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
return s.storeError("ai_service.backend.error.config_serialize_failed", nil, err)
|
|
}
|
|
if err := os.MkdirAll(s.configDir, 0o755); err != nil {
|
|
return s.storeError("ai_service.backend.error.config_dir_create_failed", nil, err)
|
|
}
|
|
if err := os.WriteFile(s.configPath(), data, 0o644); err != nil {
|
|
return s.storeError("ai_service.backend.error.config_write_failed", nil, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ProviderConfigStore) readStoredSnapshot() (aiConfig, ProviderConfigStoreSnapshot, error) {
|
|
snapshot := ProviderConfigStoreSnapshot{
|
|
Providers: []ai.ProviderConfig{},
|
|
SafetyLevel: ai.PermissionReadOnly,
|
|
ContextLevel: ai.ContextSchemaOnly,
|
|
MCPServers: []ai.MCPServerConfig{},
|
|
Skills: []ai.SkillConfig{},
|
|
}
|
|
|
|
data, err := os.ReadFile(s.configPath())
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return aiConfig{}, snapshot, nil
|
|
}
|
|
return aiConfig{}, snapshot, s.storeError("ai_service.backend.error.config_read_failed", nil, err)
|
|
}
|
|
|
|
var cfg aiConfig
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return aiConfig{}, snapshot, s.storeError("ai_service.backend.error.config_load_failed", nil, err)
|
|
}
|
|
|
|
snapshot.ActiveProvider = cfg.ActiveProvider
|
|
switch ai.SQLPermissionLevel(cfg.SafetyLevel) {
|
|
case ai.PermissionReadOnly, ai.PermissionReadWrite, ai.PermissionFull:
|
|
snapshot.SafetyLevel = ai.SQLPermissionLevel(cfg.SafetyLevel)
|
|
}
|
|
switch ai.ContextLevel(cfg.ContextLevel) {
|
|
case ai.ContextSchemaOnly, ai.ContextWithSamples, ai.ContextWithResults:
|
|
snapshot.ContextLevel = ai.ContextLevel(cfg.ContextLevel)
|
|
}
|
|
snapshot.UserPromptSettings = cfg.UserPromptSettings
|
|
snapshot.MCPServers = append([]ai.MCPServerConfig(nil), cfg.MCPServers...)
|
|
snapshot.Skills = append([]ai.SkillConfig(nil), cfg.Skills...)
|
|
|
|
providers := make([]ai.ProviderConfig, 0, len(cfg.Providers))
|
|
for _, providerConfig := range cfg.Providers {
|
|
providers = append(providers, normalizeProviderConfig(providerConfig))
|
|
}
|
|
if providers == nil {
|
|
providers = []ai.ProviderConfig{}
|
|
}
|
|
snapshot.Providers = providers
|
|
|
|
return cfg, snapshot, nil
|
|
}
|
|
|
|
func (s *ProviderConfigStore) loadStoredProviderConfig(config ai.ProviderConfig) (ai.ProviderConfig, bool, error) {
|
|
meta, bundle := splitProviderSecrets(config)
|
|
if bundle.hasAny() {
|
|
storedMeta, err := persistProviderSecretBundleWithLocalizer(s.dailySecrets, meta, bundle, s.localizer)
|
|
if err != nil {
|
|
return meta, false, err
|
|
}
|
|
return mergeProviderSecrets(storedMeta, bundle), true, nil
|
|
}
|
|
|
|
if !meta.HasSecret {
|
|
return meta, false, nil
|
|
}
|
|
|
|
if stored, ok, err := s.dailySecrets.GetAIProvider(meta.ID); err != nil {
|
|
return meta, false, err
|
|
} else if ok {
|
|
rewritten := strings.TrimSpace(meta.SecretRef) != ""
|
|
meta.SecretRef = ""
|
|
return mergeProviderSecrets(meta, fromDailyProviderBundle(stored)), rewritten, nil
|
|
}
|
|
|
|
if !shouldReadLegacyProviderSecretStore() {
|
|
meta.HasSecret = false
|
|
meta.SecretRef = ""
|
|
return meta, true, nil
|
|
}
|
|
|
|
if strings.TrimSpace(meta.SecretRef) != "" {
|
|
resolved, err := resolveProviderConfigSecretsFromStoreWithLocalizer(s.secretStore, meta, s.localizer)
|
|
if err != nil {
|
|
if os.IsNotExist(err) || secretstore.IsUnavailable(err) {
|
|
meta.HasSecret = false
|
|
meta.SecretRef = ""
|
|
return meta, true, nil
|
|
}
|
|
return meta, false, err
|
|
}
|
|
_, migratedBundle := splitProviderSecrets(resolved)
|
|
storedMeta, err := persistProviderSecretBundleWithLocalizer(s.dailySecrets, meta, migratedBundle, s.localizer)
|
|
if err != nil {
|
|
return meta, false, err
|
|
}
|
|
return mergeProviderSecrets(storedMeta, migratedBundle), true, nil
|
|
}
|
|
|
|
meta.HasSecret = false
|
|
meta.SecretRef = ""
|
|
return meta, true, nil
|
|
}
|
|
|
|
func (s *ProviderConfigStore) loadRuntimeProviderConfig(config ai.ProviderConfig) (ai.ProviderConfig, error) {
|
|
runtimeConfig, _, err := s.loadStoredProviderConfig(config)
|
|
return runtimeConfig, err
|
|
}
|
|
|
|
func providerNeedsMigration(config ai.ProviderConfig) bool {
|
|
_, bundle := splitProviderSecrets(normalizeProviderConfig(config))
|
|
return bundle.hasAny() || strings.TrimSpace(config.SecretRef) != ""
|
|
}
|