Files
codex-market-app/electron/lib/prompt-analysis.js
T

162 lines
5.9 KiB
JavaScript

// prompt-analysis.js —— 图片反推生图提示词(移植自「提示词提取」项目)
// LLM 通道走本应用的 router 分组(直接读中转分组列表),与 team.js 同模式。
"use strict";
const router = require("./router");
const { SYSTEM_PROMPT, USER_INSTRUCTION } = require("./team-prompt");
// ============================================================================
// 以下 <CORE> 自 提示词提取/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 };