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.
- package/dist/assets/extension/background/service-worker.js +193 -16
- package/dist/assets/extension/manifest.json +1 -1
- package/dist/assets/extension/package.json +4 -3
- package/dist/assets/server/app.module.js +2 -1
- package/dist/assets/server/app.module.js.map +1 -1
- package/dist/assets/server/auth/auth.controller.d.ts +7 -0
- package/dist/assets/server/auth/auth.controller.js +20 -0
- package/dist/assets/server/auth/auth.controller.js.map +1 -1
- package/dist/assets/server/auth/auth.service.d.ts +11 -0
- package/dist/assets/server/auth/auth.service.js +58 -1
- package/dist/assets/server/auth/auth.service.js.map +1 -1
- package/dist/assets/server/build-info.json +1 -1
- package/dist/assets/server/extension/extension-control.controller.d.ts +3 -0
- package/dist/assets/server/extension/extension-control.controller.js +1 -1
- package/dist/assets/server/extension/extension-control.controller.js.map +1 -1
- package/dist/assets/server/extension/extension-control.service.d.ts +8 -0
- package/dist/assets/server/extension/extension-control.service.js +38 -1
- package/dist/assets/server/extension/extension-control.service.js.map +1 -1
- package/dist/assets/server/package.json +4 -3
- package/dist/assets/server/recording/recording-event-queue.service.d.ts +26 -0
- package/dist/assets/server/recording/recording-event-queue.service.js +115 -0
- package/dist/assets/server/recording/recording-event-queue.service.js.map +1 -0
- package/dist/assets/server/recording/recording-retention.service.d.ts +21 -0
- package/dist/assets/server/recording/recording-retention.service.js +85 -0
- package/dist/assets/server/recording/recording-retention.service.js.map +1 -0
- package/dist/assets/server/recording/recording.controller.d.ts +11 -1
- package/dist/assets/server/recording/recording.controller.js +18 -2
- package/dist/assets/server/recording/recording.controller.js.map +1 -1
- package/dist/assets/server/recording/recording.module.js +10 -1
- package/dist/assets/server/recording/recording.module.js.map +1 -1
- package/dist/assets/server/recording/recording.service.d.ts +5 -1
- package/dist/assets/server/recording/recording.service.js +21 -6
- package/dist/assets/server/recording/recording.service.js.map +1 -1
- package/dist/commands/cache.js +3 -4
- package/dist/commands/doctor.js +94 -12
- package/dist/commands/extension.js +14 -23
- package/dist/commands/extras.js +2 -2
- package/dist/commands/job.d.ts +4 -1
- package/dist/commands/job.js +51 -16
- package/dist/commands/login.d.ts +15 -0
- package/dist/commands/login.js +57 -12
- package/dist/commands/ops.js +18 -2
- package/dist/commands/record.js +2 -4
- package/dist/commands/report.js +2 -4
- package/dist/commands/search.js +9 -2
- package/dist/commands/server.d.ts +2 -2
- package/dist/commands/server.js +78 -17
- package/dist/commands/update.js +1 -1
- package/dist/i18n/locales/en-US.json +3 -1
- package/dist/i18n/locales/zh-CN.json +3 -1
- package/dist/index.js +38 -2
- package/dist/updater.d.ts +1 -1
- package/dist/updater.js +22 -5
- package/dist/utils/command-error.d.ts +9 -0
- package/dist/utils/command-error.js +31 -0
- package/dist/utils/proxy-detect.d.ts +20 -0
- package/dist/utils/proxy-detect.js +96 -0
- package/dist/utils/script-path.d.ts +5 -0
- package/dist/utils/script-path.js +27 -0
- package/dist/version.js +1 -1
- package/dist/watchdog.d.ts +23 -1
- package/dist/watchdog.js +95 -4
- package/dist/web/dist/404.html +1 -0
- package/dist/web/dist/_next/static/chunks/166.f30be223f694f8ee.js +1 -0
- package/dist/web/dist/_next/static/chunks/1e6d5a0c.0488932e17ecdf24.js +15 -0
- package/dist/web/dist/_next/static/chunks/539-5d0e8c5c238c56f8.js +1 -0
- package/dist/web/dist/_next/static/chunks/589-d632233641f35681.js +1 -0
- package/dist/web/dist/_next/static/chunks/671.3b8e9cc96b25b6ea.js +1 -0
- package/dist/web/dist/_next/static/chunks/935-99f57b7d4151d9e7.js +5 -0
- package/dist/web/dist/_next/static/chunks/a7067448-96dd4b8457c447e8.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/_not-found/page-fdf1a13a7656886a.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/auth/verify/page-8e5cff53d6f7d8e1.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/companies/page-e38a69e1929d0953.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/entity/page-250f8a9b6d91ab19.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/jobs/page-6a281776fec065f6.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/layout-83e623e973c1693f.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/logs/page-9f41d44fa1add723.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/matching/jobs/[id]/talents/page-f57f4b7529823601.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/matching/portraits/[id]/jobs/page-614d1e62d7b9d215.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/page-e3392de08ae81fba.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/parselog/page-58d3acece95b4089.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/recordings/page-e2e2e3c3a64083fb.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/snapshots/[id]/agent-prompt/page-fd3b7d7373b8f8b7.js +1 -0
- package/dist/web/dist/_next/static/chunks/app/talents/page-30748338da20d7e0.js +1 -0
- package/dist/web/dist/_next/static/chunks/framework-b3a558b17a070965.js +1 -0
- package/dist/web/dist/_next/static/chunks/main-5a4e0cd75dd7f04b.js +1 -0
- package/dist/web/dist/_next/static/chunks/main-app-80b038a312917d90.js +1 -0
- package/dist/web/dist/_next/static/chunks/pages/_app-da7d628f49271ece.js +4 -0
- package/dist/web/dist/_next/static/chunks/pages/_error-17756f67e0a11350.js +1 -0
- package/dist/web/dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/dist/web/dist/_next/static/chunks/webpack-1784baecc6fdcf0b.js +1 -0
- package/dist/web/dist/_next/static/css/db881c5b3072b88a.css +3 -0
- package/dist/web/dist/_next/static/css/f60d210841fab76b.css +1 -0
- package/dist/web/dist/_next/static/hqJ-7vr4X71DNfzZugHJ-/_buildManifest.js +1 -0
- package/dist/web/dist/_next/static/hqJ-7vr4X71DNfzZugHJ-/_ssgManifest.js +1 -0
- package/dist/web/dist/auth/verify.html +1 -0
- package/dist/web/dist/auth/verify.txt +8 -0
- package/dist/web/dist/companies.html +1 -0
- package/dist/web/dist/companies.txt +7 -0
- package/dist/web/dist/entity.html +1 -0
- package/dist/web/dist/entity.txt +8 -0
- package/dist/web/dist/index.html +1 -0
- package/dist/web/dist/index.txt +8 -0
- package/dist/web/dist/jobs.html +1 -0
- package/dist/web/dist/jobs.txt +7 -0
- package/dist/web/dist/logs.html +1 -0
- package/dist/web/dist/logs.txt +8 -0
- package/dist/web/dist/matching/jobs/placeholder/talents.html +1 -0
- package/dist/web/dist/matching/jobs/placeholder/talents.txt +8 -0
- package/dist/web/dist/matching/portraits/placeholder/jobs.html +1 -0
- package/dist/web/dist/matching/portraits/placeholder/jobs.txt +8 -0
- package/dist/web/dist/parselog.html +1 -0
- package/dist/web/dist/parselog.txt +8 -0
- package/dist/web/dist/recordings.html +1 -0
- package/dist/web/dist/recordings.txt +8 -0
- package/dist/web/dist/snapshots/placeholder/agent-prompt.html +1 -0
- package/dist/web/dist/snapshots/placeholder/agent-prompt.txt +8 -0
- package/dist/web/dist/talents.html +1 -0
- package/dist/web/dist/talents.txt +7 -0
- package/package.json +25 -5
package/dist/commands/server.js
CHANGED
|
@@ -55,25 +55,50 @@ export function findServerRoot() {
|
|
|
55
55
|
}
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
|
+
/** 探活:server /health 是否可访问(短超时,永不 throw) */
|
|
59
|
+
async function probeServerHealth(url, timeoutMs = 1500) {
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
62
|
+
return res.ok;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** 从 config.serverUrl 解析默认端口(与 runStart 逻辑一致) */
|
|
69
|
+
function defaultPortFromConfig(serverUrl) {
|
|
70
|
+
return new URL(serverUrl).port ? Number(new URL(serverUrl).port) : 34570;
|
|
71
|
+
}
|
|
58
72
|
/** 对应 ChromePilot start: 启动本地 NestJS Server(提供 REST + WebSocket + SPA) */
|
|
59
73
|
export async function runStart(opts) {
|
|
60
74
|
const config = loadConfig();
|
|
61
|
-
const defaultPort =
|
|
62
|
-
? Number(new URL(config.serverUrl).port)
|
|
63
|
-
: 34570;
|
|
75
|
+
const defaultPort = defaultPortFromConfig(config.serverUrl);
|
|
64
76
|
const port = opts.port ?? defaultPort;
|
|
65
77
|
const host = opts.host ?? '127.0.0.1';
|
|
66
78
|
const url = `http://${host}:${port}`;
|
|
79
|
+
// 启动过程中的告警说明(会拼进最终返回 msg,保证用户看得见)
|
|
80
|
+
const notes = [];
|
|
67
81
|
const existing = tryReadPid();
|
|
68
82
|
if (existing) {
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
83
|
+
// pid 文件存在 ≠ server 存活。pid 可能被系统中其他进程复用
|
|
84
|
+
// (Windows 现场:残留 node.exe 复用了 pid → 误判 already running,
|
|
85
|
+
// server 实际从未被拉起,日志停在旧时间戳,用户无任何感知)。
|
|
86
|
+
// 因此必须先 /health 探活再决定是否走"已在运行"分支。
|
|
87
|
+
if (await probeServerHealth(url)) {
|
|
88
|
+
// 已在运行: 先做版本一致性检测(磁盘版本 vs 运行版本,server 不一致则自动重启)
|
|
89
|
+
// 动态 import 避免与 version.ts 的循环依赖(version.ts 静态引入本文件的 runStart/runStop)
|
|
90
|
+
const { ensureRuntimeConsistency } = await import('../version.js');
|
|
91
|
+
const report = await ensureRuntimeConsistency({ force: true });
|
|
92
|
+
if (report.actions.serverRestart && report.performed.length) {
|
|
93
|
+
return { started: true, pid: null, url, msg: report.performed.join('; ') };
|
|
94
|
+
}
|
|
95
|
+
return { started: false, pid: existing, url, msg: `server already running (pid=${existing})` };
|
|
75
96
|
}
|
|
76
|
-
|
|
97
|
+
// stale pid:进程号存活但不是我们的 server(或 server 已僵死)。清理后继续启动,
|
|
98
|
+
// 不 kill —— 那个进程可能属于别的程序(误杀 foreign 进程红线)。
|
|
99
|
+
notes.push(`⚠ 检测到 stale server.pid (pid=${existing}):该进程存活但 ${url}/health 无响应,` +
|
|
100
|
+
`疑似 pid 被其他进程复用,已忽略并继续启动`);
|
|
101
|
+
cleanPidFile(PID_FILE());
|
|
77
102
|
}
|
|
78
103
|
// server 入口: bundled 模式下 <cli>/dist/assets/server/main.js;monorepo 下 packages/server/dist/main.js 或 src/main.ts(开发模式 ts-node)
|
|
79
104
|
const serverRoot = findServerRoot();
|
|
@@ -150,8 +175,11 @@ export async function runStart(opts) {
|
|
|
150
175
|
wd.unref();
|
|
151
176
|
if (wd.pid)
|
|
152
177
|
fs.writeFileSync(WATCHDOG_PID_FILE(), String(wd.pid));
|
|
153
|
-
// 等待 server health
|
|
154
|
-
|
|
178
|
+
// 等待 server health 就绪。窗口给足:NestJS 全量路由映射 + TypeORM schema 同步
|
|
179
|
+
// 在慢机/Windows 上实测 10-15s,原 8s 会在 route mapping 阶段误报"未就绪"
|
|
180
|
+
// (server 实际 3 秒后就绪,watchdog 会保活,误报只会误导用户)。
|
|
181
|
+
const probeDeadlineMs = 30_000;
|
|
182
|
+
const deadline = Date.now() + probeDeadlineMs;
|
|
155
183
|
let ready = false;
|
|
156
184
|
while (Date.now() < deadline) {
|
|
157
185
|
await new Promise((r) => setTimeout(r, 500));
|
|
@@ -179,7 +207,8 @@ export async function runStart(opts) {
|
|
|
179
207
|
started: false,
|
|
180
208
|
pid: wd.pid ?? null,
|
|
181
209
|
url,
|
|
182
|
-
msg:
|
|
210
|
+
msg: (notes.length ? notes.join('\n') + '\n' : '') +
|
|
211
|
+
`✗ server watchdog 启动后 ${probeDeadlineMs / 1000}s 内未就绪(pid=${wd.pid}),日志尾部:\n${tail || '(server.log 为空)'}\n` +
|
|
183
212
|
`排障: hunter-mate doctor 或 TROUBLESHOOTING.md#server-启动即崩abi`,
|
|
184
213
|
};
|
|
185
214
|
}
|
|
@@ -193,12 +222,34 @@ export async function runStart(opts) {
|
|
|
193
222
|
}
|
|
194
223
|
}
|
|
195
224
|
catch { /* ignore */ }
|
|
196
|
-
return {
|
|
225
|
+
return {
|
|
226
|
+
started: true,
|
|
227
|
+
pid: wd.pid ?? null,
|
|
228
|
+
url,
|
|
229
|
+
msg: (notes.length ? notes.join('\n') + '\n' : '') +
|
|
230
|
+
`server watchdog started (pid=${wd.pid}) on ${url} — logs: ${LOG_FILE()}`,
|
|
231
|
+
};
|
|
197
232
|
}
|
|
198
|
-
export function runStop() {
|
|
233
|
+
export async function runStop() {
|
|
234
|
+
const config = loadConfig();
|
|
235
|
+
const url = `http://127.0.0.1:${defaultPortFromConfig(config.serverUrl)}`;
|
|
236
|
+
// kill 任何进程前先 /health 确认它真的是我们的 server。
|
|
237
|
+
// pid 文件可能被复用(Windows 现场:残留 node.exe 占用 pid),误杀 foreign 进程不可接受。
|
|
238
|
+
const healthy = await probeServerHealth(url);
|
|
199
239
|
// 优先停止 watchdog,让它去停止子 server
|
|
200
240
|
const wdPid = tryReadPidFile(WATCHDOG_PID_FILE());
|
|
201
241
|
if (wdPid) {
|
|
242
|
+
if (!healthy) {
|
|
243
|
+
// server 不响应时不杀 watchdog pid(可能已被其他进程复用),只清理 pid 文件
|
|
244
|
+
cleanPidFile(WATCHDOG_PID_FILE());
|
|
245
|
+
cleanPidFile(PID_FILE());
|
|
246
|
+
return {
|
|
247
|
+
stopped: false,
|
|
248
|
+
pid: wdPid,
|
|
249
|
+
msg: `✗ server 无响应(${url}/health 不可达),watchdog pid=${wdPid} 未杀(疑似 stale pid,` +
|
|
250
|
+
`可能已被其他进程复用)。已清理 pid 文件;如确有残留进程请手工确认后 kill`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
202
253
|
const ok = killGracefullySync(wdPid);
|
|
203
254
|
cleanPidFile(WATCHDOG_PID_FILE());
|
|
204
255
|
if (ok) {
|
|
@@ -218,6 +269,16 @@ export function runStop() {
|
|
|
218
269
|
const pid = tryReadPid();
|
|
219
270
|
if (!pid)
|
|
220
271
|
return { stopped: false, pid: null, msg: 'no server.pid found or process dead' };
|
|
272
|
+
if (!healthy) {
|
|
273
|
+
// pid 存活但 server 不响应:不 kill(可能复用了别人的 pid),只清 pid 文件
|
|
274
|
+
cleanPidFile(PID_FILE());
|
|
275
|
+
return {
|
|
276
|
+
stopped: false,
|
|
277
|
+
pid,
|
|
278
|
+
msg: `✗ pid=${pid} 存活但 server 无响应,未发送 kill 信号(疑似 stale pid,` +
|
|
279
|
+
`可能属于其他进程)。已清理 pid 文件;如确认是僵死 server 请手工 kill`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
221
282
|
const ok = killGracefullySync(pid);
|
|
222
283
|
if (!ok) {
|
|
223
284
|
return { stopped: false, pid, msg: `failed to signal ${pid}` };
|
|
@@ -238,8 +299,8 @@ export const startCommand = new Command('start')
|
|
|
238
299
|
});
|
|
239
300
|
export const stopCommand = new Command('stop')
|
|
240
301
|
.description('停止本地 server(读 ~/.hunter-mate/server.pid 并 SIGTERM)')
|
|
241
|
-
.action(() => {
|
|
242
|
-
const r = runStop();
|
|
302
|
+
.action(async () => {
|
|
303
|
+
const r = await runStop();
|
|
243
304
|
console.log(r.msg);
|
|
244
305
|
if (!r.stopped && r.pid)
|
|
245
306
|
process.exit(1);
|
package/dist/commands/update.js
CHANGED
|
@@ -105,7 +105,7 @@ export const updateCommand = new Command('update')
|
|
|
105
105
|
}
|
|
106
106
|
else {
|
|
107
107
|
console.log(chalk.cyan(`CLI 自更新: ${installed.cli} → ${latest}`));
|
|
108
|
-
const r = npmSelfUpdate(npmPackage);
|
|
108
|
+
const r = await npmSelfUpdate(npmPackage);
|
|
109
109
|
console.log(r.ok ? chalk.green(r.msg) : chalk.yellow(r.msg));
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -57,5 +57,7 @@
|
|
|
57
57
|
"purge.done.removed": "Removed",
|
|
58
58
|
"purge.done.server": "Server",
|
|
59
59
|
"purge.done.next": "To re-initialize, run",
|
|
60
|
-
"purge.done.nothing": "Nothing to purge"
|
|
60
|
+
"purge.done.nothing": "Nothing to purge",
|
|
61
|
+
"login.serverOfflineSaasFallback": "Warning: local server is not running; logged in via SaaS directly. Start the server and run login again for local data operations.",
|
|
62
|
+
"login.serverOfflineReloginHint": "Note: the saved SaaS token will be rejected (401) by local server protected routes."
|
|
61
63
|
}
|
|
@@ -57,5 +57,7 @@
|
|
|
57
57
|
"purge.done.removed": "清理数据量",
|
|
58
58
|
"purge.done.server": "Server 状态",
|
|
59
59
|
"purge.done.next": "如需重新初始化,请运行",
|
|
60
|
-
"purge.done.nothing": "无需清理"
|
|
60
|
+
"purge.done.nothing": "无需清理",
|
|
61
|
+
"login.serverOfflineSaasFallback": "警告: 本地 server 未运行,已直连 SaaS 登录;本地数据操作需启动 server 后重新 login。",
|
|
62
|
+
"login.serverOfflineReloginHint": "提示: 本次保存的是 SaaS 令牌,本地 server 的受保护路由将返回 401。"
|
|
61
63
|
}
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,25 @@ import { i18n } from './i18n/index.js';
|
|
|
26
26
|
import { initCliLogger, flushCliLogger, getCliLogger } from './logger.js';
|
|
27
27
|
import packageJson from '../package.json' with { type: 'json' };
|
|
28
28
|
const CLI_VERSION = packageJson.version;
|
|
29
|
+
// 未捕获异常兜底(fix #27):任何命令抛出的未处理 Promise 统一友好提示 + exit 1,
|
|
30
|
+
// 不再裸奔 stack 或以 exit code 0 静默结束。
|
|
31
|
+
process.on('unhandledRejection', (reason) => {
|
|
32
|
+
const status = reason?.response?.status;
|
|
33
|
+
const message = reason?.response?.data?.message || reason?.response?.data?.error || reason?.message || String(reason);
|
|
34
|
+
if (status === 401 || status === 403) {
|
|
35
|
+
console.error('登录已过期或无权访问,请运行 hunter-mate login 后重试');
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.error(`命令执行出错: ${message}`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
getCliLogger().error('cli:unhandled-rejection', { status, error: message });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// logger 未就绪时忽略
|
|
45
|
+
}
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
|
29
48
|
initSentry();
|
|
30
49
|
// patchConsole + patchStreams:把业务代码里的 console.error/warn 统一转发到 logger-kit,
|
|
31
50
|
// 避免两套日志写法。AGENTS.md §11。
|
|
@@ -55,16 +74,33 @@ program
|
|
|
55
74
|
// 2) server 在跑时对比磁盘版本 vs 运行版本,插件不一致 → reload,server 不一致 → 重启。
|
|
56
75
|
// HUNTER_MATE_NO_AUTOUPDATE=1 可禁用;update/start/stop 自行处理避免递归;
|
|
57
76
|
// server 未运行时一致性零成本跳过(本地 pid 探测,60s 节流)。
|
|
77
|
+
// doctor 跳过(fix #13): doctor 是只读自检, 不应触发 extension reload 广播,
|
|
78
|
+
// 也不应让 auto-doctor 以 repair 模式写数据(repair-linkedin-dupes 同属此类)。
|
|
58
79
|
if (process.env.HUNTER_MATE_NO_AUTOUPDATE === '1')
|
|
59
80
|
return;
|
|
60
81
|
const name = thisCommand.name();
|
|
61
82
|
if (name === 'update' || name === 'start' || name === 'stop')
|
|
62
83
|
return;
|
|
84
|
+
if (name === 'doctor' || name === 'repair-linkedin-dupes')
|
|
85
|
+
return;
|
|
86
|
+
// fix #30: 命令以 --json(或 --format/-f json)输出时, [auto-update] 提示不得混入 stdout 的 JSON
|
|
87
|
+
const argv = process.argv;
|
|
88
|
+
const jsonMode = argv.includes('--json') ||
|
|
89
|
+
argv.includes('--format=json') ||
|
|
90
|
+
argv.includes('-f=json') ||
|
|
91
|
+
argv.some((a, i) => (a === '--format' || a === '-f') && argv[i + 1] === 'json');
|
|
92
|
+
const notify = (line) => {
|
|
93
|
+
const text = chalk.gray(` [auto-update] ${line}`);
|
|
94
|
+
if (jsonMode)
|
|
95
|
+
console.error(text);
|
|
96
|
+
else
|
|
97
|
+
console.log(text);
|
|
98
|
+
};
|
|
63
99
|
try {
|
|
64
100
|
const { maybeCliSelfUpdate } = await import('./updater.js');
|
|
65
101
|
const upd = await maybeCliSelfUpdate();
|
|
66
102
|
for (const line of upd.logs)
|
|
67
|
-
|
|
103
|
+
notify(line);
|
|
68
104
|
}
|
|
69
105
|
catch {
|
|
70
106
|
/* CLI 自更新检测永不阻塞主命令 */
|
|
@@ -74,7 +110,7 @@ program
|
|
|
74
110
|
const report = await ensureRuntimeConsistency({});
|
|
75
111
|
if (report.performed.length) {
|
|
76
112
|
for (const line of report.performed)
|
|
77
|
-
|
|
113
|
+
notify(line);
|
|
78
114
|
}
|
|
79
115
|
try {
|
|
80
116
|
const { runAutoDoctor } = await import('./auto-doctor.js');
|
package/dist/updater.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface NpmSelfUpdateResult {
|
|
|
13
13
|
ok: boolean;
|
|
14
14
|
msg: string;
|
|
15
15
|
}
|
|
16
|
-
export declare function npmSelfUpdate(pkg?: string): NpmSelfUpdateResult
|
|
16
|
+
export declare function npmSelfUpdate(pkg?: string): Promise<NpmSelfUpdateResult>;
|
|
17
17
|
export interface CliUpdateOutcome {
|
|
18
18
|
logs: string[];
|
|
19
19
|
devSkipped: boolean;
|
package/dist/updater.js
CHANGED
|
@@ -52,17 +52,34 @@ export const npmLatestVersion = kitUpdater.npmLatestVersion;
|
|
|
52
52
|
export const updateCheckIntervalMs = kitUpdater.updateCheckIntervalMs;
|
|
53
53
|
export const shouldCheckUpdate = kitUpdater.shouldCheckUpdate;
|
|
54
54
|
export const markUpdateChecked = kitUpdater.markUpdateChecked;
|
|
55
|
-
export function npmSelfUpdate(pkg = 'hunter-mate') {
|
|
55
|
+
export async function npmSelfUpdate(pkg = 'hunter-mate') {
|
|
56
56
|
const prefix = CLI_PREFIX();
|
|
57
57
|
fs.mkdirSync(prefix, { recursive: true });
|
|
58
58
|
// 优先使用用户通过环境变量指定的 npm 路径(沙盒内可能只有局部 npm)
|
|
59
59
|
const npmBin = process.env.HUNTER_MATE_NPM_BIN || 'npm';
|
|
60
60
|
const args = ['install', `${pkg}@latest`, '--prefix', prefix];
|
|
61
|
+
// 死代理处理:npm 会继承 HTTP(S)_PROXY;指向死端口时 registry 查询必失败
|
|
62
|
+
// (现场真凶:update 查 registry 失败 + prebuild 下载失败回退 7 分钟源码编译)。
|
|
63
|
+
// 检测后从子进程环境剔除死代理变量,让 npm 直连,并在结果里告知用户。
|
|
64
|
+
const childEnv = { ...process.env };
|
|
65
|
+
let proxyNote = '';
|
|
66
|
+
try {
|
|
67
|
+
const { findDeadProxies } = await import('./utils/proxy-detect.js');
|
|
68
|
+
const dead = await findDeadProxies(800);
|
|
69
|
+
if (dead.length) {
|
|
70
|
+
for (const d of dead)
|
|
71
|
+
delete childEnv[d.varName];
|
|
72
|
+
proxyNote =
|
|
73
|
+
`⚠ 检测到死代理(${dead.map((d) => `${d.varName}=${d.value}`).join('; ')}),` +
|
|
74
|
+
`已在本轮 npm install 中临时剔除使其直连;清除方法见 hunter-mate doctor 的 proxy_env 检查项\n`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch { /* 检测失败不阻断更新 */ }
|
|
61
78
|
const r = spawnSync(npmBin, args, {
|
|
62
79
|
stdio: 'inherit',
|
|
63
80
|
timeout: 120_000,
|
|
64
81
|
env: {
|
|
65
|
-
...
|
|
82
|
+
...childEnv,
|
|
66
83
|
// 防止 npm 因为用户 home 不可写而失败;--prefix 已指定安装位置
|
|
67
84
|
npm_config_cache: path.join(prefix, '.npm-cache'),
|
|
68
85
|
},
|
|
@@ -70,12 +87,12 @@ export function npmSelfUpdate(pkg = 'hunter-mate') {
|
|
|
70
87
|
if (r.status === 0) {
|
|
71
88
|
return {
|
|
72
89
|
ok: true,
|
|
73
|
-
msg: `${npmBin} install ${pkg}@latest --prefix ${prefix} 完成,下一条命令起使用新版本`,
|
|
90
|
+
msg: proxyNote + `${npmBin} install ${pkg}@latest --prefix ${prefix} 完成,下一条命令起使用新版本`,
|
|
74
91
|
};
|
|
75
92
|
}
|
|
76
93
|
return {
|
|
77
94
|
ok: false,
|
|
78
|
-
msg: `${npmBin} install ${pkg}@latest --prefix ${prefix} 失败 (exit=${r.status}),可手动执行`,
|
|
95
|
+
msg: proxyNote + `${npmBin} install ${pkg}@latest --prefix ${prefix} 失败 (exit=${r.status}),可手动执行`,
|
|
79
96
|
};
|
|
80
97
|
}
|
|
81
98
|
/**
|
|
@@ -107,7 +124,7 @@ export async function maybeCliSelfUpdate() {
|
|
|
107
124
|
if (latest === installed)
|
|
108
125
|
return outcome;
|
|
109
126
|
logs.push(`CLI 自更新: ${installed} → ${latest}`);
|
|
110
|
-
const r = npmSelfUpdate();
|
|
127
|
+
const r = await npmSelfUpdate();
|
|
111
128
|
logs.push(r.msg);
|
|
112
129
|
outcome.updated = r.ok;
|
|
113
130
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令错误统一出口(对应 fix #18/#28):
|
|
3
|
+
* - logger 留痕(后台可检索,与现有 getCliLogger().error 一致)
|
|
4
|
+
* - console.error 落终端(用户能直接看懂,不再静默 exit 1)
|
|
5
|
+
* - 401/403 时给出重新登录的中文提示
|
|
6
|
+
*
|
|
7
|
+
* 调用方随后自行 process.exit(1)。
|
|
8
|
+
*/
|
|
9
|
+
export declare function printCommandError(scope: string, err: unknown): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getCliLogger } from '../logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* 命令错误统一出口(对应 fix #18/#28):
|
|
4
|
+
* - logger 留痕(后台可检索,与现有 getCliLogger().error 一致)
|
|
5
|
+
* - console.error 落终端(用户能直接看懂,不再静默 exit 1)
|
|
6
|
+
* - 401/403 时给出重新登录的中文提示
|
|
7
|
+
*
|
|
8
|
+
* 调用方随后自行 process.exit(1)。
|
|
9
|
+
*/
|
|
10
|
+
export function printCommandError(scope, err) {
|
|
11
|
+
const e = err;
|
|
12
|
+
const status = e?.response?.status;
|
|
13
|
+
const body = e?.response?.data;
|
|
14
|
+
const bodyText = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : '';
|
|
15
|
+
const message = e?.response?.data?.message || e?.response?.data?.error || e?.message || String(err);
|
|
16
|
+
try {
|
|
17
|
+
getCliLogger().error(scope, { status, error: message });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// logger 未就绪时忽略
|
|
21
|
+
}
|
|
22
|
+
if (status === 401 || status === 403) {
|
|
23
|
+
console.error(`✗ ${message} (HTTP ${status})`);
|
|
24
|
+
console.error('登录已过期或无权访问,请运行 hunter-mate login 后重试');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.error(`✗ ${scope}: ${message}${status ? ` (HTTP ${status})` : ''}`);
|
|
28
|
+
if (bodyText && bodyText !== message) {
|
|
29
|
+
console.error(` 响应: ${bodyText}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ProxyEnvEntry {
|
|
2
|
+
varName: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
export interface DeadProxyFinding {
|
|
6
|
+
varName: string;
|
|
7
|
+
value: string;
|
|
8
|
+
host: string;
|
|
9
|
+
port: number;
|
|
10
|
+
reason: 'refused' | 'timeout' | 'unreachable';
|
|
11
|
+
}
|
|
12
|
+
/** 收集四个代理环境变量中已设置的项 */
|
|
13
|
+
export declare function collectProxyEnv(): ProxyEnvEntry[];
|
|
14
|
+
/**
|
|
15
|
+
* 找出指向 loopback 但端口不通的死代理。
|
|
16
|
+
* 远程代理(非 loopback)不探测,直接视为有效(由网络栈自己处理)。
|
|
17
|
+
*/
|
|
18
|
+
export declare function findDeadProxies(timeoutMs?: number): Promise<DeadProxyFinding[]>;
|
|
19
|
+
/** 生成各 shell 的清除指令提示(用户直接复制执行) */
|
|
20
|
+
export declare function buildUnsetHint(findings: DeadProxyFinding[]): string;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 死代理检测 — Windows/终端现场高频坑。
|
|
3
|
+
*
|
|
4
|
+
* 背景:用户的 shell 环境变量里可能残留指向已关闭本地代理的
|
|
5
|
+
* HTTP_PROXY / HTTPS_PROXY / http_proxy / https_proxy(如 http://127.0.0.1:1962)。
|
|
6
|
+
* 后果链(2026-09-02 现场):
|
|
7
|
+
* npm registry 查询失败 → CLI 自更新误判"已是最新"或报错;
|
|
8
|
+
* better-sqlite3 prebuild 下载失败 → 回退 node-gyp 源码编译 7 分钟+。
|
|
9
|
+
* 本模块只检测 loopback 代理(远程代理探测需要出网,不属于"死代理"语义),
|
|
10
|
+
* 且永不 throw —— 检测结果用于告警,不影响主流程。
|
|
11
|
+
*/
|
|
12
|
+
import net from 'node:net';
|
|
13
|
+
const PROXY_VARS = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'];
|
|
14
|
+
/** 收集四个代理环境变量中已设置的项 */
|
|
15
|
+
export function collectProxyEnv() {
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const varName of PROXY_VARS) {
|
|
18
|
+
const value = process.env[varName];
|
|
19
|
+
if (value)
|
|
20
|
+
out.push({ varName, value });
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** 解析代理值为 {host, port}(仅处理显式端口的 http(s) 代理) */
|
|
25
|
+
function parseProxyUrl(value) {
|
|
26
|
+
try {
|
|
27
|
+
const u = new URL(value);
|
|
28
|
+
if ((u.protocol !== 'http:' && u.protocol !== 'https:') || !u.port)
|
|
29
|
+
return null;
|
|
30
|
+
return { host: u.hostname, port: Number(u.port) };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function isLoopback(host) {
|
|
37
|
+
return host === '127.0.0.1' || host === 'localhost' || host === '::1' || host === '[::1]';
|
|
38
|
+
}
|
|
39
|
+
function probePort(host, port, timeoutMs) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const socket = new net.Socket();
|
|
42
|
+
let settled = false;
|
|
43
|
+
const done = (r) => {
|
|
44
|
+
if (settled)
|
|
45
|
+
return;
|
|
46
|
+
settled = true;
|
|
47
|
+
socket.destroy();
|
|
48
|
+
resolve(r);
|
|
49
|
+
};
|
|
50
|
+
socket.setTimeout(timeoutMs);
|
|
51
|
+
socket.once('connect', () => done('open'));
|
|
52
|
+
socket.once('timeout', () => done('timeout'));
|
|
53
|
+
socket.once('error', (err) => done(err.code === 'ECONNREFUSED' ? 'refused' : 'unreachable'));
|
|
54
|
+
try {
|
|
55
|
+
socket.connect(port, host);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
done('unreachable');
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 找出指向 loopback 但端口不通的死代理。
|
|
64
|
+
* 远程代理(非 loopback)不探测,直接视为有效(由网络栈自己处理)。
|
|
65
|
+
*/
|
|
66
|
+
export async function findDeadProxies(timeoutMs = 800) {
|
|
67
|
+
const findings = [];
|
|
68
|
+
for (const { varName, value } of collectProxyEnv()) {
|
|
69
|
+
const parsed = parseProxyUrl(value);
|
|
70
|
+
if (!parsed || !isLoopback(parsed.host))
|
|
71
|
+
continue;
|
|
72
|
+
const result = await probePort(parsed.host, parsed.port, timeoutMs);
|
|
73
|
+
if (result !== 'open') {
|
|
74
|
+
findings.push({
|
|
75
|
+
varName,
|
|
76
|
+
value,
|
|
77
|
+
host: parsed.host,
|
|
78
|
+
port: parsed.port,
|
|
79
|
+
reason: result === 'refused' ? 'refused' : result === 'timeout' ? 'timeout' : 'unreachable',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return findings;
|
|
84
|
+
}
|
|
85
|
+
/** 生成各 shell 的清除指令提示(用户直接复制执行) */
|
|
86
|
+
export function buildUnsetHint(findings) {
|
|
87
|
+
const vars = [...new Set(findings.map((f) => f.varName))].join(', ');
|
|
88
|
+
return [
|
|
89
|
+
`死代理告警: ${findings.map((f) => `${f.varName}=${f.value} (${f.reason === 'refused' ? '连接被拒绝' : f.reason === 'timeout' ? '连接超时' : '不可达'})`).join('; ')}`,
|
|
90
|
+
`该代理端口已不可用,会导致 npm registry 查询失败 / prebuild 下载失败回退源码编译。清除(当前会话):`,
|
|
91
|
+
` PowerShell: Remove-Item Env:${vars.replace(/, /g, ', Env:')}`,
|
|
92
|
+
` CMD: set ${findings.map((f) => f.varName).join('= && set ')}=`,
|
|
93
|
+
` bash/zsh: unset ${findings.map((f) => f.varName).join(' ')}`,
|
|
94
|
+
` npm 专用: npm config delete proxy && npm config delete https-proxy`,
|
|
95
|
+
].join('\n');
|
|
96
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function resolveScriptPath(p: string | undefined): string | undefined;
|
|
2
|
+
/** 可测形式:传入 argv[1] 判定入口(单测用,业务代码请用 isMainModule) */
|
|
3
|
+
export declare function isInvokedAsEntry(metaUrl: string, argv1: string | undefined): boolean;
|
|
4
|
+
/** 当前模块是否作为 node 独立入口执行(import 时不触发);argv 读取只应发生在本模块 */
|
|
5
|
+
export declare function isMainModule(metaUrl: string): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// utils/script-path.ts — 跨平台入口脚本路径解析(Windows .cmd shim 兼容)
|
|
2
|
+
// Windows 下 node 经 .cmd shim 调用时 process.argv[1] 可能是 shim 路径/8.3 短路径,
|
|
3
|
+
// 必须 realpath 后再与 import.meta.url 比对(AGENTS.md 平台兼容红线)
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
export function resolveScriptPath(p) {
|
|
8
|
+
if (!p)
|
|
9
|
+
return undefined;
|
|
10
|
+
try {
|
|
11
|
+
return fs.realpathSync(p);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return path.resolve(p);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** 可测形式:传入 argv[1] 判定入口(单测用,业务代码请用 isMainModule) */
|
|
18
|
+
export function isInvokedAsEntry(metaUrl, argv1) {
|
|
19
|
+
const entryPath = resolveScriptPath(argv1);
|
|
20
|
+
if (!entryPath)
|
|
21
|
+
return false;
|
|
22
|
+
return entryPath === resolveScriptPath(fileURLToPath(metaUrl));
|
|
23
|
+
}
|
|
24
|
+
/** 当前模块是否作为 node 独立入口执行(import 时不触发);argv 读取只应发生在本模块 */
|
|
25
|
+
export function isMainModule(metaUrl) {
|
|
26
|
+
return isInvokedAsEntry(metaUrl, process.argv[1]);
|
|
27
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -301,7 +301,7 @@ export async function ensureRuntimeConsistency(opts = {}) {
|
|
|
301
301
|
// 2) server 重启(stop + start)
|
|
302
302
|
if (actions.serverRestart) {
|
|
303
303
|
try {
|
|
304
|
-
runStop();
|
|
304
|
+
await runStop();
|
|
305
305
|
const r = await runStart({});
|
|
306
306
|
performed.push(r.started
|
|
307
307
|
? `server restarted (${actions.serverReason}) — new pid=${r.pid}`
|
package/dist/watchdog.d.ts
CHANGED
|
@@ -1 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* server 活性探针(FID-057 #17-A3):子进程不退但假活(事件循环被拖死)时,
|
|
3
|
+
* exit 监听永远等不到触发。探针定时 GET /health,连续失败达到阈值判定假活,
|
|
4
|
+
* 由 watchdog kill 子进程走正常重启路径。
|
|
5
|
+
*/
|
|
6
|
+
export declare class LivenessProbe {
|
|
7
|
+
private readonly opts;
|
|
8
|
+
private failCount;
|
|
9
|
+
private timer;
|
|
10
|
+
constructor(opts: {
|
|
11
|
+
healthUrl: string;
|
|
12
|
+
intervalMs: number;
|
|
13
|
+
threshold: number;
|
|
14
|
+
onFailure: (lastError: string) => void;
|
|
15
|
+
/** 可注入的探测实现(测试用),默认 HTTP GET /health */
|
|
16
|
+
check?: (url: string) => Promise<void>;
|
|
17
|
+
});
|
|
18
|
+
private doCheck;
|
|
19
|
+
/** 立即探测一次并更新失败计数;达到阈值时回调 onFailure 并清零 */
|
|
20
|
+
checkNow(): Promise<void>;
|
|
21
|
+
start(): void;
|
|
22
|
+
stop(): void;
|
|
23
|
+
}
|