hunter-mate 0.1.6 → 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 (261) hide show
  1. package/dist/assets/extension/background/service-worker.js +911 -159
  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 +12 -4
  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 +12 -1
  12. package/dist/assets/server/app.module.js.map +1 -1
  13. package/dist/assets/server/auth/auth.controller.d.ts +7 -0
  14. package/dist/assets/server/auth/auth.controller.js +20 -0
  15. package/dist/assets/server/auth/auth.controller.js.map +1 -1
  16. package/dist/assets/server/auth/auth.middleware.js +9 -1
  17. package/dist/assets/server/auth/auth.middleware.js.map +1 -1
  18. package/dist/assets/server/auth/auth.service.d.ts +11 -0
  19. package/dist/assets/server/auth/auth.service.js +58 -1
  20. package/dist/assets/server/auth/auth.service.js.map +1 -1
  21. package/dist/assets/server/build-info.json +1 -1
  22. package/dist/assets/server/customer/customer.controller.d.ts +18 -0
  23. package/dist/assets/server/customer/customer.controller.js +81 -0
  24. package/dist/assets/server/customer/customer.controller.js.map +1 -0
  25. package/dist/assets/server/customer/customer.entity.d.ts +17 -0
  26. package/dist/assets/server/customer/customer.entity.js +82 -0
  27. package/dist/assets/server/customer/customer.entity.js.map +1 -0
  28. package/dist/assets/server/customer/customer.module.d.ts +2 -0
  29. package/dist/assets/server/customer/customer.module.js +28 -0
  30. package/dist/assets/server/customer/customer.module.js.map +1 -0
  31. package/dist/assets/server/customer/customer.service.d.ts +36 -0
  32. package/dist/assets/server/customer/customer.service.js +200 -0
  33. package/dist/assets/server/customer/customer.service.js.map +1 -0
  34. package/dist/assets/server/db/schema-validator.service.d.ts +2 -0
  35. package/dist/assets/server/db/schema-validator.service.js +32 -1
  36. package/dist/assets/server/db/schema-validator.service.js.map +1 -1
  37. package/dist/assets/server/db/sqlite.module.d.ts +1 -0
  38. package/dist/assets/server/db/sqlite.module.js +12 -2
  39. package/dist/assets/server/db/sqlite.module.js.map +1 -1
  40. package/dist/assets/server/devops/devops-sink-init.d.ts +1 -14
  41. package/dist/assets/server/devops/devops-sink-init.js +3 -8
  42. package/dist/assets/server/devops/devops-sink-init.js.map +1 -1
  43. package/dist/assets/server/extension/extension-control.controller.d.ts +15 -0
  44. package/dist/assets/server/extension/extension-control.controller.js +42 -2
  45. package/dist/assets/server/extension/extension-control.controller.js.map +1 -1
  46. package/dist/assets/server/extension/extension-control.service.d.ts +14 -1
  47. package/dist/assets/server/extension/extension-control.service.js +65 -2
  48. package/dist/assets/server/extension/extension-control.service.js.map +1 -1
  49. package/dist/assets/server/extraction/adapter.service.d.ts +3 -0
  50. package/dist/assets/server/extraction/adapter.service.js +127 -6
  51. package/dist/assets/server/extraction/adapter.service.js.map +1 -1
  52. package/dist/assets/server/extraction/anomaly-report.service.js +1 -1
  53. package/dist/assets/server/extraction/anomaly-report.service.js.map +1 -1
  54. package/dist/assets/server/extraction/entities/adapter-rule.entity.d.ts +2 -0
  55. package/dist/assets/server/extraction/entities/adapter-rule.entity.js +10 -0
  56. package/dist/assets/server/extraction/entities/adapter-rule.entity.js.map +1 -1
  57. package/dist/assets/server/extraction/extraction-entity.controller.d.ts +21 -3
  58. package/dist/assets/server/extraction/extraction-entity.controller.js +44 -9
  59. package/dist/assets/server/extraction/extraction-entity.controller.js.map +1 -1
  60. package/dist/assets/server/extraction/extraction.module.js +13 -3
  61. package/dist/assets/server/extraction/extraction.module.js.map +1 -1
  62. package/dist/assets/server/extraction/extraction.service.d.ts +4 -1
  63. package/dist/assets/server/extraction/extraction.service.js +59 -4
  64. package/dist/assets/server/extraction/extraction.service.js.map +1 -1
  65. package/dist/assets/server/extraction/harness-overlay.service.d.ts +41 -0
  66. package/dist/assets/server/extraction/harness-overlay.service.js +319 -0
  67. package/dist/assets/server/extraction/harness-overlay.service.js.map +1 -0
  68. package/dist/assets/server/extraction/liepin-extractor.d.ts +1 -0
  69. package/dist/assets/server/extraction/liepin-extractor.js +172 -0
  70. package/dist/assets/server/extraction/liepin-extractor.js.map +1 -0
  71. package/dist/assets/server/extraction/script-rule.js +4 -0
  72. package/dist/assets/server/extraction/script-rule.js.map +1 -1
  73. package/dist/assets/server/governance/action-audit.entity.d.ts +12 -0
  74. package/dist/assets/server/governance/action-audit.entity.js +66 -0
  75. package/dist/assets/server/governance/action-audit.entity.js.map +1 -0
  76. package/dist/assets/server/governance/governance.controller.d.ts +14 -0
  77. package/dist/assets/server/governance/governance.controller.js +77 -0
  78. package/dist/assets/server/governance/governance.controller.js.map +1 -0
  79. package/dist/assets/server/governance/governance.ingester.d.ts +20 -0
  80. package/dist/assets/server/governance/governance.ingester.js +114 -0
  81. package/dist/assets/server/governance/governance.ingester.js.map +1 -0
  82. package/dist/assets/server/governance/governance.module.d.ts +2 -0
  83. package/dist/assets/server/governance/governance.module.js +26 -0
  84. package/dist/assets/server/governance/governance.module.js.map +1 -0
  85. package/dist/assets/server/governance/governance.service.d.ts +114 -0
  86. package/dist/assets/server/governance/governance.service.js +356 -0
  87. package/dist/assets/server/governance/governance.service.js.map +1 -0
  88. package/dist/assets/server/jobcase/jobcase.controller.d.ts +1 -0
  89. package/dist/assets/server/jobcase/jobcase.entity.d.ts +1 -0
  90. package/dist/assets/server/jobcase/jobcase.entity.js +6 -0
  91. package/dist/assets/server/jobcase/jobcase.entity.js.map +1 -1
  92. package/dist/assets/server/jobcase/jobcase.service.d.ts +2 -0
  93. package/dist/assets/server/jobcase/jobcase.service.js +2 -1
  94. package/dist/assets/server/jobcase/jobcase.service.js.map +1 -1
  95. package/dist/assets/server/main.js +1 -0
  96. package/dist/assets/server/main.js.map +1 -1
  97. package/dist/assets/server/package.json +5 -4
  98. package/dist/assets/server/preference/preference.controller.d.ts +14 -0
  99. package/dist/assets/server/preference/preference.controller.js +66 -0
  100. package/dist/assets/server/preference/preference.controller.js.map +1 -0
  101. package/dist/assets/server/preference/preference.entity.d.ts +13 -0
  102. package/dist/assets/server/preference/preference.entity.js +77 -0
  103. package/dist/assets/server/preference/preference.entity.js.map +1 -0
  104. package/dist/assets/server/preference/preference.module.d.ts +2 -0
  105. package/dist/assets/server/preference/preference.module.js +26 -0
  106. package/dist/assets/server/preference/preference.module.js.map +1 -0
  107. package/dist/assets/server/preference/preference.service.d.ts +33 -0
  108. package/dist/assets/server/preference/preference.service.js +122 -0
  109. package/dist/assets/server/preference/preference.service.js.map +1 -0
  110. package/dist/assets/server/recording/recording-event-queue.service.d.ts +26 -0
  111. package/dist/assets/server/recording/recording-event-queue.service.js +115 -0
  112. package/dist/assets/server/recording/recording-event-queue.service.js.map +1 -0
  113. package/dist/assets/server/recording/recording-retention.service.d.ts +38 -0
  114. package/dist/assets/server/recording/recording-retention.service.js +174 -0
  115. package/dist/assets/server/recording/recording-retention.service.js.map +1 -0
  116. package/dist/assets/server/recording/recording-snapshot.service.d.ts +1 -0
  117. package/dist/assets/server/recording/recording-snapshot.service.js +4 -0
  118. package/dist/assets/server/recording/recording-snapshot.service.js.map +1 -1
  119. package/dist/assets/server/recording/recording.controller.d.ts +21 -1
  120. package/dist/assets/server/recording/recording.controller.js +37 -2
  121. package/dist/assets/server/recording/recording.controller.js.map +1 -1
  122. package/dist/assets/server/recording/recording.gateway.js +11 -0
  123. package/dist/assets/server/recording/recording.gateway.js.map +1 -1
  124. package/dist/assets/server/recording/recording.module.js +16 -2
  125. package/dist/assets/server/recording/recording.module.js.map +1 -1
  126. package/dist/assets/server/recording/recording.service.d.ts +10 -1
  127. package/dist/assets/server/recording/recording.service.js +31 -6
  128. package/dist/assets/server/recording/recording.service.js.map +1 -1
  129. package/dist/assets/server/remark/remark.controller.d.ts +1 -1
  130. package/dist/assets/server/remark/remark.controller.js +1 -10
  131. package/dist/assets/server/remark/remark.controller.js.map +1 -1
  132. package/dist/assets/server/remark/remark.entity.d.ts +1 -1
  133. package/dist/assets/server/remark/remark.entity.js.map +1 -1
  134. package/dist/assets/server/site/entities/site-registry.entity.d.ts +1 -1
  135. package/dist/assets/server/site/entities/site-registry.entity.js +1 -1
  136. package/dist/assets/server/site/entities/site-registry.entity.js.map +1 -1
  137. package/dist/assets/server/site/site.controller.d.ts +1 -1
  138. package/dist/assets/server/site/site.service.d.ts +5 -1
  139. package/dist/assets/server/site/site.service.js +13 -0
  140. package/dist/assets/server/site/site.service.js.map +1 -1
  141. package/dist/assets/server/system/system.controller.d.ts +1 -0
  142. package/dist/assets/server/system/system.controller.js +1 -1
  143. package/dist/assets/server/system/system.controller.js.map +1 -1
  144. package/dist/assets/server/system/system.service.d.ts +1 -0
  145. package/dist/assets/server/system/system.service.js +3 -2
  146. package/dist/assets/server/system/system.service.js.map +1 -1
  147. package/dist/cache/db.js +6 -6
  148. package/dist/commands/addons.js +2 -1
  149. package/dist/commands/cache.js +3 -4
  150. package/dist/commands/case.d.ts +67 -0
  151. package/dist/commands/case.js +101 -0
  152. package/dist/commands/config.d.ts +4 -0
  153. package/dist/commands/config.js +8 -1
  154. package/dist/commands/customer.d.ts +25 -0
  155. package/dist/commands/customer.js +137 -0
  156. package/dist/commands/doctor.js +119 -13
  157. package/dist/commands/extension.js +26 -32
  158. package/dist/commands/extras.d.ts +22 -3
  159. package/dist/commands/extras.js +116 -7
  160. package/dist/commands/init.d.ts +1 -1
  161. package/dist/commands/init.js +3 -4
  162. package/dist/commands/issue-env.d.ts +23 -0
  163. package/dist/commands/issue-env.js +101 -0
  164. package/dist/commands/job.d.ts +6 -2
  165. package/dist/commands/job.js +75 -20
  166. package/dist/commands/login.d.ts +19 -0
  167. package/dist/commands/login.js +85 -13
  168. package/dist/commands/migrate.d.ts +32 -0
  169. package/dist/commands/migrate.js +116 -0
  170. package/dist/commands/note.d.ts +3 -0
  171. package/dist/commands/note.js +75 -0
  172. package/dist/commands/ops.js +82 -15
  173. package/dist/commands/preference.d.ts +6 -0
  174. package/dist/commands/preference.js +80 -0
  175. package/dist/commands/record.js +2 -4
  176. package/dist/commands/report.js +2 -4
  177. package/dist/commands/search.js +9 -2
  178. package/dist/commands/server.d.ts +9 -2
  179. package/dist/commands/server.js +118 -19
  180. package/dist/commands/status.d.ts +4 -0
  181. package/dist/commands/status.js +16 -6
  182. package/dist/commands/update.js +8 -9
  183. package/dist/config.d.ts +21 -0
  184. package/dist/config.js +27 -0
  185. package/dist/i18n/locales/en-US.json +19 -1
  186. package/dist/i18n/locales/zh-CN.json +19 -1
  187. package/dist/index.js +52 -2
  188. package/dist/keychain.d.ts +16 -1
  189. package/dist/keychain.js +43 -16
  190. package/dist/logger.js +2 -2
  191. package/dist/updater.d.ts +1 -1
  192. package/dist/updater.js +24 -7
  193. package/dist/utils/command-error.d.ts +11 -0
  194. package/dist/utils/command-error.js +47 -0
  195. package/dist/utils/link.d.ts +18 -0
  196. package/dist/utils/link.js +83 -0
  197. package/dist/utils/proxy-detect.d.ts +20 -0
  198. package/dist/utils/proxy-detect.js +96 -0
  199. package/dist/utils/script-path.d.ts +5 -0
  200. package/dist/utils/script-path.js +27 -0
  201. package/dist/version.js +34 -9
  202. package/dist/watchdog.d.ts +23 -1
  203. package/dist/watchdog.js +97 -6
  204. package/dist/web/dist/404.html +1 -0
  205. package/dist/web/dist/_next/static/WdJr3NhHjlu5xN0qOap1k/_buildManifest.js +1 -0
  206. package/dist/web/dist/_next/static/WdJr3NhHjlu5xN0qOap1k/_ssgManifest.js +1 -0
  207. package/dist/web/dist/_next/static/chunks/166.f30be223f694f8ee.js +1 -0
  208. package/dist/web/dist/_next/static/chunks/1e6d5a0c.0488932e17ecdf24.js +15 -0
  209. package/dist/web/dist/_next/static/chunks/539-5d0e8c5c238c56f8.js +1 -0
  210. package/dist/web/dist/_next/static/chunks/589-d632233641f35681.js +1 -0
  211. package/dist/web/dist/_next/static/chunks/671.3b8e9cc96b25b6ea.js +1 -0
  212. package/dist/web/dist/_next/static/chunks/935-99f57b7d4151d9e7.js +5 -0
  213. package/dist/web/dist/_next/static/chunks/a7067448-96dd4b8457c447e8.js +1 -0
  214. package/dist/web/dist/_next/static/chunks/app/_not-found/page-fdf1a13a7656886a.js +1 -0
  215. package/dist/web/dist/_next/static/chunks/app/auth/verify/page-8e5cff53d6f7d8e1.js +1 -0
  216. package/dist/web/dist/_next/static/chunks/app/companies/page-e38a69e1929d0953.js +1 -0
  217. package/dist/web/dist/_next/static/chunks/app/entity/page-250f8a9b6d91ab19.js +1 -0
  218. package/dist/web/dist/_next/static/chunks/app/jobs/page-6a281776fec065f6.js +1 -0
  219. package/dist/web/dist/_next/static/chunks/app/layout-83e623e973c1693f.js +1 -0
  220. package/dist/web/dist/_next/static/chunks/app/logs/page-d478031d49f7ba00.js +1 -0
  221. package/dist/web/dist/_next/static/chunks/app/matching/jobs/[id]/talents/page-f57f4b7529823601.js +1 -0
  222. package/dist/web/dist/_next/static/chunks/app/matching/portraits/[id]/jobs/page-614d1e62d7b9d215.js +1 -0
  223. package/dist/web/dist/_next/static/chunks/app/page-e3392de08ae81fba.js +1 -0
  224. package/dist/web/dist/_next/static/chunks/app/parselog/page-58d3acece95b4089.js +1 -0
  225. package/dist/web/dist/_next/static/chunks/app/recordings/page-e2e2e3c3a64083fb.js +1 -0
  226. package/dist/web/dist/_next/static/chunks/app/snapshots/[id]/agent-prompt/page-fd3b7d7373b8f8b7.js +1 -0
  227. package/dist/web/dist/_next/static/chunks/app/talents/page-30748338da20d7e0.js +1 -0
  228. package/dist/web/dist/_next/static/chunks/framework-b3a558b17a070965.js +1 -0
  229. package/dist/web/dist/_next/static/chunks/main-5a4e0cd75dd7f04b.js +1 -0
  230. package/dist/web/dist/_next/static/chunks/main-app-80b038a312917d90.js +1 -0
  231. package/dist/web/dist/_next/static/chunks/pages/_app-da7d628f49271ece.js +4 -0
  232. package/dist/web/dist/_next/static/chunks/pages/_error-17756f67e0a11350.js +1 -0
  233. package/dist/web/dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  234. package/dist/web/dist/_next/static/chunks/webpack-1784baecc6fdcf0b.js +1 -0
  235. package/dist/web/dist/_next/static/css/db881c5b3072b88a.css +3 -0
  236. package/dist/web/dist/_next/static/css/f60d210841fab76b.css +1 -0
  237. package/dist/web/dist/auth/verify.html +1 -0
  238. package/dist/web/dist/auth/verify.txt +8 -0
  239. package/dist/web/dist/companies.html +1 -0
  240. package/dist/web/dist/companies.txt +7 -0
  241. package/dist/web/dist/entity.html +1 -0
  242. package/dist/web/dist/entity.txt +8 -0
  243. package/dist/web/dist/index.html +1 -0
  244. package/dist/web/dist/index.txt +8 -0
  245. package/dist/web/dist/jobs.html +1 -0
  246. package/dist/web/dist/jobs.txt +7 -0
  247. package/dist/web/dist/logs.html +1 -0
  248. package/dist/web/dist/logs.txt +8 -0
  249. package/dist/web/dist/matching/jobs/placeholder/talents.html +1 -0
  250. package/dist/web/dist/matching/jobs/placeholder/talents.txt +8 -0
  251. package/dist/web/dist/matching/portraits/placeholder/jobs.html +1 -0
  252. package/dist/web/dist/matching/portraits/placeholder/jobs.txt +8 -0
  253. package/dist/web/dist/parselog.html +1 -0
  254. package/dist/web/dist/parselog.txt +8 -0
  255. package/dist/web/dist/recordings.html +1 -0
  256. package/dist/web/dist/recordings.txt +8 -0
  257. package/dist/web/dist/snapshots/placeholder/agent-prompt.html +1 -0
  258. package/dist/web/dist/snapshots/placeholder/agent-prompt.txt +8 -0
  259. package/dist/web/dist/talents.html +1 -0
  260. package/dist/web/dist/talents.txt +7 -0
  261. package/package.json +26 -6
