hunter-mate 0.1.7 → 0.1.8

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 (220) hide show
  1. package/dist/assets/extension/background/service-worker.js +727 -152
  2. package/dist/assets/extension/build-info.json +1 -0
  3. package/dist/assets/extension/content/index.js +22 -4
  4. package/dist/assets/extension/manifest.json +5 -4
  5. package/dist/assets/extension/offscreen/offscreen.html +8 -0
  6. package/dist/assets/extension/offscreen/offscreen.js +149 -0
  7. package/dist/assets/extension/package.json +8 -1
  8. package/dist/assets/extension/sidepanel/index.css +2 -0
  9. package/dist/assets/extension/sidepanel/index.html +7 -394
  10. package/dist/assets/extension/sidepanel/index.js +53 -923
  11. package/dist/assets/server/app.module.js +11 -1
  12. package/dist/assets/server/app.module.js.map +1 -1
  13. package/dist/assets/server/auth/auth.middleware.js +9 -1
  14. package/dist/assets/server/auth/auth.middleware.js.map +1 -1
  15. package/dist/assets/server/build-info.json +1 -1
  16. package/dist/assets/server/customer/customer.controller.d.ts +18 -0
  17. package/dist/assets/server/customer/customer.controller.js +81 -0
  18. package/dist/assets/server/customer/customer.controller.js.map +1 -0
  19. package/dist/assets/server/customer/customer.entity.d.ts +17 -0
  20. package/dist/assets/server/customer/customer.entity.js +82 -0
  21. package/dist/assets/server/customer/customer.entity.js.map +1 -0
  22. package/dist/assets/server/customer/customer.module.d.ts +2 -0
  23. package/dist/assets/server/customer/customer.module.js +28 -0
  24. package/dist/assets/server/customer/customer.module.js.map +1 -0
  25. package/dist/assets/server/customer/customer.service.d.ts +36 -0
  26. package/dist/assets/server/customer/customer.service.js +200 -0
  27. package/dist/assets/server/customer/customer.service.js.map +1 -0
  28. package/dist/assets/server/db/schema-validator.service.d.ts +2 -0
  29. package/dist/assets/server/db/schema-validator.service.js +32 -1
  30. package/dist/assets/server/db/schema-validator.service.js.map +1 -1
  31. package/dist/assets/server/db/sqlite.module.d.ts +1 -0
  32. package/dist/assets/server/db/sqlite.module.js +12 -2
  33. package/dist/assets/server/db/sqlite.module.js.map +1 -1
  34. package/dist/assets/server/devops/devops-sink-init.d.ts +1 -14
  35. package/dist/assets/server/devops/devops-sink-init.js +3 -8
  36. package/dist/assets/server/devops/devops-sink-init.js.map +1 -1
  37. package/dist/assets/server/extension/extension-control.controller.d.ts +12 -0
  38. package/dist/assets/server/extension/extension-control.controller.js +41 -1
  39. package/dist/assets/server/extension/extension-control.controller.js.map +1 -1
  40. package/dist/assets/server/extension/extension-control.service.d.ts +6 -1
  41. package/dist/assets/server/extension/extension-control.service.js +27 -1
  42. package/dist/assets/server/extension/extension-control.service.js.map +1 -1
  43. package/dist/assets/server/extraction/adapter.service.d.ts +3 -0
  44. package/dist/assets/server/extraction/adapter.service.js +127 -6
  45. package/dist/assets/server/extraction/adapter.service.js.map +1 -1
  46. package/dist/assets/server/extraction/anomaly-report.service.js +1 -1
  47. package/dist/assets/server/extraction/anomaly-report.service.js.map +1 -1
  48. package/dist/assets/server/extraction/entities/adapter-rule.entity.d.ts +2 -0
  49. package/dist/assets/server/extraction/entities/adapter-rule.entity.js +10 -0
  50. package/dist/assets/server/extraction/entities/adapter-rule.entity.js.map +1 -1
  51. package/dist/assets/server/extraction/extraction-entity.controller.d.ts +21 -3
  52. package/dist/assets/server/extraction/extraction-entity.controller.js +44 -9
  53. package/dist/assets/server/extraction/extraction-entity.controller.js.map +1 -1
  54. package/dist/assets/server/extraction/extraction.module.js +13 -3
  55. package/dist/assets/server/extraction/extraction.module.js.map +1 -1
  56. package/dist/assets/server/extraction/extraction.service.d.ts +4 -1
  57. package/dist/assets/server/extraction/extraction.service.js +59 -4
  58. package/dist/assets/server/extraction/extraction.service.js.map +1 -1
  59. package/dist/assets/server/extraction/harness-overlay.service.d.ts +41 -0
  60. package/dist/assets/server/extraction/harness-overlay.service.js +319 -0
  61. package/dist/assets/server/extraction/harness-overlay.service.js.map +1 -0
  62. package/dist/assets/server/extraction/liepin-extractor.d.ts +1 -0
  63. package/dist/assets/server/extraction/liepin-extractor.js +172 -0
  64. package/dist/assets/server/extraction/liepin-extractor.js.map +1 -0
  65. package/dist/assets/server/extraction/script-rule.js +4 -0
  66. package/dist/assets/server/extraction/script-rule.js.map +1 -1
  67. package/dist/assets/server/governance/action-audit.entity.d.ts +12 -0
  68. package/dist/assets/server/governance/action-audit.entity.js +66 -0
  69. package/dist/assets/server/governance/action-audit.entity.js.map +1 -0
  70. package/dist/assets/server/governance/governance.controller.d.ts +14 -0
  71. package/dist/assets/server/governance/governance.controller.js +77 -0
  72. package/dist/assets/server/governance/governance.controller.js.map +1 -0
  73. package/dist/assets/server/governance/governance.ingester.d.ts +20 -0
  74. package/dist/assets/server/governance/governance.ingester.js +114 -0
  75. package/dist/assets/server/governance/governance.ingester.js.map +1 -0
  76. package/dist/assets/server/governance/governance.module.d.ts +2 -0
  77. package/dist/assets/server/governance/governance.module.js +26 -0
  78. package/dist/assets/server/governance/governance.module.js.map +1 -0
  79. package/dist/assets/server/governance/governance.service.d.ts +114 -0
  80. package/dist/assets/server/governance/governance.service.js +356 -0
  81. package/dist/assets/server/governance/governance.service.js.map +1 -0
  82. package/dist/assets/server/jobcase/jobcase.controller.d.ts +1 -0
  83. package/dist/assets/server/jobcase/jobcase.entity.d.ts +1 -0
  84. package/dist/assets/server/jobcase/jobcase.entity.js +6 -0
  85. package/dist/assets/server/jobcase/jobcase.entity.js.map +1 -1
  86. package/dist/assets/server/jobcase/jobcase.service.d.ts +2 -0
  87. package/dist/assets/server/jobcase/jobcase.service.js +2 -1
  88. package/dist/assets/server/jobcase/jobcase.service.js.map +1 -1
  89. package/dist/assets/server/main.js +1 -0
  90. package/dist/assets/server/main.js.map +1 -1
  91. package/dist/assets/server/package.json +1 -1
  92. package/dist/assets/server/preference/preference.controller.d.ts +14 -0
  93. package/dist/assets/server/preference/preference.controller.js +66 -0
  94. package/dist/assets/server/preference/preference.controller.js.map +1 -0
  95. package/dist/assets/server/preference/preference.entity.d.ts +13 -0
  96. package/dist/assets/server/preference/preference.entity.js +77 -0
  97. package/dist/assets/server/preference/preference.entity.js.map +1 -0
  98. package/dist/assets/server/preference/preference.module.d.ts +2 -0
  99. package/dist/assets/server/preference/preference.module.js +26 -0
  100. package/dist/assets/server/preference/preference.module.js.map +1 -0
  101. package/dist/assets/server/preference/preference.service.d.ts +33 -0
  102. package/dist/assets/server/preference/preference.service.js +122 -0
  103. package/dist/assets/server/preference/preference.service.js.map +1 -0
  104. package/dist/assets/server/recording/recording-event-queue.service.js +1 -1
  105. package/dist/assets/server/recording/recording-event-queue.service.js.map +1 -1
  106. package/dist/assets/server/recording/recording-retention.service.d.ts +19 -2
  107. package/dist/assets/server/recording/recording-retention.service.js +95 -6
  108. package/dist/assets/server/recording/recording-retention.service.js.map +1 -1
  109. package/dist/assets/server/recording/recording-snapshot.service.d.ts +1 -0
  110. package/dist/assets/server/recording/recording-snapshot.service.js +4 -0
  111. package/dist/assets/server/recording/recording-snapshot.service.js.map +1 -1
  112. package/dist/assets/server/recording/recording.controller.d.ts +10 -0
  113. package/dist/assets/server/recording/recording.controller.js +19 -0
  114. package/dist/assets/server/recording/recording.controller.js.map +1 -1
  115. package/dist/assets/server/recording/recording.gateway.js +11 -0
  116. package/dist/assets/server/recording/recording.gateway.js.map +1 -1
  117. package/dist/assets/server/recording/recording.module.js +6 -1
  118. package/dist/assets/server/recording/recording.module.js.map +1 -1
  119. package/dist/assets/server/recording/recording.service.d.ts +5 -0
  120. package/dist/assets/server/recording/recording.service.js +10 -0
  121. package/dist/assets/server/recording/recording.service.js.map +1 -1
  122. package/dist/assets/server/remark/remark.controller.d.ts +1 -1
  123. package/dist/assets/server/remark/remark.controller.js +1 -10
  124. package/dist/assets/server/remark/remark.controller.js.map +1 -1
  125. package/dist/assets/server/remark/remark.entity.d.ts +1 -1
  126. package/dist/assets/server/remark/remark.entity.js.map +1 -1
  127. package/dist/assets/server/site/entities/site-registry.entity.d.ts +1 -1
  128. package/dist/assets/server/site/entities/site-registry.entity.js +1 -1
  129. package/dist/assets/server/site/entities/site-registry.entity.js.map +1 -1
  130. package/dist/assets/server/site/site.controller.d.ts +1 -1
  131. package/dist/assets/server/site/site.service.d.ts +5 -1
  132. package/dist/assets/server/site/site.service.js +13 -0
  133. package/dist/assets/server/site/site.service.js.map +1 -1
  134. package/dist/assets/server/system/system.controller.d.ts +1 -0
  135. package/dist/assets/server/system/system.controller.js +1 -1
  136. package/dist/assets/server/system/system.controller.js.map +1 -1
  137. package/dist/assets/server/system/system.service.d.ts +1 -0
  138. package/dist/assets/server/system/system.service.js +3 -2
  139. package/dist/assets/server/system/system.service.js.map +1 -1
  140. package/dist/cache/db.js +6 -6
  141. package/dist/commands/addons.js +2 -1
  142. package/dist/commands/cache.js +1 -1
  143. package/dist/commands/case.d.ts +67 -0
  144. package/dist/commands/case.js +101 -0
  145. package/dist/commands/config.d.ts +4 -0
  146. package/dist/commands/config.js +8 -1
  147. package/dist/commands/customer.d.ts +25 -0
  148. package/dist/commands/customer.js +137 -0
  149. package/dist/commands/doctor.js +33 -9
  150. package/dist/commands/extension.js +18 -15
  151. package/dist/commands/extras.d.ts +22 -3
  152. package/dist/commands/extras.js +114 -5
  153. package/dist/commands/init.d.ts +1 -1
  154. package/dist/commands/init.js +3 -4
  155. package/dist/commands/issue-env.d.ts +23 -0
  156. package/dist/commands/issue-env.js +101 -0
  157. package/dist/commands/job.d.ts +2 -1
  158. package/dist/commands/job.js +25 -5
  159. package/dist/commands/login.d.ts +4 -0
  160. package/dist/commands/login.js +30 -3
  161. package/dist/commands/migrate.d.ts +32 -0
  162. package/dist/commands/migrate.js +116 -0
  163. package/dist/commands/note.d.ts +3 -0
  164. package/dist/commands/note.js +75 -0
  165. package/dist/commands/ops.js +64 -13
  166. package/dist/commands/preference.d.ts +6 -0
  167. package/dist/commands/preference.js +80 -0
  168. package/dist/commands/record.js +1 -1
  169. package/dist/commands/report.js +1 -1
  170. package/dist/commands/search.js +1 -1
  171. package/dist/commands/server.d.ts +7 -0
  172. package/dist/commands/server.js +40 -2
  173. package/dist/commands/status.d.ts +4 -0
  174. package/dist/commands/status.js +16 -6
  175. package/dist/commands/update.js +7 -8
  176. package/dist/config.d.ts +21 -0
  177. package/dist/config.js +27 -0
  178. package/dist/i18n/locales/en-US.json +17 -1
  179. package/dist/i18n/locales/zh-CN.json +17 -1
  180. package/dist/index.js +14 -0
  181. package/dist/keychain.d.ts +16 -1
  182. package/dist/keychain.js +43 -16
  183. package/dist/logger.js +2 -2
  184. package/dist/updater.js +2 -2
  185. package/dist/utils/command-error.d.ts +4 -2
  186. package/dist/utils/command-error.js +19 -3
  187. package/dist/utils/link.d.ts +18 -0
  188. package/dist/utils/link.js +83 -0
  189. package/dist/version.js +33 -8
  190. package/dist/watchdog.js +3 -3
  191. package/dist/web/dist/404.html +1 -1
  192. package/dist/web/dist/_next/static/chunks/app/logs/page-d478031d49f7ba00.js +1 -0
  193. package/dist/web/dist/auth/verify.html +1 -1
  194. package/dist/web/dist/auth/verify.txt +1 -1
  195. package/dist/web/dist/companies.html +1 -1
  196. package/dist/web/dist/companies.txt +1 -1
  197. package/dist/web/dist/entity.html +1 -1
  198. package/dist/web/dist/entity.txt +1 -1
  199. package/dist/web/dist/index.html +1 -1
  200. package/dist/web/dist/index.txt +1 -1
  201. package/dist/web/dist/jobs.html +1 -1
  202. package/dist/web/dist/jobs.txt +1 -1
  203. package/dist/web/dist/logs.html +1 -1
  204. package/dist/web/dist/logs.txt +2 -2
  205. package/dist/web/dist/matching/jobs/placeholder/talents.html +1 -1
  206. package/dist/web/dist/matching/jobs/placeholder/talents.txt +1 -1
  207. package/dist/web/dist/matching/portraits/placeholder/jobs.html +1 -1
  208. package/dist/web/dist/matching/portraits/placeholder/jobs.txt +1 -1
  209. package/dist/web/dist/parselog.html +1 -1
  210. package/dist/web/dist/parselog.txt +1 -1
  211. package/dist/web/dist/recordings.html +1 -1
  212. package/dist/web/dist/recordings.txt +1 -1
  213. package/dist/web/dist/snapshots/placeholder/agent-prompt.html +1 -1
  214. package/dist/web/dist/snapshots/placeholder/agent-prompt.txt +1 -1
  215. package/dist/web/dist/talents.html +1 -1
  216. package/dist/web/dist/talents.txt +1 -1
  217. package/package.json +2 -2
  218. package/dist/web/dist/_next/static/chunks/app/logs/page-9f41d44fa1add723.js +0 -1
  219. /package/dist/web/dist/_next/static/{hqJ-7vr4X71DNfzZugHJ- → WdJr3NhHjlu5xN0qOap1k}/_buildManifest.js +0 -0
  220. /package/dist/web/dist/_next/static/{hqJ-7vr4X71DNfzZugHJ- → WdJr3NhHjlu5xN0qOap1k}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import WebSocket from 'ws';
