Просмотр исходного кода

feat(desktop-host): 新增"仅作为桌面主机运行"模式

桌面主机菜单可勾选:不启动本机 Web 服务与本机回环通道,只连接
远程夭夭服务器(卫星机形态)。显示窗口时给出说明而非空等本机服务;
取消勾选即恢复启动本机服务。默认关闭,现有行为不变。
Samien 1 месяц назад
Родитель
Сommit
3d62c523b1
4 измененных файлов с 60 добавлено и 13 удалено
  1. 1 1
      desktop/lifecycle.test.mjs
  2. 37 5
      desktop/main.mjs
  3. 6 4
      desktop/preferences.mjs
  4. 16 3
      desktop/preferences.test.mjs

+ 1 - 1
desktop/lifecycle.test.mjs

@@ -92,7 +92,7 @@ test('real desktop launches its bundled service, survives window close and recov
     assert.equal(updateStatus.installationMode,'desktop');assert.equal(updateStatus.supported,false)
     assert.equal(await desktop.evaluate(({Menu})=>Menu.getApplicationMenu().getMenuItemById('desktop-background').checked),false)
     await desktop.evaluate(async({Menu})=>{const item=Menu.getApplicationMenu().getMenuItemById('desktop-background');await item.click(item)})
-    assert.deepEqual(await until(async()=>JSON.parse(await readFile(join(home,'desktop-preferences.json'),'utf8')),value=>value.backgroundAtLogin===true,5000),{backgroundAtLogin:true})
+    assert.deepEqual(await until(async()=>JSON.parse(await readFile(join(home,'desktop-preferences.json'),'utf8')),value=>value.backgroundAtLogin===true,5000),{backgroundAtLogin:true,hostOnly:false})
     const external = new DesktopServiceManager({ home, port, fork: () => { throw new Error('must not fork a second service') } })
     assert.equal((await external.start()).external, true)
     await external.stop()

+ 37 - 5
desktop/main.mjs

@@ -101,6 +101,12 @@ else {
 
   function show() {
     if (closing || quitting) return
+    if (preferences.value.hostOnly) {
+      if (window && !window.isDestroyed()) { window.show(); window.focus(); return }
+      void dialog.showMessageBox({ type: 'info', title: '夭夭', message: '仅桌面主机模式',
+        detail: '这台 Mac 正在作为远程桌面主机运行,本机 Web 服务未启动。如需使用本机服务,请打开菜单「桌面主机」,取消勾选「仅作为桌面主机运行」。' })
+      return
+    }
     if (!window || window.isDestroyed()) createWindow()
     window.show(); window.focus()
     if (manager) void manager.reconnect().then(() => stateChanged(manager.state)).catch(error => log(error.message))
@@ -260,6 +266,26 @@ else {
       const selection=await dialog.showOpenDialog(window,{title:'导入桌面主机配置',properties:['openFile'],filters:[{name:'桌面主机配置',extensions:['json']}]})
       if(!selection.canceled&&selection.filePaths[0]&&!closing)await hostAction(()=>hostManager.importFile(selection.filePaths[0]))
     }
+    async function toggleHostOnly(item){
+      const enabled=item.checked
+      try{
+        await preferences.setHostOnly(enabled)
+        if(enabled){
+          await environmentHost?.close().catch(error=>log(error.message))
+          await runnerManager?.stop().catch(error=>log(error.message))
+          await manager?.stop().catch(error=>log(error.message))
+          if(window&&!window.isDestroyed())window.hide()
+          await dialog.showMessageBox({type:'info',title:'夭夭',message:'已切换到仅桌面主机模式',detail:'本机 Web 服务已停止,这台 Mac 将只通过「桌面主机」连接远程夭夭服务器。需要本机服务时,请取消勾选。'})
+        }else{
+          await manager?.start().catch(error=>log(error.message))
+          if(!closing)environmentHost?.start()
+          await dialog.showMessageBox({type:'info',title:'夭夭',message:'已恢复本机 Web 服务',detail:'本机夭夭服务正在启动,稍后点按「显示窗口」即可使用。'})
+        }
+      }catch(error){
+        item.checked=preferences.value.hostOnly
+        await dialog.showMessageBox(window,{type:'error',message:'无法切换仅桌面主机模式',detail:error.message})
+      }
+    }
     async function importRunner(){
       const selection=await dialog.showOpenDialog(window,{title:'导入执行节点配置',properties:['openFile'],filters:[{name:'Runner 配置',extensions:['json']}]})
       if(!selection.canceled&&selection.filePaths[0]&&!closing)await runnerAction(()=>runnerManager.importFile(selection.filePaths[0]))
@@ -294,7 +320,9 @@ else {
       {label:'桌面主机',submenu:[{id:'desktop-host-status',label:'未配置桌面主机',enabled:false},{type:'separator'},
         {id:'desktop-host-import',label:'导入主机配置…',click:importDesktopHost},
         {id:'desktop-host-reconnect',label:'重新连接',click:()=>hostAction(()=>hostManager.start())},
-        {id:'desktop-host-forget',label:'断开并忘记配置',click:()=>hostAction(()=>hostManager.forget())}]},
+        {id:'desktop-host-forget',label:'断开并忘记配置',click:()=>hostAction(()=>hostManager.forget())},{type:'separator'},
+        {id:'desktop-host-only',label:'仅作为桌面主机运行',type:'checkbox',checked:preferences.value.hostOnly,
+          toolTip:'不启动本机 Web 服务,只连接远程夭夭服务器',click:toggleHostOnly}]},
       { role: 'editMenu', label: '编辑' }, { role: 'viewMenu', label: '显示' }, { role: 'windowMenu', label: '窗口' },
     ]))
     const icon = nativeImage.createFromPath(join(desktopRoot, 'icon.png')).resize({ width: 20, height: 20 })
