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:
+45
-3
@@ -134,8 +134,9 @@ async function toggleDetail(id, el) {
|
|||||||
const respStr = typeof log.responseBody === 'string' ? log.responseBody : JSON.stringify(log.responseBody ?? {}, null, 2);
|
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 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 errorHtml = log.error ? '<span style="color:#c62828">'+escapeHtml(log.error)+'</span><br><br>' : '';
|
||||||
|
const copyId = 'copy-'+id;
|
||||||
|
|
||||||
// 渲染 prompt messages:system 蓝色背景,user 灰色
|
// 渲染 prompt messages
|
||||||
const renderMessages = (msgs) => {
|
const renderMessages = (msgs) => {
|
||||||
if (!msgs || msgs.length === 0) return '';
|
if (!msgs || msgs.length === 0) return '';
|
||||||
return msgs.map((m,i) => {
|
return msgs.map((m,i) => {
|
||||||
@@ -149,12 +150,39 @@ async function toggleDetail(id, el) {
|
|||||||
}).join('');
|
}).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 = '';
|
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) {
|
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 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">';
|
html += '<div class="side" style="margin-bottom:12px">';
|
||||||
if (paramsStr) {
|
if (paramsStr) {
|
||||||
html += '<div><h4>⚙️ 调用参数</h4><pre style="background:#fff3e0;border-color:#ffe0b2;font-size:12px">'+escapeHtml(paramsStr)+'</pre></div>';
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(); });
|
$('#filterEndpoint').addEventListener('change', () => { loadedDetails = {}; loadLogs(); });
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
|||||||
+2
-164
@@ -1,32 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* AI 能力路由
|
* AI 能力路由(仅异步任务模式,旧同步端点已移除)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { FastifyInstance } from "fastify";
|
import type { FastifyInstance } from "fastify";
|
||||||
import { chat, isAvailable, loadConfig, LlmError } from "../llm/client.js";
|
import { isAvailable, loadConfig } from "../llm/client.js";
|
||||||
import { aggregateReportPrompt, compareRecallPrompt, generateMindMapPrompt, type FragmentInput } from "../llm/prompts.js";
|
|
||||||
import { logStore } from "../admin/store.js";
|
|
||||||
import { createTask, getTask, type TaskType } from "../task-queue.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> {
|
export async function aiRoutes(app: FastifyInstance): Promise<void> {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
@@ -36,147 +15,6 @@ export async function aiRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
model: config.model,
|
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 */
|
/** 提交 AI 任务,立即返回 taskId */
|
||||||
app.post<{ Body: { type: string; params: Record<string, unknown> } }>("/ai/tasks", async (request, reply) => {
|
app.post<{ Body: { type: string; params: Record<string, unknown> } }>("/ai/tasks", async (request, reply) => {
|
||||||
const { type, params } = request.body ?? {};
|
const { type, params } = request.body ?? {};
|
||||||
|
|||||||
Reference in New Issue
Block a user