aiexecode 1.0.94 → 1.0.127
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.
Potentially problematic release.
This version of aiexecode might be problematic. Click here for more details.
- package/README.md +198 -88
- package/index.js +310 -86
- package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
- package/package.json +4 -4
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
- package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +3 -3
- package/payload_viewer/web_server.js +361 -0
- package/prompts/completion_judge.txt +4 -0
- package/prompts/orchestrator.txt +116 -3
- package/src/LLMClient/client.js +401 -18
- package/src/LLMClient/converters/responses-to-claude.js +67 -18
- package/src/LLMClient/converters/responses-to-zai.js +667 -0
- package/src/LLMClient/errors.js +30 -4
- package/src/LLMClient/index.js +5 -0
- package/src/ai_based/completion_judge.js +263 -186
- package/src/ai_based/orchestrator.js +171 -35
- package/src/commands/agents.js +70 -0
- package/src/commands/apikey.js +1 -1
- package/src/commands/bg.js +129 -0
- package/src/commands/commands.js +51 -0
- package/src/commands/debug.js +52 -0
- package/src/commands/help.js +11 -1
- package/src/commands/model.js +42 -7
- package/src/commands/reasoning_effort.js +2 -2
- package/src/commands/skills.js +46 -0
- package/src/config/ai_models.js +106 -6
- package/src/config/constants.js +71 -0
- package/src/config/feature_flags.js +6 -7
- package/src/frontend/App.js +108 -1
- package/src/frontend/components/AutocompleteMenu.js +7 -1
- package/src/frontend/components/BackgroundProcessList.js +175 -0
- package/src/frontend/components/ConversationItem.js +26 -10
- package/src/frontend/components/CurrentModelView.js +2 -2
- package/src/frontend/components/HelpView.js +106 -2
- package/src/frontend/components/Input.js +33 -11
- package/src/frontend/components/ModelListView.js +1 -1
- package/src/frontend/components/SetupWizard.js +51 -8
- package/src/frontend/hooks/useFileCompletion.js +467 -0
- package/src/frontend/utils/toolUIFormatter.js +261 -0
- package/src/system/agents_loader.js +289 -0
- package/src/system/ai_request.js +156 -12
- package/src/system/background_process.js +317 -0
- package/src/system/code_executer.js +496 -56
- package/src/system/command_parser.js +33 -3
- package/src/system/conversation_state.js +265 -0
- package/src/system/conversation_trimmer.js +132 -0
- package/src/system/custom_command_loader.js +386 -0
- package/src/system/file_integrity.js +73 -10
- package/src/system/log.js +10 -2
- package/src/system/output_helper.js +52 -9
- package/src/system/session.js +213 -58
- package/src/system/session_memory.js +30 -2
- package/src/system/skill_loader.js +318 -0
- package/src/system/system_info.js +254 -40
- package/src/system/tool_approval.js +10 -0
- package/src/system/tool_registry.js +15 -1
- package/src/system/ui_events.js +11 -0
- package/src/tools/code_editor.js +16 -10
- package/src/tools/file_reader.js +66 -9
- package/src/tools/glob.js +0 -3
- package/src/tools/ripgrep.js +5 -7
- package/src/tools/skill_tool.js +122 -0
- package/src/tools/web_downloader.js +0 -3
- package/src/util/clone.js +174 -0
- package/src/util/config.js +55 -2
- package/src/util/config_migration.js +174 -0
- package/src/util/debug_log.js +8 -2
- package/src/util/exit_handler.js +8 -0
- package/src/util/file_reference_parser.js +132 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +91 -1
- package/src/util/safe_fs.js +66 -3
- package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_ssgManifest.js +0 -0
package/src/system/ai_request.js
CHANGED
|
@@ -10,6 +10,26 @@ import { UnifiedLLMClient } from "../LLMClient/index.js";
|
|
|
10
10
|
|
|
11
11
|
const debugLog = createDebugLogger('ai_request.log', 'ai_request');
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* 안전한 JSON 파싱 헬퍼 함수
|
|
15
|
+
* 파싱 실패 시 에러를 던지지 않고 기본값 반환
|
|
16
|
+
* @param {string} jsonString - 파싱할 JSON 문자열
|
|
17
|
+
* @param {*} defaultValue - 파싱 실패 시 반환할 기본값
|
|
18
|
+
* @returns {*} 파싱된 객체 또는 기본값
|
|
19
|
+
*/
|
|
20
|
+
function safeJsonParse(jsonString, defaultValue = null) {
|
|
21
|
+
if (typeof jsonString !== 'string') {
|
|
22
|
+
return defaultValue;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(jsonString);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
debugLog(`[safeJsonParse] JSON parsing failed: ${error.message}`);
|
|
28
|
+
debugLog(`[safeJsonParse] Input (first 200 chars): ${jsonString.substring(0, 200)}`);
|
|
29
|
+
return defaultValue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
// OpenAI SDK를 사용하기 위한 환경변수를 불러옵니다.
|
|
14
34
|
dotenv.config({ quiet: true });
|
|
15
35
|
|
|
@@ -55,15 +75,24 @@ async function getLLMClient() {
|
|
|
55
75
|
const currentModel = process.env.MODEL || settings?.MODEL || DEFAULT_MODEL;
|
|
56
76
|
const modelInfo = getModelInfo(currentModel);
|
|
57
77
|
const provider = modelInfo?.provider || 'openai';
|
|
78
|
+
const baseUrl = process.env.BASE_URL || settings?.BASE_URL || null;
|
|
58
79
|
|
|
59
|
-
debugLog('[getLLMClient] Initializing UnifiedLLMClient with API key
|
|
60
|
-
debugLog(`[getLLMClient] Model: ${currentModel}, Provider: ${provider}`);
|
|
61
|
-
|
|
80
|
+
debugLog('[getLLMClient] Initializing UnifiedLLMClient with API key: [PRESENT]');
|
|
81
|
+
debugLog(`[getLLMClient] Model: ${currentModel}, Provider: ${provider}, BaseURL: ${baseUrl || '(default)'}`);
|
|
82
|
+
|
|
83
|
+
const clientConfig = {
|
|
62
84
|
apiKey: process.env.API_KEY,
|
|
63
85
|
model: currentModel,
|
|
64
86
|
provider: provider,
|
|
65
87
|
logDir: PAYLOAD_LLM_LOG_DIR
|
|
66
|
-
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// 커스텀 BASE_URL이 설정되어 있으면 추가
|
|
91
|
+
if (baseUrl) {
|
|
92
|
+
clientConfig.baseUrl = baseUrl;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
llmClient = new UnifiedLLMClient(clientConfig);
|
|
67
96
|
|
|
68
97
|
debugLog('[getLLMClient] Client created successfully');
|
|
69
98
|
return llmClient;
|
|
@@ -181,6 +210,7 @@ function toAbsolutePath(anyPath) {
|
|
|
181
210
|
// 모든 AI 요청 과정에서 요청/응답 로그를 남기고 결과를 돌려줍니다.
|
|
182
211
|
export async function request(taskName, requestPayload) {
|
|
183
212
|
debugLog(`[request] ========== START: ${taskName} ==========`);
|
|
213
|
+
|
|
184
214
|
const provider = await getCurrentProvider();
|
|
185
215
|
debugLog(`[request] Provider: ${provider}`);
|
|
186
216
|
|
|
@@ -218,8 +248,8 @@ export async function request(taskName, requestPayload) {
|
|
|
218
248
|
const msg = payloadCopy.input[i];
|
|
219
249
|
const { type, call_id, output } = msg;
|
|
220
250
|
if (type !== 'function_call_output') continue;
|
|
221
|
-
const parsedOutput =
|
|
222
|
-
if (!isValidJSON(parsedOutput.stdout)) {
|
|
251
|
+
const parsedOutput = safeJsonParse(output, { stdout: output });
|
|
252
|
+
if (!parsedOutput || !isValidJSON(parsedOutput.stdout)) {
|
|
223
253
|
// stdout와 stderr를 모두 포함하는 형식으로 변환
|
|
224
254
|
const hasStdout = parsedOutput.stdout;
|
|
225
255
|
const hasStderr = parsedOutput.stderr;
|
|
@@ -235,7 +265,10 @@ export async function request(taskName, requestPayload) {
|
|
|
235
265
|
msg.output = parsedOutput.stdout;
|
|
236
266
|
}
|
|
237
267
|
} else {
|
|
238
|
-
|
|
268
|
+
const parsedStdout = safeJsonParse(parsedOutput.stdout, null);
|
|
269
|
+
if (parsedStdout !== null) {
|
|
270
|
+
parsedOutput.stdout = parsedStdout;
|
|
271
|
+
}
|
|
239
272
|
if (parsedOutput.original_result) {
|
|
240
273
|
parsedOutput.stdout = ({ ...parsedOutput.original_result, ...(parsedOutput.stdout) });
|
|
241
274
|
delete parsedOutput.original_result;
|
|
@@ -496,16 +529,24 @@ export async function request(taskName, requestPayload) {
|
|
|
496
529
|
debugLog(`[request] Request prepared - logging to file`);
|
|
497
530
|
|
|
498
531
|
// 로그는 원본 포맷으로 저장 (API 호출 전)
|
|
532
|
+
const logStartTime = Date.now();
|
|
499
533
|
await logger(`${taskName}_REQ`, originalRequest, provider);
|
|
500
|
-
debugLog(`[request] Request logged - calling LLM API`);
|
|
534
|
+
debugLog(`[request] Request logged (${Date.now() - logStartTime}ms) - calling LLM API`);
|
|
501
535
|
|
|
502
536
|
// AbortController의 signal을 options로 전달
|
|
503
537
|
const requestOptions = {
|
|
504
538
|
signal: currentAbortController.signal
|
|
505
539
|
};
|
|
506
|
-
|
|
540
|
+
|
|
541
|
+
// ===== API 호출 시작 =====
|
|
542
|
+
const apiCallStartTime = Date.now();
|
|
543
|
+
debugLog(`[request] >>>>> API CALL START: ${new Date(apiCallStartTime).toISOString()}`);
|
|
507
544
|
|
|
508
545
|
response = await client.response(originalRequest, requestOptions);
|
|
546
|
+
|
|
547
|
+
const apiCallEndTime = Date.now();
|
|
548
|
+
const apiCallDuration = apiCallEndTime - apiCallStartTime;
|
|
549
|
+
debugLog(`[request] <<<<< API CALL END: ${apiCallDuration}ms (${new Date(apiCallEndTime).toISOString()})`);
|
|
509
550
|
debugLog(`[request] Response received - id: ${response?.id}, status: ${response?.status}, output items: ${response?.output?.length || 0}`);
|
|
510
551
|
|
|
511
552
|
// 원본 응답을 깊은 복사로 보존 (이후 수정으로부터 보호)
|
|
@@ -549,6 +590,84 @@ export async function request(taskName, requestPayload) {
|
|
|
549
590
|
// 로그는 원본 포맷으로 저장
|
|
550
591
|
debugLog(`[request] Logging response to file`);
|
|
551
592
|
await logger(`${taskName}_RES`, originalResponse, provider);
|
|
593
|
+
|
|
594
|
+
// 화면에 응답 결과 표시 (raw JSON)
|
|
595
|
+
// 캐시 토큰 정보 추출
|
|
596
|
+
const cacheTokens = response.usage?.cache_read_input_tokens || response.usage?.input_tokens_details?.cached_tokens || 0;
|
|
597
|
+
const inputTokens = response.usage?.input_tokens || 0;
|
|
598
|
+
const cacheRatio = inputTokens > 0 ? Math.round(cacheTokens / inputTokens * 100) : 0;
|
|
599
|
+
|
|
600
|
+
const rawOutput = {
|
|
601
|
+
status: response.status,
|
|
602
|
+
output: (response.output || []).map(o => {
|
|
603
|
+
if (o.type === 'reasoning') {
|
|
604
|
+
// thinking/reasoning 블록 표시
|
|
605
|
+
const thinking = o.content?.[0]?.thinking || '';
|
|
606
|
+
return {
|
|
607
|
+
type: 'thinking',
|
|
608
|
+
content: thinking.length > 150 ? thinking.substring(0, 150) + `... (${thinking.length} chars)` : thinking
|
|
609
|
+
};
|
|
610
|
+
} else if (o.type === 'function_call') {
|
|
611
|
+
return {
|
|
612
|
+
type: o.type,
|
|
613
|
+
name: o.name,
|
|
614
|
+
arguments: (() => {
|
|
615
|
+
try {
|
|
616
|
+
const args = JSON.parse(o.arguments || '{}');
|
|
617
|
+
// 긴 인자값은 축약
|
|
618
|
+
const truncated = {};
|
|
619
|
+
for (const [k, v] of Object.entries(args)) {
|
|
620
|
+
if (typeof v === 'string' && v.length > 100) {
|
|
621
|
+
truncated[k] = v.substring(0, 100) + `... (${v.length} chars)`;
|
|
622
|
+
} else {
|
|
623
|
+
truncated[k] = v;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return truncated;
|
|
627
|
+
} catch {
|
|
628
|
+
return o.arguments;
|
|
629
|
+
}
|
|
630
|
+
})()
|
|
631
|
+
};
|
|
632
|
+
} else if (o.type === 'message') {
|
|
633
|
+
const text = response.output_text || '';
|
|
634
|
+
return {
|
|
635
|
+
type: o.type,
|
|
636
|
+
text: text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
return { type: o.type };
|
|
640
|
+
}),
|
|
641
|
+
// 토큰 사용량 정보 추가
|
|
642
|
+
tokens: {
|
|
643
|
+
input: inputTokens,
|
|
644
|
+
output: response.usage?.output_tokens || 0,
|
|
645
|
+
cached: cacheTokens > 0 ? `${cacheTokens} (${cacheRatio}%)` : 0
|
|
646
|
+
},
|
|
647
|
+
output_text: (() => {
|
|
648
|
+
const text = response.output_text || '';
|
|
649
|
+
if (taskName === 'completion_judge') {
|
|
650
|
+
// GLM 모델 대응: 텍스트 끝에 붙은 JSON 추출
|
|
651
|
+
try {
|
|
652
|
+
return JSON.parse(text);
|
|
653
|
+
} catch {
|
|
654
|
+
// 텍스트에서 JSON 추출 시도
|
|
655
|
+
const jsonMatch = text.match(/\{[^{}]*"should_complete"\s*:\s*(true|false)[^{}]*\}/);
|
|
656
|
+
if (jsonMatch) {
|
|
657
|
+
try {
|
|
658
|
+
return JSON.parse(jsonMatch[0]);
|
|
659
|
+
} catch {
|
|
660
|
+
return text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text;
|
|
667
|
+
})()
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
|
|
552
671
|
debugLog(`[request] ========== END: ${taskName} ==========`);
|
|
553
672
|
|
|
554
673
|
return response;
|
|
@@ -566,21 +685,46 @@ export async function request(taskName, requestPayload) {
|
|
|
566
685
|
*
|
|
567
686
|
* @see https://platform.openai.com/docs/guides/error-codes
|
|
568
687
|
*/
|
|
688
|
+
/*
|
|
689
|
+
* Z.AI/GLM 컨텍스트 초과 에러 샘플:
|
|
690
|
+
* {
|
|
691
|
+
* "error": {
|
|
692
|
+
* "code": "1210",
|
|
693
|
+
* "message": "Invalid API parameter, please check the documentation.Request 320006 input tokens exceeds the model's maximum context length 202750"
|
|
694
|
+
* },
|
|
695
|
+
* "request_id": "20260124000100026f0914d2e744f5"
|
|
696
|
+
* }
|
|
697
|
+
* HTTP_STATUS: 400
|
|
698
|
+
*/
|
|
569
699
|
export function shouldRetryWithTrim(error) {
|
|
570
700
|
if (!error) return false;
|
|
571
701
|
|
|
572
|
-
//
|
|
702
|
+
// 에러 코드 및 메시지 추출
|
|
573
703
|
const errorCode = error?.code || error?.error?.code;
|
|
704
|
+
const errorMessage = error?.message || error?.error?.message || '';
|
|
574
705
|
|
|
706
|
+
// OpenAI: context_length_exceeded
|
|
575
707
|
if (errorCode === 'context_length_exceeded') {
|
|
576
|
-
debugLog('[shouldRetryWithTrim] Detected: context_length_exceeded');
|
|
708
|
+
debugLog('[shouldRetryWithTrim] Detected: context_length_exceeded (OpenAI)');
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Z.AI/GLM: code "1210" (context length exceeded)
|
|
713
|
+
if (errorCode === '1210' || errorCode === 1210) {
|
|
714
|
+
debugLog('[shouldRetryWithTrim] Detected: code 1210 (GLM context exceeded)');
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// 메시지에서 컨텍스트 초과 패턴 감지 (다양한 API 대응)
|
|
719
|
+
const contextExceededPattern = /exceeds.*(?:context|token).*(?:length|limit|maximum)/i;
|
|
720
|
+
if (contextExceededPattern.test(errorMessage)) {
|
|
721
|
+
debugLog(`[shouldRetryWithTrim] Detected context exceeded in message: ${errorMessage.substring(0, 100)}`);
|
|
577
722
|
return true;
|
|
578
723
|
}
|
|
579
724
|
|
|
580
725
|
// 400 에러도 trim 후 재시도 대상
|
|
581
726
|
if (error?.status === 400 || error?.response?.status === 400) {
|
|
582
727
|
const errorType = error?.type || error?.error?.type;
|
|
583
|
-
const errorMessage = error?.message || error?.error?.message || '';
|
|
584
728
|
debugLog(
|
|
585
729
|
`[shouldRetryWithTrim] Detected 400 error - ` +
|
|
586
730
|
`Type: ${errorType}, Code: ${errorCode}, ` +
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 백그라운드 프로세스 관리 모듈
|
|
3
|
+
*
|
|
4
|
+
* AI Agent가 백그라운드로 명령어를 실행하고 관리할 수 있도록 지원
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import { createDebugLogger } from '../util/debug_log.js';
|
|
10
|
+
import { whichCommand } from './code_executer.js';
|
|
11
|
+
|
|
12
|
+
const debugLog = createDebugLogger('background_process.log', 'background_process');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 백그라운드 프로세스 정보
|
|
16
|
+
* @typedef {Object} BackgroundProcess
|
|
17
|
+
* @property {string} id - 고유 ID
|
|
18
|
+
* @property {string} command - 실행된 명령어
|
|
19
|
+
* @property {number} pid - 프로세스 ID
|
|
20
|
+
* @property {string} status - 상태 (running, completed, failed, killed)
|
|
21
|
+
* @property {Date} startedAt - 시작 시간
|
|
22
|
+
* @property {Date|null} endedAt - 종료 시간
|
|
23
|
+
* @property {string} stdout - 표준 출력
|
|
24
|
+
* @property {string} stderr - 표준 에러
|
|
25
|
+
* @property {number|null} exitCode - 종료 코드
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
class BackgroundProcessManager extends EventEmitter {
|
|
29
|
+
constructor() {
|
|
30
|
+
super();
|
|
31
|
+
/** @type {Map<string, BackgroundProcess>} */
|
|
32
|
+
this.processes = new Map();
|
|
33
|
+
this.nextId = 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 고유 ID 생성
|
|
38
|
+
*/
|
|
39
|
+
_generateId() {
|
|
40
|
+
return `bg_${this.nextId++}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 백그라운드로 명령어 실행
|
|
45
|
+
* @param {string} script - 실행할 스크립트
|
|
46
|
+
* @param {Object} options - 옵션
|
|
47
|
+
* @returns {Promise<{id: string, pid: number}>}
|
|
48
|
+
*/
|
|
49
|
+
async run(script, options = {}) {
|
|
50
|
+
const shellPath = await whichCommand("bash") || await whichCommand("sh");
|
|
51
|
+
if (!shellPath) {
|
|
52
|
+
throw new Error('No shell found');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const id = this._generateId();
|
|
56
|
+
const startedAt = new Date();
|
|
57
|
+
|
|
58
|
+
debugLog(`[BackgroundProcess] Starting: id=${id}, script="${script.substring(0, 50)}..."`);
|
|
59
|
+
|
|
60
|
+
const child = spawn(shellPath, ['-c', script], {
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
62
|
+
env: {
|
|
63
|
+
...process.env,
|
|
64
|
+
DEBIAN_FRONTEND: 'noninteractive',
|
|
65
|
+
CI: 'true',
|
|
66
|
+
BATCH: '1',
|
|
67
|
+
},
|
|
68
|
+
cwd: options.cwd || process.cwd(),
|
|
69
|
+
detached: true,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const processInfo = {
|
|
73
|
+
id,
|
|
74
|
+
command: script,
|
|
75
|
+
pid: child.pid,
|
|
76
|
+
status: 'running',
|
|
77
|
+
startedAt,
|
|
78
|
+
endedAt: null,
|
|
79
|
+
stdout: '',
|
|
80
|
+
stderr: '',
|
|
81
|
+
exitCode: null,
|
|
82
|
+
_process: child, // 내부용
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
this.processes.set(id, processInfo);
|
|
86
|
+
|
|
87
|
+
// stdout 수집
|
|
88
|
+
child.stdout.on('data', (data) => {
|
|
89
|
+
processInfo.stdout += data.toString();
|
|
90
|
+
this.emit('output', { id, type: 'stdout', data: data.toString() });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// stderr 수집
|
|
94
|
+
child.stderr.on('data', (data) => {
|
|
95
|
+
processInfo.stderr += data.toString();
|
|
96
|
+
this.emit('output', { id, type: 'stderr', data: data.toString() });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// 프로세스 종료 처리
|
|
100
|
+
child.on('close', (code) => {
|
|
101
|
+
processInfo.status = code === 0 ? 'completed' : 'failed';
|
|
102
|
+
processInfo.exitCode = code;
|
|
103
|
+
processInfo.endedAt = new Date();
|
|
104
|
+
delete processInfo._process;
|
|
105
|
+
|
|
106
|
+
debugLog(`[BackgroundProcess] Closed: id=${id}, code=${code}`);
|
|
107
|
+
this.emit('close', { id, code, status: processInfo.status });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
child.on('error', (err) => {
|
|
111
|
+
processInfo.status = 'failed';
|
|
112
|
+
processInfo.stderr += `\nError: ${err.message}`;
|
|
113
|
+
processInfo.endedAt = new Date();
|
|
114
|
+
delete processInfo._process;
|
|
115
|
+
|
|
116
|
+
debugLog(`[BackgroundProcess] Error: id=${id}, error=${err.message}`);
|
|
117
|
+
this.emit('error', { id, error: err });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
this.emit('started', { id, pid: child.pid, command: script });
|
|
121
|
+
|
|
122
|
+
return { id, pid: child.pid };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 프로세스 종료
|
|
127
|
+
* @param {string} id - 프로세스 ID
|
|
128
|
+
* @param {string} signal - 시그널 (기본: SIGTERM)
|
|
129
|
+
* @returns {boolean} 성공 여부
|
|
130
|
+
*/
|
|
131
|
+
kill(id, signal = 'SIGTERM') {
|
|
132
|
+
const processInfo = this.processes.get(id);
|
|
133
|
+
if (!processInfo) {
|
|
134
|
+
debugLog(`[BackgroundProcess] Kill failed: id=${id} not found`);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (processInfo.status !== 'running') {
|
|
139
|
+
debugLog(`[BackgroundProcess] Kill skipped: id=${id} already ${processInfo.status}`);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const child = processInfo._process;
|
|
144
|
+
if (!child) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// 프로세스 그룹 전체 종료
|
|
150
|
+
process.kill(-child.pid, signal);
|
|
151
|
+
processInfo.status = 'killed';
|
|
152
|
+
processInfo.endedAt = new Date();
|
|
153
|
+
debugLog(`[BackgroundProcess] Killed: id=${id}, signal=${signal}`);
|
|
154
|
+
this.emit('killed', { id, signal });
|
|
155
|
+
return true;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
try {
|
|
158
|
+
child.kill(signal);
|
|
159
|
+
processInfo.status = 'killed';
|
|
160
|
+
processInfo.endedAt = new Date();
|
|
161
|
+
debugLog(`[BackgroundProcess] Killed (fallback): id=${id}`);
|
|
162
|
+
this.emit('killed', { id, signal });
|
|
163
|
+
return true;
|
|
164
|
+
} catch (err2) {
|
|
165
|
+
debugLog(`[BackgroundProcess] Kill error: id=${id}, error=${err2.message}`);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 프로세스 정보 조회
|
|
173
|
+
* @param {string} id - 프로세스 ID
|
|
174
|
+
* @returns {BackgroundProcess|null}
|
|
175
|
+
*/
|
|
176
|
+
get(id) {
|
|
177
|
+
const info = this.processes.get(id);
|
|
178
|
+
if (!info) return null;
|
|
179
|
+
|
|
180
|
+
// _process 필드 제외하고 반환
|
|
181
|
+
const { _process, ...publicInfo } = info;
|
|
182
|
+
return publicInfo;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 모든 프로세스 목록 조회
|
|
187
|
+
* @param {Object} filter - 필터 옵션
|
|
188
|
+
* @returns {BackgroundProcess[]}
|
|
189
|
+
*/
|
|
190
|
+
list(filter = {}) {
|
|
191
|
+
let result = [];
|
|
192
|
+
|
|
193
|
+
for (const [id, info] of this.processes) {
|
|
194
|
+
const { _process, ...publicInfo } = info;
|
|
195
|
+
|
|
196
|
+
// 필터 적용
|
|
197
|
+
if (filter.status && publicInfo.status !== filter.status) continue;
|
|
198
|
+
if (filter.running && publicInfo.status !== 'running') continue;
|
|
199
|
+
|
|
200
|
+
result.push(publicInfo);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 시작 시간순 정렬
|
|
204
|
+
result.sort((a, b) => b.startedAt - a.startedAt);
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 실행 중인 프로세스 수
|
|
211
|
+
*/
|
|
212
|
+
get runningCount() {
|
|
213
|
+
let count = 0;
|
|
214
|
+
for (const info of this.processes.values()) {
|
|
215
|
+
if (info.status === 'running') count++;
|
|
216
|
+
}
|
|
217
|
+
return count;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 완료된 프로세스 정리
|
|
222
|
+
* @param {number} maxAge - 최대 보관 시간 (ms)
|
|
223
|
+
*/
|
|
224
|
+
cleanup(maxAge = 3600000) {
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
const toDelete = [];
|
|
227
|
+
|
|
228
|
+
for (const [id, info] of this.processes) {
|
|
229
|
+
if (info.status !== 'running' && info.endedAt) {
|
|
230
|
+
if (now - info.endedAt.getTime() > maxAge) {
|
|
231
|
+
toDelete.push(id);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const id of toDelete) {
|
|
237
|
+
this.processes.delete(id);
|
|
238
|
+
debugLog(`[BackgroundProcess] Cleaned up: id=${id}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return toDelete.length;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 모든 실행 중인 프로세스 종료
|
|
246
|
+
*/
|
|
247
|
+
async killAll() {
|
|
248
|
+
const killed = [];
|
|
249
|
+
for (const [id, info] of this.processes) {
|
|
250
|
+
if (info.status === 'running') {
|
|
251
|
+
if (this.kill(id)) {
|
|
252
|
+
killed.push(id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return killed;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 싱글톤 인스턴스
|
|
261
|
+
let instance = null;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* BackgroundProcessManager 싱글톤 인스턴스 가져오기
|
|
265
|
+
*/
|
|
266
|
+
export function getBackgroundProcessManager() {
|
|
267
|
+
if (!instance) {
|
|
268
|
+
instance = new BackgroundProcessManager();
|
|
269
|
+
}
|
|
270
|
+
return instance;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 백그라운드로 명령어 실행
|
|
275
|
+
* @param {string} script - 실행할 스크립트
|
|
276
|
+
* @param {Object} options - 옵션
|
|
277
|
+
* @returns {Promise<{id: string, pid: number}>}
|
|
278
|
+
*/
|
|
279
|
+
export async function runInBackground(script, options = {}) {
|
|
280
|
+
const manager = getBackgroundProcessManager();
|
|
281
|
+
return await manager.run(script, options);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 백그라운드 프로세스 종료
|
|
286
|
+
* @param {string} id - 프로세스 ID
|
|
287
|
+
*/
|
|
288
|
+
export function killBackgroundProcess(id) {
|
|
289
|
+
const manager = getBackgroundProcessManager();
|
|
290
|
+
return manager.kill(id);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 백그라운드 프로세스 목록 조회
|
|
295
|
+
*/
|
|
296
|
+
export function listBackgroundProcesses(filter = {}) {
|
|
297
|
+
const manager = getBackgroundProcessManager();
|
|
298
|
+
return manager.list(filter);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 백그라운드 프로세스 정보 조회
|
|
303
|
+
*/
|
|
304
|
+
export function getBackgroundProcess(id) {
|
|
305
|
+
const manager = getBackgroundProcessManager();
|
|
306
|
+
return manager.get(id);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 모든 백그라운드 프로세스 종료 (앱 종료 시)
|
|
311
|
+
*/
|
|
312
|
+
export async function killAllBackgroundProcesses() {
|
|
313
|
+
const manager = getBackgroundProcessManager();
|
|
314
|
+
return await manager.killAll();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export { BackgroundProcessManager };
|