@ondevplann/remotetype 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/agent.js CHANGED
@@ -1,382 +1,382 @@
1
- 'use strict';
2
-
3
- /**
4
- * Rust input-agent 子进程管理器。
5
- *
6
- * 关键设计(需求文档 4.2.2 第 4 条):
7
- * * Node 业务层 <-> Rust 子进程通过 stdio 管道通信,单行 JSON,`\n` 分隔;
8
- * * 刻意不使用 FFI/ABI,子进程崩溃不会拖垮 Node 主进程(故障隔离);
9
- * * exit/close 事件触发自动重启(等待 2s);
10
- * * 心跳检测(ping)判定卡死,超时强制重启;
11
- * * 正常退出时优雅关闭子进程。
12
- *
13
- * 由于「一条输入完成再处理下一条」(串行队列在 queue.js),
14
- * 这里对每条指令采用独立 Promise 的方式按顺序调用即可,
15
- * 同时仍保留 inFlight 标记防止误用。
16
- */
17
-
18
- const { spawn } = require('child_process');
19
- const path = require('path');
20
- const fs = require('fs');
21
-
22
- /** 子进程状态 */
23
- const STATE = {
24
- STOPPED: 'stopped',
25
- STARTING: 'starting',
26
- READY: 'ready',
27
- RESTARTING: 'restarting',
28
- STOPPING: 'stopping',
29
- };
30
-
31
- class AgentManager {
32
- /**
33
- * @param {object} opts
34
- * @param {string} opts.binaryPath Rust 二进制绝对路径
35
- * @param {object} opts.logger
36
- * @param {number} [opts.restartDelayMs] 崩溃后重启延迟,默认 2000
37
- * @param {number} [opts.requestTimeoutMs]单条指令超时(作为心跳兜底),默认 30000
38
- * @param {number} [opts.heartbeatMs] 空闲心跳间隔,默认 15000
39
- * @param {number} [opts.maxRestarts] 连续快速重启上限,超过则停止并告警,默认 10
40
- */
41
- constructor(opts) {
42
- this.binaryPath = opts.binaryPath;
43
- this.logger = opts.logger;
44
- this.restartDelayMs = opts.restartDelayMs ?? 2000;
45
- this.requestTimeoutMs = opts.requestTimeoutMs ?? 30000;
46
- this.heartbeatMs = opts.heartbeatMs ?? 15000;
47
- this.maxRestarts = opts.maxRestarts ?? 10;
48
-
49
- this.child = null;
50
- this.state = STATE.STOPPED;
51
- this.shuttingDown = false;
52
- this.restartCount = 0;
53
- this.lastStartAt = 0;
54
- this.heartbeatTimer = null;
55
- this.restartTimer = null;
56
-
57
- this._stdoutBuffer = '';
58
- /** 等待中的请求:FIFO,子进程按序应答,因此队首即当前请求 */
59
- this._pending = [];
60
-
61
- this.stats = { spawned: 0, restarts: 0, crashes: 0, timeouts: 0, commands: 0, failures: 0 };
62
- }
63
-
64
- /** 启动子进程。若已启动则直接返回。 */
65
- start() {
66
- if (this.child) return this.child;
67
-
68
- if (!fs.existsSync(this.binaryPath)) {
69
- throw new Error(`input-agent binary not found: ${this.binaryPath}`);
70
- }
71
- try {
72
- fs.accessSync(this.binaryPath, fs.constants.X_OK);
73
- } catch {
74
- // 从 npm 包解压后可能丢失可执行位,这里自动补上
75
- try {
76
- fs.chmodSync(this.binaryPath, 0o755);
77
- this.logger?.debug('restored executable bit', { binaryPath: this.binaryPath });
78
- } catch (e) {
79
- this.logger?.warn('binary is not executable and chmod failed', {
80
- binaryPath: this.binaryPath,
81
- error: e.message,
82
- });
83
- }
84
- }
85
-
86
- this.state = STATE.STARTING;
87
- this.lastStartAt = Date.now();
88
- this._stdoutBuffer = '';
89
-
90
- this.logger?.info('spawning rust input-agent', { binaryPath: this.binaryPath });
91
-
92
- this.child = spawn(this.binaryPath, [], {
93
- stdio: ['pipe', 'pipe', 'pipe'],
94
- windowsHide: true,
95
- // 不继承 shell,避免注入风险
96
- shell: false,
97
- });
98
- this.stats.spawned += 1;
99
-
100
- this.child.stdout.setEncoding('utf8');
101
- this.child.stdin.setDefaultEncoding?.('utf8');
102
-
103
- this.child.stdout.on('data', (chunk) => this._onStdout(chunk));
104
-
105
- this.child.stderr.setEncoding('utf8');
106
- this.child.stderr.on('data', (chunk) => {
107
- const text = String(chunk).trimEnd();
108
- if (text) this.logger?.debug(`[input-agent] ${text}`);
109
- });
110
-
111
- this.child.on('error', (e) => {
112
- this.logger?.error('failed to spawn input-agent', { error: e.message });
113
- this._failAllPending(new Error(`input-agent spawn error: ${e.message}`));
114
- this._scheduleRestart('spawn-error');
115
- });
116
-
117
- this.child.on('exit', (code, signal) => {
118
- const wasShuttingDown = this.shuttingDown;
119
- const uptime = Date.now() - this.lastStartAt;
120
- this.logger?.warn('input-agent exited', { code, signal, uptimeMs: uptime });
121
- this._failAllPending(new Error(`input-agent exited (code=${code}, signal=${signal})`));
122
- this.child = null;
123
- this._clearHeartbeat();
124
-
125
- if (wasShuttingDown) {
126
- this.state = STATE.STOPPED;
127
- return;
128
- }
129
- // 非正常退出视为崩溃
130
- if (code !== 0 || signal) this.stats.crashes += 1;
131
- this._scheduleRestart(`exit code=${code} signal=${signal}`);
132
- });
133
-
134
- this.state = STATE.READY;
135
- this._startHeartbeat();
136
- return this.child;
137
- }
138
-
139
- /** 处理子进程 stdout:按行切分并交给等待队列队首。 */
140
- _onStdout(chunk) {
141
- this._stdoutBuffer += String(chunk);
142
- let idx;
143
- // 逐行消费,最后一段可能是不完整行,留在 buffer 里等下一个 data 事件
144
- while ((idx = this._stdoutBuffer.indexOf('\n')) >= 0) {
145
- const line = this._stdoutBuffer.slice(0, idx).trim();
146
- this._stdoutBuffer = this._stdoutBuffer.slice(idx + 1);
147
- if (!line) continue;
148
- this._deliverLine(line);
149
- }
150
- // 防御:buffer 异常膨胀说明协议错乱
151
- if (this._stdoutBuffer.length > 1024 * 1024) {
152
- this.logger?.error('stdout buffer overflow, resetting');
153
- this._stdoutBuffer = '';
154
- }
155
- }
156
-
157
- _deliverLine(line) {
158
- let parsed;
159
- try {
160
- parsed = JSON.parse(line);
161
- } catch {
162
- this.logger?.warn('unparseable line from input-agent', { line: line.slice(0, 300) });
163
- return; // 无法解析的行无法对应到具体请求,丢弃并继续
164
- }
165
- const pending = this._pending.shift();
166
- if (!pending) {
167
- this.logger?.debug('unexpected response with no pending request', { parsed });
168
- return;
169
- }
170
- clearTimeout(pending.timer);
171
- pending.resolve(parsed);
172
- }
173
-
174
- _failAllPending(err) {
175
- while (this._pending.length) {
176
- const p = this._pending.shift();
177
- clearTimeout(p.timer);
178
- p.reject(err);
179
- }
180
- }
181
-
182
- _clearHeartbeat() {
183
- if (this.heartbeatTimer) {
184
- clearInterval(this.heartbeatTimer);
185
- this.heartbeatTimer = null;
186
- }
187
- }
188
-
189
- _startHeartbeat() {
190
- this._clearHeartbeat();
191
- if (this.heartbeatMs <= 0) return;
192
- this.heartbeatTimer = setInterval(() => {
193
- // 仅在空闲时心跳,避免与正常指令抢答
194
- if (this._pending.length > 0 || this.state !== STATE.READY) return;
195
- this._send({ action: 'ping' }, this.heartbeatMs)
196
- .then((res) => {
197
- if (!res?.ok) this.logger?.warn('heartbeat returned not-ok', { res });
198
- })
199
- .catch((e) => {
200
- this.stats.timeouts += 1;
201
- this.logger?.error('heartbeat failed, agent considered hung', { error: e.message });
202
- this._killAndRestart('heartbeat-timeout');
203
- });
204
- }, this.heartbeatMs);
205
- this.heartbeatTimer.unref?.();
206
- }
207
-
208
- /** 按需求:崩溃后等待 2s 自动重启;连续快速重启超限则告警停止。 */
209
- _scheduleRestart(reason) {
210
- if (this.shuttingDown) return;
211
- if (this.restartTimer) return;
212
-
213
- const uptime = Date.now() - this.lastStartAt;
214
- // 存活 < 5s 视为快速重启
215
- if (uptime < 5000) {
216
- this.restartCount += 1;
217
- } else {
218
- this.restartCount = 0;
219
- }
220
-
221
- if (this.restartCount > this.maxRestarts) {
222
- this.state = STATE.STOPPED;
223
- this.logger?.error(
224
- 'input-agent keeps crashing, giving up auto-restart. check platform permissions.',
225
- { restarts: this.restartCount, binaryPath: this.binaryPath },
226
- );
227
- return;
228
- }
229
-
230
- this.state = STATE.RESTARTING;
231
- this.stats.restarts += 1;
232
- this.logger?.warn(`restarting input-agent in ${this.restartDelayMs}ms`, { reason });
233
- this.restartTimer = setTimeout(() => {
234
- this.restartTimer = null;
235
- if (this.shuttingDown) return;
236
- try {
237
- this.start();
238
- } catch (e) {
239
- this.logger?.error('restart failed', { error: e.message });
240
- this._scheduleRestart('restart-failed');
241
- }
242
- }, this.restartDelayMs);
243
- this.restartTimer.unref?.();
244
- }
245
-
246
- _killAndRestart(reason) {
247
- const child = this.child;
248
- this._failAllPending(new Error(`agent killed: ${reason}`));
249
- if (!child) {
250
- this._scheduleRestart(reason);
251
- return;
252
- }
253
- this.shuttingDown = false; // 仍属「重启」而非「退出」
254
- this._childExitReason = reason;
255
- this.logger?.warn('killing hung input-agent', { reason, pid: child.pid });
256
- // 先 SIGTERM,超时后 SIGKILL
257
- child.kill('SIGTERM');
258
- const escalate = setTimeout(() => {
259
- if (this.child === child) {
260
- this.logger?.warn('SIGTERM ineffective, sending SIGKILL', { pid: child.pid });
261
- try { child.kill('SIGKILL'); } catch { /* ignore */ }
262
- }
263
- }, 2000);
264
- escalate.unref?.();
265
- }
266
-
267
- /**
268
- * 发送一条指令并等待结果。
269
- * @param {object} payload 例如 { action: 'type_text', text: '...' }
270
- * @param {number} [timeoutMs]
271
- * @returns {Promise<{ok:boolean,msg?:string}>}
272
- */
273
- send(payload, timeoutMs = this.requestTimeoutMs) {
274
- this.stats.commands += 1;
275
- return this._send(payload, timeoutMs);
276
- }
277
-
278
- _send(payload, timeoutMs) {
279
- return new Promise((resolve, reject) => {
280
- if (!this.child || !this.child.stdin || this.child.stdin.destroyed) {
281
- // 子进程不可用时尝试拉起一次,仍失败则直接报错
282
- try {
283
- this.start();
284
- } catch (e) {
285
- this.stats.failures += 1;
286
- return reject(new Error(`input-agent unavailable: ${e.message}`));
287
- }
288
- }
289
-
290
- const line = `${JSON.stringify(payload)}\n`;
291
- const entry = {
292
- payload,
293
- resolve,
294
- reject,
295
- timer: setTimeout(() => {
296
- // 从等待队列里摘除自己
297
- const i = this._pending.indexOf(entry);
298
- if (i >= 0) this._pending.splice(i, 1);
299
- this.stats.timeouts += 1;
300
- this.stats.failures += 1;
301
- reject(new Error(`input-agent request timeout after ${timeoutMs}ms`));
302
- // 超时说明子进程可能卡死,主动重启
303
- this._killAndRestart('request-timeout');
304
- }, timeoutMs),
305
- };
306
- entry.timer.unref?.();
307
- this._pending.push(entry);
308
-
309
- try {
310
- this.child.stdin.write(line, (err) => {
311
- if (err) {
312
- const i = this._pending.indexOf(entry);
313
- if (i >= 0) this._pending.splice(i, 1);
314
- clearTimeout(entry.timer);
315
- this.stats.failures += 1;
316
- reject(err);
317
- }
318
- });
319
- } catch (e) {
320
- const i = this._pending.indexOf(entry);
321
- if (i >= 0) this._pending.splice(i, 1);
322
- clearTimeout(entry.timer);
323
- this.stats.failures += 1;
324
- reject(e);
325
- }
326
- });
327
- }
328
-
329
- /** 便捷方法:注入文本。 */
330
- async typeText(text, timeoutMs) {
331
- const res = await this.send({ action: 'type_text', text }, timeoutMs);
332
- return res;
333
- }
334
-
335
- /** 便捷方法:向焦点输入框发送 N 次退格(relay 对齐模式的删除原语)。 */
336
- async backspace(count, timeoutMs) {
337
- const res = await this.send({ action: 'backspace', count }, timeoutMs);
338
- return res;
339
- }
340
-
341
- /** 优雅关闭:结束 stdin -> 等待退出 -> 超时强杀。 */
342
- async stop() {
343
- this.shuttingDown = true;
344
- this._clearHeartbeat();
345
- if (this.restartTimer) {
346
- clearTimeout(this.restartTimer);
347
- this.restartTimer = null;
348
- }
349
-
350
- const child = this.child;
351
- if (!child) {
352
- this.state = STATE.STOPPED;
353
- return;
354
- }
355
- this.state = STATE.STOPPING;
356
- this._failAllPending(new Error('agent shutting down'));
357
-
358
- await new Promise((resolve) => {
359
- let settled = false;
360
- const done = () => {
361
- if (settled) return;
362
- settled = true;
363
- resolve();
364
- };
365
- child.once('exit', done);
366
- // 关闭 stdin 让 agent 的 read loop 自然结束
367
- try { child.stdin.end(); } catch { /* ignore */ }
368
- const t = setTimeout(() => {
369
- this.logger?.warn('agent did not exit in time, killing', { pid: child.pid });
370
- try { child.kill('SIGKILL'); } catch { /* ignore */ }
371
- done();
372
- }, 3000);
373
- t.unref?.();
374
- });
375
-
376
- this.child = null;
377
- this.state = STATE.STOPPED;
378
- this.logger?.info('input-agent stopped', { stats: this.stats });
379
- }
380
- }
381
-
382
- module.exports = { AgentManager, STATE };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Rust input-agent 子进程管理器。
5
+ *
6
+ * 关键设计(需求文档 4.2.2 第 4 条):
7
+ * * Node 业务层 <-> Rust 子进程通过 stdio 管道通信,单行 JSON,`\n` 分隔;
8
+ * * 刻意不使用 FFI/ABI,子进程崩溃不会拖垮 Node 主进程(故障隔离);
9
+ * * exit/close 事件触发自动重启(等待 2s);
10
+ * * 心跳检测(ping)判定卡死,超时强制重启;
11
+ * * 正常退出时优雅关闭子进程。
12
+ *
13
+ * 由于「一条输入完成再处理下一条」(串行队列在 queue.js),
14
+ * 这里对每条指令采用独立 Promise 的方式按顺序调用即可,
15
+ * 同时仍保留 inFlight 标记防止误用。
16
+ */
17
+
18
+ const { spawn } = require('child_process');
19
+ const path = require('path');
20
+ const fs = require('fs');
21
+
22
+ /** 子进程状态 */
23
+ const STATE = {
24
+ STOPPED: 'stopped',
25
+ STARTING: 'starting',
26
+ READY: 'ready',
27
+ RESTARTING: 'restarting',
28
+ STOPPING: 'stopping',
29
+ };
30
+
31
+ class AgentManager {
32
+ /**
33
+ * @param {object} opts
34
+ * @param {string} opts.binaryPath Rust 二进制绝对路径
35
+ * @param {object} opts.logger
36
+ * @param {number} [opts.restartDelayMs] 崩溃后重启延迟,默认 2000
37
+ * @param {number} [opts.requestTimeoutMs]单条指令超时(作为心跳兜底),默认 30000
38
+ * @param {number} [opts.heartbeatMs] 空闲心跳间隔,默认 15000
39
+ * @param {number} [opts.maxRestarts] 连续快速重启上限,超过则停止并告警,默认 10
40
+ */
41
+ constructor(opts) {
42
+ this.binaryPath = opts.binaryPath;
43
+ this.logger = opts.logger;
44
+ this.restartDelayMs = opts.restartDelayMs ?? 2000;
45
+ this.requestTimeoutMs = opts.requestTimeoutMs ?? 30000;
46
+ this.heartbeatMs = opts.heartbeatMs ?? 15000;
47
+ this.maxRestarts = opts.maxRestarts ?? 10;
48
+
49
+ this.child = null;
50
+ this.state = STATE.STOPPED;
51
+ this.shuttingDown = false;
52
+ this.restartCount = 0;
53
+ this.lastStartAt = 0;
54
+ this.heartbeatTimer = null;
55
+ this.restartTimer = null;
56
+
57
+ this._stdoutBuffer = '';
58
+ /** 等待中的请求:FIFO,子进程按序应答,因此队首即当前请求 */
59
+ this._pending = [];
60
+
61
+ this.stats = { spawned: 0, restarts: 0, crashes: 0, timeouts: 0, commands: 0, failures: 0 };
62
+ }
63
+
64
+ /** 启动子进程。若已启动则直接返回。 */
65
+ start() {
66
+ if (this.child) return this.child;
67
+
68
+ if (!fs.existsSync(this.binaryPath)) {
69
+ throw new Error(`input-agent binary not found: ${this.binaryPath}`);
70
+ }
71
+ try {
72
+ fs.accessSync(this.binaryPath, fs.constants.X_OK);
73
+ } catch {
74
+ // 从 npm 包解压后可能丢失可执行位,这里自动补上
75
+ try {
76
+ fs.chmodSync(this.binaryPath, 0o755);
77
+ this.logger?.debug('restored executable bit', { binaryPath: this.binaryPath });
78
+ } catch (e) {
79
+ this.logger?.warn('binary is not executable and chmod failed', {
80
+ binaryPath: this.binaryPath,
81
+ error: e.message,
82
+ });
83
+ }
84
+ }
85
+
86
+ this.state = STATE.STARTING;
87
+ this.lastStartAt = Date.now();
88
+ this._stdoutBuffer = '';
89
+
90
+ this.logger?.info('spawning rust input-agent', { binaryPath: this.binaryPath });
91
+
92
+ this.child = spawn(this.binaryPath, [], {
93
+ stdio: ['pipe', 'pipe', 'pipe'],
94
+ windowsHide: true,
95
+ // 不继承 shell,避免注入风险
96
+ shell: false,
97
+ });
98
+ this.stats.spawned += 1;
99
+
100
+ this.child.stdout.setEncoding('utf8');
101
+ this.child.stdin.setDefaultEncoding?.('utf8');
102
+
103
+ this.child.stdout.on('data', (chunk) => this._onStdout(chunk));
104
+
105
+ this.child.stderr.setEncoding('utf8');
106
+ this.child.stderr.on('data', (chunk) => {
107
+ const text = String(chunk).trimEnd();
108
+ if (text) this.logger?.debug(`[input-agent] ${text}`);
109
+ });
110
+
111
+ this.child.on('error', (e) => {
112
+ this.logger?.error('failed to spawn input-agent', { error: e.message });
113
+ this._failAllPending(new Error(`input-agent spawn error: ${e.message}`));
114
+ this._scheduleRestart('spawn-error');
115
+ });
116
+
117
+ this.child.on('exit', (code, signal) => {
118
+ const wasShuttingDown = this.shuttingDown;
119
+ const uptime = Date.now() - this.lastStartAt;
120
+ this.logger?.warn('input-agent exited', { code, signal, uptimeMs: uptime });
121
+ this._failAllPending(new Error(`input-agent exited (code=${code}, signal=${signal})`));
122
+ this.child = null;
123
+ this._clearHeartbeat();
124
+
125
+ if (wasShuttingDown) {
126
+ this.state = STATE.STOPPED;
127
+ return;
128
+ }
129
+ // 非正常退出视为崩溃
130
+ if (code !== 0 || signal) this.stats.crashes += 1;
131
+ this._scheduleRestart(`exit code=${code} signal=${signal}`);
132
+ });
133
+
134
+ this.state = STATE.READY;
135
+ this._startHeartbeat();
136
+ return this.child;
137
+ }
138
+
139
+ /** 处理子进程 stdout:按行切分并交给等待队列队首。 */
140
+ _onStdout(chunk) {
141
+ this._stdoutBuffer += String(chunk);
142
+ let idx;
143
+ // 逐行消费,最后一段可能是不完整行,留在 buffer 里等下一个 data 事件
144
+ while ((idx = this._stdoutBuffer.indexOf('\n')) >= 0) {
145
+ const line = this._stdoutBuffer.slice(0, idx).trim();
146
+ this._stdoutBuffer = this._stdoutBuffer.slice(idx + 1);
147
+ if (!line) continue;
148
+ this._deliverLine(line);
149
+ }
150
+ // 防御:buffer 异常膨胀说明协议错乱
151
+ if (this._stdoutBuffer.length > 1024 * 1024) {
152
+ this.logger?.error('stdout buffer overflow, resetting');
153
+ this._stdoutBuffer = '';
154
+ }
155
+ }
156
+
157
+ _deliverLine(line) {
158
+ let parsed;
159
+ try {
160
+ parsed = JSON.parse(line);
161
+ } catch {
162
+ this.logger?.warn('unparseable line from input-agent', { line: line.slice(0, 300) });
163
+ return; // 无法解析的行无法对应到具体请求,丢弃并继续
164
+ }
165
+ const pending = this._pending.shift();
166
+ if (!pending) {
167
+ this.logger?.debug('unexpected response with no pending request', { parsed });
168
+ return;
169
+ }
170
+ clearTimeout(pending.timer);
171
+ pending.resolve(parsed);
172
+ }
173
+
174
+ _failAllPending(err) {
175
+ while (this._pending.length) {
176
+ const p = this._pending.shift();
177
+ clearTimeout(p.timer);
178
+ p.reject(err);
179
+ }
180
+ }
181
+
182
+ _clearHeartbeat() {
183
+ if (this.heartbeatTimer) {
184
+ clearInterval(this.heartbeatTimer);
185
+ this.heartbeatTimer = null;
186
+ }
187
+ }
188
+
189
+ _startHeartbeat() {
190
+ this._clearHeartbeat();
191
+ if (this.heartbeatMs <= 0) return;
192
+ this.heartbeatTimer = setInterval(() => {
193
+ // 仅在空闲时心跳,避免与正常指令抢答
194
+ if (this._pending.length > 0 || this.state !== STATE.READY) return;
195
+ this._send({ action: 'ping' }, this.heartbeatMs)
196
+ .then((res) => {
197
+ if (!res?.ok) this.logger?.warn('heartbeat returned not-ok', { res });
198
+ })
199
+ .catch((e) => {
200
+ this.stats.timeouts += 1;
201
+ this.logger?.error('heartbeat failed, agent considered hung', { error: e.message });
202
+ this._killAndRestart('heartbeat-timeout');
203
+ });
204
+ }, this.heartbeatMs);
205
+ this.heartbeatTimer.unref?.();
206
+ }
207
+
208
+ /** 按需求:崩溃后等待 2s 自动重启;连续快速重启超限则告警停止。 */
209
+ _scheduleRestart(reason) {
210
+ if (this.shuttingDown) return;
211
+ if (this.restartTimer) return;
212
+
213
+ const uptime = Date.now() - this.lastStartAt;
214
+ // 存活 < 5s 视为快速重启
215
+ if (uptime < 5000) {
216
+ this.restartCount += 1;
217
+ } else {
218
+ this.restartCount = 0;
219
+ }
220
+
221
+ if (this.restartCount > this.maxRestarts) {
222
+ this.state = STATE.STOPPED;
223
+ this.logger?.error(
224
+ 'input-agent keeps crashing, giving up auto-restart. check platform permissions.',
225
+ { restarts: this.restartCount, binaryPath: this.binaryPath },
226
+ );
227
+ return;
228
+ }
229
+
230
+ this.state = STATE.RESTARTING;
231
+ this.stats.restarts += 1;
232
+ this.logger?.warn(`restarting input-agent in ${this.restartDelayMs}ms`, { reason });
233
+ this.restartTimer = setTimeout(() => {
234
+ this.restartTimer = null;
235
+ if (this.shuttingDown) return;
236
+ try {
237
+ this.start();
238
+ } catch (e) {
239
+ this.logger?.error('restart failed', { error: e.message });
240
+ this._scheduleRestart('restart-failed');
241
+ }
242
+ }, this.restartDelayMs);
243
+ this.restartTimer.unref?.();
244
+ }
245
+
246
+ _killAndRestart(reason) {
247
+ const child = this.child;
248
+ this._failAllPending(new Error(`agent killed: ${reason}`));
249
+ if (!child) {
250
+ this._scheduleRestart(reason);
251
+ return;
252
+ }
253
+ this.shuttingDown = false; // 仍属「重启」而非「退出」
254
+ this._childExitReason = reason;
255
+ this.logger?.warn('killing hung input-agent', { reason, pid: child.pid });
256
+ // 先 SIGTERM,超时后 SIGKILL
257
+ child.kill('SIGTERM');
258
+ const escalate = setTimeout(() => {
259
+ if (this.child === child) {
260
+ this.logger?.warn('SIGTERM ineffective, sending SIGKILL', { pid: child.pid });
261
+ try { child.kill('SIGKILL'); } catch { /* ignore */ }
262
+ }
263
+ }, 2000);
264
+ escalate.unref?.();
265
+ }
266
+
267
+ /**
268
+ * 发送一条指令并等待结果。
269
+ * @param {object} payload 例如 { action: 'type_text', text: '...' }
270
+ * @param {number} [timeoutMs]
271
+ * @returns {Promise<{ok:boolean,msg?:string}>}
272
+ */
273
+ send(payload, timeoutMs = this.requestTimeoutMs) {
274
+ this.stats.commands += 1;
275
+ return this._send(payload, timeoutMs);
276
+ }
277
+
278
+ _send(payload, timeoutMs) {
279
+ return new Promise((resolve, reject) => {
280
+ if (!this.child || !this.child.stdin || this.child.stdin.destroyed) {
281
+ // 子进程不可用时尝试拉起一次,仍失败则直接报错
282
+ try {
283
+ this.start();
284
+ } catch (e) {
285
+ this.stats.failures += 1;
286
+ return reject(new Error(`input-agent unavailable: ${e.message}`));
287
+ }
288
+ }
289
+
290
+ const line = `${JSON.stringify(payload)}\n`;
291
+ const entry = {
292
+ payload,
293
+ resolve,
294
+ reject,
295
+ timer: setTimeout(() => {
296
+ // 从等待队列里摘除自己
297
+ const i = this._pending.indexOf(entry);
298
+ if (i >= 0) this._pending.splice(i, 1);
299
+ this.stats.timeouts += 1;
300
+ this.stats.failures += 1;
301
+ reject(new Error(`input-agent request timeout after ${timeoutMs}ms`));
302
+ // 超时说明子进程可能卡死,主动重启
303
+ this._killAndRestart('request-timeout');
304
+ }, timeoutMs),
305
+ };
306
+ entry.timer.unref?.();
307
+ this._pending.push(entry);
308
+
309
+ try {
310
+ this.child.stdin.write(line, (err) => {
311
+ if (err) {
312
+ const i = this._pending.indexOf(entry);
313
+ if (i >= 0) this._pending.splice(i, 1);
314
+ clearTimeout(entry.timer);
315
+ this.stats.failures += 1;
316
+ reject(err);
317
+ }
318
+ });
319
+ } catch (e) {
320
+ const i = this._pending.indexOf(entry);
321
+ if (i >= 0) this._pending.splice(i, 1);
322
+ clearTimeout(entry.timer);
323
+ this.stats.failures += 1;
324
+ reject(e);
325
+ }
326
+ });
327
+ }
328
+
329
+ /** 便捷方法:注入文本。 */
330
+ async typeText(text, timeoutMs) {
331
+ const res = await this.send({ action: 'type_text', text }, timeoutMs);
332
+ return res;
333
+ }
334
+
335
+ /** 便捷方法:向焦点输入框发送 N 次退格(relay 对齐模式的删除原语)。 */
336
+ async backspace(count, timeoutMs) {
337
+ const res = await this.send({ action: 'backspace', count }, timeoutMs);
338
+ return res;
339
+ }
340
+
341
+ /** 优雅关闭:结束 stdin -> 等待退出 -> 超时强杀。 */
342
+ async stop() {
343
+ this.shuttingDown = true;
344
+ this._clearHeartbeat();
345
+ if (this.restartTimer) {
346
+ clearTimeout(this.restartTimer);
347
+ this.restartTimer = null;
348
+ }
349
+
350
+ const child = this.child;
351
+ if (!child) {
352
+ this.state = STATE.STOPPED;
353
+ return;
354
+ }
355
+ this.state = STATE.STOPPING;
356
+ this._failAllPending(new Error('agent shutting down'));
357
+
358
+ await new Promise((resolve) => {
359
+ let settled = false;
360
+ const done = () => {
361
+ if (settled) return;
362
+ settled = true;
363
+ resolve();
364
+ };
365
+ child.once('exit', done);
366
+ // 关闭 stdin 让 agent 的 read loop 自然结束
367
+ try { child.stdin.end(); } catch { /* ignore */ }
368
+ const t = setTimeout(() => {
369
+ this.logger?.warn('agent did not exit in time, killing', { pid: child.pid });
370
+ try { child.kill('SIGKILL'); } catch { /* ignore */ }
371
+ done();
372
+ }, 3000);
373
+ t.unref?.();
374
+ });
375
+
376
+ this.child = null;
377
+ this.state = STATE.STOPPED;
378
+ this.logger?.info('input-agent stopped', { stats: this.stats });
379
+ }
380
+ }
381
+
382
+ module.exports = { AgentManager, STATE };