@reconcrap/boss-recommend-mcp 2.1.22 → 2.1.24
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 +5 -0
- package/bin/boss-recommend-mcp.js +4 -4
- package/package.json +14 -8
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +43 -42
- package/skills/boss-chat/SKILL.md +107 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +47 -47
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +301 -127
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +291 -114
- package/src/core/capture/index.js +2930 -1201
- package/src/core/cv-acquisition/index.js +512 -238
- package/src/core/cv-capture-target/index.js +513 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +11 -2
- package/src/core/reporting/legacy-csv.js +12 -12
- package/src/core/run/detached-launcher.js +305 -0
- package/src/core/run/index.js +112 -42
- package/src/core/run/timing.js +33 -33
- package/src/core/run/windows-detached-worker.ps1 +106 -0
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +989 -973
- package/src/core/self-heal/viewport.js +1505 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/action-journal.js +443 -0
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +1684 -401
- package/src/domains/chat/index.js +8 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +157 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +1801 -760
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +515 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +92 -92
- package/src/domains/recommend/detail.js +12 -3
- package/src/domains/recommend/filters.js +611 -611
- package/src/domains/recommend/index.js +9 -9
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -736
- package/src/domains/recommend/refresh.js +410 -329
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +1783 -592
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1817 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +124 -5
- package/src/parser.js +1296 -1296
- package/src/recommend-mcp.js +515 -80
- package/src/recommend-scheduler.js +66 -0
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import { normalizeText } from "../screening/index.js";
|
|
2
|
-
|
|
3
|
-
export const GREET_CREDITS_EXHAUSTED_CODE = "GREET_CREDITS_EXHAUSTED";
|
|
4
|
-
|
|
5
|
-
function coerceQuota(quota) {
|
|
6
|
-
if (!quota || typeof quota !== "object") return null;
|
|
7
|
-
return {
|
|
8
|
-
found: Boolean(quota.found),
|
|
9
|
-
text: normalizeText(quota.text),
|
|
10
|
-
numerator: Number.isFinite(Number(quota.numerator)) ? Number(quota.numerator) : null,
|
|
11
|
-
denominator: Number.isFinite(Number(quota.denominator)) ? Number(quota.denominator) : null,
|
|
12
|
-
exhausted: Boolean(quota.exhausted)
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function parseGreetQuota(label = "") {
|
|
17
|
-
const text = normalizeText(label);
|
|
18
|
-
const match = text.match(/立即沟通\s*[\((]\s*(\d+)\s*[//]\s*(\d+)\s*[\))]/);
|
|
19
|
-
if (!match) {
|
|
20
|
-
return {
|
|
21
|
-
found: false,
|
|
22
|
-
text,
|
|
23
|
-
numerator: null,
|
|
24
|
-
denominator: null,
|
|
25
|
-
exhausted: false
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
const numerator = Number(match[1]);
|
|
29
|
-
const denominator = Number(match[2]);
|
|
30
|
-
return {
|
|
31
|
-
found: true,
|
|
32
|
-
text,
|
|
33
|
-
numerator,
|
|
34
|
-
denominator,
|
|
35
|
-
exhausted: numerator > denominator
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function normalizeGreetQuotaSource(source = "") {
|
|
40
|
-
return coerceQuota(source) || parseGreetQuota(source);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function assertGreetQuotaAvailable(source = "") {
|
|
44
|
-
const quota = normalizeGreetQuotaSource(source);
|
|
45
|
-
if (quota.exhausted) {
|
|
46
|
-
const error = new Error(
|
|
47
|
-
`Greet credits exhausted according to Boss quota text: ${quota.numerator}/${quota.denominator}`
|
|
48
|
-
);
|
|
49
|
-
error.code = GREET_CREDITS_EXHAUSTED_CODE;
|
|
50
|
-
error.greet_quota = quota;
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
53
|
-
return quota;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function describeGreetQuotaAfterSpend(source = "") {
|
|
57
|
-
const quota = normalizeGreetQuotaSource(source);
|
|
58
|
-
if (!quota.found || quota.numerator === null || quota.denominator === null) {
|
|
59
|
-
return {
|
|
60
|
-
...quota,
|
|
61
|
-
remaining_after_spend: null,
|
|
62
|
-
exhausted_after_spend: false
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
const remaining = quota.denominator - quota.numerator;
|
|
66
|
-
return {
|
|
67
|
-
...quota,
|
|
68
|
-
remaining_after_spend: remaining,
|
|
69
|
-
exhausted_after_spend: remaining < quota.numerator
|
|
70
|
-
};
|
|
71
|
-
}
|
|
1
|
+
import { normalizeText } from "../screening/index.js";
|
|
2
|
+
|
|
3
|
+
export const GREET_CREDITS_EXHAUSTED_CODE = "GREET_CREDITS_EXHAUSTED";
|
|
4
|
+
|
|
5
|
+
function coerceQuota(quota) {
|
|
6
|
+
if (!quota || typeof quota !== "object") return null;
|
|
7
|
+
return {
|
|
8
|
+
found: Boolean(quota.found),
|
|
9
|
+
text: normalizeText(quota.text),
|
|
10
|
+
numerator: Number.isFinite(Number(quota.numerator)) ? Number(quota.numerator) : null,
|
|
11
|
+
denominator: Number.isFinite(Number(quota.denominator)) ? Number(quota.denominator) : null,
|
|
12
|
+
exhausted: Boolean(quota.exhausted)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseGreetQuota(label = "") {
|
|
17
|
+
const text = normalizeText(label);
|
|
18
|
+
const match = text.match(/立即沟通\s*[\((]\s*(\d+)\s*[//]\s*(\d+)\s*[\))]/);
|
|
19
|
+
if (!match) {
|
|
20
|
+
return {
|
|
21
|
+
found: false,
|
|
22
|
+
text,
|
|
23
|
+
numerator: null,
|
|
24
|
+
denominator: null,
|
|
25
|
+
exhausted: false
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const numerator = Number(match[1]);
|
|
29
|
+
const denominator = Number(match[2]);
|
|
30
|
+
return {
|
|
31
|
+
found: true,
|
|
32
|
+
text,
|
|
33
|
+
numerator,
|
|
34
|
+
denominator,
|
|
35
|
+
exhausted: numerator > denominator
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeGreetQuotaSource(source = "") {
|
|
40
|
+
return coerceQuota(source) || parseGreetQuota(source);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function assertGreetQuotaAvailable(source = "") {
|
|
44
|
+
const quota = normalizeGreetQuotaSource(source);
|
|
45
|
+
if (quota.exhausted) {
|
|
46
|
+
const error = new Error(
|
|
47
|
+
`Greet credits exhausted according to Boss quota text: ${quota.numerator}/${quota.denominator}`
|
|
48
|
+
);
|
|
49
|
+
error.code = GREET_CREDITS_EXHAUSTED_CODE;
|
|
50
|
+
error.greet_quota = quota;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
return quota;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function describeGreetQuotaAfterSpend(source = "") {
|
|
57
|
+
const quota = normalizeGreetQuotaSource(source);
|
|
58
|
+
if (!quota.found || quota.numerator === null || quota.denominator === null) {
|
|
59
|
+
return {
|
|
60
|
+
...quota,
|
|
61
|
+
remaining_after_spend: null,
|
|
62
|
+
exhausted_after_spend: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const remaining = quota.denominator - quota.numerator;
|
|
66
|
+
return {
|
|
67
|
+
...quota,
|
|
68
|
+
remaining_after_spend: remaining,
|
|
69
|
+
exhausted_after_spend: remaining < quota.numerator
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -893,7 +893,8 @@ export async function readVisibleInfiniteListItems({
|
|
|
893
893
|
nodeIds = [],
|
|
894
894
|
readCandidate,
|
|
895
895
|
keyForCandidate = candidateKeyFromProfile,
|
|
896
|
-
state = null
|
|
896
|
+
state = null,
|
|
897
|
+
shouldRethrowReadError = null
|
|
897
898
|
} = {}) {
|
|
898
899
|
if (typeof readCandidate !== "function") {
|
|
899
900
|
throw new Error("readVisibleInfiniteListItems requires readCandidate");
|
|
@@ -915,6 +916,12 @@ export async function readVisibleInfiniteListItems({
|
|
|
915
916
|
error: error?.message || String(error)
|
|
916
917
|
});
|
|
917
918
|
}
|
|
919
|
+
if (
|
|
920
|
+
typeof shouldRethrowReadError === "function"
|
|
921
|
+
&& shouldRethrowReadError(error, { nodeId, visibleIndex })
|
|
922
|
+
) {
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
918
925
|
continue;
|
|
919
926
|
}
|
|
920
927
|
const key = keyForCandidate(candidate, {
|
|
@@ -1134,6 +1141,7 @@ export async function getNextInfiniteListCandidate({
|
|
|
1134
1141
|
readCandidate,
|
|
1135
1142
|
detectBottomMarker = null,
|
|
1136
1143
|
keyForCandidate = candidateKeyFromProfile,
|
|
1144
|
+
shouldRethrowReadError = null,
|
|
1137
1145
|
maxScrolls = 20,
|
|
1138
1146
|
stableSignatureLimit = 2,
|
|
1139
1147
|
minScrollsBeforeEnd = 3,
|
|
@@ -1160,7 +1168,8 @@ export async function getNextInfiniteListCandidate({
|
|
|
1160
1168
|
nodeIds,
|
|
1161
1169
|
readCandidate,
|
|
1162
1170
|
keyForCandidate,
|
|
1163
|
-
state
|
|
1171
|
+
state,
|
|
1172
|
+
shouldRethrowReadError
|
|
1164
1173
|
});
|
|
1165
1174
|
const signature = updateInfiniteListVisibleSignature(state, items);
|
|
1166
1175
|
const next = firstUnseenInfiniteListItem(state, items);
|
|
@@ -41,18 +41,18 @@ export const LEGACY_RESULT_HEADER = [
|
|
|
41
41
|
];
|
|
42
42
|
|
|
43
43
|
const SEARCH_PARAM_ORDER = [
|
|
44
|
-
"school_tag",
|
|
45
|
-
"degree",
|
|
46
|
-
"degrees",
|
|
47
|
-
"gender",
|
|
48
|
-
"recent_not_view",
|
|
49
|
-
"current_city_only",
|
|
50
|
-
"activity_level",
|
|
51
|
-
"city",
|
|
52
|
-
"schools",
|
|
53
|
-
"keyword",
|
|
54
|
-
"filter_recent_viewed",
|
|
55
|
-
"skip_recent_colleague_contacted",
|
|
44
|
+
"school_tag",
|
|
45
|
+
"degree",
|
|
46
|
+
"degrees",
|
|
47
|
+
"gender",
|
|
48
|
+
"recent_not_view",
|
|
49
|
+
"current_city_only",
|
|
50
|
+
"activity_level",
|
|
51
|
+
"city",
|
|
52
|
+
"schools",
|
|
53
|
+
"keyword",
|
|
54
|
+
"filter_recent_viewed",
|
|
55
|
+
"skip_recent_colleague_contacted",
|
|
56
56
|
"job",
|
|
57
57
|
"start_from",
|
|
58
58
|
"target_count",
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const WINDOWS_WRAPPER_PATH = fileURLToPath(new URL("./windows-detached-worker.ps1", import.meta.url));
|
|
8
|
+
const SAFE_DOMAIN_PATTERN = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
9
|
+
const SAFE_RUN_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
10
|
+
const SAFE_DOMAINS = new Set(["chat", "recommend", "recruit"]);
|
|
11
|
+
|
|
12
|
+
function createLauncherError(code, message, cause = null) {
|
|
13
|
+
const error = new Error(message);
|
|
14
|
+
error.code = code;
|
|
15
|
+
if (cause) error.cause = cause;
|
|
16
|
+
return error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensureLogFile(filePath) {
|
|
20
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
21
|
+
fs.closeSync(fs.openSync(filePath, "a"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertControlledPath(value, label, platform) {
|
|
25
|
+
const normalized = String(value || "");
|
|
26
|
+
const pathApi = platform === "win32" ? path.win32 : path;
|
|
27
|
+
if (!normalized || !pathApi.isAbsolute(normalized)) {
|
|
28
|
+
throw createLauncherError("DETACHED_WORKER_PATH_INVALID", `${label} must be an absolute path`);
|
|
29
|
+
}
|
|
30
|
+
if (/[\0\r\n"]/.test(normalized)) {
|
|
31
|
+
throw createLauncherError("DETACHED_WORKER_PATH_INVALID", `${label} contains unsupported characters`);
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function assertControlledToken(value, label, pattern) {
|
|
37
|
+
const normalized = String(value || "");
|
|
38
|
+
if (!pattern.test(normalized)) {
|
|
39
|
+
throw createLauncherError("DETACHED_WORKER_ARGUMENT_INVALID", `${label} contains unsupported characters`);
|
|
40
|
+
}
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function quoteWindowsCommandLineArgument(value) {
|
|
45
|
+
const input = String(value ?? "");
|
|
46
|
+
if (input.includes("\0")) {
|
|
47
|
+
throw createLauncherError("DETACHED_WORKER_ARGUMENT_INVALID", "Windows command-line arguments cannot contain NUL");
|
|
48
|
+
}
|
|
49
|
+
if (input && !/[\s"]/.test(input)) return input;
|
|
50
|
+
let output = '"';
|
|
51
|
+
let backslashes = 0;
|
|
52
|
+
for (const character of input) {
|
|
53
|
+
if (character === "\\") {
|
|
54
|
+
backslashes += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (character === '"') {
|
|
58
|
+
output += "\\".repeat((backslashes * 2) + 1);
|
|
59
|
+
output += '"';
|
|
60
|
+
backslashes = 0;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
output += "\\".repeat(backslashes);
|
|
64
|
+
output += character;
|
|
65
|
+
backslashes = 0;
|
|
66
|
+
}
|
|
67
|
+
output += "\\".repeat(backslashes * 2);
|
|
68
|
+
output += '"';
|
|
69
|
+
return output;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function quotePowerShellLiteral(value) {
|
|
73
|
+
return `'${String(value ?? "").replaceAll("'", "''")}'`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function windowsPowerShellPath(environment = process.env) {
|
|
77
|
+
const systemRoot = String(environment?.SystemRoot || environment?.WINDIR || "").trim();
|
|
78
|
+
return systemRoot
|
|
79
|
+
? path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
80
|
+
: "powershell.exe";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildWindowsDetachedWorkerCommand({
|
|
84
|
+
powershellPath,
|
|
85
|
+
wrapperScriptPath,
|
|
86
|
+
nodePath,
|
|
87
|
+
workerScriptPath,
|
|
88
|
+
domain,
|
|
89
|
+
runId,
|
|
90
|
+
stdoutPath,
|
|
91
|
+
stderrPath,
|
|
92
|
+
chatRuntimeHomePath = "",
|
|
93
|
+
screenConfigPath = ""
|
|
94
|
+
}) {
|
|
95
|
+
const args = [
|
|
96
|
+
powershellPath,
|
|
97
|
+
"-NoProfile",
|
|
98
|
+
"-NonInteractive",
|
|
99
|
+
"-ExecutionPolicy",
|
|
100
|
+
"Bypass",
|
|
101
|
+
"-WindowStyle",
|
|
102
|
+
"Hidden",
|
|
103
|
+
"-File",
|
|
104
|
+
wrapperScriptPath,
|
|
105
|
+
"-NodePath",
|
|
106
|
+
nodePath,
|
|
107
|
+
"-WorkerScriptPath",
|
|
108
|
+
workerScriptPath,
|
|
109
|
+
"-Domain",
|
|
110
|
+
domain,
|
|
111
|
+
"-RunId",
|
|
112
|
+
runId,
|
|
113
|
+
"-StdoutPath",
|
|
114
|
+
stdoutPath,
|
|
115
|
+
"-StderrPath",
|
|
116
|
+
stderrPath
|
|
117
|
+
];
|
|
118
|
+
if (chatRuntimeHomePath) {
|
|
119
|
+
args.push("-ChatRuntimeHomePath", chatRuntimeHomePath);
|
|
120
|
+
}
|
|
121
|
+
if (screenConfigPath) {
|
|
122
|
+
args.push("-ScreenConfigPath", screenConfigPath);
|
|
123
|
+
}
|
|
124
|
+
return args.map(quoteWindowsCommandLineArgument).join(" ");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function buildWindowsCimEncodedCommand(commandLine) {
|
|
128
|
+
const script = [
|
|
129
|
+
"$ErrorActionPreference = 'Stop'",
|
|
130
|
+
`$commandLine = ${quotePowerShellLiteral(commandLine)}`,
|
|
131
|
+
"$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = $commandLine }",
|
|
132
|
+
"$payload = [ordered]@{ return_value = [int]$result.ReturnValue; process_id = [int]$result.ProcessId }",
|
|
133
|
+
"[Console]::Out.Write(($payload | ConvertTo-Json -Compress))",
|
|
134
|
+
"if ([int]$result.ReturnValue -ne 0) { exit 1 }"
|
|
135
|
+
].join("; ");
|
|
136
|
+
return Buffer.from(script, "utf16le").toString("base64");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseCimPayload(stdout = "") {
|
|
140
|
+
const lines = String(stdout || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
141
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(lines[index]);
|
|
144
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
145
|
+
} catch {
|
|
146
|
+
// PowerShell may emit non-JSON diagnostics before the compact payload.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function launchWindowsDetachedWorker(options) {
|
|
153
|
+
const {
|
|
154
|
+
nodePath,
|
|
155
|
+
workerScriptPath,
|
|
156
|
+
domain,
|
|
157
|
+
runId,
|
|
158
|
+
stdoutPath,
|
|
159
|
+
stderrPath,
|
|
160
|
+
chatRuntimeHomePath,
|
|
161
|
+
screenConfigPath,
|
|
162
|
+
wrapperScriptPath = WINDOWS_WRAPPER_PATH,
|
|
163
|
+
powershellPath = windowsPowerShellPath(options.environment),
|
|
164
|
+
spawnSyncImpl = spawnSync
|
|
165
|
+
} = options;
|
|
166
|
+
const commandLine = buildWindowsDetachedWorkerCommand({
|
|
167
|
+
powershellPath,
|
|
168
|
+
wrapperScriptPath,
|
|
169
|
+
nodePath,
|
|
170
|
+
workerScriptPath,
|
|
171
|
+
domain,
|
|
172
|
+
runId,
|
|
173
|
+
stdoutPath,
|
|
174
|
+
stderrPath,
|
|
175
|
+
chatRuntimeHomePath,
|
|
176
|
+
screenConfigPath
|
|
177
|
+
});
|
|
178
|
+
const encodedCommand = buildWindowsCimEncodedCommand(commandLine);
|
|
179
|
+
const result = spawnSyncImpl(powershellPath, [
|
|
180
|
+
"-NoProfile",
|
|
181
|
+
"-NonInteractive",
|
|
182
|
+
"-ExecutionPolicy",
|
|
183
|
+
"Bypass",
|
|
184
|
+
"-EncodedCommand",
|
|
185
|
+
encodedCommand
|
|
186
|
+
], {
|
|
187
|
+
encoding: "utf8",
|
|
188
|
+
windowsHide: true,
|
|
189
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
190
|
+
});
|
|
191
|
+
if (result?.error) {
|
|
192
|
+
throw createLauncherError(
|
|
193
|
+
"WINDOWS_CIM_LAUNCH_FAILED",
|
|
194
|
+
`Unable to invoke the Windows detached-worker launcher: ${result.error.message || result.error}`,
|
|
195
|
+
result.error
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const payload = parseCimPayload(result?.stdout);
|
|
199
|
+
const returnValue = Number(payload?.return_value);
|
|
200
|
+
const workerPid = Number(payload?.process_id);
|
|
201
|
+
if (Number.isFinite(returnValue) && returnValue !== 0) {
|
|
202
|
+
throw createLauncherError(
|
|
203
|
+
"WINDOWS_CIM_CREATE_FAILED",
|
|
204
|
+
`Win32_Process.Create returned ${returnValue} while launching the detached worker.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (result?.signal || !Number.isInteger(result?.status) || result.status !== 0) {
|
|
208
|
+
const stderr = String(result?.stderr || "").trim();
|
|
209
|
+
throw createLauncherError(
|
|
210
|
+
"WINDOWS_CIM_LAUNCH_FAILED",
|
|
211
|
+
stderr || `PowerShell CIM launcher exited with status ${result?.status ?? "unknown"}.`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (!Number.isInteger(workerPid) || workerPid <= 0 || returnValue !== 0) {
|
|
215
|
+
throw createLauncherError(
|
|
216
|
+
"WINDOWS_CIM_RESULT_INVALID",
|
|
217
|
+
"Win32_Process.Create did not return a valid detached worker PID."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
pid: workerPid,
|
|
222
|
+
unref() {}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function launchPosixDetachedWorker({
|
|
227
|
+
nodePath,
|
|
228
|
+
workerScriptPath,
|
|
229
|
+
domain,
|
|
230
|
+
runId,
|
|
231
|
+
stdoutPath,
|
|
232
|
+
stderrPath,
|
|
233
|
+
spawnImpl = spawn,
|
|
234
|
+
environment = process.env
|
|
235
|
+
}) {
|
|
236
|
+
const stdoutFd = fs.openSync(stdoutPath, "a");
|
|
237
|
+
const stderrFd = fs.openSync(stderrPath, "a");
|
|
238
|
+
try {
|
|
239
|
+
return spawnImpl(nodePath, [
|
|
240
|
+
workerScriptPath,
|
|
241
|
+
"--domain",
|
|
242
|
+
domain,
|
|
243
|
+
"--run-id",
|
|
244
|
+
runId
|
|
245
|
+
], {
|
|
246
|
+
detached: true,
|
|
247
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
248
|
+
windowsHide: true,
|
|
249
|
+
env: environment
|
|
250
|
+
});
|
|
251
|
+
} finally {
|
|
252
|
+
fs.closeSync(stdoutFd);
|
|
253
|
+
fs.closeSync(stderrFd);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function launchDetachedWorker(options = {}) {
|
|
258
|
+
const platform = String(options.platform || process.platform);
|
|
259
|
+
const nodePath = assertControlledPath(options.nodePath || process.execPath, "nodePath", platform);
|
|
260
|
+
const workerScriptPath = assertControlledPath(options.workerScriptPath, "workerScriptPath", platform);
|
|
261
|
+
const stdoutPath = assertControlledPath(options.stdoutPath, "stdoutPath", platform);
|
|
262
|
+
const stderrPath = assertControlledPath(options.stderrPath, "stderrPath", platform);
|
|
263
|
+
const domain = assertControlledToken(options.domain, "domain", SAFE_DOMAIN_PATTERN);
|
|
264
|
+
if (!SAFE_DOMAINS.has(domain)) {
|
|
265
|
+
throw createLauncherError("DETACHED_WORKER_ARGUMENT_INVALID", "domain is not supported by the detached launcher");
|
|
266
|
+
}
|
|
267
|
+
const runId = assertControlledToken(options.runId, "runId", SAFE_RUN_ID_PATTERN);
|
|
268
|
+
const wrapperScriptPath = platform === "win32"
|
|
269
|
+
? assertControlledPath(options.wrapperScriptPath || WINDOWS_WRAPPER_PATH, "wrapperScriptPath", platform)
|
|
270
|
+
: options.wrapperScriptPath;
|
|
271
|
+
const chatRuntimeHomePath = options.chatRuntimeHomePath
|
|
272
|
+
? assertControlledPath(options.chatRuntimeHomePath, "chatRuntimeHomePath", platform)
|
|
273
|
+
: "";
|
|
274
|
+
const screenConfigPath = options.screenConfigPath
|
|
275
|
+
? assertControlledPath(options.screenConfigPath, "screenConfigPath", platform)
|
|
276
|
+
: "";
|
|
277
|
+
const prepareLogFile = typeof options.prepareLogFileImpl === "function"
|
|
278
|
+
? options.prepareLogFileImpl
|
|
279
|
+
: ensureLogFile;
|
|
280
|
+
prepareLogFile(stdoutPath);
|
|
281
|
+
prepareLogFile(stderrPath);
|
|
282
|
+
const normalized = {
|
|
283
|
+
...options,
|
|
284
|
+
platform,
|
|
285
|
+
nodePath,
|
|
286
|
+
workerScriptPath,
|
|
287
|
+
stdoutPath,
|
|
288
|
+
stderrPath,
|
|
289
|
+
domain,
|
|
290
|
+
runId,
|
|
291
|
+
wrapperScriptPath,
|
|
292
|
+
chatRuntimeHomePath,
|
|
293
|
+
screenConfigPath
|
|
294
|
+
};
|
|
295
|
+
return platform === "win32"
|
|
296
|
+
? launchWindowsDetachedWorker(normalized)
|
|
297
|
+
: launchPosixDetachedWorker(normalized);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const __testables = {
|
|
301
|
+
WINDOWS_WRAPPER_PATH,
|
|
302
|
+
parseCimPayload,
|
|
303
|
+
quotePowerShellLiteral,
|
|
304
|
+
windowsPowerShellPath
|
|
305
|
+
};
|