@reconcrap/boss-recommend-mcp 1.2.5 → 1.2.7

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/src/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  runRecommendCalibration
10
10
  } from "./adapters.js";
11
11
  import { runRecommendPipeline } from "./pipeline.js";
12
+ import { runRecommendSelfHeal } from "./self-heal.js";
12
13
  import {
13
14
  RUN_MODE_ASYNC,
14
15
  RUN_STAGE_PREFLIGHT,
@@ -39,6 +40,7 @@ const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
39
40
  const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
40
41
  const TOOL_RUN_FEATURED_CALIBRATION = "run_featured_calibration";
41
42
  const TOOL_GET_FEATURED_CALIBRATION_STATUS = "get_featured_calibration_status";
43
+ const TOOL_RUN_RECOMMEND_SELF_HEAL = "run_recommend_self_heal";
42
44
 
43
45
  const SERVER_NAME = "boss-recommend-mcp";
44
46
  const FRAMING_UNKNOWN = "unknown";
@@ -49,6 +51,7 @@ const DETACHED_WORKER_RUN_ID_FLAG = "--run-id";
49
51
  const DETACHED_WORKER_RESUME_FLAG = "--resume";
50
52
 
51
53
  let runPipelineImpl = runRecommendPipeline;
54
+ let runSelfHealImpl = runRecommendSelfHeal;
52
55
  let spawnProcessImpl = spawn;
53
56
  const TERMINAL_RUN_STATES = new Set([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED]);
54
57
 
@@ -321,6 +324,43 @@ function createRunFeaturedCalibrationInputSchema() {
321
324
  };
322
325
  }
323
326
 
327
+ function createRunRecommendSelfHealInputSchema() {
328
+ return {
329
+ type: "object",
330
+ properties: {
331
+ mode: {
332
+ type: "string",
333
+ enum: ["scan", "apply"],
334
+ description: "scan=扫描漂移;apply=按 repair_session_id 应用高置信度修复"
335
+ },
336
+ scope: {
337
+ type: "string",
338
+ enum: ["full", "search_screen", "selectors_only"],
339
+ description: "扫描范围,默认 full"
340
+ },
341
+ validation_profile: {
342
+ type: "string",
343
+ enum: ["safe", "full"],
344
+ description: "校验强度,默认 full"
345
+ },
346
+ port: {
347
+ type: "integer",
348
+ minimum: 1,
349
+ description: "可选,Boss Chrome 远程调试端口"
350
+ },
351
+ repair_session_id: {
352
+ type: "string",
353
+ description: "apply 模式必填,来自 scan 返回值"
354
+ },
355
+ confirm_apply: {
356
+ type: "boolean",
357
+ description: "apply 模式必填,必须显式传 true"
358
+ }
359
+ },
360
+ additionalProperties: false
361
+ };
362
+ }
363
+
324
364
  function createToolsSchema() {
325
365
  return [
326
366
  {
@@ -389,6 +429,11 @@ function createToolsSchema() {
389
429
  properties: {},
390
430
  additionalProperties: false
391
431
  }
432
+ },
433
+ {
434
+ name: TOOL_RUN_RECOMMEND_SELF_HEAL,
435
+ description: "手动运维自愈工具:扫描 Boss recommend 相关 selector / network 规则漂移,并在确认后应用高置信度修复。",
436
+ inputSchema: createRunRecommendSelfHealInputSchema()
392
437
  }
393
438
  ];
394
439
  }
@@ -448,6 +493,42 @@ function validateRunFeaturedCalibrationArgs(args) {
448
493
  return null;
449
494
  }
450
495
 
