/** * keyhelper.js — 长驻 PowerShell 子进程。 * * ⚠ 非生产代码 —— 当前没有任何主进程路径 require 它或调用它。 * 历史:早期版本的「点选一条 → 自动粘贴到目标窗口」功能走这条路, * 需要 PowerShell 用 SendKeys 发 Ctrl+V。在 2026-07-30 那一轮把 * 自动粘贴整体移除了(用户明确不想要自动粘贴触发的「窗口缩一下」 * 动画),主进程只剩手动 Ctrl+V 这条路。 * * 用途(保留作备用 / bug 复现): * - 在 Windows 上发 Ctrl+V(避开 robotjs/nut-js 之类的原生模块编译) * - 通过 stdin/stdout JSON-RPC 与主进程通信,单进程常驻,延迟 ~5ms * * 不在 package.json 的 build.files 里 —— 打包后不会进用户机器。 * 想恢复自动粘贴:把 require('./keyhelper') 加回 main.js,把发键的 * 逻辑塞到 clips:movetop 处理器的"clipboard.writeText 之后、IPC 返回 * 之前"那段窗口隐藏逻辑即可(参见 README「不自动粘贴」说明)。 * * 协议: * in : {"id":1,"action":"sendCtrlV"} * out: {"id":1,"ok":true} */ 'use strict'; const { spawn } = require('node:child_process'); const PS_SCRIPT = String.raw` $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Windows.Forms while ($true) { $line = [Console]::In.ReadLine() if ([string]::IsNullOrEmpty($line)) { continue } try { $req = $line | ConvertFrom-Json $id = $req.id switch ($req.action) { 'sendCtrlV' { [System.Windows.Forms.SendKeys]::SendWait('^v') [Console]::Out.WriteLine(('{"id":' + $id + ',"ok":true}')) } default { [Console]::Out.WriteLine(('{"id":' + $id + ',"ok":false,"error":"unknown action"}')) } } } catch { [Console]::Out.WriteLine(('{"id":-1,"ok":false,"error":"' + ($_.Exception.Message -replace '"','\\"') + '"}')) } } `; class KeyHelper { constructor() { this.id = 0; this.pending = new Map(); this.buffer = ''; this._reconnectTimer = null; this.disposed = false; this._spawn(); } _spawn() { this.ps = spawn( 'powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', PS_SCRIPT], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true } ); this.ps.stdout.on('data', (chunk) => this._onStdout(chunk)); this.ps.stderr.on('data', (chunk) => { console.error('[keyhelper stderr]', chunk.toString()); }); this.ps.on('exit', (code) => { console.error('[keyhelper] powershell exited with code', code); this._rejectAll(new Error('keyhelper exited')); this._reconnect(); }); } _reconnect(retryMs = 500) { if (this.disposed || this._reconnectTimer) return; const tryOnce = () => { this._reconnectTimer = null; if (this.disposed) return; try { this._spawn(); } catch (e) { console.error('[keyhelper] respawn failed:', e.message); this._reconnectTimer = setTimeout(tryOnce, Math.min(retryMs * 2, 8000)); } }; this._reconnectTimer = setTimeout(tryOnce, retryMs); } _onStdout(chunk) { this.buffer += chunk.toString('utf8'); let nl; while ((nl = this.buffer.indexOf('\n')) >= 0) { const line = this.buffer.slice(0, nl).trim(); this.buffer = this.buffer.slice(nl + 1); if (!line) continue; try { const resp = JSON.parse(line); if (resp.id === -1) { console.error('[keyhelper error]', resp.error); continue; } const cb = this.pending.get(resp.id); if (cb) { this.pending.delete(resp.id); cb.resolve(resp); } } catch (e) { // ignore parse error } } } _send(action, params = {}, timeoutMs = 10000) { return new Promise((resolve, reject) => { if (!this.ps || !this.ps.stdin || this.ps.stdin.destroyed) { return reject(new Error('keyhelper not running')); } const id = ++this.id; let settled = false; const t = setTimeout(() => { if (settled) return; settled = true; this.pending.delete(id); reject(new Error('keyhelper timeout')); }, timeoutMs); // 定时器句柄要存下来:dispose() / 进程退出时必须能取消, // 否则挂起的请求会在 10 秒后抛一个没人接的 rejection, // 同时这个 timer 还会一直吊着事件循环。 this.pending.set(id, { timer: t, resolve: (v) => { if (settled) return; settled = true; clearTimeout(t); resolve(v); }, reject: (e) => { if (settled) return; settled = true; clearTimeout(t); reject(e); }, }); try { this.ps.stdin.write(JSON.stringify({ id, action, ...params }) + '\n'); } catch (e) { if (settled) return; settled = true; clearTimeout(t); this.pending.delete(id); reject(e); } }); } sendCtrlV() { return this._send('sendCtrlV'); } _rejectAll(err) { for (const [, p] of this.pending) p.reject(err); this.pending.clear(); } dispose() { this.disposed = true; if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } this._rejectAll(new Error('keyhelper disposed')); if (this.ps) { // 别再因为我们自己的 kill 触发重连 this.ps.removeAllListeners('exit'); try { this.ps.stdin.end(); } catch (e) {} try { this.ps.kill(); } catch (e) {} } } } module.exports = KeyHelper;