cleanup(lpt-ai):移除废弃的同步 API + 管理面板增加复制按钮

routes/ai.ts:
- 移除 /ai/aggregate-report、/ai/generate-mind-map、/ai/compare-recall
  三个同步端点,仅保留 /health + 异步任务路由
- 清除不再需要的 import(chat、prompts、LlmError、logStore)

admin/routes.ts:
- 日志详情增加「复制到剪贴板」按钮,一键复制完整请求/响应/
  prompt/参数文本,方便粘贴到外部工具调试
This commit is contained in:
2026-07-04 17:31:50 +08:00
parent 438045b57a
commit decebd143c
2 changed files with 47 additions and 167 deletions
+45 -3
View File
@@ -134,8 +134,9 @@ async function toggleDetail(id, el) {
const respStr = typeof log.responseBody === 'string' ? log.responseBody : JSON.stringify(log.responseBody ?? {}, null, 2);
const paramsStr = log.params ? JSON.stringify(log.params, null, 2) : null;
const errorHtml = log.error ? '<span style="color:#c62828">'+escapeHtml(log.error)+'</span><br><br>' : '';
const copyId = 'copy-'+id;
// 渲染 prompt messagessystem 蓝色背景,user 灰色
// 渲染 prompt messages
const renderMessages = (msgs) => {
if (!msgs || msgs.length === 0) return '';
return msgs.map((m,i) => {
@@ -149,12 +150,39 @@ async function toggleDetail(id, el) {
}).join('');
};
// 构建复制的纯文本
const buildCopyText = () => {
let txt = '=== lpt-ai 请求日志 ===\n';
txt += '端点: '+(log.endpoint||'')+'\n';
txt += '模型: '+(log.model||'')+'\n';
txt += '耗时: '+log.durationMs+'ms\n';
txt += '状态: '+log.status+'\n';
if (paramsStr) txt += '参数: '+paramsStr+'\n';
if (log.messages) {
log.messages.forEach((m,i) => {
txt += '\n--- '+(m.role==='system'?'System Prompt':'User Message')+' ---\n';
txt += m.content;
});
}
txt += '\n\n--- 原始请求体 ---\n';
txt += reqStr;
txt += '\n\n--- 响应体 ---\n';
if (log.error) txt += '错误: '+log.error+'\n';
txt += respStr;
return txt;
};
let html = '';
// prompt 区域(如果有)
// 复制按钮
html += '<div style="display:flex;gap:6px;margin-bottom:10px">'+
'<button class="copy-btn" onclick="copyLog(\''+copyId+'\',this)">📋 复制到剪贴板</button>'+
'<textarea id="'+copyId+'" style="position:absolute;left:-9999px">'+escapeHtml(buildCopyText())+'</textarea>'+
'</div>';
// prompt
if (log.messages && log.messages.length > 0) {
html += '<div style="margin-bottom:12px"><h4>'+(log.messages.filter(m=>m.role==='system').length>0?'🧠':'')+' Prompt</h4>'+renderMessages(log.messages)+'</div>';
}
// 参数 + 请求/响应
// 参数 + 耗时
html += '<div class="side" style="margin-bottom:12px">';
if (paramsStr) {
html += '<div><h4>⚙️ 调用参数</h4><pre style="background:#fff3e0;border-color:#ffe0b2;font-size:12px">'+escapeHtml(paramsStr)+'</pre></div>';
@@ -180,6 +208,20 @@ function escapeHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function copyLog(id, btn) {
const ta = document.getElementById(id);
if (!ta) return;
ta.style.position = 'fixed';
ta.style.left = '0';
ta.style.top = '0';
ta.style.width = '1px';
ta.style.height = '1px';
ta.select();
try { document.execCommand('copy'); btn.textContent = '✅ 已复制'; setTimeout(()=>{btn.textContent='📋 复制到剪贴板';},2000); } catch(e) {}
ta.style.position = 'absolute';
ta.style.left = '-9999px';
}
$('#filterEndpoint').addEventListener('change', () => { loadedDetails = {}; loadLogs(); });
async function init() {
+2 -164
View File
@@ -1,32 +1,11 @@
/**
* AI 能力路由
* AI 能力路由(仅异步任务模式,旧同步端点已移除)
*/
import type { FastifyInstance } from "fastify";
import { chat, isAvailable, loadConfig, LlmError } from "../llm/client.js";
import { aggregateReportPrompt, compareRecallPrompt, generateMindMapPrompt, type FragmentInput } from "../llm/prompts.js";
import { logStore } from "../admin/store.js";
import { isAvailable, loadConfig } from "../llm/client.js";
import { createTask, getTask, type TaskType } from "../task-queue.js";
interface AggregateReportBody {
taskName: string;
fragments: FragmentInput[];
expectation?: string;
}
interface GenerateMindMapBody {
taskName: string;
taskDescription?: string;
reports: string[];
fragments: string[];
}
interface CompareRecallBody {
taskName: string;
standardOutline: string;
recallOutline: string;
}
export async function aiRoutes(app: FastifyInstance): Promise<void> {
const config = loadConfig();
@@ -36,147 +15,6 @@ export async function aiRoutes(app: FastifyInstance): Promise<void> {
model: config.model,
}));
/**
* 残片聚合为学习报告
*/
app.post<{ Body: AggregateReportBody }>("/ai/aggregate-report", async (request, reply) => {
const { taskName, fragments, expectation } = request.body ?? ({} as AggregateReportBody);
const t0 = Date.now();
const params = { temperature: 0.3, max_tokens: 2048 };
if (!taskName || !Array.isArray(fragments) || fragments.length === 0) {
logStore.add({
timestamp: new Date().toISOString(), endpoint: "/ai/aggregate-report",
model: config.model, durationMs: Date.now() - t0, status: 400,
requestBody: request.body ?? {}, params,
responseBody: { error: "taskName 与 fragments 不可为空" },
});
return reply.status(400).send({ error: "taskName 与 fragments 不可为空" });
}
try {
const messages = aggregateReportPrompt(taskName, fragments, expectation);
const report = await chat(config, messages);
const resp = { report: report.trim(), model: config.model };
logStore.add({
timestamp: new Date().toISOString(), endpoint: "/ai/aggregate-report",
model: config.model, durationMs: Date.now() - t0, status: 200,
requestBody: request.body ?? {}, messages, params, responseBody: resp,
});
return resp;
} catch (err) {
const status = err instanceof LlmError ? err.statusCode : 500;
const errMsg = err instanceof Error ? err.message : String(err);
logStore.add({
timestamp: new Date().toISOString(), endpoint: "/ai/aggregate-report",
model: config.model, durationMs: Date.now() - t0, status,
requestBody: request.body ?? {}, params, responseBody: {}, error: errMsg,
});
if (err instanceof LlmError) {
return reply.status(err.statusCode).send({ error: err.message });
}
throw err;
}
});
/**
* 从学习数据生成思维导图大纲(缩进文本,与后端 MindMapTreeTool 格式一致)
*/
app.post<{ Body: GenerateMindMapBody }>("/ai/generate-mind-map", async (request, reply) => {
const { taskName, taskDescription, reports, fragments } =
request.body ?? ({} as GenerateMindMapBody);
const t0 = Date.now();
const params = { temperature: 0.3, max_tokens: 4096 };
if (!taskName || ((reports?.length ?? 0) === 0 && (fragments?.length ?? 0) === 0)) {
logStore.add({
timestamp: new Date().toISOString(), endpoint: "/ai/generate-mind-map",
model: config.model, durationMs: Date.now() - t0, status: 400,
requestBody: request.body ?? {}, params,
responseBody: { error: "taskName 不可为空且 reports/fragments 至少一项非空" },
});
return reply.status(400).send({ error: "taskName 不可为空且 reports/fragments 至少一项非空" });
}
try {
const messages = generateMindMapPrompt(
taskName,
taskDescription ?? "",
reports ?? [],
fragments ?? [],
);
const outline = await chat(config, messages, { maxTokens: 4096 });
const resp = { outline: outline.trim(), model: config.model };
logStore.add({
timestamp: new Date().toISOString(), endpoint: "/ai/generate-mind-map",
model: config.model, durationMs: Date.now() - t0, status: 200,
requestBody: request.body ?? {}, messages, params, responseBody: resp,
});
return resp;
} catch (err) {
const status = err instanceof LlmError ? err.statusCode : 500;
const errMsg = err instanceof Error ? err.message : String(err);
logStore.add({
timestamp: new Date().toISOString(), endpoint: "/ai/generate-mind-map",
model: config.model, durationMs: Date.now() - t0, status,
requestBody: request.body ?? {}, params, responseBody: {}, error: errMsg,
});
if (err instanceof LlmError) {
return reply.status(err.statusCode).send({ error: err.message });
}
throw err;
}
});
/**
* 回忆对比:将用户凭记忆写的导图大纲与标准导图语义对比。
*/
app.post<{ Body: CompareRecallBody }>("/ai/compare-recall", async (request, reply) => {
const { taskName, standardOutline, recallOutline } = request.body ?? ({} as CompareRecallBody);
const t0 = Date.now();
const params = { temperature: 0.2, max_tokens: 4096 };
if (!taskName || !standardOutline?.trim() || !recallOutline?.trim()) {
logStore.add({
timestamp: new Date().toISOString(), endpoint: "/ai/compare-recall",
model: config.model, durationMs: Date.now() - t0, status: 400,
requestBody: request.body ?? {}, params,
responseBody: { error: "taskName、standardOutline、recallOutline 均不可为空" },
});
return reply.status(400).send({ error: "taskName、standardOutline、recallOutline 均不可为空" });
}
try {
const messages = compareRecallPrompt(taskName, standardOutline, recallOutline);
const raw = await chat(config, messages, { temperature: 0.2, maxTokens: 4096 });
// LLM 可能把 JSON 包在 ```json ... ``` 里,做一次清理
const clean = raw.trim().replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
const result = JSON.parse(clean);
logStore.add({
timestamp: new Date().toISOString(), endpoint: "/ai/compare-recall",
model: config.model, durationMs: Date.now() - t0, status: 200,
requestBody: request.body ?? {}, messages, params,
responseBody: result,
});
return result;
} catch (err) {
const status = err instanceof LlmError ? err.statusCode
: err instanceof SyntaxError ? 502 : 500;
const errMsg = err instanceof Error ? err.message : String(err);
logStore.add({
timestamp: new Date().toISOString(), endpoint: "/ai/compare-recall",
model: config.model, durationMs: Date.now() - t0, status,
requestBody: request.body ?? {}, params, responseBody: {}, error: errMsg,
});
if (err instanceof LlmError) {
return reply.status(err.statusCode).send({ error: err.message });
}
throw err;
}
});
// ============ 异步任务(提交 + 轮询)============
/** 提交 AI 任务,立即返回 taskId */
app.post<{ Body: { type: string; params: Record<string, unknown> } }>("/ai/tasks", async (request, reply) => {
const { type, params } = request.body ?? {};