3
3
  import * as readline from 'readline';
4
- import { loadConfig } from '../config.js';
4
+ import { loadConfig, resolveDataDir } from '../config.js';
5
5
  import axios from 'axios';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
@@ -10,12 +10,114 @@ import { runStop, tryReadPid } from './server.js';
10
10
  import { i18n } from '../i18n/index.js';
11
11
  import { getCliLogger } from '../logger.js';
12
12
  /**
13
- * 通知所有已连接扩展 reload(自愈主链路):
14
- * 1) 优先 HTTP POST /system/broadcast-extension(返回明确 broadcast 计数和错误)
15
- * 2) Fallback: 直连 WS /ws 发 {type:'broadcast', payload:{type:'reload'}}(兼容旧 server 无 HTTP 端点)
13
+ * FID-092:扫描扩展目录最新 mtime,等待其静止——reload 广播前确认 dist 已写完。
14
+ * 背景:R5 实机 reload 后 3 秒 dist 仍在写入,Chrome 重读读到写了一半的
15
+ * service-worker.js → SW eval 崩溃 → 无 alarm 监听/无 offscreen/零重连,
16
+ * 所有自愈路径全部失效,只能人工点。Windows 兼容:纯 node fs,无 shell。
17
+ */
18
+ export async function waitForExtensionDirQuiet(dir, opts = {}) {
19
+ const quietMs = opts.quietMs ?? 2500;
20
+ const maxWaitMs = opts.maxWaitMs ?? 15000;
21
+ const pollMs = opts.pollMs ?? 400;
22
+ const start = Date.now();
23
+ const newestMtime = (d) => {
24
+ let latest = 0;
25
+ let entries;
26
+ try {
27
+ entries = fs.readdirSync(d, { withFileTypes: true });
28
+ }
29
+ catch {
30
+ return latest; // 目录不可读(未安装/权限)时不阻塞 reload
31
+ }
32
+ for (const e of entries) {
33
+ const full = path.join(d, e.name);
34
+ try {
35
+ if (e.isDirectory()) {
36
+ latest = Math.max(latest, newestMtime(full));
37
+ }
38
+ else {
39
+ latest = Math.max(latest, fs.statSync(full).mtimeMs);
40
+ }
41
+ }
42
+ catch {
43
+ /* 单个文件竞态删除,跳过 */
44
+ }
45
+ }
46
+ return latest;
47
+ };
48
+ let newest = newestMtime(dir);
49
+ while (newest > 0 && Date.now() - newest < quietMs && Date.now() - start < maxWaitMs) {
50
+ await new Promise((r) => setTimeout(r, pollMs));
51
+ newest = newestMtime(dir);
52
+ }
53
+ return { quiet: newest === 0 || Date.now() - newest >= quietMs, waitedMs: Date.now() - start, newestMtimeMs: newest };
54
+ }
55
+ /** FID-092:轮询 server 已连接扩展数(>=1 即判定 reload 后已回连)。 */
56
+ export async function extensionConnectedCount(serverUrl, timeoutMs = 3000) {
57
+ try {
58
+ const r = await axios.get(`${serverUrl}/extensions`, { timeout: timeoutMs, proxy: false });
59
+ return Array.isArray(r.data) ? r.data.length : 0;
60
+ }
61
+ catch {
62
+ return -1; // server 不可达
63
+ }
64
+ }
65
+ /**
66
+ * 通知所有已连接扩展 reload(自愈主链路),FID-092 三段式:
67
+ * 1) dist 静止确认(防 reload 读到写一半的扩展文件)
68
+ * 2) 广播(broadcastReload):优先 HTTP POST /system/broadcast-extension;
69
+ * 旧 server 无该端点时退回直连 WS /ws 广播
70
+ * 3) 健康校验 + 自动重试(广播后 12s 内未回连 → 再广播,最多 3 次)
16
71
  */
