update
This commit is contained in:
174
keyhelper.js
Normal file
174
keyhelper.js
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user