@@ -2,8 +2,9 @@ import { Command } from 'commander';
2
2
  import axios from 'axios';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
- import { loadConfig } from '../config.js';
5
+ import { loadConfig, resolveDataDir } from '../config.js';
6
6
  import { getCliLogger } from '../logger.js';
7
+ import { printCommandError } from '../utils/command-error.js';
7
8
  // OpenCLI 风格的通用选项
8
9
  function withFormat(cmd) {
9
10
  return cmd.option('-f, --format <format>', '输出格式: table|json|yaml|md|csv', 'json');
@@ -76,8 +77,37 @@ withFormat(recordingsCommand.command('list').description('列出录制会话(
76
77
  printResult(r.data, opts.format);
77
78
  }
78
79
  catch (e) {
79
- // server 暂未实现 recordings list 时给出空结果与提示,避免 crash
80
- printResult({ note: '/api/recordings GET 未实现,HunterMate server 正在逐步补齐。先从 /api/recordings/:sessionId GET 查看单个。', sessions: [] }, opts.format);
80
+ // hmh#44: /api/recordings GET 早已实现,连接失败应如实报错,不再回退到假提示
81
+ await printCommandError('ops:recordings-list-failed', e);
82
+ process.exit(1);
83
+ }
84
+ });
85
+ /** hmh#44: 按会话删除录制数据(DELETE /api/recordings/:sessionId) */
86
+ recordingsCommand
87
+ .command('delete <sessionId>')
88
+ .description('删除指定录制会话的事件与快照 (DELETE /api/recordings/:id)')
89
+ .action(async (sessionId) => {
90
+ try {
91
+ const r = await http().delete(`/api/recordings/${sessionId}`);
92
+ console.log(JSON.stringify(r.data, null, 2));
93
+ }
94
+ catch (e) {
95
+ await printCommandError('ops:recordings-delete-failed', e);
96
+ process.exit(1);
97
+ }
98
+ });
99
+ /** hmh#44: 手动 VACUUM 缩容,删除数据后让 SQLite 文件真实释放磁盘 */
100
+ recordingsCommand
101
+ .command('compact')
102
+ .description('VACUUM 压缩录制库,释放已删除数据占用的磁盘 (POST /api/recordings/compact)')
103
+ .action(async () => {
104
+ try {
105
+ const r = await http().post('/api/recordings/compact', {});
106
+ console.log(JSON.stringify(r.data, null, 2));
107
+ }
108
+ catch (e) {
109
+ await printCommandError('ops:recordings-compact-failed', e);
110
+ process.exit(1);
81
111
  }
82
112
  });