496
+ function validateRunRecommendSelfHealArgs(args) {
497
+ if (!args || typeof args !== "object" || Array.isArray(args)) {
498
+ return "arguments must be an object";
499
+ }
500
+
501
+ if (Object.prototype.hasOwnProperty.call(args, "mode")) {
502
+ const mode = normalizeText(args.mode).toLowerCase();
503
+ if (!["scan", "apply"].includes(mode)) {
504
+ return "mode must be one of: scan, apply";
505
+ }
506
+ }
507
+
508
+ if (Object.prototype.hasOwnProperty.call(args, "scope")) {
509
+ const scope = normalizeText(args.scope).toLowerCase();
510
+ if (!["full", "search_screen", "selectors_only"].includes(scope)) {
511
+ return "scope must be one of: full, search_screen, selectors_only";
512
+ }
513
+ }
514
+
515
+ if (Object.prototype.hasOwnProperty.call(args, "validation_profile")) {
516
+ const profile = normalizeText(args.validation_profile).toLowerCase();
517
+ if (!["safe", "full"].includes(profile)) {
518
+ return "validation_profile must be one of: safe, full";
519
+ }
520
+ }
521
+
522
+ if (Object.prototype.hasOwnProperty.call(args, "port")) {
523
+ const port = Number.parseInt(String(args.port), 10);
524
+ if (!Number.isFinite(port) || port <= 0) {
525
+ return "port must be a positive integer";
526
+ }
527
+ }
528
+
529
+ return null;
530
+ }
531
+
451
532
  function getLastOutputLine(text) {
452
533
  const lines = String(text || "")
453
534
  .split(/\r?\n/)
@@ -1340,6 +1421,10 @@ async function handleRunFeaturedCalibrationTool({ workspaceRoot, args }) {
1340
1421
  };
1341
1422
  }
1342
1423
 
