Update server.js via upload script - 2026-07-27 10:09:57
This commit is contained in:
parent
0ad7227ae6
commit
cc525ce1e9
77
server.js
77
server.js
|
|
@ -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专属:输出口类型数组,只能用这些token:USB、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/60Hz(Auto 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 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue