@reconcrap/boss-recommend-mcp 1.3.3 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -216,6 +216,13 @@ chat-only 交互建议:
216
216
  - 先调用一次 `start_boss_chat_run`(可不带参数),服务会先导航到 `https://www.zhipin.com/web/chat/index` 并返回 `NEED_INPUT`,其中包含岗位 `job_options` 与待补字段。
217
217
  - 然后基于 `job_options` 让用户选择 `job`,并补齐 `start_from`、`target_count`、`criteria` 后再次调用 `start_boss_chat_run` 启动任务。
218
218
 
219
+ Trae-CN / 长对话防循环建议:
220
+
221
+ - 固定流程:`boss_chat_health_check` -> `start_boss_chat_run(空参可)` -> 一次性补齐 `job/start_from/target_count/criteria` -> 再次 `start_boss_chat_run`。
222
+ - `start_boss_chat_run` 返回 `ACCEPTED` 后直接结束当前回合,不要自动轮询。
223
+ - 缺参或校验失败时,一次性列出全部缺失/错误项,避免重复同一句提示触发宿主“陷入循环”保护。
224
+ - 仅当用户明确要求“查进度”时再调用 `get_boss_chat_run`。
225
+
219
226
  ## 长流程 Agent 兼容模式
220
227
 
221
228
  当宿主 agent 对“长时间无回包”敏感(容易误判失败)时,建议改用异步工具:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -3,3 +3,24 @@
3
3
  Bundled chat-page automation skill shipped with `boss-recommend-mcp`.
4
4
 
5
5
  Use this skill when the user wants a chat-only Boss workflow without installing `boss-chat` separately.
6
+
7
+ ## Stable Prompt Template (Trae-CN)
8
+
9
+ Use the following prompt when host agents are prone to loop detection in long conversations:
10
+
11
+ ```text
12
+ Please run a Boss chat-only task (do not switch to recommend flow).
13
+
14
+ Execution order:
15
+ 1) Call boss_chat_health_check.
16
+ 2) Call start_boss_chat_run once (empty params allowed) to fetch job_options and missing fields.
17
+ 3) Ask for these required fields in one shot: job, start_from (unread/all), target_count, criteria.
18
+ 4) After user reply, call start_boss_chat_run exactly once to start the run.
19
+ 5) If ACCEPTED, reply only with run_id and "task started"; no auto polling.
20
+
21
+ Anti-loop rules:
22
+ - Do not repeat the same sentence across turns.
23
+ - On validation errors, list all missing/invalid fields once.
24
+ - Do not call start_boss_chat_run repeatedly in one turn.
25
+ - Do not call get_boss_chat_run unless user explicitly asks for progress.
26
+ ```
package/src/boss-chat.js CHANGED
@@ -30,6 +30,69 @@ function parsePositiveInteger(value, fallback = null) {
30
30
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
31
31
  }
32
32
 