@@ -303,12 +331,16 @@ else {
     tray.setToolTip('夭夭')
     tray.setContextMenu(Menu.buildFromTemplate([{ label: '打开夭夭', click: show }, { label: '查看日志', click: () => shell.showItemInFolder(logFile) }, { type: 'separator' }, { label: '退出夭夭(后台继续运行)', click: () => requestQuit() }, { label: '停止后台服务并退出', click: () => requestQuit(true) }]))
     tray.on('click', show)
-    createWindow()
+    if(!preferences.value.hostOnly)createWindow()
     powerMonitor.on('resume', () => { if(!closing&&!quitting)void manager.check() })
     timer = setInterval(() => { void manager.check() }, 10_000); timer.unref()
-    await manager.start().catch(error => log(error.message))
-    if(!closing)environmentHost.start()
+    if(preferences.value.hostOnly){
+      log('仅桌面主机模式:不启动本机 Web 服务')
+    }else{
+      await manager.start().catch(error => log(error.message))
+      if(!closing)environmentHost.start()
+    }
     if(!closing)hostManager.start().catch(error=>{hostManager.publish(error.message)})
-    if(!closing&&manager.state.phase==='ready')await runnerManager.start().catch(error=>{runnerManager.publish(error.message)})
+    if(!closing&&!preferences.value.hostOnly&&manager.state.phase==='ready')await runnerManager.start().catch(error=>{runnerManager.publish(error.message)})
   }).catch(error => { log(error.stack || error.message); quitting = true; app.quit() })
 }

+ 6 - 4
desktop/preferences.mjs

