@reconcrap/boss-recommend-mcp 1.3.26 → 1.3.28
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
package/src/test-boss-chat.js
CHANGED
|
@@ -1015,6 +1015,374 @@ async function testBossChatLlmShouldApplyThinkingDefaultsAndOverrides() {
|
|
|
1015
1015
|
assert.deepEqual(responsesPayload.reasoning, { effort: "low" });
|
|
1016
1016
|
}
|
|
1017
1017
|
|
|
1018
|
+
async function testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime() {
|
|
1019
|
+
const calls = [];
|
|
1020
|
+
const page = {
|
|
1021
|
+
async ensureReady() {
|
|
1022
|
+
calls.push("ensureReady");
|
|
1023
|
+
return { hasListContainer: true, listItemCount: 1 };
|
|
1024
|
+
},
|
|
1025
|
+
async activatePrimaryChatLabel(label) {
|
|
1026
|
+
calls.push(`activatePrimaryChatLabel:${label}`);
|
|
1027
|
+
return { changed: false, verified: true, activeLabel: label };
|
|
1028
|
+
},
|
|
1029
|
+
async selectJob(jobSelection) {
|
|
1030
|
+
calls.push(`selectJob:${jobSelection.label}`);
|
|
1031
|
+
return jobSelection;
|
|
1032
|
+
},
|
|
1033
|
+
async activateUnreadFilter() {
|
|
1034
|
+
calls.push("activateUnreadFilter");
|
|
1035
|
+
return { changed: false, verified: true, activeLabel: "未读" };
|
|
1036
|
+
},
|
|
1037
|
+
async primeConversationByFirstCandidate() {
|
|
1038
|
+
calls.push("primeConversationByFirstCandidate:1");
|
|
1039
|
+
return {
|
|
1040
|
+
candidate: {
|
|
1041
|
+
customerId: "1001",
|
|
1042
|
+
name: "候选人A",
|
|
1043
|
+
sourceJob: "算法工程师",
|
|
1044
|
+
domIndex: 0,
|
|
1045
|
+
},
|
|
1046
|
+
totalVisibleCandidates: 1,
|
|
1047
|
+
readyState: {
|
|
1048
|
+
hasOnlineResume: true,
|
|
1049
|
+
hasAskResume: true,
|
|
1050
|
+
hasAttachmentResume: false,
|
|
1051
|
+
},
|
|
1052
|
+
};
|
|
1053
|
+
},
|
|
1054
|
+
async getLoadedCustomers() {
|
|
1055
|
+
calls.push("getLoadedCustomers:1");
|
|
1056
|
+
return [];
|
|
1057
|
+
},
|
|
1058
|
+
async closeResumeModalDomOnce() {
|
|
1059
|
+
return {
|
|
1060
|
+
closed: true,
|
|
1061
|
+
method: "already-closed",
|
|
1062
|
+
finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
|
|
1063
|
+
};
|
|
1064
|
+
},
|
|
1065
|
+
};
|
|
1066
|
+
const stateStore = {
|
|
1067
|
+
async load() {},
|
|
1068
|
+
hasAny() {
|
|
1069
|
+
return false;
|
|
1070
|
+
},
|
|
1071
|
+
async record() {},
|
|
1072
|
+
};
|
|
1073
|
+
const app = new BossChatApp({
|
|
1074
|
+
page,
|
|
1075
|
+
llmClient: {},
|
|
1076
|
+
interaction: {
|
|
1077
|
+
async sleepRange() {},
|
|
1078
|
+
async maybeRest() {},
|
|
1079
|
+
},
|
|
1080
|
+
resumeCaptureService: {},
|
|
1081
|
+
stateStore,
|
|
1082
|
+
reportStore: {
|
|
1083
|
+
async write() {
|
|
1084
|
+
return "report.json";
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
logger: { log() {} },
|
|
1088
|
+
dryRun: true,
|
|
1089
|
+
artifactRootDir: os.tmpdir(),
|
|
1090
|
+
resumeOpenCooldownMs: 0,
|
|
1091
|
+
});
|
|
1092
|
+
app.waitForCandidateList = async ({ reason } = {}) => {
|
|
1093
|
+
calls.push(`waitForCandidateList:${reason || "unknown"}`);
|
|
1094
|
+
return {
|
|
1095
|
+
ready: true,
|
|
1096
|
+
waitedMs: 0,
|
|
1097
|
+
attempts: 1,
|
|
1098
|
+
listItemCount: 1,
|
|
1099
|
+
lastError: "",
|
|
1100
|
+
};
|
|
1101
|
+
};
|
|
1102
|
+
app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
|
|
1103
|
+
calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
|
|
1104
|
+
return {
|
|
1105
|
+
name: "候选人A",
|
|
1106
|
+
passed: false,
|
|
1107
|
+
requested: false,
|
|
1108
|
+
reason: "skip",
|
|
1109
|
+
error: "",
|
|
1110
|
+
artifacts: {},
|
|
1111
|
+
};
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
const summary = await app.run({
|
|
1115
|
+
screeningCriteria: "有 AI 项目经验",
|
|
1116
|
+
targetCount: 1,
|
|
1117
|
+
startFrom: "unread",
|
|
1118
|
+
jobSelection: { label: "算法工程师", value: "job-1" },
|
|
1119
|
+
chrome: { port: 9222 },
|
|
1120
|
+
llm: { model: "gpt-test" },
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
assert.deepEqual(calls.slice(0, 4), [
|
|
1124
|
+
"ensureReady",
|
|
1125
|
+
"activatePrimaryChatLabel:全部",
|
|
1126
|
+
"selectJob:算法工程师",
|
|
1127
|
+
"activateUnreadFilter",
|
|
1128
|
+
]);
|
|
1129
|
+
assert.equal(calls.includes("primeConversationByFirstCandidate:1"), true);
|
|
1130
|
+
assert.equal(calls.includes("processCustomer:skip"), true);
|
|
1131
|
+
assert.equal(summary.inspected, 1);
|
|
1132
|
+
assert.equal(summary.skipped, 1);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async function testBossChatAppShouldRestoreListContextAfterRecovery() {
|
|
1136
|
+
const calls = [];
|
|
1137
|
+
let primeCount = 0;
|
|
1138
|
+
let loadedCount = 0;
|
|
1139
|
+
const page = {
|
|
1140
|
+
async ensureReady() {
|
|
1141
|
+
return { hasListContainer: true, listItemCount: 1 };
|
|
1142
|
+
},
|
|
1143
|
+
async activatePrimaryChatLabel(label) {
|
|
1144
|
+
calls.push(`activatePrimaryChatLabel:${label}`);
|
|
1145
|
+
return { changed: false, verified: true, activeLabel: label };
|
|
1146
|
+
},
|
|
1147
|
+
async selectJob(jobSelection) {
|
|
1148
|
+
calls.push(`selectJob:${jobSelection.label}`);
|
|
1149
|
+
return jobSelection;
|
|
1150
|
+
},
|
|
1151
|
+
async activateUnreadFilter() {
|
|
1152
|
+
calls.push("activateUnreadFilter");
|
|
1153
|
+
return { changed: false, verified: true, activeLabel: "未读" };
|
|
1154
|
+
},
|
|
1155
|
+
async primeConversationByFirstCandidate() {
|
|
1156
|
+
primeCount += 1;
|
|
1157
|
+
calls.push(`primeConversationByFirstCandidate:${primeCount}`);
|
|
1158
|
+
if (primeCount === 1) {
|
|
1159
|
+
throw new Error("NO_FIRST_CANDIDATE");
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
candidate: {
|
|
1163
|
+
customerId: "1002",
|
|
1164
|
+
name: "候选人B",
|
|
1165
|
+
sourceJob: "算法工程师",
|
|
1166
|
+
domIndex: 0,
|
|
1167
|
+
},
|
|
1168
|
+
totalVisibleCandidates: 1,
|
|
1169
|
+
readyState: {
|
|
1170
|
+
hasOnlineResume: true,
|
|
1171
|
+
hasAskResume: true,
|
|
1172
|
+
hasAttachmentResume: false,
|
|
1173
|
+
},
|
|
1174
|
+
};
|
|
1175
|
+
},
|
|
1176
|
+
async getLoadedCustomers() {
|
|
1177
|
+
loadedCount += 1;
|
|
1178
|
+
calls.push(`getLoadedCustomers:${loadedCount}`);
|
|
1179
|
+
if (loadedCount === 1) {
|
|
1180
|
+
throw new Error("CHAT_CARD_LIST_NOT_FOUND");
|
|
1181
|
+
}
|
|
1182
|
+
return [];
|
|
1183
|
+
},
|
|
1184
|
+
async recoverToChatIndex() {
|
|
1185
|
+
calls.push("recoverToChatIndex");
|
|
1186
|
+
return { changed: true, href: "https://www.zhipin.com/web/chat/index" };
|
|
1187
|
+
},
|
|
1188
|
+
async closeResumeModalDomOnce() {
|
|
1189
|
+
return {
|
|
1190
|
+
closed: true,
|
|
1191
|
+
method: "already-closed",
|
|
1192
|
+
finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
|
|
1193
|
+
};
|
|
1194
|
+
},
|
|
1195
|
+
};
|
|
1196
|
+
const stateStore = {
|
|
1197
|
+
async load() {},
|
|
1198
|
+
hasAny() {
|
|
1199
|
+
return false;
|
|
1200
|
+
},
|
|
1201
|
+
async record() {},
|
|
1202
|
+
};
|
|
1203
|
+
const app = new BossChatApp({
|
|
1204
|
+
page,
|
|
1205
|
+
llmClient: {},
|
|
1206
|
+
interaction: {
|
|
1207
|
+
async sleepRange() {},
|
|
1208
|
+
async maybeRest() {},
|
|
1209
|
+
},
|
|
1210
|
+
resumeCaptureService: {},
|
|
1211
|
+
stateStore,
|
|
1212
|
+
reportStore: {
|
|
1213
|
+
async write() {
|
|
1214
|
+
return "report.json";
|
|
1215
|
+
},
|
|
1216
|
+
},
|
|
1217
|
+
logger: { log() {} },
|
|
1218
|
+
dryRun: true,
|
|
1219
|
+
artifactRootDir: os.tmpdir(),
|
|
1220
|
+
resumeOpenCooldownMs: 0,
|
|
1221
|
+
});
|
|
1222
|
+
app.waitForCandidateList = async ({ reason } = {}) => {
|
|
1223
|
+
calls.push(`waitForCandidateList:${reason || "unknown"}`);
|
|
1224
|
+
return {
|
|
1225
|
+
ready:
|
|
1226
|
+
reason === "initial-context-restore" ||
|
|
1227
|
+
reason === "post-recovery-context-restore",
|
|
1228
|
+
waitedMs: 0,
|
|
1229
|
+
attempts: 1,
|
|
1230
|
+
listItemCount:
|
|
1231
|
+
reason === "initial-context-restore" ||
|
|
1232
|
+
reason === "post-recovery-context-restore"
|
|
1233
|
+
? 1
|
|
1234
|
+
: 0,
|
|
1235
|
+
lastError: "",
|
|
1236
|
+
};
|
|
1237
|
+
};
|
|
1238
|
+
app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
|
|
1239
|
+
calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
|
|
1240
|
+
return {
|
|
1241
|
+
name: "候选人B",
|
|
1242
|
+
passed: false,
|
|
1243
|
+
requested: false,
|
|
1244
|
+
reason: "skip",
|
|
1245
|
+
error: "",
|
|
1246
|
+
artifacts: {},
|
|
1247
|
+
};
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
const summary = await app.run({
|
|
1251
|
+
screeningCriteria: "有 AI 项目经验",
|
|
1252
|
+
targetCount: 1,
|
|
1253
|
+
startFrom: "unread",
|
|
1254
|
+
jobSelection: { label: "算法工程师", value: "job-1" },
|
|
1255
|
+
chrome: { port: 9222 },
|
|
1256
|
+
llm: { model: "gpt-test" },
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
assert.equal(calls.filter((item) => item === "activatePrimaryChatLabel:全部").length, 2);
|
|
1260
|
+
const recoverIndex = calls.indexOf("recoverToChatIndex");
|
|
1261
|
+
assert.equal(recoverIndex >= 0, true);
|
|
1262
|
+
assert.equal(calls[recoverIndex + 1], "activatePrimaryChatLabel:全部");
|
|
1263
|
+
assert.equal(calls[recoverIndex + 2], "selectJob:算法工程师");
|
|
1264
|
+
assert.equal(calls[recoverIndex + 3], "activateUnreadFilter");
|
|
1265
|
+
assert.equal(calls[recoverIndex + 4], "waitForCandidateList:post-recovery-context-restore");
|
|
1266
|
+
assert.equal(calls[recoverIndex + 5], "primeConversationByFirstCandidate:2");
|
|
1267
|
+
assert.equal(calls.includes("processCustomer:skip"), true);
|
|
1268
|
+
assert.equal(summary.inspected, 1);
|
|
1269
|
+
assert.equal(summary.skipped, 1);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
async function testBossChatAppShouldWaitForCandidateListBeforePriming() {
|
|
1273
|
+
const calls = [];
|
|
1274
|
+
let pageStateCall = 0;
|
|
1275
|
+
const page = {
|
|
1276
|
+
async ensureReady() {
|
|
1277
|
+
calls.push("ensureReady");
|
|
1278
|
+
return { hasListContainer: false, listItemCount: 0 };
|
|
1279
|
+
},
|
|
1280
|
+
async activatePrimaryChatLabel(label) {
|
|
1281
|
+
calls.push(`activatePrimaryChatLabel:${label}`);
|
|
1282
|
+
return { changed: false, verified: true, activeLabel: label };
|
|
1283
|
+
},
|
|
1284
|
+
async selectJob(jobSelection) {
|
|
1285
|
+
calls.push(`selectJob:${jobSelection.label}`);
|
|
1286
|
+
return jobSelection;
|
|
1287
|
+
},
|
|
1288
|
+
async activateUnreadFilter() {
|
|
1289
|
+
calls.push("activateUnreadFilter");
|
|
1290
|
+
return { changed: true, verified: true, activeLabel: "未读" };
|
|
1291
|
+
},
|
|
1292
|
+
async getPageState() {
|
|
1293
|
+
pageStateCall += 1;
|
|
1294
|
+
calls.push(`getPageState:${pageStateCall}`);
|
|
1295
|
+
return {
|
|
1296
|
+
href: "https://www.zhipin.com/web/chat/index",
|
|
1297
|
+
readyState: "complete",
|
|
1298
|
+
hasListContainer: pageStateCall >= 3,
|
|
1299
|
+
listItemCount: pageStateCall >= 3 ? 2 : 0,
|
|
1300
|
+
};
|
|
1301
|
+
},
|
|
1302
|
+
async primeConversationByFirstCandidate() {
|
|
1303
|
+
calls.push("primeConversationByFirstCandidate:1");
|
|
1304
|
+
return {
|
|
1305
|
+
candidate: {
|
|
1306
|
+
customerId: "1003",
|
|
1307
|
+
name: "候选人C",
|
|
1308
|
+
sourceJob: "算法工程师",
|
|
1309
|
+
domIndex: 0,
|
|
1310
|
+
},
|
|
1311
|
+
totalVisibleCandidates: 2,
|
|
1312
|
+
readyState: {
|
|
1313
|
+
hasOnlineResume: true,
|
|
1314
|
+
hasAskResume: true,
|
|
1315
|
+
hasAttachmentResume: false,
|
|
1316
|
+
},
|
|
1317
|
+
};
|
|
1318
|
+
},
|
|
1319
|
+
async getLoadedCustomers() {
|
|
1320
|
+
calls.push("getLoadedCustomers:1");
|
|
1321
|
+
return [];
|
|
1322
|
+
},
|
|
1323
|
+
async closeResumeModalDomOnce() {
|
|
1324
|
+
return {
|
|
1325
|
+
closed: true,
|
|
1326
|
+
method: "already-closed",
|
|
1327
|
+
finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
|
|
1328
|
+
};
|
|
1329
|
+
},
|
|
1330
|
+
};
|
|
1331
|
+
const stateStore = {
|
|
1332
|
+
async load() {},
|
|
1333
|
+
hasAny() {
|
|
1334
|
+
return false;
|
|
1335
|
+
},
|
|
1336
|
+
async record() {},
|
|
1337
|
+
};
|
|
1338
|
+
const app = new BossChatApp({
|
|
1339
|
+
page,
|
|
1340
|
+
llmClient: {},
|
|
1341
|
+
interaction: {
|
|
1342
|
+
async sleepRange() {},
|
|
1343
|
+
async maybeRest() {},
|
|
1344
|
+
},
|
|
1345
|
+
resumeCaptureService: {},
|
|
1346
|
+
stateStore,
|
|
1347
|
+
reportStore: {
|
|
1348
|
+
async write() {
|
|
1349
|
+
return "report.json";
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
logger: { log() {} },
|
|
1353
|
+
dryRun: true,
|
|
1354
|
+
artifactRootDir: os.tmpdir(),
|
|
1355
|
+
resumeOpenCooldownMs: 0,
|
|
1356
|
+
});
|
|
1357
|
+
app.processCustomer = async (_customer, _profile, _runId, options = {}) => {
|
|
1358
|
+
calls.push(`processCustomer:${options.skipCardClick === true ? "skip" : "click"}`);
|
|
1359
|
+
return {
|
|
1360
|
+
name: "候选人C",
|
|
1361
|
+
passed: false,
|
|
1362
|
+
requested: false,
|
|
1363
|
+
reason: "skip",
|
|
1364
|
+
error: "",
|
|
1365
|
+
artifacts: {},
|
|
1366
|
+
};
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
const summary = await app.run({
|
|
1370
|
+
screeningCriteria: "有 AI 项目经验",
|
|
1371
|
+
targetCount: 1,
|
|
1372
|
+
startFrom: "unread",
|
|
1373
|
+
jobSelection: { label: "算法工程师", value: "job-1" },
|
|
1374
|
+
chrome: { port: 9222 },
|
|
1375
|
+
llm: { model: "gpt-test" },
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
const primeIndex = calls.indexOf("primeConversationByFirstCandidate:1");
|
|
1379
|
+
const thirdStateIndex = calls.indexOf("getPageState:3");
|
|
1380
|
+
assert.equal(thirdStateIndex >= 0, true);
|
|
1381
|
+
assert.equal(primeIndex > thirdStateIndex, true);
|
|
1382
|
+
assert.equal(summary.inspected, 1);
|
|
1383
|
+
assert.equal(summary.skipped, 1);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1018
1386
|
async function testBossChatAppShouldPersistEvidenceArtifacts() {
|
|
1019
1387
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-artifacts-"));
|
|
1020
1388
|
await mkdir(tempDir, { recursive: true });
|
|
@@ -1150,6 +1518,9 @@ async function main() {
|
|
|
1150
1518
|
testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence();
|
|
1151
1519
|
await testBossChatLlmTextChunkFallbackShouldWork();
|
|
1152
1520
|
await testBossChatLlmShouldApplyThinkingDefaultsAndOverrides();
|
|
1521
|
+
await testBossChatAppShouldResetPrimaryChatLabelBeforeInitialPrime();
|
|
1522
|
+
await testBossChatAppShouldRestoreListContextAfterRecovery();
|
|
1523
|
+
await testBossChatAppShouldWaitForCandidateListBeforePriming();
|
|
1153
1524
|
await testBossChatAppShouldPersistEvidenceArtifacts();
|
|
1154
1525
|
console.log("boss-chat tests passed");
|
|
1155
1526
|
}
|
|
@@ -64,6 +64,9 @@ function hasResumeRequestSentMessage(state = {}) {
|
|
|
64
64
|
return recent.some((item) => normalizeText(item).includes('简历请求已发送'));
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
const CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS = 5000;
|
|
68
|
+
const CANDIDATE_LIST_WAIT_POLL_MS = 500;
|
|
69
|
+
|
|
67
70
|
export class BossChatApp {
|
|
68
71
|
constructor({
|
|
69
72
|
page,
|
|
@@ -142,6 +145,75 @@ export class BossChatApp {
|
|
|
142
145
|
} catch {}
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
async restoreListContext(profile) {
|
|
149
|
+
if (typeof this.page.activatePrimaryChatLabel === 'function') {
|
|
150
|
+
await this.page.activatePrimaryChatLabel('全部');
|
|
151
|
+
}
|
|
152
|
+
await this.page.selectJob(profile.jobSelection);
|
|
153
|
+
return profile.startFrom === 'all'
|
|
154
|
+
? this.page.activateAllFilter()
|
|
155
|
+
: this.page.activateUnreadFilter();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async waitForCandidateList({
|
|
159
|
+
reason = 'unknown',
|
|
160
|
+
maxWaitMs = CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS,
|
|
161
|
+
pollMs = CANDIDATE_LIST_WAIT_POLL_MS,
|
|
162
|
+
} = {}) {
|
|
163
|
+
const startedAt = Date.now();
|
|
164
|
+
let attempts = 0;
|
|
165
|
+
let lastState = null;
|
|
166
|
+
let lastError = '';
|
|
167
|
+
|
|
168
|
+
while (Date.now() - startedAt <= maxWaitMs) {
|
|
169
|
+
attempts += 1;
|
|
170
|
+
try {
|
|
171
|
+
if (typeof this.page.getPageState === 'function') {
|
|
172
|
+
lastState = await this.page.getPageState();
|
|
173
|
+
if (Number(lastState?.listItemCount || 0) > 0) {
|
|
174
|
+
return {
|
|
175
|
+
ready: true,
|
|
176
|
+
waitedMs: Date.now() - startedAt,
|
|
177
|
+
attempts,
|
|
178
|
+
listItemCount: Number(lastState?.listItemCount || 0),
|
|
179
|
+
lastState,
|
|
180
|
+
lastError,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
} else if (typeof this.page.getLoadedCustomers === 'function') {
|
|
184
|
+
const customers = await this.page.getLoadedCustomers();
|
|
185
|
+
if (Array.isArray(customers) && customers.length > 0) {
|
|
186
|
+
return {
|
|
187
|
+
ready: true,
|
|
188
|
+
waitedMs: Date.now() - startedAt,
|
|
189
|
+
attempts,
|
|
190
|
+
listItemCount: customers.length,
|
|
191
|
+
lastState,
|
|
192
|
+
lastError,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
lastError = String(error?.message || error || '');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (Date.now() - startedAt >= maxWaitMs) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
ready: false,
|
|
208
|
+
waitedMs: Date.now() - startedAt,
|
|
209
|
+
attempts,
|
|
210
|
+
listItemCount: Number(lastState?.listItemCount || 0),
|
|
211
|
+
lastState,
|
|
212
|
+
lastError,
|
|
213
|
+
reason,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
145
217
|
async run(profile) {
|
|
146
218
|
const startedAt = new Date().toISOString();
|
|
147
219
|
const runId = runToken(new Date());
|
|
@@ -158,13 +230,34 @@ export class BossChatApp {
|
|
|
158
230
|
} catch (error) {
|
|
159
231
|
this.logger.log(`页面就绪检查告警:${error?.message || error},将继续执行预热恢复流程。`);
|
|
160
232
|
}
|
|
161
|
-
await this.
|
|
162
|
-
|
|
163
|
-
const filterResult =
|
|
164
|
-
startFrom === 'all'
|
|
165
|
-
? await this.page.activateAllFilter()
|
|
166
|
-
: await this.page.activateUnreadFilter();
|
|
233
|
+
let filterResult = await this.restoreListContext(profile);
|
|
167
234
|
await this.interaction.sleepRange(420, 160);
|
|
235
|
+
let initialListWait = await this.waitForCandidateList({
|
|
236
|
+
reason: 'initial-context-restore',
|
|
237
|
+
});
|
|
238
|
+
if (initialListWait.ready) {
|
|
239
|
+
this.logger.log(
|
|
240
|
+
`候选人列表已就绪:reason=initial-context-restore | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount}`,
|
|
241
|
+
);
|
|
242
|
+
} else {
|
|
243
|
+
this.logger.log(
|
|
244
|
+
`候选人列表等待超时:reason=initial-context-restore | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount} | lastError=${initialListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
245
|
+
);
|
|
246
|
+
filterResult = await this.restoreListContext(profile);
|
|
247
|
+
await this.interaction.sleepRange(420, 160);
|
|
248
|
+
initialListWait = await this.waitForCandidateList({
|
|
249
|
+
reason: 'initial-context-restore-reapply',
|
|
250
|
+
});
|
|
251
|
+
if (initialListWait.ready) {
|
|
252
|
+
this.logger.log(
|
|
253
|
+
`候选人列表二次恢复成功:reason=initial-context-restore-reapply | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount}`,
|
|
254
|
+
);
|
|
255
|
+
} else {
|
|
256
|
+
this.logger.log(
|
|
257
|
+
`候选人列表二次等待仍超时:reason=initial-context-restore-reapply | waited=${initialListWait.waitedMs}ms | attempts=${initialListWait.attempts} | count=${initialListWait.listItemCount} | lastError=${initialListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
168
261
|
this.logger.log('预热步骤:准备点击首位人选初始化聊天容器...');
|
|
169
262
|
let primedCustomer = null;
|
|
170
263
|
|
|
@@ -264,12 +357,66 @@ export class BossChatApp {
|
|
|
264
357
|
message,
|
|
265
358
|
)
|
|
266
359
|
) {
|
|
360
|
+
const delayedListWait = await this.waitForCandidateList({
|
|
361
|
+
reason: `main-loop:${message}`,
|
|
362
|
+
});
|
|
363
|
+
if (delayedListWait.ready) {
|
|
364
|
+
this.logger.log(
|
|
365
|
+
`候选人列表延迟恢复成功:reason=main-loop:${message} | waited=${delayedListWait.waitedMs}ms | attempts=${delayedListWait.attempts} | count=${delayedListWait.listItemCount},继续重试扫描。`,
|
|
366
|
+
);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
267
369
|
try {
|
|
268
370
|
const recover = await this.page.recoverToChatIndex();
|
|
269
371
|
this.logger.log(
|
|
270
372
|
`页面恢复:changed=${recover.changed} | href=${recover.href || 'unknown'},准备重新预热并继续。`,
|
|
271
373
|
);
|
|
272
374
|
await this.interaction.sleepRange(900, 220);
|
|
375
|
+
let recoveredFilterResult = await this.restoreListContext(profile);
|
|
376
|
+
this.logger.log(
|
|
377
|
+
`恢复后列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
|
|
378
|
+
recoveredFilterResult.changed
|
|
379
|
+
? recoveredFilterResult.verified === false
|
|
380
|
+
? '(已尝试切换,未验证 active)'
|
|
381
|
+
: '(已切换)'
|
|
382
|
+
: '(已在目标筛选)'
|
|
383
|
+
}${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
|
|
384
|
+
);
|
|
385
|
+
let recoveredListWait = await this.waitForCandidateList({
|
|
386
|
+
reason: 'post-recovery-context-restore',
|
|
387
|
+
});
|
|
388
|
+
if (recoveredListWait.ready) {
|
|
389
|
+
this.logger.log(
|
|
390
|
+
`恢复后候选人列表已就绪:reason=post-recovery-context-restore | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount}`,
|
|
391
|
+
);
|
|
392
|
+
} else {
|
|
393
|
+
this.logger.log(
|
|
394
|
+
`恢复后候选人列表等待超时:reason=post-recovery-context-restore | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount} | lastError=${recoveredListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
395
|
+
);
|
|
396
|
+
recoveredFilterResult = await this.restoreListContext(profile);
|
|
397
|
+
this.logger.log(
|
|
398
|
+
`恢复后二次应用列表上下文:岗位=${profile.jobSelection?.label || profile.jobSelection?.value || '未知'};列表范围: ${filterLabel}${
|
|
399
|
+
recoveredFilterResult.changed
|
|
400
|
+
? recoveredFilterResult.verified === false
|
|
401
|
+
? '(已尝试切换,未验证 active)'
|
|
402
|
+
: '(已切换)'
|
|
403
|
+
: '(已在目标筛选)'
|
|
404
|
+
}${recoveredFilterResult?.activeLabel ? ` | active=${recoveredFilterResult.activeLabel}` : ''}`,
|
|
405
|
+
);
|
|
406
|
+
await this.interaction.sleepRange(420, 160);
|
|
407
|
+
recoveredListWait = await this.waitForCandidateList({
|
|
408
|
+
reason: 'post-recovery-context-restore-reapply',
|
|
409
|
+
});
|
|
410
|
+
if (recoveredListWait.ready) {
|
|
411
|
+
this.logger.log(
|
|
412
|
+
`恢复后二次候选人列表恢复成功:reason=post-recovery-context-restore-reapply | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount}`,
|
|
413
|
+
);
|
|
414
|
+
} else {
|
|
415
|
+
this.logger.log(
|
|
416
|
+
`恢复后二次候选人列表等待仍超时:reason=post-recovery-context-restore-reapply | waited=${recoveredListWait.waitedMs}ms | attempts=${recoveredListWait.attempts} | count=${recoveredListWait.listItemCount} | lastError=${recoveredListWait.lastError || 'n/a'},继续尝试预热。`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
273
420
|
const prime = await this.page.primeConversationByFirstCandidate();
|
|
274
421
|
const candidate = prime?.candidate || {};
|
|
275
422
|
const candidateBase = {
|
|
@@ -276,6 +276,63 @@ async function browserActivateFilterTab(label) {
|
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
async function browserActivatePrimaryChatLabel(label) {
|
|
280
|
+
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
281
|
+
const isVisible = (node) => {
|
|
282
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
283
|
+
const rect = node.getBoundingClientRect();
|
|
284
|
+
if (rect.width <= 2 || rect.height <= 2) return false;
|
|
285
|
+
const style = getComputedStyle(node);
|
|
286
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0.01;
|
|
287
|
+
};
|
|
288
|
+
const matchesLabel = (node) => {
|
|
289
|
+
const text = normalize(node?.textContent || '');
|
|
290
|
+
return text === label || text.startsWith(`${label}(`);
|
|
291
|
+
};
|
|
292
|
+
const hasActiveState = (node) =>
|
|
293
|
+
node instanceof HTMLElement &&
|
|
294
|
+
(
|
|
295
|
+
node.classList.contains('active') ||
|
|
296
|
+
node.classList.contains('selected') ||
|
|
297
|
+
node.getAttribute('aria-selected') === 'true' ||
|
|
298
|
+
node.getAttribute('data-active') === 'true'
|
|
299
|
+
);
|
|
300
|
+
const getLabels = () =>
|
|
301
|
+
Array.from(document.querySelectorAll('.label-list .chat-label-item, .chat-label-item'))
|
|
302
|
+
.filter((node) => node instanceof HTMLElement && isVisible(node));
|
|
303
|
+
const getActiveLabel = () => {
|
|
304
|
+
const activeNode = getLabels().find((node) => hasActiveState(node));
|
|
305
|
+
return normalize(activeNode?.textContent || '');
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const labels = getLabels();
|
|
309
|
+
const candidates = labels.filter((node) => matchesLabel(node));
|
|
310
|
+
if (candidates.length === 0) {
|
|
311
|
+
return { ok: false, error: `PRIMARY_CHAT_LABEL_NOT_FOUND:${label}` };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const activeLabelBefore = getActiveLabel();
|
|
315
|
+
if (candidates.some((node) => hasActiveState(node)) || matchesLabel({ textContent: activeLabelBefore })) {
|
|
316
|
+
return { ok: true, changed: false, verified: true, activeLabel: activeLabelBefore || label };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
candidates[0].click();
|
|
320
|
+
await new Promise((resolve) => window.setTimeout(resolve, 420));
|
|
321
|
+
|
|
322
|
+
const refreshedCandidates = getLabels().filter((node) => matchesLabel(node));
|
|
323
|
+
const activeLabelAfter = getActiveLabel();
|
|
324
|
+
const verified =
|
|
325
|
+
matchesLabel({ textContent: activeLabelAfter }) ||
|
|
326
|
+
refreshedCandidates.some((node) => hasActiveState(node));
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
ok: true,
|
|
330
|
+
changed: true,
|
|
331
|
+
verified,
|
|
332
|
+
activeLabel: activeLabelAfter || '',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
279
336
|
function browserGetLoadedCustomers() {
|
|
280
337
|
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
281
338
|
const isScrollable = (el) =>
|
|
@@ -2317,6 +2374,14 @@ export class BossChatPage {
|
|
|
2317
2374
|
return result;
|
|
2318
2375
|
}
|
|
2319
2376
|
|
|
2377
|
+
async activatePrimaryChatLabel(label = '全部') {
|
|
2378
|
+
const result = await this.chromeClient.callFunction(browserActivatePrimaryChatLabel, label);
|
|
2379
|
+
if (!result?.ok) {
|
|
2380
|
+
throw new Error(result?.error || `ACTIVATE_PRIMARY_CHAT_LABEL_FAILED:${label}`);
|
|
2381
|
+
}
|
|
2382
|
+
return result;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2320
2385
|
async getLoadedCustomers() {
|
|
2321
2386
|
let lastError = 'GET_LOADED_CUSTOMERS_FAILED';
|
|
2322
2387
|
for (let attempt = 0; attempt < 6; attempt += 1) {
|