/* * @Author: 李皓 hao_li_work@163.com * @Date: 2026-07-28 16:31:45 * @LastEditors: 李皓 hao_li_work@163.com * @LastEditTime: 2026-07-28 16:44:07 * @FilePath: \pc\preload.js * @Description: * @Version: 1.0.0 */ const { contextBridge, ipcRenderer } = require('electron') const nodeNet = require('net') const dns = require('dns') const { execFile } = require('child_process') function tryConnect(host, port, timeout) { return new Promise((resolve) => { const socket = new nodeNet.Socket() socket.setTimeout(timeout) socket.on('connect', () => { socket.destroy(); resolve(true) }) socket.on('error', () => { socket.destroy(); resolve(false) }) socket.on('timeout', () => { socket.destroy(); resolve(false) }) socket.connect(port, host) }) } function normalizePingHost(input) { const raw = String(input || '').trim() if (!raw) return '' try { return new URL(raw).hostname } catch {} return raw.replace(/^https?:\/\//i, '').split('/')[0].split(':')[0] } function trySystemPing(host, timeout) { return new Promise((resolve) => { const wait = Math.max(1000, Number(timeout) || 2000) execFile('ping', ['-n', '1', '-w', String(wait), host], { windowsHide: true }, (err) => { resolve(!err) }) }) } const net = { async ping(host, timeout) { const target = normalizePingHost(host) if (!target) return false const wait = Math.max(1000, Number(timeout) || 2000) const hosts = [...new Set([target, 'www.qq.com', 'www.microsoft.com'])] for (const currentHost of hosts) { const isIp = nodeNet.isIP(currentHost) !== 0 try { if (!isIp) await dns.promises.lookup(currentHost) } catch { if (!await trySystemPing(currentHost, wait)) continue } for (const port of [443, 80, 53]) { if (await tryConnect(currentHost, port, wait)) { ipcRenderer.send('app-network-status', true) return true } } if (await trySystemPing(currentHost, wait)) { ipcRenderer.send('app-network-status', true) return true } } const online = !!globalThis.navigator?.onLine ipcRenderer.send('app-network-status', online) return online } } function currentRouteText() { return window.location.hash || window.location.pathname || '#/' } function reportRoute() { ipcRenderer.send('app-route-changed', currentRouteText()) } function installRouteReporter() { reportRoute() window.addEventListener('hashchange', reportRoute) window.addEventListener('popstate', reportRoute) const timer = setInterval(reportRoute, 1000) window.addEventListener('beforeunload', () => clearInterval(timer)) } installRouteReporter() function injectShellDrawer() { if (document.getElementById('__electron_shell_drawer__')) return const style = document.createElement('style') style.id = '__electron_shell_drawer_style__' style.textContent = ` #__electron_shell_drawer__{position:fixed;left:0;top:0;bottom:0;z-index:2147483647;pointer-events:none;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} #__electron_shell_drawer__ *{box-sizing:border-box} #__electron_shell_handle__{position:absolute;left:0;top:50%;transform:translateY(-50%);width:18px;height:120px;border-radius:0 12px 12px 0;background:#2f8cff;color:#fff;display:flex;align-items:center;justify-content:center;pointer-events:auto;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.16)} #__electron_shell_panel__{position:absolute;left:0;top:56px;bottom:56px;width:320px;max-width:88vw;background:rgba(255,255,255,.98);border-right:1px solid rgba(0,0,0,.08);box-shadow:6px 0 24px rgba(0,0,0,.12);transform:translateX(-100%);transition:transform .18s ease;pointer-events:auto;overflow:auto;padding:12px} #__electron_shell_drawer__.open #__electron_shell_panel__{transform:translateX(0)} #__electron_shell_header__{font-size:13px;font-weight:600;margin:0 0 10px} #__electron_shell_row__{display:flex;gap:8px;align-items:center;margin:8px 0} #__electron_shell_url__{width:100%;padding:8px 10px;border:1px solid #cfd7e3;border-radius:6px;font-size:13px;outline:none} .__electron_shell_btn__{border:1px solid #c7d7ef;background:#fff;color:#1f3f6d;border-radius:6px;padding:7px 10px;font-size:12px;cursor:pointer} .__electron_shell_btn_primary__{background:#2f8cff;border-color:#2f8cff;color:#fff} #__electron_shell_status__{font-size:12px;line-height:1.5;color:#445} #__electron_shell_routes__{margin-top:8px;font-size:12px;word-break:break-all;color:#234} ` document.head.appendChild(style) const wrap = document.createElement('div') wrap.id = '__electron_shell_drawer__' wrap.innerHTML = `
壳层入口
` document.body.appendChild(wrap) const handle = wrap.querySelector('#__electron_shell_handle__') const panel = wrap.querySelector('#__electron_shell_panel__') const input = wrap.querySelector('#__electron_shell_url__') const status = wrap.querySelector('#__electron_shell_status__') const routes = wrap.querySelector('#__electron_shell_routes__') const openBtn = wrap.querySelector('#__electron_shell_open__') const localBtn = wrap.querySelector('#__electron_shell_local__') const copyBtn = wrap.querySelector('#__electron_shell_copy__') const syncState = async () => { const url = await app.shellGetTargetUrl().catch(() => '') input.value = url || '' status.textContent = [ `网络:${globalThis.navigator?.onLine ? '在线' : '离线'}`, `倾角:${JSON.stringify(sensor.sensorList ? JSON.parse(sensor.sensorList()).data?.some?.(el => String(el?.name || '').includes('HWT6053')) : false)}`, ].join(' | ') routes.textContent = `当前:${window.location.hash || window.location.pathname || '#/'}` } handle.addEventListener('click', () => wrap.classList.toggle('open')) openBtn.addEventListener('click', async () => { const url = input.value.trim() await app.shellSetTargetUrl(url) location.reload() }) localBtn.addEventListener('click', async () => { await app.shellSetTargetUrl('') location.reload() }) copyBtn.addEventListener('click', async () => { await navigator.clipboard?.writeText(window.location.href).catch(() => {}) }) setInterval(syncState, 1500) syncState() } const shellOpenObserver = new MutationObserver(() => { if (document.body && !document.getElementById('__electron_shell_drawer__')) injectShellDrawer() }) shellOpenObserver.observe(document.documentElement || document, { childList: true, subtree: true }) const appCallbacks = { onFileWrite: null, onFileRead: null, onFileDelete: null, onPageModeChanged: null, onShellOpenUrlDialog: null } ipcRenderer.on('app-file-write-callback', (_event, token, filePath, success, err) => { if (typeof appCallbacks.onFileWrite === 'function') appCallbacks.onFileWrite(token, filePath, success, err) }) ipcRenderer.on('app-file-read-callback', (_event, token, filePath, content) => { if (typeof appCallbacks.onFileRead === 'function') appCallbacks.onFileRead(token, filePath, content) }) ipcRenderer.on('app-file-delete-callback', (_event, token, filePath, success) => { if (typeof appCallbacks.onFileDelete === 'function') appCallbacks.onFileDelete(token, filePath, success) }) ipcRenderer.on('app-page-mode', (_event, mode) => { if (typeof appCallbacks.onPageModeChanged === 'function') appCallbacks.onPageModeChanged(mode) }) ipcRenderer.on('app-shell-open-url-dialog', () => { if (typeof appCallbacks.onShellOpenUrlDialog === 'function') appCallbacks.onShellOpenUrlDialog() }) function getRouteQueryValue(name) { try { const hash = window.location.hash || '' const queryText = hash.includes('?') ? hash.slice(hash.indexOf('?') + 1) : window.location.search.slice(1) return new URLSearchParams(queryText).get(name) } catch { return null } } function tryParseJson(value) { try { return value ? JSON.parse(value) : null } catch { return null } } function restoreZcDataFromList() { const id = getRouteQueryValue('id') if (!id) return null const list = tryParseJson(localStorage.getItem('List')) || [] const current = list.find(item => String(item?.idString) === String(id)) if (!current) return null const restored = current.zcName ? list.filter(item => item?.zcName === current.zcName) : [current] if (!restored.length) return null const value = JSON.stringify(restored) try { localStorage.setItem('zcData', value) ipcRenderer.sendSync('set-cache-sync', 'zcData', value) } catch {} return value } function isZcDataUsable(value) { const id = getRouteQueryValue('id') if (!id) return true const data = tryParseJson(value) return Array.isArray(data) && data.some(item => String(item?.idString) === String(id)) } const app = { clearCache() { return ipcRenderer.sendSync('app-clear-cache-sync') }, versionCode() { return ipcRenderer.sendSync('app-version-code-sync') }, shellGetTargetUrl() { return ipcRenderer.invoke('app-shell-get-target-url') }, shellSetTargetUrl(targetUrl) { return ipcRenderer.invoke('app-shell-set-target-url', targetUrl) }, setCache(key, value) { try { if (key != null) localStorage.setItem(String(key), String(value ?? '')) } catch {} return ipcRenderer.sendSync('set-cache-sync', key, value) }, getCache(key) { const value = ipcRenderer.sendSync('get-cache-sync', key) if (value != null) { if (key !== 'zcData' || isZcDataUsable(value)) return value const restored = restoreZcDataFromList() if (restored != null) return restored return value } try { const localValue = localStorage.getItem(String(key)) if (localValue != null) { if (key !== 'zcData' || isZcDataUsable(localValue)) return localValue const restored = restoreZcDataFromList() if (restored != null) return restored return localValue } } catch {} if (key === 'zcData') return restoreZcDataFromList() return null }, offlinePage() { ipcRenderer.send('app-offline-page') }, onlinePage() { ipcRenderer.send('app-online-page') }, exportFile(filename, content, token) { ipcRenderer.send('app-export-file', filename, content, token) }, fileList(dir) { return ipcRenderer.sendSync('app-file-list-sync', dir) }, fileWriteString(filePath, content, token) { ipcRenderer.send('app-file-write-string', filePath, content, token) }, fileReadString(filePath, token) { ipcRenderer.send('app-file-read-string', filePath, token) }, fileDelete(filePath, token) { ipcRenderer.send('app-file-delete', filePath, token) }, get onFileWrite() { return appCallbacks.onFileWrite }, set onFileWrite(fn) { appCallbacks.onFileWrite = fn }, get onFileRead() { return appCallbacks.onFileRead }, set onFileRead(fn) { appCallbacks.onFileRead = fn }, get onFileDelete() { return appCallbacks.onFileDelete }, set onFileDelete(fn) { appCallbacks.onFileDelete = fn }, get onPageModeChanged() { return appCallbacks.onPageModeChanged }, set onPageModeChanged(fn) { appCallbacks.onPageModeChanged = fn }, get onShellOpenUrlDialog() { return appCallbacks.onShellOpenUrlDialog }, set onShellOpenUrlDialog(fn) { appCallbacks.onShellOpenUrlDialog = fn } } const sensorCallbacks = { onSensorAttached: null, onSensorDetached: null, onOperationCallback: null } ipcRenderer.on('sensor-attached', (_event, data) => { if (typeof sensorCallbacks.onSensorAttached === 'function') sensorCallbacks.onSensorAttached(data.id, data.name) }) ipcRenderer.on('sensor-detached', (_event, data) => { if (typeof sensorCallbacks.onSensorDetached === 'function') sensorCallbacks.onSensorDetached(data.id, data.name) }) ipcRenderer.on('sensor-operation-callback', (_event, id, opId, result) => { if (typeof sensorCallbacks.onOperationCallback === 'function') sensorCallbacks.onOperationCallback(id, opId, result) }) const sensor = { initAll() { return ipcRenderer.sendSync('sensor-init-all-sync') }, flush() { return ipcRenderer.sendSync('sensor-flush-sync') }, sensorList() { const data = ipcRenderer.sendSync('sensor-list-sync') return JSON.stringify({ code: 0, data: Array.isArray(data) ? data : [] }) }, init(id) { return ipcRenderer.sendSync('sensor-init-sync', id) }, open(id) { return ipcRenderer.sendSync('sensor-open-sync', id) }, close(id) { return ipcRenderer.sendSync('sensor-close-sync', id) }, operation(id, opId, args) { return ipcRenderer.sendSync('sensor-operation-sync', id, opId, args) }, operationAsync(id, opId, args) { ipcRenderer.send('sensor-operation-async', id, opId, args) }, get onSensorAttached() { return sensorCallbacks.onSensorAttached }, set onSensorAttached(fn) { sensorCallbacks.onSensorAttached = fn }, get onSensorDetached() { return sensorCallbacks.onSensorDetached }, set onSensorDetached(fn) { sensorCallbacks.onSensorDetached = fn }, get onOperationCallback() { return sensorCallbacks.onOperationCallback }, set onOperationCallback(fn) { sensorCallbacks.onOperationCallback = fn } } const appSensorDataArr = [] const nativeAppSensor = { version: '1.0.0', name: 'appSensor', open() { return ipcRenderer.sendSync('app-sensor-open-sync') }, close() { return ipcRenderer.sendSync('app-sensor-close-sync') }, isOpen() { return ipcRenderer.sendSync('app-sensor-is-open-sync') } } ipcRenderer.on('sensor-data', (_event, data) => { if (data && data.sensor && data.method) { const index = appSensorDataArr.findIndex(item => item.sensor === data.sensor && item.method === data.method) if (index >= 0) { appSensorDataArr.splice(index, 1, data) } else { appSensorDataArr.push(data) } } window.postMessage({ __electronSensorData: data }, '*') }) function injectAppSensorBridge() { const source = ` ;(() => { const dataArr = [] const callbacks = { onValueCallback: null } function upsertSensorData(data) { const targetArr = window.appSensor && Array.isArray(window.appSensor.dataArr) ? window.appSensor.dataArr : dataArr if (data && data.sensor && data.method) { const index = targetArr.findIndex(item => item.sensor === data.sensor && item.method === data.method) if (index >= 0) targetArr.splice(index, 1, data) else targetArr.push(data) } if (typeof callbacks.onValueCallback === 'function') callbacks.onValueCallback(data) } Object.defineProperty(window, 'appSensor', { configurable: true, enumerable: true, value: { version: '1.0.0', name: 'appSensor', dataArr, open() { return window.__electronNativeAppSensor.open() }, close() { return window.__electronNativeAppSensor.close() }, isOpen() { return window.__electronNativeAppSensor.isOpen() }, get onValueCallback() { return callbacks.onValueCallback }, set onValueCallback(fn) { callbacks.onValueCallback = fn } } }) window.addEventListener('message', event => { const payload = event.data if (!payload || !payload.__electronSensorData) return upsertSensorData(payload.__electronSensorData) }) })() ` const script = document.createElement('script') script.textContent = source ;(document.documentElement || document.head).appendChild(script) script.remove() } function injectAppSensorBridgeWhenReady() { if (document.documentElement || document.head) { injectAppSensorBridge() return } const timer = setInterval(() => { if (!(document.documentElement || document.head)) return clearInterval(timer) injectAppSensorBridge() }, 0) } injectAppSensorBridgeWhenReady() const appLocationCallbacks = { onOpened: null, onLocationChanged: null } ipcRenderer.on('app-location-opened', (_event, status) => { if (typeof appLocationCallbacks.onOpened === 'function') appLocationCallbacks.onOpened(status) }) ipcRenderer.on('app-location-changed', (_event, location) => { if (typeof appLocationCallbacks.onLocationChanged === 'function') appLocationCallbacks.onLocationChanged(location) }) const appLocation = { isSupport() { return ipcRenderer.sendSync('app-location-is-support-sync') }, isOpen() { return ipcRenderer.sendSync('app-location-is-open-sync') }, open() { ipcRenderer.send('app-location-open') }, close() { ipcRenderer.send('app-location-close') }, get onOpened() { return appLocationCallbacks.onOpened }, set onOpened(fn) { appLocationCallbacks.onOpened = fn }, get onLocationChanged() { return appLocationCallbacks.onLocationChanged }, set onLocationChanged(fn) { appLocationCallbacks.onLocationChanged = fn } } const usbSerialCallbacks = { onDeviceAttached: null, onDeviceDetached: null, onDeviceDiscovered: null, onDeviceProbeResult: null, onDeviceOpened: null, onDeviceState: null, onDeviceData: null } for (const [eventName, cbName] of [ ['usb-callback-device-attached', 'onDeviceAttached'], ['usb-callback-device-detached', 'onDeviceDetached'], ['usb-callback-device-discovered', 'onDeviceDiscovered'], ['usb-callback-device-probe-result', 'onDeviceProbeResult'], ['usb-callback-device-opened', 'onDeviceOpened'], ['usb-callback-device-state', 'onDeviceState'], ['usb-callback-device-data', 'onDeviceData'] ]) { ipcRenderer.on(eventName, (_event, jsonStr) => { let parsed = jsonStr try { if (typeof jsonStr === 'string') parsed = JSON.parse(jsonStr) } catch {} console.log(`[Electron USB] ${cbName}`, parsed) const cb = usbSerialCallbacks[cbName] if (typeof cb === 'function') cb(jsonStr) window.postMessage({ __electronUsbSerialEvent: true, callbackName: cbName, value: jsonStr }, '*') }) } const usbSerial = { get onDeviceAttached() { return usbSerialCallbacks.onDeviceAttached }, set onDeviceAttached(fn) { usbSerialCallbacks.onDeviceAttached = fn }, get onDeviceDetached() { return usbSerialCallbacks.onDeviceDetached }, set onDeviceDetached(fn) { usbSerialCallbacks.onDeviceDetached = fn }, get onDeviceDiscovered() { return usbSerialCallbacks.onDeviceDiscovered }, set onDeviceDiscovered(fn) { usbSerialCallbacks.onDeviceDiscovered = fn }, get onDeviceProbeResult() { return usbSerialCallbacks.onDeviceProbeResult }, set onDeviceProbeResult(fn) { usbSerialCallbacks.onDeviceProbeResult = fn }, get onDeviceOpened() { return usbSerialCallbacks.onDeviceOpened }, set onDeviceOpened(fn) { usbSerialCallbacks.onDeviceOpened = fn }, get onDeviceState() { return usbSerialCallbacks.onDeviceState }, set onDeviceState(fn) { usbSerialCallbacks.onDeviceState = fn }, get onDeviceData() { return usbSerialCallbacks.onDeviceData }, set onDeviceData(fn) { usbSerialCallbacks.onDeviceData = fn }, startDiscovery() { ipcRenderer.send('usb-start-discovery') }, stopDiscovery() { ipcRenderer.send('usb-stop-discovery') }, getDiscoveredDevices() { return ipcRenderer.invoke('usb-get-discovered') }, deviceProbe(deviceName, portNumber, baudRate, hexCmd, timeoutMs) { ipcRenderer.send('usb-device-probe', { deviceName, portNumber, baudRate, hexCmd, timeoutMs }) }, deviceOpen(deviceName, portNumber, baudRate) { ipcRenderer.send('usb-device-open', { deviceName, portNumber, baudRate }) }, deviceClose(deviceName, portNumber) { ipcRenderer.send('usb-device-close', { deviceName, portNumber }) }, deviceWrite(deviceName, portNumber, hexData) { ipcRenderer.send('usb-device-write', { deviceName, portNumber, hexData }) } } function injectUsbSerialBridge() { const source = ` ;(() => { if (window.usbSerial) return const callbacks = { onDeviceAttached: null, onDeviceDetached: null, onDeviceDiscovered: null, onDeviceProbeResult: null, onDeviceOpened: null, onDeviceState: null, onDeviceData: null } Object.defineProperty(window, 'usbSerial', { configurable: true, enumerable: true, value: { get onDeviceAttached() { return callbacks.onDeviceAttached }, set onDeviceAttached(fn) { callbacks.onDeviceAttached = fn }, get onDeviceDetached() { return callbacks.onDeviceDetached }, set onDeviceDetached(fn) { callbacks.onDeviceDetached = fn }, get onDeviceDiscovered() { return callbacks.onDeviceDiscovered }, set onDeviceDiscovered(fn) { callbacks.onDeviceDiscovered = fn }, get onDeviceProbeResult() { return callbacks.onDeviceProbeResult }, set onDeviceProbeResult(fn) { callbacks.onDeviceProbeResult = fn }, get onDeviceOpened() { return callbacks.onDeviceOpened }, set onDeviceOpened(fn) { callbacks.onDeviceOpened = fn }, get onDeviceState() { return callbacks.onDeviceState }, set onDeviceState(fn) { callbacks.onDeviceState = fn }, get onDeviceData() { return callbacks.onDeviceData }, set onDeviceData(fn) { callbacks.onDeviceData = fn }, startDiscovery() { window.__electronNativeUsbSerial.startDiscovery() }, stopDiscovery() { window.__electronNativeUsbSerial.stopDiscovery() }, getDiscoveredDevices() { return window.__electronNativeUsbSerial.getDiscoveredDevices() }, deviceProbe(deviceName, portNumber, baudRate, hexCmd, timeoutMs) { return window.__electronNativeUsbSerial.deviceProbe(deviceName, portNumber, baudRate, hexCmd, timeoutMs) }, deviceOpen(deviceName, portNumber, baudRate) { return window.__electronNativeUsbSerial.deviceOpen(deviceName, portNumber, baudRate) }, deviceClose(deviceName, portNumber) { return window.__electronNativeUsbSerial.deviceClose(deviceName, portNumber) }, deviceWrite(deviceName, portNumber, hexData) { return window.__electronNativeUsbSerial.deviceWrite(deviceName, portNumber, hexData) } } }) window.addEventListener('message', event => { const payload = event.data if (!payload || !payload.__electronUsbSerialEvent) return const cb = callbacks[payload.callbackName] if (typeof cb === 'function') cb(payload.value) }) })() ` const script = document.createElement('script') script.textContent = source ;(document.documentElement || document.head).appendChild(script) script.remove() } function injectUsbSerialBridgeWhenReady() { if (document.documentElement || document.head) { injectUsbSerialBridge() return } const timer = setInterval(() => { if (!(document.documentElement || document.head)) return clearInterval(timer) injectUsbSerialBridge() }, 0) } injectUsbSerialBridgeWhenReady() const bluetoothSppCallbacks = { onDeviceSelected: null, onDeviceOpened: null, onDeviceDataReceived: null, onDeviceStateChanged: null } ipcRenderer.on('bluetooth-device-selected', (_event, success, address) => { if (typeof bluetoothSppCallbacks.onDeviceSelected === 'function') { bluetoothSppCallbacks.onDeviceSelected(success, address) } }) ipcRenderer.on('bluetooth-device-opened', (_event, success, mac) => { if (typeof bluetoothSppCallbacks.onDeviceOpened === 'function') { bluetoothSppCallbacks.onDeviceOpened(success, mac) } }) ipcRenderer.on('bluetooth-device-data-received', (_event, mac, data) => { if (typeof bluetoothSppCallbacks.onDeviceDataReceived === 'function') { bluetoothSppCallbacks.onDeviceDataReceived(mac, data) } }) ipcRenderer.on('bluetooth-device-state-changed', (_event, mac, state) => { if (typeof bluetoothSppCallbacks.onDeviceStateChanged === 'function') { bluetoothSppCallbacks.onDeviceStateChanged(mac, state) } }) const bluetoothSpp = { deviceSelect(namePrefix) { ipcRenderer.send('bluetooth-device-select', namePrefix) }, deviceOpen(mac) { ipcRenderer.send('bluetooth-device-open', mac) }, deviceWrite(mac, data) { ipcRenderer.send('bluetooth-device-write', mac, data) }, deviceClose(mac) { ipcRenderer.send('bluetooth-device-close', mac) }, get onDeviceSelected() { return bluetoothSppCallbacks.onDeviceSelected }, set onDeviceSelected(fn) { bluetoothSppCallbacks.onDeviceSelected = fn }, get onDeviceOpened() { return bluetoothSppCallbacks.onDeviceOpened }, set onDeviceOpened(fn) { bluetoothSppCallbacks.onDeviceOpened = fn }, get onDeviceDataReceived() { return bluetoothSppCallbacks.onDeviceDataReceived }, set onDeviceDataReceived(fn) { bluetoothSppCallbacks.onDeviceDataReceived = fn }, get onDeviceStateChanged() { return bluetoothSppCallbacks.onDeviceStateChanged }, set onDeviceStateChanged(fn) { bluetoothSppCallbacks.onDeviceStateChanged = fn } } contextBridge.exposeInMainWorld('app', app) contextBridge.exposeInMainWorld('net', net) contextBridge.exposeInMainWorld('sensor', sensor) contextBridge.exposeInMainWorld('__electronNativeAppSensor', nativeAppSensor) contextBridge.exposeInMainWorld('appLocation', appLocation) contextBridge.exposeInMainWorld('__electronNativeUsbSerial', usbSerial) contextBridge.exposeInMainWorld('bluetoothSpp', bluetoothSpp)