hunter-mate 0.1.6 → 0.1.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.
Files changed (120) hide show
  1. package/dist/assets/extension/background/service-worker.js +193 -16
  2. package/dist/assets/extension/manifest.json +1 -1
  3. package/dist/assets/extension/package.json +4 -3
  4. package/dist/assets/server/app.module.js +2 -1
  5. package/dist/assets/server/app.module.js.map +1 -1
  6. package/dist/assets/server/auth/auth.controller.d.ts +7 -0
  7. package/dist/assets/server/auth/auth.controller.js +20 -0
  8. package/dist/assets/server/auth/auth.controller.js.map +1 -1
  9. package/dist/assets/server/auth/auth.service.d.ts +11 -0
  10. package/dist/assets/server/auth/auth.service.js +58 -1
  11. package/dist/assets/server/auth/auth.service.js.map +1 -1
  12. package/dist/assets/server/build-info.json +1 -1
  13. package/dist/assets/server/extension/extension-control.controller.d.ts +3 -0
  14. package/dist/assets/server/extension/extension-control.controller.js +1 -1
  15. package/dist/assets/server/extension/extension-control.controller.js.map +1 -1
  16. package/dist/assets/server/extension/extension-control.service.d.ts +8 -0
  17. package/dist/assets/server/extension/extension-control.service.js +38 -1
  18. package/dist/assets/server/extension/extension-control.service.js.map +1 -1
  19. package/dist/assets/server/package.json +4 -3
  20. package/dist/assets/server/recording/recording-event-queue.service.d.ts +26 -0
  21. package/dist/assets/server/recording/recording-event-queue.service.js +115 -0
  22. package/dist/assets/server/recording/recording-event-queue.service.js.map +1 -0
  23. package/dist/assets/server/recording/recording-retention.service.d.ts +21 -0
  24. package/dist/assets/server/recording/recording-retention.service.js +85 -0
  25. package/dist/assets/server/recording/recording-retention.service.js.map +1 -0
  26. package/dist/assets/server/recording/recording.controller.d.ts +11 -1
  27. package/dist/assets/server/recording/recording.controller.js +18 -2
  28. package/dist/assets/server/recording/recording.controller.js.map +1 -1
  29. package/dist/assets/server/recording/recording.module.js +10 -1
  30. package/dist/assets/server/recording/recording.module.js.map +1 -1
  31. package/dist/assets/server/recording/recording.service.d.ts +5 -1
  32. package/dist/assets/server/recording/recording.service.js +21 -6
  33. package/dist/assets/server/recording/recording.service.js.map +1 -1
  34. package/dist/commands/cache.js +3 -4
  35. package/dist/commands/doctor.js +94 -12
  36. package/dist/commands/extension.js +14 -23
  37. package/dist/commands/extras.js +2 -2
  38. package/dist/commands/job.d.ts +4 -1
  39. package/dist/commands/job.js +51 -16
  40. package/dist/commands/login.d.ts +15 -0
  41. package/dist/commands/login.js +57 -12
  42. package/dist/commands/ops.js +18 -2
  43. package/dist/commands/record.js +2 -4
  44. package/dist/commands/report.js +2 -4
  45. package/dist/commands/search.js +9 -2
  46. package/dist/commands/server.d.ts +2 -2
  47. package/dist/commands/server.js +78 -17
  48. package/dist/commands/update.js +1 -1
  49. package/dist/i18n/locales/en-US.json +3 -1
  50. package/dist/i18n/locales/zh-CN.json +3 -1
  51. package/dist/index.js +38 -2
  52. package/dist/updater.d.ts +1 -1
  53. package/dist/updater.js +22 -5
  54. package/dist/utils/command-error.d.ts +9 -0
  55. package/dist/utils/command-error.js +31 -0
  56. package/dist/utils/proxy-detect.d.ts +20 -0
  57. package/dist/utils/proxy-detect.js +96 -0
  58. package/dist/utils/script-path.d.ts +5 -0
  59. package/dist/utils/script-path.js +27 -0
  60. package/dist/version.js +1 -1
  61. package/dist/watchdog.d.ts +23 -1
  62. package/dist/watchdog.js +95 -4
  63. package/dist/web/dist/404.html +1 -0
  64. package/dist/web/dist/_next/static/chunks/166.f30be223f694f8ee.js +1 -0
  65. package/dist/web/dist/_next/static/chunks/1e6d5a0c.0488932e17ecdf24.js +15 -0
  66. package/dist/web/dist/_next/static/chunks/539-5d0e8c5c238c56f8.js +1 -0
  67. package/dist/web/dist/_next/static/chunks/589-d632233641f35681.js +1 -0
  68. package/dist/web/dist/_next/static/chunks/671.3b8e9cc96b25b6ea.js +1 -0
  69. package/dist/web/dist/_next/static/chunks/935-99f57b7d4151d9e7.js +5 -0
  70. package/dist/web/dist/_next/static/chunks/a7067448-96dd4b8457c447e8.js +1 -0
  71. package/dist/web/dist/_next/static/chunks/app/_not-found/page-fdf1a13a7656886a.js +1 -0
  72. package/dist/web/dist/_next/static/chunks/app/auth/verify/page-8e5cff53d6f7d8e1.js +1 -0
  73. package/dist/web/dist/_next/static/chunks/app/companies/page-e38a69e1929d0953.js +1 -0
  74. package/dist/web/dist/_next/static/chunks/app/entity/page-250f8a9b6d91ab19.js +1 -0
  75. package/dist/web/dist/_next/static/chunks/app/jobs/page-6a281776fec065f6.js +1 -0
  76. package/dist/web/dist/_next/static/chunks/app/layout-83e623e973c1693f.js +1 -0
  77. package/dist/web/dist/_next/static/chunks/app/logs/page-9f41d44fa1add723.js +1 -0
  78. package/dist/web/dist/_next/static/chunks/app/matching/jobs/[id]/talents/page-f57f4b7529823601.js +1 -0
  79. package/dist/web/dist/_next/static/chunks/app/matching/portraits/[id]/jobs/page-614d1e62d7b9d215.js +1 -0
  80. package/dist/web/dist/_next/static/chunks/app/page-e3392de08ae81fba.js +1 -0
  81. package/dist/web/dist/_next/static/chunks/app/parselog/page-58d3acece95b4089.js +1 -0
  82. package/dist/web/dist/_next/static/chunks/app/recordings/page-e2e2e3c3a64083fb.js +1 -0
  83. package/dist/web/dist/_next/static/chunks/app/snapshots/[id]/agent-prompt/page-fd3b7d7373b8f8b7.js +1 -0
  84. package/dist/web/dist/_next/static/chunks/app/talents/page-30748338da20d7e0.js +1 -0
  85. package/dist/web/dist/_next/static/chunks/framework-b3a558b17a070965.js +1 -0
  86. package/dist/web/dist/_next/static/chunks/main-5a4e0cd75dd7f04b.js +1 -0
  87. package/dist/web/dist/_next/static/chunks/main-app-80b038a312917d90.js +1 -0
  88. package/dist/web/dist/_next/static/chunks/pages/_app-da7d628f49271ece.js +4 -0
  89. package/dist/web/dist/_next/static/chunks/pages/_error-17756f67e0a11350.js +1 -0
  90. package/dist/web/dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  91. package/dist/web/dist/_next/static/chunks/webpack-1784baecc6fdcf0b.js +1 -0
  92. package/dist/web/dist/_next/static/css/db881c5b3072b88a.css +3 -0
  93. package/dist/web/dist/_next/static/css/f60d210841fab76b.css +1 -0
  94. package/dist/web/dist/_next/static/hqJ-7vr4X71DNfzZugHJ-/_buildManifest.js +1 -0
  95. package/dist/web/dist/_next/static/hqJ-7vr4X71DNfzZugHJ-/_ssgManifest.js +1 -0
  96. package/dist/web/dist/auth/verify.html +1 -0
  97. package/dist/web/dist/auth/verify.txt +8 -0
  98. package/dist/web/dist/companies.html +1 -0
  99. package/dist/web/dist/companies.txt +7 -0
  100. package/dist/web/dist/entity.html +1 -0
  101. package/dist/web/dist/entity.txt +8 -0
  102. package/dist/web/dist/index.html +1 -0
  103. package/dist/web/dist/index.txt +8 -0
  104. package/dist/web/dist/jobs.html +1 -0
  105. package/dist/web/dist/jobs.txt +7 -0
  106. package/dist/web/dist/logs.html +1 -0
  107. package/dist/web/dist/logs.txt +8 -0
  108. package/dist/web/dist/matching/jobs/placeholder/talents.html +1 -0
  109. package/dist/web/dist/matching/jobs/placeholder/talents.txt +8 -0
  110. package/dist/web/dist/matching/portraits/placeholder/jobs.html +1 -0
  111. package/dist/web/dist/matching/portraits/placeholder/jobs.txt +8 -0
  112. package/dist/web/dist/parselog.html +1 -0
  113. package/dist/web/dist/parselog.txt +8 -0
  114. package/dist/web/dist/recordings.html +1 -0
  115. package/dist/web/dist/recordings.txt +8 -0
  116. package/dist/web/dist/snapshots/placeholder/agent-prompt.html +1 -0
  117. package/dist/web/dist/snapshots/placeholder/agent-prompt.txt +8 -0
  118. package/dist/web/dist/talents.html +1 -0
  119. package/dist/web/dist/talents.txt +7 -0
  120. package/package.json +25 -5