83
113
  withFormat(recordingsCommand.command('show <sessionId>').description('查看录制详情 (/api/recordings/:id) + 可选 parse'))
@@ -91,7 +121,23 @@ withFormat(recordingsCommand.command('show <sessionId>').description('查看录
91
121
  printResult({ detail, parsed }, opts.format);
92
122
  }
93
123
  catch (e) {
94
- getCliLogger().error('ops:recordings-show-failed', { error: e.message });
124
+ console.error(`✗ ops:recordings-show-failed: ${e.message}`);
125
+ process.exit(1);
126
+ }
127
+ });
128
+ // 手动触发保留期清理(FID-057 #17-B);server 另有每日定时清理
129
+ recordingsCommand
130
+ .command('prune')
131
+ .description('按保留期清理录制事件与快照 (POST /api/recordings/prune)')
132
+ .option('--days <n>', '保留天数(默认取 server 的 RECORDING_RETENTION_DAYS,通常 30)')
133
+ .action(async (opts) => {
134
+ try {
135
+ const body = opts.days !== undefined ? { days: Number(opts.days) } : {};
136
+ const r = await http().post('/api/recordings/prune', body);
137
+ console.log(JSON.stringify(r.data, null, 2));
138
+ }
139
+ catch (e) {
140
+ console.error(`✗ ops:recordings-prune-failed: ${e.message}`);
95
141
  process.exit(1);
96
142
  }
97
143
  });