17
72
  export async function runReload(timeoutMs = 5000) {
18
73
  const config = loadConfig();
74
+ // FID-092 步骤 1:dist 静止确认(软链/目录均可,扫描真实路径)
75
+ let extDir;
76
+ try {
77
+ extDir = fs.realpathSync(path.join(resolveDataDir(), 'extension'));
78
+ }
79
+ catch {
80
+ extDir = path.join(resolveDataDir(), 'extension');
81
+ }
82
+ const q = await waitForExtensionDirQuiet(extDir);
83
+ const quietNote = q.waitedMs > 500 ? `(等待 dist 写入静止 ${q.waitedMs}ms) ` : '';
84
+ // FID-092 步骤 2+3:广播 → 校验回连 → 失败重试
85
+ const VERIFY_WINDOW_MS = 12000;
86
+ const MAX_ATTEMPTS = 3;
87
+ let last = await broadcastReload(config, timeoutMs);
88
+ if (last.broadcast <= 0) {
89
+ return { ...last, msg: `${quietNote}${last.msg}` };
90
+ }
91
+ const start = Date.now();
92
+ while (Date.now() - start < VERIFY_WINDOW_MS) {
93
+ await new Promise((r) => setTimeout(r, 1000));
94
+ const n = await extensionConnectedCount(config.serverUrl);
95
+ if (n > 0) {
96
+ return { ...last, msg: `${quietNote}${last.msg};已校验扩展回连(connected=${n})。` };
97
+ }
98
+ if (n === -1)
99
+ break; // server 挂了,重试无意义
100
+ }
101
+ // 未回连:重试广播(第一次读到的可能是写了一半的文件,重读即恢复)
102
+ for (let attempt = 2; attempt <= MAX_ATTEMPTS; attempt++) {
103
+ await new Promise((r) => setTimeout(r, 1000));
104
+ last = await broadcastReload(config, timeoutMs);
105
+ if (last.broadcast <= 0)
106
+ break;
107
+ const deadline = Date.now() + VERIFY_WINDOW_MS;
108
+ while (Date.now() < deadline) {
109
+ await new Promise((r) => setTimeout(r, 1000));
110
+ if ((await extensionConnectedCount(config.serverUrl)) > 0) {
111
+ return { ...last, msg: `${quietNote}第 ${attempt} 次广播后已校验扩展回连。` };
112
+ }
113
+ }
114
+ }
115
+ return {
116
+ ...last,
117
+ msg: `${quietNote}${MAX_ATTEMPTS} 次广播后扩展仍未回连(server /extensions=0)——扩展可能处于错误态,请到 chrome://extensions 查看 HunterMate「错误」并点「重新加载」。`,
118
+ };
119
+ }
120
+ async function broadcastReload(config, timeoutMs = 5000) {
19
121
  // ---------- 1) HTTP 优先(POST /system/broadcast-extension) ----------
20
122
  try {
21
123
  const res = await axios.post(`${config.serverUrl}/system/broadcast-extension`, { payload: { type: 'reload' } }, { timeout: timeoutMs, proxy: false });
@@ -29,6 +131,13 @@ export async function runReload(timeoutMs = 5000) {
29
131
  msg: `reload 广播已通过 HTTP 通道发送给 ${broadcast} 个扩展实例:200ms 后每个实例会自动调 chrome.runtime.reload(),Chrome 将重新读取磁盘上的扩展文件。`,
30
132
  };
31
133
  }
134
+ // FID-086 deferred reload: server 已挂起意图,扩展重连(≤30s)自动下发——断连态不失败、不静默
135
+ if (body.queued) {
136
+ return {
137
+ broadcast: 0,
138
+ msg: '扩展当前未连接,重载请求已挂起:扩展重连(通常 ≤30 秒,chrome.alarms 唤醒)后将自动重新加载。无需手动干预。',
139
+ };
140
+ }
32
141
  if (errors.length) {
33
142
  return {
34
143
  broadcast: 0,
@@ -143,7 +252,7 @@ export const openExtCommand = new Command('open-extension-folder')
143
252
  // ---------------------------------------------------------------------------
144
253
  // purge — 彻底清理本地 hunter-mate 数据
145
254
  // ---------------------------------------------------------------------------
146
- const HM_ROOT = () => path.join(process.env.HUNTER_MATE_HOME || process.env.HOME || process.env.USERPROFILE || '.', '.hunter-mate');
255
+ const HM_ROOT = resolveDataDir;
147
256
  /** 交互式二次确认 */
148
257
  function confirmPrompt(question) {
149
258
  return new Promise((resolve) => {
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- /** 对应 ChromePilot init: 创建 ~/.hunter-mate 目录、SQLite、初始 token */
2
+ /** 对应 ChromePilot init: 创建数据目录、SQLite、初始 token(目录位置随 dataDir 配置,FID-071) */
3
3
  export declare function runInit(force?: boolean): {
4
4
  dir: string;
5
5
  extensionDir: string;
@@ -1,12 +1,11 @@
1
1
  import { Command } from 'commander';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
- import { ensureConfigDir, loadConfig, saveConfig } from '../config.js';
4
+ import { ensureConfigDir, loadConfig, saveConfig, resolveDataDir } from '../config.js';
5
5
  import { getCliLogger } from '../logger.js';
6
- /** 对应 ChromePilot init: 创建 ~/.hunter-mate 目录、SQLite、初始 token */
6
+ /** 对应 ChromePilot init: 创建数据目录、SQLite、初始 token(目录位置随 dataDir 配置,FID-071) */
7
7
  export function runInit(force = false) {
8
- const HOME = process.env.HUNTER_MATE_HOME || process.env.HOME || process.env.USERPROFILE || '.';
9
- const ROOT = path.join(HOME, '.hunter-mate');
8
+ const ROOT = resolveDataDir();
10
9
  const EXT_DIR = path.join(ROOT, 'extension');
11
10
  const DB_FILE = path.join(ROOT, 'hunter-mate.db');
12
11
  const SESSIONS_DIR = path.join(ROOT, 'sessions');
@@ -0,0 +1,23 @@
1
+ import { Command } from 'commander';
2
+ export interface IssueEnvGitInfo {
3
+ repoRoot: string;
4
+ head: string | null;
5
+ branch: string | null;
6
+ /** 最近一次提交(短 hash + 时间 + subject),与 issue 模板「代码 HEAD」行对应 */
7
+ lastCommit: string | null;
8
+ /** 有未提交改动 = dist 可能滞后于 HEAD,提 issue 必须注明 */
9
+ dirty: boolean;
10
+ }
11
+ export interface IssueEnvResult {
12
+ cliVersion: string;
13
+ /** dev = CLI 位于 git 工作区(软链/shim 到源码仓);package = 发布包安装 */
14
+ installType: 'dev' | 'package';
15
+ git: IssueEnvGitInfo | null;
16
+ /** server 未运行时为 null */
17
+ serverBuildTime: string | null;
18
+ os: string;
19
+ node: string;
20
+ dataDir: string;
21
+ }
22
+ export declare function runIssueEnv(): Promise<IssueEnvResult>;
23
+ export declare const issueEnvCommand: Command;
@@ -0,0 +1,101 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { execFileSync } from 'node:child_process';
7
+ import axios from 'axios';
8
+ import { i18n } from '../i18n/index.js';
9
+ import { loadConfig, resolveDataDir } from '../config.js';
10
+ import { readInstalledVersions } from '../version.js';
11
+ // dev 软链/shim 安装时 __dirname 位于仓内(packages/cli/dist/commands),
12
+ // 发布包安装时位于全局 node_modules —— 向上找含 .git 的目录即可判定并定位仓根。
13
+ const START_DIR = path.dirname(fileURLToPath(import.meta.url));
14
+ function findRepoRoot(start) {
15
+ let dir = start;
16
+ while (true) {
17
+ if (fs.existsSync(path.join(dir, '.git')))
18
+ return dir;
19
+ const parent = path.dirname(dir);
20
+ if (parent === dir)
21
+ return null;
22
+ dir = parent;
23
+ }
24
+ }
25
+ /** 静默执行 git,失败(git 不可用/非工作区)返回 null */
26
+ function git(args, cwd) {
27
+ try {
28
+ const out = execFileSync('git', args, {
29
+ cwd,
30
+ encoding: 'utf-8',
31
+ stdio: ['ignore', 'pipe', 'ignore'],
32
+ }).trim();
33
+ return out.length > 0 ? out : null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export async function runIssueEnv() {
40
+ const installed = readInstalledVersions();
41
+ const repoRoot = findRepoRoot(START_DIR);
42
+ let gitInfo = null;
43
+ if (repoRoot) {
44
+ const porcelain = git(['status', '--porcelain'], repoRoot) ?? '';
45
+ gitInfo = {
46
+ repoRoot,
47
+ head: git(['rev-parse', '--short', 'HEAD'], repoRoot),
48
+ branch: git(['branch', '--show-current'], repoRoot),
49
+ lastCommit: git(['log', '-1', '--format=%h %ci %s'], repoRoot),
50
+ dirty: porcelain.length > 0,
51
+ };
52
+ }
53
+ let serverBuildTime = null;
54
+ try {
55
+ const r = await axios.get(`${loadConfig().serverUrl}/health`, { timeout: 2000 });
56
+ serverBuildTime = r.data?.buildTime ?? null;
57
+ }
58
+ catch {
59
+ /* server 未运行,保持 null */
60
+ }
61
+ return {
62
+ cliVersion: installed.cli ?? 'unknown',
63
+ installType: repoRoot ? 'dev' : 'package',
64
+ git: gitInfo,
65
+ serverBuildTime,
66
+ os: `${os.platform()} ${os.release()} ${os.arch()}`,
67
+ node: process.version,
68
+ dataDir: resolveDataDir(),
69
+ };
70
+ }
71
+ function renderText(r) {
72
+ const lines = [];
73
+ lines.push(`${i18n.t('issueEnv.cliVersion')}: ${r.cliVersion}`);
74
+ lines.push(`${i18n.t('issueEnv.installType')}: ` +
75
+ (r.installType === 'dev' ? i18n.t('issueEnv.installDev') : i18n.t('issueEnv.installPackage')));
76
+ if (r.git) {
77
+ lines.push(`${i18n.t('issueEnv.gitHead')}: ${r.git.lastCommit ?? r.git.head ?? 'unknown'}`);
78
+ lines.push(`${i18n.t('issueEnv.gitBranch')}: ${r.git.branch ?? 'unknown'} · ` +
79
+ (r.git.dirty ? i18n.t('issueEnv.gitDirty') : i18n.t('issueEnv.gitClean')));
80
+ lines.push(`${i18n.t('issueEnv.repoRoot')}: ${r.git.repoRoot}`);
81
+ }
82
+ else {
83
+ lines.push(`${i18n.t('issueEnv.gitHead')}: ${i18n.t('issueEnv.gitUnavailable')}`);
84
+ }
85
+ lines.push(`${i18n.t('issueEnv.serverBuildTime')}: ${r.serverBuildTime ?? i18n.t('issueEnv.serverNotRunning')}`);
86
+ lines.push(`OS: ${r.os}`);
87
+ lines.push(`Node: ${r.node}`);
88
+ lines.push(`dataDir: ${r.dataDir}`);
89
+ return lines.join('\n');
90
+ }
91
+ export const issueEnvCommand = new Command('issue-env')
92
+ .description(i18n.t('issueEnv.description'))
93
+ .option('-f, --format <fmt>', '输出格式: text|json', 'text')
94
+ .action(async (opts) => {
95
+ const result = await runIssueEnv();
96
+ if (opts.format === 'json') {
97
+ console.log(JSON.stringify(result, null, 2));
98
+ return;
99
+ }
100
+ console.log(renderText(result));
101
+ });
@@ -3,8 +3,9 @@ export interface CreateJobCaseResult {
3
3
  id: number;
4
4
  externalId: string;
5
5
  status: string;
6
+ customerId?: number;
6
7
  }
7
- export declare function runJobCreate(folderPath: string): Promise<CreateJobCaseResult>;
8
+ export declare function runJobCreate(folderPath: string, customerRef?: string): Promise<CreateJobCaseResult>;
8
9
  /**
9
10
  * 优先查 server 的 /job-cases;server 不可达或未登录时兜底本地 sqlite 缓存。
10
11
  */
@@ -8,6 +8,7 @@ import { loadConfig } from '../config.js';
8
8
  import { getToken } from '../keychain.js';
9
9
  import { i18n } from '../i18n/index.js';
10
10
  import { printCommandError } from '../utils/command-error.js';
11
+ import { runCustomerList } from './customer.js';
11
12
  /** 递归收集目录下的 .md/.txt/.pdf(含 raw/jd/ 等子目录),键为相对路径(根目录文件不带前缀)。 */
12
13
  function collectJobFiles(dir, relPrefix = '', out = {}) {
13
14
  const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
@@ -24,10 +25,27 @@ function collectJobFiles(dir, relPrefix = '', out = {}) {
24
25
  }
25
26
  return out;
26
27
  }
27
- export async function runJobCreate(folderPath) {
28
+ export async function runJobCreate(folderPath, customerRef) {
28
29
  const config = loadConfig();
29
30
  const token = await getToken();
30
31
  const headers = token ? { Authorization: `UTOKEN ${token}` } : {};
32
+ // --customer:按名称/别名解析客户 id,让职位 case 挂到客户锚点上
33
+ let customerId;
34
+ if (customerRef) {
35
+ if (/^\d+$/.test(customerRef.trim())) {
36
+ customerId = parseInt(customerRef.trim(), 10);
37
+ }
38
+ else {
39
+ const hits = await runCustomerList(customerRef.trim());
40
+ if (!hits.length) {
41
+ throw new Error(`客户 "${customerRef}" 未入库,请先: hunter-mate customer upsert --name "${customerRef}"`);
42
+ }
43
+ customerId = hits[0].id;
44
+ if (hits.length > 1) {
45
+ console.error(`ℹ 客户 "${customerRef}" 命中 ${hits.length} 条,使用第一个: [${hits[0].id}] ${hits[0].canonicalName}`);
46
+ }
47
+ }
48
+ }
31
49
  const resolved = path.resolve(folderPath);
32
50
  if (!fs.existsSync(resolved)) {
33
51
  throw new Error(`Folder not found: ${resolved}`);
@@ -45,6 +63,7 @@ export async function runJobCreate(folderPath) {
45
63
  title,
46
64
  folderPath: resolved,
47
65
  rawFiles,
66
+ customerId,
48
67
  }, { headers, proxy: false });
49
68
  const serverCase = res.data;
50
69
  ensureCacheDir();
@@ -92,13 +111,14 @@ export const jobCommand = new Command('job')
92
111
  jobCommand
93
112
  .command('create <folder>')
94
113
  .description(i18n.t('job.create'))
95
- .action(async (folderPath) => {
114
+ .option('--customer <idOrName>', '关联客户(名称/别名/id),未入库会先报错提示 upsert')
115
+ .action(async (folderPath, opts) => {
96
116
  try {
97
- const serverCase = await runJobCreate(folderPath);
98
- console.log(`Created JobCase ${serverCase.id} (${serverCase.externalId})`);
117
+ const serverCase = await runJobCreate(folderPath, opts.customer);
118
+ console.log(`Created JobCase ${serverCase.id} (${serverCase.externalId})${serverCase.customerId ? ` customer=${serverCase.customerId}` : ''}`);
99
119
  }
100
120
  catch (err) {
101
- printCommandError('job:create-failed', err);
121
+ await printCommandError('job:create-failed', err);
102
122
  process.exit(1);
103
123
  }
104
124
  });
@@ -9,6 +9,10 @@ export declare function sendEmailCode(email: string): Promise<EmailLoginSource>;
9
9
  /**
10
10
  * 邮箱登录第二步:校验验证码并保存 token。
11
11
  * 分支选择与 sendEmailCode 保持一致:server 在线走本地代理自签,否则直连 SaaS。
12
+ *
13
+ * hmh#43:两个分支签发的 token 密钥不同,必须打 issuer 标记 —
14
+ * SaaS fallback 拿到的是 SaaS 签发的 token,本地 server 的 AuthMiddleware
15
+ * (USER_SESSION_SECRET)验不过,数据面(search 等)会 401。
12
16
  */
13
17
  export declare function verifyEmailCode(email: string, code: string): Promise<{
14
18
  token: string;
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import axios from 'axios';
3
3
  import open from 'open';
4
4
  import { loadConfig } from '../config.js';
5
- import { saveToken } from '../keychain.js';
5
+ import { saveToken, getToken } from '../keychain.js';
6
6
  import { i18n } from '../i18n/index.js';
7
7
  import { getCliLogger } from '../logger.js';
8
8
  function sleep(ms) {
@@ -35,6 +35,10 @@ export async function sendEmailCode(email) {
35
35
  /**
36
36
  * 邮箱登录第二步:校验验证码并保存 token。
37
37
  * 分支选择与 sendEmailCode 保持一致:server 在线走本地代理自签,否则直连 SaaS。
38
+ *
39
+ * hmh#43:两个分支签发的 token 密钥不同,必须打 issuer 标记 —
40
+ * SaaS fallback 拿到的是 SaaS 签发的 token,本地 server 的 AuthMiddleware
41
+ * (USER_SESSION_SECRET)验不过,数据面(search 等)会 401。
38
42
  */
39
43
  export async function verifyEmailCode(email, code) {
40
44
  const config = loadConfig();
@@ -43,14 +47,17 @@ export async function verifyEmailCode(email, code) {
43
47
  if (!data.access_token) {
44
48
  throw new Error('verify returned no access_token');
45
49
  }
46
- await saveToken(data.access_token);
50
+ await saveToken(data.access_token, 'local');
47
51
  return { token: data.access_token, source: 'server' };
48
52
  }
49
53
  const { data } = await axios.post(`${config.saasUrl}/hunter-mate/auth/email/verify`, { email, code }, { proxy: false });
50
54
  if (!data.access_token) {
51
55
  throw new Error('verify returned no access_token');
52
56
  }
53
- await saveToken(data.access_token);
57
+ await saveToken(data.access_token, 'saas');
58
+ console.warn('⚠️ 本地 server 未运行,本次登录由 SaaS 签发 token。\n' +
59
+ ' 本地数据面(search 等)在 server 启动后会提示登录失效;\n' +
60
+ ' 请先运行 hunter-mate server(或 hunter-mate extension check 自动拉起)再重新登录。');
54
61
  return { token: data.access_token, source: 'saas' };
55
62
  }
56
63
  export async function runLogin() {
@@ -111,6 +118,26 @@ export const loginCommand = new Command('login')
111
118
  }
112
119
  getCliLogger().info('login:token-saved', { method: 'email', email: options.email, source });
113
120
  console.log(i18n.t('login.success'));
121
+ // 登录后自检:拿新 token 打一次受保护端点,把「登录成功但搜索 401」的
122
+ // 静默失败变成确定的成功/失败提示(FID-057 #36)
123
+ try {
124
+ const token = await getToken();
125
+ const probe = await axios.get(`${loadConfig().serverUrl}/job-cases`, {
126
+ headers: token ? { Authorization: `UTOKEN ${token}` } : {},
127
+ timeout: 5000,
128
+ proxy: false,
129
+ });
130
+ console.log(`${i18n.t('login.selfCheckOk')} (HTTP ${probe.status})`);
131
+ }
132
+ catch (probeErr) {
133
+ const status = probeErr?.response?.status;
134
+ if (status === 401 || status === 403) {
135
+ console.error(`${i18n.t('login.selfCheckFail')} (HTTP ${status})`);
136
+ }
137
+ else {
138
+ console.warn(`${i18n.t('login.selfCheckSkipped')} (HTTP ${status || 'network'})`);
139
+ }
140
+ }
114
141
  return;
115
142
  }
116
143
  if (options.code) {
@@ -0,0 +1,32 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * 跨卷安全移动:同卷 renameSync;rename 失败(典型 EXDEV 跨盘,Windows C:→D:)
4
+ * 回退递归 copy + rm。symlink 项读 link 目标后经 linkDev 重建
5
+ * (Windows 普通用户无 symlink 权限,junction/copy+marker 兜底)。
6
+ */
7
+ export declare function moveEntry(src: string, dest: string): void;
8
+ export interface MigrateResult {
9
+ from: string;
10
+ to: string;
11
+ moved: string[];
12
+ skipped: string[];
13
+ serverRestarted: boolean;
14
+ }
15
+ /**
16
+ * 数据目录迁移(FID-071):
17
+ * 1. server 在跑则先停(记录状态,迁移后按需重启)
18
+ * 2. 源目录(除 config.json 外)全部移动到目标目录
19
+ * 3. config.json 留在原 CONFIG_DIR,写入 dataDir=目标
20
+ * 4. 重启 server(若迁移前在跑)
21
+ */
22
+ export declare function runMigrateDataDir(targetArg: string, deps: {
23
+ isRunning: () => Promise<boolean>;
24
+ stop: () => Promise<{
25
+ stopped: boolean;
26
+ }>;
27
+ start: () => Promise<{
28
+ started: boolean;
29
+ msg: string;
30
+ }>;
31
+ }): Promise<MigrateResult>;
32
+ export declare const migrateDataDirCommand: Command;
@@ -0,0 +1,116 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { loadConfig, saveConfig, resolveDataDir } from '../config.js';
5
+ import { getCliLogger } from '../logger.js';
6
+ import { linkDev } from '../utils/link.js';
7
+ /**
8
+ * 跨卷安全移动:同卷 renameSync;rename 失败(典型 EXDEV 跨盘,Windows C:→D:)
9
+ * 回退递归 copy + rm。symlink 项读 link 目标后经 linkDev 重建
10
+ * (Windows 普通用户无 symlink 权限,junction/copy+marker 兜底)。
11
+ */
12
+ export function moveEntry(src, dest) {
13
+ const stat = fs.lstatSync(src);
14
+ if (stat.isSymbolicLink()) {
15
+ const linkTarget = fs.readlinkSync(src);
16
+ const r = linkDev(linkTarget, dest, stat.isDirectory() ? 'dir' : 'file');
17
+ if (!r.ok)
18
+ throw new Error(`重建软链失败 ${src} → ${dest}: ${r.error}`);
19
+ fs.unlinkSync(src);
20
+ return;
21
+ }
22
+ try {
23
+ fs.renameSync(src, dest);
24
+ }
25
+ catch {
26
+ fs.cpSync(src, dest, { recursive: true });
27
+ fs.rmSync(src, { recursive: true, force: true });
28
+ }
29
+ }
30
+ /**
31
+ * 数据目录迁移(FID-071):
32
+ * 1. server 在跑则先停(记录状态,迁移后按需重启)
33
+ * 2. 源目录(除 config.json 外)全部移动到目标目录
34
+ * 3. config.json 留在原 CONFIG_DIR,写入 dataDir=目标
35
+ * 4. 重启 server(若迁移前在跑)
36
+ */
37
+ export async function runMigrateDataDir(targetArg, deps) {
38
+ const target = path.resolve(targetArg);
39
+ const source = resolveDataDir();
40
+ if (target === source) {
41
+ throw new Error(`目标目录与当前数据目录相同: ${target}`);
42
+ }
43
+ if (target.startsWith(source + path.sep)) {
44
+ throw new Error(`目标目录不能位于当前数据目录内部: ${target}`);
45
+ }
46
+ if (!fs.existsSync(source)) {
47
+ throw new Error(`当前数据目录不存在: ${source}(先运行 hunter-mate init)`);
48
+ }
49
+ // 1) server 状态探测并停止
50
+ const wasRunning = await deps.isRunning();
51
+ if (wasRunning) {
52
+ const stopRes = await deps.stop();
53
+ if (!stopRes.stopped) {
54
+ throw new Error('server 停止失败,已中止迁移(避免移动正在被写入的数据库)');
55
+ }
56
+ }
57
+ // 2) 逐项移动(config.json 是配置的锚点,永远留在 CONFIG_DIR)
58
+ fs.mkdirSync(target, { recursive: true });
59
+ const entries = fs.readdirSync(source, { withFileTypes: true });
60
+ const moved = [];
61
+ const skipped = [];
62
+ for (const entry of entries) {
63
+ if (entry.name === 'config.json') {
64
+ skipped.push(entry.name);
65
+ continue;
66
+ }
67
+ moveEntry(path.join(source, entry.name), path.join(target, entry.name));
68
+ moved.push(entry.name);
69
+ }
70
+ // 3) 写回配置
71
+ saveConfig({ dataDir: target });
72
+ loadConfig();
73
+ // 4) 按需重启
74
+ let serverRestarted = false;
75
+ if (wasRunning) {
76
+ const startRes = await deps.start();
77
+ serverRestarted = startRes.started;
78
+ if (!startRes.started) {
79
+ getCliLogger().warn('migrate:server-restart-failed', { msg: startRes.msg });
80
+ }
81
+ }
82
+ return { from: source, to: target, moved, skipped, serverRestarted };
83
+ }
84
+ export const migrateDataDirCommand = new Command('migrate-data-dir')
85
+ .description('把 ~/.hunter-mate 的重数据(数据库/日志/缓存/扩展等)迁移到指定目录,如 D:\\hm-data(FID-071)')
86
+ .argument('<target>', '目标目录(绝对路径,如 D:\\hm-data)')
87
+ .action(async (target) => {
88
+ const { tryReadPid, runStop, runStart } = await import('./server.js');
89
+ const result = await runMigrateDataDir(target, {
90
+ isRunning: async () => {
91
+ const pid = tryReadPid();
92
+ if (!pid)
93
+ return false;
94
+ const config = loadConfig();
95
+ try {
96
+ const res = await fetch(`${config.serverUrl}/health`, {
97
+ signal: AbortSignal.timeout(1500),
98
+ });
99
+ return res.ok;
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ },
105
+ stop: () => runStop(),
106
+ start: () => runStart({}),
107
+ });
108
+ console.log(`✓ 数据目录已迁移: ${result.from} → ${result.to}`);
109
+ console.log(` 移动 ${result.moved.length} 项: ${result.moved.join(', ') || '(空)'}`);
110
+ if (result.skipped.length) {
111
+ console.log(` 保留在原位: ${result.skipped.join(', ')}(配置文件锚点,不迁移)`);
112
+ }
113
+ console.log(` server: ${result.serverRestarted ? '已重启' : '迁移前未运行(下次 start 自动使用新目录)'}`);
114
+ console.log(' 提示: 若 Chrome 扩展加载自旧目录,请在新目录重新加载扩展(chrome://extensions)');
115
+ getCliLogger().info('migrate-data-dir:done', { from: result.from, to: result.to, moved: result.moved.length });
116
+ });
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ /** 业务备注(Remark 实体)CLI 入口:推荐反馈/沟通结论/不合适原因等记忆载体 */
3
+ export declare const noteCommand: Command;