Files
MyGoNavi/cmd/manualtestseed/main.go
2026-04-10 21:29:45 +08:00

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")
}