@@ -99,11 +145,12 @@ withFormat(recordingsCommand.command('show <sessionId>').description('查看录
99
145
  // adapter (OpenCLI 风格)
100
146
  // ---------------------------------------------------------------------------
101
147
  export const adapterCommand = withFormat(new Command('adapter')
102
- .description('站点适配器(对应 chromepilot adapter): list | extract <url>')
103
- .argument('[action]', '操作: list | extract', 'list')
148
+ .description('站点适配器(对应 chromepilot adapter): list | extract <url> | sync')
149
+ .argument('[action]', '操作: list | extract | sync', 'list')
104
150
  .argument('[target]', 'list: 站点名过滤;extract: 页面 URL', '')
105
151
  .option('--tab <pageId>', 'extract: 指定目标 tab(page id;默认复用同站点 tab 或新建后台 tab,不抢焦点)')
106
- .option('--settle-ms <n>', 'extract: 导航后首查等待 ms(默认 1200)', '1200')).action(async (action, target, opts) => {
152
+ .option('--settle-ms <n>', 'extract: 导航后首查等待 ms(默认 1200)', '1200')
153
+ .option('--dry-run', 'sync: 只统计差异不写库', false)).action(async (action, target, opts) => {
107
154
  if (action === 'list' || !action) {
108
155
  try {
109
156
  const r = await http().get('/adapters');
@@ -116,6 +163,7 @@ export const adapterCommand = withFormat(new Command('adapter')
116
163
  // server 暂未实现 /adapters 时退回内置清单
117
164
  printResult({ note: `GET /adapters failed (${e.message}),fallback 内置清单`, adapters: [
118
165
  { platform: 'linkedin', host: 'linkedin.com', command: 'hm.linkedin.extract' },
166
+ { platform: 'liepin', host: 'h.liepin.com', command: 'hm.liepin.extract' },
119
167
  ] }, opts.format);
120
168
  }
121
169
  return;
@@ -147,12 +195,34 @@ export const adapterCommand = withFormat(new Command('adapter')
147
195
  }
148
196
  catch (e) {
149
197
  const detail = e.response?.data?.message || e.message;
150
- getCliLogger().error('ops:adapter-extract-failed', { error: detail });
198
+ console.error(`✗ ops:adapter-extract-failed: ${detail}`);
199
+ process.exit(1);
200
+ }
201
+ return;
202
+ }
203
+ if (action === 'sync') {
204
+ // hunter-mate-harness adapters/ overlay 同步 @fid FID-081-adapter-overlay-suite
205
+ // 路径优先级: env HUNTER_MATE_HARNESS_PATH > config.json harnessPath
206
+ const cfg = loadConfig();
207
+ const harnessPath = process.env.HUNTER_MATE_HARNESS_PATH || cfg?.harnessPath;
208
+ if (!harnessPath || !harnessPath.trim()) {
209
+ getCliLogger().error('ops:adapter-sync-missing-harness-path', {
210
+ hint: '先配置: hunter-mate config --harness-path <hunter-mate-harness 仓绝对路径>(或设 env HUNTER_MATE_HARNESS_PATH)',
211
+ });
212
+ process.exit(1);
213
+ }
214
+ try {
215
+ const r = await http().post('/adapters/sync', { harnessPath, dryRun: !!opts.dryRun });
216
+ printResult(r.data, opts.format);
217
+ }
218
+ catch (e) {
219
+ const detail = e.response?.data?.message || e.message;
220
+ console.error(`✗ ops:adapter-sync-failed: ${detail}`);
151
221
  process.exit(1);
152
222
  }
153
223
  return;
154
224
  }
155
- printResult({ error: `unknown action: ${action}`, supported: ['list', 'extract'] }, opts.format);
225
+ printResult({ error: `unknown action: ${action}`, supported: ['list', 'extract', 'sync'] }, opts.format);
156
226
  });
157
227
  // ---------------------------------------------------------------------------
158
228
  // memory (站点记忆: 基于 selector/流程建议 的持久化 KV)
@@ -162,8 +232,7 @@ export const memoryCommand = withFormat(new Command('memory')
162
232
  .argument('[action]', '操作 (默认 list)', 'list')
163
233
  .argument('[site]', '站点名', '')
164
234
  .argument('[ref]', 'suggest/delete 的元素引用', '')).action((action, site, ref, opts) => {
165
- const HOME = process.env.HUNTER_MATE_HOME || process.env.HOME || process.env.USERPROFILE || '.';
166
- const STORE = path.join(HOME, '.hunter-mate', 'memory.json');
235
+ const STORE = path.join(resolveDataDir(), 'memory.json');
167
236
  let store = {};
168
237
  try {
169
238
  store = JSON.parse(fs.readFileSync(STORE, 'utf-8'));
@@ -288,8 +357,7 @@ export const traceCommand = withFormat(new Command('trace')
288
357
  }
289
358
  catch (e) {
290
359
  // server 未就绪时: 本地追加 fallbacks 到 session JSONL 文件
291
- const HOME = process.env.HUNTER_MATE_HOME || process.env.HOME || process.env.USERPROFILE || '.';
292
- const f = path.join(HOME, '.hunter-mate', 'sessions', `${sessionId}.trace.jsonl`);
360
+ const f = path.join(resolveDataDir(), 'sessions', `${sessionId}.trace.jsonl`);
293
361
  fs.appendFileSync(f, JSON.stringify({ type: opts.type, payload, ts: Date.now() }) + '\n');
294
362
  printResult({ ok: true, stored: 'local', file: f }, opts.format);
295
363
  }
@@ -302,8 +370,7 @@ export const traceCommand = withFormat(new Command('trace')
302
370
  }
303
371
  printResult({ ok: true, sessionId, note: '本地 trace 文件删除(server 侧请通过 api/recordings/* 管理)' }, opts.format);
304
372
  try {
305
- const HOME = process.env.HUNTER_MATE_HOME || process.env.HOME || process.env.USERPROFILE || '.';
306
- const f = path.join(HOME, '.hunter-mate', 'sessions', `${sessionId}.trace.jsonl`);
373
+ const f = path.join(resolveDataDir(), 'sessions', `${sessionId}.trace.jsonl`);
307
374
  if (fs.existsSync(f))
308
375
  fs.unlinkSync(f);
309
376
  }
@@ -0,0 +1,6 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * 三层偏好:scope=user(顾问本人)/ customer(用人单位)/ case(具体职位)。
4
+ * 查询 case 层时自动按 case → customer → user 逐层 merge,按 weight 降序。
5
+ */
6
+ export declare const preferenceCommand: Command;
@@ -0,0 +1,80 @@
1
+ import { Command } from 'commander';
2
+ import axios from 'axios';
3
+ import { loadConfig } from '../config.js';
4
+ import { getToken } from '../keychain.js';
5
+ import { printCommandError } from '../utils/command-error.js';
6
+ const SCOPES = ['user', 'customer', 'case'];
7
+ async function authedHeaders() {
8
+ const token = await getToken().catch(() => undefined);
9
+ return token ? { Authorization: `UTOKEN ${token}` } : {};
10
+ }
11
+ /**
12
+ * 三层偏好:scope=user(顾问本人)/ customer(用人单位)/ case(具体职位)。
13
+ * 查询 case 层时自动按 case → customer → user 逐层 merge,按 weight 降序。
14
+ */
15
+ export const preferenceCommand = new Command('preference').description('三层偏好沉淀(user/customer/case),用于人才推荐筛选');
16
+ preferenceCommand
17
+ .command('set')
18
+ .requiredOption('--scope <scope>', `范围: ${SCOPES.join('|')}`)
19
+ .requiredOption('--id <scopeId>', '范围 id(user id / customer id / job case id)')
20
+ .requiredOption('--dimension <dim>', '维度,如 industry/comp_size/school_tier/reject_reason/must_have')
21
+ .requiredOption('--value <json>', '偏好值(JSON 字符串,如 \'"electronics"\' 或 \'{"min":5}\')')
22
+ .option('--weight <w>', '权重(默认 1)', parseFloat)
23
+ .option('--evidence <text>', '证据来源:哪次推荐/哪条反馈')
24
+ .option('--json', 'JSON 输出')
25
+ .action(async (opts) => {
26
+ try {
27
+ if (!SCOPES.includes(opts.scope)) {
28
+ throw new Error(`--scope 必须是: ${SCOPES.join('|')}`);
29
+ }
30
+ let value;
31
+ try {
32
+ value = JSON.parse(opts.value);
33
+ }
34
+ catch {
35
+ throw new Error(`--value 必须是合法 JSON 字符串: ${opts.value}`);
36
+ }
37
+ const config = loadConfig();
38
+ const res = await axios.put(`${config.serverUrl}/preferences`, {
39
+ scope: opts.scope,
40
+ scopeId: parseInt(opts.id, 10),
41
+ items: [{ dimension: opts.dimension, value, weight: opts.weight, evidence: opts.evidence }],
42
+ }, { headers: await authedHeaders(), proxy: false });
43
+ if (opts.json)
44
+ console.log(JSON.stringify(res.data, null, 2));
45
+ else
46
+ console.log(`✓ 偏好已写入 (${opts.scope}#${opts.id}, ${opts.dimension}=${opts.value})`);
47
+ }
48
+ catch (err) {
49
+ printCommandError('preference:set-failed', err);
50
+ process.exit(1);
51
+ }
52
+ });
53
+ preferenceCommand
54
+ .command('get')
55
+ .requiredOption('--scope <scope>', `范围: ${SCOPES.join('|')}`)
56
+ .requiredOption('--id <scopeId>', '范围 id')
57
+ .option('--customer-id <id>', 'scope=case 时传客户 id,一并带出 customer/user 两层')
58
+ .option('--json', 'JSON 输出(默认 JSON:三层 merge 结构)')
59
+ .action(async (opts) => {
60
+ try {
61
+ if (!SCOPES.includes(opts.scope)) {
62
+ throw new Error(`--scope 必须是: ${SCOPES.join('|')}`);
63
+ }
64
+ const config = loadConfig();
65
+ const res = await axios.get(`${config.serverUrl}/preferences`, {
66
+ params: {
67
+ scope: opts.scope,
68
+ scopeId: parseInt(opts.id, 10),
69
+ ...(opts.customerId ? { customerId: parseInt(opts.customerId, 10) } : {}),
70
+ },
71
+ headers: await authedHeaders(),
72
+ proxy: false,
73
+ });
74
+ console.log(JSON.stringify(res.data, null, 2));
75
+ }
76
+ catch (err) {
77
+ printCommandError('preference:get-failed', err);
78
+ process.exit(1);
79
+ }
80
+ });
@@ -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
+ await 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
+ await 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
+ await printCommandError('search:failed', err);
44
+ process.exit(1);
45
+ }
39
46
  });
@@ -1,4 +1,11 @@
1
1
  import { Command } from 'commander';
2
+ /**
3
+ * server.log 无界增长曾达 800M+ 挤占用户磁盘(FID-068)。
4
+ * 后台 daemon 启动前按大小轮转:超过 HM_LOG_MAX_BYTES(默认 50MB)时
5
+ * server.log → server.log.1 → …保留最近 LOG_KEEP_FILES 份,最旧丢弃。
6
+ * 纯 fs 实现,Windows 兼容(rename 前先清目标,Windows 不允许覆盖已有文件)。
7
+ */
8
+ export declare function rotateLogIfNeeded(logFile: string, maxBytes?: number, keep?: number): boolean;
2
9
  declare function tryReadPid(): number | null;
3
10
  export { tryReadPid };
4
11
  /** 定位 server 包根(dist 优先,ts-node 开发模式兜底);找不到返回 null */
@@ -14,10 +21,10 @@ export declare function runStart(opts: {
14
21
  url: string;
15
22
  msg: string;
16
23
  }>;
17
- export declare function runStop(): {
24
+ export declare function runStop(): Promise<{
18
25
  stopped: boolean;
19
26
  pid: number | null;
20
27
  msg: string;
21
- };
28
+ }>;
22
29
  export declare const startCommand: Command;
23
30
  export declare const stopCommand: Command;
@@ -3,11 +3,12 @@ import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { spawn } from 'child_process';
6
- import { loadConfig, saveConfig } from '../config.js';
6
+ import { loadConfig, saveConfig, resolveDataDir } from '../config.js';
7
7
  import { killGracefullySync } from '../utils/proc.js';
8
8
  import { findBundledAssets } from '../bundled-assets.js';
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
- const PID_FILE = () => path.join(process.env.HUNTER_MATE_HOME || process.env.HOME || process.env.USERPROFILE || '.', '.hunter-mate', 'server.pid');
10
+ /** server.pid 等运行时文件随数据目录走(FID-071:dataDir 可配置后 server 状态文件一并迁移) */
11
+ const PID_FILE = () => path.join(resolveDataDir(), 'server.pid');
11
12
  const LOG_FILE = () => PID_FILE().replace(/server\.pid$/, 'server.log');
12
13
  const WATCHDOG_PID_FILE = () => PID_FILE().replace(/server\.pid$/, 'watchdog.pid');
13
14
  function tryReadPidFile(file) {
@@ -29,6 +30,38 @@ function cleanPidFile(file) {
29
30
  }
30
31
  catch { /* ignore */ }
31
32
  }
33
+ const LOG_MAX_BYTES = () => Number(process.env.HM_LOG_MAX_BYTES || 50 * 1024 * 1024);
34
+ const LOG_KEEP_FILES = 3;
35
+ /**
36
+ * server.log 无界增长曾达 800M+ 挤占用户磁盘(FID-068)。
37
+ * 后台 daemon 启动前按大小轮转:超过 HM_LOG_MAX_BYTES(默认 50MB)时
38
+ * server.log → server.log.1 → …保留最近 LOG_KEEP_FILES 份,最旧丢弃。
39
+ * 纯 fs 实现,Windows 兼容(rename 前先清目标,Windows 不允许覆盖已有文件)。
40
+ */
41
+ export function rotateLogIfNeeded(logFile, maxBytes = LOG_MAX_BYTES(), keep = LOG_KEEP_FILES) {
42
+ try {
43
+ if (!fs.existsSync(logFile))
44
+ return false;
45
+ if (fs.statSync(logFile).size <= maxBytes)
46
+ return false;
47
+ for (let i = keep - 1; i >= 1; i--) {
48
+ const src = `${logFile}.${i}`;
49
+ const dest = `${logFile}.${i + 1}`;
50
+ if (!fs.existsSync(src))
51
+ continue;
52
+ try {
53
+ fs.unlinkSync(dest);
54
+ }
55
+ catch { /* dest 不存在时忽略 */ }
56
+ fs.renameSync(src, dest);
57
+ }
58
+ fs.renameSync(logFile, `${logFile}.1`);
59
+ return true;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
32
65
  function tryReadPid() {
33
66
  const pid = tryReadPidFile(PID_FILE());
34
67
  if (pid)
@@ -55,25 +88,50 @@ export function findServerRoot() {
55
88
  }
56
89
  return null;
57
90
  }
91
+ /** 探活:server /health 是否可访问(短超时,永不 throw) */
92
+ async function probeServerHealth(url, timeoutMs = 1500) {
93
+ try {
94
+ const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(timeoutMs) });
95
+ return res.ok;
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
101
+ /** 从 config.serverUrl 解析默认端口(与 runStart 逻辑一致) */
102
+ function defaultPortFromConfig(serverUrl) {
103
+ return new URL(serverUrl).port ? Number(new URL(serverUrl).port) : 34570;
104
+ }
58
105
  /** 对应 ChromePilot start: 启动本地 NestJS Server(提供 REST + WebSocket + SPA) */
59
106
  export async function runStart(opts) {
60
107
  const config = loadConfig();
61
- const defaultPort = new URL(config.serverUrl).port
62
- ? Number(new URL(config.serverUrl).port)
63
- : 34570;
108
+ const defaultPort = defaultPortFromConfig(config.serverUrl);
64
109
  const port = opts.port ?? defaultPort;
65
110
  const host = opts.host ?? '127.0.0.1';
66
111
  const url = `http://${host}:${port}`;
112
+ // 启动过程中的告警说明(会拼进最终返回 msg,保证用户看得见)
113
+ const notes = [];
67
114
  const existing = tryReadPid();
68
115
  if (existing) {
69
- // 已在运行: 先做版本一致性检测(磁盘版本 vs 运行版本,server 不一致则自动重启)
70
- // 动态 import 避免与 version.ts 的循环依赖(version.ts 静态引入本文件的 runStart/runStop)
71
- const { ensureRuntimeConsistency } = await import('../version.js');
72
- const report = await ensureRuntimeConsistency({ force: true });
73
- if (report.actions.serverRestart && report.performed.length) {
74
- return { started: true, pid: null, url, msg: report.performed.join('; ') };
116
+ // pid 文件存在 ≠ server 存活。pid 可能被系统中其他进程复用
117
+ // (Windows 现场:残留 node.exe 复用了 pid → 误判 already running,
118
+ // server 实际从未被拉起,日志停在旧时间戳,用户无任何感知)。
119
+ // 因此必须先 /health 探活再决定是否走"已在运行"分支。
120
+ if (await probeServerHealth(url)) {
121
+ // 已在运行: 先做版本一致性检测(磁盘版本 vs 运行版本,server 不一致则自动重启)
122
+ // 动态 import 避免与 version.ts 的循环依赖(version.ts 静态引入本文件的 runStart/runStop)
123
+ const { ensureRuntimeConsistency } = await import('../version.js');
124
+ const report = await ensureRuntimeConsistency({ force: true });
125
+ if (report.actions.serverRestart && report.performed.length) {
126
+ return { started: true, pid: null, url, msg: report.performed.join('; ') };
127
+ }
128
+ return { started: false, pid: existing, url, msg: `server already running (pid=${existing})` };
75
129
  }
76
- return { started: false, pid: existing, url, msg: `server already running (pid=${existing})` };
130
+ // stale pid:进程号存活但不是我们的 server(或 server 已僵死)。清理后继续启动,
131
+ // 不 kill —— 那个进程可能属于别的程序(误杀 foreign 进程红线)。
132
+ notes.push(`⚠ 检测到 stale server.pid (pid=${existing}):该进程存活但 ${url}/health 无响应,` +
133
+ `疑似 pid 被其他进程复用,已忽略并继续启动`);
134
+ cleanPidFile(PID_FILE());
77
135
  }
78
136
  // server 入口: bundled 模式下 <cli>/dist/assets/server/main.js;monorepo 下 packages/server/dist/main.js 或 src/main.ts(开发模式 ts-node)
79
137
  const serverRoot = findServerRoot();
@@ -106,6 +164,8 @@ export async function runStart(opts) {
106
164
  ...process.env,
107
165
  PORT: String(port),
108
166
  HOST: host,
167
+ // FID-071: server 所有数据落点(db/备份/runtime.json/artifacts)由 CLI 的数据目录决定
168
+ HM_DATA_DIR: resolveDataDir(),
109
169
  };
110
170
  const nodeArgs = [];
111
171
  if (entry.endsWith('.ts')) {
@@ -123,6 +183,8 @@ export async function runStart(opts) {
123
183
  const logDir = path.dirname(LOG_FILE());
124
184
  if (!fs.existsSync(logDir))
125
185
  fs.mkdirSync(logDir, { recursive: true });
186
+ // 后台 daemon 启动前轮转 server.log(超 50MB 保留 3 份),防止无界增长挤占磁盘
187
+ rotateLogIfNeeded(LOG_FILE());
126
188
  // 后台模式由独立 watchdog 进程托管 server,实现崩溃后自动重启
127
189
  const watchdogEntry = path.resolve(__dirname, '../watchdog.js');
128
190
  if (!fs.existsSync(watchdogEntry)) {
@@ -138,6 +200,7 @@ export async function runStart(opts) {
138
200
  HM_SERVER_ENTRY: entry,
139
201
  HM_SERVER_LOG: LOG_FILE(),
140
202
  HM_SERVER_NODE_ARGS: nodeArgs.join(','),
203
+ HM_DATA_DIR: resolveDataDir(),
141
204
  PORT: String(port),
142
205
  HOST: host,
143
206
  };
@@ -150,8 +213,11 @@ export async function runStart(opts) {
150
213
  wd.unref();
151
214
  if (wd.pid)
152
215
  fs.writeFileSync(WATCHDOG_PID_FILE(), String(wd.pid));
153
- // 等待 server health 就绪(最多 8s)
154
- const deadline = Date.now() + 8000;
216
+ // 等待 server health 就绪。窗口给足:NestJS 全量路由映射 + TypeORM schema 同步
217
+ // 在慢机/Windows 上实测 10-15s,原 8s 会在 route mapping 阶段误报"未就绪"
218
+ // (server 实际 3 秒后就绪,watchdog 会保活,误报只会误导用户)。
219
+ const probeDeadlineMs = 30_000;
220
+ const deadline = Date.now() + probeDeadlineMs;
155
221
  let ready = false;
156
222
  while (Date.now() < deadline) {
157
223
  await new Promise((r) => setTimeout(r, 500));
@@ -179,7 +245,8 @@ export async function runStart(opts) {
179
245
  started: false,
180
246
  pid: wd.pid ?? null,
181
247
  url,
182
- msg: `✗ server watchdog 启动后未就绪(pid=${wd.pid}),日志尾部:\n${tail || '(server.log 为空)'}\n` +
248
+ msg: (notes.length ? notes.join('\n') + '\n' : '') +
249
+ `✗ server watchdog 启动后 ${probeDeadlineMs / 1000}s 内未就绪(pid=${wd.pid}),日志尾部:\n${tail || '(server.log 为空)'}\n` +
183
250
  `排障: hunter-mate doctor 或 TROUBLESHOOTING.md#server-启动即崩abi`,
184
251
  };
185
252
  }
@@ -193,12 +260,34 @@ export async function runStart(opts) {
193
260
  }
194
261
  }
195
262
  catch { /* ignore */ }
196
- return { started: true, pid: wd.pid ?? null, url, msg: `server watchdog started (pid=${wd.pid}) on ${url} — logs: ${LOG_FILE()}` };
263
+ return {
264
+ started: true,
265
+ pid: wd.pid ?? null,
266
+ url,
267
+ msg: (notes.length ? notes.join('\n') + '\n' : '') +
268
+ `server watchdog started (pid=${wd.pid}) on ${url} — logs: ${LOG_FILE()}`,
269
+ };
197
270
  }
198
- export function runStop() {
271
+ export async function runStop() {
272
+ const config = loadConfig();
273
+ const url = `http://127.0.0.1:${defaultPortFromConfig(config.serverUrl)}`;
274
+ // kill 任何进程前先 /health 确认它真的是我们的 server。
275
+ // pid 文件可能被复用(Windows 现场:残留 node.exe 占用 pid),误杀 foreign 进程不可接受。
276
+ const healthy = await probeServerHealth(url);
199
277
  // 优先停止 watchdog,让它去停止子 server
200
278
  const wdPid = tryReadPidFile(WATCHDOG_PID_FILE());
201
279
  if (wdPid) {
280
+ if (!healthy) {
281
+ // server 不响应时不杀 watchdog pid(可能已被其他进程复用),只清理 pid 文件
282
+ cleanPidFile(WATCHDOG_PID_FILE());
283
+ cleanPidFile(PID_FILE());
284
+ return {
285
+ stopped: false,
286
+ pid: wdPid,
287
+ msg: `✗ server 无响应(${url}/health 不可达),watchdog pid=${wdPid} 未杀(疑似 stale pid,` +
288
+ `可能已被其他进程复用)。已清理 pid 文件;如确有残留进程请手工确认后 kill`,
289
+ };
290
+ }
202
291
  const ok = killGracefullySync(wdPid);
203
292
  cleanPidFile(WATCHDOG_PID_FILE());
204
293
  if (ok) {
@@ -218,6 +307,16 @@ export function runStop() {
218
307
  const pid = tryReadPid();
219
308
  if (!pid)
220
309
  return { stopped: false, pid: null, msg: 'no server.pid found or process dead' };
310
+ if (!healthy) {
311
+ // pid 存活但 server 不响应:不 kill(可能复用了别人的 pid),只清 pid 文件
312
+ cleanPidFile(PID_FILE());
313
+ return {
314
+ stopped: false,
315
+ pid,
316
+ msg: `✗ pid=${pid} 存活但 server 无响应,未发送 kill 信号(疑似 stale pid,` +
317
+ `可能属于其他进程)。已清理 pid 文件;如确认是僵死 server 请手工 kill`,
318
+ };
319
+ }
221
320
  const ok = killGracefullySync(pid);
222
321
  if (!ok) {
223
322
  return { stopped: false, pid, msg: `failed to signal ${pid}` };
@@ -238,8 +337,8 @@ export const startCommand = new Command('start')
238
337
  });
239
338
  export const stopCommand = new Command('stop')
240
339
  .description('停止本地 server(读 ~/.hunter-mate/server.pid 并 SIGTERM)')
241
- .action(() => {
242
- const r = runStop();
340
+ .action(async () => {
341
+ const r = await runStop();
243
342
  console.log(r.msg);
244
343
  if (!r.stopped && r.pid)
245
344
  process.exit(1);
@@ -1,8 +1,11 @@
1
1
  import { Command } from 'commander';
2
+ import { TokenIssuer } from '../keychain.js';
2
3
  import { InstalledVersions, SystemVersions } from '@hunter-mate/shared';
3
4
  export interface StatusResult {
4
5
  version: string;
5
6
  loggedIn: boolean;
7
+ /** token 签发方(hmh#43):saas 签发的 token 会被本地 server 拒绝 */
8
+ tokenIssuer?: TokenIssuer;
6
9
  server: 'running' | 'not running' | 'unknown';
7
10
  serverUrl: string;
8
11
  extensionCount: number;
@@ -15,6 +18,7 @@ export interface StatusResult {
15
18
  }>;
16
19
  installed?: InstalledVersions;
17
20
  live?: SystemVersions | null;
21
+ dataDir: string;
18
22
  }
19
23
  export declare function runStatus(): Promise<StatusResult>;
20
24
  export declare const statusCommand: Command;
@@ -1,13 +1,14 @@
1
1
  import { Command } from 'commander';
2
2
  import axios from 'axios';
3
3
  import { i18n } from '../i18n/index.js';
4
- import { loadConfig } from '../config.js';
5
- import { getToken } from '../keychain.js';
4
+ import { loadConfig, resolveDataDir } from '../config.js';
5
+ import { getTokenInfo } from '../keychain.js';
6
6
  import { readInstalledVersions, fetchLiveVersions } from '../version.js';
7
7
  import { getCliLogger } from '../logger.js';
8
8
  export async function runStatus() {
9
9
  const config = loadConfig();
10
- const token = await getToken();
10
+ const tokenInfo = await getTokenInfo();
11
+ const token = tokenInfo?.token ?? null;
11
12
  let serverStatus = 'unknown';
12
13
  let extensions = [];
13
14
  let live = null;
@@ -28,15 +29,18 @@ export async function runStatus() {
28
29
  serverStatus = 'not running';
29
30
  getCliLogger().warn('status:server-not-running', { serverUrl: config.serverUrl });
30
31
  }
32
+ const installed = readInstalledVersions();
31
33
  return {
32
- version: '0.1.0',
34
+ version: installed.cli ?? '0.0.0',
33
35
  loggedIn: !!token,
36
+ tokenIssuer: tokenInfo?.issuer,
34
37
  server: serverStatus,
35
38
  serverUrl: config.serverUrl,
36
39
  extensionCount: extensions.length,
37
40
  extensions,
38
- installed: readInstalledVersions(),
41
+ installed,
39
42
  live,
43
+ dataDir: resolveDataDir(),
40
44
  };
41
45
  }
42
46
  export const statusCommand = new Command('status')
@@ -49,8 +53,14 @@ export const statusCommand = new Command('status')
49
53
  return;
50
54
  }
51
55
  console.log(`${i18n.t('status.cli')}: ${status.version}`);
52
- console.log(`${i18n.t('status.loggedIn')}: ${status.loggedIn ? 'yes' : 'no'}`);
56
+ const loggedInText = status.loggedIn
57
+ ? status.tokenIssuer === 'saas'
58
+ ? 'yes (SaaS 签发 — 本地 server 会拒绝,启动 server 后请重新登录)'
59
+ : 'yes'
60
+ : 'no';
61
+ console.log(`${i18n.t('status.loggedIn')}: ${loggedInText}`);
53
62
  console.log(`${i18n.t('status.server')}: ${status.server} (${status.serverUrl})`);
63
+ console.log(`dataDir: ${status.dataDir}`);
54
64
  // 版本一致性: 磁盘(期望) vs 运行中
55
65
  if (status.installed) {
56
66
  const inst = status.installed;