tt-help-cli-ycl 1.4.112 → 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/product-fill.js +127 -14
- 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 +112 -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/lib/constants.js
CHANGED
|
@@ -148,6 +148,7 @@ const HELP_TEXT = [
|
|
|
148
148
|
` --location <国家代码> 国家筛选,逗号分隔,默认 ${DEFAULT_TARGET_LOCATIONS_CSV}`,
|
|
149
149
|
" --job-locations <国家> 任务国家筛选,逗号分隔(仅筛选服务端任务),基于真实国家字段 location_created",
|
|
150
150
|
" --max-videos <数量> 每用户最大视频数,默认 16",
|
|
151
|
+
" --mini 精简模式:只采集视频列表及由视频推导的信息(EC视频、最新发布时间),跳过用户信息、国家采样、关注/粉丝与商品详情,大幅提速",
|
|
151
152
|
" --enable-follow 启用关注/粉丝提取(默认启用)",
|
|
152
153
|
" --disable-follow 禁用关注/粉丝提取",
|
|
153
154
|
" --max-following <数量> 最大获取关注数,默认 100",
|
|
@@ -162,6 +163,27 @@ const HELP_TEXT = [
|
|
|
162
163
|
"--proxy <地址> 浏览器代理(如 socks5://127.0.0.1:1080),未指定时使用配置文件 proxy",
|
|
163
164
|
"--webserver <URL> 集中式视频国家查询服务(如 http://127.0.0.1:3000),省去本地 Playwright 浏览器",
|
|
164
165
|
"",
|
|
166
|
+
" tt-help refresh-mini [选项]",
|
|
167
|
+
" 视频信息更新专用客户端:固定 CDP 端口 4222,只做 mini 采集(视频列表+视频推导信息)",
|
|
168
|
+
" 不检测登录、不做多账户轮换;Clash 节点自动轮换(每 10 分钟主动切换)",
|
|
169
|
+
" 验证码 → 等待人工在浏览器中解决(超时兜底切节点)",
|
|
170
|
+
" 被封 / 连续任务错误 ≥ 3 / CDN 限流连续 ≥ 3 → 切换代理节点",
|
|
171
|
+
" 选项:",
|
|
172
|
+
" --server <URL> 服务端地址,默认 http://127.0.0.1:3001",
|
|
173
|
+
` --location <国家代码> 国家筛选,逗号分隔,默认 ${DEFAULT_TARGET_LOCATIONS_CSV}`,
|
|
174
|
+
" --job-locations <国家> 任务国家筛选,逗号分隔(仅筛选服务端任务)",
|
|
175
|
+
" --max-videos <数量> 每用户最大视频数,默认 16",
|
|
176
|
+
" --max-users <数量> 最大处理用户数,默认无限制",
|
|
177
|
+
" -i, --interval <秒数> 无任务时轮询间隔,默认 30 秒",
|
|
178
|
+
" --max-age <秒数> 最小刷新间隔,默认 0(不限)",
|
|
179
|
+
" --port <端口号> 固定 CDP 端口,默认 4222",
|
|
180
|
+
" --user-id <编号> 客户端编号(设备ID),默认自动生成",
|
|
181
|
+
" --rotate-min <分钟> 代理节点轮换周期,默认 10(0=关闭定时轮换)",
|
|
182
|
+
" --captcha-wait-min <分钟> 验证码等待人工解决上限,默认 10",
|
|
183
|
+
" --ensure-daemon 启动时检查 clash-daemon 是否存活,未存活则自动拉起(默认不检测)",
|
|
184
|
+
" --preset <预设> 延迟预设 fast/normal/slow/stealth,默认 normal",
|
|
185
|
+
" --proxy <地址> 浏览器代理(默认走 Clash 本地端口,切换节点才有效)",
|
|
186
|
+
"",
|
|
165
187
|
" tt-help precheck [选项]",
|
|
166
188
|
" 对目标商家用户进行轻量预检,通过 HTTP 获取 videoCount 判断是否有新视频",
|
|
167
189
|
" 有更新 → 标记 pending_refresh=1(等待 refresh 客户端完整采集)",
|
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
|
+
});
|