@reconcrap/boss-recommend-mcp 1.2.1 → 1.2.2
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
|
@@ -13,11 +13,52 @@ const RESUME_CAPTURE_WAIT_MS = 60000;
|
|
|
13
13
|
const RESUME_CAPTURE_MAX_ATTEMPTS = 4;
|
|
14
14
|
const RESUME_CAPTURE_RETRY_DELAY_MS = 1200;
|
|
15
15
|
const MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES = 10;
|
|
16
|
+
const DEFAULT_VISION_MAX_IMAGE_PIXELS = 36000000;
|
|
17
|
+
const DEFAULT_VISION_RETRY_MAX_IMAGE_PIXELS = 30000000;
|
|
18
|
+
let visionSharpFactory = null;
|
|
16
19
|
const PAGE_SCOPE_TAB_STATUS = {
|
|
17
20
|
recommend: "0",
|
|
18
21
|
latest: "1",
|
|
19
22
|
featured: "3"
|
|
20
23
|
};
|
|
24
|
+
const BOTTOM_HINT_KEYWORDS = ["没有更多", "已显示全部", "已经到底", "暂无更多", "推荐完了", "没有更多人选"];
|
|
25
|
+
const LOAD_MORE_HINT_KEYWORDS = ["滚动加载更多", "下滑加载更多", "继续下滑", "继续滑动", "滑动加载", "正在加载", "加载中"];
|
|
26
|
+
|
|
27
|
+
function classifyFinishedWrapState(finishedWrapText, refreshButtonVisible = false) {
|
|
28
|
+
const normalizedText = normalizeText(finishedWrapText);
|
|
29
|
+
const matchedBottomKeyword = BOTTOM_HINT_KEYWORDS.find((keyword) => normalizedText.includes(keyword)) || null;
|
|
30
|
+
if (matchedBottomKeyword) {
|
|
31
|
+
return {
|
|
32
|
+
isBottom: true,
|
|
33
|
+
reason: matchedBottomKeyword,
|
|
34
|
+
matched_bottom_keyword: matchedBottomKeyword,
|
|
35
|
+
matched_load_more_keyword: null
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const matchedLoadMoreKeyword = LOAD_MORE_HINT_KEYWORDS.find((keyword) => normalizedText.includes(keyword)) || null;
|
|
39
|
+
if (matchedLoadMoreKeyword) {
|
|
40
|
+
return {
|
|
41
|
+
isBottom: false,
|
|
42
|
+
reason: null,
|
|
43
|
+
matched_bottom_keyword: null,
|
|
44
|
+
matched_load_more_keyword: matchedLoadMoreKeyword
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (refreshButtonVisible) {
|
|
48
|
+
return {
|
|
49
|
+
isBottom: true,
|
|
50
|
+
reason: "refresh_button_visible",
|
|
51
|
+
matched_bottom_keyword: null,
|
|
52
|
+
matched_load_more_keyword: null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
isBottom: false,
|
|
57
|
+
reason: null,
|
|
58
|
+
matched_bottom_keyword: null,
|
|
59
|
+
matched_load_more_keyword: null
|
|
60
|
+
};
|
|
61
|
+
}
|
|
21
62
|
|
|
22
63
|
function getCodexHome() {
|
|
23
64
|
return process.env.CODEX_HOME
|
|
@@ -65,6 +106,35 @@ function parsePositiveInteger(raw) {
|
|
|
65
106
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
66
107
|
}
|
|
67
108
|
|
|
109
|
+
function resolveVisionPixelLimitFromEnv(envName, fallback) {
|
|
110
|
+
const parsed = parsePositiveInteger(process.env[envName]);
|
|
111
|
+
return parsed || fallback;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveVisionRetryPixelLimit(primaryLimit) {
|
|
115
|
+
const safePrimary = parsePositiveInteger(primaryLimit) || DEFAULT_VISION_MAX_IMAGE_PIXELS;
|
|
116
|
+
const fallback = Math.max(1, Math.floor(safePrimary * 0.8));
|
|
117
|
+
const parsed = resolveVisionPixelLimitFromEnv("BOSS_RECOMMEND_VISION_RETRY_MAX_IMAGE_PIXELS", DEFAULT_VISION_RETRY_MAX_IMAGE_PIXELS);
|
|
118
|
+
const candidate = parsePositiveInteger(parsed) || fallback;
|
|
119
|
+
return Math.min(Math.max(1, candidate), Math.max(1, safePrimary - 1));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function loadVisionSharp() {
|
|
123
|
+
if (!visionSharpFactory) {
|
|
124
|
+
visionSharpFactory = require("sharp");
|
|
125
|
+
}
|
|
126
|
+
return visionSharpFactory;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isVisionImageSizeLimitMessage(message) {
|
|
130
|
+
const text = normalizeText(message).toLowerCase();
|
|
131
|
+
if (!text) return false;
|
|
132
|
+
return (
|
|
133
|
+
/(像素|pixel|pixels|too large|image size|image dimension|too many pixels|max(?:imum)?[^a-z0-9]{0,8}(?:pixel|image)|超过|超出|上限)/i.test(text)
|
|
134
|
+
|| (text.includes("image") && text.includes("limit"))
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
68
138
|
function normalizePostAction(value) {
|
|
69
139
|
const normalized = normalizeText(value).toLowerCase();
|
|
70
140
|
if (!normalized) return null;
|
|
@@ -1041,7 +1111,8 @@ const jsDetectBottom = `(() => {
|
|
|
1041
1111
|
const finishedWrap = Array.from(doc.querySelectorAll('.finished-wrap')).find((el) => isVisible(el)) || null;
|
|
1042
1112
|
const refreshButton = Array.from(doc.querySelectorAll('.finished-wrap .btn.btn-refresh, .finished-wrap .btn-refresh, .no-data-refresh .btn-refresh'))
|
|
1043
1113
|
.find((el) => isVisible(el)) || null;
|
|
1044
|
-
const keywords =
|
|
1114
|
+
const keywords = ${JSON.stringify(BOTTOM_HINT_KEYWORDS)};
|
|
1115
|
+
const loadMoreKeywords = ${JSON.stringify(LOAD_MORE_HINT_KEYWORDS)};
|
|
1045
1116
|
const elements = Array.from(doc.querySelectorAll('div,span,p'));
|
|
1046
1117
|
for (const el of elements) {
|
|
1047
1118
|
if (el.offsetParent === null) continue;
|
|
@@ -1059,12 +1130,21 @@ const jsDetectBottom = `(() => {
|
|
|
1059
1130
|
}
|
|
1060
1131
|
}
|
|
1061
1132
|
}
|
|
1133
|
+
const finishedWrapText = finishedWrap ? String(finishedWrap.textContent || '').replace(/\s+/g, ' ').trim() : '';
|
|
1134
|
+
const matchedBottomKeyword = keywords.find((keyword) => finishedWrapText.includes(keyword)) || null;
|
|
1135
|
+
const matchedLoadMoreKeyword = loadMoreKeywords.find((keyword) => finishedWrapText.includes(keyword)) || null;
|
|
1136
|
+
const inferredBottom = matchedBottomKeyword
|
|
1137
|
+
? true
|
|
1138
|
+
: (Boolean(refreshButton) && !matchedLoadMoreKeyword);
|
|
1062
1139
|
return {
|
|
1063
|
-
isBottom:
|
|
1064
|
-
reason:
|
|
1140
|
+
isBottom: inferredBottom,
|
|
1141
|
+
reason: matchedBottomKeyword || (inferredBottom ? 'refresh_button_visible' : null),
|
|
1065
1142
|
finished_wrap_visible: Boolean(finishedWrap),
|
|
1143
|
+
finished_wrap_text: finishedWrapText || null,
|
|
1066
1144
|
refresh_button_visible: Boolean(refreshButton),
|
|
1067
|
-
refresh_button_text: refreshButton ? String(refreshButton.textContent || '').replace(/\s+/g, ' ').trim() : null
|
|
1145
|
+
refresh_button_text: refreshButton ? String(refreshButton.textContent || '').replace(/\s+/g, ' ').trim() : null,
|
|
1146
|
+
matched_bottom_keyword: matchedBottomKeyword,
|
|
1147
|
+
matched_load_more_keyword: matchedLoadMoreKeyword
|
|
1068
1148
|
};
|
|
1069
1149
|
})()`;
|
|
1070
1150
|
const jsWaitForDetail = `(() => {
|
|
@@ -2615,15 +2695,21 @@ class RecommendScreenCli {
|
|
|
2615
2695
|
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
2616
2696
|
}
|
|
2617
2697
|
const doc = frame.contentDocument;
|
|
2618
|
-
const
|
|
2698
|
+
const recommendInner = Array.from(doc.querySelectorAll('.card-inner[data-geekid]'))
|
|
2619
2699
|
.find((item) => (item.getAttribute('data-geekid') || '') === String(candidateKey)) || null;
|
|
2620
|
-
const
|
|
2700
|
+
const latestInner = recommendInner
|
|
2701
|
+
? null
|
|
2702
|
+
: Array.from(doc.querySelectorAll('.candidate-card-wrap .card-inner[data-geek], .candidate-card-wrap [data-geek]'))
|
|
2703
|
+
.find((item) => (item.getAttribute('data-geek') || '') === String(candidateKey)) || null;
|
|
2704
|
+
const featuredAnchor = (recommendInner || latestInner)
|
|
2621
2705
|
? null
|
|
2622
2706
|
: Array.from(doc.querySelectorAll('li.geek-info-card a[data-geekid], a[data-geekid]'))
|
|
2623
2707
|
.find((item) => (item.getAttribute('data-geekid') || '') === String(candidateKey)) || null;
|
|
2624
|
-
const card =
|
|
2625
|
-
? (
|
|
2626
|
-
:
|
|
2708
|
+
const card = recommendInner
|
|
2709
|
+
? (recommendInner.closest('li.card-item') || recommendInner.closest('.card-item'))
|
|
2710
|
+
: latestInner
|
|
2711
|
+
? (latestInner.closest('.candidate-card-wrap') || latestInner.closest('li.card-item') || latestInner.closest('.card-item'))
|
|
2712
|
+
: (featuredAnchor ? (featuredAnchor.closest('li.geek-info-card') || featuredAnchor.closest('.geek-info-card')) : null);
|
|
2627
2713
|
if (!card) {
|
|
2628
2714
|
return { ok: false, error: 'CANDIDATE_CARD_NOT_FOUND' };
|
|
2629
2715
|
}
|
|
@@ -2716,6 +2802,130 @@ class RecommendScreenCli {
|
|
|
2716
2802
|
}
|
|
2717
2803
|
|
|
2718
2804
|
async callVisionModel(imagePath) {
|
|
2805
|
+
const primaryLimit = resolveVisionPixelLimitFromEnv(
|
|
2806
|
+
"BOSS_RECOMMEND_VISION_MAX_IMAGE_PIXELS",
|
|
2807
|
+
DEFAULT_VISION_MAX_IMAGE_PIXELS
|
|
2808
|
+
);
|
|
2809
|
+
const retryLimit = resolveVisionRetryPixelLimit(primaryLimit);
|
|
2810
|
+
const preparedPrimary = await this.prepareVisionImageForModel(imagePath, primaryLimit, "primary");
|
|
2811
|
+
try {
|
|
2812
|
+
return await this.requestVisionModel(preparedPrimary.imagePath);
|
|
2813
|
+
} catch (error) {
|
|
2814
|
+
if (!isVisionImageSizeLimitMessage(error?.message || "")) {
|
|
2815
|
+
throw error;
|
|
2816
|
+
}
|
|
2817
|
+
log(
|
|
2818
|
+
`[VISION] 检测到图片尺寸超限,准备降采样重试: ` +
|
|
2819
|
+
`primary_limit=${primaryLimit} source=${preparedPrimary.source} ` +
|
|
2820
|
+
`source_pixels=${preparedPrimary.sourcePixels ?? "unknown"}`
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
const preparedRetry = await this.prepareVisionImageForModel(imagePath, retryLimit, "retry");
|
|
2824
|
+
try {
|
|
2825
|
+
return await this.requestVisionModel(preparedRetry.imagePath);
|
|
2826
|
+
} catch (retryError) {
|
|
2827
|
+
if (!isVisionImageSizeLimitMessage(retryError?.message || "")) {
|
|
2828
|
+
throw retryError;
|
|
2829
|
+
}
|
|
2830
|
+
throw this.buildError(
|
|
2831
|
+
"VISION_IMAGE_SIZE_LIMIT_EXCEEDED",
|
|
2832
|
+
`Vision model still rejected image after retry downscale; ` +
|
|
2833
|
+
`primary_limit=${primaryLimit}; retry_limit=${retryLimit}; ` +
|
|
2834
|
+
`source_pixels=${preparedRetry.sourcePixels ?? "unknown"}; ` +
|
|
2835
|
+
`retry_pixels=${preparedRetry.currentPixels ?? "unknown"}; ` +
|
|
2836
|
+
`last_error=${normalizeText(retryError?.message || retryError)}`
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
async prepareVisionImageForModel(imagePath, maxPixels, attemptTag = "primary") {
|
|
2842
|
+
const resolvedMaxPixels = parsePositiveInteger(maxPixels);
|
|
2843
|
+
if (!resolvedMaxPixels) {
|
|
2844
|
+
return {
|
|
2845
|
+
imagePath,
|
|
2846
|
+
source: "no_limit",
|
|
2847
|
+
sourcePixels: null,
|
|
2848
|
+
currentPixels: null
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
let sharp;
|
|
2852
|
+
try {
|
|
2853
|
+
sharp = loadVisionSharp();
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
log(`[VISION] 加载 sharp 失败,跳过预缩放: ${error?.message || error}`);
|
|
2856
|
+
return {
|
|
2857
|
+
imagePath,
|
|
2858
|
+
source: "sharp_unavailable",
|
|
2859
|
+
sourcePixels: null,
|
|
2860
|
+
currentPixels: null
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
let metadata;
|
|
2864
|
+
try {
|
|
2865
|
+
metadata = await sharp(imagePath).metadata();
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
log(`[VISION] 读取图片尺寸失败,跳过预缩放: ${error?.message || error}`);
|
|
2868
|
+
return {
|
|
2869
|
+
imagePath,
|
|
2870
|
+
source: "metadata_error",
|
|
2871
|
+
sourcePixels: null,
|
|
2872
|
+
currentPixels: null
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
const width = Number(metadata?.width || 0);
|
|
2876
|
+
const height = Number(metadata?.height || 0);
|
|
2877
|
+
const sourcePixels = width > 0 && height > 0 ? width * height : null;
|
|
2878
|
+
if (!sourcePixels || sourcePixels <= resolvedMaxPixels) {
|
|
2879
|
+
return {
|
|
2880
|
+
imagePath,
|
|
2881
|
+
source: "within_limit",
|
|
2882
|
+
sourcePixels,
|
|
2883
|
+
currentPixels: sourcePixels
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
const scale = Math.sqrt(resolvedMaxPixels / sourcePixels);
|
|
2887
|
+
const targetWidth = Math.max(1, Math.floor(width * scale));
|
|
2888
|
+
const targetHeight = Math.max(1, Math.floor(height * scale));
|
|
2889
|
+
const parsedPath = path.parse(imagePath);
|
|
2890
|
+
const resizedPath = path.join(
|
|
2891
|
+
parsedPath.dir,
|
|
2892
|
+
`${parsedPath.name}.${attemptTag}.max${resolvedMaxPixels}.png`
|
|
2893
|
+
);
|
|
2894
|
+
try {
|
|
2895
|
+
await sharp(imagePath)
|
|
2896
|
+
.resize({
|
|
2897
|
+
width: targetWidth,
|
|
2898
|
+
height: targetHeight,
|
|
2899
|
+
fit: "inside",
|
|
2900
|
+
withoutEnlargement: true
|
|
2901
|
+
})
|
|
2902
|
+
.png()
|
|
2903
|
+
.toFile(resizedPath);
|
|
2904
|
+
const resizedMeta = await sharp(resizedPath).metadata();
|
|
2905
|
+
const resizedPixels = Number(resizedMeta?.width || 0) * Number(resizedMeta?.height || 0);
|
|
2906
|
+
log(
|
|
2907
|
+
`[VISION] 图片预缩放完成: ${width}x${height}(${sourcePixels}) -> ` +
|
|
2908
|
+
`${resizedMeta?.width || "?"}x${resizedMeta?.height || "?"}(${resizedPixels || "?"}); ` +
|
|
2909
|
+
`limit=${resolvedMaxPixels}; attempt=${attemptTag}`
|
|
2910
|
+
);
|
|
2911
|
+
return {
|
|
2912
|
+
imagePath: resizedPath,
|
|
2913
|
+
source: "resized",
|
|
2914
|
+
sourcePixels,
|
|
2915
|
+
currentPixels: resizedPixels || null
|
|
2916
|
+
};
|
|
2917
|
+
} catch (error) {
|
|
2918
|
+
log(`[VISION] 预缩放失败,继续使用原图: ${error?.message || error}`);
|
|
2919
|
+
return {
|
|
2920
|
+
imagePath,
|
|
2921
|
+
source: "resize_failed",
|
|
2922
|
+
sourcePixels,
|
|
2923
|
+
currentPixels: sourcePixels
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
async requestVisionModel(imagePath) {
|
|
2719
2929
|
const imageBase64 = fs.readFileSync(imagePath, "base64");
|
|
2720
2930
|
const rawBaseUrl = this.args.baseUrl;
|
|
2721
2931
|
log(`[callVisionModel] baseUrl 原始值类型=${typeof rawBaseUrl}, 长度=${rawBaseUrl != null ? String(rawBaseUrl).length : "null/undefined"}, JSON编码=${JSON.stringify(rawBaseUrl)}`);
|
|
@@ -3361,9 +3571,28 @@ class RecommendScreenCli {
|
|
|
3361
3571
|
} else {
|
|
3362
3572
|
this.resetResumeCaptureFailureStreak();
|
|
3363
3573
|
}
|
|
3364
|
-
if (
|
|
3574
|
+
if (error.code === "TEXT_MODEL_FAILED") {
|
|
3365
3575
|
throw error;
|
|
3366
3576
|
}
|
|
3577
|
+
if (error.code === "VISION_MODEL_FAILED") {
|
|
3578
|
+
if (isVisionImageSizeLimitMessage(error?.message || "")) {
|
|
3579
|
+
log(
|
|
3580
|
+
`[候选人跳过] ${nextCandidate.name || nextCandidate.geek_id || "unknown"} 触发视觉模型像素限制,` +
|
|
3581
|
+
"已在本轮跳过并继续处理下一位。"
|
|
3582
|
+
);
|
|
3583
|
+
} else {
|
|
3584
|
+
log(
|
|
3585
|
+
`[候选人跳过] ${nextCandidate.name || nextCandidate.geek_id || "unknown"} 视觉模型调用失败,` +
|
|
3586
|
+
"已在本轮跳过并继续处理下一位。"
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
if (error.code === "VISION_IMAGE_SIZE_LIMIT_EXCEEDED") {
|
|
3591
|
+
log(
|
|
3592
|
+
`[候选人跳过] ${nextCandidate.name || nextCandidate.geek_id || "unknown"} 触发视觉模型像素限制,` +
|
|
3593
|
+
"已完成预缩放和重试,仍失败,继续处理下一位。"
|
|
3594
|
+
);
|
|
3595
|
+
}
|
|
3367
3596
|
} finally {
|
|
3368
3597
|
const closed = await this.closeDetailPage();
|
|
3369
3598
|
if (!closed) {
|
|
@@ -3485,7 +3714,8 @@ if (require.main === module) {
|
|
|
3485
3714
|
parseFavoriteActionFromKnownRequest,
|
|
3486
3715
|
parseFavoriteActionFromActionLog,
|
|
3487
3716
|
parseFavoriteActionFromWsPayload,
|
|
3488
|
-
isRecoverablePostActionError
|
|
3717
|
+
isRecoverablePostActionError,
|
|
3718
|
+
classifyFinishedWrapState
|
|
3489
3719
|
}
|
|
3490
3720
|
};
|
|
3491
3721
|
}
|
|
@@ -382,6 +382,41 @@ async function testNetworkMissShouldFallbackToImageCapture() {
|
|
|
382
382
|
assert.equal(result.result.resume_source, "image_fallback");
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
async function testVisionModelFailureShouldSkipCandidateAndContinue() {
|
|
386
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-vision-failure-skip-"));
|
|
387
|
+
const first = { key: "vision-fail-1", geek_id: "vision-fail-1", name: "vision-fail-1" };
|
|
388
|
+
const second = { key: "vision-pass-2", geek_id: "vision-pass-2", name: "vision-pass-2" };
|
|
389
|
+
const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
|
|
390
|
+
candidates: [first, second],
|
|
391
|
+
captureOutcomes: new Map([
|
|
392
|
+
["vision-fail-1", { stitchedImage: path.join(tempDir, "vision-fail-1.png") }],
|
|
393
|
+
["vision-pass-2", { stitchedImage: path.join(tempDir, "vision-pass-2.png") }]
|
|
394
|
+
]),
|
|
395
|
+
screeningByKey: new Map([
|
|
396
|
+
["vision-pass-2", { passed: true, reason: "ok", summary: "ok" }]
|
|
397
|
+
])
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
cli.callVisionModel = async () => {
|
|
401
|
+
if (cli.lastCapturedCandidateKey === "vision-fail-1") {
|
|
402
|
+
const error = new Error("model backend timeout");
|
|
403
|
+
error.code = "VISION_MODEL_FAILED";
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
passed: true,
|
|
408
|
+
reason: "ok",
|
|
409
|
+
summary: "ok"
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const result = await cli.run();
|
|
414
|
+
assert.equal(result.status, "COMPLETED");
|
|
415
|
+
assert.equal(result.result.processed_count, 2);
|
|
416
|
+
assert.equal(result.result.passed_count, 1);
|
|
417
|
+
assert.equal(result.result.skipped_count, 1);
|
|
418
|
+
}
|
|
419
|
+
|
|
385
420
|
async function testFeaturedNetworkMissShouldSkipWithoutImageCapture() {
|
|
386
421
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-network-only-"));
|
|
387
422
|
const args = createArgs(tempDir);
|
|
@@ -551,6 +586,50 @@ function testFavoriteActionParserShouldOnlyTrustKnownRequestShapes() {
|
|
|
551
586
|
assert.equal(userMark, "add");
|
|
552
587
|
}
|
|
553
588
|
|
|
589
|
+
function testFinishedWrapClassifierShouldNotTreatLoadMoreAsBottom() {
|
|
590
|
+
const loadMore = __testables.classifyFinishedWrapState("滚动加载更多", false);
|
|
591
|
+
const loading = __testables.classifyFinishedWrapState("正在加载数据...", false);
|
|
592
|
+
const noMore = __testables.classifyFinishedWrapState("没有更多人选", false);
|
|
593
|
+
const refreshOnly = __testables.classifyFinishedWrapState("", true);
|
|
594
|
+
|
|
595
|
+
assert.equal(loadMore.isBottom, false);
|
|
596
|
+
assert.equal(loadMore.matched_load_more_keyword, "滚动加载更多");
|
|
597
|
+
assert.equal(loading.isBottom, false);
|
|
598
|
+
assert.equal(loading.matched_load_more_keyword, "正在加载");
|
|
599
|
+
assert.equal(noMore.isBottom, true);
|
|
600
|
+
assert.equal(noMore.matched_bottom_keyword, "没有更多");
|
|
601
|
+
assert.equal(refreshOnly.isBottom, true);
|
|
602
|
+
assert.equal(refreshOnly.reason, "refresh_button_visible");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function testGetCenteredCandidateClickPointShouldSupportLatestSelector() {
|
|
606
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-latest-click-locator-"));
|
|
607
|
+
const args = createArgs(tempDir);
|
|
608
|
+
args.pageScope = "latest";
|
|
609
|
+
const cli = new RecommendScreenCli(args);
|
|
610
|
+
|
|
611
|
+
let expressionCaptured = "";
|
|
612
|
+
cli.evaluate = async (expression) => {
|
|
613
|
+
expressionCaptured = String(expression || "");
|
|
614
|
+
return {
|
|
615
|
+
ok: true,
|
|
616
|
+
x: 100,
|
|
617
|
+
y: 100,
|
|
618
|
+
width: 120,
|
|
619
|
+
height: 64
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const result = await cli.getCenteredCandidateClickPoint({
|
|
624
|
+
key: "latest-test-key",
|
|
625
|
+
geek_id: "latest-test-key"
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
assert.equal(result.ok, true);
|
|
629
|
+
assert.equal(expressionCaptured.includes(".candidate-card-wrap .card-inner[data-geek]"), true);
|
|
630
|
+
assert.equal(expressionCaptured.includes("getAttribute('data-geek')"), true);
|
|
631
|
+
}
|
|
632
|
+
|
|
554
633
|
async function testFeaturedPostActionFailureShouldStillRecordPassedCandidate() {
|
|
555
634
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-featured-action-failure-"));
|
|
556
635
|
const args = createArgs(tempDir);
|
|
@@ -726,6 +805,7 @@ async function main() {
|
|
|
726
805
|
await testFeaturedShouldUseNetworkResumeOnly();
|
|
727
806
|
await testRecommendShouldKeepImageCaptureEvenWhenNetworkResumeExists();
|
|
728
807
|
await testNetworkMissShouldFallbackToImageCapture();
|
|
808
|
+
await testVisionModelFailureShouldSkipCandidateAndContinue();
|
|
729
809
|
await testFeaturedNetworkMissShouldSkipWithoutImageCapture();
|
|
730
810
|
await testFeaturedFavoriteShouldNotUseDomFallback();
|
|
731
811
|
await testFeaturedFavoriteShouldSkipClickWhenAlreadyInterested();
|
|
@@ -735,6 +815,8 @@ async function main() {
|
|
|
735
815
|
testFavoriteActionParserShouldSupportFallbackRequestShape();
|
|
736
816
|
testFavoriteActionParserShouldSupportWebSocketPayload();
|
|
737
817
|
testFavoriteActionParserShouldOnlyTrustKnownRequestShapes();
|
|
818
|
+
testFinishedWrapClassifierShouldNotTreatLoadMoreAsBottom();
|
|
819
|
+
await testGetCenteredCandidateClickPointShouldSupportLatestSelector();
|
|
738
820
|
await testFeaturedPostActionFailureShouldStillRecordPassedCandidate();
|
|
739
821
|
await testStitchWithSharpShouldComposeExpectedImage();
|
|
740
822
|
testStitchWithAvailablePythonShouldFallbackToPython();
|