@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.
@@ -2404,6 +2404,62 @@ async function testFinalReviewShouldIncludeFollowUpChatSummary() {
2404
2404
  assert.equal(finalReview?.value?.follow_up?.chat?.target_count, 5);
2405
2405
  }
2406
2406
 
2407
+ async function testFollowUpChatGreetingTextShouldPassThroughWhenProvided() {
2408
+ let capturedChatInput = null;
2409
+ const result = await runRecommendPipeline(
2410
+ {
2411
+ workspaceRoot: process.cwd(),
2412
+ instruction: "test",
2413
+ confirmation: createJobConfirmedConfirmation(),
2414
+ overrides: {},
2415
+ followUp: createFollowUpChat({ greeting_text: "您好,方便发下简历吗?" })
2416
+ },
2417
+ {
2418
+ parseRecommendInstruction: () => createParsed(),
2419
+ runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9555 }),
2420
+ ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
2421
+ listRecommendJobs: async () => createJobListResult(),
2422
+ runRecommendSearchCli: async () => ({
2423
+ ok: true,
2424
+ summary: {
2425
+ candidate_count: 6,
2426
+ applied_filters: {},
2427
+ page_state: {}
2428
+ }
2429
+ }),
2430
+ runRecommendScreenCli: async () => ({
2431
+ ok: true,
2432
+ summary: {
2433
+ processed_count: 6,
2434
+ passed_count: 2,
2435
+ skipped_count: 0
2436
+ }
2437
+ }),
2438
+ startBossChatRun: async ({ input }) => {
2439
+ capturedChatInput = input;
2440
+ return {
2441
+ status: "ACCEPTED",
2442
+ run_id: "chat-run-greeting",
2443
+ message: "chat started"
2444
+ };
2445
+ },
2446
+ getBossChatRun: async () => ({
2447
+ status: "COMPLETED",
2448
+ run: {
2449
+ runId: "chat-run-greeting",
2450
+ state: "completed",
2451
+ lastMessage: "chat completed",
2452
+ progress: { processed: 2, matched: 2 }
2453
+ }
2454
+ })
2455
+ }
2456
+ );
2457
+
2458
+ assert.equal(result.status, "COMPLETED");
2459
+ assert.equal(capturedChatInput?.greeting_text, "您好,方便发下简历吗?");
2460
+ assert.equal(result.follow_up?.chat?.input?.greeting_text, "您好,方便发下简历吗?");
2461
+ }
2462
+
2407
2463
  async function testCompletedPipelineShouldRunChatFollowUp() {
2408
2464
  let getChatRunCalls = 0;
2409
2465
  const result = await runRecommendPipeline(
@@ -2635,6 +2691,7 @@ async function main() {
2635
2691
  await testFollowUpChatMissingTargetCountShouldNeedInput();
2636
2692
  await testFollowUpChatInvalidTargetCountShouldNeedInputWithDiagnostics();
2637
2693
  await testFinalReviewShouldIncludeFollowUpChatSummary();
2694
+ await testFollowUpChatGreetingTextShouldPassThroughWhenProvided();
2638
2695
  await testCompletedPipelineShouldRunChatFollowUp();
2639
2696
  await testFollowUpChatAllTargetCountShouldLaunchUnlimited();
2640
2697
  await testFollowUpChatPassedTargetCountShouldLaunchWithPassedCount();
@@ -1,8 +1,9 @@
1
- import { mkdir } from 'node:fs/promises';
2
- import path from 'node:path';
3
-
4
- import { isDomProfileConsistentWithCard, NETWORK_RESUME_RETRY_WAIT_MS } from './services/resume-network.js';
5
- import { createCustomerAliases, createCustomerKey } from './utils/customer-key.js';
1
+ import { mkdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { isDomProfileConsistentWithCard, NETWORK_RESUME_RETRY_WAIT_MS } from './services/resume-network.js';
5
+ import { DEFAULT_GREETING_TEXT } from './services/profile-store.js';
6
+ import { createCustomerAliases, createCustomerKey } from './utils/customer-key.js';
6
7
 
7
8
  function runToken(date = new Date()) {
8
9
  return date.toISOString().replace(/[:.]/g, '-');
@@ -37,12 +38,16 @@ function shouldContinue(summary, targetCount) {
37
38
  return summary.inspected < targetCount;
38
39
  }
39
40
 
40
- function hasResumeRequestSentMessage(state = {}) {
41
- const lastText = normalizeText(state?.lastText || '');
42
- const recent = Array.isArray(state?.recent) ? state.recent : [];
43
- if (lastText.includes('简历请求已发送')) return true;
44
- return recent.some((item) => normalizeText(item).includes('简历请求已发送'));
45
- }
41
+ function hasResumeRequestSentMessage(state = {}) {
42
+ const lastText = normalizeText(state?.lastText || '');
43
+ const recent = Array.isArray(state?.recent) ? state.recent : [];
44
+ if (lastText.includes('简历请求已发送')) return true;
45
+ return recent.some((item) => normalizeText(item).includes('简历请求已发送'));
46
+ }
47
+
48
+ function resolveGreetingText(profile = {}) {
49
+ return normalizeText(profile?.greetingText || '') || DEFAULT_GREETING_TEXT;
50
+ }
46
51
 
47
52
  const CANDIDATE_LIST_WAIT_AFTER_CONTEXT_MS = 5000;
48
53
  const CANDIDATE_LIST_WAIT_POLL_MS = 500;
@@ -1323,13 +1328,13 @@ export class BossChatApp {
1323
1328
  return baseResult;
1324
1329
  }
1325
1330
 
1326
- const greetingText = 'Hi同学,能麻烦发下简历吗?';
1331
+ const greetingText = resolveGreetingText(profile);
1327
1332
  this.logger.log(`候选人通过,先发送消息:${greetingText}`);
1328
1333
  await this.checkpoint();
1329
- const editorState = await this.page.setEditorMessage(greetingText);
1330
- if (!String(editorState?.value || '').includes('Hi同学')) {
1331
- throw new Error('CHAT_EDITOR_MESSAGE_MISMATCH');
1332
- }
1334
+ const editorState = await this.page.setEditorMessage(greetingText);
1335
+ if (!normalizeText(editorState?.value || '').includes(normalizeText(greetingText))) {
1336
+ throw new Error('CHAT_EDITOR_MESSAGE_MISMATCH');
1337
+ }
1333
1338
  this.logger.log(
1334
1339
  `招呼语写入输入框:activeSubmit=${Boolean(editorState?.activeSubmit)} | valueLen=${String(editorState?.value || '').length}`,
1335
1340
  );
@@ -189,14 +189,15 @@ function parseArgs(argv) {
189
189
  json: false,
190
190
  runId: '',
191
191
  detachedWorker: false,
192
- overrides: {
193
- startFrom: undefined,
194
- targetCount: undefined,
195
- screeningCriteria: undefined,
196
- jobSelection: undefined,
197
- llm: {},
198
- chrome: {},
199
- runtime: {},
192
+ overrides: {
193
+ startFrom: undefined,
194
+ targetCount: undefined,
195
+ screeningCriteria: undefined,
196
+ greetingText: undefined,
197
+ jobSelection: undefined,
198
+ llm: {},
199
+ chrome: {},
200
+ runtime: {},
200
201
  },
201
202
  };
202
203
 
@@ -246,14 +247,19 @@ function parseArgs(argv) {
246
247
  case 'startFrom':
247
248
  args.overrides.startFrom = parseStartFrom(value, 'unread');
248
249
  break;
249
- case 'criteria':
250
- case 'screeningCriteria':
251
- args.overrides.screeningCriteria = String(value || '').trim();
252
- break;
253
- case 'job':
254
- case 'jobSelection':
255
- args.overrides.jobSelection = String(value || '').trim();
256
- break;
250
+ case 'criteria':
251
+ case 'screeningCriteria':
252
+ args.overrides.screeningCriteria = String(value || '').trim();
253
+ break;
254
+ case 'greeting':
255
+ case 'greeting-text':
256
+ case 'greetingText':
257
+ args.overrides.greetingText = String(value || '').trim();
258
+ break;
259
+ case 'job':
260
+ case 'jobSelection':
261
+ args.overrides.jobSelection = String(value || '').trim();
262
+ break;
257
263
  case 'baseurl':
258
264
  case 'baseUrl':
259
265
  args.overrides.llm.baseUrl = value || '';
@@ -401,10 +407,11 @@ function printUsage() {
401
407
  console.log('Run options:');
402
408
  console.log(' --dry-run Evaluate and click, but do not request resume');
403
409
  console.log(' --no-state Disable in-run candidate deduplication');
404
- console.log(' --job <text|value|index> Select job by label/value/index');
405
- console.log(' --criteria <text> Screening criteria for resume evaluation');
406
- console.log(' --start-from <unread|all> Start from unread or all list');
407
- console.log(' --targetCount <n|all> Maximum candidates to process; all means unlimited');
410
+ console.log(' --job <text|value|index> Select job by label/value/index');
411
+ console.log(' --criteria <text> Screening criteria for resume evaluation');
412
+ console.log(' --greeting <text> Optional greeting message sent before asking for resume');
413
+ console.log(' --start-from <unread|all> Start from unread or all list');
414
+ console.log(' --targetCount <n|all> Maximum candidates to process; all means unlimited');
408
415
  console.log(' --baseurl <url> Override LLM base URL');
409
416
  console.log(' --apikey <key> Override LLM API key');
410
417
  console.log(' --model <name> Override LLM model');
@@ -615,9 +622,10 @@ async function promptRunProfile({ page, persistentProfile, overrides }) {
615
622
  }
616
623
  }
617
624
 
618
- let startFrom = overrides.startFrom;
619
- let screeningCriteria = overrides.screeningCriteria;
620
- let targetCount = overrides.targetCount;
625
+ let startFrom = overrides.startFrom;
626
+ let screeningCriteria = overrides.screeningCriteria;
627
+ let targetCount = overrides.targetCount;
628
+ let greetingText = String(overrides.greetingText || '').trim() || String(persistentProfile?.greetingText || '').trim();
621
629
 
622
630
  if (process.stdin.isTTY) {
623
631
  const rl = readline.createInterface({
@@ -672,10 +680,11 @@ async function promptRunProfile({ page, persistentProfile, overrides }) {
672
680
  label: selectedJob.label,
673
681
  },
674
682
  startFrom,
675
- screeningCriteria,
676
- targetCount: targetCount ?? null,
677
- });
678
- }
683
+ screeningCriteria,
684
+ targetCount: targetCount ?? null,
685
+ greetingText,
686
+ });
687
+ }
679
688
 
680
689
  function validateStartRunArgs(args) {
681
690
  const missing = [];
@@ -967,10 +976,11 @@ async function handlePrepareRunCommand(args, dataDir) {
967
976
  blank_chat_page: blankChatPage,
968
977
  renavigate_attempts: renavigateAttempts,
969
978
  required_fields: CHAT_START_REQUIRED_FIELDS.slice(),
970
- defaults: {
971
- profile: String(args.profile || 'default').trim() || 'default',
972
- start_from: 'unread',
973
- },
979
+ defaults: {
980
+ profile: String(args.profile || 'default').trim() || 'default',
981
+ start_from: 'unread',
982
+ greeting_text: String(args.overrides.greetingText || '').trim() || String(mergedProfile.greetingText || '').trim(),
983
+ },
974
984
  job_options: jobs.map((job, index) => ({
975
985
  index: index + 1,
976
986
  label: String(job.label || ''),
@@ -1000,9 +1010,12 @@ async function handlePrepareRunCommand(args, dataDir) {
1000
1010
  function buildDetachedRunArgs(args, runId) {
1001
1011
  const workerArgs = [CLI_FILE_PATH, 'run', '--detached-worker', '--run-id', runId];
1002
1012
  workerArgs.push('--profile', args.profile);
1003
- workerArgs.push('--job', String(args.overrides.jobSelection));
1004
- workerArgs.push('--start-from', String(args.overrides.startFrom));
1005
- workerArgs.push('--criteria', String(args.overrides.screeningCriteria));
1013
+ workerArgs.push('--job', String(args.overrides.jobSelection));
1014
+ workerArgs.push('--start-from', String(args.overrides.startFrom));
1015
+ workerArgs.push('--criteria', String(args.overrides.screeningCriteria));
1016
+ if (String(args.overrides.greetingText || '').trim()) {
1017
+ workerArgs.push('--greeting', String(args.overrides.greetingText));
1018
+ }
1006
1019
 
1007
1020
  if (args.dryRun) workerArgs.push('--dry-run');
1008
1021
  if (args.noState) workerArgs.push('--no-state');
@@ -1084,13 +1097,17 @@ async function handleStartRunCommand(args, dataDir) {
1084
1097
  dryRun: Boolean(args.dryRun),
1085
1098
  noState: Boolean(args.noState),
1086
1099
  input: {
1087
- job: String(args.overrides.jobSelection || ''),
1088
- startFrom: String(args.overrides.startFrom || ''),
1089
- criteria: String(args.overrides.screeningCriteria || ''),
1090
- targetCount: args.overrides.targetCount ?? null,
1091
- },
1092
- },
1093
- });
1100
+ job: String(args.overrides.jobSelection || ''),
1101
+ startFrom: String(args.overrides.startFrom || ''),
1102
+ criteria: String(args.overrides.screeningCriteria || ''),
1103
+ targetCount: args.overrides.targetCount ?? null,
1104
+ greetingText:
1105
+ String(args.overrides.greetingText || '').trim()
1106
+ || String(mergedProfile.greetingText || '').trim()
1107
+ || null,
1108
+ },
1109
+ },
1110
+ });
1094
1111
  writeRunState(dataDir, snapshot);
1095
1112
  appendRunEvent(dataDir, runId, {
1096
1113
  type: 'lifecycle',
@@ -1417,16 +1434,17 @@ async function executeRunCommand(args, dataDir) {
1417
1434
  logger.log('检测到聊天页处于空白未初始化状态,将继续通过岗位选择和首位候选人预热来恢复列表。');
1418
1435
  }
1419
1436
 
1420
- const runProfile = await promptRunProfile({
1421
- page,
1422
- persistentProfile,
1423
- overrides: args.overrides,
1424
- });
1437
+ const runProfile = await promptRunProfile({
1438
+ page,
1439
+ persistentProfile,
1440
+ overrides: args.overrides,
1441
+ });
1425
1442
  const appliedJob = await page.selectJob(runProfile.jobSelection);
1426
- runProfile.jobSelection = {
1427
- value: appliedJob.value || runProfile.jobSelection.value,
1428
- label: appliedJob.label || runProfile.jobSelection.label,
1429
- };
1443
+ runProfile.jobSelection = {
1444
+ value: appliedJob.value || runProfile.jobSelection.value,
1445
+ label: appliedJob.label || runProfile.jobSelection.label,
1446
+ };
1447
+ await profileStore.save(args.profile, toPersistentProfile(runProfile));
1430
1448
 
1431
1449
  if (asyncMode) {
1432
1450
  updateRunState(dataDir, runId, {
@@ -1,12 +1,15 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
-
4
- const DEFAULT_PROFILE = {
5
- screeningCriteria: '',
6
- targetCount: null,
7
- startFrom: 'unread',
8
- jobSelection: null,
9
- llm: {
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export const DEFAULT_GREETING_TEXT = 'Hi同学,能麻烦发下简历吗?';
5
+
6
+ const DEFAULT_PROFILE = {
7
+ screeningCriteria: '',
8
+ targetCount: null,
9
+ startFrom: 'unread',
10
+ greetingText: DEFAULT_GREETING_TEXT,
11
+ jobSelection: null,
12
+ llm: {
10
13
  baseUrl: '',
11
14
  apiKey: '',
12
15
  model: '',
@@ -74,12 +77,13 @@ function normalizeJobSelection(jobSelection) {
74
77
  };
75
78
  }
76
79
 
77
- export function toPersistentProfile(profile = {}) {
78
- const normalized = normalizeProfile(profile);
79
- return {
80
- llm: {
81
- baseUrl: normalized.llm.baseUrl,
82
- apiKey: normalized.llm.apiKey,
80
+ export function toPersistentProfile(profile = {}) {
81
+ const normalized = normalizeProfile(profile);
82
+ return {
83
+ greetingText: normalized.greetingText,
84
+ llm: {
85
+ baseUrl: normalized.llm.baseUrl,
86
+ apiKey: normalized.llm.apiKey,
83
87
  model: normalized.llm.model,
84
88
  thinkingLevel: normalized.llm.thinkingLevel,
85
89
  timeoutMs: normalized.llm.timeoutMs,
@@ -95,12 +99,13 @@ export function toPersistentProfile(profile = {}) {
95
99
  };
96
100
  }
97
101
 
98
- export function normalizeProfile(profile = {}) {
99
- const merged = mergeProfile(cloneDefaultProfile(), profile);
100
- merged.screeningCriteria = String(merged.screeningCriteria || '').trim();
101
- merged.startFrom = String(merged.startFrom || '').trim().toLowerCase() === 'all' ? 'all' : 'unread';
102
- merged.targetCount = normalizeOptionalPositiveNumber(merged.targetCount, null);
103
- merged.jobSelection = normalizeJobSelection(merged.jobSelection);
102
+ export function normalizeProfile(profile = {}) {
103
+ const merged = mergeProfile(cloneDefaultProfile(), profile);
104
+ merged.screeningCriteria = String(merged.screeningCriteria || '').trim();
105
+ merged.startFrom = String(merged.startFrom || '').trim().toLowerCase() === 'all' ? 'all' : 'unread';
106
+ merged.targetCount = normalizeOptionalPositiveNumber(merged.targetCount, null);
107
+ merged.greetingText = String(merged.greetingText || '').trim() || DEFAULT_GREETING_TEXT;
108
+ merged.jobSelection = normalizeJobSelection(merged.jobSelection);
104
109
  merged.chrome.port = normalizeNumber(merged.chrome.port, DEFAULT_PROFILE.chrome.port);
105
110
  merged.llm.baseUrl = String(merged.llm.baseUrl || '').trim().replace(/\/+$/, '');
106
111
  merged.llm.apiKey = String(merged.llm.apiKey || '').trim();