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