33
+ function isUnlimitedTargetCountToken(value) {
34
+ const token = normalizeText(value).toLowerCase();
35
+ if (!token) return false;
36
+ return [
37
+ "all",
38
+ "unlimited",
39
+ "infinity",
40
+ "inf",
41
+ "max",
42
+ "full",
43
+ "全部",
44
+ "全量",
45
+ "不限",
46
+ "扫到底",
47
+ "直到完成所有人选"
48
+ ].includes(token);
49
+ }
50
+
51
+ function parseBossChatTargetCount(value) {
52
+ if (value === undefined || value === null) {
53
+ return {
54
+ provided: false,
55
+ targetCount: null,
56
+ cliArg: null
57
+ };
58
+ }
59
+ const raw = normalizeText(value);
60
+ if (!raw) {
61
+ return {
62
+ provided: false,
63
+ targetCount: null,
64
+ cliArg: null
65
+ };
66
+ }
67
+ if (isUnlimitedTargetCountToken(raw)) {
68
+ return {
69
+ provided: true,
70
+ targetCount: null,
71
+ cliArg: "-1"
72
+ };
73
+ }
74
+ const parsed = Number.parseInt(String(raw), 10);
75
+ if (Number.isFinite(parsed) && parsed === -1) {
76
+ return {
77
+ provided: true,
78
+ targetCount: null,
79
+ cliArg: "-1"
80
+ };
81
+ }
82
+ if (Number.isFinite(parsed) && parsed > 0) {
83
+ return {
84
+ provided: true,
85
+ targetCount: parsed,
86
+ cliArg: String(parsed)
87
+ };
88
+ }
89
+ return {
90
+ provided: false,
91
+ targetCount: null,
92
+ cliArg: null
93
+ };
94
+ }
95
+
33
96
  function parseJsonOutput(text) {
34
97
  const trimmed = String(text || "").trim();
35
98
  if (!trimmed) return null;
@@ -152,14 +215,16 @@ function normalizeBossChatStartInput(input = {}) {
152
215
  const startFromRaw = normalizeText(input.startFrom || input.start_from).toLowerCase();
153
216
  const startFrom = startFromRaw === "all" ? "all" : startFromRaw === "unread" ? "unread" : "";
154
217
  const criteria = normalizeText(input.criteria);
155
- const targetCount = parsePositiveInteger(input.targetCount ?? input.target_count);
218
+ const parsedTarget = parseBossChatTargetCount(input.targetCount ?? input.target_count);
156
219
  const port = parsePositiveInteger(input.port);
157
220
  return {
158
221
  profile,
159
222
  job,
160
223
  startFrom,
161
224
  criteria,
162
- targetCount,
225
+ targetCount: parsedTarget.targetCount,
226
+ targetCountArg: parsedTarget.cliArg,
227
+ targetCountProvided: parsedTarget.provided,
163
228
  port,
164
229
  dryRun: input.dryRun === true || input.dry_run === true,
165
230
  noState: input.noState === true || input.no_state === true,
@@ -181,7 +246,7 @@ function getMissingBossChatStartFields(input = {}) {
181
246
  const missing = [];
182
247
  if (!normalized.job) missing.push("job");
183
248
  if (!normalized.startFrom) missing.push("start_from");
184
- if (!normalized.targetCount) missing.push("target_count");
249
+ if (!normalized.targetCountProvided) missing.push("target_count");
185
250
  if (!normalized.criteria) missing.push("criteria");
186
251
  return missing;
187
252
  }
@@ -194,7 +259,7 @@ function buildBossChatCliArgs(command, input, resolvedConfig) {
194
259
  if (normalized.job) args.push("--job", normalized.job);
195
260
  if (normalized.startFrom) args.push("--start-from", normalized.startFrom);
196
261
  if (normalized.criteria) args.push("--criteria", normalized.criteria);
197
- if (normalized.targetCount) args.push("--targetCount", String(normalized.targetCount));
262
+ if (normalized.targetCountArg) args.push("--targetCount", normalized.targetCountArg);
198
263
  args.push("--port", String(normalized.port || resolvedConfig.debugPort || 9222));
199
264
  args.push("--baseurl", resolvedConfig.baseUrl);
200
265
  args.push("--apikey", resolvedConfig.apiKey);
@@ -210,8 +275,8 @@ function buildBossChatCliArgs(command, input, resolvedConfig) {
210
275
  args.push("--job", normalized.job);
211
276
  args.push("--start-from", normalized.startFrom);
212
277
  args.push("--criteria", normalized.criteria);
213
- if (normalized.targetCount) {
214
- args.push("--targetCount", String(normalized.targetCount));
278
+ if (normalized.targetCountArg) {
279
+ args.push("--targetCount", normalized.targetCountArg);
215
280
  }
216
281
  args.push("--baseurl", resolvedConfig.baseUrl);
217
282
  args.push("--apikey", resolvedConfig.apiKey);
package/src/index.js CHANGED
@@ -70,9 +70,27 @@ let runSelfHealImpl = runRecommendSelfHeal;
70
70
  let spawnProcessImpl = spawn;
71
71
  const TERMINAL_RUN_STATES = new Set([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED]);
72
72
 
73
- function normalizeText(value) {
74
- return String(value || "").replace(/\s+/g, " ").trim();
75
- }
73
+ function normalizeText(value) {
74
+ return String(value || "").replace(/\s+/g, " ").trim();
75
+ }
76
+
77
+ function isUnlimitedTargetCountToken(value) {
78
+ const token = normalizeText(value).toLowerCase();
79
+ if (!token) return false;
80
+ return [
81
+ "all",
82
+ "unlimited",
83
+ "infinity",
84
+ "inf",
85
+ "max",
86
+ "full",
87
+ "全部",
88
+ "全量",
89
+ "不限",
90
+ "扫到底",
91
+ "直到完成所有人选"
92
+ ].includes(token);
93
+ }
76
94
 
77
95
  function parsePositiveInteger(raw, fallback) {
78
96
  const value = Number.parseInt(String(raw || ""), 10);
@@ -358,8 +376,16 @@ function createRunInputSchema() {
358
376
  enum: ["unread", "all"]
359
377
  },
360
378
  target_count: {
361
- type: "integer",
362
- minimum: 1
379
+ oneOf: [
380
+ {
381
+ type: "integer",
382
+ minimum: 1
383
+ },
384
+ {
385
+ type: "string",
386
+ enum: ["all", "unlimited", "全部", "不限", "扫到底", "全量"]
387
+ }
388
+ ]
363
389
  },
364
390
  dry_run: { type: "boolean" },
365
391
  no_state: { type: "boolean" },
@@ -399,9 +425,17 @@ function createBossChatStartInputSchema() {
399
425
  description: "boss-chat 的筛选 criteria"
400
426
  },
401
427
  target_count: {
402
- type: "integer",
403
- minimum: 1,
404
- description: "本次处理人数上限;chat-only 模式必填(可先不传,服务会返回 NEED_INPUT 引导补齐)"
428
+ oneOf: [
429
+ {
430
+ type: "integer",
431
+ minimum: 1
432
+ },
433
+ {
434
+ type: "string",
435
+ enum: ["all", "unlimited", "全部", "不限", "扫到底", "全量"]
436
+ }
437
+ ],
438
+ description: "本次处理人数上限;支持正整数或 all/不限(扫到底)"
405
439
  },
406
440
  port: {
407
441
  type: "integer",
@@ -673,9 +707,13 @@ function validateBossChatStartArgs(args) {
673
707
  }
674
708
  }
675
709
  if (Object.prototype.hasOwnProperty.call(args, "target_count")) {
676
- const targetCount = Number.parseInt(String(args.target_count), 10);
677
- if (!Number.isFinite(targetCount) || targetCount <= 0) {
678
- return "target_count must be a positive integer";
710
+ const rawTargetCount = args.target_count;
711
+ const targetCount = Number.parseInt(String(rawTargetCount), 10);
712
+ const tokenAllowed =
713
+ typeof rawTargetCount === "string" && isUnlimitedTargetCountToken(rawTargetCount);
714
+ const numericUnlimited = Number.isFinite(targetCount) && targetCount === -1;
715
+ if ((!Number.isFinite(targetCount) || targetCount <= 0) && !tokenAllowed && !numericUnlimited) {
716
+ return "target_count must be a positive integer or one of: all, unlimited, 全部, 不限, 扫到底, 全量";
679
717
  }
680
718
  }
681
719
  if (Object.prototype.hasOwnProperty.call(args, "port")) {
@@ -226,6 +226,7 @@ export class BossChatApp {
226
226
 
227
227
  let consecutiveErrors = 0;
228
228
  let exhaustedScrolls = 0;
229
+ const exhaustedScrollLimit = targetCount ? 3 : 8;
229
230
 
230
231
  try {
231
232
  while (shouldContinue(summary, targetCount)) {
@@ -336,7 +337,7 @@ export class BossChatApp {
336
337
  `列表滚动:ratio=${ratio.toFixed(2)} | didScroll=${Boolean(scrollResult.didScroll)} | top=${scrollResult.after?.top ?? 'n/a'} | scrollRetry=${exhaustedScrolls + 1}`,
337
338
  );
338
339
  exhaustedScrolls = scrollResult.didScroll ? exhaustedScrolls + 1 : exhaustedScrolls + 2;
339
- if (exhaustedScrolls >= 3) {
340
+ if (exhaustedScrolls >= exhaustedScrollLimit) {
340
341
  summary.exhausted = true;
341
342
  this.logger.log('列表滚动终止:连续无可处理候选人,判定为 exhausted。');
342
343
  break;
@@ -630,6 +630,40 @@ function browserActivateCandidate(options = {}) {
630
630
  function browserScrollCustomerList(options = {}) {
631
631
  const ratio = Number(options.ratio || 0.72);
632
632
  const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
633
+ const isOverflowScrollable = (el) => {
634
+ if (!(el instanceof HTMLElement)) return false;
635
+ const style = getComputedStyle(el);
636
+ return /(auto|scroll|overlay)/i.test(String(style.overflowY || ''));
637
+ };
638
+ const findBestScrollableContainer = (seedCard) => {
639
+ const candidates = [];
640
+ const pushCandidate = (node) => {
641
+ if (node instanceof HTMLElement) candidates.push(node);
642
+ };
643
+ if (seedCard instanceof HTMLElement) {
644
+ let current = seedCard.parentElement;
645
+ let depth = 0;
646
+ while (current && depth < 30) {
647
+ pushCandidate(current);
648
+ current = current.parentElement;
649
+ depth += 1;
650
+ }
651
+ }
652
+ const unique = Array.from(new Set(candidates));
653
+ let best = null;
654
+ let bestScore = -Infinity;
655
+ for (const node of unique) {
656
+ const scrollRange = Number(node.scrollHeight || 0) - Number(node.clientHeight || 0);
657
+ const styleBonus = isOverflowScrollable(node) ? 80 : 0;
658
+ const classBonus = /user|list|chat/i.test(String(node.className || '')) ? 24 : 0;
659
+ const score = scrollRange + styleBonus + classBonus;
660
+ if (score > bestScore) {
661
+ best = node;
662
+ bestScore = score;
663
+ }
664
+ }
665
+ return best;
666
+ };
633
667
  const isScrollable = (el) =>
634
668
  el instanceof HTMLElement &&
635
669
  Number(el.scrollHeight || 0) > Number(el.clientHeight || 0) + 16 &&
@@ -672,27 +706,59 @@ function browserScrollCustomerList(options = {}) {
672
706
  return { ok: false, error: 'CHAT_LIST_CONTAINER_NOT_FOUND' };
673
707
  }
674
708
 
709
+ const firstCard = listContainer.querySelector('div[role="listitem"]') || document.querySelector('div[role="listitem"]');
710
+ if (firstCard instanceof HTMLElement) {
711
+ const best = findBestScrollableContainer(firstCard);
712
+ if (best instanceof HTMLElement) {
713
+ listContainer = best;
714
+ }
715
+ }
716
+
675
717
  const before = {
676
718
  top: Number(listContainer.scrollTop || 0),
677
719
  height: Number(listContainer.scrollHeight || 0),
678
720
  clientHeight: Number(listContainer.clientHeight || 0),
721
+ cardCount: Number(listContainer.querySelectorAll('div[role="listitem"]').length || 0),
679
722
  };
680
723
  const amount = Math.max(120, Math.round(before.clientHeight * Math.max(0.35, Math.min(0.95, ratio))));
681
724
  const maxScroll = Math.max(0, before.height - before.clientHeight);
682
- listContainer.scrollTop = clamp(before.top + amount, 0, maxScroll);
725
+ if (maxScroll > 0) {
726
+ listContainer.scrollTop = clamp(before.top + amount, 0, maxScroll);
727
+ } else {
728
+ const cards = Array.from(listContainer.querySelectorAll('div[role="listitem"]')).filter(
729
+ (node) => node instanceof HTMLElement,
730
+ );
731
+ const tail = cards[cards.length - 1];
732
+ if (tail instanceof HTMLElement) {
733
+ tail.scrollIntoView({ block: 'end', inline: 'nearest' });
734
+ try {
735
+ listContainer.dispatchEvent(
736
+ new WheelEvent('wheel', {
737
+ deltaY: amount,
738
+ bubbles: true,
739
+ cancelable: true,
740
+ }),
741
+ );
742
+ } catch {}
743
+ }
744
+ }
683
745
  listContainer.dispatchEvent(new Event('scroll', { bubbles: true }));
684
746
 
685
747
  const after = {
686
748
  top: Number(listContainer.scrollTop || 0),
687
749
  height: Number(listContainer.scrollHeight || 0),
688
750
  clientHeight: Number(listContainer.clientHeight || 0),
751
+ cardCount: Number(listContainer.querySelectorAll('div[role="listitem"]').length || 0),
689
752
  };
690
753
 
691
754
  return {
692
755
  ok: true,
693
756
  before,
694
757
  after,
695
- didScroll: before.top !== after.top || before.height !== after.height,
758
+ didScroll:
759
+ before.top !== after.top ||
760
+ before.height !== after.height ||
761
+ before.cardCount !== after.cardCount,
696
762
  };
697
763
  }
698
764
 
@@ -725,29 +791,46 @@ function browserConversationReadyState() {
725
791
  const disabledAttr = Boolean(el?.hasAttribute?.('disabled'));
726
792
  return classText.includes('disabled') || ariaDisabled || disabledAttr;
727
793
  };
794
+ const isDisabledDeep = (el) => {
795
+ if (!(el instanceof HTMLElement)) return true;
796
+ let current = el;
797
+ let depth = 0;
798
+ while (current && depth < 5) {
799
+ if (isDisabled(current)) return true;
800
+ current = current.parentElement;
801
+ depth += 1;
802
+ }
803
+ return false;
804
+ };
805
+ const resolveAttachmentButton = () => {
806
+ const candidates = Array.from(
807
+ document.querySelectorAll(
808
+ '.resume-btn-file, .btn.resume-btn-file, [class*="resume-btn-file"]',
809
+ ),
810
+ ).filter((el) => isVisible(el));
811
+ const match = candidates.find((el) => {
812
+ const text = normalize(el.textContent || '');
813
+ if (!text) return false;
814
+ if (!text.includes('附件简历')) return false;
815
+ if (text.includes('求附件简历')) return false;
816
+ return true;
817
+ });
818
+ return match || null;
819
+ };
728
820
  const onlineResume = Array.from(
729
821
  document.querySelectorAll(
730
822
  'a.btn.resume-btn-online, a.resume-btn-online, .resume-btn-online, .btn.resume-btn-online',
731
823
  ),
732
824
  ).find((el) => {
733
825
  if (!isVisible(el)) return false;
734
- if (!normalize(el.textContent || '').includes('在线简历')) return false;
735
- return !isDisabled(el);
736
- });
737
- const attachmentResume = Array.from(
738
- document.querySelectorAll(
739
- '.btn.resume-btn-file, .resume-btn-file, [class*="resume-btn-file"], button, a, div, span',
740
- ),
741
- ).find((el) => {
742
- if (!isVisible(el)) return false;
743
- if (!normalize(el.textContent || '').includes('附件简历')) return false;
744
- const classText = String(el.className || '').toLowerCase();
745
- return classText.includes('resume-btn-file') || classText.includes('resume') || classText.includes('btn');
826
+ if (!normalize(el.textContent || '').includes('在线简历')) return false;
827
+ return !isDisabled(el);
746
828
  });
829
+ const attachmentResume = resolveAttachmentButton();
747
830
  const askResume = Array.from(document.querySelectorAll('span.operate-btn, button, a, span')).find(
748
831
  (el) => isVisible(el) && isAskResumeText(el.textContent || ''),
749
832
  );
750
- const attachmentResumeEnabled = Boolean(attachmentResume) && !isDisabled(attachmentResume);
833
+ const attachmentResumeEnabled = Boolean(attachmentResume) && !isDisabledDeep(attachmentResume);
751
834
  return {
752
835
  hasOnlineResume: Boolean(onlineResume),
753
836
  hasAskResume: Boolean(askResume),
@@ -131,11 +131,35 @@ function parseStartFrom(value, fallback = 'unread') {
131
131
  return fallback;
132
132
  }
133
133
 
134
+ function isUnlimitedTargetCountToken(value) {
135
+ const token = String(value || '').trim().toLowerCase();
136
+ if (!token) return false;
137
+ return [
138
+ 'all',
139
+ 'unlimited',
140
+ 'infinity',
141
+ 'inf',
142
+ 'max',
143
+ 'full',
144
+ '全部',
145
+ '全量',
146
+ '不限',
147
+ '扫到底',
148
+ '直到完成所有人选',
149
+ ].includes(token);
150
+ }
151
+
134
152
  function parseTargetCount(value) {
135
153
  if (value === undefined || value === null || String(value).trim() === '') {
136
154
  return null;
137
155
  }
156
+ if (isUnlimitedTargetCountToken(value)) {
157
+ return -1;
158
+ }
138
159
  const parsed = Number.parseInt(String(value), 10);
160
+ if (Number.isFinite(parsed) && parsed === -1) {
161
+ return -1;
162
+ }
139
163
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
140
164
  }
141
165
 
@@ -266,7 +290,7 @@ function printUsage() {
266
290
  console.log(' --job <text|value|index> Select job by label/value/index');
267
291
  console.log(' --criteria <text> Screening criteria for resume evaluation');
268
292
  console.log(' --start-from <unread|all> Start from unread or all list');
269
- console.log(' --targetCount <n> Maximum candidates to process; empty means unlimited');
293
+ console.log(' --targetCount <n|all> Maximum candidates to process; all means unlimited');
270
294
  console.log(' --baseurl <url> Override LLM base URL');
271
295
  console.log(' --apikey <key> Override LLM API key');
272
296
  console.log(' --model <name> Override LLM model');
@@ -540,7 +564,9 @@ function validateStartRunArgs(args) {
540
564
  const missing = [];
541
565
  if (!args?.overrides?.jobSelection) missing.push('--job');
542
566
  if (!args?.overrides?.startFrom) missing.push('--start-from');
543
- if (!args?.overrides?.targetCount) missing.push('--targetCount');
567
+ if (args?.overrides?.targetCount === undefined || args?.overrides?.targetCount === null) {
568
+ missing.push('--targetCount');
569
+ }
544
570
  if (!args?.overrides?.screeningCriteria) missing.push('--criteria');
545
571
 
546
572
  if (missing.length === 0) return null;
@@ -557,7 +583,12 @@ function validateStartRunArgs(args) {
557
583
  function buildPreparePendingQuestions(args, jobs = []) {
558
584
  const pendingQuestions = [];
559
585
  const startFromValue = String(args?.overrides?.startFrom || '').trim().toLowerCase();
560
- const targetCountValue = Number.parseInt(String(args?.overrides?.targetCount || ''), 10);
586
+ const targetCountValue = Number.parseInt(String(args?.overrides?.targetCount ?? ''), 10);
587
+ const hasTargetCount =
588
+ args?.overrides?.targetCount !== undefined &&
589
+ args?.overrides?.targetCount !== null &&
590
+ Number.isFinite(targetCountValue) &&
591
+ (targetCountValue > 0 || targetCountValue === -1);
561
592
  const criteriaValue = String(args?.overrides?.screeningCriteria || '').trim();
562
593
  const jobValue = String(args?.overrides?.jobSelection || '').trim();
563
594
  const jobOptions = jobs.map((job, index) => ({
@@ -586,10 +617,10 @@ function buildPreparePendingQuestions(args, jobs = []) {
586
617
  ],
587
618
  });
588
619
  }
589
- if (!Number.isFinite(targetCountValue) || targetCountValue <= 0) {
620
+ if (!hasTargetCount) {
590
621
  pendingQuestions.push({
591
622
  field: 'target_count',
592
- question: '请输入目标数量(正整数)',
623
+ question: '请输入目标数量(正整数)或 all(扫到底)',
593
624
  required: true,
594
625
  });
595
626
  }
@@ -1198,8 +1229,12 @@ async function executeRunCommand(args, dataDir) {
1198
1229
  cleanupRuntimeControls = setupRuntimeControls(runControl);
1199
1230
 
1200
1231
  logger.log('开始处理 Boss 聊天候选人列表...');
1232
+ const targetCountLabel =
1233
+ Number.isFinite(Number(runProfile.targetCount)) && Number(runProfile.targetCount) > 0
1234
+ ? String(runProfile.targetCount)
1235
+ : '扫到底';
1201
1236
  logger.log(
1202
- `本次设置: 岗位=${runProfile.jobSelection.label}, 范围=${runProfile.startFrom === 'all' ? '全部' : '未读'}, 上限=${runProfile.targetCount || '扫到底'}`,
1237
+ `本次设置: 岗位=${runProfile.jobSelection.label}, 范围=${runProfile.startFrom === 'all' ? '全部' : '未读'}, 上限=${targetCountLabel}`,
1203
1238
  );
1204
1239
  logger.log('运行中快捷键: p=暂停/继续, r=继续, q=停止, Ctrl+C=停止');
1205
1240