fix: canvas image/video download - fix data URL parsing in cvDownloadPreview (v1.5.21, hot 1.5.23)
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user