hunter-mate 0.1.3 → 0.1.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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/dist/assets/extension/background/service-worker.js +113 -24
  3. package/dist/assets/extension/content/index.js +239 -134
  4. package/dist/assets/extension/manifest.json +2 -4
  5. package/dist/assets/extension/sidepanel/index.js +76 -2
  6. package/dist/assets/server/app.module.js +4 -2
  7. package/dist/assets/server/app.module.js.map +1 -1
  8. package/dist/assets/server/build-info.json +1 -1
  9. package/dist/assets/server/db/sqlite.module.js +4 -0
  10. package/dist/assets/server/db/sqlite.module.js.map +1 -1
  11. package/dist/assets/server/devops/devops-sink-init.d.ts +14 -1
  12. package/dist/assets/server/devops/devops-sink-init.js +8 -3
  13. package/dist/assets/server/devops/devops-sink-init.js.map +1 -1
  14. package/dist/assets/server/doctor/doctor.controller.d.ts +13 -0
  15. package/dist/assets/server/doctor/doctor.controller.js +62 -0
  16. package/dist/assets/server/doctor/doctor.controller.js.map +1 -0
  17. package/dist/assets/server/doctor/doctor.module.d.ts +2 -0
  18. package/dist/assets/server/doctor/doctor.module.js +27 -0
  19. package/dist/assets/server/doctor/doctor.module.js.map +1 -0
  20. package/dist/assets/server/doctor/doctor.service.d.ts +15 -0
  21. package/dist/assets/server/doctor/doctor.service.js +209 -0
  22. package/dist/assets/server/doctor/doctor.service.js.map +1 -0
  23. package/dist/assets/server/doctor/doctor.types.d.ts +46 -0
  24. package/dist/assets/server/doctor/doctor.types.js +3 -0
  25. package/dist/assets/server/doctor/doctor.types.js.map +1 -0
  26. package/dist/assets/server/extraction/adapter.service.d.ts +18 -4
  27. package/dist/assets/server/extraction/adapter.service.js +129 -15
  28. package/dist/assets/server/extraction/adapter.service.js.map +1 -1
  29. package/dist/assets/server/extraction/anomaly-report.service.d.ts +12 -0
  30. package/dist/assets/server/extraction/anomaly-report.service.js +81 -0
  31. package/dist/assets/server/extraction/anomaly-report.service.js.map +1 -0
  32. package/dist/assets/server/extraction/entities/adapter-rule.entity.d.ts +13 -0
  33. package/dist/assets/server/extraction/entities/adapter-rule.entity.js +71 -0
  34. package/dist/assets/server/extraction/entities/adapter-rule.entity.js.map +1 -0
  35. package/dist/assets/server/extraction/entities/parse-log.entity.d.ts +4 -1
  36. package/dist/assets/server/extraction/entities/parse-log.entity.js +15 -0
  37. package/dist/assets/server/extraction/entities/parse-log.entity.js.map +1 -1
  38. package/dist/assets/server/extraction/extraction-entity.controller.d.ts +40 -2
  39. package/dist/assets/server/extraction/extraction-entity.controller.js +196 -13
  40. package/dist/assets/server/extraction/extraction-entity.controller.js.map +1 -1
  41. package/dist/assets/server/extraction/extraction-rule.service.js +16 -8
  42. package/dist/assets/server/extraction/extraction-rule.service.js.map +1 -1
  43. package/dist/assets/server/extraction/extraction.module.js +12 -2
  44. package/dist/assets/server/extraction/extraction.module.js.map +1 -1
  45. package/dist/assets/server/extraction/extraction.service.d.ts +17 -1
  46. package/dist/assets/server/extraction/extraction.service.js +125 -42
  47. package/dist/assets/server/extraction/extraction.service.js.map +1 -1
  48. package/dist/assets/server/extraction/linkedin-extractor.d.ts +1 -0
  49. package/dist/assets/server/extraction/linkedin-extractor.js +244 -0
  50. package/dist/assets/server/extraction/linkedin-extractor.js.map +1 -0
  51. package/dist/assets/server/extraction/rrweb-snapshot.utils.d.ts +15 -0
  52. package/dist/assets/server/extraction/rrweb-snapshot.utils.js +48 -0
  53. package/dist/assets/server/extraction/rrweb-snapshot.utils.js.map +1 -0
  54. package/dist/assets/server/extraction/rrweb-snapshot.utils.spec.d.ts +1 -0
  55. package/dist/assets/server/extraction/rrweb-snapshot.utils.spec.js +60 -0
  56. package/dist/assets/server/extraction/rrweb-snapshot.utils.spec.js.map +1 -0
  57. package/dist/assets/server/extraction/script-rule.d.ts +1 -0
  58. package/dist/assets/server/extraction/script-rule.js +4 -0
  59. package/dist/assets/server/extraction/script-rule.js.map +1 -1
  60. package/dist/assets/server/extraction/snapshot-analysis.service.d.ts +23 -0
  61. package/dist/assets/server/extraction/snapshot-analysis.service.js +147 -0
  62. package/dist/assets/server/extraction/snapshot-analysis.service.js.map +1 -0
  63. package/dist/assets/server/recording/entities/recording-snapshot.entity.d.ts +16 -0
  64. package/dist/assets/server/recording/entities/recording-snapshot.entity.js +91 -0
  65. package/dist/assets/server/recording/entities/recording-snapshot.entity.js.map +1 -0
  66. package/dist/assets/server/recording/recording-event-bus.service.d.ts +4 -0
  67. package/dist/assets/server/recording/recording-event-bus.service.js +25 -0
  68. package/dist/assets/server/recording/recording-event-bus.service.js.map +1 -0
  69. package/dist/assets/server/recording/recording-snapshot.service.d.ts +13 -0
  70. package/dist/assets/server/recording/recording-snapshot.service.js +81 -0
  71. package/dist/assets/server/recording/recording-snapshot.service.js.map +1 -0
  72. package/dist/assets/server/recording/recording-snapshot.service.spec.d.ts +1 -0
  73. package/dist/assets/server/recording/recording-snapshot.service.spec.js +65 -0
  74. package/dist/assets/server/recording/recording-snapshot.service.spec.js.map +1 -0
  75. package/dist/assets/server/recording/recording.controller.d.ts +3 -1
  76. package/dist/assets/server/recording/recording.controller.js +7 -3
  77. package/dist/assets/server/recording/recording.controller.js.map +1 -1
  78. package/dist/assets/server/recording/recording.module.js +11 -4
  79. package/dist/assets/server/recording/recording.module.js.map +1 -1
  80. package/dist/assets/server/recording/recording.service.d.ts +5 -21
  81. package/dist/assets/server/recording/recording.service.js +22 -9
  82. package/dist/assets/server/recording/recording.service.js.map +1 -1
  83. package/dist/assets/server/site/entities/site-registry.entity.d.ts +2 -0
  84. package/dist/assets/server/site/entities/site-registry.entity.js +10 -0
  85. package/dist/assets/server/site/entities/site-registry.entity.js.map +1 -1
  86. package/dist/assets/server/site/site.controller.d.ts +2 -0
  87. package/dist/assets/server/site/site.controller.js +1 -1
  88. package/dist/assets/server/site/site.controller.js.map +1 -1
  89. package/dist/assets/server/site/site.service.d.ts +2 -1
  90. package/dist/assets/server/site/site.service.js +10 -3
  91. package/dist/assets/server/site/site.service.js.map +1 -1
  92. package/dist/auto-doctor.d.ts +8 -0
  93. package/dist/auto-doctor.js +34 -0
  94. package/dist/commands/doctor.d.ts +43 -0
  95. package/dist/commands/doctor.js +179 -7
  96. package/dist/commands/extension.js +2 -2
  97. package/dist/commands/extras.d.ts +1 -1
  98. package/dist/commands/extras.js +4 -3
  99. package/dist/commands/install.js +11 -8
  100. package/dist/index.js +7 -0
  101. package/dist/native-check.js +8 -1
  102. package/dist/updater.d.ts +9 -5
  103. package/dist/updater.js +75 -9
  104. package/package.json +3 -3
