Update server.js via upload script - 2026-07-27 10:09:57

This commit is contained in:
mike 2026-07-27 10:10:19 +08:00
parent 0ad7227ae6
commit cc525ce1e9
1 changed files with 63 additions and 14 deletions

View File

@ -175,16 +175,56 @@ async function callDeepSeekWithRetry(apiKey, messages, opts, retries = 1) {
}
// 从DeepSeek返回的内容里提取JSON——模型有时会用```json代码块包一层或者前后带几句解释文字
// 这里做宽松解析:优先找代码块,找不到就找第一个 { 或 [ 到最后一个 } 或 ] 之间的内容。
// 这里做宽松解析:优先找代码块,找不到就找第一个 { 或 [ 开始的内容。
// 返回 { data, truncated }如果完整JSON解析失败常见原因是撞到了max_tokens数组还没写完
// 就被截断了),会尝试抢救出前面已经完整生成的那些顶层对象,丢弃最后一个写了一半的,这样至少
// 能拿到部分结果,而不是整批因为最后一个字符缺失就全部作废。
function extractJson(content) {
const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
const raw = fenced ? fenced[1] : content;
const start = raw.search(/[\[{]/);
const endBrace = raw.lastIndexOf('}');
const endBracket = raw.lastIndexOf(']');
const end = Math.max(endBrace, endBracket);
if (start === -1 || end === -1 || end < start) throw new Error('AI返回内容里没有找到JSON');
return JSON.parse(raw.slice(start, end + 1));
if (start === -1) throw new Error('AI返回内容里没有找到JSON');
const trimmed = raw.slice(start);
try {
return { data: JSON.parse(trimmed), truncated: false };
} catch (e) {
const salvaged = salvageTruncatedJsonArray(trimmed);
if (salvaged !== null) return { data: salvaged, truncated: true };
throw new Error(`AI返回的内容不是完整的JSON解析失败${e.message}`);
}
}
// 只处理"最外层是数组"的截断抢救:逐字符扫描(跳过字符串内部的引号/转义),每次遇到顶层元素
// 完整闭合(深度归零)就记一下位置,最后从抢救到的最后一个完整位置那里把数组截断重新拼上 ]。
function salvageTruncatedJsonArray(text) {
const t = text.trim();
if (!t.startsWith('[')) return null;
let depth = 0;
let lastCompleteEnd = -1;
let inString = false;
let escapeNext = false;
for (let i = 1; i < t.length; i++) {
const ch = t[i];
if (inString) {
if (escapeNext) escapeNext = false;
else if (ch === '\\') escapeNext = true;
else if (ch === '"') inString = false;
continue;
}
if (ch === '"') { inString = true; continue; }
if (ch === '{' || ch === '[') depth++;
else if (ch === '}' || ch === ']') {
depth--;
if (depth === 0 && ch === '}') lastCompleteEnd = i; // 顶层数组里某个对象完整闭合了
}
}
if (lastCompleteEnd === -1) return null;
try {
return JSON.parse(t.slice(0, lastCompleteEnd + 1) + ']');
} catch (e) {
return null;
}
}
/* ------------------------------ data file ------------------------------ */
@ -462,15 +502,24 @@ OCR识别可能有少量错字或格式错乱请结合上下文合理判断
"name": "型号,比如 POE-532E",
"brand": "品牌中文名,从厂家名称/logo文字判断看不出就填空字符串",
"category": "产品大类中文名从这些里选最接近的UPS不间断电源 / 光伏逆变器 / 储能电池;不确定就填 UPS不间断电源",
"type": "细分品类中文名。如果是这种带USB/DC/POE多路直流输出口的小型UPS填 迷你UPS其他常见值机架式UPS、高频塔式UPS、工频UPS、模块化UPS、在线互动式UPS、离线式UPS不确定就留空字符串",
"type": "细分品类中文名。如果是这种带USB/DC/POE多路直流输出口的小型UPS填 迷你UPS如果是没有LCD/LED显示、简单的家用小型UPS(型号通常带Offline/后备式字样),填 离线式UPS如果标注了Line-Interactive/互动式,填 在线互动式UPS其他常见值机架式UPS、高频塔式UPS、工频UPS、模块化UPS不确定就留空字符串",
"machineVariant": "标机 或 长机标准电池容量版本填标机型号带L后缀或电池容量明显更大的加长版填长机不确定就填标机",
"powerOrCapacity": "非UPS大类的功率/容量如果是迷你UPS这里填输出功率如 17W其他情况不确定就填空字符串",
"voltage": "如果是迷你UPS这里填输入电压如 100~240Vac/50-60Hz其他情况填常规电压参数,不确定填空字符串",
"powerOrCapacity": "非UPS大类的功率/容量如果是迷你UPS这里填输出功率如 17W如果是离线式UPS/在线互动式UPS这里填Capacity/容量(如 400VA/240W其他情况不确定就填空字符串",
"voltage": "如果是迷你UPS这里填输入电压如 100~240Vac/50-60Hz如果是离线式UPS/在线互动式UPS这个字段不用填用下面的inputVoltage/outputVoltage代替其他情况填常规电压参数,不确定填空字符串",
"ratedPower": "", "ratedPowerUnit": "VA", "powerFactor": "",
"outputPortCount": "迷你UPS专属这个型号有几路输出口数字字符串如 4不适用就填空字符串",
"outputPortTypes": "迷你UPS专属输出口类型数组只能用这些tokenUSB、DC、DC1、DC2、DC3、POE、Type-C、DC interface、POE interface不适用就填空数组",
"outputPorts": "迷你UPS专属数组每项 {\\"type\\":\\"USB\\",\\"voltage\\":\\"5Vdc\\",\\"current\\":\\"3.0A\\"}和outputPortTypes一一对应不适用就填空数组",
"batterySpec": "电池数量和容量,如 2200mAh*4不确定填空字符串",
"inputVoltage": "离线式UPS/在线互动式UPS专属Input Voltage/输入电压,如 110V/120Vac or 220V/230Vac不适用就填空字符串",
"inputVoltageRange": "离线式UPS/在线互动式UPS专属Voltage range/输入电压范围,如 162-268Vac/170-280Vac不适用就填空字符串",
"inputFrequency": "离线式UPS/在线互动式UPS专属Frequency/输入频率,如 50/60HzAuto sensing不适用就填空字符串",
"outputVoltage": "离线式UPS/在线互动式UPS专属Output Voltage/输出电压,如 110V/120Vac or 220V/230Vac不适用就填空字符串",
"outputVoltageRange": "离线式UPS/在线互动式UPS专属输出电压范围如 ±10%;不适用就填空字符串",
"transferTime": "离线式UPS/在线互动式UPS专属Transfer time/转换时间,如 Typical 2-8ms, 13ms max不适用就填空字符串",
"waveform": "离线式UPS/在线互动式UPS专属Wave form/波形,如 Simulated Sine Wave 或 Sine Wave不适用就填空字符串",
"batteryVoltage": "离线式UPS/在线互动式UPS专属Battery Voltage/电池电压,如 12V 或 24V不适用就填空字符串",
"chargeCurrent": "离线式UPS/在线互动式UPS专属Charge current/充电电流,如 1A不适用就填空字符串",
"batterySpec": "电池数量和容量/电池型号和数量,如 2200mAh*4 或 12V4.5AH*1不确定填空字符串",
"dimensions": "尺寸(mm),如 175*105*30不确定填空字符串",
"netWeight": "净重(kg),如 0.4,不确定填空字符串",
"notes": "其他有用但上面字段装不下的信息,一两句话,没有就填空字符串"
@ -531,7 +580,7 @@ app.post('/api/ai/parse-catalog', requireAuth, aiLimiter, (req, res) => {
{ role: 'system', content: CATALOG_PARSE_SYSTEM_PROMPT },
{ role: 'user', content: userContent },
], { maxTokens: 8000, temperature: 0.2 });
const products = extractJson(content);
const { data: products, truncated } = extractJson(content);
if (!Array.isArray(products)) throw new Error('AI没有返回预期的数组格式请重试');
// 双保险即使AI没完全听话这里直接强制覆盖保证和用户预选的一致
const finalProducts = products.map((p) => ({
@ -540,7 +589,7 @@ app.post('/api/ai/parse-catalog', requireAuth, aiLimiter, (req, res) => {
brand: hintBrand || p.brand,
type: hintType || p.type,
}));
res.json({ products: finalProducts, ocrTextLength: text.length });
res.json({ products: finalProducts, ocrTextLength: text.length, truncated });
} catch (e) {
const hint = /empty response/i.test(e.message)
? '重试后仍然失败可能是一次上传的文件太多导致AI输出内容超长建议减少单次上传的数量比如分成2-3批每批3-5张'
@ -573,11 +622,11 @@ app.post('/api/ai/translate-terms', requireAuth, aiLimiter, async (req, res) =>
const userPrompt = JSON.stringify(terms);
try {
const content = await callDeepSeek(cfg.apiKey, [
const content = await callDeepSeekWithRetry(cfg.apiKey, [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
], { maxTokens: 2000, temperature: 0.3 });
const translations = extractJson(content);
const { data: translations } = extractJson(content);
res.json({ translations });
} catch (e) {
res.status(502).json({ error: e.message });