@@ -4,6 +4,7 @@ import { loadConfig } from '../config.js';
4
4
  import { getToken } from '../keychain.js';
5
5
  import { ensureCacheDir, saveJobCase, saveCommunicationRecord, getDb } from '../cache/db.js';
6
6
  import { getCliLogger } from '../logger.js';
7
+ import { printCommandError } from '../utils/command-error.js';
7
8
  export async function runCacheSync() {
8
9
  const config = loadConfig();
9
10
  const token = await getToken();
@@ -43,10 +44,8 @@ cacheCommand
43
44
  }
44
45
  catch (err) {
45
46
  // patchConsole 已统一转发,但 logger-kit 提供结构化字段,
46
- // 业务错误统一走 getCliLogger().error
47
- getCliLogger().error('cache:sync-failed', {
48
- error: err.response?.data?.message || err.message,
49
- });
47
+ // 业务错误统一走 getCliLogger().error;终端可见性由 printCommandError 保证
48
+ printCommandError('cache:sync-failed', err);
50
49
  process.exit(1);
51
50
  }
52
51
  });
@@ -67,6 +67,27 @@ export async function runDoctor(opts) {
67
67
  status: nodeVersionFriendly ? 'ok' : 'warn',
68
68
  detail: `Node ${process.versions.node}${nodeVersionFriendly ? '' : ' (hunter-mate 预构建包对 Node 20/22/24 支持最好,Windows 无编译器时建议切到 Node 22 LTS)'}`,
69
69
  });
