mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-13 00:13:33 +08:00
- 新增 web-server 运行模式与浏览器端运行时桥接 - 支持管理员密码、会话策略、Google Authenticator 与恢复码 - 设置中心补充浏览器访问认证状态与改密入口 - 优化初始化向导,未启用 2FA 时跳过验证器步骤 Refs #618
296 lines
9.5 KiB
Go
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 : []); }
|
|
};
|
|
})();`
|
|
}
|