1424
+ async function handleRunRecommendSelfHealTool({ workspaceRoot, args }) {
1425
+ return runSelfHealImpl({ workspaceRoot, args });
1426
+ }
1427
+
1343
1428
  async function handleRequest(message, workspaceRoot) {
1344
1429
  if (!message || message.jsonrpc !== "2.0") {
1345
1430
  return createJsonRpcError(null, -32600, "Invalid JSON-RPC request");
@@ -1396,6 +1481,13 @@ async function handleRequest(message, workspaceRoot) {
1396
1481
  }
1397
1482
  }
1398
1483
 
1484
+ if (toolName === TOOL_RUN_RECOMMEND_SELF_HEAL) {
1485
+ const inputError = validateRunRecommendSelfHealArgs(args);
1486
+ if (inputError) {
1487
+ return createJsonRpcError(id, -32602, inputError);
1488
+ }
1489
+ }
1490
+
1399
1491
  if ([TOOL_GET_RUN, TOOL_CANCEL_RUN, TOOL_PAUSE_RUN, TOOL_RESUME_RUN].includes(toolName)) {
1400
1492
  if (!args || typeof args.run_id !== "string" || !normalizeText(args.run_id)) {
1401
1493
  return createJsonRpcError(id, -32602, "run_id is required and must be a string");
@@ -1418,6 +1510,8 @@ async function handleRequest(message, workspaceRoot) {
1418
1510
  payload = handleGetFeaturedCalibrationStatusTool(workspaceRoot);
1419
1511
  } else if (toolName === TOOL_RUN_FEATURED_CALIBRATION) {
1420
1512
  payload = await handleRunFeaturedCalibrationTool({ workspaceRoot, args });
1513
+ } else if (toolName === TOOL_RUN_RECOMMEND_SELF_HEAL) {
1514
+ payload = await handleRunRecommendSelfHealTool({ workspaceRoot, args });
1421
1515
  } else {
1422
1516
  return createJsonRpcError(id, -32602, `Unknown tool: ${toolName || ""}`);
1423
1517
  }
@@ -1553,6 +1647,9 @@ export const __testables = {
1553
1647
  },
1554
1648
  setRunPipelineImplForTests(nextImpl) {
1555
1649
  runPipelineImpl = typeof nextImpl === "function" ? nextImpl : runRecommendPipeline;
1650
+ },
1651
+ setRunSelfHealImplForTests(nextImpl) {
1652
+ runSelfHealImpl = typeof nextImpl === "function" ? nextImpl : runRecommendSelfHeal;
1556
1653
  }
1557
1654
  };
1558
1655
 
package/src/pipeline.js CHANGED
@@ -16,6 +16,8 @@ import {
16
16
 
17
17
  const FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY = "近14天没有";
18
18
  const MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS = 5;
19
+ const MAX_SEARCH_NO_IFRAME_RETRY_ATTEMPTS = 1;
20
+ const SEARCH_NO_IFRAME_RETRY_DELAY_MS = 1200;
19
21
  const PAGE_SCOPE_TO_TAB_STATUS = {
20
22
  recommend: "0",
21
23
  latest: "1",
@@ -40,6 +42,10 @@ function normalizeText(value) {
40
42
  return String(value || "").replace(/\s+/g, " ").trim();
41
43
  }
42
44
 
45
+ function sleep(ms) {
46
+ return new Promise((resolve) => setTimeout(resolve, ms));
47
+ }
48
+
43
49
  function normalizePageScope(value) {
44
50
  const normalized = normalizeText(value).toLowerCase();
45
51
  if (!normalized) return null;
@@ -886,6 +892,7 @@ export async function runRecommendPipeline(
886
892
  let shouldRunSearch = !skipSearchOnResume;
887
893
  let screenAutoRecoveryCount = 0;
888
894
  let lastAutoRecovery = null;
895
+ let searchNoIframeRetryCount = 0;
889
896
  let activeTabStatus = null;
890
897
  let currentResumeConfig = {
891
898
  checkpoint_path: resume?.checkpoint_path || null,
@@ -971,11 +978,14 @@ export async function runRecommendPipeline(
971
978
  if (!searchResult.ok) {
972
979
  const searchErrorCode = String(searchResult.error?.code || "");
973
980
  const searchErrorMessage = String(searchResult.error?.message || "");
981
+ const isNoIframeSearchFailure = (
982
+ searchErrorCode === "NO_RECOMMEND_IFRAME"
983
+ || searchErrorMessage.includes("NO_RECOMMEND_IFRAME")
984
+ );
974
985
  const loginRelatedSearchFailure = (
975
986
  searchErrorCode === "LOGIN_REQUIRED"
976
- || searchErrorCode === "NO_RECOMMEND_IFRAME"
987
+ || isNoIframeSearchFailure
977
988
  || searchErrorMessage.includes("LOGIN_REQUIRED")
978
- || searchErrorMessage.includes("NO_RECOMMEND_IFRAME")
979
989
  );
980
990
  if (loginRelatedSearchFailure) {
981
991
  const recheck = await ensureRecommendPageReady(workspaceRoot, {
@@ -1006,6 +1016,28 @@ export async function runRecommendPipeline(
1006
1016
  }
1007
1017
  );
1008
1018
  }
1019
+ if (
1020
+ isNoIframeSearchFailure
1021
+ && recheck.state === "RECOMMEND_READY"
1022
+ && searchNoIframeRetryCount < MAX_SEARCH_NO_IFRAME_RETRY_ATTEMPTS
1023
+ ) {
1024
+ searchNoIframeRetryCount += 1;
1025
+ const retryDelayMs = SEARCH_NO_IFRAME_RETRY_DELAY_MS;
1026
+ const retryDiagnostics = {
1027
+ trigger: "NO_RECOMMEND_IFRAME",
1028
+ attempt: searchNoIframeRetryCount,
1029
+ max_attempts: MAX_SEARCH_NO_IFRAME_RETRY_ATTEMPTS,
1030
+ delay_ms: retryDelayMs,
1031
+ page_state: recheck.page_state || null
1032
+ };
1033
+ runtimeHooks.setStage(
1034
+ "search_recovery",
1035
+ `检测到 recommend iframe 暂未就绪,等待 ${Math.round(retryDelayMs / 1000)} 秒后重试 search(第 ${searchNoIframeRetryCount}/${MAX_SEARCH_NO_IFRAME_RETRY_ATTEMPTS} 次)。`
1036
+ );
1037
+ runtimeHooks.heartbeat("search_recovery", retryDiagnostics);
1038
+ await sleep(retryDelayMs);
1039
+ continue;
1040
+ }
1009
1041
  }
1010
1042
  return buildFailedResponse(
1011
1043
  searchResult.error?.code || "RECOMMEND_SEARCH_FAILED",
@@ -0,0 +1,131 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { createRequire } from "node:module";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const currentFilePath = fileURLToPath(import.meta.url);
9
+ const defaultRulesPath = path.join(path.dirname(currentFilePath), "recommend-healing-rules.json");
10
+
11
+ let cachedRulesPath = null;
12
+ let cachedRulesMtime = null;
13
+ let cachedRules = null;
14
+
15
+ function clone(value) {
16
+ return JSON.parse(JSON.stringify(value));
17
+ }
18
+
19
+ function normalizePathLike(value) {
20
+ return String(value || "").trim();
21
+ }
22
+
23
+ function getNestedValue(root, pathParts = []) {
24
+ let current = root;
25
+ for (const part of pathParts) {
26
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
27
+ return undefined;
28
+ }
29
+ current = current[part];
30
+ }
31
+ return current;
32
+ }
33
+
34
+ export function getRecommendHealingRulesPath() {
35
+ const fromEnv = normalizePathLike(process.env.BOSS_RECOMMEND_HEALING_RULES_FILE);
36
+ return fromEnv ? path.resolve(fromEnv) : defaultRulesPath;
37
+ }
38
+
39
+ export function loadRecommendHealingRules(options = {}) {
40
+ const fresh = options.fresh === true;
41
+ const rulesPath = getRecommendHealingRulesPath();
42
+ const stats = fs.statSync(rulesPath);
43
+ if (
44
+ !fresh
45
+ && cachedRules
46
+ && cachedRulesPath === rulesPath
47
+ && cachedRulesMtime === Number(stats.mtimeMs)
48
+ ) {
49
+ return clone(cachedRules);
50
+ }
51
+ const nextRules = require(rulesPath);
52
+ cachedRulesPath = rulesPath;
53
+ cachedRulesMtime = Number(stats.mtimeMs);
54
+ cachedRules = clone(nextRules);
55
+ return clone(cachedRules);
56
+ }
57
+
58
+ export function saveRecommendHealingRules(nextRules) {
59
+ const rulesPath = getRecommendHealingRulesPath();
60
+ const serialized = `${JSON.stringify(nextRules, null, 2)}\n`;
61
+ fs.writeFileSync(rulesPath, serialized, "utf8");
62
+ cachedRulesPath = rulesPath;
63
+ cachedRulesMtime = Number(fs.statSync(rulesPath).mtimeMs);
64
+ cachedRules = clone(nextRules);
65
+ return rulesPath;
66
+ }
67
+
68
+ export function getRecommendSelectorRule(pathParts = [], fallback = []) {
69
+ const value = getNestedValue(loadRecommendHealingRules(), ["selectors", ...pathParts]);
70
+ return Array.isArray(value) && value.length > 0 ? value.map((item) => String(item)) : fallback.slice();
71
+ }
72
+
73
+ export function getRecommendNetworkRule(pathParts = [], fallback = null) {
74
+ const value = getNestedValue(loadRecommendHealingRules(), ["network", ...pathParts]);
75
+ if (Array.isArray(value)) return value.map((item) => String(item));
76
+ if (value && typeof value === "object") return clone(value);
77
+ if (typeof value === "string") return value;
78
+ if (Array.isArray(fallback)) return fallback.slice();
79
+ if (fallback && typeof fallback === "object") return clone(fallback);
80
+ return fallback;
81
+ }
82
+
83
+ export function compileRegexList(patterns = []) {
84
+ return (Array.isArray(patterns) ? patterns : [])
85
+ .map((pattern) => {
86
+ try {
87
+ return new RegExp(String(pattern), "i");
88
+ } catch {
89
+ return null;
90
+ }
91
+ })
92
+ .filter(Boolean);
93
+ }
94
+
95
+ export function findFirstMatchingPattern(value, patterns = []) {
96
+ const text = String(value || "");
97
+ for (const pattern of compileRegexList(patterns)) {
98
+ if (pattern.test(text)) return pattern.source;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ export function matchesAnyPattern(value, patterns = []) {
104
+ return Boolean(findFirstMatchingPattern(value, patterns));
105
+ }
106
+
107
+ export function buildFirstSelectorLookupExpression(selectors = [], rootExpr = "document") {
108
+ return `(() => {
109
+ const selectors = ${JSON.stringify(selectors)};
110
+ for (const selector of selectors) {
111
+ try {
112
+ const node = ${rootExpr}.querySelector(selector);
113
+ if (node) return node;
114
+ } catch {}
115
+ }
116
+ return null;
117
+ })()`;
118
+ }
119
+
120
+ export function buildSelectorCollectionExpression(selectors = [], rootExpr = "document") {
121
+ return `(() => {
122
+ const selectors = ${JSON.stringify(selectors)};
123
+ const nodes = [];
124
+ for (const selector of selectors) {
125
+ try {
126
+ nodes.push(...Array.from(${rootExpr}.querySelectorAll(selector)));
127
+ } catch {}
128
+ }
129
+ return Array.from(new Set(nodes));
130
+ })()`;
131
+ }
@@ -0,0 +1,261 @@
1
+ {
2
+ "version": 1,
3
+ "selectors": {
4
+ "top": {
5
+ "recommend_iframe": [
6
+ "iframe[name=\"recommendFrame\"]",
7
+ "iframe[src*=\"/web/frame/recommend/\"]",
8
+ "iframe"
9
+ ]
10
+ },
11
+ "frame": {
12
+ "tab_items": [
13
+ "li.tab-item[data-status]",
14
+ "li[data-status][class*=\"tab\"]"
15
+ ],
16
+ "filter_trigger": [
17
+ ".filter-label-wrap",
18
+ ".recommend-filter.op-filter"
19
+ ],
20
+ "filter_panel": [
21
+ ".recommend-filter.op-filter .filter-panel",
22
+ ".recommend-filter .filter-panel",
23
+ ".filter-panel"
24
+ ],
25
+ "filter_confirm_button": [
26
+ ".recommend-filter.op-filter .filter-panel .btn",
27
+ ".recommend-filter.op-filter .filter-panel button",
28
+ ".filter-panel .btn",
29
+ ".filter-panel button"
30
+ ],
31
+ "filter_group_container": [
32
+ ".check-box"
33
+ ],
34
+ "filter_group_school": [
35
+ ".check-box.school"
36
+ ],
37
+ "filter_group_degree": [
38
+ ".check-box.degree"
39
+ ],
40
+ "filter_group_gender": [
41
+ ".check-box.gender"
42
+ ],
43
+ "filter_group_recent_not_view": [
44
+ ".check-box.recentNotView"
45
+ ],
46
+ "filter_option": [
47
+ ".check-box .default.option",
48
+ ".check-box .options .option",
49
+ ".check-box .option"
50
+ ],
51
+ "filter_option_all": [
52
+ ".default.option, .options .option, .option"
53
+ ],
54
+ "filter_option_active": [
55
+ ".default.option.active, .options .option.active, .option.active"
56
+ ],
57
+ "filter_scroll_container": [
58
+ ".recommend-filter.op-filter .filter-panel .top",
59
+ ".recommend-filter.op-filter .top",
60
+ ".recommend-filter.op-filter .filter-panel"
61
+ ],
62
+ "filter_confirm_candidates": [
63
+ ".btn, button"
64
+ ],
65
+ "job_dropdown_trigger": [
66
+ ".top-chat-search",
67
+ ".chat-job-select",
68
+ ".chat-job-selector",
69
+ ".job-selecter",
70
+ ".job-selector",
71
+ ".job-select-wrap",
72
+ ".job-select",
73
+ ".job-select-box",
74
+ ".job-wrap",
75
+ ".chat-job-name"
76
+ ],
77
+ "job_list_items": [
78
+ ".ui-dropmenu-list .job-list .job-item",
79
+ ".job-selecter-options .job-list .job-item",
80
+ ".job-selector-options .job-list .job-item",
81
+ ".dropmenu-list .job-list .job-item",
82
+ ".job-list .job-item"
83
+ ],
84
+ "job_item_label": [
85
+ ".ui-dropmenu-list .job-list .job-item .label",
86
+ ".job-list .job-item .label",
87
+ ".label"
88
+ ],
89
+ "job_search_input": [
90
+ ".top-chat-search"
91
+ ],
92
+ "job_selected_label": [
93
+ ".job-selecter-wrap .ui-dropmenu-label",
94
+ ".ui-dropmenu-label",
95
+ ".chat-job-name",
96
+ ".job-selecter .label",
97
+ ".job-selecter .job-name",
98
+ ".job-select .label"
99
+ ],
100
+ "recommend_cards": [
101
+ "ul.card-list > li.card-item"
102
+ ],
103
+ "recommend_card_inner": [
104
+ ".card-inner[data-geekid]"
105
+ ],
106
+ "featured_cards": [
107
+ "li.geek-info-card"
108
+ ],
109
+ "featured_card_anchor": [
110
+ "a[data-geekid]"
111
+ ],
112
+ "latest_cards": [
113
+ ".candidate-card-wrap"
114
+ ],
115
+ "latest_card_inner": [
116
+ ".candidate-card-wrap .card-inner[data-geek]",
117
+ ".candidate-card-wrap [data-geek]"
118
+ ],
119
+ "refresh_finished_wrap": [
120
+ ".finished-wrap"
121
+ ],
122
+ "refresh_button": [
123
+ ".finished-wrap .btn.btn-refresh",
124
+ ".finished-wrap .btn-refresh",
125
+ ".no-data-refresh .btn-refresh"
126
+ ]
127
+ },
128
+ "detail": {
129
+ "popup": [
130
+ ".dialog-wrap.active",
131
+ ".boss-popup__wrapper",
132
+ ".boss-popup_wrapper",
133
+ ".boss-dialog_wrapper",
134
+ ".boss-dialog",
135
+ ".resume-item-detail",
136
+ ".geek-detail-modal",
137
+ "[class*=\"popup\"][class*=\"wrapper\"]",
138
+ "[class*=\"dialog\"][class*=\"wrapper\"]"
139
+ ],
140
+ "resume_iframe": [
141
+ "iframe[src*=\"/web/frame/c-resume/\"]",
142
+ "iframe[name*=\"resume\"]"
143
+ ],
144
+ "close_button": [
145
+ ".boss-popup__close",
146
+ ".popup-close",
147
+ ".modal-close",
148
+ ".dialog-close",
149
+ ".close-btn",
150
+ "button[aria-label*=\"关闭\"]",
151
+ "button[title*=\"关闭\"]",
152
+ ".icon-close"
153
+ ],
154
+ "close_fallback_candidates": [
155
+ "[aria-label*=\"关闭\"]",
156
+ "[aria-label*=\"返回\"]",
157
+ "[title*=\"关闭\"]",
158
+ "[title*=\"返回\"]",
159
+ "[class*=\"close\"]",
160
+ "[class*=\"Close\"]",
161
+ "[class*=\"back\"]",
162
+ "[class*=\"Back\"]",
163
+ "button",
164
+ "a",
165
+ "i",
166
+ "span"
167
+ ],
168
+ "ack_button": [
169
+ "button.btn-v2.btn-sure-v2",
170
+ "button.btn",
171
+ ".boss-dialog button",
172
+ ".boss-popup__wrapper button"
173
+ ],
174
+ "favorite_button": [
175
+ ".like-icon-and-text",
176
+ ".resume-footer.item-operate [class*=\"collect\"]",
177
+ ".resume-footer.item-operate [class*=\"favorite\"]",
178
+ ".resume-footer-wrap [class*=\"collect\"]",
179
+ ".resume-footer-wrap [class*=\"favorite\"]"
180
+ ],
181
+ "greet_button_recommend": [
182
+ "button.btn-v2.btn-sure-v2.btn-greet",
183
+ ".resume-footer.item-operate button.btn-v2",
184
+ ".resume-footer-wrap button.btn-v2",
185
+ ".resume-footer.item-operate button",
186
+ ".resume-footer-wrap button"
187
+ ],
188
+ "greet_button_featured": [
189
+ "button.btn-v2.position-rights.btn-sure-v2",
190
+ "button.btn-v2.btn-sure-v2.position-rights",
191
+ ".resume-footer.item-operate button.btn-v2",
192
+ ".resume-footer-wrap button.btn-v2",
193
+ ".resume-footer.item-operate button",
194
+ ".resume-footer-wrap button"
195
+ ]
196
+ }
197
+ },
198
+ "network": {
199
+ "resume": {
200
+ "info_url_patterns": [
201
+ "\\/wapi\\/zpjob\\/view\\/geek\\/info\\b",
202
+ "\\/wapi\\/zpitem\\/web\\/boss\\/[^?#]*\\/geek\\/info\\b",
203
+ "\\/boss\\/[^?#]*\\/geek\\/info\\b",
204
+ "\\/geek\\/info\\b",
205
+ "[?&](?:geekid|geek_id|encryptgeekid|encryptjid|jid|securityid)="
206
+ ],
207
+ "related_keywords": [
208
+ "geek",
209
+ "resume",
210
+ "candidate",
211
+ "friend"
212
+ ]
213
+ },
214
+ "favorite": {
215
+ "request_source_keywords": [
216
+ "usermark",
217
+ "actionlog/common.json"
218
+ ],
219
+ "add_patterns": [
220
+ "(?:^|[_\\W])(add|favorite|collect|interest(?:ed)?)(?:$|[_\\W])",
221
+ "\\/add(?:\\/|$)|[?&](?:action|op|operation|type)=add\\b|[?&](?:status|p3|favorite|collect|interested)=1\\b"
222
+ ],
223
+ "remove_patterns": [
224
+ "(?:^|[_\\W])(del|delete|remove|cancel|unfavorite|uncollect|uninterest)(?:$|[_\\W])",
225
+ "\\/del(?:\\/|$)|[?&](?:action|op|operation|type)=del\\b|[?&](?:status|p3|favorite|collect|interested)=0\\b"
226
+ ],
227
+ "actionlog_action_name": "star-interest-click",
228
+ "status_keys": [
229
+ "p3",
230
+ "status",
231
+ "state",
232
+ "favorite",
233
+ "collect",
234
+ "interested",
235
+ "markstatus",
236
+ "isfavorite",
237
+ "iscollect"
238
+ ],
239
+ "operation_keys": [
240
+ "action",
241
+ "op",
242
+ "operation",
243
+ "type",
244
+ "mode",
245
+ "mark",
246
+ "interest"
247
+ ]
248
+ },
249
+ "greet": {
250
+ "url_patterns": [
251
+ "greet",
252
+ "sayhi",
253
+ "hello",
254
+ "friend/add",
255
+ "chat/start",
256
+ "boss\\/friend",
257
+ "boss\\/dialog"
258
+ ]
259
+ }
260
+ }
261
+ }