diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 848588e..fa8a8b2 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -26a843d5fd071d0c7e9d8022e98eb4e3 \ No newline at end of file +571d014306268cf67665967059cda912 \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8ac5720..131d55d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1702,7 +1702,7 @@ function App() { const importKind = detectConnectionImportKind(raw); if (importKind === 'invalid') { - void message.error('文件格式错误:仅支持 GoNavi 恢复包或历史 JSON 连接数组'); + void message.error('文件格式错误:仅支持 GoNavi 恢复包、历史 JSON 连接数组或 MySQL Workbench XML'); return; } diff --git a/frontend/src/utils/connectionExport.ts b/frontend/src/utils/connectionExport.ts index 22de9eb..2a790c3 100644 --- a/frontend/src/utils/connectionExport.ts +++ b/frontend/src/utils/connectionExport.ts @@ -1,6 +1,6 @@ import type { ConnectionConfig, SavedConnection } from '../types'; -export type ConnectionImportKind = 'app-managed-package' | 'encrypted-package' | 'legacy-json' | 'invalid'; +export type ConnectionImportKind = 'app-managed-package' | 'encrypted-package' | 'legacy-json' | 'mysql-workbench-xml' | 'invalid'; export type ConnectionPackageDialogSnapshot = { open: boolean; mode: 'export' | 'import'; @@ -105,7 +105,15 @@ const parseConnectionImportRaw = (raw: unknown): unknown => { } }; +const isMySQLWorkbenchXML = (raw: string): boolean => ( + raw.includes(' { + if (typeof raw === 'string' && isMySQLWorkbenchXML(raw)) { + return 'mysql-workbench-xml'; + } + const parsed = parseConnectionImportRaw(raw); if (isConnectionPackageV2AppManagedEnvelope(parsed)) { diff --git a/frontend/src/utils/connectionModalPresentation.test.ts b/frontend/src/utils/connectionModalPresentation.test.ts index cca5dd9..3cedede 100644 --- a/frontend/src/utils/connectionModalPresentation.test.ts +++ b/frontend/src/utils/connectionModalPresentation.test.ts @@ -25,7 +25,7 @@ describe('connectionModalPresentation', () => { it('maps missing saved-connection errors to a secret-specific hint', () => { expect(normalizeConnectionSecretErrorMessage('saved connection not found: conn-1')).toBe( - '未找到当前连接对应的已保存密文,请重新填写密码并保存后再试', + '未找到当前连接对应的已保存密文,请编辑当前连接,并输入密码后保存', ); }); @@ -39,7 +39,7 @@ describe('connectionModalPresentation', () => { reason: 'saved connection not found: conn-1', fallback: '连接失败', })).toEqual({ - message: '测试失败: 未找到当前连接对应的已保存密文,请重新填写密码并保存后再试', + message: '测试失败: 未找到当前连接对应的已保存密文,请编辑当前连接,并输入密码后保存', shouldToast: true, }); }); diff --git a/frontend/src/utils/connectionModalPresentation.ts b/frontend/src/utils/connectionModalPresentation.ts index fbd841a..b507c68 100644 --- a/frontend/src/utils/connectionModalPresentation.ts +++ b/frontend/src/utils/connectionModalPresentation.ts @@ -41,7 +41,7 @@ export const normalizeConnectionSecretErrorMessage = ( const lower = text.toLowerCase(); if (lower.includes('saved connection not found:')) { - return '未找到当前连接对应的已保存密文,请重新填写密码并保存后再试'; + return '未找到当前连接对应的已保存密文,请编辑当前连接,并输入密码后保存'; } if (lower.includes('secret store unavailable')) { return '系统密文存储当前不可用,请检查系统钥匙串或凭据管理器后再试'; diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts index 4445dac..3bbea84 100644 --- a/frontend/wailsjs/runtime/runtime.d.ts +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -246,4 +246,85 @@ export function OnFileDropOff() :void export function CanResolveFilePaths(): boolean; // Resolves file paths for an array of files -export function ResolveFilePaths(files: File[]): void \ No newline at end of file +export function ResolveFilePaths(files: File[]): void + +// Notification types +export interface NotificationOptions { + id: string; + title: string; + subtitle?: string; // macOS and Linux only + body?: string; + categoryId?: string; + data?: { [key: string]: any }; +} + +export interface NotificationAction { + id?: string; + title?: string; + destructive?: boolean; // macOS-specific +} + +export interface NotificationCategory { + id?: string; + actions?: NotificationAction[]; + hasReplyField?: boolean; + replyPlaceholder?: string; + replyButtonTitle?: string; +} + +// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications) +// Initializes the notification service for the application. +// This must be called before sending any notifications. +export function InitializeNotifications(): Promise; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise; + +// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions) +// Sends a notification with action buttons. Requires a registered category. +export function SendNotificationWithActions(options: NotificationOptions): Promise; + +// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory) +// Registers a notification category that can be used with SendNotificationWithActions. +export function RegisterNotificationCategory(category: NotificationCategory): Promise; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise; + +// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification) +// Removes a notification by its identifier (cross-platform convenience function). +export function RemoveNotification(identifier: string): Promise; \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js index 7cb89d7..556621e 100644 --- a/frontend/wailsjs/runtime/runtime.js +++ b/frontend/wailsjs/runtime/runtime.js @@ -239,4 +239,60 @@ export function CanResolveFilePaths() { export function ResolveFilePaths(files) { return window.runtime.ResolveFilePaths(files); +} + +export function InitializeNotifications() { + return window.runtime.InitializeNotifications(); +} + +export function CleanupNotifications() { + return window.runtime.CleanupNotifications(); +} + +export function IsNotificationAvailable() { + return window.runtime.IsNotificationAvailable(); +} + +export function RequestNotificationAuthorization() { + return window.runtime.RequestNotificationAuthorization(); +} + +export function CheckNotificationAuthorization() { + return window.runtime.CheckNotificationAuthorization(); +} + +export function SendNotification(options) { + return window.runtime.SendNotification(options); +} + +export function SendNotificationWithActions(options) { + return window.runtime.SendNotificationWithActions(options); +} + +export function RegisterNotificationCategory(category) { + return window.runtime.RegisterNotificationCategory(category); +} + +export function RemoveNotificationCategory(categoryId) { + return window.runtime.RemoveNotificationCategory(categoryId); +} + +export function RemoveAllPendingNotifications() { + return window.runtime.RemoveAllPendingNotifications(); +} + +export function RemovePendingNotification(identifier) { + return window.runtime.RemovePendingNotification(identifier); +} + +export function RemoveAllDeliveredNotifications() { + return window.runtime.RemoveAllDeliveredNotifications(); +} + +export function RemoveDeliveredNotification(identifier) { + return window.runtime.RemoveDeliveredNotification(identifier); +} + +export function RemoveNotification(identifier) { + return window.runtime.RemoveNotification(identifier); } \ No newline at end of file diff --git a/go.mod b/go.mod index 29edc5a..f195fd3 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/redis/go-redis/v9 v9.17.3 github.com/sijms/go-ora/v2 v2.9.0 github.com/taosdata/driver-go/v3 v3.7.8 - github.com/wailsapp/wails/v2 v2.11.0 + github.com/wailsapp/wails/v2 v2.12.0 github.com/xuri/excelize/v2 v2.10.0 go.mongodb.org/mongo-driver v1.17.9 go.mongodb.org/mongo-driver/v2 v2.5.0 @@ -27,6 +27,7 @@ require ( ) require ( + git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index a93b158..cbf67a4 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= gitea.com/kingbase/gokb v0.0.0-20201021123113-29bd62a876c3 h1:QjslQNaH5Nuap5i4nijS0OYV6GMk5kqrAmgU90zBKd4= gitea.com/kingbase/gokb v0.0.0-20201021123113-29bd62a876c3/go.mod h1:7lH5A1jzCXD9Nl16DzaBUOfDAT8NPrDmZwKu1p5wf94= gitee.com/chunanyong/dm v1.8.22 h1:H7fsrnUIvEA0jlDWew7vwELry1ff+tLMIu2Fk2cIBSg= @@ -243,8 +245,8 @@ github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6N github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= -github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= +github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c= +github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= diff --git a/internal/app/connection_package_transfer.go b/internal/app/connection_package_transfer.go index 514fa17..18a07d0 100644 --- a/internal/app/connection_package_transfer.go +++ b/internal/app/connection_package_transfer.go @@ -2,8 +2,10 @@ package app import ( "encoding/json" + "encoding/xml" "errors" "fmt" + "strconv" "strings" "time" @@ -268,6 +270,21 @@ func (a *App) ImportConnectionsPayload(raw string, password string) ([]connectio return sanitizeSavedConnectionViews(views), nil } + if isMySQLWorkbenchXML(trimmed) { + inputs, err := parseMySQLWorkbenchXML(trimmed) + if err != nil { + return nil, fmt.Errorf("解析 MySQL Workbench XML 失败: %w", err) + } + if len(inputs) == 0 { + return nil, fmt.Errorf("未在 XML 中找到有效的连接配置") + } + views, err := a.importSavedConnectionsAtomically(inputs) + if err != nil { + return nil, err + } + return sanitizeSavedConnectionViews(views), nil + } + var legacy []connection.LegacySavedConnection if err := json.Unmarshal([]byte(trimmed), &legacy); err != nil { return nil, errConnectionPackageUnsupported @@ -372,3 +389,126 @@ func (s connectionPackageImportRollbackSnapshot) restore(a *App) error { } return nil } + +// --- MySQL Workbench XML import --- + +func isMySQLWorkbenchXML(content string) bool { + return strings.Contains(content, " 0 { + port = p + } + user := params["userName"] + schema := params["schema"] + password := params["password"] + + useSSL := false + if v, err := strconv.Atoi(params["useSSL"]); err == nil && v > 0 { + useSSL = true + } + + dbType := "mysql" + if strings.Contains(driverKey, "mariadb") { + dbType = "mariadb" + } + + connID := "conn-" + uuid.New().String()[:8] + + config := connection.ConnectionConfig{ + ID: connID, + Type: dbType, + Host: host, + Port: port, + User: user, + Password: password, + Database: schema, + UseSSL: useSSL, + } + + if connName == "" { + connName = fmt.Sprintf("%s@%s:%d", user, host, port) + } + + return connection.SavedConnectionInput{ + ID: connID, + Name: connName, + Config: config, + } +} diff --git a/internal/app/connection_secret_resolution.go b/internal/app/connection_secret_resolution.go index 8f02900..e1c7981 100644 --- a/internal/app/connection_secret_resolution.go +++ b/internal/app/connection_secret_resolution.go @@ -37,6 +37,14 @@ func (a *App) resolveConnectionSecrets(config connection.ConnectionConfig) (conn } resolved := mergeConnectionSecretBundleIntoConfig(base, bundle) resolved.ID = view.ID + + if !connectionConfigCarriesInlineSecrets(config) && !bundle.hasAny() { + _, dailyExists, _ := repo.dailySecrets().GetConnection(view.ID) + if !dailyExists { + return resolved, fmt.Errorf("未找到当前连接对应的已保存密文,请编辑当前连接,并输入密码后保存") + } + } + return resolved, nil } @@ -102,9 +110,9 @@ func normalizeConnectionSecretResolutionError(config connection.ConnectionConfig if connectionMetadataLooksEmpty(config) { return fmt.Errorf("未找到已保存连接,可能已被删除,请刷新后重试") } - return fmt.Errorf("未找到当前连接对应的已保存密文,请重新填写密码并保存后再试") + return fmt.Errorf("未找到当前连接对应的已保存密文,请编辑当前连接,并输入密码后保存") case errors.Is(err, os.ErrNotExist): - return fmt.Errorf("未找到当前连接对应的已保存密文,请重新填写密码并保存后再试") + return fmt.Errorf("未找到当前连接对应的已保存密文,请编辑当前连接,并输入密码后保存") case strings.Contains(lower, "secret store unavailable"): return fmt.Errorf("系统密文存储当前不可用,请检查系统钥匙串或凭据管理器后再试") default: diff --git a/internal/app/methods_file.go b/internal/app/methods_file.go index b8dec2c..e5bebf3 100644 --- a/internal/app/methods_file.go +++ b/internal/app/methods_file.go @@ -290,6 +290,10 @@ func (a *App) ImportConfigFile() connection.QueryResult { DisplayName: "JSON Files (*.json)", Pattern: "*.json", }, + { + DisplayName: "MySQL Workbench Connections (*.xml)", + Pattern: "*.xml", + }, }, })