Merge pull request #382 from anyanfei/feature/add_import_xml_dev

This commit is contained in:
Syngnat
2026-04-15 13:50:49 +08:00
11 changed files with 306 additions and 9 deletions

View File

@@ -1 +1 @@
26a843d5fd071d0c7e9d8022e98eb4e3
571d014306268cf67665967059cda912

View File

@@ -1702,14 +1702,18 @@ function App() {
const importKind = detectConnectionImportKind(raw);
if (importKind === 'invalid') {
void message.error('文件格式错误:仅支持 GoNavi 恢复包历史 JSON 连接数组');
void message.error('文件格式错误:仅支持 GoNavi 恢复包历史 JSON 连接数组或 MySQL Workbench XML');
return;
}
try {
setPendingConnectionImportPayload(null);
const importedViews = await importConnectionsPayload(raw, '');
void message.success(`成功导入 ${importedViews.length} 个连接`);
if (importKind === 'mysql-workbench-xml' && importedViews.some(v => !v.hasPrimaryPassword)) {
void message.warning(`成功导入 ${importedViews.length} 个连接,部分连接未包含密码,请编辑对应连接并输入密码后保存`);
} else {
void message.success(`成功导入 ${importedViews.length} 个连接`);
}
} catch (e: any) {
if (isConnectionPackagePasswordRequiredError(e)) {
setPendingConnectionImportPayload(raw);

View File

@@ -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('<data') && raw.includes('grt_format') && raw.includes('db.mgmt.Connection')
);
export const detectConnectionImportKind = (raw: unknown): ConnectionImportKind => {
if (typeof raw === 'string' && isMySQLWorkbenchXML(raw)) {
return 'mysql-workbench-xml';
}
const parsed = parseConnectionImportRaw(raw);
if (isConnectionPackageV2AppManagedEnvelope(parsed)) {

View File

@@ -54,4 +54,4 @@ describe('connectionModalPresentation', () => {
shouldToast: false,
});
});
});
});

View File

@@ -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
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<void>;
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
// Cleans up notification resources and releases any held connections.
export function CleanupNotifications(): Promise<void>;
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
// Checks if notifications are available on the current platform.
export function IsNotificationAvailable(): Promise<boolean>;
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
// Requests notification authorization from the user (macOS only).
export function RequestNotificationAuthorization(): Promise<boolean>;
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
// Checks the current notification authorization status (macOS only).
export function CheckNotificationAuthorization(): Promise<boolean>;
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
// Sends a basic notification with the given options.
export function SendNotification(options: NotificationOptions): Promise<void>;
// [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<void>;
// [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<void>;
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
// Removes a previously registered notification category.
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
// Removes all pending notifications from the notification center.
export function RemoveAllPendingNotifications(): Promise<void>;
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
// Removes a specific pending notification by its identifier.
export function RemovePendingNotification(identifier: string): Promise<void>;
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
// Removes all delivered notifications from the notification center.
export function RemoveAllDeliveredNotifications(): Promise<void>;
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
// Removes a specific delivered notification by its identifier.
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
// [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<void>;

View File

@@ -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);
}

3
go.mod
View File

@@ -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

6
go.sum
View File

@@ -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=

View File

@@ -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, "<data") && strings.Contains(content, "grt_format") && strings.Contains(content, "db.mgmt.Connection")
}
// mysqlWorkbenchData is the root XML element.
type mysqlWorkbenchData struct {
XMLName xml.Name `xml:"data"`
Value mysqlWorkbenchTopValue `xml:"value"`
}
type mysqlWorkbenchTopValue struct {
Values []mysqlWorkbenchConnection `xml:"value"`
}
type mysqlWorkbenchConnection struct {
StructName string `xml:"struct-name,attr"`
Values []mysqlWorkbenchValue `xml:"value"`
Links []mysqlWorkbenchLinkValue `xml:"link"`
}
type mysqlWorkbenchValue struct {
Type string `xml:"type,attr"`
Key string `xml:"key,attr"`
StructName string `xml:"struct-name,attr"`
Content string `xml:",chardata"`
Children []mysqlWorkbenchValue `xml:"value"`
}
type mysqlWorkbenchLinkValue struct {
Key string `xml:"key,attr"`
Content string `xml:",chardata"`
}
func parseMySQLWorkbenchXML(content string) ([]connection.SavedConnectionInput, error) {
var data mysqlWorkbenchData
if err := xml.Unmarshal([]byte(content), &data); err != nil {
return nil, err
}
var inputs []connection.SavedConnectionInput
for _, conn := range data.Value.Values {
if conn.StructName != "db.mgmt.Connection" {
continue
}
input := parseMySQLWorkbenchConnection(conn)
inputs = append(inputs, input)
}
return inputs, nil
}
func parseMySQLWorkbenchConnection(conn mysqlWorkbenchConnection) connection.SavedConnectionInput {
params := make(map[string]string)
connName := ""
driverKey := ""
for _, v := range conn.Values {
key := strings.TrimSpace(v.Key)
switch {
case key == "name" && v.Type == "string":
connName = strings.TrimSpace(v.Content)
case key == "parameterValues" && v.Type == "dict":
for _, child := range v.Children {
childKey := strings.TrimSpace(child.Key)
if childKey == "" {
continue
}
params[childKey] = strings.TrimSpace(child.Content)
}
}
}
for _, link := range conn.Links {
if strings.TrimSpace(link.Key) == "driver" {
driverKey = strings.TrimSpace(link.Content)
}
}
host := params["hostName"]
port := 3306
if p, err := strconv.Atoi(params["port"]); err == nil && p > 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,
}
}

View File

@@ -37,6 +37,7 @@ func (a *App) resolveConnectionSecrets(config connection.ConnectionConfig) (conn
}
resolved := mergeConnectionSecretBundleIntoConfig(base, bundle)
resolved.ID = view.ID
return resolved, nil
}

View File

@@ -290,6 +290,10 @@ func (a *App) ImportConfigFile() connection.QueryResult {
DisplayName: "JSON Files (*.json)",
Pattern: "*.json",
},
{
DisplayName: "MySQL Workbench Connections (*.xml)",
Pattern: "*.xml",
},
},
})