diff --git a/package-lock.json b/package-lock.json index 7b01687..fb9d795 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "await-to-js": "^3.0.0", "axios": "^1.13.2", + "bonjour-service": "^1.3.0", "electron-squirrel-startup": "^1.0.1", "element-plus": "^2.13.6", "pinia": "^3.0.4", @@ -2524,6 +2525,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.22.tgz", @@ -4350,6 +4357,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5281,6 +5298,18 @@ "node": ">=8" } }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -6834,7 +6863,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -9203,6 +9231,19 @@ "dev": true, "license": "MIT" }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, "node_modules/mute-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", @@ -10995,6 +11036,12 @@ "dev": true, "license": "MIT" }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT" + }, "node_modules/tiny-each-async": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/tiny-each-async/-/tiny-each-async-2.0.3.tgz", diff --git a/package.json b/package.json index 92d2c8b..b1df50e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "dependencies": { "await-to-js": "^3.0.0", "axios": "^1.13.2", + "bonjour-service": "^1.3.0", "electron-squirrel-startup": "^1.0.1", "element-plus": "^2.13.6", "pinia": "^3.0.4", diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 2139ad0..0000000 --- a/src/main.js +++ /dev/null @@ -1,56 +0,0 @@ -import { app, BrowserWindow } from 'electron' -import path from 'node:path' -import started from 'electron-squirrel-startup' - -// Handle creating/removing shortcuts on Windows when installing/uninstalling. -if (started) { - app.quit() -} - -const createWindow = () => { - // Create the browser window. - const mainWindow = new BrowserWindow({ - width: 800, - height: 600, - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - }, - }) - - // and load the index.html of the app. - if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { - mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL) - } else { - mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)) - } - - // Open the DevTools. - mainWindow.webContents.openDevTools() -} - -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. -app.whenReady().then(() => { - createWindow() - - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow() - } - }) -}) - -// Quit when all windows are closed, except on macOS. There, it's common -// for applications and their menu bar to stay active until the user quits -// explicitly with Cmd + Q. -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit() - } -}) - -// In this file you can include the rest of your app's specific main process -// code. You can also put them in separate files and import them here. diff --git a/src/main/index.js b/src/main/index.js index b3924f1..1e2eaf5 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -4,9 +4,13 @@ import fs from 'node:fs' import net from 'node:net' import { spawn } from 'node:child_process' import started from 'electron-squirrel-startup' +import Bonjour from 'bonjour-service' if (started) app.quit() +const bonjour = new Bonjour() +const devices = new Map() + // ========== OpenCode 服务管理 ========== const DEFAULT_PORT = 4096 let opencodeProcess = null @@ -164,6 +168,25 @@ function registerIpcHandlers() { if (!res.ok) throw new Error(`发送消息失败: ${res.status}`) return res.json() }) + + // ========== Bonjour 设备发现 IPC ========== + ipcMain.handle('get-devices', () => { + return Array.from(devices.values()) + }) + + ipcMain.on('refresh-devices', () => { + // 重新扫描逻辑 + // const allWindows = BrowserWindow.getAllWindows(); + // const mainWindow = allWindows[0]; // 假设第一个是主窗口 + + // 停止并重新开始搜索 + // 注意:这里需要访问 browser 实例,我们可以在 registerIpcHandlers 外层定义或在应用启动时初始化 + if (global.bonjourBrowser) { + global.bonjourBrowser.stop() + devices.clear() + global.bonjourBrowser.start() + } + }) } // ========== 窗口 ========== @@ -184,6 +207,32 @@ const createWindow = () => { mainWindow.once('ready-to-show', () => mainWindow.show()) + // Setup Bonjour discovery + const browser = bonjour.find({}) + global.bonjourBrowser = browser + + browser.on('up', (service) => { + console.log('Found device:', service.name) + const device = { + id: service.fqdn, + name: service.name, + type: service.type, + port: service.port, + addresses: service.addresses, + txt: service.txt, + host: service.host, + referer: service.referer, + } + devices.set(device.id, device) + mainWindow.webContents.send('device-found', device) + }) + + browser.on('down', (service) => { + console.log('Lost device:', service.name) + devices.delete(service.fqdn) + mainWindow.webContents.send('device-lost', service.fqdn) + }) + mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url) return { action: 'deny' } diff --git a/src/preload.js b/src/preload.js deleted file mode 100644 index 5e9d369..0000000 --- a/src/preload.js +++ /dev/null @@ -1,2 +0,0 @@ -// See the Electron documentation for details on how to use preload scripts: -// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts diff --git a/src/preload/index.js b/src/preload/index.js index b009f18..94ae8a0 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -5,6 +5,12 @@ contextBridge.exposeInMainWorld('electronAPI', { send: (channel, data) => ipcRenderer.send(channel, data), on: (channel, callback) => ipcRenderer.on(channel, (_event, ...args) => callback(...args)), invoke: (channel, data) => ipcRenderer.invoke(channel, data), + + // Bonjour 设备发现 API + onDeviceFound: (callback) => ipcRenderer.on('device-found', (_event, value) => callback(value)), + onDeviceLost: (callback) => ipcRenderer.on('device-lost', (_event, value) => callback(value)), + getDevices: () => ipcRenderer.invoke('get-devices'), + refreshDevices: () => ipcRenderer.send('refresh-devices'), }) contextBridge.exposeInMainWorld('opencode', { diff --git a/src/renderer/layouts/DefaultLayout.vue b/src/renderer/layouts/DefaultLayout.vue index a8aa44f..5b619e0 100644 --- a/src/renderer/layouts/DefaultLayout.vue +++ b/src/renderer/layouts/DefaultLayout.vue @@ -20,6 +20,10 @@ + + + + diff --git a/src/renderer/router/index.js b/src/renderer/router/index.js index 9c09f78..812f3a2 100644 --- a/src/renderer/router/index.js +++ b/src/renderer/router/index.js @@ -17,6 +17,12 @@ const routes = [ component: () => import('@/views/chat/ChatView.vue'), meta: { title: 'OpenCode 对话' }, }, + { + path: '/devices', + name: 'Devices', + component: () => import('@/views/devices/DevicesView.vue'), + meta: { title: '发现设备' }, + }, ], }, ] diff --git a/src/renderer/views/devices/DevicesView.vue b/src/renderer/views/devices/DevicesView.vue new file mode 100644 index 0000000..fc8d0bd --- /dev/null +++ b/src/renderer/views/devices/DevicesView.vue @@ -0,0 +1,109 @@ + + + + +