diff --git a/src/admin/routes.ts b/src/admin/routes.ts index 6de57a0..fbf93f2 100644 --- a/src/admin/routes.ts +++ b/src/admin/routes.ts @@ -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 ? ''+escapeHtml(log.error)+'

' : ''; + const copyId = 'copy-'+id; - // 渲染 prompt messages:system 蓝色背景,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 += '
'+ + ''+ + ''+ + '
'; + // prompt if (log.messages && log.messages.length > 0) { html += '

'+(log.messages.filter(m=>m.role==='system').length>0?'🧠':'')+' Prompt

'+renderMessages(log.messages)+'
'; } - // 参数 + 请求/响应 + // 参数 + 耗时 html += '
'; if (paramsStr) { html += '

⚙️ 调用参数

'+escapeHtml(paramsStr)+'
'; @@ -180,6 +208,20 @@ function escapeHtml(s) { return String(s).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(); }); async function init() { diff --git a/src/routes/ai.ts b/src/routes/ai.ts index 28d5272..5ca68b9 100644 --- a/src/routes/ai.ts +++ b/src/routes/ai.ts @@ -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 { const config = loadConfig(); @@ -36,147 +15,6 @@ export async function aiRoutes(app: FastifyInstance): Promise { 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 } }>("/ai/tasks", async (request, reply) => { const { type, params } = request.body ?? {};