mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-11 17:19:45 +08:00
340 lines
9.0 KiB
Go
340 lines
9.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"GoNavi-Wails/internal/ai"
|
|
aiservice "GoNavi-Wails/internal/ai/service"
|
|
"GoNavi-Wails/internal/app"
|
|
"GoNavi-Wails/internal/connection"
|
|
"GoNavi-Wails/internal/secretstore"
|
|
)
|
|
|
|
const (
|
|
modeSeedSecureStorage = "seed-secure-storage"
|
|
modeSeedAIUpdate = "seed-ai-update"
|
|
)
|
|
|
|
const (
|
|
testConnectionID = "manualtest-postgres"
|
|
testSecureProviderID = "manualtest-secure-provider"
|
|
testPendingProviderID = "manualtest-pending-provider"
|
|
testBackupDirName = "manual-test-backups"
|
|
connectionsFileName = "connections.json"
|
|
globalProxyFileName = "global_proxy.json"
|
|
aiConfigFileName = "ai_config.json"
|
|
securityUpdateFileName = "config-security-update.json"
|
|
)
|
|
|
|
type backupManifest struct {
|
|
CreatedAt string `json:"createdAt"`
|
|
ConfigDir string `json:"configDir"`
|
|
Files []backupManifestFile `json:"files"`
|
|
}
|
|
|
|
type backupManifestFile struct {
|
|
RelativePath string `json:"relativePath"`
|
|
Existed bool `json:"existed"`
|
|
}
|
|
|
|
type storedAIConfig struct {
|
|
SchemaVersion int `json:"schemaVersion,omitempty"`
|
|
Providers []ai.ProviderConfig `json:"providers"`
|
|
ActiveProvider string `json:"activeProvider"`
|
|
SafetyLevel string `json:"safetyLevel"`
|
|
ContextLevel string `json:"contextLevel"`
|
|
}
|
|
|
|
func main() {
|
|
mode := flag.String("mode", modeSeedSecureStorage, "seed mode: seed-secure-storage | seed-ai-update")
|
|
flag.Parse()
|
|
|
|
configDir, err := resolveConfigDir()
|
|
if err != nil {
|
|
fatalf("resolve config dir failed: %v", err)
|
|
}
|
|
|
|
store := secretstore.NewKeyringStore()
|
|
if err := store.HealthCheck(); err != nil {
|
|
fatalf("secret store unavailable: %v", err)
|
|
}
|
|
|
|
backupDir, err := backupConfigFiles(configDir)
|
|
if err != nil {
|
|
fatalf("backup config files failed: %v", err)
|
|
}
|
|
|
|
switch strings.TrimSpace(*mode) {
|
|
case modeSeedSecureStorage:
|
|
if err := seedSecureStorage(configDir, store); err != nil {
|
|
fatalf("seed secure storage failed: %v", err)
|
|
}
|
|
fmt.Printf("mode=%s\nbackup=%s\nconnectionId=%s\nproviderId=%s\n", modeSeedSecureStorage, backupDir, testConnectionID, testSecureProviderID)
|
|
case modeSeedAIUpdate:
|
|
if err := seedAIUpdate(configDir, store); err != nil {
|
|
fatalf("seed ai update failed: %v", err)
|
|
}
|
|
fmt.Printf("mode=%s\nbackup=%s\npendingProviderId=%s\n", modeSeedAIUpdate, backupDir, testPendingProviderID)
|
|
default:
|
|
fatalf("unsupported mode: %s", *mode)
|
|
}
|
|
}
|
|
|
|
func fatalf(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func resolveConfigDir() (string, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(homeDir, ".gonavi"), nil
|
|
}
|
|
|
|
func backupConfigFiles(configDir string) (string, error) {
|
|
backupDir := filepath.Join(configDir, testBackupDirName, time.Now().Format("20060102-150405"))
|
|
files := []string{
|
|
connectionsFileName,
|
|
globalProxyFileName,
|
|
aiConfigFileName,
|
|
filepath.Join("migrations", securityUpdateFileName),
|
|
}
|
|
|
|
manifest := backupManifest{
|
|
CreatedAt: time.Now().Format(time.RFC3339),
|
|
ConfigDir: configDir,
|
|
Files: make([]backupManifestFile, 0, len(files)),
|
|
}
|
|
|
|
for _, relativePath := range files {
|
|
srcPath := filepath.Join(configDir, relativePath)
|
|
info, err := os.Stat(srcPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
manifest.Files = append(manifest.Files, backupManifestFile{
|
|
RelativePath: relativePath,
|
|
Existed: false,
|
|
})
|
|
continue
|
|
}
|
|
return "", err
|
|
}
|
|
if info.IsDir() {
|
|
continue
|
|
}
|
|
|
|
dstPath := filepath.Join(backupDir, relativePath)
|
|
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
data, err := os.ReadFile(srcPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.WriteFile(dstPath, data, 0o644); err != nil {
|
|
return "", err
|
|
}
|
|
manifest.Files = append(manifest.Files, backupManifestFile{
|
|
RelativePath: relativePath,
|
|
Existed: true,
|
|
})
|
|
}
|
|
|
|
if err := os.MkdirAll(backupDir, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
manifestData, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(backupDir, "manifest.json"), manifestData, 0o644); err != nil {
|
|
return "", err
|
|
}
|
|
return backupDir, nil
|
|
}
|
|
|
|
func seedSecureStorage(configDir string, store secretstore.SecretStore) error {
|
|
if err := cleanupKnownTestSecrets(store); err != nil {
|
|
return err
|
|
}
|
|
|
|
appService := app.NewAppWithSecretStore(store)
|
|
_ = appService.DeleteConnection(testConnectionID)
|
|
|
|
if _, err := appService.SaveConnection(connection.SavedConnectionInput{
|
|
ID: testConnectionID,
|
|
Name: "手工测试 PostgreSQL",
|
|
Config: connection.ConnectionConfig{
|
|
ID: testConnectionID,
|
|
Type: "postgres",
|
|
Host: "127.0.0.1",
|
|
Port: 5432,
|
|
User: "postgres",
|
|
Password: "manualtest-pg-secret",
|
|
Database: "postgres",
|
|
},
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := appService.SaveGlobalProxy(connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "http",
|
|
Host: "127.0.0.1",
|
|
Port: 7890,
|
|
User: "manual-test",
|
|
Password: "manualtest-proxy-secret",
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
storeConfig := aiservice.NewProviderConfigStore(configDir, store)
|
|
snapshot, err := storeConfig.LoadRuntime()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
snapshot.Providers = filterProviders(snapshot.Providers, testSecureProviderID, testPendingProviderID)
|
|
snapshot.Providers = append(snapshot.Providers, ai.ProviderConfig{
|
|
ID: testSecureProviderID,
|
|
Type: "custom",
|
|
Name: "手工测试 Secure Provider",
|
|
APIKey: "manualtest-ai-secret",
|
|
BaseURL: "https://api.openai.com/v1",
|
|
Model: "gpt-4o-mini",
|
|
APIFormat: "openai",
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer manualtest-header-secret",
|
|
"X-Trace-Id": "manualtest-visible",
|
|
},
|
|
MaxTokens: 2048,
|
|
Temperature: 0.2,
|
|
})
|
|
if snapshot.SafetyLevel == "" {
|
|
snapshot.SafetyLevel = ai.PermissionReadOnly
|
|
}
|
|
if snapshot.ContextLevel == "" {
|
|
snapshot.ContextLevel = ai.ContextSchemaOnly
|
|
}
|
|
return storeConfig.Save(snapshot)
|
|
}
|
|
|
|
func seedAIUpdate(configDir string, store secretstore.SecretStore) error {
|
|
if err := cleanupKnownTestSecrets(store); err != nil {
|
|
return err
|
|
}
|
|
|
|
configPath := filepath.Join(configDir, aiConfigFileName)
|
|
cfg, err := readStoredAIConfig(configPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg.Providers = filterProviders(cfg.Providers, testSecureProviderID, testPendingProviderID)
|
|
cfg.Providers = append(cfg.Providers, ai.ProviderConfig{
|
|
ID: testPendingProviderID,
|
|
Type: "custom",
|
|
Name: "手工测试 待迁移 AI",
|
|
APIKey: "manualtest-ai-update-secret",
|
|
BaseURL: "https://api.openai.com/v1",
|
|
Model: "gpt-4o-mini",
|
|
APIFormat: "openai",
|
|
MaxTokens: 1024,
|
|
})
|
|
if cfg.SchemaVersion == 0 {
|
|
cfg.SchemaVersion = 2
|
|
}
|
|
if cfg.Providers == nil {
|
|
cfg.Providers = []ai.ProviderConfig{}
|
|
}
|
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
data, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(configPath, data, 0o644)
|
|
}
|
|
|
|
func readStoredAIConfig(configPath string) (storedAIConfig, error) {
|
|
cfg := storedAIConfig{
|
|
Providers: []ai.ProviderConfig{},
|
|
SafetyLevel: string(ai.PermissionReadOnly),
|
|
ContextLevel: string(ai.ContextSchemaOnly),
|
|
SchemaVersion: 2,
|
|
ActiveProvider: "",
|
|
}
|
|
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return cfg, nil
|
|
}
|
|
return storedAIConfig{}, err
|
|
}
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return storedAIConfig{}, err
|
|
}
|
|
if cfg.Providers == nil {
|
|
cfg.Providers = []ai.ProviderConfig{}
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func filterProviders(providers []ai.ProviderConfig, excludedIDs ...string) []ai.ProviderConfig {
|
|
excluded := make(map[string]struct{}, len(excludedIDs))
|
|
for _, id := range excludedIDs {
|
|
excluded[strings.TrimSpace(id)] = struct{}{}
|
|
}
|
|
filtered := make([]ai.ProviderConfig, 0, len(providers))
|
|
for _, provider := range providers {
|
|
if _, skip := excluded[strings.TrimSpace(provider.ID)]; skip {
|
|
continue
|
|
}
|
|
filtered = append(filtered, provider)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func cleanupKnownTestSecrets(store secretstore.SecretStore) error {
|
|
type secretRef struct {
|
|
kind string
|
|
id string
|
|
}
|
|
refs := []secretRef{
|
|
{kind: "connection", id: testConnectionID},
|
|
{kind: "global-proxy", id: "default"},
|
|
{kind: "ai-provider", id: testSecureProviderID},
|
|
{kind: "ai-provider", id: testPendingProviderID},
|
|
}
|
|
|
|
for _, item := range refs {
|
|
ref, err := secretstore.BuildRef(item.kind, item.id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := store.Delete(ref); err != nil && !isIgnorableDeleteError(err) {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isIgnorableDeleteError(err error) bool {
|
|
if err == nil || os.IsNotExist(err) {
|
|
return true
|
|
}
|
|
message := strings.ToLower(strings.TrimSpace(err.Error()))
|
|
return strings.Contains(message, "could not be found") ||
|
|
strings.Contains(message, "not be found in the keyring") ||
|
|
strings.Contains(message, "element not found")
|
|
}
|