@@ -9,6 +9,7 @@ import { runInstall } from './install.js';
9
9
  import { runExtensionCheck } from './extension.js';
10
10
  import { checkNativeModules } from '../native-check.js';
11
11
  import { getCliLogger } from '../logger.js';
12
+ import { getToken } from '../keychain.js';
12
13
  const B = {
13
14
  reset: '\x1b[0m',
14
15
  bold: '\x1b[1m',
@@ -18,6 +19,32 @@ const B = {
18
19
  cyan: '\x1b[36m',
19
20
  magenta: '\x1b[35m',
20
21
  };
22
+ /**
23
+ * 调用 server /doctor/run 统一入口。
24
+ * 失败时返回 null,调用方决定如何处理。
25
+ */
26
+ export async function runDoctorChecks(opts) {
27
+ const config = loadConfig();
28
+ const token = await getToken().catch(() => undefined);
29
+ const checks = opts.checks && opts.checks.length ? opts.checks : undefined;
30
+ const timeout = opts.timeout ?? 60_000;
31
+ try {
32
+ const { data } = await axios.post(`${config.serverUrl}/doctor/run`, {
33
+ checks,
34
+ mode: opts.fix ? 'repair' : 'check',
35
+ dryRun: opts.dryRun === true,
36
+ }, {
37
+ timeout,
38
+ headers: token ? { Authorization: `UTOKEN ${token}` } : {},
39
+ });
40
+ return data;
41
+ }
42
+ catch (e) {
43
+ const message = e?.response?.data?.message || e.message || String(e);
44
+ getCliLogger().warn('doctor:checks:failed', { error: message, fix: opts.fix, checks });
45
+ return null;
46
+ }
47
+ }
21
48
  /** 对应 ChromePilot doctor: 全链路健康检查 — 带 auto-heal, 无感启动 server/安装扩展 */
22
49
  export async function runDoctor(opts) {
23
50
  const checks = [];
@@ -25,13 +52,21 @@ export async function runDoctor(opts) {
25
52
  const healed = [];
26
53
  const actions = [];
27
54
  const autoHeal = opts.autoHeal !== false; // default true
28
- const interactive = opts.interactive !== false; // default true
55
+ const fix = opts.fix === true;
29
56
  // ── Banner (human 模式) ──
30
57
  if (!opts.json) {
31
58
  console.log(`\n${B.bold}${B.cyan}╭─────────────────────────────────────────────╮${B.reset}`);
32
59
  console.log(`${B.bold}${B.cyan}│ HunterMate Doctor · 环境全链路自检与自愈 │${B.reset}`);
33
60
  console.log(`${B.bold}${B.cyan}╰─────────────────────────────────────────────╯${B.reset}\n`);
34
61
  }
62
+ // ── 0) Node 版本(Windows better-sqlite3 预构建友好度) ──
63
+ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
64
+ const nodeVersionFriendly = [20, 22, 24].includes(nodeMajor);
65
+ checks.push({
66
+ key: 'node_version',
67
+ status: nodeVersionFriendly ? 'ok' : 'warn',
68
+ detail: `Node ${process.versions.node}${nodeVersionFriendly ? '' : ' (hunter-mate 预构建包对 Node 20/22/24 支持最好,Windows 无编译器时建议切到 Node 22 LTS)'}`,
69
+ });
35
70
  // ── 1) 目录 & token ──
36
71
  let initResult = null;
37
72
  try {
@@ -184,7 +219,7 @@ export async function runDoctor(opts) {
184
219
  if (!opts.json) {
185
220
  console.log(`${B.magenta}→ hunter-mate 正在为你打开扩展所在目录...${B.reset}`);
186
221
  }
187
- open(initResult.extensionDir).catch(() => { });
222
+ open(path.dirname(initResult.extensionDir)).catch(() => { });
188
223
  healed.push('open_ext_dir');
189
224
  }
190
225
  if (!opts.json) {
@@ -197,9 +232,11 @@ export async function runDoctor(opts) {
197
232
  }
198
233
  }
199
234
  // ── 5) 登录态(不强制) ──
235
+ let loggedIn = false;
200
236
  try {
201
237
  const { getToken } = await import('../keychain.js');
202
238
  const token = await getToken();
239
+ loggedIn = !!token;
203
240
  checks.push({
204
241
  key: 'auth',
205
242
  status: token ? 'ok' : 'warn',
@@ -211,6 +248,35 @@ export async function runDoctor(opts) {
211
248
  catch (e) {
212
249
  checks.push({ key: 'auth', status: 'skip', detail: `跳过(keychain 读取失败): ${e.message}` });
213
250
  }
251
+ // ── 6) 数据面 doctor checks ──
252
+ let data = null;
253
+ if (serverUp) {
254
+ if (!opts.json) {
255
+ console.log(`\n${B.bold}${B.cyan}→ 正在运行数据面检查${fix ? ' (repair 模式)' : ' (只读 check 模式)'}...${B.reset}`);
256
+ }
257
+ data = await runDoctorChecks({
258
+ fix,
259
+ checks: opts.checks,
260
+ json: opts.json,
261
+ timeout: 120_000,
262
+ });
263
+ if (data) {
264
+ for (const c of data.checks) {
265
+ const status = c.ok ? 'ok' : c.severity === 'error' ? 'fail' : 'warn';
266
+ checks.push({
267
+ key: `data_${c.id}`,
268
+ status,
269
+ detail: c.message || c.description,
270
+ });
271
+ }
272
+ }
273
+ else {
274
+ checks.push({ key: 'data_doctor', status: 'warn', detail: '无法连接 /doctor/run (server 可能尚未就绪)' });
275
+ }
276
+ }
277
+ else {
278
+ checks.push({ key: 'data_doctor', status: 'skip', detail: 'server 未运行,跳过数据面检查' });
279
+ }
214
280
  // ── Summary ──
215
281
  const anyFail = checks.some((c) => c.status === 'fail');
216
282
  const anyWarn = checks.some((c) => c.status === 'warn');
@@ -247,7 +313,14 @@ export async function runDoctor(opts) {
247
313
  console.log(`\n${B.bold}${B.red}✗ 环境未就绪, 请按上方引导完成修复后重试。${B.reset}\n`);
248
314
  }
249
315
  }
250
- return { summary, checks, hints, healed, actions };
316
+ return {
317
+ summary, checks, hints, healed, actions, data,
318
+ nodeVersion: process.versions.node,
319
+ serverRunning: serverUp,
320
+ extensionInstalled: extInstalled,
321
+ extensionConnected: extConnected,
322
+ loggedIn,
323
+ };
251
324
  }
252
325
  /** 友好中文引导 — 当环境不完备时给用户清晰的操作步骤 */
253
326
  function showFriendlyGuide(extInstalled, extConnected, serverUp, healed, extDir) {
@@ -261,21 +334,24 @@ function showFriendlyGuide(extInstalled, extConnected, serverUp, healed, extDir)
261
334
  console.log(` hunter-mate 已尝试自动启动。若持续失败请手动执行:`);
262
335
  console.log(` ${B.bold}hunter-mate start -f${B.reset} (前台模式可看日志)\n`);
263
336
  }
337
+ const extParentDir = path.dirname(extDir);
338
+ const extFolderName = path.basename(extDir);
264
339
  if (!extInstalled) {
265
340
  console.log(`${B.bold}${B.yellow}⚠ Hunter Mate Chrome 扩展尚未安装${B.reset}`);
266
341
  console.log(` 步骤: hunter-mate 已自动把扩展文件复制到:`);
267
342
  console.log(` ${B.cyan}${extDir}${B.reset}`);
343
+ console.log(` 并已打开扩展所在目录: ${B.cyan}${extParentDir}${B.reset}`);
268
344
  console.log(` ${B.bold}接下来需要你手动在 Chrome 中加载(Chrome 安全策略限制命令行自动化):${B.reset}`);
269
345
  console.log(` ${B.magenta}① 在 Chrome 地址栏输入:${B.reset} chrome://extensions`);
270
346
  console.log(` ${B.magenta}② 打开右上角「开发者模式」开关${B.reset}`);
271
- console.log(` ${B.magenta}③ 点击「加载已解压的扩展程序」${B.reset}`);
272
- console.log(` ${B.magenta}④ 选择上面的目录:${B.reset} ${B.cyan}${extDir}${B.reset}`);
273
- console.log(` ${B.magenta}⑤ Chrome 工具栏应出现 Hunter Mate 图标${B.reset}\n`);
347
+ console.log(` ${B.magenta}③ 把 ${B.bold}${extFolderName}${B.reset} 文件夹拖到 chrome://extensions 页面里${B.reset}`);
348
+ console.log(` (也可以点击「加载已解压的扩展程序」后选择 ${B.cyan}${extDir}${B.reset})`);
349
+ console.log(` ${B.magenta}④ Chrome 工具栏应出现 Hunter Mate 图标,侧栏显示「已连接」${B.reset}\n`);
274
350
  }
275
351
  if (extInstalled && !extConnected) {
276
352
  console.log(`${B.bold}${B.yellow}⚠ 扩展已安装但尚未连接到 Server${B.reset}`);
277
353
  console.log(` hunter-mate 已自动帮你打开了:`);
278
- console.log(` · 扩展所在目录: ${B.cyan}${extDir}${B.reset}`);
354
+ console.log(` · 扩展所在目录: ${B.cyan}${extParentDir}${B.reset}`);
279
355
  console.log(` · Chrome 扩展管理页: ${B.cyan}chrome://extensions${B.reset}`);
280
356
  console.log(` ${B.bold}请确认以下几点:${B.reset}`);
281
357
  console.log(` ${B.magenta}① 扩展已在 chrome://extensions 中「已开启」${B.reset} (灰色开关变蓝色)`);
@@ -304,14 +380,110 @@ export const doctorCommand = new Command('doctor')
304
380
  .option('--json', '仅以 JSON 输出检查结果与建议(跳过引导文案)')
305
381
  .option('--no-auto-heal', '禁用自动修复(仅检查并给出建议, 不自动启动/安装)')
306
382
  .option('--no-interactive', '无交互模式(不打印友好引导, 仅输出结构化结果)')
383
+ .option('--fix', '执行可修复的数据检查(默认只读 check)')
384
+ .option('--checks <ids>', '只跑指定检查,逗号分隔(如 linkedin-dupes)', (val) => val.split(',').map((s) => s.trim()).filter(Boolean))
385
+ .option('--list', '列出 server 上注册的数据检查项')
307
386
  .action(async (opts) => {
387
+ if (opts.list) {
388
+ const config = loadConfig();
389
+ try {
390
+ const { data } = await axios.get(`${config.serverUrl}/doctor/checks`, { timeout: 10_000 });
391
+ if (opts.json) {
392
+ console.log(JSON.stringify(data, null, 2));
393
+ }
394
+ else {
395
+ console.log(`\n${B.bold}${B.cyan}已注册 Doctor 检查项:${B.reset}`);
396
+ for (const c of data) {
397
+ const fixTag = c.fixable ? `${B.green}[可修复]${B.reset}` : `${B.yellow}[只读]${B.reset}`;
398
+ console.log(` ${fixTag} ${B.bold}${c.id}${B.reset} (${c.severity})`);
399
+ console.log(` ${c.name}`);
400
+ console.log(` ${c.description}`);
401
+ }
402
+ }
403
+ process.exit(0);
404
+ }
405
+ catch (e) {
406
+ const message = e?.response?.data?.message || e.message || String(e);
407
+ getCliLogger().error('doctor:list failed', { message });
408
+ if (opts.json) {
409
+ console.log(JSON.stringify({ error: message }, null, 2));
410
+ }
411
+ else {
412
+ console.log(`${B.red}获取检查列表失败:${B.reset} ${message}`);
413
+ }
414
+ process.exit(1);
415
+ }
416
+ return;
417
+ }
308
418
  const r = await runDoctor({
309
419
  json: opts.json,
310
420
  autoHeal: opts.autoHeal !== false,
311
421
  interactive: opts.interactive !== false,
422
+ fix: opts.fix === true,
423
+ checks: opts.checks,
312
424
  });
313
425
  if (opts.json) {
314
426
  console.log(JSON.stringify(r, null, 2));
315
427
  }
316
428
  process.exit(r.summary === 'fail' ? 1 : 0);
317
429
  });
430
+ export const repairLinkedInDupesCommand = new Command('repair-linkedin-dupes')
431
+ .description('修复 LinkedIn /in/<slug>/details/... 子页产生的重复 talent 数据')
432
+ .option('--dry-run', '仅预览,不真正合并/删除')
433
+ .option('--json', '仅输出 JSON 结果')
434
+ .action(async (opts) => {
435
+ const data = await runDoctorChecks({
436
+ fix: true,
437
+ checks: ['linkedin-dupes'],
438
+ dryRun: opts.dryRun === true,
439
+ json: opts.json,
440
+ timeout: 120_000,
441
+ });
442
+ if (!data) {
443
+ const message = '无法连接 server /doctor/run, 请确认 server 已启动';
444
+ getCliLogger().error('repair-linkedin-dupes failed', { message });
445
+ if (opts.json) {
446
+ console.log(JSON.stringify({ error: message }, null, 2));
447
+ }
448
+ else {
449
+ console.log(`修复失败: ${message}`);
450
+ }
451
+ process.exit(1);
452
+ }
453
+ const c = data.checks.find((x) => x.id === 'linkedin-dupes');
454
+ const details = c?.details || {};
455
+ const compat = {
456
+ dryRun: data.dryRun,
457
+ totalCandidates: details.totalCandidates ?? 0,
458
+ duplicateGroups: details.duplicateGroups ?? 0,
459
+ repairedCount: data.repaired?.length ?? 0,
460
+ merged: data.repaired ?? [],
461
+ summary: data.summary,
462
+ };
463
+ if (opts.json) {
464
+ console.log(JSON.stringify({ ...compat, raw: data }, null, 2));
465
+ }
466
+ else {
467
+ const B = {
468
+ reset: '\x1b[0m',
469
+ bold: '\x1b[1m',
470
+ green: '\x1b[32m',
471
+ yellow: '\x1b[33m',
472
+ cyan: '\x1b[36m',
473
+ };
474
+ console.log(`\n${B.bold}${B.cyan}LinkedIn 重复 talent 修复结果${B.reset}`);
475
+ console.log(` 模式: ${data.dryRun ? B.yellow + 'dry-run' + B.reset : B.green + '正式修复' + B.reset}`);
476
+ console.log(` LinkedIn talent 总数: ${compat.totalCandidates}`);
477
+ console.log(` 归一组数: ${compat.duplicateGroups}`);
478
+ console.log(` 需合并组数: ${compat.repairedCount}`);
479
+ if (compat.merged.length) {
480
+ console.log(`\n${B.bold}合并详情:${B.reset}`);
481
+ for (const m of compat.merged) {
482
+ console.log(` • master #${m.masterId} ← ${m.removedIds.map((id) => `#${id}`).join(', ')}`);
483
+ console.log(` ${m.sourceUrl}`);
484
+ }
485
+ }
486
+ }
487
+ process.exit(0);
488
+ });
489
+ doctorCommand.addCommand(repairLinkedInDupesCommand);
@@ -151,10 +151,10 @@ export function ensureEnvReady() {
151
151
  process.exit(1);
152
152
  }
153
153
  console.log(`${B.green}✓ 扩展已自动安装到 ${r.target}${B.reset}`);
154
- // 打开目录引导
154
+ // 打开扩展所在目录(父目录),方便用户把 extension 文件夹拖到 chrome://extensions
155
155
  try {
156
156
  const { default: open } = await import('open');
157
- open(init.extensionDir).catch(() => { });
157
+ open(path.dirname(init.extensionDir)).catch(() => { });
158
158
  }
159
159
  catch { }
160
160
  showInstallGuide(init.extensionDir);
@@ -12,7 +12,7 @@ export declare function runReload(timeoutMs?: number): Promise<{
12
12
  error: string;
13
13
  }>;
14
14
  }>;
15
- /** 对应 ChromePilot open-ext / open-extension-folder: 打开 ~/.hunter-mate/extension */
15
+ /** 对应 ChromePilot open-ext / open-extension-folder: 打开扩展所在目录(~/.hunter-mate),方便拖到 chrome://extensions */
16
16
  export declare function runOpenExt(target?: string): Promise<string>;
17
17
  export declare const reloadCommand: Command;
18
18
  export declare const openExtCommand: Command;
@@ -102,10 +102,11 @@ export async function runReload(timeoutMs = 5000) {
102
102
  });
103
103
  });
104
104
  }
105
- /** 对应 ChromePilot open-ext / open-extension-folder: 打开 ~/.hunter-mate/extension */
105
+ /** 对应 ChromePilot open-ext / open-extension-folder: 打开扩展所在目录(~/.hunter-mate),方便拖到 chrome://extensions */
106
106
  export async function runOpenExt(target) {
107
107
  const init = runInit();
108
- const dir = target ?? init.extensionDir;
108
+ // 默认打开父目录,让用户能把 extension 文件夹拖进 Chrome;--target 可覆盖
109
+ const dir = target ?? path.dirname(init.extensionDir);
109
110
  if (!fs.existsSync(dir))
110
111
  fs.mkdirSync(dir, { recursive: true });
111
112
  try {
@@ -133,7 +134,7 @@ export const reloadCommand = new Command('reload')
133
134
  });
134
135
  export const openExtCommand = new Command('open-extension-folder')
135
136
  .alias('open-ext')
136
- .description('打开 ~/.hunter-mate/extension 文件夹,方便拖到 chrome://extensions 加载(对应 chromepilot open-ext)')
137
+ .description('打开扩展所在目录(~/.hunter-mate),方便把 extension 文件夹拖到 chrome://extensions 加载(对应 chromepilot open-ext)')
137
138
  .option('-t, --target <dir>', '指定自定义扩展目录')
138
139
  .action(async (opts) => {
139
140
  const dir = await runOpenExt(opts.target);
@@ -52,16 +52,19 @@ const B = {
52
52
  };
53
53
  /** 友好引导: 告诉用户如何在 Chrome 中加载 Hunter Mate 扩展 */
54
54
  export function showInstallGuide(extDir) {
55
+ const parentDir = path.dirname(extDir);
56
+ const folderName = path.basename(extDir);
55
57
  console.log(`\n${B.bold}${B.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━ Hunter Mate 扩展安装指引 ━━━━━━━━━━━━━━━━━━━━━━━━━━${B.reset}`);
56
58
  console.log(`\n${B.bold}扩展文件已经准备好:${B.reset}`);
57
- console.log(` ${B.cyan}${extDir}${B.reset}`);
58
- console.log(`\n${B.bold}请按以下 5 步在 Chrome 中加载(约 30 秒):${B.reset}\n`);
59
+ console.log(` 扩展文件夹: ${B.cyan}${extDir}${B.reset}`);
60
+ console.log(` 已为你打开: ${B.cyan}${parentDir}${B.reset}`);
61
+ console.log(`\n${B.bold}请按以下 4 步在 Chrome 中加载(约 30 秒):${B.reset}\n`);
59
62
  console.log(` ${B.magenta}①${B.reset} 打开 Chrome, 地址栏输入: ${B.cyan}chrome://extensions${B.reset}`);
60
63
  console.log(` (hunter-mate 已帮你自动打开了这个页面)`);
61
64
  console.log(` ${B.magenta}②${B.reset} 右上角打开「${B.bold}开发者模式${B.reset}」开关`);
62
- console.log(` ${B.magenta}③${B.reset} 点击「${B.bold}加载已解压的扩展程序${B.reset}」按钮`);
63
- console.log(` ${B.magenta}④${B.reset} 在文件选择框中选择目录: ${B.cyan}${extDir}${B.reset}`);
64
- console.log(` ${B.magenta}⑤${B.reset} Chrome 工具栏应出现 Hunter Mate 🤝 图标\n`);
65
+ console.log(` ${B.magenta}③${B.reset} 把文件夹 ${B.bold}${folderName}${B.reset} 拖到 chrome://extensions 页面里`);
66
+ console.log(` (也可以点击「加载已解压的扩展程序」后选择 ${B.cyan}${extDir}${B.reset})`);
67
+ console.log(` ${B.magenta}④${B.reset} Chrome 工具栏应出现 Hunter Mate 🤝 图标,侧栏显示「已连接」\n`);
65
68
  console.log(`${B.bold}${B.yellow}常见问题:${B.reset}`);
66
69
  console.log(` · 没看到「加载已解压的扩展程序」? → 先开启「开发者模式」开关`);
67
70
  console.log(` · 加载后图标灰显? → 点击图标确认侧栏显示「已连接」`);
@@ -69,7 +72,7 @@ export function showInstallGuide(extDir) {
69
72
  console.log(`\n${B.bold}${B.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${B.reset}\n`);
70
73
  }
71
74
  export const installCommand = new Command('install')
72
- .description('把内置 extension dist 复制到 ~/.hunter-mate/extension(对应 chromepilot install)。完成后自动打开目录并给出中文引导。')
75
+ .description('把内置 extension dist 复制到 ~/.hunter-mate/extension(对应 chromepilot install)。完成后自动打开扩展所在目录(~/.hunter-mate)并给出中文引导,方便把 extension 文件夹拖到 chrome://extensions。')
73
76
  .option('-t, --target <dir>', '目标目录,默认 ~/.hunter-mate/extension')
74
77
  .option('--no-open', '不自动打开扩展所在目录')
75
78
  .action(async (opts) => {
@@ -82,11 +85,11 @@ export const installCommand = new Command('install')
82
85
  return;
83
86
  }
84
87
  console.log(`${B.green}✓ hunter-mate install OK: ${r.copied} files${B.reset} → ${B.cyan}${r.target}${B.reset}`);
85
- // 自动打开目录 + 引导
88
+ // 自动打开扩展所在目录(父目录),方便用户把 extension 文件夹拖到 chrome://extensions
86
89
  if (opts.open !== false) {
87
90
  try {
88
91
  const { default: open } = await import('open');
89
- open(r.target).catch(() => { });
92
+ open(path.dirname(r.target)).catch(() => { });
90
93
  }
91
94
  catch { }
92
95
  }
package/dist/index.js CHANGED
@@ -76,6 +76,13 @@ program
76
76
  for (const line of report.performed)
77
77
  console.log(chalk.gray(` [auto-update] ${line}`));
78
78
  }
79
+ try {
80
+ const { runAutoDoctor } = await import('./auto-doctor.js');
81
+ await runAutoDoctor(report);
82
+ }
83
+ catch {
84
+ /* auto-doctor 永不阻塞主命令 */
85
+ }
79
86
  }
80
87
  catch {
81
88
  /* 一致性检测永不阻塞主命令 */
@@ -18,6 +18,13 @@ function locateServerEntryDir() {
18
18
  const checker = createNativeChecker({
19
19
  probeDir: () => locateServerEntryDir(),
20
20
  skipDetail: "未找到 server 入口(packages/server/dist|src),跳过原生模块检查",
21
- rebuildHint: (moduleName, compiledLabel) => `二选一:① 切回 ${compiledLabel} 的 node 再运行;② 用当前 node 重编译:pnpm rebuild -r ${moduleName}(重编后重启 server)`,
21
+ rebuildHint: (moduleName, compiledLabel) => {
22
+ const isWin = process.platform === "win32";
23
+ const base = `二选一:① 切回 ${compiledLabel} 的 node 再运行;② 用当前 node 重编译:pnpm rebuild -r ${moduleName}(重编后重启 server)`;
24
+ if (isWin && moduleName === "better-sqlite3") {
25
+ return `${base}。Windows 无 C++ 编译器时,推荐直接安装 Node 22 LTS(预构建包最全),无需安装 Visual Studio Build Tools`;
26
+ }
27
+ return base;
28
+ },
22
29
  });
23
30
  export const checkNativeModules = checker.checkNativeModules;
package/dist/updater.d.ts CHANGED
@@ -6,17 +6,21 @@
6
6
  export declare function computeDevLinked(pkgRoot: string): boolean;
7
7
  export declare function isDevLinked(): boolean;
8
8
  export declare const npmLatestVersion: (pkg?: string) => string | null;
9
- export declare const npmSelfUpdate: (pkg?: string) => {
10
- ok: boolean;
11
- msg: string;
12
- };
13
9
  export declare const updateCheckIntervalMs: () => number;
14
10
  export declare const shouldCheckUpdate: (stateFile?: string, now?: number, intervalMs?: number) => boolean;
15
11
  export declare const markUpdateChecked: (stateFile?: string) => void;
12
+ export interface NpmSelfUpdateResult {
13
+ ok: boolean;
14
+ msg: string;
15
+ }
16
+ export declare function npmSelfUpdate(pkg?: string): NpmSelfUpdateResult;
16
17
  export interface CliUpdateOutcome {
17
18
  logs: string[];
18
19
  devSkipped: boolean;
19
20
  updated: boolean;
20
21
  }
21
- /** preAction 集成入口:返回日志行数组,永不 throw */
22
+ /**
23
+ * preAction 集成入口:返回日志行数组,永不 throw。
24
+ * 逻辑等价于 kit 的 autoCliUpdate,但安装命令走 --prefix。
25
+ */
22
26
  export declare function maybeCliSelfUpdate(): Promise<CliUpdateOutcome>;
package/dist/updater.js CHANGED
@@ -6,18 +6,25 @@
6
6
  * - dev 判定(hc-hw local worktree 布局 + HUNTER_MATE_DEV=1)为本仓特有安装形态
7
7
  * - 已安装版本读取走 version.ts 的 package.json SSOT 扫描
8
8
  *
9
+ * 重要: hunter-mate 使用用户级安装(--prefix ~/.hunter-mate/cli),
10
+ * 避免 Workbuddy/Trae 沙盒拦截 `npm install -g` 全局写入。
11
+ * 因此覆盖 kit 默认的 npmSelfUpdate/autoCliUpdate,统一走 --prefix。
12
+ *
9
13
  * 逃生开关(kit autoCliUpdate 内统一处理):
10
14
  * - HUNTER_MATE_NO_AUTOUPDATE=1 关闭全部自动更新(CLI + 一致性,preAction 层)
11
15
  * - HUNTER_MATE_UPDATE_CHECK_INTERVAL_MS 覆盖节流间隔(默认 3600000)
12
16
  * - HUNTER_MATE_DEV=1 强制视为 dev 模式(测试用)
13
17
  */
18
+ import * as fs from 'node:fs';
14
19
  import * as path from 'path';
20
+ import { spawnSync } from 'node:child_process';
15
21
  import { fileURLToPath } from 'node:url';
16
22
  import { createCliUpdater } from '@chromepilot/kit';
17
23
  import { readInstalledVersions } from './version.js';
18
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
25
  const HOME = () => process.env.HUNTER_MATE_HOME || process.env.HOME || process.env.USERPROFILE || '.';
20
26
  const ROOT = () => path.join(HOME(), '.hunter-mate');
27
+ const CLI_PREFIX = () => path.join(ROOT(), 'cli');
21
28
  /**
22
29
  * 判定是否开发软链模式(纯函数,便于单测):
23
30
  * 包根路径含 /worktrees/ 段(hc-hw local worktree 布局)或 HUNTER_MATE_DEV=1。
@@ -31,7 +38,7 @@ export function computeDevLinked(pkgRoot) {
31
38
  export function isDevLinked() {
32
39
  return computeDevLinked(path.join(__dirname, '..'));
33
40
  }
34
- const updater = createCliUpdater({
41
+ const kitUpdater = createCliUpdater({
35
42
  npmPackage: 'hunter-mate',
36
43
  envPrefix: 'HUNTER_MATE',
37
44
  stateDir: ROOT,
@@ -39,14 +46,73 @@ const updater = createCliUpdater({
39
46
  isDev: isDevLinked,
40
47
  });
41
48
  // ---------------------------------------------------------------------------
42
- // 兼容导出(调用方零改动)
49
+ // 复用 kit 的工具函数(版本查询、节流标记等)
43
50
  // ---------------------------------------------------------------------------
44
- export const npmLatestVersion = updater.npmLatestVersion;
45
- export const npmSelfUpdate = updater.npmSelfUpdate;
46
- export const updateCheckIntervalMs = updater.updateCheckIntervalMs;
47
- export const shouldCheckUpdate = updater.shouldCheckUpdate;
48
- export const markUpdateChecked = updater.markUpdateChecked;
49
- /** preAction 集成入口:返回日志行数组,永不 throw */
51
+ export const npmLatestVersion = kitUpdater.npmLatestVersion;
52
+ export const updateCheckIntervalMs = kitUpdater.updateCheckIntervalMs;
53
+ export const shouldCheckUpdate = kitUpdater.shouldCheckUpdate;
54
+ export const markUpdateChecked = kitUpdater.markUpdateChecked;
55
+ export function npmSelfUpdate(pkg = 'hunter-mate') {
56
+ const prefix = CLI_PREFIX();
57
+ fs.mkdirSync(prefix, { recursive: true });
58
+ // 优先使用用户通过环境变量指定的 npm 路径(沙盒内可能只有局部 npm)
59
+ const npmBin = process.env.HUNTER_MATE_NPM_BIN || 'npm';
60
+ const args = ['install', `${pkg}@latest`, '--prefix', prefix];
61
+ const r = spawnSync(npmBin, args, {
62
+ stdio: 'inherit',
63
+ timeout: 120_000,
64
+ env: {
65
+ ...process.env,
66
+ // 防止 npm 因为用户 home 不可写而失败;--prefix 已指定安装位置
67
+ npm_config_cache: path.join(prefix, '.npm-cache'),
68
+ },
69
+ });
70
+ if (r.status === 0) {
71
+ return {
72
+ ok: true,
73
+ msg: `${npmBin} install ${pkg}@latest --prefix ${prefix} 完成,下一条命令起使用新版本`,
74
+ };
75
+ }
76
+ return {
77
+ ok: false,
78
+ msg: `${npmBin} install ${pkg}@latest --prefix ${prefix} 失败 (exit=${r.status}),可手动执行`,
79
+ };
80
+ }
81
+ /**
82
+ * preAction 集成入口:返回日志行数组,永不 throw。
83
+ * 逻辑等价于 kit 的 autoCliUpdate,但安装命令走 --prefix。
84
+ */
50
85
  export async function maybeCliSelfUpdate() {
51
- return updater.autoCliUpdate();
86
+ const logs = [];
87
+ const outcome = { logs, devSkipped: false, updated: false };
88
+ try {
89
+ if (process.env.HUNTER_MATE_NO_AUTOUPDATE === '1')
90
+ return outcome;
91
+ if (isDevLinked()) {
92
+ outcome.devSkipped = true;
93
+ logs.push('dev 软链模式,跳过 CLI 自更新(改代码请 pnpm build;reload/restart 自愈不受影响)');
94
+ return outcome;
95
+ }
96
+ if (!shouldCheckUpdate())
97
+ return outcome;
98
+ markUpdateChecked();
99
+ const installed = readInstalledVersions().cli;
100
+ if (!installed || installed === '0.0.0')
101
+ return outcome;
102
+ const latest = npmLatestVersion();
103
+ if (!latest) {
104
+ logs.push(`npm registry 查询 hunter-mate 失败,本次跳过自更新`);
105
+ return outcome;
106
+ }
107
+ if (latest === installed)
108
+ return outcome;
109
+ logs.push(`CLI 自更新: ${installed} → ${latest}`);
110
+ const r = npmSelfUpdate();
111
+ logs.push(r.msg);
112
+ outcome.updated = r.ok;
113
+ }
114
+ catch (err) {
115
+ logs.push(`自动更新检测异常(不影响命令执行): ${err instanceof Error ? err.message : String(err)}`);
116
+ }
117
+ return outcome;
52
118
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "hunter-mate",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "HiredChina 猎头 Agent 数据助手 CLI:人才库/客户/职位数据访问,浏览器录制解析简历,server/扩展一致性自愈",
6
- "license": "UNLICENSED",
6
+ "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+github.com:hiredchina-com/hunter-mate.git"
@@ -31,7 +31,7 @@
31
31
  "open": "^8.4.0",
32
32
  "uuid": "^9.0.0",
33
33
  "ws": "^8.21.3",
34
- "@hunter-mate/shared": "0.1.2"
34
+ "@hunter-mate/shared": "0.1.4"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/better-sqlite3": "^7.6.0",