@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.25
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 +6 -1
- 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/chat-runtime-config.js +7 -5
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +302 -145
- 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 +31 -31
- package/src/core/reporting/legacy-csv.js +12 -12
- package/src/core/run/detached-launcher.js +305 -0
- package/src/core/run/index.js +109 -55
- package/src/core/run/timing.js +33 -33
- package/src/core/run/windows-detached-worker.ps1 +106 -0
- package/src/core/screening/index.js +2400 -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 +536 -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 +1219 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +1079 -333
- package/src/domains/recommend/constants.js +92 -92
- package/src/domains/recommend/detail.js +4037 -136
- package/src/domains/recommend/filters.js +608 -590
- package/src/domains/recommend/index.js +9 -9
- package/src/domains/recommend/jobs.js +571 -542
- package/src/domains/recommend/location.js +754 -707
- package/src/domains/recommend/refresh.js +677 -392
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +4048 -1447
- 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 +16 -1
- package/src/parser.js +1296 -1296
- package/src/recommend-mcp.js +1061 -450
- package/src/recommend-scheduler.js +75 -75
|
@@ -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
|
+
}
|
|
@@ -889,13 +889,13 @@ export async function detectInfiniteListBottomMarker(client, {
|
|
|
889
889
|
};
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
-
export async function readVisibleInfiniteListItems({
|
|
893
|
-
nodeIds = [],
|
|
894
|
-
readCandidate,
|
|
895
|
-
keyForCandidate = candidateKeyFromProfile,
|
|
896
|
-
state = null,
|
|
897
|
-
shouldRethrowReadError = null
|
|
898
|
-
} = {}) {
|
|
892
|
+
export async function readVisibleInfiniteListItems({
|
|
893
|
+
nodeIds = [],
|
|
894
|
+
readCandidate,
|
|
895
|
+
keyForCandidate = candidateKeyFromProfile,
|
|
896
|
+
state = null,
|
|
897
|
+
shouldRethrowReadError = null
|
|
898
|
+
} = {}) {
|
|
899
899
|
if (typeof readCandidate !== "function") {
|
|
900
900
|
throw new Error("readVisibleInfiniteListItems requires readCandidate");
|
|
901
901
|
}
|
|
@@ -905,25 +905,25 @@ export async function readVisibleInfiniteListItems({
|
|
|
905
905
|
let candidate;
|
|
906
906
|
try {
|
|
907
907
|
candidate = await readCandidate(nodeId, { visibleIndex });
|
|
908
|
-
} catch (error) {
|
|
909
|
-
if (state) {
|
|
910
|
-
state.read_error_count = (state.read_error_count || 0) + 1;
|
|
908
|
+
} catch (error) {
|
|
909
|
+
if (state) {
|
|
910
|
+
state.read_error_count = (state.read_error_count || 0) + 1;
|
|
911
911
|
state.ledger?.push({
|
|
912
912
|
at: nowIso(),
|
|
913
913
|
event: "candidate_read_error",
|
|
914
914
|
node_id: nodeId,
|
|
915
915
|
visible_index: visibleIndex,
|
|
916
|
-
error: error?.message || String(error)
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
if (
|
|
920
|
-
typeof shouldRethrowReadError === "function"
|
|
921
|
-
&& shouldRethrowReadError(error, { nodeId, visibleIndex })
|
|
922
|
-
) {
|
|
923
|
-
throw error;
|
|
924
|
-
}
|
|
925
|
-
continue;
|
|
926
|
-
}
|
|
916
|
+
error: error?.message || String(error)
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
if (
|
|
920
|
+
typeof shouldRethrowReadError === "function"
|
|
921
|
+
&& shouldRethrowReadError(error, { nodeId, visibleIndex })
|
|
922
|
+
) {
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
927
|
const key = keyForCandidate(candidate, {
|
|
928
928
|
nodeId,
|
|
929
929
|
visibleIndex,
|
|
@@ -1139,9 +1139,9 @@ export async function getNextInfiniteListCandidate({
|
|
|
1139
1139
|
state,
|
|
1140
1140
|
findNodeIds,
|
|
1141
1141
|
readCandidate,
|
|
1142
|
-
detectBottomMarker = null,
|
|
1143
|
-
keyForCandidate = candidateKeyFromProfile,
|
|
1144
|
-
shouldRethrowReadError = null,
|
|
1142
|
+
detectBottomMarker = null,
|
|
1143
|
+
keyForCandidate = candidateKeyFromProfile,
|
|
1144
|
+
shouldRethrowReadError = null,
|
|
1145
1145
|
maxScrolls = 20,
|
|
1146
1146
|
stableSignatureLimit = 2,
|
|
1147
1147
|
minScrollsBeforeEnd = 3,
|
|
@@ -1164,13 +1164,13 @@ export async function getNextInfiniteListCandidate({
|
|
|
1164
1164
|
const maxAttempts = Math.max(0, Number(maxScrolls) || 0);
|
|
1165
1165
|
for (let scrollAttempt = 0; scrollAttempt <= maxAttempts; scrollAttempt += 1) {
|
|
1166
1166
|
const nodeIds = await findNodeIds();
|
|
1167
|
-
const items = await readVisibleInfiniteListItems({
|
|
1168
|
-
nodeIds,
|
|
1169
|
-
readCandidate,
|
|
1170
|
-
keyForCandidate,
|
|
1171
|
-
state,
|
|
1172
|
-
shouldRethrowReadError
|
|
1173
|
-
});
|
|
1167
|
+
const items = await readVisibleInfiniteListItems({
|
|
1168
|
+
nodeIds,
|
|
1169
|
+
readCandidate,
|
|
1170
|
+
keyForCandidate,
|
|
1171
|
+
state,
|
|
1172
|
+
shouldRethrowReadError
|
|
1173
|
+
});
|
|
1174
1174
|
const signature = updateInfiniteListVisibleSignature(state, items);
|
|
1175
1175
|
const next = firstUnseenInfiniteListItem(state, items);
|
|
1176
1176
|
attempts.push({
|
|
@@ -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
|
+
};
|