tt-help-cli-ycl 1.4.113 → 1.4.114
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/package.json +1 -1
- package/scripts/monitor-refresh-mini.mjs +161 -0
- package/src/clash/checker.js +18 -4
- package/src/clash/daemon.js +9 -0
- package/src/cli/refresh-mini.js +714 -0
- package/src/cli/refresh.js +28 -12
- package/src/cli/watchdog.js +5 -2
- package/src/lib/api-client.js +57 -42
- package/src/lib/api-interceptor.js +165 -49
- package/src/lib/args.js +86 -0
- package/src/lib/constants.js +22 -0
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/scraper/explore-core.js +24 -9
- package/src/watch/llm-config.js +14 -0
- package/src/watch/server.js +17 -0
- package/src/watchdog/process-manager.js +58 -1
- package/src/watchdog/reporter.js +1 -0
- package/src/watchdog/standalone-server.js +228 -0
package/src/main.js
CHANGED
|
@@ -13,6 +13,7 @@ import { handleVideoStats } from "./cli/videostats.js";
|
|
|
13
13
|
import { handleDbImport } from "./cli/db-import.js";
|
|
14
14
|
import { handleWebserver } from "./cli/webserver.js";
|
|
15
15
|
import { handleRefresh } from "./cli/refresh.js";
|
|
16
|
+
import { handleRefreshMini } from "./cli/refresh-mini.js";
|
|
16
17
|
import { handlePrecheck } from "./cli/precheck.js";
|
|
17
18
|
import {
|
|
18
19
|
handleTag,
|
|
@@ -54,6 +55,8 @@ async function main() {
|
|
|
54
55
|
return handleDbImport(parsed);
|
|
55
56
|
case "refresh":
|
|
56
57
|
return handleRefresh(parsed);
|
|
58
|
+
case "refresh-mini":
|
|
59
|
+
return handleRefreshMini(parsed);
|
|
57
60
|
case "precheck":
|
|
58
61
|
return handlePrecheck(parsed);
|
|
59
62
|
case "tag":
|
package/src/npm-main.js
CHANGED
|
@@ -9,6 +9,7 @@ import { handleConfig, showConfig, showUsage, version } from "./cli/config.js";
|
|
|
9
9
|
import { handleOpen } from "./cli/open.js";
|
|
10
10
|
import { handleComments } from "./cli/comments.js";
|
|
11
11
|
import { handleRefresh } from "./cli/refresh.js";
|
|
12
|
+
import { handleRefreshMini } from "./cli/refresh-mini.js";
|
|
12
13
|
import { handlePrecheck } from "./cli/precheck.js";
|
|
13
14
|
import { handleWebserver } from "./cli/webserver.js";
|
|
14
15
|
import {
|
|
@@ -55,6 +56,8 @@ async function main() {
|
|
|
55
56
|
return handleComments(parsed);
|
|
56
57
|
case "refresh":
|
|
57
58
|
return handleRefresh(parsed);
|
|
59
|
+
case "refresh-mini":
|
|
60
|
+
return handleRefreshMini(parsed);
|
|
58
61
|
case "precheck":
|
|
59
62
|
return handlePrecheck(parsed);
|
|
60
63
|
case "tag":
|
|
@@ -28,6 +28,7 @@ async function processExplore(page, username, options, log) {
|
|
|
28
28
|
proxyServer = null,
|
|
29
29
|
webserverUrl = null,
|
|
30
30
|
noProduct = false,
|
|
31
|
+
mini = false, // 精简模式:仅采集视频数据,跳过用户信息/国家采样/关注粉丝/商品详情
|
|
31
32
|
} = options;
|
|
32
33
|
|
|
33
34
|
const result = {
|
|
@@ -63,13 +64,17 @@ async function processExplore(page, username, options, log) {
|
|
|
63
64
|
log(` 访问 @${username} 主页...`);
|
|
64
65
|
const videoList = await collectVideos(page, username, maxVideos, log);
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
if (!mini) {
|
|
68
|
+
log(" 获取用户信息...");
|
|
69
|
+
const info = await getUserInfo(page);
|
|
70
|
+
if (info) {
|
|
71
|
+
result.userInfo = info;
|
|
72
|
+
log(
|
|
73
|
+
` 用户: ${info.nickname || username} | 粉丝: ${info.followerCount || "-"} | 视频: ${info.videoCount || "-"}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
log(" [mini] 跳过用户信息");
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
const captcha = await detectCaptcha(page);
|
|
@@ -132,8 +137,8 @@ async function processExplore(page, username, options, log) {
|
|
|
132
137
|
};
|
|
133
138
|
|
|
134
139
|
// 如果有商品链接且未禁用,通过 WebService API 抓取商品详情页(评分、销量等)
|
|
135
|
-
// --no-product
|
|
136
|
-
if (topVideo.seoUrl && options.webserverUrl && !noProduct) {
|
|
140
|
+
// --no-product / mini 模式时跳过(由独立商品程序异步补抓,避免阻塞主流程)
|
|
141
|
+
if (topVideo.seoUrl && options.webserverUrl && !noProduct && !mini) {
|
|
137
142
|
try {
|
|
138
143
|
const resp = await fetch(
|
|
139
144
|
`${options.webserverUrl}/api/tiktok/product`,
|
|
@@ -166,6 +171,16 @@ async function processExplore(page, username, options, log) {
|
|
|
166
171
|
return result;
|
|
167
172
|
}
|
|
168
173
|
|
|
174
|
+
// mini 模式:跳过国家采样与关注/粉丝,仅登记视频列表,大幅提速
|
|
175
|
+
if (mini) {
|
|
176
|
+
result.keepFollow = false;
|
|
177
|
+
result.hasFollowData = false;
|
|
178
|
+
result.videoList = videoArray;
|
|
179
|
+
result.processed = true;
|
|
180
|
+
log(` [mini] 仅采集视频数据,跳过国家采样与关注/粉丝`);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
169
184
|
// 国家采样判断
|
|
170
185
|
// explore 模式:采样 5 个,优先命中目标国家,不匹配则回退众数 → 写 locationCreated
|
|
171
186
|
// refresh 模式:采样 7 个,纯众数逻辑 → 写 confirmedLocation(二次确认)
|
package/src/watch/llm-config.js
CHANGED
|
@@ -177,6 +177,20 @@ export function getLlmConfig() {
|
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* 获取完整 LLM 配置(含完整 API Key)— 仅供本机内部服务使用
|
|
182
|
+
* 由 /api/llm-config/raw 端点暴露,该端点仅允许 127.0.0.1 访问
|
|
183
|
+
*/
|
|
184
|
+
export function getLlmConfigRaw() {
|
|
185
|
+
if (!_loaded) loadLlmConfig();
|
|
186
|
+
return {
|
|
187
|
+
provider: _cachedProvider,
|
|
188
|
+
model: _cachedModel,
|
|
189
|
+
url: _cachedUrl,
|
|
190
|
+
apiKey: _cachedApiKey,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
180
194
|
/** 获取当前 LLM URL(内部调用) */
|
|
181
195
|
export function getLlmUrl() {
|
|
182
196
|
if (!_loaded) loadLlmConfig();
|
package/src/watch/server.js
CHANGED
|
@@ -447,6 +447,23 @@ export function startWatchServer(
|
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
+
// ── LLM 完整配置端点(仅本机可访问,供看门狗独立服务动态拉取) ──
|
|
451
|
+
if (req.method === "GET" && routePath === "/api/llm-config/raw") {
|
|
452
|
+
const remote = req.socket.remoteAddress || "";
|
|
453
|
+
const isLocal =
|
|
454
|
+
remote === "127.0.0.1" ||
|
|
455
|
+
remote === "::1" ||
|
|
456
|
+
remote === "::ffff:127.0.0.1";
|
|
457
|
+
if (!isLocal) {
|
|
458
|
+
console.error(`[llm-raw] 拒绝非本机访问: ${remote}`);
|
|
459
|
+
sendJSON(res, 403, { error: "forbidden" });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const { getLlmConfigRaw } = await import("./llm-config.js");
|
|
463
|
+
sendJSON(res, 200, getLlmConfigRaw());
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
450
467
|
if (req.method === "POST" && routePath === "/api/users") {
|
|
451
468
|
try {
|
|
452
469
|
const { usernames, sources, guessedLocation } = await readBody(req);
|
|
@@ -24,6 +24,60 @@ const IS_WIN = process.platform === "win32";
|
|
|
24
24
|
const SPAWN_STAGGER_MS = 30000; // 批量启动时每个进程间隔 30 秒,避免抢网络
|
|
25
25
|
const STALE_LOG_TIMEOUT_MS = 15 * 60 * 1000; // 15 分钟无日志输出视为假死
|
|
26
26
|
|
|
27
|
+
// === 控制台日志格式化(仅显示层增强,不影响任何功能链路) ===
|
|
28
|
+
// 目标:多进程日志交错打印时,通过时间戳 + 固定宽度彩色 [cmdId] 前缀方便人眼分流。
|
|
29
|
+
// 兼容:Windows(Windows Terminal 彩色 / 旧 conhost 自动降级纯文本)与 macOS(原生 ANSI 彩色)。
|
|
30
|
+
|
|
31
|
+
// 每进程固定一种颜色(按 cmdId 哈希取色),新增进程自动分配且不重复
|
|
32
|
+
const PROC_COLORS = [32, 36, 33, 35, 34, 92, 96, 93, 95, 94];
|
|
33
|
+
const ID_PAD_WIDTH = 10; // cmdId 前缀固定宽度,短 ID 补齐保持消息列对齐
|
|
34
|
+
|
|
35
|
+
function getProcColor(id) {
|
|
36
|
+
let sum = 0;
|
|
37
|
+
const s = String(id);
|
|
38
|
+
for (let i = 0; i < s.length; i++) sum = (sum + s.charCodeAt(i)) % 997;
|
|
39
|
+
return PROC_COLORS[sum % PROC_COLORS.length];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function canUseColor() {
|
|
43
|
+
if (!process.stderr.isTTY) return false;
|
|
44
|
+
if (process.env.NO_COLOR) return false;
|
|
45
|
+
if (process.env.FORCE_COLOR === "1" || process.env.FORCE_COLOR === "true")
|
|
46
|
+
return true;
|
|
47
|
+
// macOS 终端恒 ≥4;Windows Terminal ≥4;旧 conhost 未开启 VT 时为 1 → 自动降级纯文本
|
|
48
|
+
const depth =
|
|
49
|
+
typeof process.stderr.getColorDepth === "function"
|
|
50
|
+
? process.stderr.getColorDepth()
|
|
51
|
+
: 1;
|
|
52
|
+
return depth >= 4;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fmtClock(d = new Date()) {
|
|
56
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
57
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 打印子进程日志行(替代裸 console.error)。
|
|
62
|
+
* 只改显示形态,msg 内容一行不少;多行消息逐行加前缀。
|
|
63
|
+
*/
|
|
64
|
+
function printProcessLine(id, msg) {
|
|
65
|
+
const lines = String(msg).split(/\r?\n/);
|
|
66
|
+
const prefixId = `[${String(id).padEnd(ID_PAD_WIDTH)}]`;
|
|
67
|
+
const color = getProcColor(id);
|
|
68
|
+
const useColor = canUseColor();
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const clock = fmtClock();
|
|
71
|
+
if (useColor) {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
`\x1b[90m${clock}\x1b[0m \x1b[${color}m${prefixId}\x1b[0m ${line}\n`,
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
process.stderr.write(`${clock} ${prefixId} ${line}\n`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
27
81
|
// 从命令行中提取 CDP 端口列表(解析 --base-port 和 --port-count)
|
|
28
82
|
function extractCdpPorts(cmdLine) {
|
|
29
83
|
const match = cmdLine.match(/--base-port\s+(\d+)/);
|
|
@@ -219,7 +273,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
219
273
|
const msg = data.toString().trim();
|
|
220
274
|
if (msg) {
|
|
221
275
|
const short = msg.substring(0, 200);
|
|
222
|
-
|
|
276
|
+
// 仅显示层格式化:时间戳 + 彩色固定宽度前缀,内容不变。
|
|
277
|
+
// 前缀优先用命令名(如 explore-F1),未命名时退回 cmdId。
|
|
278
|
+
const displayId = (cmdDef && cmdDef.name) || id;
|
|
279
|
+
printProcessLine(displayId, short);
|
|
223
280
|
// 环形缓冲:保留最近 N 行供 LLM 分析
|
|
224
281
|
const info = this.processes.get(id);
|
|
225
282
|
if (info) {
|
package/src/watchdog/reporter.js
CHANGED
|
@@ -39,6 +39,7 @@ export function getClientConfig() {
|
|
|
39
39
|
const cfg = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
40
40
|
return {
|
|
41
41
|
server: cfg.server || "",
|
|
42
|
+
watchdogServer: cfg.watchdogServer || "",
|
|
42
43
|
proxy: cfg.proxy || "",
|
|
43
44
|
browser: cfg.browser || "",
|
|
44
45
|
userId: cfg.userId || "",
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 看门狗独立服务 — 与 watch 完全隔离的独立进程
|
|
3
|
+
*
|
|
4
|
+
* 复用 src/watch/watchdog-server.js 的全部看门狗逻辑
|
|
5
|
+
* (内存状态 + ~/.tt-help-watchdog.json 持久化),
|
|
6
|
+
* 不接触 watch 的 SQLite 数据库、不依赖 watch 的 http 服务。
|
|
7
|
+
*
|
|
8
|
+
* 用法:
|
|
9
|
+
* node src/watchdog/standalone-server.js [--port 17302]
|
|
10
|
+
* WATCHDOG_PORT=17302 node src/watchdog/standalone-server.js
|
|
11
|
+
*
|
|
12
|
+
* LLM 配置(可选,仅影响 POST /api/watchdog/analyze-logs):
|
|
13
|
+
* 环境变量 LLM_URL / LLM_MODEL / APIKEY,缺省时用 llm-config 内置默认值。
|
|
14
|
+
* 本进程永不打开 SQLite(getDb() 恒为 null),LLM 配置自动走环境变量兜底。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import http from "http";
|
|
18
|
+
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
createReadStream,
|
|
21
|
+
readFileSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "fs";
|
|
24
|
+
import { extname, join, normalize, dirname, sep } from "path";
|
|
25
|
+
import { fileURLToPath } from "url";
|
|
26
|
+
import { homedir } from "os";
|
|
27
|
+
import {
|
|
28
|
+
registerWatchdogRoutes,
|
|
29
|
+
tryHandleWatchdogRoute,
|
|
30
|
+
} from "../watch/watchdog-server.js";
|
|
31
|
+
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const PUBLIC_DIR = join(__dirname, "..", "watch", "public");
|
|
34
|
+
|
|
35
|
+
// ── 端口解析:--port 参数 > WATCHDOG_PORT 环境变量 > 默认 17302 ──
|
|
36
|
+
function resolvePort() {
|
|
37
|
+
const argv = process.argv.slice(2);
|
|
38
|
+
const idx = argv.indexOf("--port");
|
|
39
|
+
if (idx !== -1 && argv[idx + 1]) {
|
|
40
|
+
const p = parseInt(argv[idx + 1], 10);
|
|
41
|
+
if (p > 0 && p < 65536) return p;
|
|
42
|
+
}
|
|
43
|
+
const env = parseInt(process.env.WATCHDOG_PORT, 10);
|
|
44
|
+
if (env > 0 && env < 65536) return env;
|
|
45
|
+
return 17302;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── watch 端口(本机 127.0.0.1):--watch-port 参数 > WATCHDOG_WATCH_PORT 环境变量 > 默认 3011 ──
|
|
49
|
+
function resolveWatchPort() {
|
|
50
|
+
const argv = process.argv.slice(2);
|
|
51
|
+
const idx = argv.indexOf("--watch-port");
|
|
52
|
+
if (idx !== -1 && argv[idx + 1]) {
|
|
53
|
+
const p = parseInt(argv[idx + 1], 10);
|
|
54
|
+
if (p > 0 && p < 65536) return p;
|
|
55
|
+
}
|
|
56
|
+
const env = parseInt(process.env.WATCHDOG_WATCH_PORT, 10);
|
|
57
|
+
if (env > 0 && env < 65536) return env;
|
|
58
|
+
return 3011;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// LLM 配置本地缓存(“以前的”配置,watch 不可达时兜底)
|
|
62
|
+
const LLM_CACHE_PATH = join(homedir(), ".tt-help-watchdog-llm.json");
|
|
63
|
+
const WATCH_PORT = resolveWatchPort();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 动态刷新 LLM 配置:
|
|
67
|
+
* 1. 从 watch 的受限端点 /api/llm-config/raw(仅 127.0.0.1)拉取完整配置
|
|
68
|
+
* 2. 拉取成功 → 注入环境变量 + 更新本地缓存
|
|
69
|
+
* 3. 拉取失败 → 读本地缓存(“以前的”)注入
|
|
70
|
+
* 4. reloadLlmConfig() 强制 llm-config 重读 env(绕开其 _loaded 缓存)
|
|
71
|
+
*/
|
|
72
|
+
async function refreshLlmConfig() {
|
|
73
|
+
let cfg = null;
|
|
74
|
+
try {
|
|
75
|
+
const resp = await fetch(
|
|
76
|
+
`http://127.0.0.1:${WATCH_PORT}/api/llm-config/raw`,
|
|
77
|
+
{ signal: AbortSignal.timeout(3000) },
|
|
78
|
+
);
|
|
79
|
+
if (resp.ok) {
|
|
80
|
+
cfg = await resp.json();
|
|
81
|
+
if (cfg && cfg.apiKey) {
|
|
82
|
+
try {
|
|
83
|
+
writeFileSync(LLM_CACHE_PATH, JSON.stringify(cfg, null, 2), "utf-8");
|
|
84
|
+
} catch {}
|
|
85
|
+
console.error(
|
|
86
|
+
`[watchdog-standalone] LLM 配置已从 watch 刷新并更新本地缓存 (model=${cfg.model})`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
|
|
92
|
+
if (!cfg || !cfg.apiKey) {
|
|
93
|
+
try {
|
|
94
|
+
if (existsSync(LLM_CACHE_PATH)) {
|
|
95
|
+
cfg = JSON.parse(readFileSync(LLM_CACHE_PATH, "utf-8"));
|
|
96
|
+
if (cfg && cfg.apiKey) {
|
|
97
|
+
console.error(
|
|
98
|
+
"[watchdog-standalone] watch LLM 端点不可达,使用本地缓存配置",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (cfg) {
|
|
106
|
+
if (cfg.apiKey) process.env.APIKEY = cfg.apiKey;
|
|
107
|
+
if (cfg.url) process.env.LLM_URL = cfg.url;
|
|
108
|
+
if (cfg.model) process.env.LLM_MODEL = cfg.model;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const { reloadLlmConfig } = await import("../watch/llm-config.js");
|
|
112
|
+
reloadLlmConfig();
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const MIME_TYPES = {
|
|
117
|
+
".html": "text/html; charset=utf-8",
|
|
118
|
+
".js": "application/javascript; charset=utf-8",
|
|
119
|
+
".css": "text/css; charset=utf-8",
|
|
120
|
+
".json": "application/json; charset=utf-8",
|
|
121
|
+
".svg": "image/svg+xml",
|
|
122
|
+
".png": "image/png",
|
|
123
|
+
".ico": "image/x-icon",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function readBody(req) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
let body = "";
|
|
129
|
+
req.on("data", (chunk) => (body += chunk));
|
|
130
|
+
req.on("end", () => {
|
|
131
|
+
try {
|
|
132
|
+
resolve(body ? JSON.parse(body) : {});
|
|
133
|
+
} catch (e) {
|
|
134
|
+
reject(e);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
req.on("error", reject);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function sendJSON(res, code, data) {
|
|
142
|
+
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
|
|
143
|
+
res.end(JSON.stringify(data));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 静态文件(仅限 public 目录内,防路径穿越)
|
|
147
|
+
function serveStatic(res, pathname) {
|
|
148
|
+
let decoded;
|
|
149
|
+
try {
|
|
150
|
+
decoded = decodeURIComponent(pathname);
|
|
151
|
+
} catch {
|
|
152
|
+
sendJSON(res, 400, { error: "bad request" });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const filePath =
|
|
156
|
+
decoded === "/"
|
|
157
|
+
? join(PUBLIC_DIR, "index.html")
|
|
158
|
+
: join(PUBLIC_DIR, decoded);
|
|
159
|
+
const normalized = normalize(filePath);
|
|
160
|
+
const base = normalize(PUBLIC_DIR);
|
|
161
|
+
if (normalized !== base && !normalized.startsWith(base + sep)) {
|
|
162
|
+
sendJSON(res, 403, { error: "forbidden" });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!existsSync(normalized)) {
|
|
166
|
+
sendJSON(res, 404, { error: "not found" });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const ext = extname(normalized).toLowerCase();
|
|
170
|
+
res.writeHead(200, {
|
|
171
|
+
"Content-Type": MIME_TYPES[ext] || "application/octet-stream",
|
|
172
|
+
});
|
|
173
|
+
createReadStream(normalized).pipe(res);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const server = http.createServer(async (req, res) => {
|
|
177
|
+
const url = new URL(req.url, "http://127.0.0.1");
|
|
178
|
+
const routePath = url.pathname;
|
|
179
|
+
const params = Object.fromEntries(url.searchParams.entries());
|
|
180
|
+
|
|
181
|
+
// analyze-logs 前动态刷新 LLM 配置(watch 换 key 无需重启本服务)
|
|
182
|
+
if (req.method === "POST" && routePath === "/api/watchdog/analyze-logs") {
|
|
183
|
+
await refreshLlmConfig();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 看门狗 API:复用 watchdog-server.js 的完整路由
|
|
187
|
+
if (routePath.startsWith("/api/watchdog/")) {
|
|
188
|
+
try {
|
|
189
|
+
let body = {};
|
|
190
|
+
if (req.method === "POST") body = await readBody(req);
|
|
191
|
+
const handled = await tryHandleWatchdogRoute(
|
|
192
|
+
routePath,
|
|
193
|
+
req.method,
|
|
194
|
+
params,
|
|
195
|
+
body,
|
|
196
|
+
res,
|
|
197
|
+
);
|
|
198
|
+
if (handled) return;
|
|
199
|
+
sendJSON(res, 404, { error: "watchdog route not found" });
|
|
200
|
+
return;
|
|
201
|
+
} catch (e) {
|
|
202
|
+
sendJSON(res, 400, { error: e.message });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 其余 GET:serve watch 的前端静态页(看门狗页签同源可用)
|
|
208
|
+
if (req.method === "GET" || req.method === "HEAD") {
|
|
209
|
+
serveStatic(res, routePath);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
sendJSON(res, 404, { error: "not found" });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const port = resolvePort();
|
|
217
|
+
registerWatchdogRoutes(server);
|
|
218
|
+
server.listen(port, "0.0.0.0", () => {
|
|
219
|
+
console.error(
|
|
220
|
+
`[watchdog-standalone] 看门狗独立服务已启动: http://0.0.0.0:${port}`,
|
|
221
|
+
);
|
|
222
|
+
console.error(`[watchdog-standalone] 前端页面: http://<本机IP>:${port}/`);
|
|
223
|
+
console.error(
|
|
224
|
+
`[watchdog-standalone] LLM 配置源: http://127.0.0.1:${WATCH_PORT}/api/llm-config/raw(本地缓存兜底: ${LLM_CACHE_PATH})`,
|
|
225
|
+
);
|
|
226
|
+
// 启动后异步预热 LLM 配置(不阻塞启动,失败静默)
|
|
227
|
+
refreshLlmConfig().catch(() => {});
|
|
228
|
+
});
|