@reconcrap/boss-recommend-mcp 1.3.8 → 1.3.10
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 +24 -4
- package/package.json +1 -1
- package/skills/boss-chat/README.md +5 -2
- package/skills/boss-chat/SKILL.md +2 -2
- package/src/boss-chat.js +89 -12
- package/src/index.js +62 -35
- package/src/pipeline.js +15 -6
- package/src/test-boss-chat.js +70 -1
- package/src/test-pipeline.js +90 -0
- package/vendor/boss-chat-cli/src/app.js +61 -22
- package/vendor/boss-chat-cli/src/browser/chat-page.js +7 -1
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Boss 推荐页自动化流水线 MCP(stdio)服务。
|
|
4
4
|
|
|
5
|
+
发布入口:
|
|
6
|
+
|
|
7
|
+
- npm: `@reconcrap/boss-recommend-mcp`(https://www.npmjs.com/package/@reconcrap/boss-recommend-mcp)
|
|
8
|
+
- GitHub: `reconcrap-cpu/boss-recommend-mcp`(https://github.com/reconcrap-cpu/boss-recommend-mcp)
|
|
9
|
+
|
|
5
10
|
它把 recommend 页面上的两段能力串起来:
|
|
6
11
|
|
|
7
12
|
- `boss-recommend-search-cli`: 只负责推荐页筛选项
|
|
@@ -79,13 +84,25 @@ MCP 工具:
|
|
|
79
84
|
|
|
80
85
|
## 安装
|
|
81
86
|
|
|
87
|
+
推荐(npm 全局安装):
|
|
88
|
+
|
|
82
89
|
```bash
|
|
83
|
-
npm install
|
|
90
|
+
npm install -g @reconcrap/boss-recommend-mcp@latest
|
|
91
|
+
boss-recommend-mcp start
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
无需安装(npx 直接运行):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx -y @reconcrap/boss-recommend-mcp@latest start
|
|
84
98
|
```
|
|
85
99
|
|
|
86
|
-
|
|
100
|
+
从 GitHub 源码运行(开发/调试):
|
|
87
101
|
|
|
88
102
|
```bash
|
|
103
|
+
git clone https://github.com/reconcrap-cpu/boss-recommend-mcp.git
|
|
104
|
+
cd boss-recommend-mcp
|
|
105
|
+
npm install
|
|
89
106
|
node src/cli.js start
|
|
90
107
|
```
|
|
91
108
|
|
|
@@ -101,8 +118,9 @@ BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列
|
|
|
101
118
|
或使用 CLI fallback:
|
|
102
119
|
|
|
103
120
|
```bash
|
|
104
|
-
node src/cli.js run --instruction "推荐页筛选985男生,近14天没有,有大模型平台经验,符合标准的收藏"
|
|
105
121
|
npx -y @reconcrap/boss-recommend-mcp@latest run --instruction "推荐页筛选985男生,近14天没有,有大模型平台经验,符合标准的收藏"
|
|
122
|
+
# 源码模式(GitHub clone 后)
|
|
123
|
+
node src/cli.js run --instruction "推荐页筛选985男生,近14天没有,有大模型平台经验,符合标准的收藏"
|
|
106
124
|
```
|
|
107
125
|
|
|
108
126
|
## 配置
|
|
@@ -152,6 +170,8 @@ config/screening-config.example.json
|
|
|
152
170
|
|
|
153
171
|
## 常用命令
|
|
154
172
|
|
|
173
|
+
npm 包安装后可直接使用可执行命令 `boss-recommend-mcp`。以下示例展示源码模式(`node src/cli.js`):
|
|
174
|
+
|
|
155
175
|
```bash
|
|
156
176
|
node src/cli.js install --agent trae-cn
|
|
157
177
|
node src/cli.js init-config
|
|
@@ -217,7 +237,7 @@ chat-only 交互建议:
|
|
|
217
237
|
|
|
218
238
|
- 先调用一次 `prepare_boss_chat_run`(可不带参数),服务会先导航到 `https://www.zhipin.com/web/chat/index` 并返回 `NEED_INPUT`,其中包含岗位 `job_options` 与待补字段。
|
|
219
239
|
- 然后基于 `job_options` 让用户选择 `job`,并补齐 `start_from`、`target_count`、`criteria` 后调用 `start_boss_chat_run` 启动任务。
|
|
220
|
-
- `target_count`
|
|
240
|
+
- `target_count` 支持正整数、`all`、`-1`;若用户给出 `全部候选人` / `所有候选人`,会自动按不限(扫到底)处理。
|
|
221
241
|
|
|
222
242
|
Trae-CN / 长对话防循环建议:
|
|
223
243
|
|
package/package.json
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Bundled chat-page automation skill shipped with `boss-recommend-mcp`.
|
|
4
4
|
|
|
5
|
+
Package: `@reconcrap/boss-recommend-mcp` (npm)
|
|
6
|
+
Source: `https://github.com/reconcrap-cpu/boss-recommend-mcp`
|
|
7
|
+
|
|
5
8
|
Use this skill when the user wants a chat-only Boss workflow without installing `boss-chat` separately.
|
|
6
9
|
|
|
7
10
|
## Stable Prompt Template (Trae-CN)
|
|
@@ -27,9 +30,9 @@ Anti-loop rules:
|
|
|
27
30
|
|
|
28
31
|
target_count mapping:
|
|
29
32
|
- Positive integer means explicit cap (for example 20).
|
|
30
|
-
- `all` / `unlimited` / `全部` / `不限` / `扫到底` / `全量` means unlimited.
|
|
33
|
+
- `all` / `-1` / `unlimited` / `全部` / `不限` / `扫到底` / `全量` means unlimited.
|
|
31
34
|
- `全部候选人` / `所有候选人` must also be treated as unlimited.
|
|
32
35
|
- Always write the argument key as `target_count`.
|
|
33
|
-
- For unlimited mode,
|
|
36
|
+
- For unlimited mode, prefer `"target_count": "all"` in the tool call; `-1` is accepted for compatibility and used internally by the CLI.
|
|
34
37
|
- If start_boss_chat_run returns NEED_INPUT for `target_count`, the previous tool call omitted the argument. Retry once using `next_call_example` and include `"target_count": "all"` or a positive integer.
|
|
35
38
|
```
|
|
@@ -40,7 +40,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
|
|
|
40
40
|
`target_count` 填写规则(关键):
|
|
41
41
|
|
|
42
42
|
- 正整数:如 `20`
|
|
43
|
-
- 扫到底:`all` / `unlimited` / `全部` / `不限` / `扫到底` / `全量`
|
|
43
|
+
- 扫到底:`all` / `-1` / `unlimited` / `全部` / `不限` / `扫到底` / `全量`
|
|
44
44
|
- 同义短语也可直接用:`全部候选人` / `所有候选人`(等价于扫到底)
|
|
45
45
|
|
|
46
46
|
## Hard Rules
|
|
@@ -54,7 +54,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
|
|
|
54
54
|
- `target_count` 在 chat-only 启动前也是必填项,不能默认省略。
|
|
55
55
|
- 当用户说“全部候选人/所有候选人”时,必须按“扫到底(unlimited)”处理,不要再追问正整数。
|
|
56
56
|
- 参数名必须写 `target_count`(不要写“目标数量”等中文键名)。
|
|
57
|
-
-
|
|
57
|
+
- 当用户选择“扫到底/全部候选人/所有候选人”时,调用参数优先写:`"target_count": "all"`;`-1` 只作为兼容输入和内部 CLI 表示。
|
|
58
58
|
- 禁止 agent 自行补全 `job/start_from/criteria` 并直接执行,必须由用户明确给出或确认。
|
|
59
59
|
- chat-only 启动流程必须先进入聊天页并拉取岗位列表,再让用户从列表中选择 `job`。
|
|
60
60
|
- 必须先用空参调用 `prepare_boss_chat_run` 获取 `job_options`;不要用 `start_boss_chat_run` 做预备调用。
|
package/src/boss-chat.js
CHANGED
|
@@ -12,6 +12,8 @@ const VENDORED_BOSS_CHAT_DIR = path.join(packageRoot, "vendor", "boss-chat-cli")
|
|
|
12
12
|
const DEFAULT_BOSS_CHAT_POLL_MS = 1500;
|
|
13
13
|
const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
|
|
14
14
|
const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
|
|
15
|
+
export const TARGET_COUNT_ACCEPTED_EXAMPLES = ["all", -1, 20, "全部候选人"];
|
|
16
|
+
const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
|
|
15
17
|
|
|
16
18
|
function normalizeText(value) {
|
|
17
19
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
@@ -34,6 +36,7 @@ function isUnlimitedTargetCountToken(value) {
|
|
|
34
36
|
const token = normalizeText(value).toLowerCase();
|
|
35
37
|
if (!token) return false;
|
|
36
38
|
const compact = token.replace(/\s+/g, "");
|
|
39
|
+
const withoutAnnotation = compact.replace(/[((【[].*?[))】\]]/gu, "");
|
|
37
40
|
const knownTokens = new Set([
|
|
38
41
|
"all",
|
|
39
42
|
"unlimited",
|
|
@@ -52,33 +55,81 @@ function isUnlimitedTargetCountToken(value) {
|
|
|
52
55
|
"所有人选",
|
|
53
56
|
"直到完成所有人选"
|
|
54
57
|
]);
|
|
55
|
-
if (knownTokens.has(token) || knownTokens.has(compact)) return true;
|
|
58
|
+
if (knownTokens.has(token) || knownTokens.has(compact) || knownTokens.has(withoutAnnotation)) return true;
|
|
56
59
|
if (/^(?:all|unlimited|infinity|inf|max|full)(?:candidate|candidates)?$/i.test(compact)) return true;
|
|
60
|
+
if (/^(?:all|unlimited|infinity|inf|max|full)(?:候选人|人选|牛人|人才|人员)?$/iu.test(withoutAnnotation)) return true;
|
|
57
61
|
if (/^(?:全部|所有|全量|不限)(?:候选人|人选|牛人|人才|人员)?$/u.test(compact)) return true;
|
|
62
|
+
if (!/\d/.test(compact) && /(?:扫到底|全部候选人|所有候选人|全部人选|所有人选)/u.test(compact)) return true;
|
|
58
63
|
return false;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
function
|
|
66
|
+
function getWrappedTargetCountValue(value) {
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
68
|
+
for (const key of TARGET_COUNT_WRAPPER_KEYS) {
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
70
|
+
return value[key];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getBossChatTargetCountValue(input = {}) {
|
|
77
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
|
|
78
|
+
if (Object.prototype.hasOwnProperty.call(input, "target_count") && input.target_count !== undefined && input.target_count !== null) {
|
|
79
|
+
return input.target_count;
|
|
80
|
+
}
|
|
81
|
+
if (Object.prototype.hasOwnProperty.call(input, "targetCount") && input.targetCount !== undefined && input.targetCount !== null) {
|
|
82
|
+
return input.targetCount;
|
|
83
|
+
}
|
|
84
|
+
if (Object.prototype.hasOwnProperty.call(input, "target_count")) return input.target_count;
|
|
85
|
+
if (Object.prototype.hasOwnProperty.call(input, "targetCount")) return input.targetCount;
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function cloneForDiagnostics(value) {
|
|
90
|
+
if (value === undefined) return undefined;
|
|
91
|
+
if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(JSON.stringify(value));
|
|
94
|
+
} catch {
|
|
95
|
+
return String(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function normalizeTargetCountInput(value) {
|
|
62
100
|
if (value === undefined || value === null) {
|
|
63
101
|
return {
|
|
64
102
|
provided: false,
|
|
65
103
|
targetCount: null,
|
|
66
|
-
cliArg: null
|
|
104
|
+
cliArg: null,
|
|
105
|
+
publicValue: null,
|
|
106
|
+
rawValue: value,
|
|
107
|
+
parseError: null
|
|
67
108
|
};
|
|
68
109
|
}
|
|
69
|
-
const
|
|
110
|
+
const unwrapped = getWrappedTargetCountValue(value);
|
|
111
|
+
if (unwrapped !== value) {
|
|
112
|
+
return normalizeTargetCountInput(unwrapped);
|
|
113
|
+
}
|
|
114
|
+
const raw = normalizeText(unwrapped);
|
|
70
115
|
if (!raw) {
|
|
71
116
|
return {
|
|
72
117
|
provided: false,
|
|
73
118
|
targetCount: null,
|
|
74
|
-
cliArg: null
|
|
119
|
+
cliArg: null,
|
|
120
|
+
publicValue: null,
|
|
121
|
+
rawValue: value,
|
|
122
|
+
parseError: null
|
|
75
123
|
};
|
|
76
124
|
}
|
|
77
125
|
if (isUnlimitedTargetCountToken(raw)) {
|
|
78
126
|
return {
|
|
79
127
|
provided: true,
|
|
80
128
|
targetCount: null,
|
|
81
|
-
cliArg: "-1"
|
|
129
|
+
cliArg: "-1",
|
|
130
|
+
publicValue: "all",
|
|
131
|
+
rawValue: cloneForDiagnostics(value),
|
|
132
|
+
parseError: null
|
|
82
133
|
};
|
|
83
134
|
}
|
|
84
135
|
const parsed = Number.parseInt(String(raw), 10);
|
|
@@ -86,20 +137,29 @@ function parseBossChatTargetCount(value) {
|
|
|
86
137
|
return {
|
|
87
138
|
provided: true,
|
|
88
139
|
targetCount: null,
|
|
89
|
-
cliArg: "-1"
|
|
140
|
+
cliArg: "-1",
|
|
141
|
+
publicValue: "all",
|
|
142
|
+
rawValue: cloneForDiagnostics(value),
|
|
143
|
+
parseError: null
|
|
90
144
|
};
|
|
91
145
|
}
|
|
92
146
|
if (Number.isFinite(parsed) && parsed > 0) {
|
|
93
147
|
return {
|
|
94
148
|
provided: true,
|
|
95
149
|
targetCount: parsed,
|
|
96
|
-
cliArg: String(parsed)
|
|
150
|
+
cliArg: String(parsed),
|
|
151
|
+
publicValue: parsed,
|
|
152
|
+
rawValue: cloneForDiagnostics(value),
|
|
153
|
+
parseError: null
|
|
97
154
|
};
|
|
98
155
|
}
|
|
99
156
|
return {
|
|
100
157
|
provided: false,
|
|
101
158
|
targetCount: null,
|
|
102
|
-
cliArg: null
|
|
159
|
+
cliArg: null,
|
|
160
|
+
publicValue: null,
|
|
161
|
+
rawValue: cloneForDiagnostics(value),
|
|
162
|
+
parseError: "target_count must be a positive integer, -1, or one of: all, unlimited, 全部, 不限, 扫到底, 全量, 全部候选人, 所有候选人"
|
|
103
163
|
};
|
|
104
164
|
}
|
|
105
165
|
|
|
@@ -225,7 +285,7 @@ function normalizeBossChatStartInput(input = {}) {
|
|
|
225
285
|
const startFromRaw = normalizeText(input.startFrom || input.start_from).toLowerCase();
|
|
226
286
|
const startFrom = startFromRaw === "all" ? "all" : startFromRaw === "unread" ? "unread" : "";
|
|
227
287
|
const criteria = normalizeText(input.criteria);
|
|
228
|
-
const parsedTarget =
|
|
288
|
+
const parsedTarget = normalizeTargetCountInput(getBossChatTargetCountValue(input));
|
|
229
289
|
const port = parsePositiveInteger(input.port);
|
|
230
290
|
return {
|
|
231
291
|
profile,
|
|
@@ -235,6 +295,9 @@ function normalizeBossChatStartInput(input = {}) {
|
|
|
235
295
|
targetCount: parsedTarget.targetCount,
|
|
236
296
|
targetCountArg: parsedTarget.cliArg,
|
|
237
297
|
targetCountProvided: parsedTarget.provided,
|
|
298
|
+
targetCountPublicValue: parsedTarget.publicValue,
|
|
299
|
+
targetCountRawValue: parsedTarget.rawValue,
|
|
300
|
+
targetCountParseError: parsedTarget.parseError,
|
|
238
301
|
port,
|
|
239
302
|
dryRun: input.dryRun === true || input.dry_run === true,
|
|
240
303
|
noState: input.noState === true || input.no_state === true,
|
|
@@ -270,7 +333,7 @@ function buildTargetCountQuestionHint(item = {}) {
|
|
|
270
333
|
{ label: "全部候选人", value: "全部候选人" },
|
|
271
334
|
{ label: "所有候选人", value: "所有候选人" }
|
|
272
335
|
];
|
|
273
|
-
next.examples =
|
|
336
|
+
next.examples = TARGET_COUNT_ACCEPTED_EXAMPLES.slice();
|
|
274
337
|
next.argument_name = "target_count";
|
|
275
338
|
return next;
|
|
276
339
|
}
|
|
@@ -290,13 +353,23 @@ function buildNextCallExample(input = {}, missingFields = []) {
|
|
|
290
353
|
if (normalized.startFrom) sample.start_from = normalized.startFrom;
|
|
291
354
|
if (normalized.criteria) sample.criteria = normalized.criteria;
|
|
292
355
|
if (normalized.targetCountProvided) {
|
|
293
|
-
sample.target_count = normalized.targetCountArg === "-1" ? "all" : normalized.targetCount;
|
|
356
|
+
sample.target_count = normalized.targetCountPublicValue || (normalized.targetCountArg === "-1" ? "all" : normalized.targetCount);
|
|
294
357
|
} else if (missingFields.includes("target_count")) {
|
|
295
358
|
sample.target_count = "all";
|
|
296
359
|
}
|
|
297
360
|
return Object.keys(sample).length > 0 ? sample : null;
|
|
298
361
|
}
|
|
299
362
|
|
|
363
|
+
function buildTargetCountNeedInputDiagnostics(input = {}, missingFields = []) {
|
|
364
|
+
if (!Array.isArray(missingFields) || !missingFields.includes("target_count")) return {};
|
|
365
|
+
const normalized = normalizeBossChatStartInput(input);
|
|
366
|
+
return {
|
|
367
|
+
accepted_examples: TARGET_COUNT_ACCEPTED_EXAMPLES.slice(),
|
|
368
|
+
...(normalized.targetCountRawValue !== undefined ? { received_target_count: normalized.targetCountRawValue } : {}),
|
|
369
|
+
...(normalized.targetCountParseError ? { target_count_parse_error: normalized.targetCountParseError } : {})
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
300
373
|
function buildBossChatCliArgs(command, input, resolvedConfig) {
|
|
301
374
|
const args = [command, "--json"];
|
|
302
375
|
if (command === "prepare-run") {
|
|
@@ -493,12 +566,14 @@ export async function startBossChatRun({ workspaceRoot, input = {} }) {
|
|
|
493
566
|
: [];
|
|
494
567
|
const normalizedPendingQuestions = normalizePendingQuestions(pendingQuestions);
|
|
495
568
|
const nextCallExample = buildNextCallExample(input, missingFields);
|
|
569
|
+
const targetCountDiagnostics = buildTargetCountNeedInputDiagnostics(input, missingFields);
|
|
496
570
|
return {
|
|
497
571
|
...prepared,
|
|
498
572
|
status: "NEED_INPUT",
|
|
499
573
|
required_fields: CHAT_REQUIRED_FIELDS.slice(),
|
|
500
574
|
missing_fields: missingFields,
|
|
501
575
|
pending_questions: normalizedPendingQuestions,
|
|
576
|
+
...targetCountDiagnostics,
|
|
502
577
|
...(nextCallExample ? { next_call_example: nextCallExample } : {}),
|
|
503
578
|
message: prepared?.message
|
|
504
579
|
|| "已获取 Boss 聊天页岗位列表,请先补齐 job / start_from / target_count / criteria。"
|
|
@@ -518,11 +593,13 @@ export async function prepareBossChatRun({ workspaceRoot, input = {} }) {
|
|
|
518
593
|
))
|
|
519
594
|
: [];
|
|
520
595
|
const nextCallExample = buildNextCallExample(input, missingFields);
|
|
596
|
+
const targetCountDiagnostics = buildTargetCountNeedInputDiagnostics(input, missingFields);
|
|
521
597
|
return {
|
|
522
598
|
...payload,
|
|
523
599
|
required_fields: CHAT_REQUIRED_FIELDS.slice(),
|
|
524
600
|
missing_fields: missingFields,
|
|
525
601
|
pending_questions: normalizePendingQuestions(pendingQuestions),
|
|
602
|
+
...targetCountDiagnostics,
|
|
526
603
|
...(nextCallExample ? { next_call_example: nextCallExample } : {})
|
|
527
604
|
};
|
|
528
605
|
}
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
cancelBossChatRun,
|
|
13
13
|
getBossChatHealthCheck,
|
|
14
14
|
getBossChatRun,
|
|
15
|
+
getBossChatTargetCountValue,
|
|
16
|
+
normalizeTargetCountInput,
|
|
15
17
|
pauseBossChatRun,
|
|
16
18
|
prepareBossChatRun,
|
|
17
19
|
resumeBossChatRun,
|
|
@@ -119,6 +121,53 @@ function getLongRunPollAfterSec() {
|
|
|
119
121
|
return Math.max(60, fromEnv);
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
function createTargetCountInputSchema(description) {
|
|
125
|
+
return {
|
|
126
|
+
oneOf: [
|
|
127
|
+
{
|
|
128
|
+
type: "integer",
|
|
129
|
+
minimum: 1
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: "integer",
|
|
133
|
+
enum: [-1]
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: "string",
|
|
137
|
+
enum: ["all", "unlimited", "-1", "全部", "不限", "扫到底", "全量", "全部候选人", "所有候选人"]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
value: {
|
|
143
|
+
oneOf: [
|
|
144
|
+
{ type: "integer", minimum: 1 },
|
|
145
|
+
{ type: "integer", enum: [-1] },
|
|
146
|
+
{ type: "string" }
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
target_count: {
|
|
150
|
+
oneOf: [
|
|
151
|
+
{ type: "integer", minimum: 1 },
|
|
152
|
+
{ type: "integer", enum: [-1] },
|
|
153
|
+
{ type: "string" }
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
targetCount: {
|
|
157
|
+
oneOf: [
|
|
158
|
+
{ type: "integer", minimum: 1 },
|
|
159
|
+
{ type: "integer", enum: [-1] },
|
|
160
|
+
{ type: "string" }
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
additionalProperties: true
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
description
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
122
171
|
function getRecommendedPollAfterSec(args = {}) {
|
|
123
172
|
return hasFollowUpChatRequest(args)
|
|
124
173
|
? getLongRunPollAfterSec()
|
|
@@ -387,18 +436,7 @@ function createRunInputSchema() {
|
|
|
387
436
|
type: "string",
|
|
388
437
|
enum: ["unread", "all"]
|
|
389
438
|
},
|
|
390
|
-
target_count:
|
|
391
|
-
oneOf: [
|
|
392
|
-
{
|
|
393
|
-
type: "integer",
|
|
394
|
-
minimum: 1
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
type: "string",
|
|
398
|
-
enum: ["all", "unlimited", "全部", "不限", "扫到底", "全量", "全部候选人", "所有候选人"]
|
|
399
|
-
}
|
|
400
|
-
]
|
|
401
|
-
},
|
|
439
|
+
target_count: createTargetCountInputSchema("boss-chat follow-up 本次处理人数上限;支持正整数、all 或 -1(扫到底)"),
|
|
402
440
|
dry_run: { type: "boolean" },
|
|
403
441
|
no_state: { type: "boolean" },
|
|
404
442
|
safe_pacing: { type: "boolean" },
|
|
@@ -436,19 +474,8 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
|
|
|
436
474
|
type: "string",
|
|
437
475
|
description: "boss-chat 的筛选 criteria"
|
|
438
476
|
},
|
|
439
|
-
target_count: {
|
|
440
|
-
|
|
441
|
-
{
|
|
442
|
-
type: "integer",
|
|
443
|
-
minimum: 1
|
|
444
|
-
},
|
|
445
|
-
{
|
|
446
|
-
type: "string",
|
|
447
|
-
enum: ["all", "unlimited", "全部", "不限", "扫到底", "全量", "全部候选人", "所有候选人"]
|
|
448
|
-
}
|
|
449
|
-
],
|
|
450
|
-
description: "本次处理人数上限;支持正整数或 all/不限/全部候选人(扫到底)"
|
|
451
|
-
},
|
|
477
|
+
target_count: createTargetCountInputSchema("本次处理人数上限;支持正整数、all 或 -1(扫到底),也兼容 { value: \"all\" } 等包装对象"),
|
|
478
|
+
targetCount: createTargetCountInputSchema("兼容字段;优先使用 target_count。本次处理人数上限,支持正整数、all 或 -1(扫到底)"),
|
|
452
479
|
port: {
|
|
453
480
|
type: "integer",
|
|
454
481
|
minimum: 1,
|
|
@@ -462,7 +489,11 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
|
|
|
462
489
|
additionalProperties: false
|
|
463
490
|
};
|
|
464
491
|
if (requireFullInput) {
|
|
465
|
-
schema.required = ["job", "start_from", "
|
|
492
|
+
schema.required = ["job", "start_from", "criteria"];
|
|
493
|
+
schema.anyOf = [
|
|
494
|
+
{ required: ["target_count"] },
|
|
495
|
+
{ required: ["targetCount"] }
|
|
496
|
+
];
|
|
466
497
|
}
|
|
467
498
|
return schema;
|
|
468
499
|
}
|
|
@@ -727,15 +758,11 @@ function validateBossChatStartArgs(args) {
|
|
|
727
758
|
return "criteria must be a non-empty string when provided";
|
|
728
759
|
}
|
|
729
760
|
}
|
|
730
|
-
if (
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const numericUnlimited = Number.isFinite(targetCount) && targetCount === -1;
|
|
736
|
-
if ((!Number.isFinite(targetCount) || targetCount <= 0) && !tokenAllowed && !numericUnlimited) {
|
|
737
|
-
return "target_count must be a positive integer or one of: all, unlimited, 全部, 不限, 扫到底, 全量, 全部候选人, 所有候选人";
|
|
738
|
-
}
|
|
761
|
+
if (
|
|
762
|
+
Object.prototype.hasOwnProperty.call(args, "target_count")
|
|
763
|
+
|| Object.prototype.hasOwnProperty.call(args, "targetCount")
|
|
764
|
+
) {
|
|
765
|
+
normalizeTargetCountInput(getBossChatTargetCountValue(args));
|
|
739
766
|
}
|
|
740
767
|
if (Object.prototype.hasOwnProperty.call(args, "port")) {
|
|
741
768
|
const port = Number.parseInt(String(args.port), 10);
|
package/src/pipeline.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
cancelBossChatRun,
|
|
18
18
|
getBossChatRun,
|
|
19
|
+
normalizeTargetCountInput,
|
|
19
20
|
pauseBossChatRun,
|
|
20
21
|
resumeBossChatRun,
|
|
21
22
|
startBossChatRun
|
|
@@ -66,6 +67,10 @@ function parsePositiveIntegerValue(value) {
|
|
|
66
67
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function normalizePipelineTargetCountValue(value) {
|
|
71
|
+
return normalizeTargetCountInput(value).publicValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
function sleep(ms) {
|
|
70
75
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
76
|
}
|
|
@@ -388,16 +393,17 @@ function normalizeFollowUpChatInput(followUp = null, defaults = null) {
|
|
|
388
393
|
const defaultCriteria = normalizeText(defaults?.criteria || "");
|
|
389
394
|
const defaultStartFromRaw = normalizeText(defaults?.start_from || "").toLowerCase();
|
|
390
395
|
const defaultStartFrom = defaultStartFromRaw === "all" ? "all" : "unread";
|
|
391
|
-
const defaultTargetCount =
|
|
396
|
+
const defaultTargetCount = normalizePipelineTargetCountValue(defaults?.target_count);
|
|
392
397
|
|
|
393
398
|
const explicitCriteria = normalizeText(raw.criteria);
|
|
394
399
|
const explicitStartFromRaw = normalizeText(raw.start_from).toLowerCase();
|
|
395
400
|
const explicitStartFrom = explicitStartFromRaw === "all" ? "all" : explicitStartFromRaw === "unread" ? "unread" : "";
|
|
396
|
-
const
|
|
401
|
+
const explicitTarget = normalizeTargetCountInput(raw.target_count);
|
|
402
|
+
const explicitTargetCount = explicitTarget.publicValue;
|
|
397
403
|
|
|
398
404
|
const hasExplicitCriteria = Boolean(explicitCriteria);
|
|
399
405
|
const hasExplicitStartFrom = Boolean(explicitStartFrom);
|
|
400
|
-
const hasExplicitTargetCount =
|
|
406
|
+
const hasExplicitTargetCount = explicitTarget.provided;
|
|
401
407
|
|
|
402
408
|
const criteria = explicitCriteria || defaultCriteria;
|
|
403
409
|
const startFrom = explicitStartFrom || defaultStartFrom;
|
|
@@ -442,8 +448,11 @@ function normalizeFollowUpChatInput(followUp = null, defaults = null) {
|
|
|
442
448
|
missing_fields.push("follow_up.chat.target_count");
|
|
443
449
|
pending_questions.push({
|
|
444
450
|
field: "follow_up.chat.target_count",
|
|
445
|
-
question: "请填写 boss-chat follow-up
|
|
446
|
-
value: summary.target_count
|
|
451
|
+
question: "请填写 boss-chat follow-up 本次处理人数上限(正整数,或 all/-1 表示扫到底,必填)。",
|
|
452
|
+
value: summary.target_count,
|
|
453
|
+
accepted_examples: ["all", -1, 20, "全部候选人"],
|
|
454
|
+
...(explicitTarget.rawValue !== undefined ? { received_target_count: explicitTarget.rawValue } : {}),
|
|
455
|
+
...(explicitTarget.parseError ? { target_count_parse_error: explicitTarget.parseError } : {})
|
|
447
456
|
});
|
|
448
457
|
}
|
|
449
458
|
|
|
@@ -523,7 +532,7 @@ function buildBossChatFollowUpStatus({ payload, runId, fallbackInput = null, sta
|
|
|
523
532
|
job: normalizeText(fallbackInput?.job) || null,
|
|
524
533
|
start_from: normalizeText(fallbackInput?.start_from) || null,
|
|
525
534
|
criteria: normalizeText(fallbackInput?.criteria) || null,
|
|
526
|
-
target_count:
|
|
535
|
+
target_count: normalizePipelineTargetCountValue(fallbackInput?.target_count),
|
|
527
536
|
port: parsePositiveIntegerValue(fallbackInput?.port),
|
|
528
537
|
progress: {
|
|
529
538
|
inspected: Number.isInteger(progress.inspected) ? progress.inspected : 0,
|
package/src/test-boss-chat.js
CHANGED
|
@@ -273,6 +273,51 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
|
|
|
273
273
|
const stateAfterStartAll = readStubState(workspaceRoot);
|
|
274
274
|
assert.equal(stateAfterStartAll.last_start_args.targetCount, "-1");
|
|
275
275
|
|
|
276
|
+
for (const target_count of ["all", -1, "-1", { value: "all" }, "all(扫到底)"]) {
|
|
277
|
+
const startedVariant = await startBossChatRun({
|
|
278
|
+
workspaceRoot,
|
|
279
|
+
input: {
|
|
280
|
+
profile: "default",
|
|
281
|
+
job: "算法工程师",
|
|
282
|
+
start_from: "all",
|
|
283
|
+
criteria: "全部候选人都过一遍",
|
|
284
|
+
target_count
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
assert.equal(startedVariant.status, "ACCEPTED");
|
|
288
|
+
assert.equal(readStubState(workspaceRoot).last_start_args.targetCount, "-1");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const startedCamelCase = await startBossChatRun({
|
|
292
|
+
workspaceRoot,
|
|
293
|
+
input: {
|
|
294
|
+
profile: "default",
|
|
295
|
+
job: "算法工程师",
|
|
296
|
+
start_from: "all",
|
|
297
|
+
criteria: "全部候选人都过一遍",
|
|
298
|
+
targetCount: { targetCount: "all" }
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
assert.equal(startedCamelCase.status, "ACCEPTED");
|
|
302
|
+
assert.equal(readStubState(workspaceRoot).last_start_args.targetCount, "-1");
|
|
303
|
+
|
|
304
|
+
const invalidTarget = await startBossChatRun({
|
|
305
|
+
workspaceRoot,
|
|
306
|
+
input: {
|
|
307
|
+
profile: "default",
|
|
308
|
+
job: "算法工程师",
|
|
309
|
+
start_from: "all",
|
|
310
|
+
criteria: "全部候选人都过一遍",
|
|
311
|
+
target_count: "not a target"
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
assert.equal(invalidTarget.status, "NEED_INPUT");
|
|
315
|
+
assert.deepEqual(invalidTarget.missing_fields, ["target_count"]);
|
|
316
|
+
assert.equal(invalidTarget.received_target_count, "not a target");
|
|
317
|
+
assert.equal(Boolean(invalidTarget.target_count_parse_error), true);
|
|
318
|
+
assert.equal(invalidTarget.next_call_example.target_count, "all");
|
|
319
|
+
assert.equal(invalidTarget.accepted_examples.includes("all"), true);
|
|
320
|
+
|
|
276
321
|
const running = await getBossChatRun({
|
|
277
322
|
workspaceRoot,
|
|
278
323
|
input: {
|
|
@@ -323,7 +368,9 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
|
|
|
323
368
|
const prepareToolSchema = tools.find((item) => item.name === TOOL_BOSS_CHAT_PREPARE_RUN).inputSchema;
|
|
324
369
|
const startToolSchema = tools.find((item) => item.name === TOOL_BOSS_CHAT_START_RUN).inputSchema;
|
|
325
370
|
assert.equal(prepareToolSchema.required, undefined);
|
|
326
|
-
assert.deepEqual(startToolSchema.required, ["job", "start_from", "
|
|
371
|
+
assert.deepEqual(startToolSchema.required, ["job", "start_from", "criteria"]);
|
|
372
|
+
assert.equal(startToolSchema.anyOf.some((item) => item.required?.includes("target_count")), true);
|
|
373
|
+
assert.equal(startToolSchema.anyOf.some((item) => item.required?.includes("targetCount")), true);
|
|
327
374
|
|
|
328
375
|
const prepared = await callTool(workspaceRoot, TOOL_BOSS_CHAT_PREPARE_RUN, {}, 101);
|
|
329
376
|
assert.equal(prepared.status, "NEED_INPUT");
|
|
@@ -348,6 +395,19 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
|
|
|
348
395
|
assert.equal(missingTargetOnly.status, "NEED_INPUT");
|
|
349
396
|
assert.deepEqual(missingTargetOnly.missing_fields, ["target_count"]);
|
|
350
397
|
assert.equal(missingTargetOnly.next_call_example.target_count, "all");
|
|
398
|
+
assert.equal(missingTargetOnly.accepted_examples.includes(-1), true);
|
|
399
|
+
|
|
400
|
+
const invalidTargetOnly = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
|
|
401
|
+
job: "算法工程师",
|
|
402
|
+
start_from: "all",
|
|
403
|
+
criteria: "全部候选人都过一遍",
|
|
404
|
+
target_count: "not a target"
|
|
405
|
+
}, 112);
|
|
406
|
+
assert.equal(invalidTargetOnly.status, "NEED_INPUT");
|
|
407
|
+
assert.deepEqual(invalidTargetOnly.missing_fields, ["target_count"]);
|
|
408
|
+
assert.equal(invalidTargetOnly.received_target_count, "not a target");
|
|
409
|
+
assert.equal(Boolean(invalidTargetOnly.target_count_parse_error), true);
|
|
410
|
+
assert.equal(invalidTargetOnly.next_call_example.target_count, "all");
|
|
351
411
|
|
|
352
412
|
const invalidStartResponse = await handleRequest(
|
|
353
413
|
makeToolCall(11, TOOL_BOSS_CHAT_START_RUN, {
|
|
@@ -384,6 +444,15 @@ async function testBossChatMcpToolsShouldValidateAndRoute() {
|
|
|
384
444
|
const stateAfterStartAll = readStubState(workspaceRoot);
|
|
385
445
|
assert.equal(stateAfterStartAll.last_start_args.targetCount, "-1");
|
|
386
446
|
|
|
447
|
+
const startedCamelCase = await callTool(workspaceRoot, TOOL_BOSS_CHAT_START_RUN, {
|
|
448
|
+
job: "算法工程师",
|
|
449
|
+
start_from: "all",
|
|
450
|
+
criteria: "全部候选人都过一遍",
|
|
451
|
+
targetCount: "all"
|
|
452
|
+
}, 141);
|
|
453
|
+
assert.equal(startedCamelCase.status, "ACCEPTED");
|
|
454
|
+
assert.equal(readStubState(workspaceRoot).last_start_args.targetCount, "-1");
|
|
455
|
+
|
|
387
456
|
const running = await callTool(workspaceRoot, TOOL_BOSS_CHAT_GET_RUN, {
|
|
388
457
|
run_id: started.run_id,
|
|
389
458
|
profile: "default"
|
package/src/test-pipeline.js
CHANGED
|
@@ -2224,6 +2224,94 @@ async function testFollowUpChatMissingTargetCountShouldNeedInput() {
|
|
|
2224
2224
|
assert.equal(result.pending_questions.some((item) => item.field === "follow_up.chat.target_count"), true);
|
|
2225
2225
|
}
|
|
2226
2226
|
|
|
2227
|
+
async function testFollowUpChatInvalidTargetCountShouldNeedInputWithDiagnostics() {
|
|
2228
|
+
const result = await runRecommendPipeline(
|
|
2229
|
+
{
|
|
2230
|
+
workspaceRoot: process.cwd(),
|
|
2231
|
+
instruction: "test",
|
|
2232
|
+
confirmation: {},
|
|
2233
|
+
overrides: {},
|
|
2234
|
+
followUp: createFollowUpChat({ target_count: "not a target" })
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
parseRecommendInstruction: () => createParsed()
|
|
2238
|
+
}
|
|
2239
|
+
);
|
|
2240
|
+
|
|
2241
|
+
assert.equal(result.status, "NEED_INPUT");
|
|
2242
|
+
assert.equal(result.missing_fields.includes("follow_up.chat.target_count"), true);
|
|
2243
|
+
const targetQuestion = result.pending_questions.find((item) => item.field === "follow_up.chat.target_count");
|
|
2244
|
+
assert.equal(targetQuestion?.received_target_count, "not a target");
|
|
2245
|
+
assert.equal(Boolean(targetQuestion?.target_count_parse_error), true);
|
|
2246
|
+
assert.equal(targetQuestion?.accepted_examples.includes("all"), true);
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
async function testFollowUpChatAllTargetCountShouldLaunchUnlimited() {
|
|
2250
|
+
let capturedChatInput = null;
|
|
2251
|
+
const result = await runRecommendPipeline(
|
|
2252
|
+
{
|
|
2253
|
+
workspaceRoot: process.cwd(),
|
|
2254
|
+
instruction: "test",
|
|
2255
|
+
confirmation: createJobConfirmedConfirmation(),
|
|
2256
|
+
overrides: {},
|
|
2257
|
+
followUp: createFollowUpChat({ target_count: "all" })
|
|
2258
|
+
},
|
|
2259
|
+
{
|
|
2260
|
+
parseRecommendInstruction: () => createParsed(),
|
|
2261
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9555 }),
|
|
2262
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
2263
|
+
listRecommendJobs: async () => createJobListResult(),
|
|
2264
|
+
readRecommendTabState: async () => ({ ok: true, active_tab_status: "0" }),
|
|
2265
|
+
switchRecommendTab: async () => ({ ok: true, state: "TAB_READY" }),
|
|
2266
|
+
runRecommendSearchCli: async () => ({
|
|
2267
|
+
ok: true,
|
|
2268
|
+
summary: {
|
|
2269
|
+
candidate_count: 8,
|
|
2270
|
+
applied_filters: { degree: ["本科"] },
|
|
2271
|
+
selected_job: DEFAULT_JOB_OPTIONS[0]
|
|
2272
|
+
}
|
|
2273
|
+
}),
|
|
2274
|
+
runRecommendScreenCli: async () => ({
|
|
2275
|
+
ok: true,
|
|
2276
|
+
summary: {
|
|
2277
|
+
processed_count: 6,
|
|
2278
|
+
passed_count: 2,
|
|
2279
|
+
skipped_count: 4,
|
|
2280
|
+
output_csv: "C:/temp/recommend.csv",
|
|
2281
|
+
completion_reason: "screen_completed"
|
|
2282
|
+
}
|
|
2283
|
+
}),
|
|
2284
|
+
startBossChatRun: async ({ input }) => {
|
|
2285
|
+
capturedChatInput = input;
|
|
2286
|
+
return {
|
|
2287
|
+
status: "ACCEPTED",
|
|
2288
|
+
run_id: "chat-run-unlimited",
|
|
2289
|
+
message: "chat started"
|
|
2290
|
+
};
|
|
2291
|
+
},
|
|
2292
|
+
getBossChatRun: async () => ({
|
|
2293
|
+
status: "RUN_STATUS",
|
|
2294
|
+
run: {
|
|
2295
|
+
runId: "chat-run-unlimited",
|
|
2296
|
+
state: "completed",
|
|
2297
|
+
lastMessage: "chat completed",
|
|
2298
|
+
progress: {
|
|
2299
|
+
inspected: 3,
|
|
2300
|
+
passed: 1,
|
|
2301
|
+
requested: 1,
|
|
2302
|
+
skipped: 2,
|
|
2303
|
+
errors: 0
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
})
|
|
2307
|
+
}
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2310
|
+
assert.equal(result.status, "COMPLETED");
|
|
2311
|
+
assert.equal(capturedChatInput.target_count, "all");
|
|
2312
|
+
assert.equal(result.follow_up?.chat?.target_count, "all");
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2227
2315
|
async function testFinalReviewShouldIncludeFollowUpChatSummary() {
|
|
2228
2316
|
const result = await runRecommendPipeline(
|
|
2229
2317
|
{
|
|
@@ -2479,8 +2567,10 @@ async function main() {
|
|
|
2479
2567
|
await testFollowUpChatMissingFieldsShouldExposeRecommendDefaults();
|
|
2480
2568
|
await testFollowUpChatMissingStartFromShouldNeedInput();
|
|
2481
2569
|
await testFollowUpChatMissingTargetCountShouldNeedInput();
|
|
2570
|
+
await testFollowUpChatInvalidTargetCountShouldNeedInputWithDiagnostics();
|
|
2482
2571
|
await testFinalReviewShouldIncludeFollowUpChatSummary();
|
|
2483
2572
|
await testCompletedPipelineShouldRunChatFollowUp();
|
|
2573
|
+
await testFollowUpChatAllTargetCountShouldLaunchUnlimited();
|
|
2484
2574
|
await testCompletedPipelineShouldFailWhenChatLaunchFails();
|
|
2485
2575
|
await testCompletedPipelineShouldFailWhenChatRunFails();
|
|
2486
2576
|
console.log("pipeline tests passed");
|
|
@@ -57,6 +57,13 @@ function shouldContinue(summary, targetCount) {
|
|
|
57
57
|
return summary.inspected < targetCount;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function hasResumeRequestSentMessage(state = {}) {
|
|
61
|
+
const lastText = normalizeText(state?.lastText || '');
|
|
62
|
+
const recent = Array.isArray(state?.recent) ? state.recent : [];
|
|
63
|
+
if (lastText.includes('简历请求已发送')) return true;
|
|
64
|
+
return recent.some((item) => normalizeText(item).includes('简历请求已发送'));
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
export class BossChatApp {
|
|
61
68
|
constructor({
|
|
62
69
|
page,
|
|
@@ -691,19 +698,30 @@ export class BossChatApp {
|
|
|
691
698
|
await this.interaction.sleepRange(360, 120);
|
|
692
699
|
|
|
693
700
|
this.logger.log('候选人通过,执行求简历动作。');
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
const askResult = await this.page.clickAskResume();
|
|
700
|
-
await this.interaction.sleepRange(460, 150);
|
|
701
|
-
if (askResult?.alreadyRequested) {
|
|
702
|
-
baseResult.requested = true;
|
|
703
|
-
this.logger.log('求简历动作完成:alreadyRequested=true');
|
|
704
|
-
} else {
|
|
701
|
+
const maxRequestAttempts = 3;
|
|
702
|
+
let requestSucceeded = false;
|
|
703
|
+
let lastAttempt = null;
|
|
704
|
+
|
|
705
|
+
for (let requestAttempt = 0; requestAttempt < maxRequestAttempts; requestAttempt += 1) {
|
|
705
706
|
await this.checkpoint();
|
|
706
|
-
const
|
|
707
|
+
const messageBefore =
|
|
708
|
+
typeof this.page.getResumeRequestMessageState === 'function'
|
|
709
|
+
? await this.page.getResumeRequestMessageState()
|
|
710
|
+
: { ok: false, count: 0, lastText: '', recent: [] };
|
|
711
|
+
const askResult = await this.page.clickAskResume();
|
|
712
|
+
await this.interaction.sleepRange(460, 150);
|
|
713
|
+
|
|
714
|
+
let confirmResult = {
|
|
715
|
+
confirmed: false,
|
|
716
|
+
requestedVerified: false,
|
|
717
|
+
assumedRequested: false,
|
|
718
|
+
uiState: null,
|
|
719
|
+
};
|
|
720
|
+
if (!askResult?.alreadyRequested) {
|
|
721
|
+
await this.checkpoint();
|
|
722
|
+
confirmResult = await this.page.clickConfirmRequestResume();
|
|
723
|
+
}
|
|
724
|
+
|
|
707
725
|
let messageObserved = false;
|
|
708
726
|
let messageAfter = null;
|
|
709
727
|
if (typeof this.page.waitForResumeRequestMessage === 'function') {
|
|
@@ -712,29 +730,50 @@ export class BossChatApp {
|
|
|
712
730
|
timeoutMs: 7000,
|
|
713
731
|
pollMs: 260,
|
|
714
732
|
});
|
|
715
|
-
messageObserved = Boolean(messageCheck?.observed);
|
|
716
733
|
messageAfter = messageCheck?.state || null;
|
|
734
|
+
messageObserved = Boolean(messageCheck?.observed) || hasResumeRequestSentMessage(messageAfter || {});
|
|
717
735
|
}
|
|
718
736
|
|
|
719
|
-
const requestedVerified =
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
737
|
+
const requestedVerified = Boolean(messageObserved);
|
|
738
|
+
lastAttempt = {
|
|
739
|
+
attempt: requestAttempt + 1,
|
|
740
|
+
askResult,
|
|
741
|
+
confirmResult,
|
|
742
|
+
messageBefore,
|
|
743
|
+
messageAfter,
|
|
744
|
+
messageObserved,
|
|
745
|
+
requestedVerified,
|
|
746
|
+
};
|
|
747
|
+
|
|
723
748
|
if (messageAfter) {
|
|
724
749
|
baseResult.artifacts.resumeRequestMessageBefore = Number(messageBefore?.count || 0);
|
|
725
750
|
baseResult.artifacts.resumeRequestMessageAfter = Number(messageAfter?.count || 0);
|
|
726
751
|
baseResult.artifacts.resumeRequestMessageObserved = messageObserved;
|
|
727
752
|
baseResult.artifacts.resumeRequestMessageLastText = String(messageAfter?.lastText || '');
|
|
728
753
|
}
|
|
754
|
+
|
|
729
755
|
this.logger.log(
|
|
730
|
-
|
|
756
|
+
`求简历动作检查:attempt=${requestAttempt + 1}/${maxRequestAttempts} | alreadyRequested=${Boolean(askResult?.alreadyRequested)} | confirmed=${Boolean(confirmResult?.confirmed)} | disabledOperateAsk=${Boolean(confirmResult?.uiState?.hasDisabledOperateAsk)} | messageObserved=${messageObserved} | verified=${requestedVerified} | assumed=${Boolean(confirmResult?.assumedRequested)}`,
|
|
731
757
|
);
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
758
|
+
|
|
759
|
+
if (requestedVerified) {
|
|
760
|
+
requestSucceeded = true;
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (requestAttempt < maxRequestAttempts - 1) {
|
|
765
|
+
this.logger.log('未检测到“简历请求已发送”提示,重新发起求简历。');
|
|
766
|
+
await this.interaction.sleepRange(640, 180);
|
|
736
767
|
}
|
|
737
768
|
}
|
|
769
|
+
|
|
770
|
+
baseResult.requested = requestSucceeded;
|
|
771
|
+
if (!requestSucceeded) {
|
|
772
|
+
const confirmStateText = JSON.stringify(lastAttempt?.confirmResult?.uiState || {});
|
|
773
|
+
throw new Error(
|
|
774
|
+
`REQUEST_RESUME_MESSAGE_NOT_OBSERVED(state=${confirmStateText},messageBefore=${Number(lastAttempt?.messageBefore?.count || 0)},messageAfter=${Number(lastAttempt?.messageAfter?.count || 0)},attempts=${maxRequestAttempts})`,
|
|
775
|
+
);
|
|
776
|
+
}
|
|
738
777
|
}
|
|
739
778
|
|
|
740
779
|
await this.stateStore.record(baseResult.customerKey, baseResult, baseAliases);
|
|
@@ -2756,10 +2756,16 @@ export class BossChatPage {
|
|
|
2756
2756
|
async waitForResumeRequestMessage({ baselineCount = 0, timeoutMs = 6500, pollMs = 260 } = {}) {
|
|
2757
2757
|
const start = Date.now();
|
|
2758
2758
|
let latest = null;
|
|
2759
|
+
const hasSentMessage = (state = {}) => {
|
|
2760
|
+
const lastText = String(state?.lastText || '');
|
|
2761
|
+
const recent = Array.isArray(state?.recent) ? state.recent : [];
|
|
2762
|
+
if (lastText.includes('简历请求已发送')) return true;
|
|
2763
|
+
return recent.some((item) => String(item || '').includes('简历请求已发送'));
|
|
2764
|
+
};
|
|
2759
2765
|
while (Date.now() - start < timeoutMs) {
|
|
2760
2766
|
const state = await this.getResumeRequestMessageState();
|
|
2761
2767
|
latest = state;
|
|
2762
|
-
if (state.count > baselineCount) {
|
|
2768
|
+
if (state.count > baselineCount || hasSentMessage(state)) {
|
|
2763
2769
|
return {
|
|
2764
2770
|
observed: true,
|
|
2765
2771
|
state,
|