|
@@ -6,28 +6,43 @@ import {randomUUID} from 'node:crypto'
|
|
|
import {request} from 'node:http'
|
|
import {request} from 'node:http'
|
|
|
import {execFile} from 'node:child_process'
|
|
import {execFile} from 'node:child_process'
|
|
|
import {BrowserEnvironment} from './browser-environment.mjs'
|
|
import {BrowserEnvironment} from './browser-environment.mjs'
|
|
|
|
|
+import {listHostFiles,readHostFile,writeHostFile,execHostShell} from './host-files.mjs'
|
|
|
|
|
|
|
|
function exchange(record,body){return new Promise((resolve,reject)=>{const url=new URL('/desktop/environment',record.url);if(url.protocol!=='http:'||url.hostname!=='127.0.0.1')return reject(new Error('桌面服务地址无效'));const bytes=Buffer.from(JSON.stringify(body)),req=request(url,{method:'POST',headers:{'x-yaoyao-desktop-token':record.token,'content-type':'application/json','content-length':bytes.length}},res=>{let size=0;const chunks=[];res.on('data',chunk=>{size+=chunk.length;if(size>256*1024)res.destroy(new Error('桌面命令超过限制'));else chunks.push(chunk)});res.on('error',reject);res.on('end',()=>{try{if(res.statusCode!==200)throw new Error('桌面服务未连接');resolve(JSON.parse(Buffer.concat(chunks)))}catch(error){reject(error)}})});req.on('error',reject);req.setTimeout(5000,()=>req.destroy(new Error('桌面连接超时')));req.end(bytes)})}
|
|
function exchange(record,body){return new Promise((resolve,reject)=>{const url=new URL('/desktop/environment',record.url);if(url.protocol!=='http:'||url.hostname!=='127.0.0.1')return reject(new Error('桌面服务地址无效'));const bytes=Buffer.from(JSON.stringify(body)),req=request(url,{method:'POST',headers:{'x-yaoyao-desktop-token':record.token,'content-type':'application/json','content-length':bytes.length}},res=>{let size=0;const chunks=[];res.on('data',chunk=>{size+=chunk.length;if(size>256*1024)res.destroy(new Error('桌面命令超过限制'));else chunks.push(chunk)});res.on('error',reject);res.on('end',()=>{try{if(res.statusCode!==200)throw new Error('桌面服务未连接');resolve(JSON.parse(Buffer.concat(chunks)))}catch(error){reject(error)}})});req.on('error',reject);req.setTimeout(5000,()=>req.destroy(new Error('桌面连接超时')));req.end(bytes)})}
|
|
|
|
|
|
|
|
/** Command execution shared by the loopback host and remote desktop hosts. */
|
|
/** Command execution shared by the loopback host and remote desktop hosts. */
|
|
|
export class DesktopHostCore {
|
|
export class DesktopHostCore {
|
|
|
- constructor({root,dataRoot}){this.root=root;this.dataRoot=dataRoot;this.id=randomUUID();this.results=[];this.running=new Set();this.closed=false;this.browser=new BrowserEnvironment(join(dataRoot,'bot-browsers'));this.approved=new Set();this.permissionsFile=join(dataRoot,'computer-permissions.json');try{const list=JSON.parse(readFileSync(this.permissionsFile,'utf8'));if(Array.isArray(list))for(const key of list)if(/^[a-f0-9]{64}$/.test(key))this.approved.add(key)}catch{}this.frames=new Map()}
|
|
|
|
|
- info(){return {id:this.id,name:hostname(),platform:process.platform,screen:process.platform==='darwin'&&systemPreferences.getMediaAccessStatus('screen')==='granted',accessibility:process.platform==='darwin'&&systemPreferences.isTrustedAccessibilityClient(false),approved:[...this.approved]}}
|
|
|
|
|
- async revoke(){this.approved.clear();this.save();this.frames.clear();this.id=randomUUID()}
|
|
|
|
|
|
|
+ constructor({root,dataRoot}){this.root=root;this.dataRoot=dataRoot;this.home=process.env.HOME||'/';this.id=randomUUID();this.results=[];this.running=new Set();this.closed=false;this.browser=new BrowserEnvironment(join(dataRoot,'bot-browsers'));this.approved=new Set();this.approvedFull=new Set();this.permissionsFile=join(dataRoot,'computer-permissions.json');try{const stored=JSON.parse(readFileSync(this.permissionsFile,'utf8'));const screen=Array.isArray(stored)?stored:stored.screen;const full=Array.isArray(stored)?[]:stored.full;if(Array.isArray(screen))for(const key of screen)if(/^[a-f0-9]{64}$/.test(key))this.approved.add(key);if(Array.isArray(full))for(const key of full)if(/^[a-f0-9]{64}$/.test(key))this.approvedFull.add(key)}catch{}this.frames=new Map()}
|
|
|
|
|
+ info(){return {id:this.id,name:hostname(),platform:process.platform,screen:process.platform==='darwin'&&systemPreferences.getMediaAccessStatus('screen')==='granted',accessibility:process.platform==='darwin'&&systemPreferences.isTrustedAccessibilityClient(false),approved:[...this.approved],full:[...this.approvedFull]}}
|
|
|
|
|
+ async revoke(){this.approved.clear();this.approvedFull.clear();this.save();this.frames.clear();this.id=randomUUID()}
|
|
|
async recycle(){this.results=[];this.id=randomUUID();await this.browser.close()}
|
|
async recycle(){this.results=[];this.id=randomUUID();await this.browser.close()}
|
|
|
- save(){mkdirSync(this.dataRoot,{recursive:true,mode:0o700});writeFileSync(this.permissionsFile,JSON.stringify([...this.approved]),{mode:0o600})}
|
|
|
|
|
|
|
+ save(){mkdirSync(this.dataRoot,{recursive:true,mode:0o700});writeFileSync(this.permissionsFile,JSON.stringify({screen:[...this.approved],full:[...this.approvedFull]}),{mode:0o600})}
|
|
|
takeResults(){const sent=this.results.slice();this.results.splice(0,sent.length);return sent}
|
|
takeResults(){const sent=this.results.slice();this.results.splice(0,sent.length);return sent}
|
|
|
async handle(commands){for(const c of commands??[]){if(this.running.has(c.id))continue;this.running.add(c.id);const epoch=this.id;void this.execute(c).then(value=>{if(epoch===this.id)this.results.push({id:c.id,value})},error=>{if(epoch===this.id)this.results.push({id:c.id,error:String(error.message).slice(0,500)})}).finally(()=>this.running.delete(c.id))}}
|
|
async handle(commands){for(const c of commands??[]){if(this.running.has(c.id))continue;this.running.add(c.id);const epoch=this.id;void this.execute(c).then(value=>{if(epoch===this.id)this.results.push({id:c.id,value})},error=>{if(epoch===this.id)this.results.push({id:c.id,error:String(error.message).slice(0,500)})}).finally(()=>this.running.delete(c.id))}}
|
|
|
async execute(c){const epoch=this.id;if(Date.now()>c.deadline||this.closed)throw new Error('桌面请求已过期');if(!/^[a-f0-9]{64}$/.test(c.owner))throw new Error('桌面请求身份无效')
|
|
async execute(c){const epoch=this.id;if(Date.now()>c.deadline||this.closed)throw new Error('桌面请求已过期');if(!/^[a-f0-9]{64}$/.test(c.owner))throw new Error('桌面请求身份无效')
|
|
|
if(c.operation==='authorize'){
|
|
if(c.operation==='authorize'){
|
|
|
if(process.platform!=='darwin')throw new Error('此电脑暂不支持本机控制')
|
|
if(process.platform!=='darwin')throw new Error('此电脑暂不支持本机控制')
|
|
|
- const {response}=await dialog.showMessageBox({type:'question',title:'授权本机控制',message:`允许夭夭账号「${String(c.account??'当前账号').slice(0,100)}」的机器人查看和操作这台 Mac?`,detail:'授权后,机器人和该账号的 Web、手机端可以查看屏幕,并在接管时操作鼠标和键盘。你可以在夭夭菜单中撤销。',buttons:['取消','允许并检查系统权限'],defaultId:0,cancelId:0})
|
|
|
|
|
- if(response!==1||Date.now()>c.deadline||this.closed||epoch!==this.id)throw new Error('本机控制授权已取消')
|
|
|
|
|
- this.approved.add(c.owner);this.save()
|
|
|
|
|
|
|
+ const full=c.scope==='full'
|
|
|
|
|
+ const {response}=await dialog.showMessageBox({type:'question',title:full?'授权文件与命令':'授权本机控制',
|
|
|
|
|
+ message:`允许夭夭账号「${String(c.account??'当前账号').slice(0,100)}」的机器人${full?'使用这台 Mac 的文件并执行命令':'查看和操作这台 Mac'}?`,
|
|
|
|
|
+ detail:full?'机器人可以列出、读取和写入你用户主目录内的文件,并在这台 Mac 上执行 shell 命令。仅对信任的机器人开启;可随时在夭夭菜单中撤销。':'授权后,机器人和该账号的 Web、手机端可以查看屏幕,并在接管时操作鼠标和键盘。你可以在夭夭菜单中撤销。',
|
|
|
|
|
+ buttons:full?['取消','允许文件与命令']:['取消','允许并检查系统权限'],defaultId:0,cancelId:0})
|
|
|
|
|
+ if(response!==1||Date.now()>c.deadline||this.closed||epoch!==this.id)throw new Error(full?'文件与命令授权已取消':'本机控制授权已取消')
|
|
|
|
|
+ this.approved.add(c.owner);if(full)this.approvedFull.add(c.owner);this.save()
|
|
|
if(!systemPreferences.isTrustedAccessibilityClient(false))systemPreferences.isTrustedAccessibilityClient(true)
|
|
if(!systemPreferences.isTrustedAccessibilityClient(false))systemPreferences.isTrustedAccessibilityClient(true)
|
|
|
if(systemPreferences.getMediaAccessStatus('screen')!=='granted'){await desktopCapturer.getSources({types:['screen'],thumbnailSize:{width:1,height:1}}).catch(()=>{});if(systemPreferences.getMediaAccessStatus('screen')!=='granted')await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture')}
|
|
if(systemPreferences.getMediaAccessStatus('screen')!=='granted'){await desktopCapturer.getSources({types:['screen'],thumbnailSize:{width:1,height:1}}).catch(()=>{});if(systemPreferences.getMediaAccessStatus('screen')!=='granted')await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture')}
|
|
|
return {ok:true}
|
|
return {ok:true}
|
|
|
}
|
|
}
|
|
|
|
|
+ if(c.operation==='file'||c.operation==='shell'){
|
|
|
|
|
+ if(process.platform!=='darwin')throw new Error('此电脑暂不支持文件与命令')
|
|
|
|
|
+ if(!this.approvedFull.has(c.owner))throw new Error('请在桌面端授权文件与命令访问')
|
|
|
|
|
+ if(c.operation==='shell')return execHostShell(this.home,c.action)
|
|
|
|
|
+ const action=c.action??{}
|
|
|
|
|
+ if(action.op==='list')return listHostFiles(this.home,action.path)
|
|
|
|
|
+ if(action.op==='read')return readHostFile(this.home,action.path)
|
|
|
|
|
+ if(action.op==='write')return writeHostFile(this.home,action)
|
|
|
|
|
+ throw new Error('文件操作无效')
|
|
|
|
|
+ }
|
|
|
if(!['local','browser'].includes(c.mode)||!['persistent','temporary'].includes(c.profile)||!['open','view','input','browser','browser-state'].includes(c.operation))throw new Error('桌面命令无效')
|
|
if(!['local','browser'].includes(c.mode)||!['persistent','temporary'].includes(c.profile)||!['open','view','input','browser','browser-state'].includes(c.operation))throw new Error('桌面命令无效')
|
|
|
if(c.mode==='browser'){
|
|
if(c.mode==='browser'){
|
|
|
if(!/^[a-f0-9]{64}$/.test(c.resource))throw new Error('浏览器资源无效')
|
|
if(!/^[a-f0-9]{64}$/.test(c.resource))throw new Error('浏览器资源无效')
|