computer-viewer.mjs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  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, viewerURL, currentTargetURL, approvedClose = false, opening = Promise.resolve()
  5. const trusted = event => event.sender === owner()?.webContents && event.senderFrame === event.sender.mainFrame
  6. && new URL(event.senderFrame.url).origin === origin()
  7. const targetURL = (id, target = {}) => {
  8. if (typeof id !== 'string' || !/^[\w-]{1,128}$/.test(id)) throw new Error('电脑窗口请求无效')
  9. if (!target || typeof target !== 'object' || Array.isArray(target)
  10. || Object.keys(target).some(key => !['backend', 'host'].includes(key))
  11. || (target.backend !== undefined && !['cloud', 'vm', 'managed-browser'].includes(target.backend))
  12. || target.host !== undefined)
  13. throw new Error('所选电脑无效,请重新选择')
  14. const url = new URL(`/conversations/computer/${encodeURIComponent(id)}`, origin())
  15. if (target.backend) url.searchParams.set('backend', target.backend)
  16. return url.href
  17. }
  18. ipcMain.handle('desktop-computer:open', (event, id, target) => {
  19. if (!trusted(event)) throw new Error('电脑窗口请求无效')
  20. const requestedURL = targetURL(id, target)
  21. const pending = opening.catch(() => {}).then(() => open(requestedURL))
  22. opening = pending
  23. return pending
  24. })
  25. async function open(url) {
  26. if (quitting()) return false
  27. if (viewer && !viewer.isDestroyed()) {
  28. if (currentTargetURL === url) { viewer.show(); viewer.focus(); return true }
  29. // The old page must return its lease before another target can open.
  30. const previous = viewer
  31. await new Promise((resolve, reject) => {
  32. const closed = () => { clearTimeout(timeout); resolve() }
  33. const timeout = setTimeout(() => {
  34. previous.removeListener('closed', closed)
  35. if (!previous.isDestroyed()) { previous.show(); previous.focus() }
  36. reject(new Error('当前电脑尚未交还,请在电脑窗口交还并关闭后重试'))
  37. }, 15000)
  38. previous.once('closed', closed)
  39. previous.close()
  40. })
  41. }
  42. if (quitting()) return false
  43. viewerURL = url; currentTargetURL = url; approvedClose = false
  44. const current = viewer = new BrowserWindow({ parent: owner(), modal: false, width: 1220, height: 820,
  45. minWidth: 760, minHeight: 520, show: false, title: '电脑接管 · 夭夭 AI', backgroundColor: '#111315', autoHideMenuBar: true,
  46. webPreferences: { preload, sandbox: true, contextIsolation: true, nodeIntegration: false, webSecurity: true } })
  47. current.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
  48. current.webContents.on('will-navigate', (event, target) => { if (target !== url) event.preventDefault() })
  49. current.webContents.on('will-redirect', event => event.preventDefault())
  50. current.webContents.on('will-attach-webview', event => event.preventDefault())
  51. current.on('close', event => {
  52. if (!approvedClose && !quitting() && !current.webContents.isCrashed()) {
  53. event.preventDefault(); current.webContents.send('desktop-computer:request-close')
  54. }
  55. })
  56. current.on('closed', () => { if (viewer === current) { viewer = undefined; viewerURL = undefined; currentTargetURL = undefined } })
  57. current.once('ready-to-show', () => { current.show(); current.focus() })
  58. try { await current.loadURL(url) } catch (error) { approvedClose = true; current.close(); throw error }
  59. return true
  60. }
  61. ipcMain.handle('desktop-computer:target', (event, target) => {
  62. if (event.sender !== viewer?.webContents || event.senderFrame !== event.sender.mainFrame
  63. || event.senderFrame.url !== viewerURL) throw new Error('电脑窗口身份无效')
  64. currentTargetURL = targetURL(decodeURIComponent(new URL(viewerURL).pathname.split('/').at(-1)), target)
  65. })
  66. ipcMain.handle('desktop-computer:closed', event => {
  67. if (event.sender !== viewer?.webContents || event.senderFrame !== event.sender.mainFrame
  68. || event.senderFrame.url !== viewerURL) throw new Error('电脑窗口身份无效')
  69. approvedClose = true; viewer.close()
  70. })
  71. const close = () => { approvedClose = true; viewer?.destroy() }
  72. close.owns = contents => contents === viewer?.webContents
  73. return close
  74. }