feat(lpt-ai):新增 /fetch-title 网页标题抓取接口

POST /fetch-title { url } → { url, title }
- 服务端 fetch 目标页面,正则提取 <title> 标签
- 10 秒超时,抓取失败时返回 title: null
- 请求日志记录到 logStore
This commit is contained in:
2026-07-04 22:34:07 +08:00
parent decebd143c
commit 200f0881a5
+45
View File
@@ -5,6 +5,7 @@
import type { FastifyInstance } from "fastify";
import { isAvailable, loadConfig } from "../llm/client.js";
import { createTask, getTask, type TaskType } from "../task-queue.js";
import { logStore } from "../admin/store.js";
export async function aiRoutes(app: FastifyInstance): Promise<void> {
const config = loadConfig();
@@ -39,4 +40,48 @@ export async function aiRoutes(app: FastifyInstance): Promise<void> {
}
return task;
});
/** 抓取网页标题 */
app.post<{ Body: { url: string } }>("/fetch-title", async (request, reply) => {
const { url } = request.body ?? {};
const t0 = Date.now();
if (!url || typeof url !== "string" || (!url.startsWith("http://") && !url.startsWith("https://"))) {
return reply.status(400).send({ error: "无效的 URL" });
}
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timer);
const html = await response.text();
const match = html.match(/<title[^>]*>([^<]+)<\/title>/i);
const title = match ? match[1].trim() : null;
logStore.add({
timestamp: new Date().toISOString(),
endpoint: "/fetch-title",
model: "",
durationMs: Date.now() - t0,
status: 200,
requestBody: { url },
responseBody: { url, title },
});
return { url, title };
} catch (err) {
logStore.add({
timestamp: new Date().toISOString(),
endpoint: "/fetch-title",
model: "",
durationMs: Date.now() - t0,
status: 502,
requestBody: { url },
responseBody: { url, title: null },
error: err instanceof Error ? err.message : String(err),
});
return { url, title: null };
}
});
}