2 Commity 216e4a8ea9 ... d4e497a052

Autor SHA1 Wiadomość Data
  yichael d4e497a052 预览功能 2 tygodni temu
  yichael 9c1f6d726d 激活手机功能 2 tygodni temu
55 zmienionych plików z 301 dodań i 213 usunięć
  1. 60 0
      doc/打包流程.md
  2. 48 41
      electron/main.js
  3. 1 0
      electron/preload.js
  4. 2 1
      lib/scrcpy-adb/scrcpy-console.bat
  5. 9 0
      lib/scrcpy-adb/scrcpy-noconsole-debug.bat
  6. 3 0
      lib/scrcpy-adb/scrcpy-noconsole.bat
  7. 8 3
      lib/scrcpy-adb/scrcpy-noconsole.vbs
  8. 0 53
      nodejs/adb/scan-connect-ip.js
  9. 55 49
      nodejs/adb/screenshot.js
  10. 57 0
      nodejs/enable-wirless-connect.js
  11. 0 0
      package/package-arm64.bat
  12. 0 0
      package/package-arm64.js
  13. 0 0
      package/package-x64.bat
  14. 0 0
      package/package-x64.js
  15. BIN
      python/py/_asyncio.pyd
  16. BIN
      python/py/_bz2.pyd
  17. BIN
      python/py/_ctypes.pyd
  18. BIN
      python/py/_decimal.pyd
  19. BIN
      python/py/_elementtree.pyd
  20. BIN
      python/py/_hashlib.pyd
  21. BIN
      python/py/_lzma.pyd
  22. BIN
      python/py/_msi.pyd
  23. BIN
      python/py/_multiprocessing.pyd
  24. BIN
      python/py/_overlapped.pyd
  25. BIN
      python/py/_queue.pyd
  26. BIN
      python/py/_socket.pyd
  27. BIN
      python/py/_sqlite3.pyd
  28. BIN
      python/py/_ssl.pyd
  29. BIN
      python/py/_uuid.pyd
  30. BIN
      python/py/_wmi.pyd
  31. BIN
      python/py/_zoneinfo.pyd
  32. BIN
      python/py/libcrypto-3-arm64.dll
  33. BIN
      python/py/libffi-8.dll
  34. BIN
      python/py/libssl-3-arm64.dll
  35. BIN
      python/py/pyexpat.pyd
  36. BIN
      python/py/python.cat
  37. BIN
      python/py/python.exe
  38. BIN
      python/py/python3.dll
  39. 5 0
      python/py/python312._pth
  40. BIN
      python/py/python312.dll
  41. BIN
      python/py/python312.zip
  42. BIN
      python/py/pythonw.exe
  43. BIN
      python/py/select.pyd
  44. BIN
      python/py/sqlite3.dll
  45. BIN
      python/py/unicodedata.pyd
  46. BIN
      python/py/vcruntime140.dll
  47. BIN
      python/py/vcruntime140_1.dll
  48. BIN
      python/py/winsound.pyd
  49. 5 32
      src/page/device/connect-item/connect-item.js
  50. 9 0
      src/page/device/connect-item/connect-item.jsx
  51. 4 0
      src/page/device/connect-item/connect-item.scss
  52. 18 23
      src/page/device/device.js
  53. 3 2
      src/page/device/device.jsx
  54. 13 8
      src/page/device/device.scss
  55. 1 1
      static/scrcpy-pid.json

+ 60 - 0
doc/打包流程.md