70
+ // ── 0.5) 死代理检测(Windows 现场:npm registry 查询失败 / prebuild 下载失败
71
+ // 回退 7 分钟源码编译的根因是 shell 残留指向死端口的代理变量) ──
72
+ try {
73
+ const { findDeadProxies, buildUnsetHint } = await import('../utils/proxy-detect.js');
74
+ const dead = await findDeadProxies(800);
75
+ if (dead.length) {
76
+ checks.push({ key: 'proxy_env', status: 'warn', detail: buildUnsetHint(dead) });
77
+ hints.push('清除死代理环境变量后重试(见 proxy_env 检查项的各 shell 指令)');
78
+ }
79
+ else {
80
+ const proxyEnv = (await import('../utils/proxy-detect.js')).collectProxyEnv();
81
+ checks.push({
82
+ key: 'proxy_env',
83
+ status: 'ok',
84
+ detail: proxyEnv.length
85
+ ? `代理变量已设置且端口存活: ${proxyEnv.map((p) => `${p.varName}=${p.value}`).join(', ')}`
86
+ : '未设置代理环境变量(直连)',
87
+ });
88
+ }
89
+ }
90
+ catch { /* 检测失败不阻断 doctor */ }
70
91
  // ── 1) 目录 & token ──
71
92
  let initResult = null;
