// prompt-analysis.js —— 图片反推生图提示词(移植自「提示词提取」项目) // LLM 通道走本应用的 router 分组(直接读中转分组列表),与 team.js 同模式。 "use strict"; const router = require("./router"); const { SYSTEM_PROMPT, USER_INSTRUCTION } = require("./team-prompt"); // ============================================================================ // 以下 自 提示词提取/server.js 原样提取(extractJson / callChat),未改动逻辑。 // ============================================================================ function extractJson(text) { if (!text) throw new Error('模型返回为空'); let t = text.trim(); const fence = t.match(/```(?:json)?\s*([\s\S]*?)```/i); if (fence) t = fence[1].trim(); const start = t.indexOf('{'); const end = t.lastIndexOf('}'); if (start !== -1 && end !== -1 && end > start) t = t.slice(start, end + 1); const tryParse = (s) => { try { return JSON.parse(s); } catch (_) { return undefined; } }; let r = tryParse(t); if (r !== undefined) return r; r = tryParse(t.replace(/,\s*([}\]])/g, '$1')); if (r !== undefined) return r; throw new Error('模型输出的 JSON 格式有误,请重试'); } // 流式调用:空闲超时——120 秒收不到任何数据就判定挂起并中止 const CHAT_IDLE_MS = 120000; function chatTimeoutError() { return Object.assign(new Error('模型长时间无响应(可能渠道不稳或模型太慢),请重试或换个更快的模型'), { status: 504 }); } async function callChat(gwBase, apiKey, model, messages, maxTokens) { const ctrl = new AbortController(); let idle; const resetIdle = () => { clearTimeout(idle); idle = setTimeout(() => ctrl.abort(), CHAT_IDLE_MS); }; resetIdle(); let upstream; try { upstream = await fetch(`${gwBase}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model, temperature: 0.4, max_tokens: maxTokens, messages, stream: true }), signal: ctrl.signal, }); } catch (e) { clearTimeout(idle); if (e.name === 'AbortError') throw chatTimeoutError(); throw e; } if (!upstream.ok) { clearTimeout(idle); const detail = await upstream.text().catch(() => ''); const err = new Error(`上游模型调用失败 (${upstream.status})`); err.status = upstream.status === 401 ? 401 : 502; err.detail = detail.slice(0, 300); throw err; } const reader = upstream.body.getReader(); const decoder = new TextDecoder(); let buf = ''; let content = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; resetIdle(); buf += decoder.decode(value, { stream: true }); let nl; while ((nl = buf.indexOf('\n')) >= 0) { const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1); if (!line.startsWith('data:')) continue; const data = line.slice(5).trim(); if (!data || data === '[DONE]') continue; try { const j = JSON.parse(data); const delta = j.choices?.[0]?.delta; if (delta?.content) content += delta.content; else if (j.choices?.[0]?.message?.content) content += j.choices[0].message.content; } catch (_) { /* 跳过 keep-alive / 非 JSON 行 */ } } } } catch (e) { if (e.name === 'AbortError') throw chatTimeoutError(); throw e; } finally { clearTimeout(idle); } if (!content) throw new Error('模型未返回内容(可能超时或渠道异常)'); return content; } // ============================================================================ // 适配层:LLM 走 router 分组 // ============================================================================ // 把分组 baseUrl 归一成「+ /chat/completions 即正确」的基址(对齐 team.js) function chatBase(baseUrl) { const base = String(baseUrl || "").replace(/\/+$/, ""); if (/\/chat\/completions$/.test(base)) return base.replace(/\/chat\/completions$/, ""); if (/\/v\d+$/.test(base)) return base; return base + "/v1"; } // 取分组:优先按 id,否则用默认 LLM 分组 function resolveGroup(groupId) { let g = null; if (groupId != null && groupId !== "" && typeof router.groupById === "function") { g = router.groupById(groupId); } if (!g && typeof router.llmGroup === "function") g = router.llmGroup(); if (!g) throw new Error("没有可用的中转分组,请先在「路由」里配置好带 baseUrl 和密钥的分组"); if (!g.baseUrl || !g.key) throw new Error(`分组「${g.name || g.id}」缺少 baseUrl 或密钥`); return g; } // 获取所有可用分组列表(给前端下拉用) function listGroups() { try { return router.usableGroups().map((g) => ({ id: g.id, name: g.name || g.id, host: (() => { try { return new (require("url").URL)(g.baseUrl).host; } catch (_) { return ""; } })(), mode: g.mode || "chat", })); } catch (_) { return []; } } // 拉取指定分组的模型列表 async function listModels(groupId) { const g = resolveGroup(groupId); const models = await router.fetchModels(g.baseUrl, g.key); return models; } // 核心:图片 → 提示词分析 async function analyze({ image, model, groupId } = {}) { if (!image || typeof image !== "string") throw new Error("缺少图片数据"); if (!model) throw new Error("未选择模型"); const g = resolveGroup(groupId); const gwBase = chatBase(g.baseUrl); const content = await callChat(gwBase, g.key, model, [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: [ { type: "text", text: USER_INSTRUCTION }, { type: "image_url", image_url: { url: image } }, ], }, ], 2000); return { result: extractJson(content), model }; } module.exports = { analyze, listGroups, listModels };