@@ -0,0 +1,60 @@
+# 打包流程
+
+1. 关掉已运行的 AndroidRemoteController.exe。
+2. 项目根目录执行:`npm run build`,再执行 `npx electron-builder --win --arm64`(或 `--x64`)。
+3. 产物在 `dist\win-arm64-unpacked`(或 `dist\win-unpacked`)。
+4. 运行:`dist\win-arm64-unpacked\AndroidRemoteController.exe`。
+
+---
+
+## 打包前需确认的配置
+
+### package.json → build
+
+- **directories.output**
+  打包输出目录。例如 `"dist"` 时,产物在 `dist\win-arm64-unpacked` 或 `dist\win-unpacked`。
+
+- **files**
+  打进 asar 的内容。当前为 `["dist/**","electron/**","configs/**","nodejs/**","python/**","lib/**"]`,不要漏掉运行时用到的目录。
+
+- **asarUnpack**
+  不压进 asar、解出到 `app.asar.unpacked` 的目录。当前为 `nodejs/**`、`configs/**`、`lib/**`、`python/**`,主进程和 Node 脚本会从这里读。
+
+- **win.target**
+  - `target: "dir"`:只输出目录,不打安装包。
+  - `arch: ["arm64"]` 或 `["x64"]`:架构,与命令行 `--arm64` / `--x64` 一致。
+
+### configs/config.js
+
+- **window**
+  窗口宽高、是否隐藏菜单栏。
+
+- **adbPath.path**
+  相对项目根的 adb 路径,默认 `lib/scrcpy-adb/adb.exe`。打包后以 `app.asar.unpacked` 为根解析,一般不用改。
+
+- **pythonPath.path**
+  Python 运行时目录,按 `process.arch` 用 `python/arm64` 或 `python/x64`,打包后同上。
+
+### 目录与运行时
+
+- **lib/scrcpy-adb/**
+  需存在 adb.exe、scrcpy.exe 及 scrcpy 依赖 dll/bat;会被 `files` 打进包,在 unpacked 中供主进程和 node 脚本调用。
+
+- **python/arm64** 或 **python/x64**
+  按打包架构保留对应目录(内含嵌入式 Python 或 venv),仅在使用图像匹配/裁剪等流程时需要。打 arm64 包需有 `python/arm64`,打 x64 需有 `python/x64`。
+
+### vite.config.mjs 与打包
+
+- **base: './'**
+  打包后的 exe 从“本地文件”打开页面。不设为 `'./'` 时,js/css 链接会指到错误位置,白屏或报错;设为 `'./'` 后为相对路径“当前目录下的 assets/xxx”,可正常加载。**不要删或改成别的。**
+
+- **server**
+  只影响开发时的地址和端口,与打 exe 无关。
+
+- **构建产物**
+  `npm run build` 会生成 `dist` 文件夹(index.html + assets 等)。打 exe 时会把 `dist` 打进包,exe 启动时加载此处的 index.html。
+
+### package-lock.json
+
+- 执行 `npm install`、`npm run build` 时会按 **package-lock.json** 安装依赖,保证各环境版本一致、构建可复现。
+- 该文件不打进 exe(不在 `build.files` 里),只用于本地/CI 的安装与构建。

+ 48 - 41
electron/main.js

@@ -1,45 +1,34 @@
-const { app, BrowserWindow ,ipcMain} = require('electron')
+const { app, BrowserWindow, ipcMain } = require('electron')
 const path = require('path')
 const os = require('os')
 const fs = require('fs')
-const config = require('../configs/config.js')
+
+const unpackedRoot = app.isPackaged
+  ? path.join(path.dirname(process.execPath), 'resources', 'app.asar.unpacked')
+  : path.join(__dirname, '..')
+const config = require(path.join(unpackedRoot, 'configs', 'config.js'))
 const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
 
-// 修复缓存权限问题:设置用户数据目录到有权限的位置
-// 必须在 app.whenReady() 之前调用
+const staticDir = path.join(unpackedRoot, 'static')
+if (!fs.existsSync(staticDir)) {
+  fs.mkdirSync(staticDir, { recursive: true })
+}
+
+// 修复缓存权限:userData 与缓存目录设到临时目录,避免安装目录只读导致 "Unable to move the cache (0x5)"
 if (process.platform === 'win32') {
-  try {
-    // 设置缓存目录到用户临时目录,避免权限问题
-    const userDataPath = path.join(os.tmpdir(), 'electron-react-vite-app')
-    
-    // 确保目录存在
-    if (!fs.existsSync(userDataPath)) {
-      fs.mkdirSync(userDataPath, { recursive: true })
-    }
-    
-    // 创建缓存子目录
-    const cacheDir = path.join(userDataPath, 'cache')
-    const gpuCacheDir = path.join(userDataPath, 'gpu-cache')
-    if (!fs.existsSync(cacheDir)) {
-      fs.mkdirSync(cacheDir, { recursive: true })
-    }
-    if (!fs.existsSync(gpuCacheDir)) {
-      fs.mkdirSync(gpuCacheDir, { recursive: true })
-    }
-    
-    // 设置用户数据路径(必须在 app.whenReady() 之前)
-    app.setPath('userData', userDataPath)
-    
-    // 设置缓存目录到有权限的位置
-    app.commandLine.appendSwitch('disk-cache-dir', cacheDir)
-    app.commandLine.appendSwitch('gpu-disk-cache-dir', gpuCacheDir)
-    
-    console.log(`[OK] Cache directories set to: ${userDataPath}`)
-  } catch (error) {
-    console.warn('[WARN] Failed to set cache directories:', error.message)
-    // 如果设置失败,尝试禁用 GPU 缓存作为备选方案
-    app.commandLine.appendSwitch('disable-gpu-sandbox')
+  const userDataPath = path.join(os.tmpdir(), 'AndroidRemoteController')
+  if (!fs.existsSync(userDataPath)) {
+    fs.mkdirSync(userDataPath, { recursive: true })
   }
+  const cacheDir = path.join(userDataPath, 'Cache')
+  const gpuCacheDir = path.join(userDataPath, 'GPUCache')
+  if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true })
+  if (!fs.existsSync(gpuCacheDir)) fs.mkdirSync(gpuCacheDir, { recursive: true })
+
+  app.setPath('userData', userDataPath)
+  app.commandLine.appendSwitch('disk-cache-dir', cacheDir)
+  app.commandLine.appendSwitch('gpu-disk-cache-dir', gpuCacheDir)
+  app.commandLine.appendSwitch('disable-gpu-shader-disk-cache')
 }
 
 // 保存主窗口引用,用于推送消息
@@ -71,7 +60,7 @@ function createWindow() {
       mainWindow.webContents.openDevTools()
     }
   } else {
-    mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
+    mainWindow.loadFile(path.join(unpackedRoot, 'dist', 'index.html'))
   }
 }
 
@@ -83,7 +72,7 @@ const runningProcesses = new Map()
 // Execute Node.js script
 ipcMain.handle('run-nodejs-script', async (event, scriptName, ...parameters) => {
   return new Promise((resolve, reject) => {
-    const scriptPath = path.join(__dirname, '../nodejs', `${scriptName}.js`)
+    const scriptPath = path.join(unpackedRoot, 'nodejs', `${scriptName}.js`)
     const processKey = `${scriptName}-${parameters.join('-')}`
     
     // 如果进程已运行,先停止它
@@ -128,8 +117,8 @@ ipcMain.handle('run-nodejs-script', async (event, scriptName, ...parameters) =>
       stderr += data.toString()
     })
     
-    const isRunProcess = scriptName === 'run-process'
-    const timeoutId = isRunProcess ? null : setTimeout(() => {
+    const waitForExit = scriptName === 'run-process' || scriptName === 'enable-wirless-connect'
+    const timeoutId = waitForExit ? null : setTimeout(() => {
       if (!resolved) {
         finish({
           success: false,
@@ -175,6 +164,24 @@ ipcMain.handle('kill-nodejs-script', async (event, scriptName, ...parameters) =>
   return { killed: false }
 })
 
+/** 检查 scrcpy 是否仍在运行(读 pid 文件并用 process.kill(pid,0) 探测),用于用户直接关窗口时同步按钮状态 */
+ipcMain.handle('check-scrcpy-running', async () => {
+  const pidFile = path.join(unpackedRoot, 'static', 'scrcpy-pid.json')
+  if (!fs.existsSync(pidFile)) return { running: false }
+  let pid
+  try {
+    pid = JSON.parse(fs.readFileSync(pidFile, 'utf-8')).pid
+  } catch (_) {
+    return { running: false }
+  }
+  try {
+    process.kill(pid, 0)
+    return { running: true }
+  } catch (_) {
+    return { running: false }
+  }
+})
+
 // Execute Python script
 ipcMain.handle('run-python-script', async (event, scriptName, ...parameters) => {
   return new Promise((resolve, reject) => {
@@ -187,7 +194,7 @@ ipcMain.handle('run-python-script', async (event, scriptName, ...parameters) =>
       }
     }
     
-    const scriptPath = path.join(__dirname, '../python/scripts', `${scriptName}.py`)
+    const scriptPath = path.join(unpackedRoot, 'python', 'scripts', `${scriptName}.py`)
     
     if (!fs.existsSync(scriptPath)) {
       reject({
@@ -239,7 +246,7 @@ ipcMain.handle('ipc-request', async (event, channel, data) => {
     // 通过 IPC 调用 run-nodejs-script
     const { scriptName, ...parameters } = data
     return new Promise((resolve, reject) => {
-      const scriptPath = path.join(__dirname, '../nodejs', `${scriptName}.js`)
+      const scriptPath = path.join(unpackedRoot, 'nodejs', `${scriptName}.js`)
       const processKey = `${scriptName}-${parameters.join('-')}`
       
       if (runningProcesses.has(processKey)) {

+ 1 - 0
electron/preload.js

@@ -5,6 +5,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
     ipcRenderer.invoke('run-nodejs-script', scriptName, ...parameters),
   killNodejsScript: (scriptName, ...parameters) =>
     ipcRenderer.invoke('kill-nodejs-script', scriptName, ...parameters),
+  checkScrcpyRunning: () => ipcRenderer.invoke('check-scrcpy-running'),
   runPythonScript: (scriptName, ...parameters) =>
     ipcRenderer.invoke('run-python-script', scriptName, ...parameters)
 })

+ 2 - 1
lib/scrcpy-adb/scrcpy-console.bat

@@ -1,2 +1,3 @@
 @echo off
-scrcpy.exe --pause-on-exit=if-error %*
+cd /d "%~dp0"
+scrcpy.exe -e --pause-on-exit=if-error %*

+ 9 - 0
lib/scrcpy-adb/scrcpy-noconsole-debug.bat

@@ -0,0 +1,9 @@
+@echo off
+cd /d "%~dp0"
+echo [DEBUG] 当前目录: %CD%
+echo [DEBUG] 启动 scrcpy(带 -e 优先 TCPIP 设备),错误会显示在下方...
+echo.
+scrcpy.exe -e --pause-on-exit=if-error %*
+echo.
+echo [DEBUG] 退出码: %ERRORLEVEL%
+pause

+ 3 - 0
lib/scrcpy-adb/scrcpy-noconsole.bat

@@ -0,0 +1,3 @@
+@echo off
+cd /d "%~dp0"
+start "" scrcpy.exe %*

+ 8 - 3
lib/scrcpy-adb/scrcpy-noconsole.vbs

@@ -1,7 +1,12 @@
-strCommand = "cmd /c scrcpy.exe"
+' 在 VBS 所在目录执行 scrcpy.exe(先 cd 再带完整路径执行),保证双击即可运行且能加载同目录 dll
+Dim fso, scriptDir, exePath, strCommand, Arg
+Set fso = CreateObject("Scripting.FileSystemObject")
+scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)
+exePath = scriptDir & "\scrcpy.exe"
+strCommand = "cmd /c cd /d """ & scriptDir & """ && """ & exePath & """"
 
 For Each Arg In WScript.Arguments
-    strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """"
+    strCommand = strCommand & " """ & Replace(Arg, """", """") & """"
 Next
 
-CreateObject("Wscript.Shell").Run strCommand, 0, false
+CreateObject("Wscript.Shell").Run strCommand, 0, False

+ 0 - 53
nodejs/adb/scan-connect-ip.js

@@ -1,53 +0,0 @@
-#!/usr/bin/env node
-const { spawnSync } = require('child_process')
-const path = require('path')
-const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
-
-const projectRoot = path.resolve(__dirname, '..', '..')
-
-/** 从 config 解析并返回 adb 可执行文件路径 */
-function getAdbPath() {
-  return config.adbPath?.path
-    ? path.resolve(projectRoot, config.adbPath.path)
-    : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
-}
-
-/** 判断 adb connect 输出是否表示连接成功 */
-function isConnectSuccess(output) {
-  const s = (output || '').trim()
-  return s.includes('connected') || s.includes('already connected')
-}
-
-/** 对单个 IP 执行 adb connect,返回是否成功(不抛错,失败返回 false) */
-function tryConnectOne(adbPath, ip, port) {
-  const quotedAdb = adbPath.includes(' ') ? `"${adbPath}"` : adbPath
-  const cmd = `${quotedAdb} connect ${ip}:${port}`
-  const shell = process.platform === 'win32' ? 'cmd' : 'sh'
-  const shellArg = process.platform === 'win32' ? '/c' : '-c'
-  const r = spawnSync(shell, [shellArg, cmd], { encoding: 'utf-8', timeout: 500 })
-  return r.status === 0 && isConnectSuccess(r.stdout)
-}
-
-/** 扫描网段 basePrefix.1~.255,返回连接成功的 IP 列表 */
-function scanSubnet(adbPath, basePrefix, port) {
-  const connected = []
-  for (let i = 1; i <= 255; i++) {
-    const ip = `${basePrefix}.${i}`
-    if (tryConnectOne(adbPath, ip, port)) {
-      connected.push(ip)
-    }
-  }
-  return connected
-}
-
-const basePrefix = process.argv[2]
-const port = process.argv[3] || '5555'
-
-if (!basePrefix || !/^\d+\.\d+\.\d+$/.test(basePrefix)) {
-  process.exit(1)
-}
-
-const adbPath = getAdbPath()
-const connected = scanSubnet(adbPath, basePrefix, port)
-process.stdout.write(connected.join('\n') + (connected.length ? '\n' : ''))
-process.exit(0)

+ 55 - 49
nodejs/adb/screenshot.js

@@ -4,32 +4,19 @@ const path = require('path')
 const fs = require('fs')
 
 const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
-const projectRoot = path.resolve(__dirname, '..', '..')
+let projectRoot = path.resolve(__dirname, '..', '..')
+if (projectRoot.includes('app.asar') && !projectRoot.includes('app.asar.unpacked')) {
+  projectRoot = projectRoot.replace('app.asar', 'app.asar.unpacked')
+}
 const adbPath = path.resolve(projectRoot, config.adbPath.path)
 const scrcpyDir = path.dirname(adbPath)
 const scrcpyPath = path.join(scrcpyDir, 'scrcpy.exe')
-const scrcpyVbs = path.join(scrcpyDir, 'scrcpy-noconsole.vbs')
-const stopBat = path.join(scrcpyDir, 'stop.bat')
 const pidFile = path.join(projectRoot, 'static', 'scrcpy-pid.json')
 
 const action = process.argv[2]
 const pidArg = process.argv[3]
 
-// 获取 scrcpy 进程 PID
-function getScrcpyPid() {
-  const output = execSync(`tasklist /FI "IMAGENAME eq scrcpy.exe" /FO CSV`, { encoding: 'utf-8' })
-  const lines = output.split('\n').filter(line => line.includes('scrcpy.exe'))
-  if (lines.length === 0) {
-    return null
-  }
-  const pidMatch = lines[0].match(/"(\d+)"/)
-  if (!pidMatch) {
-    return null
-  }
-  return parseInt(pidMatch[1])
-}
-
-// 检查并终止指定 PID 的进程
+/** 终止指定 PID 的进程 */
 function killPidIfRunning(pid) {
   spawn('taskkill.exe', ['/F', '/PID', pid.toString()], {
     stdio: 'ignore',
@@ -37,50 +24,69 @@ function killPidIfRunning(pid) {
   }).unref()
 }
 
-// 启动 scrcpy
+/** 从 tasklist 获取 scrcpy.exe 的 PID(用 bat start 启动后需轮询得到 pid) */
+function getScrcpyPid() {
+  const output = execSync('tasklist /FI "IMAGENAME eq scrcpy.exe" /FO CSV', { encoding: 'utf-8' })
+  const lines = output.split('\n').filter(line => line.includes('scrcpy.exe'))
+  if (lines.length === 0) return null
+  const pidMatch = lines[0].match(/"(\d+)"/)
+  return pidMatch ? parseInt(pidMatch[1]) : null
+}
+
+/** 判断是否为 IP(或 IP:port),用于无线设备选择器 */
+function isDeviceIp(val) {
+  return val && !/^\d+$/.test(val) && /[\d.]/.test(val)
+}
+
+/** 根据传入的 IP 得到 adb/scrcpy 设备选择器,无线默认 5555 */
+function toDeviceSelector(ipOrSelector) {
+  return ipOrSelector.includes(':') ? ipOrSelector : `${ipOrSelector}:5555`
+}
+
+/** 启动 scrcpy:先 adb connect(若有 IP),再用 scrcpy-noconsole.bat(start "" scrcpy.exe %*)无控制台启动 */
 function startScrcpy() {
-  const targetPid = pidArg && /^\d+$/.test(pidArg) ? parseInt(pidArg) : 7788
-  killPidIfRunning(targetPid)
-  
-  const vbsArgs = pidArg && !/^\d+$/.test(pidArg) ? ` "${pidArg}"` : ''
-  execSync(`wscript.exe "${scrcpyVbs}"${vbsArgs}`, {
-    cwd: scrcpyDir,
-    stdio: 'ignore'
-  })
-  
-  const sleep = (ms) => {
-    const start = Date.now()
-    while (Date.now() - start < ms) {}
+  if (fs.existsSync(pidFile)) {
+    const { pid } = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
+    killPidIfRunning(pid)
   }
-  sleep(2000)
-  
+
+  let deviceSelector = ''
+  if (isDeviceIp(pidArg)) {
+    deviceSelector = toDeviceSelector(pidArg)
+    execSync(`"${adbPath}" connect ${deviceSelector}`, { encoding: 'utf-8', cwd: scrcpyDir })
+  }
+
+  const serial = deviceSelector || (pidArg && !/^\d+$/.test(pidArg) ? pidArg : '')
+  const args = serial
+    ? ['--pause-on-exit=if-error', '-s', serial]
+    : ['-e', '--pause-on-exit=if-error']
+  const argsStr = args.join(' ')
+  const vbsPath = path.join(scrcpyDir, 'scrcpy-noconsole.vbs')
+  execSync(`wscript "${vbsPath.replace(/"/g, '""')}" ${argsStr}`, { cwd: scrcpyDir })
+
+  const sleep = (ms) => { const t = Date.now(); while (Date.now() - t < ms) {} }
+  sleep(1500)
   const runningPid = getScrcpyPid()
   if (!runningPid) {
-    console.log(JSON.stringify({ 
-      success: false, 
-      error: 'Failed to start scrcpy'
-    }))
+    console.log(JSON.stringify({ success: false, error: 'scrcpy did not appear in tasklist' }))
     process.exit(1)
-    return
   }
-  
   fs.writeFileSync(pidFile, JSON.stringify({ pid: runningPid }), 'utf-8')
-  console.log(JSON.stringify({ 
-    success: true, 
-    pid: runningPid
-  }))
+  console.log(JSON.stringify({ success: true, pid: runningPid }))
   process.exit(0)
 }
 
-// 停止 scrcpy
+/** 停止 scrcpy:有 pid 则 taskkill 并删文件,返回 action: 'stop';无 pid 文件返回 action: 'none',供前端区分是否继续执行 start */
 function stopScrcpy() {
+  if (!fs.existsSync(pidFile)) {
+    console.log(JSON.stringify({ success: true, action: 'none' }))
+    process.exit(0)
+    return
+  }
   const pid = JSON.parse(fs.readFileSync(pidFile, 'utf-8')).pid
-  execSync(`"${stopBat}" ${pid}`, { stdio: 'ignore', cwd: scrcpyDir })
+  killPidIfRunning(pid)
   fs.unlinkSync(pidFile)
-
-  console.log(JSON.stringify({ 
-    success: true 
-  }))
+  console.log(JSON.stringify({ success: true, action: 'stop' }))
   process.exit(0)
 }
 

+ 57 - 0
nodejs/enable-wirless-connect.js

@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+const { execSync } = require('child_process')
+const path = require('path')
+
+// 项目根 = 脚本所在目录的上一级(nodejs -> 项目根)
+// 打包后 nodejs 在 app.asar 内执行,但 lib/configs 在 app.asar.unpacked,需用 unpacked 路径才能找到 adb
+let PROJECT_ROOT = path.resolve(__dirname, '..')
+if (PROJECT_ROOT.includes('app.asar') && !PROJECT_ROOT.includes('app.asar.unpacked')) {
+  PROJECT_ROOT = PROJECT_ROOT.replace('app.asar', 'app.asar.unpacked')
+}
+const TCPIP_PORT = 5555
+
+/** 从配置解析并返回 ADB 可执行文件路径 */
+function getAdbPath() {
+  const configPath = path.join(PROJECT_ROOT, 'configs', 'config.js')
+  const config = require(configPath)
+  return config.adbPath?.path
+    ? path.resolve(PROJECT_ROOT, config.adbPath.path)
+    : path.join(PROJECT_ROOT, 'lib', 'scrcpy-adb', 'adb.exe')
+}
+
+/** 返回当前通过 USB 连接的设备 ID 列表(仅 status 为 device 且非 IP:port,排除无线设备) */
+function getConnectedDeviceIds(adbPath) {
+  const out = execSync(`"${adbPath}" devices`, { encoding: 'utf-8' })
+  return out
+    .split('\n')
+    .filter((line) => line.trim() && !line.startsWith('List') && line.includes('\tdevice'))
+    .map((line) => line.trim().split('\t')[0])
+    .filter((id) => id && !id.includes(':'))
+}
+
+/** 操作一:在 USB 设备上开启无线调试(settings adb_wifi_enabled 1) */
+function enableWirelessSetting(adbPath, deviceId) {
+  execSync(`"${adbPath}" -s ${deviceId} shell settings put global adb_wifi_enabled 1`, { encoding: 'utf-8' })
+}
+
+/** 操作二:在 USB 设备上激活 5555 端口(tcpip),用于无线连接 */
+function enableTcpipPort(adbPath, deviceId, port) {
+  return execSync(`"${adbPath}" -s ${deviceId} tcpip ${port}`, { encoding: 'utf-8' }).trim()
+}
+
+/** 主流程:取 USB 设备,依次执行「开启无线调试」「激活 5555 端口」 */
+function run() {
+  const adbPath = getAdbPath()
+  const devices = getConnectedDeviceIds(adbPath)
+  if (devices.length === 0) {
+    process.stderr.write('No devices found. Please connect a device via USB.\n')
+    process.exit(1)
+  }
+  const deviceId = devices[0]
+  enableWirelessSetting(adbPath, deviceId)
+  const tcpipOut = enableTcpipPort(adbPath, deviceId, TCPIP_PORT)
+  process.stdout.write(tcpipOut + '\n')
+  process.exit(0)
+}
+
+run()

+ 0 - 0
package/package-arm64.bat


+ 0 - 0
package/package-arm64.js


+ 0 - 0
package/package-x64.bat


+ 0 - 0
package/package-x64.js


BIN
python/py/_asyncio.pyd


BIN
python/py/_bz2.pyd


BIN
python/py/_ctypes.pyd


BIN
python/py/_decimal.pyd


BIN
python/py/_elementtree.pyd


BIN
python/py/_hashlib.pyd


BIN
python/py/_lzma.pyd


BIN
python/py/_msi.pyd


BIN
python/py/_multiprocessing.pyd


BIN
python/py/_overlapped.pyd


BIN
python/py/_queue.pyd


BIN
python/py/_socket.pyd


BIN
python/py/_sqlite3.pyd


BIN
python/py/_ssl.pyd


BIN
python/py/_uuid.pyd


BIN
python/py/_wmi.pyd


BIN
python/py/_zoneinfo.pyd


BIN
python/py/libcrypto-3-arm64.dll


BIN
python/py/libffi-8.dll


BIN
python/py/libssl-3-arm64.dll


BIN
python/py/pyexpat.pyd


BIN
python/py/python.cat


BIN
python/py/python.exe


BIN
python/py/python3.dll


+ 5 - 0
python/py/python312._pth

@@ -0,0 +1,5 @@
+python312.zip
+.
+
+# Uncomment to run site.main() automatically
+#import site

BIN
python/py/python312.dll


BIN
python/py/python312.zip


BIN
python/py/pythonw.exe


BIN
python/py/select.pyd


BIN
python/py/sqlite3.dll


BIN
python/py/unicodedata.pyd


BIN
python/py/vcruntime140.dll


BIN
python/py/vcruntime140_1.dll


BIN
python/py/winsound.pyd


+ 5 - 32
src/page/device/connect-item/connect-item.js

@@ -2,6 +2,7 @@ class ConnectItemClass {
     constructor() {
     }
 
+    /** 初始化连接项:IP、预览状态与回调 */
     async init(ipAddress, isPreviewing, setIsPreviewing, onPreview, onRemove) {
         this.ipAddress = ipAddress
         this.is_previewing = isPreviewing || false
@@ -10,51 +11,23 @@ class ConnectItemClass {
         this.onPreviewCallback = onPreview
     }
 
+    /** 点击预览:调用 triggerPreview,按结果更新预览状态并回调 */
     async onPreview() {
         const result = await this.triggerPreview()
-        if (!result.success) {
-            return
-        }
-
-        switch (result.action) {
-            case 'start':
-                this.is_previewing = true
-                this.setIsPreviewing(true)
-                break
-            case 'stop':
-                this.is_previewing = false
-                this.setIsPreviewing(false)
-                break
-        }
-        this.onPreviewCallback?.(this.ipAddress)
     }
 
+    /** 点击移除:调用 remove 后执行 onRemove 回调 */
     async onRemove() {
         await this.remove()
         this.onRemoveCallback(this.ipAddress)
     }
 
-    // 触发 scrcpy 预览(切换启动/停止状态)
+    /** 触发 scrcpy 预览:先尝试停止,成功则返回 stop;否则启动并返回 start(主进程可能提前 resolve,exitCode 为 null 按成功处理) */
     async triggerPreview() {
-        const stopResult = await window.electronAPI.runNodejsScript('adb/screenshot', 'stop')
-
-        if (stopResult.exitCode === 0) {
-            const json = JSON.parse(stopResult.stdout)
-            if (json.success === true) {
-                return { action: 'stop', success: true }
-            }
-        }
-
         const startResult = await window.electronAPI.runNodejsScript('adb/screenshot', 'start', this.ipAddress)
-
-        if (startResult.exitCode === 0) {
-            const json = JSON.parse(startResult.stdout)
-            return { action: 'start', success: json.success === true }
-        }
-
-        return { action: 'start', success: false }
     }
 
+    /** 从 adb 移除该设备,返回是否成功 */
     async remove() {
         const result = await window.electronAPI.runNodejsScript('adb-remove', this.ipAddress)
         return result.exitCode === 0

+ 9 - 0
src/page/device/connect-item/connect-item.jsx

@@ -20,6 +20,15 @@ function ConnectItem({ ipAddress, selected=false, executionStatus = null, onSele
     }
   }, [])
 
+  useEffect(() => {
+    if (!isPreviewing) return
+    const timer = setInterval(async () => {
+      const { running } = await window.electronAPI.checkScrcpyRunning()
+      if (!running) setIsPreviewing(false)
+    }, 2000)
+    return () => clearInterval(timer)
+  }, [isPreviewing])
+
   return (
     <div className="connect-item-container">
 

+ 4 - 0
src/page/device/connect-item/connect-item.scss

@@ -81,14 +81,18 @@ $font-size-scale: 1.5;  // 字体缩放系数,调整此值可改变字体大
         justify-content: space-around;
 
         .preview-btn {
+            min-width: 4.5em;
             width: 60%;
             height: 100%;
+            flex-shrink: 0;
             @include flex-center;
 
             color: #000000;
             font-weight: bold;
             border-radius: 10px;
             @include highlight-btn(#00fb43);
+            white-space: nowrap;
+            line-height: 1.2;
         }    
         
         .status-indicator {

+ 18 - 23
src/page/device/device.js

@@ -42,33 +42,28 @@ class DeviceClass {
         }
     }
 
-    async onRefresh(e, self) {
-        self.startAnimation()
-        this.count_ip_x = 2
-        this.count_ip_y = 1
-
-        let that = this;
-        this.scanDevice(() => {
-            self.stopAnimation()
-        }, that)
-    }
-
-    async scanDevice(callback, that) {
-        const basePrefix = this.inputValue?.trim() || ''
-        if (!basePrefix) {
-            callback()
+    /** 执行开启无线连接脚本并根据结果提示 */
+    async onEnableWirlessConnect() {
+        const result = await window.electronAPI.runNodejsScript('enable-wirless-connect')
+        const noDevice = (result.stderr || '').includes('No devices') || (result.stdout || '').includes('No devices')
+        if (result.exitCode === 0) {
+            hintView.setContent('开启手机无线连接成功')
+            hintView.show()
             return
         }
-        const result = await window.electronAPI.runNodejsScript('adb/scan-connect-ip', basePrefix, '5555')
-        if (result.exitCode === 0 && result.stdout) {
-            const ips = result.stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean)
-            if (ips.length) {
-                that.setDeviceList(prev => [...new Set([...(prev || []), ...ips])])
-            }
+        if (noDevice) {
+            hintView.setContent('未检测到设备,请先用 USB 连接手机')
+            hintView.show()
+            return
         }
-        callback()
+        const raw = (result.stderr || result.stdout || '').trim().split('\n')[0] || '开启无线连接失败'
+        const msg = (raw === 'error: closed' || raw.includes('closed'))
+            ? 'ADB 连接已断开,请检查 USB 连接后重试'
+            : raw
+        hintView.setContent(msg)
+        hintView.show()
     }
-
+    
     async onAddDevice() {
         const ip = this.inputValue; 
         const ipList = await window.electronAPI.runNodejsScript('json-parser', 'read', 'device_list.json')

+ 3 - 2
src/page/device/device.jsx

@@ -45,12 +45,13 @@ function Device({ show }) {
         {/* 更新设备列表 */}
         <div className="device-update">
           <div className="device-update-title">设备列表</div>
-          <div className="device-update-btn">
+          {/* <div className="device-update-btn">
             <UpdateBtn
               onClick={(e, self) => deviceClass.current?.onRefresh(e, self)}
               title="Refresh device list"
             />
-          </div>
+          </div> */}
+          <div className="enable-wirless-connect-btn" onClick={() => deviceClass.current?.onEnableWirlessConnect()}>激活</div>
         </div>
 
         {/* 设备列表 */}

+ 13 - 8
src/page/device/device.scss

@@ -20,16 +20,21 @@
       
       @include flex-center;
     }
-
-    .device-update-btn {
+    .enable-wirless-connect-btn {
       width: 30%;
-      height: 100%;
-      @include flex-center;
+      height: 50%;
+   
+      background-color: #000000;
+      border-radius: 10px;
+      font-size: 0.8rem;
+      color: #ffffff;
+      margin-right: 5%;
 
-      .update-btn {
-        width: 50% !important;
-        height: 50% !important;
-      }
+      cursor: pointer;
+      @include div-btn-hover-effect;
+      @include div-btn-pressed-effect;
+
+      @include flex-center;
     }
 
     background-color: #efe8e88d;

+ 1 - 1
static/scrcpy-pid.json

@@ -1 +1 @@
-{"pid":13232}
+{"pid":9468}