Files
MyGoNavi/internal/webserver/runtime_bridge.go
Syngnat 714c6fc553 feat(web-server): 新增浏览器访问认证与初始化流程
- 新增 web-server 运行模式与浏览器端运行时桥接
- 支持管理员密码、会话策略、Google Authenticator 与恢复码
- 设置中心补充浏览器访问认证状态与改密入口
- 优化初始化向导,未启用 2FA 时跳过验证器步骤

Refs #618
2026-07-04 21:50:52 +08:00

296 lines
9.5 KiB
Go

package webserver
func runtimeBridgeScript() string {
return `(function () {
if (typeof window === 'undefined') {
return;
}
var apiBase = '` + internalRoutePrefix + `';
var existingRuntime = window.runtime || {};
var listeners = new Map();
var eventSource = null;
var dispatch = function (eventName, args) {
var callbacks = listeners.get(eventName);
if (!callbacks || callbacks.size === 0) {
return;
}
callbacks.forEach(function (callback) {
try {
if (!Array.isArray(args) || args.length === 0) {
callback();
return;
}
if (args.length === 1) {
callback(args[0]);
return;
}
callback.apply(null, args);
} catch (error) {
console.error('[GoNavi Web Runtime] event callback failed', error);
}
});
};
var ensureEventSource = function () {
if (eventSource || typeof EventSource === 'undefined') {
return;
}
eventSource = new EventSource(apiBase + '/events');
eventSource.addEventListener('gonavi', function (event) {
try {
var payload = JSON.parse(event.data || '{}');
dispatch(String(payload.name || ''), Array.isArray(payload.args) ? payload.args : []);
} catch (error) {
console.error('[GoNavi Web Runtime] failed to parse server event', error);
}
});
eventSource.onerror = function () {
if (!eventSource) {
return;
}
try {
eventSource.close();
} catch (_) {}
eventSource = null;
window.setTimeout(ensureEventSource, 1500);
};
};
var addListener = function (eventName, callback) {
var key = String(eventName || '').trim();
if (!key || typeof callback !== 'function') {
return function () {};
}
ensureEventSource();
if (!listeners.has(key)) {
listeners.set(key, new Set());
}
listeners.get(key).add(callback);
return function () {
var callbacks = listeners.get(key);
if (!callbacks) {
return;
}
callbacks.delete(callback);
if (callbacks.size === 0) {
listeners.delete(key);
}
};
};
var removeListeners = function () {
Array.prototype.slice.call(arguments).forEach(function (eventName) {
listeners.delete(String(eventName || '').trim());
});
};
var clearListeners = function () {
listeners.clear();
if (eventSource) {
try {
eventSource.close();
} catch (_) {}
eventSource = null;
}
};
var invoke = async function (namespace, receiver, method, args) {
var response = await fetch(apiBase + '/api/invoke', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
namespace: namespace,
receiver: receiver,
method: method,
args: Array.isArray(args) ? args : []
})
});
var payload = await response.json().catch(function () { return {}; });
if (!response.ok || payload.error) {
throw new Error(payload.error || ('invoke failed with status ' + response.status));
}
return payload.result;
};
var buildServiceProxy = function (namespace, receiver) {
return new Proxy({}, {
get: function (_target, property) {
if (typeof property !== 'string') {
return undefined;
}
return function () {
return invoke(namespace, receiver, property, Array.prototype.slice.call(arguments));
};
}
});
};
var detectPlatform = function () {
var value = '';
if (navigator.userAgentData && navigator.userAgentData.platform) {
value = navigator.userAgentData.platform;
} else if (navigator.platform) {
value = navigator.platform;
} else if (navigator.userAgent) {
value = navigator.userAgent;
}
value = String(value || '').toLowerCase();
if (value.indexOf('win') >= 0) return 'windows';
if (value.indexOf('mac') >= 0) return 'darwin';
if (value.indexOf('linux') >= 0) return 'linux';
return '';
};
var environment = function () {
return Promise.resolve({
platform: detectPlatform(),
buildType: 'web',
capabilities: {
nativeWindow: false,
fileDialog: false,
clipboard: typeof navigator !== 'undefined' && !!navigator.clipboard
}
});
};
var openExternal = function (url) {
if (!url) return;
window.open(String(url), '_blank', 'noopener,noreferrer');
};
var clipboardGetText = async function () {
if (navigator.clipboard && navigator.clipboard.readText) {
return navigator.clipboard.readText();
}
return '';
};
var clipboardSetText = async function (value) {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(String(value || ''));
}
};
var fullscreen = async function () {
if (document.documentElement && document.documentElement.requestFullscreen) {
await document.documentElement.requestFullscreen().catch(function () { return undefined; });
}
};
var unfullscreen = async function () {
if (document.exitFullscreen) {
await document.exitFullscreen().catch(function () { return undefined; });
}
};
var getSize = function () {
return Promise.resolve({ w: window.innerWidth || 0, h: window.innerHeight || 0 });
};
var getPosition = function () {
return Promise.resolve({ x: window.screenX || 0, y: window.screenY || 0 });
};
var existingGo = window.go || {};
window.__GONAVI_WEB_RUNTIME__ = {
buildType: 'web',
capabilities: {
nativeWindow: false,
fileDialog: false,
clipboard: typeof navigator !== 'undefined' && !!navigator.clipboard
}
};
window.go = {
...existingGo,
app: {
...(existingGo.app || {}),
App: buildServiceProxy('app', 'App')
},
aiservice: {
...(existingGo.aiservice || {}),
Service: buildServiceProxy('aiservice', 'Service')
}
};
window.runtime = {
...existingRuntime,
LogPrint: function (message) { console.log(message); },
LogTrace: function (message) { console.debug(message); },
LogDebug: function (message) { console.debug(message); },
LogInfo: function (message) { console.info(message); },
LogWarning: function (message) { console.warn(message); },
LogError: function (message) { console.error(message); },
LogFatal: function (message) { console.error(message); },
EventsOnMultiple: function (eventName, callback) { return addListener(eventName, callback); },
EventsOff: removeListeners,
EventsOffAll: clearListeners,
EventsEmit: function (eventName) {
dispatch(String(eventName || '').trim(), Array.prototype.slice.call(arguments, 1));
},
WindowReload: function () { window.location.reload(); },
WindowReloadApp: function () { window.location.reload(); },
WindowSetAlwaysOnTop: function () { return Promise.resolve(); },
WindowCenter: function () { return Promise.resolve(); },
WindowSetTitle: function (title) {
if (typeof title === 'string' && title.trim()) {
document.title = title;
}
return Promise.resolve();
},
BrowserOpenURL: openExternal,
Environment: environment,
WindowFullscreen: fullscreen,
WindowUnfullscreen: unfullscreen,
WindowIsFullscreen: function () { return Promise.resolve(!!document.fullscreenElement); },
WindowIsMaximised: function () { return Promise.resolve(false); },
WindowIsMinimised: function () { return Promise.resolve(false); },
WindowIsNormal: function () { return Promise.resolve(!document.fullscreenElement); },
WindowMaximise: function () { return Promise.resolve(); },
WindowMinimise: function () { return Promise.resolve(); },
WindowUnmaximise: function () { return Promise.resolve(); },
WindowUnminimise: function () { return Promise.resolve(); },
WindowToggleMaximise: function () {
if (document.fullscreenElement) {
return unfullscreen();
}
return fullscreen();
},
WindowGetSize: getSize,
WindowGetPosition: getPosition,
WindowSetSize: function (width, height) {
try { window.resizeTo(Number(width) || window.innerWidth, Number(height) || window.innerHeight); } catch (_) {}
return Promise.resolve();
},
WindowSetMaxSize: function () { return Promise.resolve(); },
WindowSetMinSize: function () { return Promise.resolve(); },
WindowSetPosition: function (x, y) {
try { window.moveTo(Number(x) || 0, Number(y) || 0); } catch (_) {}
return Promise.resolve();
},
WindowHide: function () { return Promise.resolve(); },
WindowShow: function () { return Promise.resolve(); },
ScreenGetAll: function () { return Promise.resolve([]); },
WindowSetSystemDefaultTheme: function () { return Promise.resolve(); },
WindowSetDarkTheme: function () { return Promise.resolve(); },
WindowSetLightTheme: function () { return Promise.resolve(); },
Quit: function () {
try { window.close(); } catch (_) {}
return Promise.resolve();
},
Hide: function () { return Promise.resolve(); },
Show: function () { return Promise.resolve(); },
ClipboardGetText: clipboardGetText,
ClipboardSetText: clipboardSetText,
OnFileDrop: function () { return function () {}; },
OnFileDropOff: function () { return Promise.resolve(); },
CanResolveFilePaths: function () { return Promise.resolve(false); },
ResolveFilePaths: function (files) { return Promise.resolve(Array.isArray(files) ? files : []); }
};
})();`
}