@@ -3,11 +3,13 @@ import {join} from 'node:path'
 import {randomUUID} from 'node:crypto'
 import {homedir} from 'node:os'
 export class DesktopPreferences {
-  constructor(home){this.home=home;this.path=join(home,'desktop-preferences.json');this.value={backgroundAtLogin:false}}
-  async load(){try{let source;try{source=await readFile(this.path,'utf8')}catch(error){if(error.code!=='ENOENT'||this.home!==join(homedir(),'.yaoyao'))throw error;source=await readFile(join(homedir(),'.hermes-yaoyao','desktop-preferences.json'),'utf8')}const value=JSON.parse(source);this.value={backgroundAtLogin:value.backgroundAtLogin===true}}catch(error){if(error.code!=='ENOENT')throw new Error('无法读取桌面偏好设置')}return this.value}
-  async setBackgroundAtLogin(enabled){
-    const value={backgroundAtLogin:enabled===true},temporary=this.path+'.'+randomUUID()
+  constructor(home){this.home=home;this.path=join(home,'desktop-preferences.json');this.value={backgroundAtLogin:false,hostOnly:false}}
+  async load(){try{let source;try{source=await readFile(this.path,'utf8')}catch(error){if(error.code!=='ENOENT'||this.home!==join(homedir(),'.yaoyao'))throw error;source=await readFile(join(homedir(),'.hermes-yaoyao','desktop-preferences.json'),'utf8')}const value=JSON.parse(source);this.value={backgroundAtLogin:value.backgroundAtLogin===true,hostOnly:value.hostOnly===true}}catch(error){if(error.code!=='ENOENT')throw new Error('无法读取桌面偏好设置')}return this.value}
+  async write(value){
+    const temporary=this.path+'.'+randomUUID()
     await mkdir(this.home,{recursive:true,mode:0o700})
     try{await writeFile(temporary,JSON.stringify(value),{flag:'wx',mode:0o600});await rename(temporary,this.path);this.value=value}finally{await rm(temporary,{force:true})}
   }
+  async setBackgroundAtLogin(enabled){await this.write({...this.value,backgroundAtLogin:enabled===true})}
+  async setHostOnly(enabled){await this.write({...this.value,hostOnly:enabled===true})}
 }

+ 16 - 3
desktop/preferences.test.mjs

@@ -8,11 +8,24 @@ test('background login remains opt-in and persists without changing system login
   const home=await mkdtemp(join(tmpdir(),'yaoyao-desktop-preferences-'))
   try{
     const preferences=new DesktopPreferences(home)
-    assert.deepEqual(await preferences.load(),{backgroundAtLogin:false})
+    assert.deepEqual(await preferences.load(),{backgroundAtLogin:false,hostOnly:false})
     await preferences.setBackgroundAtLogin(true)
-    assert.deepEqual(await new DesktopPreferences(home).load(),{backgroundAtLogin:true})
+    assert.deepEqual(await new DesktopPreferences(home).load(),{backgroundAtLogin:true,hostOnly:false})
     assert.equal((await stat(preferences.path)).mode&0o777,0o600)
     await preferences.setBackgroundAtLogin(false)
-    assert.deepEqual(await new DesktopPreferences(home).load(),{backgroundAtLogin:false})
+    assert.deepEqual(await new DesktopPreferences(home).load(),{backgroundAtLogin:false,hostOnly:false})
+  }finally{await rm(home,{recursive:true,force:true})}
+})
+test('host-only mode persists independently of background login',async()=>{
+  const home=await mkdtemp(join(tmpdir(),'yaoyao-desktop-preferences-'))
+  try{
+    const preferences=new DesktopPreferences(home)
+    await preferences.load()
+    await preferences.setHostOnly(true)
+    assert.deepEqual(await new DesktopPreferences(home).load(),{backgroundAtLogin:false,hostOnly:true})
+    await preferences.setBackgroundAtLogin(true)
+    assert.deepEqual(await new DesktopPreferences(home).load(),{backgroundAtLogin:true,hostOnly:true})
+    await preferences.setHostOnly(false)
+    assert.deepEqual(await new DesktopPreferences(home).load(),{backgroundAtLogin:true,hostOnly:false})
   }finally{await rm(home,{recursive:true,force:true})}
 })