This commit is contained in:
2026-09-12 13:59:12 +08:00
commit 941ce4baab
71 changed files with 13037 additions and 0 deletions

174
keyhelper.js Normal file
View 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;