@reconcrap/boss-recommend-mcp 1.3.37 → 1.3.39

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.
@@ -83,8 +83,11 @@ const PAGE_SCOPE_TAB_STATUS = {
83
83
  latest: "1",
84
84
  featured: "3"
85
85
  };
86
- const BOTTOM_HINT_KEYWORDS = ["没有更多", "已显示全部", "已经到底", "暂无更多", "推荐完了", "没有更多人选"];
87
- const LOAD_MORE_HINT_KEYWORDS = ["滚动加载更多", "下滑加载更多", "继续下滑", "继续滑动", "滑动加载", "正在加载", "加载中"];
86
+ const BOTTOM_HINT_KEYWORDS = ["没有更多", "已显示全部", "已经到底", "暂无更多", "推荐完了", "没有更多人选"];
87
+ const LOAD_MORE_HINT_KEYWORDS = ["滚动加载更多", "下滑加载更多", "继续下滑", "继续滑动", "滑动加载", "正在加载", "加载中"];
88
+ const VIEWPORT_COLLAPSE_RATIO_THRESHOLD = 0.6;
89
+ const VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH = 1000;
90
+ const VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO = 0.85;
88
91
 
89
92
  function getHealingRulesPath() {
90
93
  const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_HEALING_RULES_FILE || "");
