This commit is contained in:
DurianPankek
2026-04-11 13:23:51 +08:00
committed by GitHub
7 changed files with 410 additions and 9 deletions

View File

@@ -2,7 +2,7 @@
import { Layout, Button, ConfigProvider, theme, message, Modal, Spin, Slider, Progress, Switch, Input, InputNumber, Select, Segmented, Tooltip } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined, RobotOutlined } from '@ant-design/icons';
import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowMaximise, WindowMinimise, WindowSetPosition, WindowSetSize, WindowToggleMaximise, WindowUnfullscreen } from '../wailsjs/runtime';
import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowIsMinimised, WindowIsNormal, WindowMaximise, WindowMinimise, WindowSetPosition, WindowSetSize, WindowToggleMaximise, WindowUnfullscreen } from '../wailsjs/runtime';
import Sidebar from './components/Sidebar';
import TabManager from './components/TabManager';
import ConnectionModal from './components/ConnectionModal';
@@ -130,6 +130,9 @@ function App() {
const toggleAIPanel = useStore(state => state.toggleAIPanel);
const setAIPanelVisible = useStore(state => state.setAIPanelVisible);
const globalProxyInvalidHintShownRef = React.useRef(false);
const windowDiagSequenceRef = React.useRef(0);
const windowDiagLastSignatureRef = React.useRef('');
const windowDiagLastAtRef = React.useRef(0);
const connectionWorkbenchState = getConnectionWorkbenchState(isStoreHydrated, hasLoadedSecureConfig);
const windowCornerRadius = 14;
@@ -481,6 +484,10 @@ function App() {
const store = useStore.getState();
const newState = isFs ? 'fullscreen' : (isMax ? 'maximized' : 'normal');
if (store.windowState !== newState) {
void emitWindowDiagnostic('transition:windowState', {
from: store.windowState,
to: newState,
});
store.setWindowState(newState);
}
@@ -496,15 +503,18 @@ function App() {
const h = Math.trunc(Number(size.h || 0));
const x = Math.trunc(Number(pos.x || 0));
const y = Math.trunc(Number(pos.y || 0));
if (w < 400 || h < 300) return;
if (w < 400 || h < 300) return;
const key = `${w},${h},${x},${y}`;
if (key === lastSaved) return;
lastSaved = key;
store.setWindowBounds({ width: w, height: h, x, y });
} catch (e) {
// 静默忽略
}
const key = `${w},${h},${x},${y}`;
if (key === lastSaved) return;
lastSaved = key;
if (Math.abs(x) > 5000 || Math.abs(y) > 5000) {
void emitWindowDiagnostic('anomaly:windowBounds', { width: w, height: h, x, y });
}
store.setWindowBounds({ width: w, height: h, x, y });
} catch (e) {
// 静默忽略
}
};
const timer = window.setInterval(saveWindowState, SAVE_INTERVAL_MS);
@@ -854,6 +864,63 @@ function App() {
|| (runtimePlatform === '' && isWindowsPlatform());
const useNativeMacWindowControls = isMacRuntime && appearance.useNativeMacWindowControls === true;
const emitWindowDiagnostic = useCallback(async (stage: string, extra: Record<string, unknown> = {}) => {
if (!isMacRuntime) {
return;
}
const backendApp = (window as any).go?.app?.App;
if (typeof backendApp?.LogWindowDiagnostic !== 'function') {
return;
}
try {
const [isFullscreen, isMaximised, isMinimised, isNormal, size, position] = await Promise.all([
WindowIsFullscreen().catch(() => false),
WindowIsMaximised().catch(() => false),
WindowIsMinimised().catch(() => false),
WindowIsNormal().catch(() => false),
WindowGetSize().catch(() => null),
WindowGetPosition().catch(() => null),
]);
const payload = {
seq: ++windowDiagSequenceRef.current,
ts: new Date().toISOString(),
stage,
nativeControls: useNativeMacWindowControls,
documentVisible: document.visibilityState,
documentHasFocus: document.hasFocus(),
devicePixelRatio: Number(window.devicePixelRatio) || 1,
windowState: {
isFullscreen,
isMaximised,
isMinimised,
isNormal,
},
size: size ? { w: Math.trunc(Number(size.w || 0)), h: Math.trunc(Number(size.h || 0)) } : null,
position: position ? { x: Math.trunc(Number(position.x || 0)), y: Math.trunc(Number(position.y || 0)) } : null,
extra,
};
const signature = JSON.stringify({
stage,
nativeControls: payload.nativeControls,
visible: payload.documentVisible,
focus: payload.documentHasFocus,
state: payload.windowState,
size: payload.size,
position: payload.position,
extra,
});
const now = Date.now();
if (signature === windowDiagLastSignatureRef.current && now-windowDiagLastAtRef.current < 250) {
return;
}
windowDiagLastSignatureRef.current = signature;
windowDiagLastAtRef.current = now;
await backendApp.LogWindowDiagnostic(stage, JSON.stringify(payload));
} catch (error) {
console.warn('Failed to emit window diagnostic', error);
}
}, [isMacRuntime, useNativeMacWindowControls]);
useEffect(() => {
if (!isStoreHydrated || !isMacRuntime) {
return;
@@ -866,6 +933,104 @@ function App() {
}
}, [isMacRuntime, isStoreHydrated, useNativeMacWindowControls]);
useEffect(() => {
if (!isMacRuntime) {
return;
}
let cancelled = false;
let pollTimer: number | null = null;
let burstTimer: number | null = null;
const stopBurst = () => {
if (pollTimer !== null) {
window.clearInterval(pollTimer);
pollTimer = null;
}
if (burstTimer !== null) {
window.clearTimeout(burstTimer);
burstTimer = null;
}
};
const startBurst = (reason: string, extra: Record<string, unknown> = {}) => {
if (cancelled) {
return;
}
void emitWindowDiagnostic(`burst:start:${reason}`, extra);
if (pollTimer === null) {
pollTimer = window.setInterval(() => {
void emitWindowDiagnostic(`burst:tick:${reason}`);
}, 250);
}
if (burstTimer !== null) {
window.clearTimeout(burstTimer);
}
burstTimer = window.setTimeout(() => {
stopBurst();
void emitWindowDiagnostic(`burst:stop:${reason}`);
}, 6000);
};
const handleFocus = () => {
void emitWindowDiagnostic('event:focus');
};
const handleBlur = () => {
void emitWindowDiagnostic('event:blur');
};
const handleResize = () => {
void emitWindowDiagnostic('event:resize');
};
const handleVisibilityChange = () => {
void emitWindowDiagnostic('event:visibilitychange', { visibility: document.visibilityState });
};
const handleEditableKeydown = (event: KeyboardEvent) => {
if (!isEditableElement(event.target)) {
return;
}
const key = String(event.key || '');
const maybeFullscreenKey = key === 'Escape' || key.toLowerCase() === 'f' || key === 'Process';
const hasModifier = event.ctrlKey || event.metaKey || event.altKey;
startBurst('editable-keydown', {
key,
code: String(event.code || ''),
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
maybeFullscreenKey,
hasModifier,
});
};
const handleCompositionStart = () => {
startBurst('compositionstart');
};
const handleCompositionEnd = () => {
startBurst('compositionend');
};
void emitWindowDiagnostic('session:start');
window.addEventListener('focus', handleFocus);
window.addEventListener('blur', handleBlur);
window.addEventListener('resize', handleResize);
window.addEventListener('keydown', handleEditableKeydown, true);
window.addEventListener('compositionstart', handleCompositionStart, true);
window.addEventListener('compositionend', handleCompositionEnd, true);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
cancelled = true;
stopBurst();
window.removeEventListener('focus', handleFocus);
window.removeEventListener('blur', handleBlur);
window.removeEventListener('resize', handleResize);
window.removeEventListener('keydown', handleEditableKeydown, true);
window.removeEventListener('compositionstart', handleCompositionStart, true);
window.removeEventListener('compositionend', handleCompositionEnd, true);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [emitWindowDiagnostic, isMacRuntime]);
const formatBytes = (bytes?: number) => {
if (!bytes || bytes <= 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
@@ -1373,15 +1538,19 @@ function App() {
const handleTitleBarWindowToggle = async () => {
try {
void emitWindowDiagnostic('action:titlebar-toggle:before');
if (await WindowIsFullscreen()) {
await WindowUnfullscreen();
void emitWindowDiagnostic('action:titlebar-toggle:after-unfullscreen');
return;
}
if (useNativeMacWindowControls && isMacRuntime) {
await WindowFullscreen();
void emitWindowDiagnostic('action:titlebar-toggle:after-fullscreen');
return;
}
await WindowToggleMaximise();
void emitWindowDiagnostic('action:titlebar-toggle:after-toggle-maximise');
} catch (_) {
// ignore
}

View File

@@ -110,6 +110,8 @@ export function InstallLocalDriverPackage(arg1:string,arg2:string,arg3:string,ar
export function InstallUpdateAndRestart():Promise<connection.QueryResult>;
export function LogWindowDiagnostic(arg1:string,arg2:string):Promise<void>;
export function MongoDiscoverMembers(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
export function MySQLConnect(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;

View File

@@ -214,6 +214,10 @@ export function InstallUpdateAndRestart() {
return window['go']['app']['App']['InstallUpdateAndRestart']();
}
export function LogWindowDiagnostic(arg1, arg2) {
return window['go']['app']['App']['LogWindowDiagnostic'](arg1, arg2);
}
export function MongoDiscoverMembers(arg1) {
return window['go']['app']['App']['MongoDiscoverMembers'](arg1);
}

View File

@@ -91,6 +91,7 @@ func (a *App) startup(ctx context.Context) {
}
logger.Init()
a.loadPersistedGlobalProxy()
installMacNativeWindowDiagnostics(logger.Path())
applyMacWindowTranslucencyFix()
logger.Infof("应用启动完成(首次连接保护窗口=%s最多重试=%d 次)", startupConnectRetryWindow, startupConnectRetryAttempts)
}
@@ -108,6 +109,16 @@ func (a *App) SetMacNativeWindowControls(enabled bool) {
setMacNativeWindowControls(enabled)
}
// LogWindowDiagnostic 记录前端采集到的窗口诊断信息,便于排查 macOS 原生全屏异常。
func (a *App) LogWindowDiagnostic(stage string, payload string) {
stage = strings.TrimSpace(stage)
payload = strings.TrimSpace(payload)
if stage == "" {
stage = "unknown"
}
logger.Warnf("窗口诊断stage=%s payload=%s", stage, payload)
}
// Shutdown is called when the app terminates
func (a *App) Shutdown(ctx context.Context) {
logger.Infof("应用开始关闭,准备释放资源")

View File

@@ -5,12 +5,143 @@ package app
/*
#cgo CFLAGS: -x objective-c -fblocks
#cgo LDFLAGS: -framework Cocoa
#include <stdlib.h>
#import <Cocoa/Cocoa.h>
#import <dispatch/dispatch.h>
static inline BOOL gonaviBoolYES() { return YES; }
static inline BOOL gonaviBoolNO() { return NO; }
static char *gonaviNativeLogPath = NULL;
static BOOL gonaviNativeObserverInstalled = NO;
static void gonaviWriteNativeWindowLogLine(NSString *line) {
if (line == nil || gonaviNativeLogPath == NULL) {
return;
}
NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:[NSString stringWithUTF8String:gonaviNativeLogPath]];
if (handle == nil) {
return;
}
@try {
[handle seekToEndOfFile];
NSData *data = [line dataUsingEncoding:NSUTF8StringEncoding];
[handle writeData:data];
} @catch (__unused NSException *exception) {
} @finally {
[handle closeFile];
}
}
static NSString *gonaviWindowDiagnosticLine(NSString *eventName, NSWindow *window) {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy/MM/dd HH:mm:ss.SSSSSS"];
NSString *timestamp = [formatter stringFromDate:[NSDate date]];
[formatter release];
if (window == nil) {
return [NSString stringWithFormat:@"%@ [WARN] 原生窗口诊断event=%@ window=nil\n", timestamp, eventName ?: @"unknown"];
}
NSUInteger occlusionState = 0;
NSInteger windowNumber = [window windowNumber];
NSInteger level = [window level];
NSUInteger collectionBehavior = [window collectionBehavior];
NSString *className = NSStringFromClass([window class]);
NSString *delegateClassName = [window delegate] ? NSStringFromClass([[window delegate] class]) : @"nil";
if (@available(macOS 10.9, *)) {
occlusionState = [window occlusionState];
}
return [NSString stringWithFormat:@"%@ [WARN] 原生窗口诊断event=%@ ptr=%p number=%ld class=%@ delegate=%@ visible=%@ miniaturized=%@ key=%@ main=%@ canHide=%@ level=%ld occlusion=%lu styleMask=%lu collectionBehavior=%lu frame=%@ screen=%@ title=%@\n",
timestamp,
eventName ?: @"unknown",
window,
(long)windowNumber,
className ?: @"nil",
delegateClassName,
[window isVisible] ? @"true" : @"false",
[window isMiniaturized] ? @"true" : @"false",
[window isKeyWindow] ? @"true" : @"false",
[window isMainWindow] ? @"true" : @"false",
[window canHide] ? @"true" : @"false",
(long)level,
(unsigned long)occlusionState,
(unsigned long)[window styleMask],
(unsigned long)collectionBehavior,
NSStringFromRect([window frame]),
[window screen] ? NSStringFromRect([[window screen] frame]) : @"nil",
[window title] ?: @""];
}
@interface GoNaviNativeWindowObserver : NSObject
@end
@implementation GoNaviNativeWindowObserver
- (void)logNotification:(NSNotification *)notification {
NSString *name = [notification name] ?: @"unknown";
NSWindow *window = nil;
if ([[notification object] isKindOfClass:[NSWindow class]]) {
window = (NSWindow *)[notification object];
} else {
window = [NSApp keyWindow] ?: [NSApp mainWindow];
}
gonaviWriteNativeWindowLogLine(gonaviWindowDiagnosticLine(name, window));
}
@end
static GoNaviNativeWindowObserver *gonaviNativeWindowObserver = nil;
static void gonaviInstallNativeWindowObserver(void) {
if (gonaviNativeObserverInstalled) {
return;
}
gonaviNativeObserverInstalled = YES;
gonaviNativeWindowObserver = [[GoNaviNativeWindowObserver alloc] init];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
NSArray<NSString *> *windowNotifications = @[
NSWindowDidBecomeKeyNotification,
NSWindowDidResignKeyNotification,
NSWindowDidBecomeMainNotification,
NSWindowDidResignMainNotification,
NSWindowDidMiniaturizeNotification,
NSWindowDidDeminiaturizeNotification,
NSWindowDidEnterFullScreenNotification,
NSWindowDidExitFullScreenNotification,
NSWindowDidMoveNotification,
NSWindowDidResizeNotification,
NSWindowDidChangeOcclusionStateNotification,
];
for (NSString *notificationName in windowNotifications) {
[center addObserver:gonaviNativeWindowObserver selector:@selector(logNotification:) name:notificationName object:nil];
}
NSArray<NSString *> *appNotifications = @[
NSApplicationDidHideNotification,
NSApplicationDidUnhideNotification,
NSApplicationDidBecomeActiveNotification,
NSApplicationDidResignActiveNotification,
];
for (NSString *notificationName in appNotifications) {
[center addObserver:gonaviNativeWindowObserver selector:@selector(logNotification:) name:notificationName object:nil];
}
for (NSWindow *window in [NSApp windows]) {
gonaviWriteNativeWindowLogLine(gonaviWindowDiagnosticLine(@"observer:snapshot", window));
}
}
static void gonaviConfigureNativeWindowDiagnostics(const char *logPath) {
if (logPath == NULL || logPath[0] == '\0') {
return;
}
if (gonaviNativeLogPath != NULL) {
free(gonaviNativeLogPath);
gonaviNativeLogPath = NULL;
}
gonaviNativeLogPath = strdup(logPath);
dispatch_async(dispatch_get_main_queue(), ^{
gonaviInstallNativeWindowObserver();
});
}
static void gonaviSetWindowButtonsVisible(NSWindow *window, BOOL visible) {
if (window == nil) {
return;
@@ -24,12 +155,31 @@ static void gonaviSetWindowButtonsVisible(NSWindow *window, BOOL visible) {
}
}
static BOOL gonaviShouldApplyMacWindowStyle(NSWindow *window) {
if (window == nil) {
return NO;
}
NSString *className = NSStringFromClass([window class]) ?: @"";
NSString *delegateClassName = [window delegate] ? NSStringFromClass([[window delegate] class]) : @"";
NSString *title = [window title] ?: @"";
// 仅对主 WailsWindow 套用原生标题栏/全屏样式,避免误伤输入法候选窗、全屏过渡窗。
if ([className isEqualToString:@"WailsWindow"] || [delegateClassName isEqualToString:@"WindowDelegate"]) {
return YES;
}
return [title isEqualToString:@"GoNavi"];
}
static void gonaviApplyMacWindowStyle(BOOL enabled) {
dispatch_async(dispatch_get_main_queue(), ^{
for (NSWindow *window in [NSApp windows]) {
if (window == nil) {
continue;
}
if (!gonaviShouldApplyMacWindowStyle(window)) {
gonaviWriteNativeWindowLogLine(gonaviWindowDiagnosticLine(@"style:skip-non-app-window", window));
continue;
}
NSUInteger styleMask = [window styleMask];
styleMask |= NSWindowStyleMaskClosable;
@@ -57,12 +207,24 @@ static void gonaviApplyMacWindowStyle(BOOL enabled) {
[[window contentView] setNeedsDisplay:YES];
[window invalidateShadow];
gonaviWriteNativeWindowLogLine(gonaviWindowDiagnosticLine(enabled ? @"style:enable-native-controls" : @"style:disable-native-controls", window));
}
});
}
*/
import "C"
import "unsafe"
func installMacNativeWindowDiagnostics(logPath string) {
if logPath == "" {
return
}
cLogPath := C.CString(logPath)
defer C.free(unsafe.Pointer(cLogPath))
C.gonaviConfigureNativeWindowDiagnostics(cLogPath)
}
func setMacNativeWindowControls(enabled bool) {
state := resolveMacNativeWindowControlState(enabled)
if state.ShowNativeButtons {

View File

@@ -1,5 +1,7 @@
package app
import "strings"
type macNativeWindowControlState struct {
ShowNativeButtons bool
UseTitledWindow bool
@@ -30,3 +32,24 @@ func resolveMacNativeWindowControlState(enabled bool) macNativeWindowControlStat
AllowNativeFullscreen: false,
}
}
type macWindowIdentity struct {
ClassName string
DelegateClassName string
Title string
}
// shouldApplyMacNativeWindowStyle 只允许对主 WailsWindow 应用原生标题栏/全屏能力,
// 避免误伤输入法候选窗、全屏过渡窗等系统辅助窗口。
func shouldApplyMacNativeWindowStyle(identity macWindowIdentity) bool {
className := strings.TrimSpace(identity.ClassName)
delegateClassName := strings.TrimSpace(identity.DelegateClassName)
title := strings.TrimSpace(identity.Title)
if className == "WailsWindow" || delegateClassName == "WindowDelegate" {
return true
}
// 兜底只接受明确命名的主应用窗口,避免把无标题系统辅助窗口纳入样式改写范围。
return title == "GoNavi"
}

View File

@@ -35,3 +35,33 @@ func TestResolveMacNativeWindowControlStateDisabled(t *testing.T) {
t.Fatal("expected disabled state to avoid native fullscreen behavior")
}
}
func TestShouldApplyMacNativeWindowStyleAcceptsMainWailsWindow(t *testing.T) {
tests := []macWindowIdentity{
{ClassName: "WailsWindow", DelegateClassName: "WindowDelegate", Title: "GoNavi"},
{ClassName: "WailsWindow", DelegateClassName: "", Title: ""},
{ClassName: "", DelegateClassName: "WindowDelegate", Title: ""},
{ClassName: "", DelegateClassName: "", Title: "GoNavi"},
}
for _, tt := range tests {
if !shouldApplyMacNativeWindowStyle(tt) {
t.Fatalf("expected window identity %+v to be treated as main app window", tt)
}
}
}
func TestShouldApplyMacNativeWindowStyleRejectsSystemAuxiliaryWindows(t *testing.T) {
tests := []macWindowIdentity{
{ClassName: "TUINSWindow", DelegateClassName: "TUINSWindow", Title: ""},
{ClassName: "NSToolbarFullScreenWindow", DelegateClassName: "", Title: ""},
{ClassName: "_NSFullScreenTransitionOverlayWindow", DelegateClassName: "", Title: ""},
{ClassName: "NSPanel", DelegateClassName: "", Title: ""},
}
for _, tt := range tests {
if shouldApplyMacNativeWindowStyle(tt) {
t.Fatalf("expected window identity %+v to be rejected as auxiliary/system window", tt)
}
}
}