72
93
  try {
@@ -160,9 +181,17 @@ export async function runDoctor(opts) {
160
181
  }
161
182
  else {
162
183
  try {
163
- const r = await axios.get(`${config.serverUrl}/health`, { timeout: 3000 });
184
+ const r = await axios.get(`${config.serverUrl}/health`, { timeout: 3000, proxy: false });
164
185
  serverUp = true;
165
- checks.push({ key: 'server', status: 'ok', detail: `${config.serverUrl} (HTTP ${r.status})` });
186
+ const hv = r.data || {};
187
+ checks.push({
188
+ key: 'server',
189
+ status: 'ok',
190
+ detail: `${config.serverUrl} (HTTP ${r.status}` +
191
+ (hv.version ? `, server v${hv.version}` : '') +
192
+ (hv.buildTime ? `, built ${hv.buildTime}` : '') +
193
+ ')',
194
+ });
166
195
  }
167
196
  catch (e) {
168
197
  checks.push({ key: 'server', status: 'warn', detail: `${config.serverUrl} unreachable (${e.code || e.message})` });
@@ -227,7 +256,28 @@ export async function runDoctor(opts) {
227
256
  if (serverUp) {
228
257
  await runExtensionCheck(5000);
229
258
  extConnected = true;
230
- checks.push({ key: 'extension_ws', status: 'ok', detail: '扩展已连接, WebSocket 通道就绪' });
259
+ // 证据化输出:列出已连接扩展实例(id/版本/连接时间)。
260
+ // 现场教训:doctor 报"已连"与任务执行时"未连"曾互相矛盾,
261
+ // 把时间戳摆出来用户才能判断报告是否过期(12:03 连过 ≠ 16:32 还连着)。
262
+ let detail = '扩展已连接, WebSocket 通道就绪';
263
+ try {
264
+ const r = await axios.get(`${config.serverUrl}/extensions`, { timeout: 3000, proxy: false });
265
+ const clients = (Array.isArray(r.data) ? r.data : []);
266
+ if (clients.length) {
267
+ const brief = clients
268
+ .map((c) => {
269
+ const at = c.connectedAt ? new Date(c.connectedAt).toLocaleString() : '?';
270
+ return `${c.extensionId ?? 'unknown'}@${c.version ?? '?'} (connectedAt ${at})`;
271
+ })
272
+ .join(', ');
273
+ detail = `扩展已连接 (${clients.length} 实例): ${brief}`;
274
+ }
275
+ else {
276
+ detail = '扩展已连接,但 /extensions 注册表为空(竞态,建议重跑 doctor 确认)';
277
+ }
278
+ }
279
+ catch { /* 证据获取失败不阻断结论 */ }
280
+ checks.push({ key: 'extension_ws', status: 'ok', detail });
231
281
  }
232
282
  else {
233
283
  checks.push({ key: 'extension_ws', status: 'skip', detail: 'server 未运行, 跳过扩展连通检查' });
@@ -256,19 +306,51 @@ export async function runDoctor(opts) {
256
306
  catch { }
257
307
  }
258
308
  }
259
- // ── 5) 登录态(不强制) ──
309
+ // ── 5) 登录态(token 存在性 + 真实探测, fix #8) ──
310
+ // doctor/* 路由本身豁免认证,查 token 存在性会假阳性;
311
+ // 带 token 调受保护端点(/job-cases)才能确认登录真的有效。
260
312
  let loggedIn = false;
261
313
  try {
262
- const { getToken } = await import('../keychain.js');
263
314
  const token = await getToken();
264
- loggedIn = !!token;
265
- checks.push({
266
- key: 'auth',
267
- status: token ? 'ok' : 'warn',
268
- detail: token ? `已登录 (token head=${String(token).slice(0, 6)}...)` : '未登录, 数据面 API 可能受限',
269
- });
270
- if (!token)
315
+ if (!token) {
316
+ checks.push({
317
+ key: 'auth',
318
+ status: 'warn',
319
+ detail: '未登录, 数据面 API 可能受限',
320
+ });
271
321
  hints.push('hunter-mate login');
322
+ }
323
+ else if (!serverUp) {
324
+ loggedIn = true;
325
+ checks.push({
326
+ key: 'auth',
327
+ status: 'ok',
328
+ detail: '已登录 (server 未运行, 仅验证 token 存在性)',
329
+ });
330
+ }
331
+ else {
332
+ try {
333
+ const r = await axios.get(`${config.serverUrl}/job-cases`, {
334
+ timeout: 5000,
335
+ proxy: false,
336
+ headers: { Authorization: `UTOKEN ${token}` },
337
+ });
338
+ loggedIn = true;
339
+ checks.push({ key: 'auth', status: 'ok', detail: `已登录且 token 有效 (HTTP ${r.status})` });
340
+ }
341
+ catch (e) {
342
+ const status = e?.response?.status;
343
+ if (status === 401 || status === 403) {
344
+ // 真实 fail: server 明确拒绝了这个 token
345
+ checks.push({ key: 'auth', status: 'fail', detail: `token 已被 server 拒绝 (HTTP ${status}), 登录已过期或无权访问` });
346
+ hints.push('重新登录: hunter-mate login');
347
+ }
348
+ else {
349
+ // 网络/其他原因: 标 warn,不误报 fail
350
+ checks.push({ key: 'auth', status: 'warn', detail: `token 存在但探测失败(网络或服务异常): ${e.message}` });
351
+ }
352
+ }
353
+ }
272
354
  }
273
355
  catch (e) {
274
356
  checks.push({ key: 'auth', status: 'skip', detail: `跳过(keychain 读取失败): ${e.message}` });
@@ -6,7 +6,7 @@ import * as path from 'path';
6
6
  import { loadConfig } from '../config.js';
7
7
  import { i18n } from '../i18n/index.js';
8
8
  import { showInstallGuide, runInstall } from './install.js';
9
- import { getCliLogger } from '../logger.js';
9
+ import { printCommandError } from '../utils/command-error.js';
10
10
  const B = {
11
11
  reset: '\x1b[0m', bold: '\x1b[1m',
12
12
  green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m', magenta: '\x1b[35m',
@@ -37,9 +37,7 @@ async function httpAuth(baseUrl) {
37
37
  function requireServerUrl() {
38
38
  const config = loadConfig();
39
39
  if (!config.serverUrl) {
40
- getCliLogger().error('extension:no-server-url', {
41
- hint: '请先 hunter-mate config 设置 serverUrl',
42
- });
40
+ printCommandError('extension:no-server-url', new Error('请先 hunter-mate config 设置 serverUrl'));
43
41
  process.exit(1);
44
42
  }
45
43
  return config.serverUrl;
@@ -128,9 +126,7 @@ export function ensureEnvReady() {
128
126
  catch { /* ignore */ }
129
127
  }
130
128
  if (!serverUp) {
131
- getCliLogger().error('extension:server-start-failed', {
132
- hint: '请手动执行 hunter-mate start -f',
133
- });
129
+ printCommandError('extension:server-start-failed', new Error('请手动执行 hunter-mate start -f'));
134
130
  process.exit(1);
135
131
  }
136
132
  // 2) 检查扩展是否安装
@@ -145,9 +141,7 @@ export function ensureEnvReady() {
145
141
  const r = runInstall(init.extensionDir);
146
142
  if (!r.source) {
147
143
  // 内置 dist 不存在, 无法自动安装
148
- getCliLogger().error('extension:auto-install-failed', {
149
- hint: '请在 hunter-mate monorepo 执行 pnpm --filter @hunter-mate/extension build,或手动 hunter-mate install',
150
- });
144
+ printCommandError('extension:auto-install-failed', new Error('内置 extension dist 不存在,请在 hunter-mate monorepo 执行 pnpm --filter @hunter-mate/extension build,或手动 hunter-mate install'));
151
145
  process.exit(1);
152
146
  }
153
147
  console.log(`${B.green}✓ 扩展已自动安装到 ${r.target}${B.reset}`);
@@ -189,9 +183,7 @@ export function ensureEnvReady() {
189
183
  }
190
184
  catch {
191
185
  // /extensions 端点不可达
192
- getCliLogger().error('extension:check-failed', {
193
- hint: 'Server 可能未正确启动, 请执行 hunter-mate start -f',
194
- });
186
+ printCommandError('extension:check-failed', new Error('Server 可能未正确启动, 请执行 hunter-mate start -f'));
195
187
  process.exit(1);
196
188
  }
197
189
  })();
@@ -230,7 +222,6 @@ export async function runExtensionCheck(timeoutMs = 5000) {
230
222
  }
231
223
  registered = true;
232
224
  ws.close();
233
- globalThis.__extensionCount = count;
234
225
  resolve();
235
226
  })
236
227
  .catch((err) => {
@@ -256,8 +247,8 @@ async function withEnvCheck(fn) {
256
247
  await fn();
257
248
  }
258
249
  catch (err) {
250
+ printCommandError('extension:exec-failed', err);
259
251
  const msg = err.response?.data?.message ?? err.response?.data?.error ?? err.message;
260
- getCliLogger().error('extension:exec-failed', { error: msg });
261
252
  if (msg.includes('no Hunter Mate Chrome extension registered') || msg.includes('no Hunter Mate')) {
262
253
  const init = await import('./init.js').then((m) => m.runInit());
263
254
  showInstallGuide(init.extensionDir);
@@ -284,9 +275,7 @@ async function cmdControl(command, argsJson, opts) {
284
275
  params = JSON.parse(argsJson);
285
276
  }
286
277
  catch {
287
- getCliLogger().error('extension:invalid-control-args', {
288
- hint: 'args 必须是合法 JSON 字符串',
289
- });
278
+ console.error('✗ extension:invalid-control-args: args 必须是合法 JSON 字符串');
290
279
  process.exit(1);
291
280
  }
292
281
  }
@@ -320,6 +309,7 @@ async function cmdEval(expression, opts) {
320
309
  expression,
321
310
  timeoutMs: opts.timeout ? Number(opts.timeout) : undefined,
322
311
  extensionId: opts.extensionId,
312
+ sessionId: opts.sessionId ?? opts.taskId,
323
313
  });
324
314
  printResult(r.data, opts.format);
325
315
  });
@@ -355,9 +345,7 @@ async function cmdRun(site, command, opts) {
355
345
  params = JSON.parse(opts.args);
356
346
  }
357
347
  catch {
358
- getCliLogger().error('extension:invalid-run-args', {
359
- hint: 'args 必须是合法 JSON 字符串',
360
- });
348
+ console.error('✗ extension:invalid-run-args: args 必须是合法 JSON 字符串');
361
349
  process.exit(1);
362
350
  }
363
351
  }
@@ -387,11 +375,14 @@ extensionCommand
387
375
  .action(async () => {
388
376
  try {
389
377
  await ensureEnvReady();
390
- const count = globalThis.__extensionCount ?? 0;
378
+ // fix #31: 用 /extensions 真实查询的计数, 不再读 globalThis.__extensionCount(可能是 stale 0)
379
+ const serverUrl = requireServerUrl();
380
+ const r = await (await httpAuth(serverUrl)).get('/extensions', { timeout: 5000 });
381
+ const count = Array.isArray(r.data) ? r.data.length : 0;
391
382
  console.log(`\n${B.green}✓ Hunter Mate 扩展已连接 (${count} 个浏览器实例)${B.reset}\n`);
392
383
  }
393
384
  catch (err) {
394
- getCliLogger().error('extension:check-error', { error: err.message });
385
+ printCommandError('extension:check-error', err);
395
386
  process.exit(1);
396
387
  }
397
388
  });
@@ -128,7 +128,7 @@ export const reloadCommand = new Command('reload')
128
128
  console.log(r.msg);
129
129
  }
130
130
  catch (e) {
131
- getCliLogger().error('extras:reload-failed', { error: e.message });
131
+ console.error(`✗ extras:reload-failed: ${e.message}`);
132
132
  process.exit(1);
133
133
  }
134
134
  });
@@ -241,7 +241,7 @@ export async function runPurge(opts) {
241
241
  // 3) 停止 server(先于目录删除,避免 server 还在写日志导致删不干净)
242
242
  let serverStopResult = null;
243
243
  if (livePid || exists && fs.existsSync(path.join(root, 'server.pid'))) {
244
- serverStopResult = runStop();
244
+ serverStopResult = await runStop();
245
245
  }
246
246
  // 4) 删除目录
247
247
  let removedFiles = info.files;
@@ -5,5 +5,8 @@ export interface CreateJobCaseResult {
5
5
  status: string;
6
6
  }
7
7
  export declare function runJobCreate(folderPath: string): Promise<CreateJobCaseResult>;
8
- export declare function runJobList(): any[];
8
+ /**
9
+ * 优先查 server 的 /job-cases;server 不可达或未登录时兜底本地 sqlite 缓存。
10
+ */
11
+ export declare function runJobList(): Promise<any[]>;
9
12
  export declare const jobCommand: Command;
@@ -7,7 +7,23 @@ import { ensureCacheDir, saveJobCase, listJobCases } from '../cache/db.js';
7
7
  import { loadConfig } from '../config.js';
8
8
  import { getToken } from '../keychain.js';
9
9
  import { i18n } from '../i18n/index.js';
10
- import { getCliLogger } from '../logger.js';
10
+ import { printCommandError } from '../utils/command-error.js';
11
+ /** 递归收集目录下的 .md/.txt/.pdf(含 raw/jd/ 等子目录),键为相对路径(根目录文件不带前缀)。 */
12
+ function collectJobFiles(dir, relPrefix = '', out = {}) {
13
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
14
+ // 根目录文件优先,再递归子目录,保证 keys[0] 是根目录第一个文件(用于 title)
15
+ for (const entry of entries) {
16
+ if (entry.isFile() && /\.(md|txt|pdf)$/.test(entry.name)) {
17
+ out[relPrefix + entry.name] = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
18
+ }
19
+ }
20
+ for (const entry of entries) {
21
+ if (entry.isDirectory()) {
22
+ collectJobFiles(path.join(dir, entry.name), relPrefix + entry.name + '/', out);
23
+ }
24
+ }
25
+ return out;
26
+ }
11
27
  export async function runJobCreate(folderPath) {
12
28
  const config = loadConfig();
13
29
  const token = await getToken();
@@ -16,13 +32,14 @@ export async function runJobCreate(folderPath) {
16
32
  if (!fs.existsSync(resolved)) {
17
33
  throw new Error(`Folder not found: ${resolved}`);
18
34
  }
19
- const files = fs.readdirSync(resolved).filter((f) => f.endsWith('.md') || f.endsWith('.txt') || f.endsWith('.pdf'));
20
- const rawFiles = {};
21
- for (const f of files) {
22
- rawFiles[f] = fs.readFileSync(path.join(resolved, f), 'utf-8');
35
+ const rawFiles = collectJobFiles(resolved);
36
+ if (!Object.keys(rawFiles).length) {
37
+ throw new Error(`Folder 内未找到 .md/.txt/.pdf 文件: ${resolved}`);
23
38
  }
24
39
  const rawText = Object.values(rawFiles).join('\n---\n');
25
- const title = Object.keys(rawFiles)[0]?.replace(/\.[^.]+$/, '') || 'Untitled JobCase';
40
+ const firstKey = Object.keys(rawFiles)[0];
41
+ const titleBase = firstKey.includes('/') ? firstKey.split('/').pop() : firstKey;
42
+ const title = titleBase.replace(/\.[^.]+$/, '') || 'Untitled JobCase';
26
43
  const res = await axios.post(`${config.serverUrl}/job-cases`, {
27
44
  externalId: uuidv4(),
28
45
  title,
@@ -36,7 +53,7 @@ export async function runJobCreate(folderPath) {
36
53
  externalId: serverCase.externalId,
37
54
  parsedFrom: {
38
55
  folderPath: resolved,
39
- fileNames: files.join(','),
56
+ fileNames: Object.keys(rawFiles).join(','),
40
57
  rawText,
41
58
  },
42
59
  requirements: {},
@@ -47,8 +64,28 @@ export async function runJobCreate(folderPath) {
47
64
  saveJobCase(jobCase);
48
65
  return serverCase;
49
66
  }
50
- export function runJobList() {
51
- return listJobCases();
67
+ /**
68
+ * 优先查 server 的 /job-cases;server 不可达或未登录时兜底本地 sqlite 缓存。
69
+ */
70
+ export async function runJobList() {
71
+ const config = loadConfig();
72
+ const token = await getToken().catch(() => undefined);
73
+ try {
74
+ const res = await axios.get(`${config.serverUrl}/job-cases`, {
75
+ headers: token ? { Authorization: `UTOKEN ${token}` } : {},
76
+ proxy: false,
77
+ timeout: 8000,
78
+ });
79
+ const list = Array.isArray(res.data) ? res.data : [];
80
+ // server 为空时兜底本地缓存(server 数据可能被清理)
81
+ return list.length ? list : listJobCases();
82
+ }
83
+ catch (err) {
84
+ if (err?.response?.status === 401 || err?.response?.status === 403) {
85
+ console.error('登录已过期或无权访问,请运行 hunter-mate login 后重试(本次展示本地缓存数据)');
86
+ }
87
+ return listJobCases();
88
+ }
52
89
  }
53
90
  export const jobCommand = new Command('job')
54
91
  .description(i18n.t('job.description'));
@@ -61,16 +98,14 @@ jobCommand
61
98
  console.log(`Created JobCase ${serverCase.id} (${serverCase.externalId})`);
62
99
  }
63
100
  catch (err) {
64
- getCliLogger().error('job:create-failed', {
65
- error: err.response?.data?.message || err.message,
66
- });
101
+ printCommandError('job:create-failed', err);
67
102
  process.exit(1);
68
103
  }
69
104
  });
70
105
  jobCommand
71
106
  .command('list')
72
- .description('List local JobCases')
73
- .action(() => {
74
- const cases = runJobList();
75
- console.table(cases.map((c) => ({ id: c.id, folder: c.parsedFrom?.folderPath, status: c.status, createdAt: c.createdAt })));
107
+ .description('List JobCases (优先 server,失败兜底本地缓存)')
108
+ .action(async () => {
109
+ const cases = await runJobList();
110
+ console.table(cases.map((c) => ({ id: c.id, folder: c.parsedFrom?.folderPath ?? c.folderPath, status: c.status, createdAt: c.createdAt })));
76
111
  });
@@ -1,3 +1,18 @@
1
1
  import { Command } from 'commander';
2
+ export type EmailLoginSource = 'server' | 'saas';
3
+ /**
4
+ * 邮箱登录第一步:发送验证码。
5
+ * 优先走本地 server(SaaS 代理模式,verify 后拿到的是本地自签 UTOKEN,
6
+ * 受保护路由立即可用);server 不可达时 fallback 直连 SaaS。
7
+ */
8
+ export declare function sendEmailCode(email: string): Promise<EmailLoginSource>;
9
+ /**
10
+ * 邮箱登录第二步:校验验证码并保存 token。
11
+ * 分支选择与 sendEmailCode 保持一致:server 在线走本地代理自签,否则直连 SaaS。
12
+ */
13
+ export declare function verifyEmailCode(email: string, code: string): Promise<{
14
+ token: string;
15
+ source: EmailLoginSource;
16
+ }>;
2
17
  export declare function runLogin(): Promise<string>;
3
18
  export declare const loginCommand: Command;
@@ -8,6 +8,51 @@ import { getCliLogger } from '../logger.js';
8
8
  function sleep(ms) {
9
9
  return new Promise((resolve) => setTimeout(resolve, ms));
10
10
  }
11
+ /** 探测本地 server 是否在线(参照 doctor 的 server 检测) */
12
+ async function isLocalServerReachable(serverUrl) {
13
+ try {
14
+ await axios.get(`${serverUrl}/health`, { timeout: 2000, proxy: false });
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * 邮箱登录第一步:发送验证码。
23
+ * 优先走本地 server(SaaS 代理模式,verify 后拿到的是本地自签 UTOKEN,
24
+ * 受保护路由立即可用);server 不可达时 fallback 直连 SaaS。
25
+ */
26
+ export async function sendEmailCode(email) {
27
+ const config = loadConfig();
28
+ if (await isLocalServerReachable(config.serverUrl)) {
29
+ await axios.post(`${config.serverUrl}/auth/email/send-code`, { email }, { proxy: false });
30
+ return 'server';
31
+ }
32
+ await axios.post(`${config.saasUrl}/hunter-mate/auth/email/send-code`, { email }, { proxy: false });
33
+ return 'saas';
34
+ }
35
+ /**
36
+ * 邮箱登录第二步:校验验证码并保存 token。
37
+ * 分支选择与 sendEmailCode 保持一致:server 在线走本地代理自签,否则直连 SaaS。
38
+ */
39
+ export async function verifyEmailCode(email, code) {
40
+ const config = loadConfig();
41
+ if (await isLocalServerReachable(config.serverUrl)) {
42
+ const { data } = await axios.post(`${config.serverUrl}/auth/email/verify`, { email, code }, { proxy: false });
43
+ if (!data.access_token) {
44
+ throw new Error('verify returned no access_token');
45
+ }
46
+ await saveToken(data.access_token);
47
+ return { token: data.access_token, source: 'server' };
48
+ }
49
+ const { data } = await axios.post(`${config.saasUrl}/hunter-mate/auth/email/verify`, { email, code }, { proxy: false });
50
+ if (!data.access_token) {
51
+ throw new Error('verify returned no access_token');
52
+ }
53
+ await saveToken(data.access_token);
54
+ return { token: data.access_token, source: 'saas' };
55
+ }
11
56
  export async function runLogin() {
12
57
  const config = loadConfig();
13
58
  const { data: deviceCode } = await axios.post(`${config.serverUrl}/auth/device-code`);
@@ -44,27 +89,27 @@ export const loginCommand = new Command('login')
44
89
  .option('--code <code>', 'Email verification code (6 digits, step 2)')
45
90
  .action(async (options) => {
46
91
  try {
47
- const config = loadConfig();
48
- // 邮箱验证码登录(两步式无状态;SaaS 服务端,不经本地 server)
92
+ // 邮箱验证码登录(两步式无状态):优先走本地 server(SaaS 代理,自签 UTOKEN),
93
+ // server 不在线时 fallback 直连 SaaS 并警告(FID-057 #8)
49
94
  if (options.email) {
50
95
  if (!options.code) {
51
96
  // 第一步:发送验证码
52
- await axios.post(`${config.saasUrl}/hunter-mate/auth/email/send-code`, {
53
- email: options.email,
54
- });
55
- getCliLogger().info('login:email-code-sent', { email: options.email });
97
+ const source = await sendEmailCode(options.email);
98
+ if (source === 'saas') {
99
+ console.warn(i18n.t('login.serverOfflineSaasFallback'));
100
+ }
101
+ getCliLogger().info('login:email-code-sent', { email: options.email, source });
56
102
  console.log(`${i18n.t('login.emailCodeSent')} ${options.email}`);
57
103
  console.log(` ${i18n.t('login.emailCodeNextStep')}`);
58
104
  return;
59
105
  }
60
106
  // 第二步:校验验证码 → 保存 token
61
- const { data } = await axios.post(`${config.saasUrl}/hunter-mate/auth/email/verify`, { email: options.email, code: options.code });
62
- if (!data.access_token) {
63
- getCliLogger().error('login:email-verify-no-token');
64
- throw new Error('verify returned no access_token');
107
+ const { source } = await verifyEmailCode(options.email, options.code);
108
+ if (source === 'saas') {
109
+ console.warn(i18n.t('login.serverOfflineSaasFallback'));
110
+ console.warn(i18n.t('login.serverOfflineReloginHint'));
65
111
  }
66
- await saveToken(data.access_token);
67
- getCliLogger().info('login:token-saved', { method: 'email', email: options.email });
112
+ getCliLogger().info('login:token-saved', { method: 'email', email: options.email, source });
68
113
  console.log(i18n.t('login.success'));
69
114
  return;
70
115
  }
@@ -91,7 +91,23 @@ withFormat(recordingsCommand.command('show <sessionId>').description('查看录
91
91
  printResult({ detail, parsed }, opts.format);
92
92
  }
93
93
  catch (e) {
94
- getCliLogger().error('ops:recordings-show-failed', { error: e.message });
94
+ console.error(`✗ ops:recordings-show-failed: ${e.message}`);
95
+ process.exit(1);
96
+ }
97
+ });
98
+ // 手动触发保留期清理(FID-057 #17-B);server 另有每日定时清理
99
+ recordingsCommand
100
+ .command('prune')
101
+ .description('按保留期清理录制事件与快照 (POST /api/recordings/prune)')
102
+ .option('--days <n>', '保留天数(默认取 server 的 RECORDING_RETENTION_DAYS,通常 30)')
103
+ .action(async (opts) => {
104
+ try {
105
+ const body = opts.days !== undefined ? { days: Number(opts.days) } : {};
106
+ const r = await http().post('/api/recordings/prune', body);
107
+ console.log(JSON.stringify(r.data, null, 2));
108
+ }
109
+ catch (e) {
110
+ console.error(`✗ ops:recordings-prune-failed: ${e.message}`);
95
111
  process.exit(1);
96
112
  }
97
113
  });
@@ -147,7 +163,7 @@ export const adapterCommand = withFormat(new Command('adapter')
147
163
  }
148
164
  catch (e) {
149
165
  const detail = e.response?.data?.message || e.message;
150
- getCliLogger().error('ops:adapter-extract-failed', { error: detail });
166
+ console.error(`✗ ops:adapter-extract-failed: ${detail}`);
151
167
  process.exit(1);
152
168
  }
153
169
  return;
@@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
5
5
  import { loadConfig } from '../config.js';
6
6
  import { getToken } from '../keychain.js';
7
7
  import { ensureCacheDir } from '../cache/db.js';
8
- import { getCliLogger } from '../logger.js';
8
+ import { printCommandError } from '../utils/command-error.js';
9
9
  export async function runRecordTranscript(filePath, options) {
10
10
  const config = loadConfig();
11
11
  const token = await getToken();
@@ -42,9 +42,7 @@ recordCommand
42
42
  console.log('请使用本地 LLM 对转写文本做 NER/摘要/实体链接后确认归档。');
43
43
  }
44
44
  catch (err) {
45
- getCliLogger().error('record:upload-failed', {
46
- error: err.response?.data?.message || err.message,
47
- });
45
+ printCommandError('record:upload-failed', err);
48
46
  process.exit(1);
49
47
  }
50
48
  });
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import axios from 'axios';
3
3
  import { loadConfig } from '../config.js';
4
4
  import { getToken } from '../keychain.js';
5
- import { getCliLogger } from '../logger.js';
5
+ import { printCommandError } from '../utils/command-error.js';
6
6
  export async function runReportSales(customerId, options) {
7
7
  const config = loadConfig();
8
8
  const token = await getToken();
@@ -42,9 +42,7 @@ reportCommand
42
42
  console.log('> 请使用本地 LLM 基于以上原始数据生成最终销售方案与交付节奏。');
43
43
  }
44
44
  catch (err) {
45
- getCliLogger().error('report:generate-failed', {
46
- error: err.response?.data?.message || err.message,
47
- });
45
+ printCommandError('report:generate-failed', err);
48
46
  process.exit(1);
49
47
  }
50
48
  });
@@ -3,6 +3,7 @@ import axios from 'axios';
3
3
  import { loadConfig } from '../config.js';
4
4
  import { getToken } from '../keychain.js';
5
5
  import { i18n } from '../i18n/index.js';
6
+ import { printCommandError } from '../utils/command-error.js';
6
7
  export async function runSearch(options) {
7
8
  const config = loadConfig();
8
9
  const token = await getToken();
@@ -34,6 +35,12 @@ export const searchCommand = new Command('search')
34
35
  .option('--source <sources>', i18n.t('search.source'), 'u_person')
35
36
  .option('--top <n>', i18n.t('search.top'), '10')
36
37
  .action(async (options) => {
37
- const data = await runSearch(options);
38
- console.log(JSON.stringify(data, null, 2));
38
+ try {
39
+ const data = await runSearch(options);
40
+ console.log(JSON.stringify(data, null, 2));
41
+ }
42
+ catch (err) {
43
+ printCommandError('search:failed', err);
44
+ process.exit(1);
45
+ }
39
46
  });
@@ -14,10 +14,10 @@ export declare function runStart(opts: {
14
14
  url: string;
15
15
  msg: string;
16
16
  }>;
17
- export declare function runStop(): {
17
+ export declare function runStop(): Promise<{
18
18
  stopped: boolean;
19
19
  pid: number | null;
20
20
  msg: string;
21
- };
21
+ }>;
22
22
  export declare const startCommand: Command;
23
23
  export declare const stopCommand: Command;