@quangnv13/nonstop 1.0.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/README.md +167 -0
- package/README.vi.md +167 -0
- package/dist/bot.js +605 -0
- package/dist/config.js +174 -0
- package/dist/i18n.js +58 -0
- package/dist/index.js +50 -0
- package/dist/logger.js +86 -0
- package/dist/prompt-detection.js +27 -0
- package/dist/runtime-manager.js +77 -0
- package/dist/runtime-state.js +78 -0
- package/dist/runtime.js +622 -0
- package/dist/session-controls.js +56 -0
- package/dist/session-delivery.js +6 -0
- package/dist/session-output.js +52 -0
- package/dist/startup.js +120 -0
- package/dist/store.js +114 -0
- package/dist/terminal.js +138 -0
- package/dist/types.js +2 -0
- package/dist/ui.js +473 -0
- package/images/nonstop.png +0 -0
- package/package.json +56 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.NonstopRuntime = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const bot_js_1 = require("./bot.js");
|
|
41
|
+
const config_js_1 = require("./config.js");
|
|
42
|
+
const logger_js_1 = require("./logger.js");
|
|
43
|
+
const runtime_state_js_1 = require("./runtime-state.js");
|
|
44
|
+
const session_delivery_js_1 = require("./session-delivery.js");
|
|
45
|
+
const session_output_js_1 = require("./session-output.js");
|
|
46
|
+
const store_js_1 = require("./store.js");
|
|
47
|
+
const terminal_js_1 = require("./terminal.js");
|
|
48
|
+
class NonstopRuntime {
|
|
49
|
+
config;
|
|
50
|
+
mode;
|
|
51
|
+
startedAt = new Date().toISOString();
|
|
52
|
+
lastError = null;
|
|
53
|
+
workspaces = (0, store_js_1.loadWorkspaces)();
|
|
54
|
+
activeSession = null;
|
|
55
|
+
activeDriverRef = { current: null };
|
|
56
|
+
outputBuffer = { current: '' };
|
|
57
|
+
terminalState = createTerminalState();
|
|
58
|
+
outputTicker = null;
|
|
59
|
+
actionOutputTimeout = null;
|
|
60
|
+
heartbeatTicker = null;
|
|
61
|
+
onSessionOutputPush = null;
|
|
62
|
+
bot = null;
|
|
63
|
+
constructor(config, mode) {
|
|
64
|
+
this.config = config;
|
|
65
|
+
this.mode = mode;
|
|
66
|
+
}
|
|
67
|
+
getStatus() {
|
|
68
|
+
return {
|
|
69
|
+
pid: process.pid,
|
|
70
|
+
startedAt: this.startedAt,
|
|
71
|
+
lastHeartbeatAt: new Date().toISOString(),
|
|
72
|
+
mode: this.mode,
|
|
73
|
+
clientName: this.config.clientName || os.hostname() || 'LocalClient',
|
|
74
|
+
botRunning: this.bot !== null,
|
|
75
|
+
workspaceCount: this.workspaces.length,
|
|
76
|
+
activeSession: this.activeSession,
|
|
77
|
+
lastError: this.lastError
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
getWorkspaces() {
|
|
81
|
+
return this.workspaces;
|
|
82
|
+
}
|
|
83
|
+
saveWorkspaces(nextWorkspaces) {
|
|
84
|
+
this.workspaces = [...nextWorkspaces];
|
|
85
|
+
(0, store_js_1.saveWorkspaces)(this.workspaces);
|
|
86
|
+
this.writeHeartbeat();
|
|
87
|
+
}
|
|
88
|
+
getActiveSession() {
|
|
89
|
+
return this.activeSession;
|
|
90
|
+
}
|
|
91
|
+
getConfig() {
|
|
92
|
+
return this.config;
|
|
93
|
+
}
|
|
94
|
+
async saveConfig(nextConfig) {
|
|
95
|
+
const tokenChanged = this.config.telegramBotToken !== nextConfig.telegramBotToken;
|
|
96
|
+
this.config = nextConfig;
|
|
97
|
+
(0, config_js_1.saveConfigToDisk)(nextConfig);
|
|
98
|
+
(0, config_js_1.applyConfigToProcessEnv)(nextConfig);
|
|
99
|
+
this.writeHeartbeat();
|
|
100
|
+
if (tokenChanged) {
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
void this.restartBot();
|
|
103
|
+
}, 1000);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async restartBot() {
|
|
107
|
+
logger_js_1.logger.info('Restarting Telegram bot due to token change...');
|
|
108
|
+
await this.stopBot();
|
|
109
|
+
await this.startBot();
|
|
110
|
+
}
|
|
111
|
+
async startBot() {
|
|
112
|
+
if (this.bot) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
logger_js_1.logger.info('nonstop runtime bootstrap complete', {
|
|
116
|
+
clientName: this.config.clientName,
|
|
117
|
+
telegramUsername: this.config.telegramUsername,
|
|
118
|
+
workspaceCount: this.workspaces.length,
|
|
119
|
+
supportedPresets: terminal_js_1.SUPPORTED_PRESETS,
|
|
120
|
+
mode: this.mode
|
|
121
|
+
});
|
|
122
|
+
this.bot = (0, bot_js_1.createBotRuntime)({
|
|
123
|
+
getConfig: () => this.getConfig(),
|
|
124
|
+
saveConfig: async (config) => {
|
|
125
|
+
await this.saveConfig(config);
|
|
126
|
+
},
|
|
127
|
+
getWorkspaces: () => this.getWorkspaces(),
|
|
128
|
+
saveWorkspaces: (workspaces) => this.saveWorkspaces(workspaces),
|
|
129
|
+
getActiveSession: () => this.getActiveSession(),
|
|
130
|
+
startSession: async (chatId, workspaceId, preset) => {
|
|
131
|
+
await this.startSession(chatId, this.resolveWorkspaceById(workspaceId), preset);
|
|
132
|
+
},
|
|
133
|
+
stopSession: async () => this.stopSession(),
|
|
134
|
+
sendInput: (data) => this.sendSessionInput(data),
|
|
135
|
+
sendKey: (key) => this.sendSessionKey(key),
|
|
136
|
+
setInputMode: (inputMode) => this.setSessionInputMode(inputMode),
|
|
137
|
+
setAutoEnter: (autoEnter) => this.setSessionAutoEnter(autoEnter)
|
|
138
|
+
});
|
|
139
|
+
this.setSessionOutputPushCallback(async (chatId, text, options) => {
|
|
140
|
+
await this.bot?.pushSessionOutput(chatId, text, options);
|
|
141
|
+
});
|
|
142
|
+
// Ghi heartbeat TRƯỚC khi bot connect để UI polling nhận ngay trạng thái RUNNING
|
|
143
|
+
this.startHeartbeat();
|
|
144
|
+
await this.bot.start({
|
|
145
|
+
onStart: async (botInfo) => {
|
|
146
|
+
logger_js_1.logger.info('Telegram bot đã khởi động', {
|
|
147
|
+
username: botInfo.username,
|
|
148
|
+
mode: this.mode
|
|
149
|
+
});
|
|
150
|
+
// Gửi thông báo hello tới Telegram
|
|
151
|
+
const lastChatId = (0, bot_js_1.loadLastChatId)();
|
|
152
|
+
if (lastChatId && this.bot) {
|
|
153
|
+
try {
|
|
154
|
+
await this.bot.pushSessionOutput(lastChatId, `✅ nonstop client đã khởi động thành công và đang chạy!\n🖥 Client: ${this.config.clientName}`);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// ignore
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async stopBot() {
|
|
164
|
+
await this.stopSession();
|
|
165
|
+
if (this.bot) {
|
|
166
|
+
await this.bot.stop();
|
|
167
|
+
this.bot = null;
|
|
168
|
+
}
|
|
169
|
+
if (this.heartbeatTicker) {
|
|
170
|
+
clearInterval(this.heartbeatTicker);
|
|
171
|
+
this.heartbeatTicker = null;
|
|
172
|
+
}
|
|
173
|
+
(0, runtime_state_js_1.clearRuntimeState)();
|
|
174
|
+
}
|
|
175
|
+
setSessionInputMode(inputMode) {
|
|
176
|
+
if (this.activeSession) {
|
|
177
|
+
this.activeSession.inputMode = inputMode;
|
|
178
|
+
this.writeHeartbeat();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
setSessionAutoEnter(autoEnter) {
|
|
182
|
+
if (this.activeSession) {
|
|
183
|
+
this.activeSession.autoEnter = autoEnter;
|
|
184
|
+
this.writeHeartbeat();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async startSession(chatId, workspace, preset) {
|
|
188
|
+
if (this.activeSession?.status === 'running') {
|
|
189
|
+
throw new Error(`Session "${this.activeSession.sessionId}" is already running.`);
|
|
190
|
+
}
|
|
191
|
+
const cwd = path.resolve(workspace.path);
|
|
192
|
+
if (!fs.existsSync(cwd)) {
|
|
193
|
+
throw new Error(`Workspace path "${cwd}" does not exist.`);
|
|
194
|
+
}
|
|
195
|
+
const { command, args } = (0, terminal_js_1.resolvePreset)(preset);
|
|
196
|
+
const sessionId = createSessionId(preset);
|
|
197
|
+
this.resetOutputRuntime();
|
|
198
|
+
const nextSession = {
|
|
199
|
+
sessionId,
|
|
200
|
+
preset,
|
|
201
|
+
cwd,
|
|
202
|
+
status: 'running',
|
|
203
|
+
listenerChatId: chatId,
|
|
204
|
+
lastSentFinalText: '',
|
|
205
|
+
inputMode: true,
|
|
206
|
+
autoEnter: true
|
|
207
|
+
};
|
|
208
|
+
logger_js_1.logger.info('Starting local session', {
|
|
209
|
+
sessionId,
|
|
210
|
+
preset,
|
|
211
|
+
chatId,
|
|
212
|
+
cwd,
|
|
213
|
+
command,
|
|
214
|
+
args
|
|
215
|
+
});
|
|
216
|
+
try {
|
|
217
|
+
const driver = new terminal_js_1.NodePtyTerminalDriver(command, args, cwd);
|
|
218
|
+
this.activeSession = nextSession;
|
|
219
|
+
this.activeDriverRef.current = driver;
|
|
220
|
+
this.writeHeartbeat();
|
|
221
|
+
driver.onData((chunk) => {
|
|
222
|
+
this.bufferOutput(chunk);
|
|
223
|
+
});
|
|
224
|
+
driver.onExit((code, signal) => {
|
|
225
|
+
void this.handleDriverExit(sessionId, code, signal);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
this.activeSession = null;
|
|
230
|
+
this.activeDriverRef.current = null;
|
|
231
|
+
this.resetOutputRuntime();
|
|
232
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
233
|
+
this.writeHeartbeat();
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async stopSession() {
|
|
238
|
+
const session = this.activeSession;
|
|
239
|
+
if (!session || session.status !== 'running') {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
session.status = 'stopped';
|
|
243
|
+
const driver = this.activeDriverRef.current;
|
|
244
|
+
this.activeDriverRef.current = null;
|
|
245
|
+
if (driver) {
|
|
246
|
+
driver.kill();
|
|
247
|
+
}
|
|
248
|
+
await this.flushOutput(true);
|
|
249
|
+
this.resetOutputRuntime();
|
|
250
|
+
this.activeSession = null;
|
|
251
|
+
this.writeHeartbeat();
|
|
252
|
+
logger_js_1.logger.info('Stopped local session', {
|
|
253
|
+
sessionId: session.sessionId
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
sendSessionInput(data) {
|
|
257
|
+
const driver = this.activeDriverRef.current;
|
|
258
|
+
if (!driver || this.activeSession?.status !== 'running') {
|
|
259
|
+
logger_js_1.logger.warn('Dropping session input because no active session is running', {
|
|
260
|
+
length: data.length
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
driver.write(data);
|
|
265
|
+
}
|
|
266
|
+
sendSessionKey(key) {
|
|
267
|
+
const driver = this.activeDriverRef.current;
|
|
268
|
+
const session = this.activeSession;
|
|
269
|
+
if (!driver || !session || session.status !== 'running') {
|
|
270
|
+
logger_js_1.logger.warn('Dropping session key because no active session is running', { key });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const input = resolveKeyInput(key, session.preset);
|
|
274
|
+
if (!input) {
|
|
275
|
+
logger_js_1.logger.warn('Ignoring unsupported session key', {
|
|
276
|
+
key,
|
|
277
|
+
preset: session.preset
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
driver.write(input);
|
|
282
|
+
if (['send_escape', 'send_enter', 'send_up', 'send_down'].includes(key)) {
|
|
283
|
+
if (this.outputTicker) {
|
|
284
|
+
clearInterval(this.outputTicker);
|
|
285
|
+
this.outputTicker = null;
|
|
286
|
+
}
|
|
287
|
+
if (this.actionOutputTimeout) {
|
|
288
|
+
clearTimeout(this.actionOutputTimeout);
|
|
289
|
+
this.actionOutputTimeout = null;
|
|
290
|
+
}
|
|
291
|
+
this.actionOutputTimeout = setTimeout(async () => {
|
|
292
|
+
this.actionOutputTimeout = null;
|
|
293
|
+
await this.flushOutput(true);
|
|
294
|
+
this.ensureOutputTicker();
|
|
295
|
+
}, 5000);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
resolveWorkspaceById(workspaceId) {
|
|
299
|
+
const workspace = this.workspaces.find((candidate) => candidate.id === workspaceId);
|
|
300
|
+
if (!workspace) {
|
|
301
|
+
throw new Error(`Workspace "${workspaceId}" not found.`);
|
|
302
|
+
}
|
|
303
|
+
return workspace;
|
|
304
|
+
}
|
|
305
|
+
setSessionOutputPushCallback(callback) {
|
|
306
|
+
this.onSessionOutputPush = callback;
|
|
307
|
+
}
|
|
308
|
+
startHeartbeat() {
|
|
309
|
+
this.writeHeartbeat();
|
|
310
|
+
if (this.heartbeatTicker) {
|
|
311
|
+
clearInterval(this.heartbeatTicker);
|
|
312
|
+
}
|
|
313
|
+
this.heartbeatTicker = setInterval(() => {
|
|
314
|
+
this.writeHeartbeat();
|
|
315
|
+
}, 2000);
|
|
316
|
+
}
|
|
317
|
+
writeHeartbeat() {
|
|
318
|
+
(0, runtime_state_js_1.saveRuntimeState)(this.getStatus());
|
|
319
|
+
}
|
|
320
|
+
async handleDriverExit(sessionId, code, signal) {
|
|
321
|
+
const session = this.activeSession;
|
|
322
|
+
if (!session || session.sessionId !== sessionId || session.status !== 'running') {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
session.status = 'stopped';
|
|
326
|
+
this.activeDriverRef.current = null;
|
|
327
|
+
await this.flushOutput(true);
|
|
328
|
+
this.resetOutputRuntime();
|
|
329
|
+
this.activeSession = null;
|
|
330
|
+
this.writeHeartbeat();
|
|
331
|
+
logger_js_1.logger.warn('Local PTY session exited', {
|
|
332
|
+
sessionId,
|
|
333
|
+
code,
|
|
334
|
+
signal
|
|
335
|
+
});
|
|
336
|
+
if (this.onSessionOutputPush) {
|
|
337
|
+
await this.onSessionOutputPush(session.listenerChatId, `Session \`${sessionId}\` exited with code \`${code}\`.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
bufferOutput(chunk) {
|
|
341
|
+
if (!this.activeSession) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.outputBuffer.current += chunk;
|
|
345
|
+
applyTerminalOutput(this.terminalState, chunk, this.config.maxRenderLines);
|
|
346
|
+
this.ensureOutputTicker();
|
|
347
|
+
}
|
|
348
|
+
async flushOutput(forceSnapshot = false) {
|
|
349
|
+
const session = this.activeSession;
|
|
350
|
+
if (!session) {
|
|
351
|
+
this.outputBuffer.current = '';
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const text = this.outputBuffer.current;
|
|
355
|
+
this.outputBuffer.current = '';
|
|
356
|
+
const promptDetectionText = stripAnsi(text);
|
|
357
|
+
const snapshot = renderTerminalSnapshot(this.terminalState, this.config.maxOutputLines);
|
|
358
|
+
const finalText = snapshot || limitLines(promptDetectionText, this.config.maxOutputLines);
|
|
359
|
+
if (!text && !forceSnapshot) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!finalText.trim()) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// Lọc loading spinner và window title residue
|
|
366
|
+
if (isSpinnerOrNoiseOutput(finalText)) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if ((0, session_delivery_js_1.shouldSkipSessionOutput)(session.lastSentFinalText, finalText)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const messages = (0, session_output_js_1.buildSessionOutputMessages)({
|
|
373
|
+
sessionId: session.sessionId,
|
|
374
|
+
snapshot: finalText,
|
|
375
|
+
inputMode: session.inputMode,
|
|
376
|
+
autoEnter: session.autoEnter
|
|
377
|
+
});
|
|
378
|
+
if (!this.onSessionOutputPush) {
|
|
379
|
+
session.lastSentFinalText = finalText;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
for (const message of messages) {
|
|
383
|
+
await this.onSessionOutputPush(session.listenerChatId, message.text, message.options);
|
|
384
|
+
}
|
|
385
|
+
session.lastSentFinalText = finalText;
|
|
386
|
+
}
|
|
387
|
+
ensureOutputTicker() {
|
|
388
|
+
if (this.outputTicker || this.actionOutputTimeout) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
this.outputTicker = setInterval(() => {
|
|
392
|
+
void this.flushOutput(true);
|
|
393
|
+
}, this.config.outputInterval);
|
|
394
|
+
}
|
|
395
|
+
resetOutputRuntime() {
|
|
396
|
+
if (this.outputTicker) {
|
|
397
|
+
clearInterval(this.outputTicker);
|
|
398
|
+
this.outputTicker = null;
|
|
399
|
+
}
|
|
400
|
+
if (this.actionOutputTimeout) {
|
|
401
|
+
clearTimeout(this.actionOutputTimeout);
|
|
402
|
+
this.actionOutputTimeout = null;
|
|
403
|
+
}
|
|
404
|
+
this.outputBuffer.current = '';
|
|
405
|
+
this.terminalState = createTerminalState();
|
|
406
|
+
if (this.activeSession) {
|
|
407
|
+
this.activeSession.lastSentFinalText = '';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
exports.NonstopRuntime = NonstopRuntime;
|
|
412
|
+
function createSessionId(preset) {
|
|
413
|
+
return `${preset}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
414
|
+
}
|
|
415
|
+
function resolveKeyInput(key, preset) {
|
|
416
|
+
switch (key) {
|
|
417
|
+
case 'send_up':
|
|
418
|
+
case 'up':
|
|
419
|
+
return '\u001b[A';
|
|
420
|
+
case 'send_down':
|
|
421
|
+
case 'down':
|
|
422
|
+
return '\u001b[B';
|
|
423
|
+
case 'send_enter':
|
|
424
|
+
case 'enter':
|
|
425
|
+
return '\r';
|
|
426
|
+
case 'send_escape':
|
|
427
|
+
case 'escape':
|
|
428
|
+
case 'interrupt':
|
|
429
|
+
if (preset === 'codex' || preset === 'antigravity') {
|
|
430
|
+
return '\u001b';
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
default:
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function limitLines(text, maxLines) {
|
|
438
|
+
const lines = text.split(/\r?\n/);
|
|
439
|
+
if (lines.length <= maxLines) {
|
|
440
|
+
return text;
|
|
441
|
+
}
|
|
442
|
+
return lines.slice(-maxLines).join('\n');
|
|
443
|
+
}
|
|
444
|
+
function stripAnsi(text) {
|
|
445
|
+
const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
446
|
+
return text.replace(ansiRegex, '');
|
|
447
|
+
}
|
|
448
|
+
function createTerminalState() {
|
|
449
|
+
return { lines: [''], row: 0, col: 0, savedRow: 0, savedCol: 0 };
|
|
450
|
+
}
|
|
451
|
+
function ensureLine(state, row) {
|
|
452
|
+
while (state.lines.length <= row) {
|
|
453
|
+
state.lines.push('');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function writeAt(state, char) {
|
|
457
|
+
ensureLine(state, state.row);
|
|
458
|
+
const current = state.lines[state.row];
|
|
459
|
+
const padded = current.padEnd(state.col, ' ');
|
|
460
|
+
state.lines[state.row] = padded.slice(0, state.col) + char + padded.slice(state.col + 1);
|
|
461
|
+
state.col += 1;
|
|
462
|
+
}
|
|
463
|
+
function clearLineFromCursor(state) {
|
|
464
|
+
ensureLine(state, state.row);
|
|
465
|
+
state.lines[state.row] = state.lines[state.row].slice(0, state.col);
|
|
466
|
+
}
|
|
467
|
+
function trimTerminalHistory(state, maxRenderLines) {
|
|
468
|
+
if (state.lines.length <= maxRenderLines) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const removeCount = state.lines.length - maxRenderLines;
|
|
472
|
+
state.lines = state.lines.slice(removeCount);
|
|
473
|
+
state.row = Math.max(0, state.row - removeCount);
|
|
474
|
+
state.savedRow = Math.max(0, state.savedRow - removeCount);
|
|
475
|
+
}
|
|
476
|
+
function handleCsiSequence(state, paramsRaw, command) {
|
|
477
|
+
const privateMode = paramsRaw.startsWith('?');
|
|
478
|
+
const normalized = privateMode ? paramsRaw.slice(1) : paramsRaw;
|
|
479
|
+
const params = normalized.length > 0
|
|
480
|
+
? normalized.split(';').map((value) => {
|
|
481
|
+
const parsed = parseInt(value, 10);
|
|
482
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
483
|
+
})
|
|
484
|
+
: [];
|
|
485
|
+
switch (command) {
|
|
486
|
+
case 'A':
|
|
487
|
+
state.row = Math.max(0, state.row - (params[0] || 1));
|
|
488
|
+
return;
|
|
489
|
+
case 'B':
|
|
490
|
+
state.row += params[0] || 1;
|
|
491
|
+
ensureLine(state, state.row);
|
|
492
|
+
return;
|
|
493
|
+
case 'C':
|
|
494
|
+
state.col += params[0] || 1;
|
|
495
|
+
return;
|
|
496
|
+
case 'D':
|
|
497
|
+
state.col = Math.max(0, state.col - (params[0] || 1));
|
|
498
|
+
return;
|
|
499
|
+
case 'G':
|
|
500
|
+
state.col = Math.max(0, (params[0] || 1) - 1);
|
|
501
|
+
return;
|
|
502
|
+
case 'H':
|
|
503
|
+
case 'f':
|
|
504
|
+
state.row = Math.max(0, (params[0] || 1) - 1);
|
|
505
|
+
state.col = Math.max(0, (params[1] || 1) - 1);
|
|
506
|
+
ensureLine(state, state.row);
|
|
507
|
+
return;
|
|
508
|
+
case 'J':
|
|
509
|
+
if ((params[0] || 0) === 2) {
|
|
510
|
+
state.lines = [''];
|
|
511
|
+
state.row = 0;
|
|
512
|
+
state.col = 0;
|
|
513
|
+
state.savedRow = 0;
|
|
514
|
+
state.savedCol = 0;
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
case 'K':
|
|
518
|
+
clearLineFromCursor(state);
|
|
519
|
+
return;
|
|
520
|
+
case 's':
|
|
521
|
+
state.savedRow = state.row;
|
|
522
|
+
state.savedCol = state.col;
|
|
523
|
+
return;
|
|
524
|
+
case 'u':
|
|
525
|
+
state.row = state.savedRow;
|
|
526
|
+
state.col = state.savedCol;
|
|
527
|
+
ensureLine(state, state.row);
|
|
528
|
+
return;
|
|
529
|
+
default:
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function applyTerminalOutput(state, chunk, maxRenderLines) {
|
|
534
|
+
let index = 0;
|
|
535
|
+
while (index < chunk.length) {
|
|
536
|
+
const char = chunk[index];
|
|
537
|
+
if (char === '\u001b') {
|
|
538
|
+
const next = chunk[index + 1];
|
|
539
|
+
if (next === '[') {
|
|
540
|
+
const match = chunk.slice(index).match(/^\u001b\[([0-9;?]*)([@-~])/);
|
|
541
|
+
if (match) {
|
|
542
|
+
handleCsiSequence(state, match[1], match[2]);
|
|
543
|
+
index += match[0].length;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (next === ']') {
|
|
548
|
+
const belIndex = chunk.indexOf('\u0007', index + 2);
|
|
549
|
+
if (belIndex !== -1) {
|
|
550
|
+
index = belIndex + 1;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
index += 1;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (char === '\r') {
|
|
558
|
+
state.col = 0;
|
|
559
|
+
index += 1;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (char === '\n') {
|
|
563
|
+
state.row += 1;
|
|
564
|
+
state.col = 0;
|
|
565
|
+
ensureLine(state, state.row);
|
|
566
|
+
trimTerminalHistory(state, maxRenderLines);
|
|
567
|
+
index += 1;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (char === '\b') {
|
|
571
|
+
state.col = Math.max(0, state.col - 1);
|
|
572
|
+
index += 1;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (char === '\t') {
|
|
576
|
+
const spaces = 4 - (state.col % 4);
|
|
577
|
+
for (let i = 0; i < spaces; i += 1) {
|
|
578
|
+
writeAt(state, ' ');
|
|
579
|
+
}
|
|
580
|
+
index += 1;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (char >= ' ') {
|
|
584
|
+
writeAt(state, char);
|
|
585
|
+
}
|
|
586
|
+
index += 1;
|
|
587
|
+
}
|
|
588
|
+
trimTerminalHistory(state, maxRenderLines);
|
|
589
|
+
}
|
|
590
|
+
function renderTerminalSnapshot(state, maxOutputLines) {
|
|
591
|
+
const rawText = state.lines
|
|
592
|
+
.map((line) => line.replace(/\s+$/g, ''))
|
|
593
|
+
.join('\n')
|
|
594
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
595
|
+
.trim();
|
|
596
|
+
if (!rawText) {
|
|
597
|
+
return '';
|
|
598
|
+
}
|
|
599
|
+
const rawLines = rawText.split('\n');
|
|
600
|
+
const nonEmptyLines = rawLines.filter((line) => line.trim().length > 0);
|
|
601
|
+
const commonIndent = nonEmptyLines.length > 0
|
|
602
|
+
? Math.min(...nonEmptyLines.map((line) => line.match(/^ */)?.[0].length ?? 0))
|
|
603
|
+
: 0;
|
|
604
|
+
return limitLines(rawLines.map((line) => line.slice(commonIndent)).join('\n').trim(), maxOutputLines);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Lọc bỏ output chỉ chứa loading spinner, window title, hoặc ký tự thừa trước khi gửi Telegram.
|
|
608
|
+
*/
|
|
609
|
+
function isSpinnerOrNoiseOutput(text) {
|
|
610
|
+
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
611
|
+
if (lines.length === 0)
|
|
612
|
+
return true;
|
|
613
|
+
// Nếu tất cả các dòng đều là noise thì bỏ
|
|
614
|
+
const noisePatterns = [
|
|
615
|
+
/^[\u2800-\u28FF\s]+$/, // Braille spinner
|
|
616
|
+
/^\]0;/, // Window title sequence
|
|
617
|
+
/^q{1,4}\d?\w{0,5}$/, // "q", "q8", "qrk" etc.
|
|
618
|
+
/^[\s\u2800-\u28FF\]0;q\r\n]{1,20}$/ // Short mixed noise
|
|
619
|
+
];
|
|
620
|
+
const allNoise = lines.every(line => noisePatterns.some(pattern => pattern.test(line)));
|
|
621
|
+
return allNoise;
|
|
622
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSessionActionMarkup = buildSessionActionMarkup;
|
|
4
|
+
function buildSessionActionMarkup(options) {
|
|
5
|
+
const inputMode = options.inputMode ?? true;
|
|
6
|
+
const autoEnter = options.autoEnter ?? true;
|
|
7
|
+
const rows = [
|
|
8
|
+
[
|
|
9
|
+
{
|
|
10
|
+
text: inputMode ? '⌨️ Input OFF' : '⌨️ Input ON',
|
|
11
|
+
callback_data: `session_cmd:${options.sessionId}:toggle_input`
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
text: autoEnter ? '⏎ AutoEnter OFF' : '⏎ AutoEnter ON',
|
|
15
|
+
callback_data: `session_cmd:${options.sessionId}:toggle_enter`
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
text: '🔄 Refresh',
|
|
19
|
+
callback_data: `session_cmd:${options.sessionId}:refresh`
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
{
|
|
24
|
+
text: '⛔ Esc',
|
|
25
|
+
callback_data: `session_cmd:${options.sessionId}:send_escape`
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
text: '⬆️ Up',
|
|
29
|
+
callback_data: `session_cmd:${options.sessionId}:send_up`
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
text: '⬇️ Down',
|
|
33
|
+
callback_data: `session_cmd:${options.sessionId}:send_down`
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
text: '⏎ Enter',
|
|
39
|
+
callback_data: `session_cmd:${options.sessionId}:send_enter`
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
text: '🛑 Stop',
|
|
43
|
+
callback_data: `session_cmd:${options.sessionId}:stop`
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
];
|
|
47
|
+
if (options.includeBackButton) {
|
|
48
|
+
rows.push([
|
|
49
|
+
{
|
|
50
|
+
text: '⬅️ Back',
|
|
51
|
+
callback_data: 'sessions_list'
|
|
52
|
+
}
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
return { inline_keyboard: rows };
|
|
56
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shouldSkipSessionOutput = shouldSkipSessionOutput;
|
|
4
|
+
function shouldSkipSessionOutput(previousFinalText, nextFinalText) {
|
|
5
|
+
return Boolean(previousFinalText) && previousFinalText === nextFinalText;
|
|
6
|
+
}
|