Files
Clipboard/keyhelper.js
2026-09-12 13:59:12 +08:00

174 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;