@@ -2465,17 +2468,28 @@ const jsGetListState = `(() => {
2465
2468
  scrollHeight: body ? body.scrollHeight : 0,
2466
2469
  clientHeight: body ? body.clientHeight : 0,
2467
2470
  clientWidth: body ? body.clientWidth : 0,
2468
- frameRect: {
2469
- width: frameRect.width,
2470
- height: frameRect.height
2471
- },
2472
- viewport: {
2473
- width: (doc.defaultView && Number.isFinite(doc.defaultView.innerWidth)) ? doc.defaultView.innerWidth : 0,
2474
- height: (doc.defaultView && Number.isFinite(doc.defaultView.innerHeight)) ? doc.defaultView.innerHeight : 0
2475
- },
2476
- candidateCount: effectiveCount,
2477
- recommendCandidateCount: candidateCards.length,
2478
- featuredCandidateCount: featuredCandidates.length,
2471
+ frameRect: {
2472
+ width: frameRect.width,
2473
+ height: frameRect.height
2474
+ },
2475
+ viewport: {
2476
+ width: (doc.defaultView && Number.isFinite(doc.defaultView.innerWidth)) ? doc.defaultView.innerWidth : 0,
2477
+ height: (doc.defaultView && Number.isFinite(doc.defaultView.innerHeight)) ? doc.defaultView.innerHeight : 0
2478
+ },
2479
+ topViewport: {
2480
+ innerWidth: Number.isFinite(window.innerWidth) ? window.innerWidth : 0,
2481
+ innerHeight: Number.isFinite(window.innerHeight) ? window.innerHeight : 0,
2482
+ outerWidth: Number.isFinite(window.outerWidth) ? window.outerWidth : 0,
2483
+ outerHeight: Number.isFinite(window.outerHeight) ? window.outerHeight : 0,
2484
+ visualWidth: (window.visualViewport && Number.isFinite(window.visualViewport.width)) ? window.visualViewport.width : 0,
2485
+ visualHeight: (window.visualViewport && Number.isFinite(window.visualViewport.height)) ? window.visualViewport.height : 0,
2486
+ screenAvailWidth: (window.screen && Number.isFinite(window.screen.availWidth)) ? window.screen.availWidth : 0,
2487
+ screenAvailHeight: (window.screen && Number.isFinite(window.screen.availHeight)) ? window.screen.availHeight : 0,
2488
+ devicePixelRatio: Number.isFinite(window.devicePixelRatio) ? window.devicePixelRatio : 0
2489
+ },
2490
+ candidateCount: effectiveCount,
2491
+ recommendCandidateCount: candidateCards.length,
2492
+ featuredCandidateCount: featuredCandidates.length,
2479
2493
  latestCandidateCount: latestCandidates.length,
2480
2494
  totalCards: Math.max(cards.length, featuredCards.length, latestCards.length),
2481
2495
  activeTabStatus: inferredStatus || null
@@ -4650,23 +4664,25 @@ class RecommendScreenCli {
4650
4664
  return state?.closed === false;
4651
4665
  }
4652
4666
 
4653
- async getListState() {
4654
- const state = await this.evaluate(jsGetListState);
4655
- if (state && typeof state === "object") {
4656
- const activeStatus = normalizeText(state.activeTabStatus || "");
4657
- if (activeStatus) {
4658
- this.lastActiveTabStatus = activeStatus;
4659
- }
4660
- return state;
4661
- }
4662
- return { ok: false, error: "INVALID_LIST_STATE" };
4663
- }
4664
-
4665
- isListViewportCollapsed(state) {
4666
- if (!state?.ok) return false;
4667
- const clientHeight = Number(state.clientHeight || 0);
4668
- const clientWidth = Number(state.clientWidth || 0);
4669
- const frameWidth = Number(state.frameRect?.width || 0);
4667
+ async getListState() {
4668
+ const state = await this.evaluate(jsGetListState);
4669
+ if (state && typeof state === "object") {
4670
+ const activeStatus = normalizeText(state.activeTabStatus || "");
4671
+ if (activeStatus) {
4672
+ this.lastActiveTabStatus = activeStatus;
4673
+ }
4674
+ state.viewportDiagnostics = await this.getViewportHealthDiagnostics(state);
4675
+ return state;
4676
+ }
4677
+ return { ok: false, error: "INVALID_LIST_STATE" };
4678
+ }
4679
+
4680
+ isListViewportCollapsed(state) {
4681
+ if (!state?.ok) return false;
4682
+ if (state.viewportDiagnostics?.relativeCollapsed === true) return true;
4683
+ const clientHeight = Number(state.clientHeight || 0);
4684
+ const clientWidth = Number(state.clientWidth || 0);
4685
+ const frameWidth = Number(state.frameRect?.width || 0);
4670
4686
  const frameHeight = Number(state.frameRect?.height || 0);
4671
4687
  const viewportWidth = Number(state.viewport?.width || 0);
4672
4688
  const viewportHeight = Number(state.viewport?.height || 0);
@@ -4676,23 +4692,147 @@ class RecommendScreenCli {
4676
4692
  || (clientWidth > 0 && clientWidth < 280)
4677
4693
  || (frameHeight > 0 && frameHeight < 320)
4678
4694
  || (frameWidth > 0 && frameWidth < 460)
4679
- || (viewportHeight > 0 && viewportHeight < 260)
4680
- || (viewportWidth > 0 && viewportWidth < 360)
4681
- );
4682
- }
4683
-
4684
- async getCurrentWindowState() {
4685
- if (!this.Browser || !this.windowId || typeof this.Browser.getWindowBounds !== "function") {
4686
- return null;
4687
- }
4688
- try {
4689
- const info = await this.Browser.getWindowBounds({ windowId: this.windowId });
4690
- const state = String(info?.bounds?.windowState || "").toLowerCase();
4691
- return state || null;
4692
- } catch {
4693
- return null;
4694
- }
4695
- }
4695
+ || (viewportHeight > 0 && viewportHeight < 260)
4696
+ || (viewportWidth > 0 && viewportWidth < 360)
4697
+ );
4698
+ }
4699
+
4700
+ getPositiveNumber(...values) {
4701
+ for (const value of values) {
4702
+ const number = Number(value);
4703
+ if (Number.isFinite(number) && number > 0) return number;
4704
+ }
4705
+ return 0;
4706
+ }
4707
+
4708
+ async getWindowBoundsInfo() {
4709
+ if (!this.Browser || !this.windowId || typeof this.Browser.getWindowBounds !== "function") {
4710
+ return null;
4711
+ }
4712
+ try {
4713
+ const info = await this.Browser.getWindowBounds({ windowId: this.windowId });
4714
+ return info && typeof info === "object" ? info : null;
4715
+ } catch {
4716
+ return null;
4717
+ }
4718
+ }
4719
+
4720
+ async getPageLayoutMetrics() {
4721
+ if (!this.Page || typeof this.Page.getLayoutMetrics !== "function") {
4722
+ return null;
4723
+ }
4724
+ try {
4725
+ const metrics = await this.Page.getLayoutMetrics();
4726
+ return metrics && typeof metrics === "object" ? metrics : null;
4727
+ } catch {
4728
+ return null;
4729
+ }
4730
+ }
4731
+
4732
+ buildViewportHealthDiagnostics(state, windowInfo = null, layoutMetrics = null) {
4733
+ const topViewport = state?.topViewport || {};
4734
+ const bounds = windowInfo?.bounds || null;
4735
+ const windowState = normalizeText(bounds?.windowState || "").toLowerCase() || null;
4736
+ const windowWidth = this.getPositiveNumber(bounds?.width);
4737
+ const screenAvailWidth = this.getPositiveNumber(topViewport.screenAvailWidth);
4738
+ const topOuterWidth = this.getPositiveNumber(topViewport.outerWidth);
4739
+ const actualWidth = this.getPositiveNumber(
4740
+ layoutMetrics?.cssVisualViewport?.clientWidth,
4741
+ layoutMetrics?.cssLayoutViewport?.clientWidth,
4742
+ topViewport.visualWidth,
4743
+ topViewport.innerWidth,
4744
+ state?.viewport?.width,
4745
+ state?.clientWidth,
4746
+ state?.frameRect?.width
4747
+ );
4748
+ const actualHeight = this.getPositiveNumber(
4749
+ layoutMetrics?.cssVisualViewport?.clientHeight,
4750
+ layoutMetrics?.cssLayoutViewport?.clientHeight,
4751
+ topViewport.visualHeight,
4752
+ topViewport.innerHeight,
4753
+ state?.viewport?.height,
4754
+ state?.clientHeight,
4755
+ state?.frameRect?.height
4756
+ );
4757
+ const nearFullscreen = (
4758
+ windowState === "maximized"
4759
+ || (
4760
+ windowWidth > 0
4761
+ && screenAvailWidth > 0
4762
+ && windowWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
4763
+ )
4764
+ || (
4765
+ topOuterWidth > 0
4766
+ && screenAvailWidth > 0
4767
+ && topOuterWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
4768
+ )
4769
+ );
4770
+ let expectedWidth = 0;
4771
+ if (windowState === "maximized" && screenAvailWidth > 0) {
4772
+ expectedWidth = screenAvailWidth;
4773
+ } else if (windowWidth > 0) {
4774
+ expectedWidth = screenAvailWidth > 0 && windowWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
4775
+ ? Math.min(windowWidth, screenAvailWidth)
4776
+ : windowWidth;
4777
+ } else if (topOuterWidth > 0) {
4778
+ expectedWidth = screenAvailWidth > 0 && topOuterWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
4779
+ ? Math.min(topOuterWidth, screenAvailWidth)
4780
+ : topOuterWidth;
4781
+ }
4782
+ const widthRatio = actualWidth > 0 && expectedWidth > 0 ? actualWidth / expectedWidth : null;
4783
+ const relativeCollapsed = Boolean(
4784
+ nearFullscreen
4785
+ && expectedWidth >= VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH
4786
+ && actualWidth > 0
4787
+ && widthRatio !== null
4788
+ && widthRatio <= VIEWPORT_COLLAPSE_RATIO_THRESHOLD
4789
+ );
4790
+ return {
4791
+ relativeCollapsed,
4792
+ threshold: VIEWPORT_COLLAPSE_RATIO_THRESHOLD,
4793
+ minExpectedWidth: VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH,
4794
+ nearFullscreen,
4795
+ windowState,
4796
+ actualWidth,
4797
+ actualHeight,
4798
+ expectedWidth,
4799
+ widthRatio,
4800
+ windowBounds: bounds ? {
4801
+ left: bounds.left ?? null,
4802
+ top: bounds.top ?? null,
4803
+ width: bounds.width ?? null,
4804
+ height: bounds.height ?? null,
4805
+ windowState: bounds.windowState ?? null
4806
+ } : null,
4807
+ cssLayoutViewport: layoutMetrics?.cssLayoutViewport || null,
4808
+ cssVisualViewport: layoutMetrics?.cssVisualViewport || null,
4809
+ topViewport: {
4810
+ innerWidth: topViewport.innerWidth || 0,
4811
+ innerHeight: topViewport.innerHeight || 0,
4812
+ outerWidth: topViewport.outerWidth || 0,
4813
+ outerHeight: topViewport.outerHeight || 0,
4814
+ visualWidth: topViewport.visualWidth || 0,
4815
+ visualHeight: topViewport.visualHeight || 0,
4816
+ screenAvailWidth: topViewport.screenAvailWidth || 0,
4817
+ screenAvailHeight: topViewport.screenAvailHeight || 0,
4818
+ devicePixelRatio: topViewport.devicePixelRatio || 0
4819
+ }
4820
+ };
4821
+ }
4822
+
4823
+ async getViewportHealthDiagnostics(state) {
4824
+ const [windowInfo, layoutMetrics] = await Promise.all([
4825
+ this.getWindowBoundsInfo(),
4826
+ this.getPageLayoutMetrics()
4827
+ ]);
4828
+ return this.buildViewportHealthDiagnostics(state, windowInfo, layoutMetrics);
4829
+ }
4830
+
4831
+ async getCurrentWindowState() {
4832
+ const info = await this.getWindowBoundsInfo();
4833
+ const state = String(info?.bounds?.windowState || "").toLowerCase();
4834
+ return state || null;
4835
+ }
4696
4836
 
4697
4837
  async setWindowStateIfPossible(windowState, reason = "unknown") {
4698
4838
  if (!this.Browser || !this.windowId || typeof this.Browser.setWindowBounds !== "function") {
@@ -4734,26 +4874,31 @@ class RecommendScreenCli {
4734
4874
  return applied;
4735
4875
  }
4736
4876
 
4737
- async ensureHealthyListViewport(reason = "unknown") {
4738
- let state = await this.getListState();
4739
- if (!this.isListViewportCollapsed(state)) {
4740
- return { ok: true, recovered: false, state };
4741
- }
4742
-
4743
- log(`[视口恢复] 检测到推荐列表视口异常缩小,尝试自动恢复。原因: ${reason}`);
4744
- await this.toggleWindowStateForViewportRecovery(reason);
4745
- await sleep(humanDelay(900, 130));
4746
- state = await this.getListState();
4877
+ async ensureHealthyListViewport(reason = "unknown") {
4878
+ let state = await this.getListState();
4879
+ if (!this.isListViewportCollapsed(state)) {
4880
+ return { ok: true, recovered: false, state };
4881
+ }
4882
+
4883
+ const diagnostics = state?.viewportDiagnostics || null;
4884
+ const ratioText = Number.isFinite(diagnostics?.widthRatio)
4885
+ ? `,宽度比例: ${diagnostics.widthRatio.toFixed(3)}`
4886
+ : "";
4887
+ log(`[视口恢复] 检测到推荐列表视口异常缩小,尝试自动恢复。原因: ${reason}${ratioText}`);
4888
+ await this.toggleWindowStateForViewportRecovery(reason);
4889
+ await sleep(humanDelay(900, 130));
4890
+ state = await this.getListState();
4747
4891
  if (!this.isListViewportCollapsed(state)) {
4748
4892
  return { ok: true, recovered: true, state };
4749
4893
  }
4750
4894
 
4751
- return {
4752
- ok: false,
4753
- recovered: false,
4754
- state
4755
- };
4756
- }
4895
+ return {
4896
+ ok: false,
4897
+ recovered: false,
4898
+ state,
4899
+ diagnostics: state?.viewportDiagnostics || diagnostics || null
4900
+ };
4901
+ }
4757
4902
 
4758
4903
  async discoverCandidates() {
4759
4904
  const health = await this.ensureHealthyListViewport("discover_candidates");
@@ -185,6 +185,62 @@ function createResumeCaptureError(message = "Resume canvas not found") {
185
185
  return error;
186
186
  }
187
187
 
188
+ function createViewportState(cli, {
189
+ actualWidth,
190
+ actualHeight = 585,
191
+ screenAvailWidth = 1440,
192
+ windowState = "maximized",
193
+ windowWidth = 1454
194
+ }) {
195
+ const state = {
196
+ ok: true,
197
+ clientWidth: actualWidth,
198
+ clientHeight: actualHeight,
199
+ frameRect: {
200
+ width: actualWidth,
201
+ height: actualHeight
202
+ },
203
+ viewport: {
204
+ width: actualWidth,
205
+ height: actualHeight
206
+ },
207
+ topViewport: {
208
+ innerWidth: actualWidth,
209
+ innerHeight: actualHeight,
210
+ outerWidth: actualWidth,
211
+ outerHeight: actualHeight,
212
+ visualWidth: actualWidth,
213
+ visualHeight: actualHeight,
214
+ screenAvailWidth,
215
+ screenAvailHeight: 860,
216
+ devicePixelRatio: 2
217
+ }
218
+ };
219
+ state.viewportDiagnostics = cli.buildViewportHealthDiagnostics(
220
+ state,
221
+ {
222
+ bounds: {
223
+ left: -7,
224
+ top: -7,
225
+ width: windowWidth,
226
+ height: 874,
227
+ windowState
228
+ }
229
+ },
230
+ {
231
+ cssVisualViewport: {
232
+ clientWidth: actualWidth,
233
+ clientHeight: actualHeight
234
+ },
235
+ cssLayoutViewport: {
236
+ clientWidth: actualWidth,
237
+ clientHeight: actualHeight
238
+ }
239
+ }
240
+ );
241
+ return state;
242
+ }
243
+
188
244
  function createArgs(tempDir) {
189
245
  return {
190
246
  baseUrl: "https://example.invalid/v1",
@@ -219,6 +275,75 @@ function createArgs(tempDir) {
219
275
  };
220
276
  }
221
277
 
278
+ function testViewportCollapseRiskShouldUseRelativeWidth() {
279
+ const cli = new RecommendScreenCli(createArgs(os.tmpdir()));
280
+ const state = createViewportState(cli, {
281
+ actualWidth: 785,
282
+ screenAvailWidth: 1440,
283
+ windowState: "maximized",
284
+ windowWidth: 1454
285
+ });
286
+ assert.equal(state.viewportDiagnostics.relativeCollapsed, true);
287
+ assert.equal(cli.isListViewportCollapsed(state), true);
288
+ assert.equal(Math.round(state.viewportDiagnostics.widthRatio * 1000), 545);
289
+ }
290
+
291
+ function testNormalMaximizedViewportShouldNotCollapse() {
292
+ const cli = new RecommendScreenCli(createArgs(os.tmpdir()));
293
+ const state = createViewportState(cli, {
294
+ actualWidth: 1280,
295
+ screenAvailWidth: 1440,
296
+ windowState: "maximized",
297
+ windowWidth: 1454
298
+ });
299
+ assert.equal(state.viewportDiagnostics.relativeCollapsed, false);
300
+ assert.equal(cli.isListViewportCollapsed(state), false);
301
+ }
302
+
303
+ function testSmallNormalWindowShouldNotUseScreenWidthRatio() {
304
+ const cli = new RecommendScreenCli(createArgs(os.tmpdir()));
305
+ const state = createViewportState(cli, {
306
+ actualWidth: 785,
307
+ screenAvailWidth: 1440,
308
+ windowState: "normal",
309
+ windowWidth: 800
310
+ });
311
+ assert.equal(state.viewportDiagnostics.nearFullscreen, false);
312
+ assert.equal(state.viewportDiagnostics.relativeCollapsed, false);
313
+ assert.equal(cli.isListViewportCollapsed(state), false);
314
+ }
315
+
316
+ async function testViewportCollapseRiskShouldTriggerRecovery() {
317
+ const cli = new RecommendScreenCli(createArgs(os.tmpdir()));
318
+ const collapsedState = createViewportState(cli, {
319
+ actualWidth: 785,
320
+ screenAvailWidth: 1440,
321
+ windowState: "maximized",
322
+ windowWidth: 1454
323
+ });
324
+ const healthyState = createViewportState(cli, {
325
+ actualWidth: 1280,
326
+ screenAvailWidth: 1440,
327
+ windowState: "maximized",
328
+ windowWidth: 1454
329
+ });
330
+ let listStateCalls = 0;
331
+ let recoveryCalled = false;
332
+ cli.getListState = async () => {
333
+ listStateCalls += 1;
334
+ return listStateCalls === 1 ? collapsedState : healthyState;
335
+ };
336
+ cli.toggleWindowStateForViewportRecovery = async () => {
337
+ recoveryCalled = true;
338
+ return true;
339
+ };
340
+ const result = await cli.ensureHealthyListViewport("test_relative_viewport");
341
+ assert.equal(recoveryCalled, true);
342
+ assert.equal(result.ok, true);
343
+ assert.equal(result.recovered, true);
344
+ assert.equal(result.state, healthyState);
345
+ }
346
+
222
347
  async function withLongResumeChunkEnv(overrides, fn) {
223
348
  const envKeys = [
224
349
  "BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS",
@@ -2225,6 +2350,10 @@ async function testVisionModelShouldSendAllOrderedChunks() {
2225
2350
  async function main() {
2226
2351
  testShouldAbortResumeProbeEarly();
2227
2352
  testResumeViewportStabilityRequiresSettledScrollAndClip();
2353
+ testViewportCollapseRiskShouldUseRelativeWidth();
2354
+ testNormalMaximizedViewportShouldNotCollapse();
2355
+ testSmallNormalWindowShouldNotUseScreenWidthRatio();
2356
+ await testViewportCollapseRiskShouldTriggerRecovery();
2228
2357
  await testSingleResumeCaptureFailureIsSkipped();
2229
2358
  await testConsecutiveResumeCaptureFailuresStillAbort();
2230
2359
  await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();