@reconcrap/boss-recommend-mcp 1.1.10 → 1.1.12
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/package.json
CHANGED
|
@@ -36,6 +36,29 @@ function normalizeText(value) {
|
|
|
36
36
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function sanitizeUrl(value) {
|
|
40
|
+
const raw = String(value || "").replace(/\s+/g, " ").trim();
|
|
41
|
+
const cleaned = raw
|
|
42
|
+
.replace(/^\uFEFF/, "")
|
|
43
|
+
.replace(/[\u200B-\u200F\u2028-\u202F\u2060-\u2064\uFEFF]/g, "")
|
|
44
|
+
.replace(/^["']|["']$/g, "");
|
|
45
|
+
return cleaned.replace(/\/+$/, "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validateUrlString(raw) {
|
|
49
|
+
const sanitized = sanitizeUrl(raw);
|
|
50
|
+
if (!sanitized) return { ok: false, error: "baseUrl 为空" };
|
|
51
|
+
try {
|
|
52
|
+
const url = new URL(sanitized);
|
|
53
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
54
|
+
return { ok: false, error: `协议无效: ${url.protocol} (期望 http 或 https)` };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, sanitized, full: sanitized };
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return { ok: false, error: `URL 格式无效: ${e.message}`, raw };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
function parsePositiveInteger(raw) {
|
|
40
63
|
const value = Number.parseInt(String(raw || ""), 10);
|
|
41
64
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
@@ -68,6 +91,234 @@ function parseBoolean(value) {
|
|
|
68
91
|
return null;
|
|
69
92
|
}
|
|
70
93
|
|
|
94
|
+
function normalizeCliOptionToken(rawToken) {
|
|
95
|
+
const token = String(rawToken || "").trim();
|
|
96
|
+
if (!token) {
|
|
97
|
+
return { token: "", inlineValue: null };
|
|
98
|
+
}
|
|
99
|
+
const normalizedDashes = token.replace(/^[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]+/, "--");
|
|
100
|
+
const eqIndex = normalizedDashes.indexOf("=");
|
|
101
|
+
if (normalizedDashes.startsWith("--") && eqIndex > 2) {
|
|
102
|
+
return {
|
|
103
|
+
token: normalizedDashes.slice(0, eqIndex),
|
|
104
|
+
inlineValue: normalizedDashes.slice(eqIndex + 1)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { token: normalizedDashes, inlineValue: null };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseFavoriteActionValue(rawValue) {
|
|
111
|
+
if (rawValue === null || rawValue === undefined) return null;
|
|
112
|
+
if (typeof rawValue === "boolean") return rawValue ? "add" : "del";
|
|
113
|
+
if (typeof rawValue === "number") {
|
|
114
|
+
if (rawValue === 1) return "add";
|
|
115
|
+
if (rawValue === 0) return "del";
|
|
116
|
+
}
|
|
117
|
+
const normalized = normalizeText(rawValue).toLowerCase();
|
|
118
|
+
if (!normalized) return null;
|
|
119
|
+
if (
|
|
120
|
+
["1", "add", "favorite", "collect", "interested", "true", "yes", "on"].includes(normalized)
|
|
121
|
+
|| /(?:^|[_\W])(add|favorite|collect|interest(?:ed)?)(?:$|[_\W])/.test(normalized)
|
|
122
|
+
) {
|
|
123
|
+
return "add";
|
|
124
|
+
}
|
|
125
|
+
if (
|
|
126
|
+
["0", "del", "delete", "remove", "cancel", "unfavorite", "uncollect", "false", "no", "off"].includes(normalized)
|
|
127
|
+
|| /(?:^|[_\W])(del|delete|remove|cancel|unfavorite|uncollect|uninterest)(?:$|[_\W])/.test(normalized)
|
|
128
|
+
) {
|
|
129
|
+
return "del";
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseFavoriteActionFromObject(payload, visited = new Set()) {
|
|
135
|
+
if (!payload || typeof payload !== "object") return null;
|
|
136
|
+
if (visited.has(payload)) return null;
|
|
137
|
+
visited.add(payload);
|
|
138
|
+
|
|
139
|
+
if (Array.isArray(payload)) {
|
|
140
|
+
for (const item of payload) {
|
|
141
|
+
const action = parseFavoriteActionFromObject(item, visited);
|
|
142
|
+
if (action) return action;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const keys = Object.keys(payload);
|
|
148
|
+
const strongSignalKey = (key) => /p3|status|state|favorite|collect|interested|markstatus|isfavorite|iscollect/.test(key);
|
|
149
|
+
const weakSignalKey = (key) => /action|op|operation|type|mode|mark|interest/.test(key);
|
|
150
|
+
for (const key of keys) {
|
|
151
|
+
const value = payload[key];
|
|
152
|
+
const normalizedKey = normalizeText(key).toLowerCase();
|
|
153
|
+
if (strongSignalKey(normalizedKey)) {
|
|
154
|
+
const action = parseFavoriteActionValue(value);
|
|
155
|
+
if (action) return action;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const key of keys) {
|
|
159
|
+
const value = payload[key];
|
|
160
|
+
const normalizedKey = normalizeText(key).toLowerCase();
|
|
161
|
+
if (weakSignalKey(normalizedKey)) {
|
|
162
|
+
const action = parseFavoriteActionValue(value);
|
|
163
|
+
if (action) return action;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const key of keys) {
|
|
168
|
+
const value = payload[key];
|
|
169
|
+
if (value && typeof value === "object") {
|
|
170
|
+
const action = parseFavoriteActionFromObject(value, visited);
|
|
171
|
+
if (action) return action;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseFavoriteActionFromPostData(rawPostData) {
|
|
178
|
+
const postData = normalizeText(rawPostData);
|
|
179
|
+
if (!postData) return null;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const parsed = JSON.parse(postData);
|
|
183
|
+
const action = parseFavoriteActionFromObject(parsed);
|
|
184
|
+
if (action) return action;
|
|
185
|
+
} catch {}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const params = new URLSearchParams(postData);
|
|
189
|
+
const strongEntries = [];
|
|
190
|
+
const weakEntries = [];
|
|
191
|
+
for (const [key, value] of params.entries()) {
|
|
192
|
+
const normalizedKey = normalizeText(key).toLowerCase();
|
|
193
|
+
if (/p3|status|state|favorite|collect|interested|markstatus|isfavorite|iscollect/.test(normalizedKey)) {
|
|
194
|
+
strongEntries.push(value);
|
|
195
|
+
} else if (/action|op|operation|type|mode|mark|interest/.test(normalizedKey)) {
|
|
196
|
+
weakEntries.push(value);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const value of strongEntries) {
|
|
200
|
+
const action = parseFavoriteActionValue(value);
|
|
201
|
+
if (action) return action;
|
|
202
|
+
}
|
|
203
|
+
for (const value of weakEntries) {
|
|
204
|
+
const action = parseFavoriteActionValue(value);
|
|
205
|
+
if (action) return action;
|
|
206
|
+
}
|
|
207
|
+
} catch {}
|
|
208
|
+
|
|
209
|
+
const fallback = parseFavoriteActionValue(postData);
|
|
210
|
+
if (fallback) return fallback;
|
|
211
|
+
|
|
212
|
+
if (/star-interest-click/i.test(postData)) {
|
|
213
|
+
if (/(?:^|[?&"'\s])p3(?:["'\s:=]){1,3}1(?:$|[&"'\s,}])/i.test(postData)) return "add";
|
|
214
|
+
if (/(?:^|[?&"'\s])p3(?:["'\s:=]){1,3}0(?:$|[&"'\s,}])/i.test(postData)) return "del";
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseFavoriteActionFromRequest(url, postData = "") {
|
|
220
|
+
const normalizedUrl = normalizeText(url).toLowerCase();
|
|
221
|
+
if (!normalizedUrl) return null;
|
|
222
|
+
|
|
223
|
+
if (/\/add(?:\/|$)|[?&](?:action|op|operation|type)=add\b|[?&](?:status|p3|favorite|collect|interested)=1\b/i.test(normalizedUrl)) {
|
|
224
|
+
return "add";
|
|
225
|
+
}
|
|
226
|
+
if (/\/del(?:\/|$)|[?&](?:action|op|operation|type)=del\b|[?&](?:status|p3|favorite|collect|interested)=0\b/i.test(normalizedUrl)) {
|
|
227
|
+
return "del";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return parseFavoriteActionFromPostData(postData);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseFavoriteActionFromActionLog(postData = "") {
|
|
234
|
+
const raw = normalizeText(postData);
|
|
235
|
+
if (!raw) return null;
|
|
236
|
+
try {
|
|
237
|
+
const payload = JSON.parse(raw);
|
|
238
|
+
if (normalizeText(payload?.action).toLowerCase() !== "star-interest-click") return null;
|
|
239
|
+
return parseFavoriteActionValue(payload?.p3);
|
|
240
|
+
} catch {}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const params = new URLSearchParams(raw);
|
|
244
|
+
const actionName = normalizeText(params.get("action")).toLowerCase();
|
|
245
|
+
if (actionName !== "star-interest-click") return null;
|
|
246
|
+
return parseFavoriteActionValue(params.get("p3"));
|
|
247
|
+
} catch {}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parseFavoriteActionFromKnownRequest(url, postData = "") {
|
|
252
|
+
const normalizedUrl = normalizeText(url).toLowerCase();
|
|
253
|
+
if (!normalizedUrl) return null;
|
|
254
|
+
|
|
255
|
+
if (normalizedUrl.includes("usermark")) {
|
|
256
|
+
if (/\/add(?:\/|$)|[?&](?:action|op|operation|type)=add\b/i.test(normalizedUrl)) {
|
|
257
|
+
return "add";
|
|
258
|
+
}
|
|
259
|
+
if (/\/del(?:\/|$)|[?&](?:action|op|operation|type)=del\b/i.test(normalizedUrl)) {
|
|
260
|
+
return "del";
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (normalizedUrl.includes("actionlog/common.json")) {
|
|
266
|
+
return parseFavoriteActionFromActionLog(postData);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseFavoriteActionFromWsPayload(payload, depth = 0) {
|
|
273
|
+
if (depth > 3 || payload === null || payload === undefined) return null;
|
|
274
|
+
|
|
275
|
+
if (typeof payload === "object") {
|
|
276
|
+
if (normalizeText(payload?.action).toLowerCase() === "star-interest-click") {
|
|
277
|
+
const strictAction = parseFavoriteActionValue(payload?.p3);
|
|
278
|
+
if (strictAction) return strictAction;
|
|
279
|
+
}
|
|
280
|
+
const nestedCandidates = [
|
|
281
|
+
payload.data,
|
|
282
|
+
payload.payload,
|
|
283
|
+
payload.body,
|
|
284
|
+
payload.message,
|
|
285
|
+
payload.msg
|
|
286
|
+
];
|
|
287
|
+
for (const nested of nestedCandidates) {
|
|
288
|
+
const action = parseFavoriteActionFromWsPayload(nested, depth + 1);
|
|
289
|
+
if (action) return action;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const text = normalizeText(payload);
|
|
295
|
+
if (!text) return null;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(text);
|
|
299
|
+
const action = parseFavoriteActionFromWsPayload(parsed, depth + 1);
|
|
300
|
+
if (action) return action;
|
|
301
|
+
} catch {}
|
|
302
|
+
|
|
303
|
+
const actionFromActionLog = parseFavoriteActionFromActionLog(text);
|
|
304
|
+
if (actionFromActionLog) return actionFromActionLog;
|
|
305
|
+
if (/usermark/i.test(text)) {
|
|
306
|
+
if (/\/add(?:\/|$)|[?&](?:action|op|operation|type)=add\b/i.test(text)) return "add";
|
|
307
|
+
if (/\/del(?:\/|$)|[?&](?:action|op|operation|type)=del\b/i.test(text)) return "del";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isRecoverablePostActionError(error, action) {
|
|
314
|
+
const normalizedAction = normalizeText(action).toLowerCase();
|
|
315
|
+
const normalizedCode = normalizeText(error?.code).toUpperCase();
|
|
316
|
+
if (!normalizedAction || !normalizedCode) return false;
|
|
317
|
+
if (normalizedAction === "favorite" && normalizedCode === "FAVORITE_BUTTON_FAILED") return true;
|
|
318
|
+
if (normalizedAction === "greet" && normalizedCode === "GREET_BUTTON_FAILED") return true;
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
71
322
|
function loadCalibrationPosition(filePath) {
|
|
72
323
|
try {
|
|
73
324
|
const resolved = path.resolve(String(filePath || ""));
|
|
@@ -139,69 +390,71 @@ function parseArgs(argv) {
|
|
|
139
390
|
};
|
|
140
391
|
|
|
141
392
|
for (let index = 0; index < argv.length; index += 1) {
|
|
142
|
-
const
|
|
393
|
+
const normalizedToken = normalizeCliOptionToken(argv[index]);
|
|
394
|
+
const token = normalizedToken.token;
|
|
143
395
|
const next = argv[index + 1];
|
|
144
|
-
|
|
145
|
-
|
|
396
|
+
const inlineValue = normalizedToken.inlineValue;
|
|
397
|
+
if ((token === "--baseurl" || token === "--base-url") && (inlineValue || next)) {
|
|
398
|
+
parsed.baseUrl = inlineValue || next;
|
|
146
399
|
parsed.__provided.baseUrl = true;
|
|
147
|
-
index += 1;
|
|
148
|
-
} else if (token === "--apikey" && next) {
|
|
149
|
-
parsed.apiKey = next;
|
|
400
|
+
if (!inlineValue) index += 1;
|
|
401
|
+
} else if ((token === "--apikey" || token === "--api-key") && (inlineValue || next)) {
|
|
402
|
+
parsed.apiKey = inlineValue || next;
|
|
150
403
|
parsed.__provided.apiKey = true;
|
|
151
|
-
index += 1;
|
|
152
|
-
} else if (token === "--model" && next) {
|
|
153
|
-
parsed.model = next;
|
|
404
|
+
if (!inlineValue) index += 1;
|
|
405
|
+
} else if (token === "--model" && (inlineValue || next)) {
|
|
406
|
+
parsed.model = inlineValue || next;
|
|
154
407
|
parsed.__provided.model = true;
|
|
155
|
-
index += 1;
|
|
156
|
-
} else if (token === "--openai-organization" && next) {
|
|
157
|
-
parsed.openaiOrganization = next;
|
|
158
|
-
index += 1;
|
|
159
|
-
} else if (token === "--openai-project" && next) {
|
|
160
|
-
parsed.openaiProject = next;
|
|
161
|
-
index += 1;
|
|
162
|
-
} else if (token === "--criteria" && next) {
|
|
163
|
-
parsed.criteria = next;
|
|
408
|
+
if (!inlineValue) index += 1;
|
|
409
|
+
} else if (token === "--openai-organization" && (inlineValue || next)) {
|
|
410
|
+
parsed.openaiOrganization = inlineValue || next;
|
|
411
|
+
if (!inlineValue) index += 1;
|
|
412
|
+
} else if (token === "--openai-project" && (inlineValue || next)) {
|
|
413
|
+
parsed.openaiProject = inlineValue || next;
|
|
414
|
+
if (!inlineValue) index += 1;
|
|
415
|
+
} else if (token === "--criteria" && (inlineValue || next)) {
|
|
416
|
+
parsed.criteria = inlineValue || next;
|
|
164
417
|
parsed.__provided.criteria = true;
|
|
165
|
-
index += 1;
|
|
166
|
-
} else if (token === "--targetCount" && next) {
|
|
167
|
-
parsed.targetCount = parsePositiveInteger(next);
|
|
418
|
+
if (!inlineValue) index += 1;
|
|
419
|
+
} else if ((token === "--targetCount" || token === "--target-count") && (inlineValue || next)) {
|
|
420
|
+
parsed.targetCount = parsePositiveInteger(inlineValue || next);
|
|
168
421
|
parsed.__provided.targetCount = true;
|
|
169
|
-
index += 1;
|
|
170
|
-
} else if (token === "--max-greet-count" && next) {
|
|
171
|
-
parsed.maxGreetCount = parsePositiveInteger(next);
|
|
422
|
+
if (!inlineValue) index += 1;
|
|
423
|
+
} else if ((token === "--max-greet-count" || token === "--maxGreetCount") && (inlineValue || next)) {
|
|
424
|
+
parsed.maxGreetCount = parsePositiveInteger(inlineValue || next);
|
|
172
425
|
parsed.__provided.maxGreetCount = true;
|
|
173
|
-
index += 1;
|
|
174
|
-
} else if (token === "--page-scope" && next) {
|
|
175
|
-
parsed.pageScope = normalizePageScope(next) || "recommend";
|
|
426
|
+
if (!inlineValue) index += 1;
|
|
427
|
+
} else if ((token === "--page-scope" || token === "--pageScope" || token === "--page_scope") && (inlineValue || next)) {
|
|
428
|
+
parsed.pageScope = normalizePageScope(inlineValue || next) || "recommend";
|
|
176
429
|
parsed.__provided.pageScope = true;
|
|
177
|
-
index += 1;
|
|
178
|
-
} else if (token === "--calibration" && next) {
|
|
179
|
-
parsed.calibrationPath = path.resolve(next);
|
|
430
|
+
if (!inlineValue) index += 1;
|
|
431
|
+
} else if ((token === "--calibration" || token === "--calibration-path") && (inlineValue || next)) {
|
|
432
|
+
parsed.calibrationPath = path.resolve(inlineValue || next);
|
|
180
433
|
parsed.__provided.calibrationPath = true;
|
|
181
|
-
index += 1;
|
|
182
|
-
} else if (token === "--port" && next) {
|
|
183
|
-
parsed.port = parsePositiveInteger(next) || DEFAULT_PORT;
|
|
434
|
+
if (!inlineValue) index += 1;
|
|
435
|
+
} else if (token === "--port" && (inlineValue || next)) {
|
|
436
|
+
parsed.port = parsePositiveInteger(inlineValue || next) || DEFAULT_PORT;
|
|
184
437
|
parsed.__provided.port = true;
|
|
185
|
-
index += 1;
|
|
186
|
-
} else if (token === "--output" && next) {
|
|
187
|
-
parsed.output = path.resolve(next);
|
|
188
|
-
index += 1;
|
|
189
|
-
} else if (token === "--checkpoint-path" && next) {
|
|
190
|
-
parsed.checkpointPath = path.resolve(next);
|
|
191
|
-
index += 1;
|
|
192
|
-
} else if (token === "--pause-control-path" && next) {
|
|
193
|
-
parsed.pauseControlPath = path.resolve(next);
|
|
194
|
-
index += 1;
|
|
438
|
+
if (!inlineValue) index += 1;
|
|
439
|
+
} else if (token === "--output" && (inlineValue || next)) {
|
|
440
|
+
parsed.output = path.resolve(inlineValue || next);
|
|
441
|
+
if (!inlineValue) index += 1;
|
|
442
|
+
} else if (token === "--checkpoint-path" && (inlineValue || next)) {
|
|
443
|
+
parsed.checkpointPath = path.resolve(inlineValue || next);
|
|
444
|
+
if (!inlineValue) index += 1;
|
|
445
|
+
} else if (token === "--pause-control-path" && (inlineValue || next)) {
|
|
446
|
+
parsed.pauseControlPath = path.resolve(inlineValue || next);
|
|
447
|
+
if (!inlineValue) index += 1;
|
|
195
448
|
} else if (token === "--resume") {
|
|
196
449
|
parsed.resume = true;
|
|
197
|
-
} else if (token === "--post-action" && next) {
|
|
198
|
-
parsed.postAction = normalizePostAction(next);
|
|
450
|
+
} else if ((token === "--post-action" || token === "--postAction") && (inlineValue || next)) {
|
|
451
|
+
parsed.postAction = normalizePostAction(inlineValue || next);
|
|
199
452
|
parsed.__provided.postAction = true;
|
|
200
|
-
index += 1;
|
|
201
|
-
} else if (token === "--post-action-confirmed" && next) {
|
|
202
|
-
parsed.postActionConfirmed = parseBoolean(next);
|
|
453
|
+
if (!inlineValue) index += 1;
|
|
454
|
+
} else if ((token === "--post-action-confirmed" || token === "--postActionConfirmed") && (inlineValue || next)) {
|
|
455
|
+
parsed.postActionConfirmed = parseBoolean(inlineValue || next);
|
|
203
456
|
parsed.__provided.postActionConfirmed = true;
|
|
204
|
-
index += 1;
|
|
457
|
+
if (!inlineValue) index += 1;
|
|
205
458
|
} else if (token === "--help" || token === "-h") {
|
|
206
459
|
parsed.help = true;
|
|
207
460
|
}
|
|
@@ -1487,6 +1740,13 @@ const jsReloadRecommendFrame = `(() => {
|
|
|
1487
1740
|
class RecommendScreenCli {
|
|
1488
1741
|
constructor(args) {
|
|
1489
1742
|
this.args = args;
|
|
1743
|
+
const baseUrlCheck = validateUrlString(this.args.baseUrl);
|
|
1744
|
+
if (this.args.baseUrl && !baseUrlCheck.ok) {
|
|
1745
|
+
log(`[警告] baseUrl 校验失败: ${baseUrlCheck.error}, 原始值=${JSON.stringify(this.args.baseUrl)}`);
|
|
1746
|
+
}
|
|
1747
|
+
if (baseUrlCheck.sanitized) {
|
|
1748
|
+
this.args.baseUrl = baseUrlCheck.sanitized;
|
|
1749
|
+
}
|
|
1490
1750
|
this.client = null;
|
|
1491
1751
|
this.Runtime = null;
|
|
1492
1752
|
this.Input = null;
|
|
@@ -1516,6 +1776,8 @@ class RecommendScreenCli {
|
|
|
1516
1776
|
this.latestResumeNetworkPayload = null;
|
|
1517
1777
|
this.favoriteActionEvents = [];
|
|
1518
1778
|
this.favoriteClickPendingSince = 0;
|
|
1779
|
+
this.favoriteNetworkTraces = [];
|
|
1780
|
+
this.webSocketByRequestId = new Map();
|
|
1519
1781
|
this.resumeSourceStats = {
|
|
1520
1782
|
network: 0,
|
|
1521
1783
|
image_fallback: 0
|
|
@@ -1610,6 +1872,7 @@ class RecommendScreenCli {
|
|
|
1610
1872
|
|
|
1611
1873
|
markFavoriteClickPending() {
|
|
1612
1874
|
this.favoriteClickPendingSince = Date.now();
|
|
1875
|
+
this.favoriteNetworkTraces = [];
|
|
1613
1876
|
}
|
|
1614
1877
|
|
|
1615
1878
|
consumeFavoriteActionResult(since = 0) {
|
|
@@ -1626,6 +1889,30 @@ class RecommendScreenCli {
|
|
|
1626
1889
|
return matched.action || null;
|
|
1627
1890
|
}
|
|
1628
1891
|
|
|
1892
|
+
recordFavoriteNetworkTrace(entry) {
|
|
1893
|
+
const trace = {
|
|
1894
|
+
ts: Date.now(),
|
|
1895
|
+
...entry
|
|
1896
|
+
};
|
|
1897
|
+
this.favoriteNetworkTraces.push(trace);
|
|
1898
|
+
if (this.favoriteNetworkTraces.length > 60) {
|
|
1899
|
+
this.favoriteNetworkTraces = this.favoriteNetworkTraces.slice(-60);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
summarizeFavoriteNetworkTrace(since = 0) {
|
|
1904
|
+
const timestamp = Number.isFinite(since) ? since : 0;
|
|
1905
|
+
return this.favoriteNetworkTraces
|
|
1906
|
+
.filter((item) => Number(item?.ts || 0) >= timestamp)
|
|
1907
|
+
.slice(-12)
|
|
1908
|
+
.map((item) => {
|
|
1909
|
+
if (item.kind === "ws") {
|
|
1910
|
+
return `[ws:${item.direction}] ${item.url || "unknown"} payload=${item.payload || ""}`;
|
|
1911
|
+
}
|
|
1912
|
+
return `[http] ${item.method || "GET"} ${item.url || ""} body=${item.postData || ""}`;
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1629
1916
|
cacheResumeNetworkPayload(payload, fallbackGeekId = null) {
|
|
1630
1917
|
if (!payload || typeof payload !== "object") return;
|
|
1631
1918
|
const geekDetail = payload.geekDetail || payload;
|
|
@@ -1704,31 +1991,55 @@ class RecommendScreenCli {
|
|
|
1704
1991
|
|
|
1705
1992
|
if (this.favoriteClickPendingSince <= 0) return;
|
|
1706
1993
|
const requestTs = Date.now();
|
|
1707
|
-
if (requestTs < this.favoriteClickPendingSince
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1994
|
+
if (requestTs < this.favoriteClickPendingSince) return;
|
|
1995
|
+
const method = normalizeText(params?.request?.method || "").toUpperCase() || "GET";
|
|
1996
|
+
const postData = params?.request?.postData || "";
|
|
1997
|
+
this.recordFavoriteNetworkTrace({
|
|
1998
|
+
ts: requestTs,
|
|
1999
|
+
kind: "http",
|
|
2000
|
+
method,
|
|
2001
|
+
url: url.slice(0, 240),
|
|
2002
|
+
postData: normalizeText(postData).slice(0, 200)
|
|
2003
|
+
});
|
|
2004
|
+
const action = parseFavoriteActionFromKnownRequest(url, postData);
|
|
2005
|
+
if (!action) return;
|
|
2006
|
+
const source = url.includes("userMark")
|
|
2007
|
+
? "userMark"
|
|
2008
|
+
: url.includes("actionLog/common.json")
|
|
2009
|
+
? "actionLog"
|
|
2010
|
+
: "favorite";
|
|
2011
|
+
this.favoriteActionEvents.push({ action, ts: requestTs, source, url });
|
|
2012
|
+
}
|
|
1720
2013
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2014
|
+
handleNetworkWebSocketCreated(params) {
|
|
2015
|
+
const requestId = normalizeText(params?.requestId || "");
|
|
2016
|
+
if (!requestId) return;
|
|
2017
|
+
const url = normalizeText(params?.url || "");
|
|
2018
|
+
this.webSocketByRequestId.set(requestId, url || "");
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
handleNetworkWebSocketFrame(params, direction = "sent") {
|
|
2022
|
+
if (this.favoriteClickPendingSince <= 0) return;
|
|
2023
|
+
const ts = Date.now();
|
|
2024
|
+
if (ts < this.favoriteClickPendingSince) return;
|
|
2025
|
+
const requestId = normalizeText(params?.requestId || "");
|
|
2026
|
+
const payloadData = normalizeText(params?.response?.payloadData || "");
|
|
2027
|
+
const wsUrl = this.webSocketByRequestId.get(requestId) || "";
|
|
2028
|
+
this.recordFavoriteNetworkTrace({
|
|
2029
|
+
ts,
|
|
2030
|
+
kind: "ws",
|
|
2031
|
+
direction,
|
|
2032
|
+
url: wsUrl ? wsUrl.slice(0, 240) : requestId ? `ws:${requestId}` : "ws",
|
|
2033
|
+
payload: payloadData.slice(0, 200)
|
|
2034
|
+
});
|
|
2035
|
+
const action = parseFavoriteActionFromWsPayload(payloadData);
|
|
2036
|
+
if (!action) return;
|
|
2037
|
+
this.favoriteActionEvents.push({
|
|
2038
|
+
action,
|
|
2039
|
+
ts,
|
|
2040
|
+
source: `websocket_${direction}`,
|
|
2041
|
+
url: wsUrl || (requestId ? `ws:${requestId}` : "websocket")
|
|
2042
|
+
});
|
|
1732
2043
|
}
|
|
1733
2044
|
|
|
1734
2045
|
async handleNetworkLoadingFinished(params) {
|
|
@@ -1891,6 +2202,27 @@ class RecommendScreenCli {
|
|
|
1891
2202
|
} catch {}
|
|
1892
2203
|
});
|
|
1893
2204
|
}
|
|
2205
|
+
if (typeof this.Network.webSocketCreated === "function") {
|
|
2206
|
+
this.Network.webSocketCreated((params) => {
|
|
2207
|
+
try {
|
|
2208
|
+
this.handleNetworkWebSocketCreated(params);
|
|
2209
|
+
} catch {}
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
if (typeof this.Network.webSocketFrameSent === "function") {
|
|
2213
|
+
this.Network.webSocketFrameSent((params) => {
|
|
2214
|
+
try {
|
|
2215
|
+
this.handleNetworkWebSocketFrame(params, "sent");
|
|
2216
|
+
} catch {}
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
if (typeof this.Network.webSocketFrameReceived === "function") {
|
|
2220
|
+
this.Network.webSocketFrameReceived((params) => {
|
|
2221
|
+
try {
|
|
2222
|
+
this.handleNetworkWebSocketFrame(params, "received");
|
|
2223
|
+
} catch {}
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
1894
2226
|
if (typeof this.Network.loadingFinished === "function") {
|
|
1895
2227
|
this.Network.loadingFinished((params) => {
|
|
1896
2228
|
this.handleNetworkLoadingFinished(params).catch(() => {});
|
|
@@ -1988,7 +2320,7 @@ class RecommendScreenCli {
|
|
|
1988
2320
|
async pressEsc() {
|
|
1989
2321
|
await this.Input.dispatchKeyEvent({ type: "keyDown", windowsVirtualKeyCode: 27, key: "Escape", code: "Escape" });
|
|
1990
2322
|
await this.Input.dispatchKeyEvent({ type: "keyUp", windowsVirtualKeyCode: 27, key: "Escape", code: "Escape" });
|
|
1991
|
-
}
|
|
2323
|
+
}
|
|
1992
2324
|
async ensureDetailOpen() {
|
|
1993
2325
|
for (let index = 0; index < 20; index += 1) {
|
|
1994
2326
|
const state = await this.evaluate(jsWaitForDetail);
|
|
@@ -2308,7 +2640,9 @@ class RecommendScreenCli {
|
|
|
2308
2640
|
|
|
2309
2641
|
async callVisionModel(imagePath) {
|
|
2310
2642
|
const imageBase64 = fs.readFileSync(imagePath, "base64");
|
|
2311
|
-
const
|
|
2643
|
+
const rawBaseUrl = this.args.baseUrl;
|
|
2644
|
+
log(`[callVisionModel] baseUrl 原始值类型=${typeof rawBaseUrl}, 长度=${rawBaseUrl != null ? String(rawBaseUrl).length : "null/undefined"}, JSON编码=${JSON.stringify(rawBaseUrl)}`);
|
|
2645
|
+
const baseUrl = String(rawBaseUrl || "").replace(/\/$/, "");
|
|
2312
2646
|
const payload = {
|
|
2313
2647
|
model: this.args.model,
|
|
2314
2648
|
temperature: 0.1,
|
|
@@ -2364,7 +2698,9 @@ class RecommendScreenCli {
|
|
|
2364
2698
|
|
|
2365
2699
|
async callTextModel(resumeText) {
|
|
2366
2700
|
const safeResumeText = String(resumeText || "").slice(0, 28000);
|
|
2367
|
-
const
|
|
2701
|
+
const rawBaseUrl = this.args.baseUrl;
|
|
2702
|
+
log(`[callTextModel] baseUrl 原始值类型=${typeof rawBaseUrl}, 长度=${rawBaseUrl != null ? String(rawBaseUrl).length : "null/undefined"}, JSON编码=${JSON.stringify(rawBaseUrl)}`);
|
|
2703
|
+
const baseUrl = String(rawBaseUrl || "").replace(/\/$/, "");
|
|
2368
2704
|
const payload = {
|
|
2369
2705
|
model: this.args.model,
|
|
2370
2706
|
temperature: 0.1,
|
|
@@ -2410,7 +2746,8 @@ class RecommendScreenCli {
|
|
|
2410
2746
|
async favoriteCandidate(options = {}) {
|
|
2411
2747
|
if (this.args.pageScope === "featured") {
|
|
2412
2748
|
if (options.alreadyInterested === true) {
|
|
2413
|
-
|
|
2749
|
+
log("[FAVORITE] network profile indicates alreadyInterested=true,跳过点击以避免误取消收藏。");
|
|
2750
|
+
return { actionTaken: "already_favorited", source: "network_profile" };
|
|
2414
2751
|
}
|
|
2415
2752
|
if (!this.featuredCalibration?.position) {
|
|
2416
2753
|
throw this.buildError(
|
|
@@ -2425,6 +2762,7 @@ class RecommendScreenCli {
|
|
|
2425
2762
|
|
|
2426
2763
|
const base = this.featuredCalibration.position;
|
|
2427
2764
|
const maxClicks = 5;
|
|
2765
|
+
let detectedAlreadyFavoritedByNetwork = false;
|
|
2428
2766
|
for (let clickIndex = 0; clickIndex < maxClicks; clickIndex += 1) {
|
|
2429
2767
|
const clickStartedAt = Date.now();
|
|
2430
2768
|
this.markFavoriteClickPending();
|
|
@@ -2440,22 +2778,38 @@ class RecommendScreenCli {
|
|
|
2440
2778
|
}
|
|
2441
2779
|
|
|
2442
2780
|
let sawDel = false;
|
|
2443
|
-
for (let index = 0; index <
|
|
2781
|
+
for (let index = 0; index < 14; index += 1) {
|
|
2444
2782
|
await sleep(humanDelay(260, 80));
|
|
2445
2783
|
const networkAction = this.consumeFavoriteActionResult(clickStartedAt);
|
|
2446
2784
|
if (networkAction === "add") {
|
|
2447
|
-
return
|
|
2785
|
+
return detectedAlreadyFavoritedByNetwork
|
|
2786
|
+
? { actionTaken: "already_favorited", re_favorited: true }
|
|
2787
|
+
: { actionTaken: "favorite" };
|
|
2448
2788
|
}
|
|
2449
2789
|
if (networkAction === "del") {
|
|
2790
|
+
detectedAlreadyFavoritedByNetwork = true;
|
|
2791
|
+
log("[FAVORITE] 检测到 network=del,推断该人选原本已收藏,继续点击恢复为收藏状态。");
|
|
2450
2792
|
sawDel = true;
|
|
2451
2793
|
break;
|
|
2452
2794
|
}
|
|
2453
2795
|
}
|
|
2454
2796
|
if (!sawDel && clickIndex === maxClicks - 1) {
|
|
2797
|
+
const traceSummary = this.summarizeFavoriteNetworkTrace(clickStartedAt);
|
|
2798
|
+
if (traceSummary.length > 0) {
|
|
2799
|
+
log(`[FAVORITE_NETWORK_TRACE] ${traceSummary.join(" | ")}`);
|
|
2800
|
+
} else {
|
|
2801
|
+
log("[FAVORITE_NETWORK_TRACE] 点击后未捕获到可识别的 HTTP/WS 网络信号。");
|
|
2802
|
+
}
|
|
2455
2803
|
break;
|
|
2456
2804
|
}
|
|
2457
2805
|
}
|
|
2458
2806
|
|
|
2807
|
+
if (detectedAlreadyFavoritedByNetwork) {
|
|
2808
|
+
throw this.buildError(
|
|
2809
|
+
"FAVORITE_BUTTON_FAILED",
|
|
2810
|
+
"精选页检测到 network del(原本已收藏),但后续未检测到 network add(恢复收藏)成功信号。"
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2459
2813
|
throw this.buildError("FAVORITE_BUTTON_FAILED", "精选页收藏未检测到 network add 成功信号。");
|
|
2460
2814
|
}
|
|
2461
2815
|
|
|
@@ -2666,6 +3020,10 @@ class RecommendScreenCli {
|
|
|
2666
3020
|
if (!this.args.baseUrl || !this.args.apiKey || !this.args.model) {
|
|
2667
3021
|
throw this.buildError("SCREEN_CONFIG_ERROR", "Missing baseUrl/apiKey/model", false);
|
|
2668
3022
|
}
|
|
3023
|
+
log(
|
|
3024
|
+
`[ARGS] page_scope=${this.args.pageScope} target_count=${this.args.targetCount ?? "none"} ` +
|
|
3025
|
+
`post_action=${this.args.postAction || "unset"} port=${this.args.port}`
|
|
3026
|
+
);
|
|
2669
3027
|
|
|
2670
3028
|
if (!(this.args.postActionConfirmed === true && this.args.postAction)) {
|
|
2671
3029
|
this.args.postAction = await promptPostAction();
|
|
@@ -2854,23 +3212,41 @@ class RecommendScreenCli {
|
|
|
2854
3212
|
effectiveAction = "favorite";
|
|
2855
3213
|
this.greetLimitFallbackCount += 1;
|
|
2856
3214
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
:
|
|
3215
|
+
let actionResult = { actionTaken: "none" };
|
|
3216
|
+
try {
|
|
3217
|
+
actionResult = effectiveAction === "favorite"
|
|
3218
|
+
? await this.favoriteCandidate({
|
|
3219
|
+
alreadyInterested: networkCandidateInfo?.alreadyInterested === true
|
|
3220
|
+
})
|
|
3221
|
+
: effectiveAction === "greet"
|
|
3222
|
+
? await this.greetCandidate()
|
|
3223
|
+
: { actionTaken: "none" };
|
|
3224
|
+
} catch (postActionError) {
|
|
3225
|
+
if (!isRecoverablePostActionError(postActionError, effectiveAction)) {
|
|
3226
|
+
throw postActionError;
|
|
3227
|
+
}
|
|
3228
|
+
log(`[POST_ACTION_WARN] ${effectiveAction} 失败,继续写入通过候选人: ${postActionError.message || postActionError}`);
|
|
3229
|
+
actionResult = {
|
|
3230
|
+
actionTaken: `${effectiveAction}_failed`,
|
|
3231
|
+
errorCode: postActionError.code || "POST_ACTION_FAILED",
|
|
3232
|
+
errorMessage: normalizeText(postActionError.message || "post action failed")
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
2864
3235
|
if (actionResult.actionTaken === "greet") {
|
|
2865
3236
|
this.greetCount += 1;
|
|
2866
3237
|
}
|
|
3238
|
+
const screeningReason = normalizeText(screening.reason || screening.summary || "");
|
|
3239
|
+
const actionErrorMessage = normalizeText(actionResult.errorMessage || "");
|
|
3240
|
+
const mergedReason = actionErrorMessage
|
|
3241
|
+
? `${screeningReason}${screeningReason ? " | " : ""}[${effectiveAction}失败] ${actionErrorMessage}`
|
|
3242
|
+
: screeningReason;
|
|
2867
3243
|
this.passedCandidates.push({
|
|
2868
3244
|
name: candidateProfile.name,
|
|
2869
3245
|
school: candidateProfile.school,
|
|
2870
3246
|
major: candidateProfile.major,
|
|
2871
3247
|
company: candidateProfile.company,
|
|
2872
3248
|
position: candidateProfile.position,
|
|
2873
|
-
reason:
|
|
3249
|
+
reason: mergedReason,
|
|
2874
3250
|
action: actionResult.actionTaken,
|
|
2875
3251
|
geekId: nextCandidate.geek_id,
|
|
2876
3252
|
summary: screening.summary,
|
|
@@ -3026,8 +3402,13 @@ if (require.main === module) {
|
|
|
3026
3402
|
__testables: {
|
|
3027
3403
|
MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES,
|
|
3028
3404
|
RESUME_CAPTURE_MAX_ATTEMPTS,
|
|
3029
|
-
RESUME_CAPTURE_WAIT_MS
|
|
3405
|
+
RESUME_CAPTURE_WAIT_MS,
|
|
3406
|
+
parseFavoriteActionFromPostData,
|
|
3407
|
+
parseFavoriteActionFromRequest,
|
|
3408
|
+
parseFavoriteActionFromKnownRequest,
|
|
3409
|
+
parseFavoriteActionFromActionLog,
|
|
3410
|
+
parseFavoriteActionFromWsPayload,
|
|
3411
|
+
isRecoverablePostActionError
|
|
3030
3412
|
}
|
|
3031
3413
|
};
|
|
3032
3414
|
}
|
|
3033
|
-
|
|
@@ -4,7 +4,7 @@ const os = require("node:os");
|
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const sharp = require("sharp");
|
|
6
6
|
|
|
7
|
-
const { RecommendScreenCli, __testables } = require("./boss-recommend-screen-cli.cjs");
|
|
7
|
+
const { RecommendScreenCli, parseArgs, __testables } = require("./boss-recommend-screen-cli.cjs");
|
|
8
8
|
const { __testables: captureTestables } = require("./scripts/capture-full-resume-canvas.cjs");
|
|
9
9
|
|
|
10
10
|
class FakeRecommendScreenCli extends RecommendScreenCli {
|
|
@@ -434,6 +434,62 @@ async function testFeaturedFavoriteShouldNotUseDomFallback() {
|
|
|
434
434
|
assert.equal(evaluateCalls, 0);
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
+
async function testFeaturedFavoriteShouldSkipClickWhenAlreadyInterested() {
|
|
438
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-already-"));
|
|
439
|
+
const args = createArgs(tempDir);
|
|
440
|
+
args.pageScope = "featured";
|
|
441
|
+
const calibrationPath = path.join(tempDir, "favorite-calibration.json");
|
|
442
|
+
fs.writeFileSync(calibrationPath, JSON.stringify({
|
|
443
|
+
favoritePosition: {
|
|
444
|
+
pageX: 120,
|
|
445
|
+
pageY: 220,
|
|
446
|
+
canvasX: 0,
|
|
447
|
+
canvasY: 0
|
|
448
|
+
}
|
|
449
|
+
}, null, 2));
|
|
450
|
+
args.calibrationPath = calibrationPath;
|
|
451
|
+
const cli = new RecommendScreenCli(args);
|
|
452
|
+
let clickCalls = 0;
|
|
453
|
+
cli.simulateHumanClick = async () => {
|
|
454
|
+
clickCalls += 1;
|
|
455
|
+
};
|
|
456
|
+
const result = await cli.favoriteCandidate({ alreadyInterested: true });
|
|
457
|
+
assert.equal(result.actionTaken, "already_favorited");
|
|
458
|
+
assert.equal(result.source, "network_profile");
|
|
459
|
+
assert.equal(clickCalls, 0);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function testFeaturedFavoriteShouldRecognizeAlreadyFavoritedByDelThenAdd() {
|
|
463
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-del-add-"));
|
|
464
|
+
const args = createArgs(tempDir);
|
|
465
|
+
args.pageScope = "featured";
|
|
466
|
+
const calibrationPath = path.join(tempDir, "favorite-calibration.json");
|
|
467
|
+
fs.writeFileSync(calibrationPath, JSON.stringify({
|
|
468
|
+
favoritePosition: {
|
|
469
|
+
pageX: 120,
|
|
470
|
+
pageY: 220,
|
|
471
|
+
canvasX: 0,
|
|
472
|
+
canvasY: 0
|
|
473
|
+
}
|
|
474
|
+
}, null, 2));
|
|
475
|
+
args.calibrationPath = calibrationPath;
|
|
476
|
+
const cli = new RecommendScreenCli(args);
|
|
477
|
+
let clickCalls = 0;
|
|
478
|
+
cli.simulateHumanClick = async () => {
|
|
479
|
+
clickCalls += 1;
|
|
480
|
+
cli.favoriteActionEvents.push({
|
|
481
|
+
action: clickCalls === 1 ? "del" : "add",
|
|
482
|
+
ts: Date.now(),
|
|
483
|
+
source: "test",
|
|
484
|
+
url: clickCalls === 1 ? "userMark/del" : "userMark/add"
|
|
485
|
+
});
|
|
486
|
+
};
|
|
487
|
+
const result = await cli.favoriteCandidate();
|
|
488
|
+
assert.equal(result.actionTaken, "already_favorited");
|
|
489
|
+
assert.equal(result.re_favorited, true);
|
|
490
|
+
assert.equal(clickCalls, 2);
|
|
491
|
+
}
|
|
492
|
+
|
|
437
493
|
async function testFeaturedFavoriteWithoutCalibrationShouldFail() {
|
|
438
494
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-favorite-missing-cal-"));
|
|
439
495
|
const args = createArgs(tempDir);
|
|
@@ -449,6 +505,91 @@ async function testFeaturedFavoriteWithoutCalibrationShouldFail() {
|
|
|
449
505
|
);
|
|
450
506
|
}
|
|
451
507
|
|
|
508
|
+
function testFavoriteActionParserShouldSupportBodySignals() {
|
|
509
|
+
const addFromJson = __testables.parseFavoriteActionFromPostData(JSON.stringify({
|
|
510
|
+
action: "star-interest-click",
|
|
511
|
+
p3: 1
|
|
512
|
+
}));
|
|
513
|
+
const delFromForm = __testables.parseFavoriteActionFromPostData("action=star-interest-click&p3=0");
|
|
514
|
+
assert.equal(addFromJson, "add");
|
|
515
|
+
assert.equal(delFromForm, "del");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function testFavoriteActionParserShouldSupportFallbackRequestShape() {
|
|
519
|
+
const action = __testables.parseFavoriteActionFromRequest(
|
|
520
|
+
"https://www.zhipin.com/wapi/zpgeek/favorite/operate",
|
|
521
|
+
JSON.stringify({ op: "add", geekId: "abc" })
|
|
522
|
+
);
|
|
523
|
+
assert.equal(action, "add");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function testFavoriteActionParserShouldSupportWebSocketPayload() {
|
|
527
|
+
const addFromWsJson = __testables.parseFavoriteActionFromWsPayload(JSON.stringify({
|
|
528
|
+
action: "star-interest-click",
|
|
529
|
+
p3: 1
|
|
530
|
+
}));
|
|
531
|
+
const delFromWsForm = __testables.parseFavoriteActionFromWsPayload("action=star-interest-click&p3=0");
|
|
532
|
+
assert.equal(addFromWsJson, "add");
|
|
533
|
+
assert.equal(delFromWsForm, "del");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function testFavoriteActionParserShouldOnlyTrustKnownRequestShapes() {
|
|
537
|
+
const unknown = __testables.parseFavoriteActionFromKnownRequest(
|
|
538
|
+
"https://www.zhipin.com/wapi/other/metrics",
|
|
539
|
+
JSON.stringify({ action: "add", p3: 1 })
|
|
540
|
+
);
|
|
541
|
+
const actionLog = __testables.parseFavoriteActionFromKnownRequest(
|
|
542
|
+
"https://www.zhipin.com/wapi/zplog/actionLog/common.json",
|
|
543
|
+
JSON.stringify({ action: "star-interest-click", p3: 1 })
|
|
544
|
+
);
|
|
545
|
+
const userMark = __testables.parseFavoriteActionFromKnownRequest(
|
|
546
|
+
"https://www.zhipin.com/wapi/zpgeek/userMark/add",
|
|
547
|
+
""
|
|
548
|
+
);
|
|
549
|
+
assert.equal(unknown, null);
|
|
550
|
+
assert.equal(actionLog, "add");
|
|
551
|
+
assert.equal(userMark, "add");
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function testFeaturedPostActionFailureShouldStillRecordPassedCandidate() {
|
|
555
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-featured-action-failure-"));
|
|
556
|
+
const args = createArgs(tempDir);
|
|
557
|
+
args.pageScope = "featured";
|
|
558
|
+
args.postAction = "favorite";
|
|
559
|
+
const candidate = { key: "featured-fav-fail", geek_id: "featured-fav-fail", name: "featured candidate" };
|
|
560
|
+
const cli = new FakeRecommendScreenCli(args, {
|
|
561
|
+
candidates: [candidate]
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
cli.waitForNetworkResumeCandidateInfo = async () => ({
|
|
565
|
+
name: "featured candidate",
|
|
566
|
+
school: "测试大学",
|
|
567
|
+
major: "人工智能",
|
|
568
|
+
company: "测试公司",
|
|
569
|
+
position: "算法工程师",
|
|
570
|
+
resumeText: "满足测试标准"
|
|
571
|
+
});
|
|
572
|
+
cli.callTextModel = async () => ({
|
|
573
|
+
passed: true,
|
|
574
|
+
reason: "通过",
|
|
575
|
+
summary: "通过"
|
|
576
|
+
});
|
|
577
|
+
cli.favoriteCandidate = async () => {
|
|
578
|
+
const error = new Error("精选页收藏未检测到 network add 成功信号。");
|
|
579
|
+
error.code = "FAVORITE_BUTTON_FAILED";
|
|
580
|
+
throw error;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const result = await cli.run();
|
|
584
|
+
assert.equal(result.status, "COMPLETED");
|
|
585
|
+
assert.equal(result.result.processed_count, 1);
|
|
586
|
+
assert.equal(result.result.passed_count, 1);
|
|
587
|
+
assert.equal(result.result.skipped_count, 0);
|
|
588
|
+
assert.equal(cli.passedCandidates.length, 1);
|
|
589
|
+
assert.equal(cli.passedCandidates[0].action, "favorite_failed");
|
|
590
|
+
assert.match(cli.passedCandidates[0].reason, /\[favorite失败]/);
|
|
591
|
+
}
|
|
592
|
+
|
|
452
593
|
async function testStitchWithSharpShouldComposeExpectedImage() {
|
|
453
594
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-sharp-stitch-"));
|
|
454
595
|
const chunkA = path.join(tempDir, "chunk_000.png");
|
|
@@ -540,6 +681,27 @@ function testStitchWithAvailablePythonShouldFailWhenScriptMissing() {
|
|
|
540
681
|
assert.equal(result.attempts[0].command, "python3");
|
|
541
682
|
}
|
|
542
683
|
|
|
684
|
+
function testParseArgsShouldSupportFeaturedAliasesAndInlinePort() {
|
|
685
|
+
const parsed = parseArgs([
|
|
686
|
+
"--criteria", "test criteria",
|
|
687
|
+
"--baseurl", "https://example.com/v1",
|
|
688
|
+
"--apikey", "key",
|
|
689
|
+
"--model", "test-model",
|
|
690
|
+
"--target-count", "3",
|
|
691
|
+
"--pageScope", "featured",
|
|
692
|
+
"--port=9222",
|
|
693
|
+
"--postAction", "favorite",
|
|
694
|
+
"--postActionConfirmed", "true"
|
|
695
|
+
]);
|
|
696
|
+
assert.equal(parsed.pageScope, "featured");
|
|
697
|
+
assert.equal(parsed.port, 9222);
|
|
698
|
+
assert.equal(parsed.targetCount, 3);
|
|
699
|
+
assert.equal(parsed.postAction, "favorite");
|
|
700
|
+
assert.equal(parsed.postActionConfirmed, true);
|
|
701
|
+
assert.equal(parsed.__provided.pageScope, true);
|
|
702
|
+
assert.equal(parsed.__provided.port, true);
|
|
703
|
+
}
|
|
704
|
+
|
|
543
705
|
async function main() {
|
|
544
706
|
testShouldAbortResumeProbeEarly();
|
|
545
707
|
await testSingleResumeCaptureFailureIsSkipped();
|
|
@@ -551,10 +713,18 @@ async function main() {
|
|
|
551
713
|
await testNetworkMissShouldFallbackToImageCapture();
|
|
552
714
|
await testFeaturedNetworkMissShouldSkipWithoutImageCapture();
|
|
553
715
|
await testFeaturedFavoriteShouldNotUseDomFallback();
|
|
716
|
+
await testFeaturedFavoriteShouldSkipClickWhenAlreadyInterested();
|
|
717
|
+
await testFeaturedFavoriteShouldRecognizeAlreadyFavoritedByDelThenAdd();
|
|
554
718
|
await testFeaturedFavoriteWithoutCalibrationShouldFail();
|
|
719
|
+
testFavoriteActionParserShouldSupportBodySignals();
|
|
720
|
+
testFavoriteActionParserShouldSupportFallbackRequestShape();
|
|
721
|
+
testFavoriteActionParserShouldSupportWebSocketPayload();
|
|
722
|
+
testFavoriteActionParserShouldOnlyTrustKnownRequestShapes();
|
|
723
|
+
await testFeaturedPostActionFailureShouldStillRecordPassedCandidate();
|
|
555
724
|
await testStitchWithSharpShouldComposeExpectedImage();
|
|
556
725
|
testStitchWithAvailablePythonShouldFallbackToPython();
|
|
557
726
|
testStitchWithAvailablePythonShouldFailWhenScriptMissing();
|
|
727
|
+
testParseArgsShouldSupportFeaturedAliasesAndInlinePort();
|
|
558
728
|
console.log("recoverable resume failure tests passed");
|
|
559
729
|
}
|
|
560
730
|
|