computer-viewer.mjs 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142
  1. import { BrowserWindow, ipcMain } from 'electron'
  2. /** Local authenticated viewer. Agent ownership and control leases remain server-authoritative. */
  3. export function installComputerViewer({ owner, origin, preload, quitting = () => false }) {
  4. let viewer, agentId, approvedClose = false
  5. const trusted = event => event.sender === owner()?.webContents && event.senderFrame === event.sender.mainFrame
  6. && new URL(event.senderFrame.url).origin === origin()
  7. ipcMain.handle('desktop-computer:open', async (event, id) => {
  8. if (!trusted(event) || typeof id !== 'string' || !/^[\w-]{1,128}$/.test(id)) throw new Error('电脑窗口请求无效')
  9. if (viewer && !viewer.isDestroyed()) {
  10. viewer.show(); viewer.focus()
  11. if (agentId !== id) throw new Error('请先交还并关闭当前电脑,再接管另一台电脑')
  12. return true
  13. }
  14. const url = new URL(`/conversations/computer/${encodeURIComponent(id)}`, origin()).href
  15. agentId = id; approvedClose = false
  16. const current = viewer = new BrowserWindow({ parent: owner(), modal: false, width: 1220, height: 820,
  17. minWidth: 760, minHeight: 520, show: false, title: '电脑接管 · 夭夭 AI', backgroundColor: '#111315', autoHideMenuBar: true,
  18. webPreferences: { preload, sandbox: true, contextIsolation: true, nodeIntegration: false, webSecurity: true } })
  19. current.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
  20. current.webContents.on('will-navigate', (event, target) => { if (target !== url) event.preventDefault() })
  21. current.webContents.on('will-redirect', event => event.preventDefault())
  22. current.webContents.on('will-attach-webview', event => event.preventDefault())
  23. current.on('close', event => {
  24. if (!approvedClose && !quitting() && !current.webContents.isCrashed()) {
  25. event.preventDefault(); current.webContents.send('desktop-computer:request-close')
  26. }
  27. })
  28. current.on('closed', () => { if (viewer === current) { viewer = undefined; agentId = undefined } })
  29. current.once('ready-to-show', () => { current.show(); current.focus() })
  30. try { await current.loadURL(url) } catch (error) { approvedClose = true; current.close(); throw error }
  31. return true
  32. })
  33. ipcMain.handle('desktop-computer:closed', event => {
  34. if (event.sender !== viewer?.webContents || event.senderFrame !== event.sender.mainFrame
  35. || new URL(event.senderFrame.url).pathname !== `/conversations/computer/${agentId}`) throw new Error('电脑窗口身份无效')
  36. approvedClose = true; viewer.close()
  37. })
  38. const close = () => { approvedClose = true; viewer?.destroy() }
  39. close.owns = contents => contents === viewer?.webContents
  40. return close
  41. }