932 lines
51 KiB
JavaScript
932 lines
51 KiB
JavaScript
// 成套报价 AI 选型替换确定性引擎 v2(2026-09-25 重写:规格全解析+系列字典全维度匹配,匹配不上不替换)
|
||
// 与 SKILL.md《塑壳断路器规格完全解析知识库》同源实现:
|
||
// 附件码逐位语义:第1位极数(2/3/4) 第2位脱扣(2电磁/3热磁) 第3位附件十位(0-7) 第4位附件个位(0/8)
|
||
// 第5位用途代号(空=配电/2=电动机保护,仅此两种→32008 必非法);字母码系列整串比对。
|
||
// 用法:
|
||
// node xuanxing.js <品牌关键词> <系列关键词> --dry # 预览(默认,不改库)
|
||
// node xuanxing.js <品牌关键词> <系列关键词> --exec # 真实替换(一次 /api/replacebatch,失败项回落逐行 /api/replace)
|
||
// node xuanxing.js 德力西 CDM3S --dry --only "CDM3S-250F/3300200A"
|
||
// node xuanxing.js 德力西 CDM3S --dry --fix 修正.json # {"旧规格":"纠错后规格",...} 阶段① LLM 纠错结果喂进来
|
||
// 数据链:主程序18790清单 → dqb搜索系列/系列树(品名) → 直连拉 spe_prop/spe_opt/spe_price
|
||
// → 系列选项字典全维度展开 → 知识库逐位解析旧规格 → 全维度匹配 → 无兜底(匹配不上即跳过)
|
||
var http = require("http");
|
||
var fs = require("fs");
|
||
var path = require("path");
|
||
var os = require("os");
|
||
|
||
var args = process.argv.slice(2);
|
||
var DRY = args.indexOf("--exec") < 0; // ★默认 dry 预览,必须显式 --exec 才改库
|
||
var ONLY = "";
|
||
var oi = args.indexOf("--only");
|
||
if (oi >= 0 && args[oi + 1]) { ONLY = args[oi + 1]; args.splice(oi, 2); }
|
||
var FIX = "";
|
||
oi = args.indexOf("--fix");
|
||
if (oi >= 0 && args[oi + 1]) { FIX = args[oi + 1]; args.splice(oi, 2); }
|
||
var BRAND = args[0] || "";
|
||
var SERIES = args[1] || "";
|
||
if ((!BRAND || !SERIES) && args.indexOf("--selftest") < 0) {
|
||
console.error("用法: node xuanxing.js <品牌关键词> <系列关键词> [--dry|--exec] [--only 旧规格] [--fix 修正.json]");
|
||
process.exit(1);
|
||
}
|
||
|
||
// ================= dqb 签名(内联自 dqb_sign.js,与页面同源) =================
|
||
function dqShift(t, e) {
|
||
for (var s = 0; s < e.length - 2; s += 3) {
|
||
var r = e.charAt(s + 2);
|
||
r = r >= "a" ? r.charCodeAt(0) - 87 : Number(r);
|
||
r = "+" === e.charAt(s + 1) ? t >>> r : t << r;
|
||
t = "+" === e.charAt(s) ? (t + r) & 4294967295 : t ^ r;
|
||
}
|
||
return t;
|
||
}
|
||
function dqSign(t, e) {
|
||
if (e.length > 30) e = "" + e.substr(0, 10) + e.substr(Math.floor(e.length / 2) - 5, 10) + e.substr(-10, 10);
|
||
var u = t, h = u.split("."), m = Number(h[0]) || 0, f = Number(h[1]) || 0;
|
||
var v = [], g = 0;
|
||
for (var A = 0; A < e.length; A++) {
|
||
var y = e.charCodeAt(A);
|
||
if (y < 128) v[g++] = y;
|
||
else if (y < 2048) { v[g++] = y >> 6 | 192; v[g++] = y >> 6 & 63 | 128; }
|
||
else {
|
||
if ((y & 64512) === 55296 && A + 1 < e.length && (e.charCodeAt(A + 1) & 64512) === 56320) {
|
||
y = 65536 + ((y & 1023) << 10) + (e.charCodeAt(++A) & 1023);
|
||
v[g++] = y >> 18 | 240; v[g++] = y >> 12 & 63 | 128;
|
||
} else { v[g++] = y >> 12 | 224; v[g++] = y >> 6 & 63 | 128; }
|
||
v[g++] = y & 63 | 128;
|
||
}
|
||
}
|
||
var b = m, C = "+-a^+6", S = "+-3^+b+-f";
|
||
for (var w = 0; w < v.length; w++) { b += v[w]; b = dqShift(b, C); }
|
||
b = dqShift(b, S); b ^= f;
|
||
if (b < 0) b = 2147483648 + (b & 2147483647);
|
||
b %= 1e6;
|
||
return b.toString() + "." + (b ^ m);
|
||
}
|
||
function dqSParam(sid) {
|
||
var d = "0123456789";
|
||
function r6() { var r = ""; for (var i = 0; i < 6; i++) r += d.charAt(Math.floor(Math.random() * d.length)); return r; }
|
||
var ab = r6() + "." + r6();
|
||
return sid + "." + ab + "." + dqSign(ab, String(sid));
|
||
}
|
||
|
||
// ================= dqb 解码(内联自 dqb_decode.js) =================
|
||
function dqDeStr(t, off, mul) {
|
||
var e = [];
|
||
for (var s = 0; s + 1 < t.length; s += 2)
|
||
e.push(mul * (t[s].charCodeAt() - off) + (t[s + 1].charCodeAt() - off));
|
||
return e;
|
||
}
|
||
function dqDe(t) {
|
||
var e, s, r, a, n = [], i = "", o = 256;
|
||
for (e = 0; e < 256; e += 1) n[e] = String.fromCharCode(e);
|
||
for (s = String.fromCharCode(t[0]), r = s, e = 1; e < t.length; e += 1) {
|
||
a = t[e];
|
||
if (n[a]) i = n[a];
|
||
else { if (a !== o) return null; i = s + s.charAt(0); }
|
||
r += i; n[o++] = s + i.charAt(0); s = i;
|
||
}
|
||
return r;
|
||
}
|
||
|
||
// ================= HTTP 小工具 =================
|
||
function get(url) {
|
||
return new Promise(function (res, rej) {
|
||
http.get(url, function (r) {
|
||
var b = ""; r.setEncoding("utf-8");
|
||
r.on("data", function (c) { b += c; });
|
||
r.on("end", function () { res(b); });
|
||
}).on("error", rej);
|
||
});
|
||
}
|
||
function postForm(host, p, form) {
|
||
return new Promise(function (res, rej) {
|
||
var req = http.request({ hostname: host, port: 8080, path: p, method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" } }, function (r) {
|
||
var b = ""; r.setEncoding("utf-8");
|
||
r.on("data", function (c) { b += c; });
|
||
r.on("end", function () { res(b); });
|
||
});
|
||
req.on("error", rej); req.end(form);
|
||
});
|
||
}
|
||
function postJson(url, obj) {
|
||
return new Promise(function (res, rej) {
|
||
var body = JSON.stringify(obj);
|
||
var u = new URL(url);
|
||
var req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname + u.search, method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" } }, function (r) {
|
||
var b = ""; r.setEncoding("utf-8");
|
||
r.on("data", function (c) { b += c; });
|
||
r.on("end", function () { res(b); });
|
||
});
|
||
req.on("error", rej); req.end(body);
|
||
});
|
||
}
|
||
|
||
function readToken() {
|
||
try {
|
||
var t = fs.readFileSync(path.join(os.homedir(), ".zcode", "chengtao", "token.txt"), "utf-8").trim();
|
||
if (t) return t;
|
||
} catch (e) { }
|
||
console.error("读不到令牌(~/.zcode/chengtao/token.txt;由成套报价软件主程序启动时自动生成)");
|
||
process.exit(1);
|
||
}
|
||
async function findAppPort(token) {
|
||
for (var p = 18790; p < 18800; p++) {
|
||
try {
|
||
await get("http://127.0.0.1:" + p + "/api/xuanxing?token=" + encodeURIComponent(token));
|
||
return p;
|
||
} catch (e) { }
|
||
}
|
||
console.error("找不到主程序接口(127.0.0.1:18790 起),请确认成套报价软件已运行");
|
||
process.exit(1);
|
||
}
|
||
|
||
// ================= 知识库:附件码语法域 =================
|
||
var 电流档位 = { 6: 1, 10: 1, 16: 1, 20: 1, 25: 1, 32: 1, 40: 1, 50: 1, 60: 1, 63: 1, 80: 1, 100: 1, 125: 1, 140: 1, 150: 1, 160: 1, 180: 1, 200: 1, 225: 1, 250: 1, 315: 1, 350: 1, 400: 1, 500: 1, 630: 1, 700: 1, 800: 1, 1000: 1, 1250: 1, 1600: 1, 2000: 1, 2500: 1 };
|
||
var 字母码白名单 = { AX: 1, AL: 1, AXL: 1, SHT: 1, SHTA: 1, SHTB: 1, UVT: 1, UVTD: 1, ASUVT: 1, MO: 1, OF: 1, CC: 1, KL: 1 };
|
||
var 壳架档 = [6300, 5000, 4000, 3200, 2500, 2000, 1600, 1250, 800, 630, 400, 250, 160, 125, 100, 63, 32, 20, 16];
|
||
|
||
/// 校验一个附件码串:返回 {ok:bool, why:错误原因, js:极数("3P"), tk:脱扣数字, yt:用途位}
|
||
function 拆附件码(code) {
|
||
var out = { ok: false, why: "", js: "", tk: "", yt: "", letter: "" };
|
||
if (!code) { out.why = "空码"; return out; }
|
||
if (/^[A-Z]+$/.test(code)) { // 字母码系列:整串白名单比对
|
||
if (字母码白名单[code]) { out.ok = true; out.letter = code; }
|
||
else out.why = "字母码" + code + "不在已知码表(AX/AL/SHT/UVT/MO等)";
|
||
return out;
|
||
}
|
||
if (/^[0-7][0-8]?Y$/.test(code)) { out.ok = true; out.why = "预付费变体"; return out; } // 10Y/48Y
|
||
if (!/^\d+$/.test(code)) { out.why = "码\"" + code + "\"不是纯数字/纯字母,无法按码位解读"; return out; }
|
||
var yt = "";
|
||
var c4 = code;
|
||
if (code.length === 5) {
|
||
yt = code.charAt(4);
|
||
if (yt !== "2") { out.why = "第5位用途代号=\"\"(配电保护)或\"2\"(电动机保护),\"" + yt + "\"不存在→型号" + code + "不存在"; return out; }
|
||
c4 = code.substr(0, 4);
|
||
}
|
||
if (c4.length === 4) {
|
||
var d1 = c4.charAt(0), d2 = c4.charAt(1), d3 = c4.charAt(2), d4 = c4.charAt(3);
|
||
if ("234".indexOf(d1) < 0) { out.why = "第1位极数须为2/3/4,是\"" + d1 + "\""; return out; }
|
||
if ("23".indexOf(d2) < 0) { out.why = "第2位脱扣器须为2(电磁)/3(热磁),是\"" + d2 + "\""; return out; }
|
||
if ("01234567".indexOf(d3) < 0) { out.why = "第3位附件十位须为0-7,是\"" + d3 + "\""; return out; }
|
||
if ("08".indexOf(d4) < 0) { out.why = "第4位附件个位须为0/8,是\"" + d4 + "\""; return out; }
|
||
out.ok = true; out.js = d1 + "P"; out.tk = d2; out.yt = yt;
|
||
return out;
|
||
}
|
||
if (code.length === 3) { // NM2 制:300/308/320/328 = 极数+附件十位+个位
|
||
if (["300", "308", "320", "328"].indexOf(code) >= 0) { out.ok = true; out.js = code.charAt(0) + "P"; out.yt = ""; return out; }
|
||
out.why = "3位码仅 NM2 系列使用(300/308/320/328),\"" + code + "\"不存在"; return out;
|
||
}
|
||
if (code.length === 2) {
|
||
var e1 = code.charAt(0), e2 = code.charAt(1);
|
||
if ("01234567".indexOf(e1) < 0) { out.why = "2位码十位须为0-7,是\"" + e1 + "\""; return out; }
|
||
if ("08".indexOf(e2) < 0) { out.why = "2位码个位须为0/8,是\"" + e2 + "\""; return out; }
|
||
out.ok = true; return out; // 纯附件码制,极数另看规格
|
||
}
|
||
if (code.length === 1) { if ("012345678".indexOf(code) >= 0) { out.ok = true; return out; } out.why = "1位码须为0-8"; return out; }
|
||
out.why = "码\"" + code + "\"位数(" + code.length + ")超过5位,不是合法附件码";
|
||
return out;
|
||
}
|
||
|
||
/// 码位解读成中文(方案表展示用)
|
||
function 码解读(code, c) {
|
||
if (!code) return "无码";
|
||
if (c.letter) return code + "(字母码整串)";
|
||
var s = code + "=";
|
||
if (c.js) s += c.js;
|
||
if (c.tk) s += (c.tk === "2" ? "电磁式" : "热磁");
|
||
if (code.length >= 4) {
|
||
var d3 = code.charAt(code.length - (code.length === 5 ? 2 : 1)), d4 = code.length >= 4 ? code.charAt(code.length - (code.length === 5 ? 1 : 0)) : "";
|
||
var 十位 = { 0: "无附件", 1: "分励", 2: "辅助", 3: "欠压", 4: "分励+辅助", 5: "分励+欠压", 6: "二组辅助", 7: "辅助+欠压" };
|
||
if (十位[d3] !== undefined) s += "+" + 十位[d3];
|
||
if (d4 === "8") s += "+报警触头";
|
||
}
|
||
if (c.yt === "2") s += "+电动机保护";
|
||
else if (code.length === 5) s += "+配电保护";
|
||
return s;
|
||
}
|
||
|
||
// ================= 知识库:旧规格全解析(枚举粘连切分) =================
|
||
/// 电流提取:先剥结尾的漏电标识(30mA/100mA/300mA)再匹配,防 C16+30mA 粘连吞成 1630;
|
||
/// C/D 兜底前先剥电流单位 A(防 'C40A' 的 A 触发负向断言),并取最后一组(D747/CDM 等前缀数字不是电流)。
|
||
function 取电流(str) {
|
||
var t = String(str).replace(/(30|100|300)MA$/, "").replace(/(\d+(?:\.\d+)?)A(?!M)/g, "$1 ");
|
||
var m = t.match(/(\d+(?:\.\d+)?)A/); if (m) return parseFloat(m[1]);
|
||
var ms = t.match(/([BCD])(\d+(?:\.\d+)?)(?![\dAM])/g);
|
||
if (ms && ms.length) return parseFloat(ms[ms.length - 1].replace(/^[CD]/, ""));
|
||
return 0;
|
||
}
|
||
/// v3 微型要素:从整串提取 极数(含+N)/脱扣曲线(C·D)/漏电 mA(无附件码的微型规格解析)。
|
||
/// 漏电先按标准档位字面剥出(30/100/300mA——防 C16+30 粘连吞成 1630),再在余串上提曲线/电流。
|
||
function 解析微型(str, cand) {
|
||
var s = String(str || "");
|
||
var mm = s.match(/(30|100|300)MA/);
|
||
if (mm) {
|
||
if (!cand.ma) cand.ma = parseInt(mm[1], 10);
|
||
s = s.replace(mm[1] + "MA", "");
|
||
}
|
||
s = s.replace(/(\d+(?:\.\d+)?)A(?!M)/g, "$1 "); // 剥电流单位 A(防 'C16A'/'C16AE' 触发负向断言,保留 mA)
|
||
var m = s.match(/([1-4])P(+|\+)?N/);
|
||
if (m) { cand.poles = m[1] + "P+N"; s = s.replace(m[0], " "); }
|
||
else { m = s.match(/([1-4])P/); if (m) { cand.poles = m[1] + "P"; s = s.replace(m[0], " "); } }
|
||
if (!cand.poles) {
|
||
// "-1N(N极直通)"式:DZ47sLE-1N = 1P+N(N 极直通/可开闭都归 N 极组)
|
||
m = s.match(/-?([1-4])N(?![0-9A-Z])/);
|
||
if (m) { cand.poles = m[1] + "P+N"; s = s.replace(m[0], " "); }
|
||
}
|
||
cand.js = cand.js || (cand.poles ? cand.poles.replace("+N", "") : "");
|
||
// (剥极数后再扫曲线,防 'B161P' 里 1P 与电流粘连吞错)
|
||
// 曲线+电流:取最后一组(型号尾部才是 曲线+电流;D747/CDM 前缀误扫由"取末组"防;
|
||
// 允许 'C型16' 的"型"字间隔;B 曲线=施耐德/ABB 微型保护特性曲线)
|
||
var ms = s.match(/([BCD])型?(\d+(?:\.\d+)?)(?![\dAM])/g);
|
||
if (ms && ms.length) {
|
||
var m2 = ms[ms.length - 1].match(/^([BCD])型?(\d+(?:\.\d+)?)/);
|
||
if (m2) { cand.curve = m2[1]; if (!cand.dl) cand.dl = parseFloat(m2[2]); }
|
||
}
|
||
}
|
||
/// 返回 {cands:[合法候选解], invalid:[{code,why}] , base:{kj,fenhe,jiexian}}
|
||
/// cand = {code, codeC(拆附件码结果), js, dl, kj, fenhe, jiexian}
|
||
function parseSpecAll(spec) {
|
||
var s = String(spec || "").toUpperCase().replace(/\s+/g, "");
|
||
var base = { kj: 0, fenhe: "", jiexian: "" };
|
||
var cands = [], invalid = [];
|
||
// 接线后缀
|
||
if (s.indexOf("板后接线") >= 0) { base.jiexian = "板后接线"; s = s.replace(/板后接线/g, ""); }
|
||
else if (s.indexOf("插入式") >= 0) { base.jiexian = "插入式"; s = s.replace(/插入式/g, ""); }
|
||
// 壳架(长到短防吞位)+ 分断字母(紧贴壳架数字后、/ 前)
|
||
var m = s.match(/-(6300|5000|4000|3200|2500|2000|1600|1250|1000|800|630|400|250|160|125|100|63|32|20|16)/);
|
||
if (!m) m = s.match(/^([A-Z]+)(6300|5000|4000|3200|2500|2000|1600|1250|1000|800|630|400|250|160|125|100|63|32|20|16)/);
|
||
if (m) base.kj = parseInt(m[m.length - 1], 10);
|
||
if (!base.kj) {
|
||
// ★v3 通用兜底:串中首个"独立成词"的标准壳架数——国际命名式无横杠/前缀式套不住的
|
||
// XT4N250(250)、NSX100F(100)、Tmax XT2N160(160) 等都靠这条;先剥电流(带A)防误取
|
||
var noA = s.replace(/(\d+(?:\.\d+)?)A(?!M)/g, " ");
|
||
var fm = noA.match(/(?:^|[^0-9.])(6300|5000|4000|3200|2500|2000|1600|1250|1000|800|630|400|250|160|125|100|63|32|20|16)(?=[^0-9.]|$)/);
|
||
if (fm) base.kj = parseInt(fm[1], 10);
|
||
}
|
||
var mf = s.match(/-(\d+)([A-Z])\//); // -250F/ 型
|
||
if (mf) base.fenhe = mf[2];
|
||
else { mf = s.match(/-(\d+)([A-Z])$/); if (mf) base.fenhe = mf[2]; }
|
||
// "/"后的码段:先按纯数字段匹配(电流A留在rest),不是数字再按字母码整串
|
||
var msl = s.match(/\/(\d+)(.*)$/);
|
||
var isLetter = false;
|
||
if (!msl) { msl = s.match(/\/([A-Z]+)(.*)$/); isLetter = true; }
|
||
var restAll = s;
|
||
if (msl) {
|
||
var run = msl[1], rest = msl[2];
|
||
restAll = s.substr(0, s.indexOf("/" + run)); // 去掉 "/码段" 的前段(极数/电流兜底用)
|
||
if (isLetter) { // 字母码整串比对
|
||
var lc = 拆附件码(run);
|
||
if (lc.ok) cands.push(mkCand(run, lc, rest, 0));
|
||
else { invalid.push({ code: run, why: lc.why }); 无码兜底候选(s); }
|
||
} else if (run.length === 1 && rest.charAt(0) === "P") {
|
||
// "/1P""/2P" 是极数写法,不是 1 位附件码——不当码,走无码分支逻辑
|
||
var cp = { code: "", codeC: { ok: true }, js: run + "P", dl: 取电流(rest + " " + restAll), kj: base.kj, fenhe: base.fenhe, jiexian: base.jiexian };
|
||
if (!cp.dl) cp.dl = 取电流(restAll);
|
||
解析微型(s, cp);
|
||
if (cp.js || cp.dl) cands.push(cp);
|
||
} else {
|
||
// 数字串:尾部紧粘 A(码+电流粘连)→ 枚举全部切分;否则整段是码
|
||
var glued = rest.charAt(0) === "A";
|
||
if (glued) {
|
||
var seen = {};
|
||
for (var cl = 1; cl < run.length; cl++) {
|
||
var code = run.substr(0, cl), curStr = run.substr(cl);
|
||
var cur = parseInt(curStr, 10);
|
||
if (isNaN(cur) || cur <= 0 || !电流档位[cur]) continue;
|
||
if (base.kj && cur > base.kj) continue; // 电流必须 ≤ 壳架
|
||
var c2 = 拆附件码(code);
|
||
if (c2.ok) {
|
||
var key = code + "|" + cur;
|
||
if (!seen[key]) { seen[key] = 1; cands.push(mkCand(code, c2, "", cur)); }
|
||
} else {
|
||
invalid.push({ code: code, why: c2.why });
|
||
}
|
||
}
|
||
} else {
|
||
var c3 = 拆附件码(run);
|
||
if (c3.ok) cands.push(mkCand(run, c3, rest, 0));
|
||
else { invalid.push({ code: run, why: c3.why }); 无码兜底候选(s); }
|
||
}
|
||
}
|
||
}
|
||
function mkCand(code, c, rest, dlGiven) {
|
||
var cand = { code: code, codeC: c, js: c.js || "", dl: dlGiven || 0, kj: base.kj, fenhe: base.fenhe, jiexian: base.jiexian };
|
||
if (!cand.js) { var mj = (restAll + rest).match(/([1-4])P/); if (mj) cand.js = mj[1] + "P"; }
|
||
if (!cand.dl) cand.dl = 取电流(rest + " " + restAll);
|
||
解析微型(rest + " " + restAll, cand);
|
||
return cand;
|
||
}
|
||
/// v3 无码兜底候选:"/"后的段当码解析非法时(OCR 噪声斜杠如 C16/A e30mA),
|
||
/// 用完整串按微型要素再出一个无码候选,让微型全维度匹配兜住
|
||
function 无码兜底候选(text) {
|
||
var c = { code: "", codeC: { ok: true }, js: "", dl: 0, kj: base.kj, fenhe: base.fenhe, jiexian: base.jiexian };
|
||
var mj = text.match(/([1-4])P/); if (mj) c.js = mj[1] + "P";
|
||
c.dl = 取电流(text);
|
||
解析微型(text, c);
|
||
if (c.js || c.dl || c.curve || c.ma) cands.push(c);
|
||
}
|
||
// 无"/"码段的规格(微型等):极数/曲线/漏电/电流解析
|
||
if (!msl) {
|
||
var cand = { code: "", codeC: { ok: true }, js: "", dl: 0, kj: base.kj, fenhe: base.fenhe, jiexian: base.jiexian };
|
||
var mj2 = s.match(/([1-4])P/); if (mj2) cand.js = mj2[1] + "P";
|
||
cand.dl = 取电流(s);
|
||
解析微型(s, cand);
|
||
if (cand.js || cand.dl) cands.push(cand);
|
||
}
|
||
return { cands: cands, invalid: invalid, base: base };
|
||
}
|
||
|
||
// ================= 系列选项字典(全维度) =================
|
||
function 维度角色(propName) {
|
||
var n = String(propName || "");
|
||
if (n.indexOf("壳架") >= 0) return "kj";
|
||
if (n.indexOf("极数") >= 0) return "js";
|
||
// ★"额定剩余动作电流"含"额定电流"子串——必须先判剩余/漏电(否则 30mA 被当电流维)
|
||
if (n.indexOf("剩余") >= 0 || n.indexOf("漏电") >= 0 || n.indexOf("动作电流") >= 0) return "ma";
|
||
if (n.indexOf("额定电流") >= 0) return "dl";
|
||
if (n.indexOf("分断") >= 0) return "fenhe";
|
||
if (n.indexOf("脱扣") >= 0) return "tk";
|
||
if (n.indexOf("附件") >= 0) return "acc";
|
||
if (n.indexOf("用途") >= 0) return "yt";
|
||
if (n.indexOf("操作") >= 0) return "oper";
|
||
if (n.indexOf("接线") >= 0) return "jx";
|
||
return "other";
|
||
}
|
||
function 选项打印码(optName) { // "3【热磁式】"→"3";"3P"→"3";"1P+N(…)"/"S 35kA"→""
|
||
var t = String(optName || "").trim();
|
||
var m = t.match(/^([0-9]+)【/); if (m) return m[1];
|
||
m = t.match(/^([1-4])P(?:$|[^N++])/); if (m && t.indexOf("【") < 0) return m[1]; // 塑壳纯 '3P'
|
||
return "";
|
||
}
|
||
function 选项分断字母(optName) {
|
||
var m = String(optName || "").trim().match(/^([A-Z])\b/); return m ? m[1] : "";
|
||
}
|
||
function numFromOpt(name) {
|
||
var m = String(name || "").toUpperCase().match(/(\d+(?:\.\d+)?)/);
|
||
return m ? parseFloat(m[1]) : 0;
|
||
}
|
||
function jsFromOpt(name) {
|
||
var u = String(name || "").toUpperCase();
|
||
if (u.indexOf("1P") >= 0) return "1P";
|
||
if (u.indexOf("2P") >= 0) return "2P";
|
||
if (u.indexOf("4P") >= 0) return "4P";
|
||
if (u.indexOf("3P") >= 0) return "3P";
|
||
var m = u.match(/^([1-4])(P|极)/);
|
||
return m ? m[1] + "P" : "";
|
||
}
|
||
function normJx(label) {
|
||
var t = String(label || "");
|
||
if (t.indexOf("板后") >= 0) return "板后接线";
|
||
if (t.indexOf("插入") >= 0) return "插入式";
|
||
return "板前"; // 板前/空=默认
|
||
}
|
||
|
||
/// 把 spe_prop/spe_opt/spe_price 组装成全维度价格行;返回 {rows, dict:{codeStrSet}, propsByRole}
|
||
/// v3:微型断路器支持——每行额外提取 poles(极数含+N)/curve(C·D 曲线)/ma(漏电 mA),
|
||
/// 并收集各选项的 nameStr/nameSort(元件库页面规范型号串=系列短名+nameStr 按 nameSort 拼接)。
|
||
function 展开价格行(props, opts, prices) {
|
||
var propRole = {}, propName = {};
|
||
(props || []).forEach(function (p) {
|
||
propRole[p.propId] = 维度角色(p.propName);
|
||
propName[p.propId] = String(p.propName || "");
|
||
});
|
||
var optName = {}, optNameStr = {}, optNameSort = {};
|
||
(opts || []).forEach(function (o) {
|
||
var k = o.propId + "-" + o.optId;
|
||
optName[k] = String(o.optName || "");
|
||
optNameStr[k] = String(o.nameStr || "");
|
||
optNameSort[k] = Number(o.nameSort || o.orderSort || 9999);
|
||
});
|
||
var accProps = []; // 附件维度按 propId 升序拼码
|
||
Object.keys(propRole).forEach(function (pid) { if (propRole[pid] === "acc") accProps.push(Number(pid)); });
|
||
accProps.sort(function (a, b) { return a - b; });
|
||
// ★码拼装只对塑壳式系列(存在 脱扣/附件/用途 任一维度)——微型系列(极数/曲线/电流/漏电)恒无码
|
||
var hasCodeDims = Object.keys(propRole).some(function (pid) {
|
||
return propRole[pid] === "tk" || propRole[pid] === "acc" || propRole[pid] === "yt";
|
||
});
|
||
var rows = [];
|
||
(prices || []).forEach(function (pr) {
|
||
if (!pr || pr.compPrice === undefined || pr.compPrice === null) return;
|
||
var price = parseFloat(pr.compPrice);
|
||
if (isNaN(price)) return;
|
||
var row = { kj: 0, js: "", dl: 0, fenhe: "", code: "", oper: "", jx: "", other: 0,
|
||
poles: "", curve: "", ma: 0, nameParts: [], unnamed: [],
|
||
price: price, optStr: pr.optStr };
|
||
var codeJs = "", codeTk = "", codeAcc = "", codeYt = "";
|
||
String(pr.optStr || "").split("-").forEach(function (id) {
|
||
var pid = null, name = "", key = "";
|
||
for (var k in propRole) { if (optName[k + "-" + id] !== undefined) { pid = Number(k); name = optName[k + "-" + id]; key = k + "-" + id; break; } }
|
||
if (pid === null) return;
|
||
var role = propRole[pid];
|
||
// nameStr 拼装素材(页面规范型号串的组成段,按 nameSort 排)
|
||
if (optNameStr[key]) row.nameParts.push({ sort: optNameSort[key], str: optNameStr[key] });
|
||
if (role === "kj") { row.kj = numFromOpt(name); return; }
|
||
if (role === "js") {
|
||
row.js = jsFromOpt(name); codeJs = 选项打印码(name);
|
||
row.poles = 归一极数(name);
|
||
var mc = name.toUpperCase().match(/([BCD])\s*型/); if (mc) row.curve = mc[1];
|
||
return;
|
||
}
|
||
if (role === "dl") {
|
||
row.dl = numFromOpt(name);
|
||
var md = name.toUpperCase().match(/(\d+)\s*MA\b/); if (md) row.ma = parseInt(md[1], 10);
|
||
var mcc = name.toUpperCase().match(/^([CD])\s*型?\s*\d/); if (mcc) row.curve = mcc[1];
|
||
return;
|
||
}
|
||
if (role === "ma") { // 额定剩余动作电流(漏电档)
|
||
var mma2 = name.toUpperCase().match(/(\d+)\s*MA/);
|
||
if (mma2) row.ma = parseInt(mma2[1], 10);
|
||
else if (/无|不带/.test(name)) row.ma = 0;
|
||
row.unnamed.push(String(name || "")); // 漏电档也进未识别维度(消歧用"不带")
|
||
return;
|
||
}
|
||
if (role === "fenhe") { row.fenhe = 选项分断字母(name); return; }
|
||
if (role === "tk") {
|
||
codeTk = 选项打印码(name);
|
||
// ★曲线提取:'B型'/'C型'/'D型'(纯曲线维) 或 'C16A'(曲线+电流合一)——数字可选
|
||
var mtk = name.toUpperCase().match(/^([BCD])\s*型?\s*(\d+(?:\.\d+)?)?/);
|
||
if (mtk && mtk[1]) {
|
||
if (!row.curve) row.curve = mtk[1];
|
||
if (mtk[2] && !row.dl) row.dl = parseFloat(mtk[2]);
|
||
}
|
||
return;
|
||
}
|
||
if (role === "acc") { codeAcc += 选项打印码(name); return; }
|
||
if (role === "yt") { codeYt = 选项打印码(name); return; } // 配电保护打印空
|
||
if (role === "oper") { row.oper = String(name || ""); return; }
|
||
if (role === "jx") { row.jx = normJx(name); return; }
|
||
// 兜底维度(曲线类型/漏电等独立维):尽力提取曲线与 mA;文本存入 unnamed(歧义消解用)
|
||
var mcx = name.toUpperCase().match(/^([CD])\s*型/); if (mcx && !row.curve) row.curve = mcx[1];
|
||
var mma = name.toUpperCase().match(/(\d+)\s*MA\b/); if (mma && !row.ma) row.ma = parseInt(mma[1], 10);
|
||
if (role === "other" || role === "acc" || role === "tk" || role === "yt") row.unnamed.push(String(name || ""));
|
||
row.other++;
|
||
});
|
||
if (!row.poles && row.js) row.poles = row.js;
|
||
row.nameParts.sort(function (a, b) { return a.sort - b.sort; });
|
||
row.code = hasCodeDims ? ((codeJs + codeTk + codeAcc + codeYt) || "") : "";
|
||
// ★v3"极数即码"修正:微型系列(如 DZ47s)的 2P/3P/4P 极数选项是 '3【3P】' 带码形式,极数数字
|
||
// 被拼进 code——若 code 仅由极数位构成(无脱扣/附件/用途位)且等于极数数字,视为微型式行,清码
|
||
if (row.code && codeJs && !codeTk && !codeAcc && !codeYt
|
||
&& row.js && codeJs === row.js.replace("P", "")) row.code = "";
|
||
rows.push(row);
|
||
});
|
||
// ★v3 系列固定维度回填:某维度从未被任何价格组合选中且只有唯一选项(如 DZ47PLE 的
|
||
// 极数固定 1P+N、漏电固定 30mA——不进价格组合,行侧恒空导致全维度误判失配)——
|
||
// 把唯一选项按角色提取后回填进所有行。
|
||
try {
|
||
var 已选optId = {};
|
||
(prices || []).forEach(function (pr) {
|
||
String(pr.optStr || "").split("-").forEach(function (id) { 已选optId[id] = 1; });
|
||
});
|
||
(props || []).forEach(function (p) {
|
||
var 本维选项 = (opts || []).filter(function (o) { return Number(o.propId) === Number(p.propId); });
|
||
if (本维选项.length !== 1) return;
|
||
var o = 本维选项[0];
|
||
if (已选optId[String(o.optId)]) return; // 被选中过=非固定维度
|
||
var name = String(o.optName || "");
|
||
var role = propRole[p.propId];
|
||
var mm;
|
||
rows.forEach(function (r) {
|
||
if (role === "ma") { mm = name.toUpperCase().match(/(\d+)\s*MA/); if (mm && !r.ma) r.ma = parseInt(mm[1], 10); }
|
||
else if (role === "js") { if (!r.poles) r.poles = 归一极数(name) || r.poles; if (!r.js) r.js = jsFromOpt(name); }
|
||
else if (role === "tk") { mm = name.toUpperCase().match(/^([CD])/); if (mm && !r.curve) r.curve = mm[1]; }
|
||
else if (role === "kj") { if (!r.kj) r.kj = numFromOpt(name); }
|
||
else if (role === "dl") { if (!r.dl) r.dl = numFromOpt(name); }
|
||
});
|
||
});
|
||
} catch (eFix) { }
|
||
return { rows: rows, propRole: propRole, propName: propName };
|
||
}
|
||
|
||
/// 极数原文归一:'1P+N(N极可开闭)'/'1P+N'→'1P+N';'2P'→'2P';'1N(N极直通)'→'1P+N'
|
||
/// (保留 N 极信息,微型匹配关键维度)
|
||
function 归一极数(name) {
|
||
var u = String(name || "").toUpperCase().replace(/\s+/g, "");
|
||
var m = u.match(/^([1-4])P/);
|
||
if (m) {
|
||
var 带N = /^([1-4])P(?:+|\+)?N/.test(u) || /N极/.test(String(name || ""));
|
||
return m[1] + "P" + (带N ? "+N" : "");
|
||
}
|
||
m = u.match(/^([1-4])N/); // '1N(N极直通)'式
|
||
if (m) return m[1] + "P+N";
|
||
return "";
|
||
}
|
||
|
||
// ================= 全维度匹配(无兜底) =================
|
||
/// 返回 null=命中;否则=失配维度名。
|
||
/// 塑壳模式(cand/row 带 code):原全维度比对。
|
||
/// 微型模式(双方都无 code,v3 新增):极数(含+N)/脱扣曲线(C·D)/漏电 mA/电流 全等才命中。
|
||
function 全维度匹配(cand, row) {
|
||
if (row.kj && cand.kj && row.kj !== cand.kj) return "壳架(" + cand.kj + "≠" + row.kj + ")";
|
||
if (row.js && cand.js && row.js !== cand.js) return "极数(" + cand.js + "≠" + row.js + ")";
|
||
if (row.dl && cand.dl && row.dl !== cand.dl) return "电流(" + cand.dl + "≠" + row.dl + ")";
|
||
if (row.code && cand.code && row.code !== cand.code) return "附件码(" + cand.code + "≠" + row.code + ")";
|
||
if (row.code && !cand.code) return "附件码(旧规格无码,系列型号带码" + row.code + ")";
|
||
if (!row.code && cand.code) return "附件码(系列型号不带码,旧规格码" + cand.code + ")";
|
||
if (row.fenhe && cand.fenhe && row.fenhe !== cand.fenhe) return "分断(" + cand.fenhe + "≠" + row.fenhe + ")";
|
||
if (normJx(cand.jiexian) !== (row.jx || "板前")) return "接线(" + (cand.jiexian || "板前") + "≠" + (row.jx || "板前") + ")";
|
||
if (!row.code && !cand.code) {
|
||
// 微型维度(极数N/曲线/漏电):双方都有→必须相等;cand 有 row 无→行不完整失配;
|
||
// cand 无 row 有(如旧规格未标漏电档/未标曲线)→放行,交由价格消歧(缺省/无附件档在歧义分支选)
|
||
if (row.poles && cand.poles && row.poles !== cand.poles)
|
||
return "极数N(" + cand.poles + "≠" + row.poles + ")";
|
||
if (!row.poles && cand.poles) return "极数N(系列组合未含极数)";
|
||
if (row.curve && cand.curve && row.curve !== cand.curve)
|
||
return "曲线(" + cand.curve + "≠" + row.curve + ")";
|
||
if (!row.curve && cand.curve) return "曲线(系列组合未含曲线)";
|
||
if (row.ma && cand.ma && row.ma !== cand.ma)
|
||
return "漏电(" + cand.ma + "mA≠" + row.ma + "mA)";
|
||
if (!row.ma && cand.ma) return "漏电(系列该组合不带)";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// 匹配诊断:码在该系列是否存在、差在哪个维度(跳过原因用)
|
||
function 匹配诊断(cand, rows) {
|
||
var 同码 = rows.filter(function (r) { return !r.code || r.code === cand.code; });
|
||
if (cand.code && 同码.length === 0) return "附件码" + cand.code + "不在系列选项表(该系列无此型号)";
|
||
var whyCount = {};
|
||
同码.forEach(function (r) { var w = 全维度匹配(cand, r); if (w) { var k = w.split("(")[0]; whyCount[k] = (whyCount[k] || 0) + 1; } });
|
||
var ks = Object.keys(whyCount);
|
||
if (!ks.length) return "无组合(价格表缺行)";
|
||
return ks.map(function (k) { return k + "不匹配"; }).join("+");
|
||
}
|
||
|
||
// ================= 主流程 =================
|
||
async function main() {
|
||
var token = readToken();
|
||
var port = await findAppPort(token);
|
||
|
||
var list = JSON.parse(await get("http://127.0.0.1:" + port + "/api/xuanxing?token=" + encodeURIComponent(token)));
|
||
if (!list.ok) { console.error("读选型页失败: " + (list.error || "")); process.exit(1); }
|
||
var fixMap = {};
|
||
if (FIX) {
|
||
try { fixMap = JSON.parse(fs.readFileSync(FIX, "utf-8")); console.log("已载入修正文件 " + FIX + "(" + Object.keys(fixMap).length + " 条)"); }
|
||
catch (e) { console.error("修正文件读取失败: " + e.message); process.exit(1); }
|
||
}
|
||
var targets = (list.rows || []).filter(function (r) {
|
||
if (r.fenlei !== "塑壳" && r.fenlei !== "微型") {
|
||
// v3:fenlei=null 的疑似断路器行(OCR 乱码 D747s/0747s 等)——能解析出断路器要素即纳入(推断行);
|
||
// 明确其他类型(隔离开关等)坚决跳过
|
||
if (r.fenlei) return false;
|
||
var P0 = parseSpecAll(r.guige);
|
||
var c0 = P0.cands[0];
|
||
if (!P0.cands.length || (!c0.code && !c0.curve && !c0.ma && !c0.js)) return false;
|
||
r.tuiDuan = true;
|
||
}
|
||
if (ONLY && r.guige !== ONLY) return false;
|
||
return true;
|
||
});
|
||
var 推断数 = 0;
|
||
targets.forEach(function (t) { if (t.tuiDuan) 推断数++; });
|
||
console.log("选型页共 " + list.rows.length + " 行,可替换目标 " + targets.length + " 行"
|
||
+ (推断数 ? "(含 " + 推断数 + " 行 fenlei=null 推断行——解析出断路器要素,方案表已标注 ⚠️推断)" : "")
|
||
+ (ONLY ? "(限定 " + ONLY + ")" : ""));
|
||
if (!targets.length) { console.log("无可替换行,结束。"); return; }
|
||
|
||
// 目标系列:搜索候选里系列名最贴切的第一个有价系列(品牌+系列关键词都过滤)
|
||
var search = JSON.parse(await postForm("127.0.0.1", "/api8k/get_data_search2", "inputstr=" + encodeURIComponent(SERIES)));
|
||
var cands0 = [];
|
||
(search.data || []).forEach(function (f) {
|
||
if (BRAND && (String(f.factoryname || "").indexOf(BRAND) < 0) && (String(f.fsortname || "").indexOf(BRAND) < 0)) return;
|
||
(f.data || []).forEach(function (ser) {
|
||
var nm = String(ser.seriesname || "");
|
||
if (nm.toUpperCase().indexOf(SERIES.toUpperCase()) < 0) return;
|
||
cands0.push({ id: ser.SeriesId, name: nm, factory: f.factoryname, rank: (nm.toUpperCase().indexOf(SERIES.toUpperCase()) === 0 ? 0 : 1) });
|
||
});
|
||
});
|
||
cands0.sort(function (a, b) { return a.rank - b.rank; });
|
||
if (!cands0.length) { console.error("元件库里没找到 [" + BRAND + " " + SERIES + "] 系列(搜索只认系列名)"); process.exit(1); }
|
||
|
||
var hit = null, speData = null, tried = [];
|
||
for (var ci = 0; ci < Math.min(cands0.length, 5); ci++) {
|
||
var c0 = cands0[ci];
|
||
var raw0 = await get("http://127.0.0.1:8080/api8k/get_spedata_all?s=" + dqSParam(c0.id));
|
||
var j0 = JSON.parse(dqDe(dqDeStr(raw0.trim(), 903, 500)) || "{}");
|
||
var d0 = (j0 && j0.data) || {};
|
||
var pr0 = d0.spe_price || [];
|
||
tried.push(c0.name + (pr0.length ? "(有价" + pr0.length + "组合)" : "(未定价)"));
|
||
if (pr0.length) { hit = c0; speData = d0; break; }
|
||
}
|
||
if (!hit) { console.error("候选系列均未定价:" + tried.join("、")); process.exit(1); }
|
||
console.log("目标系列: " + hit.name + " (SeriesId=" + hit.id + ", " + hit.factory + ")");
|
||
if (tried.length > 1) console.log(" 候选: " + tried.join("、"));
|
||
|
||
// ★全维度字典:拉全部 spe_prop + spe_opt(不只壳架/极数/电流三个)
|
||
var 展 = 展开价格行(speData.spe_prop || [], speData.spe_opt || [], speData.spe_price || []);
|
||
var rows = 展.rows;
|
||
var 角色表 = 展.propRole;
|
||
var 维度清单 = [];
|
||
Object.keys(角色表).forEach(function (pid) { 维度清单.push(展.propName[pid] + "#" + 角色表[pid]); });
|
||
console.log("系列组合价格 " + rows.length + " 条;维度: " + 维度清单.join("、"));
|
||
var 有码维度 = rows.some(function (r) { return r.code; });
|
||
var 有操作维度 = rows.some(function (r) { return r.oper; });
|
||
|
||
// 品名:系列树拿 DustryName(与元件库"替换"按钮同源)
|
||
var dustry = "";
|
||
try {
|
||
for (var fi = 1; fi <= 2 && !dustry; fi++) {
|
||
var treeRaw = await get("http://127.0.0.1:8080/api8k/get_seriesdata?factId=" + fi);
|
||
var tree = JSON.parse(treeRaw);
|
||
(function walk(nodes) {
|
||
(nodes || []).forEach(function (n) {
|
||
if (Number(n.SeriesId) === Number(hit.id)) { dustry = String(n.DustryName || ""); hit.treeName = String(n.SeriesName || n.seriesname || hit.name); }
|
||
if (n.child) walk(n.child);
|
||
});
|
||
})(tree.data || tree);
|
||
}
|
||
} catch (e) { }
|
||
if (!dustry) console.log(" (系列树未取到 DustryName,xin_pinming 将省略由软件按系列名补)");
|
||
|
||
// ★折扣现读(元件库页面 localStorage;读不到按 100→系数1)
|
||
var bt = 100;
|
||
try {
|
||
var pr = JSON.parse(await postJson("http://127.0.0.1:" + port + "/api/page?token=" + encodeURIComponent(token), {
|
||
js: "(function(){var o={};for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i);if(k&&k.indexOf('__dqb折扣@')===0){try{o[k.slice(8)]=JSON.parse(localStorage.getItem(k))}catch(e){}}}return JSON.stringify(o)})()"
|
||
}));
|
||
if (pr && pr.ok && pr.result) {
|
||
var map = JSON.parse(JSON.parse(pr.result));
|
||
var key = Object.keys(map).filter(function (k) { return k === hit.name || k === hit.treeName; })[0];
|
||
if (key && map[key] && map[key].bt) bt = parseFloat(map[key].bt);
|
||
}
|
||
} catch (e) { }
|
||
var xishu = bt / 100;
|
||
console.log("采购系数=" + xishu + "(系列折扣bt=" + bt + "%,页面 localStorage 现读" + (bt === 100 ? ",无存档按100" : "") + ")");
|
||
|
||
// ===== 逐行解析+全维度匹配(无兜底:匹配不上即跳过) =====
|
||
var done = [], skip = [];
|
||
for (var i = 0; i < targets.length; i++) {
|
||
var t = targets[i];
|
||
var guige = t.guige;
|
||
var fixedNote = "";
|
||
if (fixMap[guige]) { fixedNote = " [已按修正文件: " + fixMap[guige] + "]"; guige = fixMap[guige]; }
|
||
var P = parseSpecAll(guige);
|
||
if (!P.cands.length) {
|
||
var why = P.invalid.length ? P.invalid.map(function (x) { return "附件码" + x.code + "非法:" + x.why; }).join(";") : "解析不出壳架/极数/电流";
|
||
skip.push({ guige: t.guige, fenlei: t.fenlei, why: why + fixedNote });
|
||
continue;
|
||
}
|
||
// 逐候选解试全维度匹配
|
||
var 方案 = [];
|
||
P.cands.forEach(function (cand) {
|
||
var hits = rows.filter(function (r) { return !全维度匹配(cand, r); });
|
||
// 操作方式默认=手柄直接操作(旧规格不带操作附件信息时,取手柄直操行;系列无手柄行则保留全部)
|
||
if (hits.length && 有操作维度) {
|
||
var 手柄 = hits.filter(function (r) { return String(r.oper).indexOf("手柄") >= 0 || String(r.oper).indexOf("直接") >= 0; });
|
||
if (手柄.length) hits = 手柄;
|
||
}
|
||
if (!hits.length) return;
|
||
// 价格唯一性:同维度命中行价格不一致 → 先按"默认配置"消歧,仍不唯一才跳过
|
||
var prices = {};
|
||
hits.forEach(function (r) { prices[r.price] = 1; });
|
||
var pk = Object.keys(prices);
|
||
if (pk.length > 1) {
|
||
// v3 默认配置偏好:多价歧义常见于 过压保护/远程附件/选用说明 等未识别维度——
|
||
// 优先取"未识别维度全为 无/不带/标准"的行,过滤后价格唯一即采纳(方案表标注);仍多价才跳过
|
||
var 默认行 = hits.filter(function (r) {
|
||
return r.unnamed.every(function (t) { return /无|不带|标准|普通|缺省|—/.test(t); });
|
||
});
|
||
var dp = {};
|
||
默认行.forEach(function (r) { dp[r.price] = 1; });
|
||
if (Object.keys(dp).length === 1) {
|
||
方案.push({ cand: cand, row: 默认行[0], hits: 默认行.length, 默认注: "按标准/无附件配置(过压保护等附加项取无)" });
|
||
return;
|
||
}
|
||
方案.push({ cand: cand, 歧义: pk.join("/") });
|
||
return;
|
||
}
|
||
方案.push({ cand: cand, row: hits[0], hits: hits.length });
|
||
});
|
||
var 歧义方案 = 方案.filter(function (x) { return x.歧义; });
|
||
var 实方案 = 方案.filter(function (x) { return x.row; });
|
||
if (!实方案.length) {
|
||
var why2;
|
||
if (歧义方案.length) why2 = "多价歧义(" + 歧义方案[0].歧义 + "),需人工确认";
|
||
else {
|
||
var best = P.cands[0];
|
||
why2 = "系列内无全维度匹配:" + 匹配诊断(best, rows) + (P.cands.length > 1 ? "(候选解" + P.cands.length + "个均未命中)" : "") + (有码维度 ? "" : "(该系列型号不带附件码)") + fixedNote;
|
||
}
|
||
skip.push({ guige: t.guige, fenlei: t.fenlei, why: why2 });
|
||
continue;
|
||
}
|
||
// 多个候选解都命中且新规格不同 → 歧义跳过
|
||
var specs = {};
|
||
实方案.forEach(function (x) { specs[组装新规格(x.cand, x.row)] = 1; });
|
||
if (Object.keys(specs).length > 1) {
|
||
skip.push({ guige: t.guige, fenlei: t.fenlei, why: "粘连多解均命中(" + Object.keys(specs).join(" / ") + "),需人工确认" + fixedNote });
|
||
continue;
|
||
}
|
||
var x = 实方案[0];
|
||
done.push({
|
||
old: t.guige, cand: x.cand, row: x.row, neu: 组装新规格(x.cand, x.row),
|
||
price: x.row.price, hits: x.hits,
|
||
note: (x.cand.code ? 码解读(x.cand.code, x.cand.codeC) : 微型解读(x.cand))
|
||
+ (x.默认注 ? " [" + x.默认注 + "]" : "")
|
||
+ (t.tuiDuan ? " ⚠️推断(fenlei=null,解析认定)" : "")
|
||
+ (fixedNote || "")
|
||
});
|
||
}
|
||
|
||
/// v3 微型要素解读(方案表展示用)
|
||
function 微型解读(c) {
|
||
var s = [];
|
||
if (c.poles) s.push(c.poles);
|
||
if (c.curve) s.push(c.curve + "型");
|
||
if (c.dl) s.push(c.dl + "A");
|
||
if (c.ma) s.push(c.ma + "mA");
|
||
return s.length ? "微型 " + s.join(" ") : "微型";
|
||
}
|
||
|
||
function 组装新规格(cand, row) {
|
||
var shortName = hit.name.replace(/系列.*/, "");
|
||
// v3 微型:规范型号串=系列短名 + 各选项 nameStr 按 nameSort 顺序拼接(与元件库页面拼装同源,
|
||
// 2026-09-26 从页面 chunk 逆向确认;nameStr 自带前后空格,直接连接后压空白)
|
||
if (!row.code && row.nameParts.length) {
|
||
return (shortName + row.nameParts.map(function (p) { return p.str; }).join("")).replace(/\s+/g, " ").trim();
|
||
}
|
||
var kjShow = row.kj || cand.kj;
|
||
var parts = shortName + (kjShow ? "-" + kjShow : "") + (row.fenhe || "");
|
||
if (row.code) parts += "/" + row.code;
|
||
parts += (row.dl || cand.dl) ? " " + (row.dl || cand.dl) + "A" : "";
|
||
if (cand.js && !row.code) parts += " " + cand.js;
|
||
var jx = normJx(cand.jiexian);
|
||
if (jx !== "板前") parts += " " + jx;
|
||
return parts.replace(/\s+/g, " ").trim();
|
||
}
|
||
|
||
// ===== 输出 / 提交 =====
|
||
console.log("");
|
||
console.log(DRY ? "=== 方案表(dry 预览,未改库;确认后 --exec 执行)===" : "=== 执行替换 ===");
|
||
if (DRY) {
|
||
done.forEach(function (d) {
|
||
console.log("| " + d.old + " | " + d.note + " | " + d.neu + " | 表价" + d.price.toFixed(2) + " | 系数" + xishu + " | 命中" + d.hits + "行组合 |");
|
||
});
|
||
if (skip.length) {
|
||
console.log("=== 跳过 " + skip.length + " 行(匹配不上就不替换)===");
|
||
skip.forEach(function (k) { console.log("| " + k.guige + " | " + (k.fenlei || "?") + " | " + k.why + " |"); });
|
||
}
|
||
console.log("");
|
||
console.log("共 " + done.length + " 行可替换 / " + skip.length + " 行跳过。");
|
||
return;
|
||
}
|
||
|
||
// --exec:一次 /api/replacebatch;失败项回落逐行 /api/replace
|
||
var items = done.map(function (d) {
|
||
return {
|
||
jiu_mingcheng: d.ming || "", jiu_guige: d.old,
|
||
xin_pinming: dustry, xin_guige: d.neu, xin_biaojia: d.price, xin_xishu: xishu,
|
||
xin_pinpai: hit.factory, xin_xilie: hit.treeName || hit.name
|
||
};
|
||
});
|
||
var 批 = { items: [] };
|
||
var resp = null;
|
||
if (items.length) {
|
||
try {
|
||
resp = JSON.parse(await postJson("http://127.0.0.1:" + port + "/api/replacebatch?token=" + encodeURIComponent(token), { items: items }));
|
||
} catch (e) { console.error("replacebatch 请求失败: " + e.message); }
|
||
}
|
||
var okSet = {};
|
||
if (resp && resp.ok) {
|
||
(resp.results || []).forEach(function (r, idx) {
|
||
if (r && !r.error && r.rows > 0) okSet[idx] = 1;
|
||
});
|
||
}
|
||
// 失败项回落逐行(同款协议)
|
||
for (var di = 0; di < done.length; di++) {
|
||
if (okSet[di]) {
|
||
var rr = (resp.results || [])[di] || {};
|
||
console.log("| " + done[di].old + " | " + done[di].neu + " | 表价" + done[di].price.toFixed(2) + " | 影响" + rr.rows + "行 | 批量命中 |");
|
||
continue;
|
||
}
|
||
var it = items[di];
|
||
var r2 = null;
|
||
try { r2 = JSON.parse(await postJson("http://127.0.0.1:" + port + "/api/replace?token=" + encodeURIComponent(token), {
|
||
jiu_mingcheng: it.jiu_mingcheng, jiu_guige: it.jiu_guige,
|
||
xin_pinming: it.xin_pinming, xin_guige: it.xin_guige, xin_biaojia: it.xin_biaojia,
|
||
xin_xishu: it.xin_xishu, xin_pinpai: it.xin_pinpai, xin_xilie: it.xin_xilie
|
||
})); } catch (e) { }
|
||
if (r2 && r2.ok && r2.rows > 0) console.log("| " + done[di].old + " | " + done[di].neu + " | 表价" + done[di].price.toFixed(2) + " | 影响" + r2.rows + "行 | 回落单条 |");
|
||
else skip.push({ guige: done[di].old, fenlei: "", why: "接口拒绝: " + ((r2 && r2.error) || (resp && resp.error) || "未知") });
|
||
}
|
||
if (skip.length) {
|
||
console.log("=== 跳过/失败 " + skip.length + " 行 ===");
|
||
skip.forEach(function (k) { console.log("| " + k.guige + " | " + (k.fenlei || "?") + " | " + k.why + " |"); });
|
||
}
|
||
console.log("");
|
||
console.log("完成:执行 " + done.length + " 项,跳过/失败 " + skip.length + " 项。");
|
||
}
|
||
|
||
// ================= 离线自测(node xuanxing.js --selftest) =================
|
||
function selftest() {
|
||
var fail = 0;
|
||
function eq(name, got, want) {
|
||
var ok = JSON.stringify(got) === JSON.stringify(want);
|
||
if (!ok) { fail++; console.log("✗ " + name + ": got " + JSON.stringify(got) + " want " + JSON.stringify(want)); }
|
||
else console.log("✓ " + name);
|
||
}
|
||
eq("码33002合法", (function () { var c = 拆附件码("33002"); return [c.ok, c.js, c.tk, c.yt]; })(), [true, "3P", "3", "2"]);
|
||
eq("码32008非法(用途位)", 拆附件码("32008").ok, false);
|
||
eq("码32008原因含'用途'", 拆附件码("32008").why.indexOf("用途") >= 0, true);
|
||
eq("码3300合法", (function () { var c = 拆附件码("3300"); return [c.ok, c.js, c.tk]; })(), [true, "3P", "3"]);
|
||
eq("码3200合法(电磁)", 拆附件码("3200").tk, "2");
|
||
eq("码3320合法", 拆附件码("3320").ok, true);
|
||
eq("码3308合法", 拆附件码("3308").ok, true);
|
||
eq("码3400非法(个位4)", 拆附件码("3400").ok, false);
|
||
eq("码3900非法(脱扣9)", 拆附件码("3900").ok, false);
|
||
eq("码320/328合法(NM2)", [拆附件码("320").ok, 拆附件码("328").ok], [true, true]);
|
||
eq("码330/338非法(NM2无此码)", [拆附件码("330").ok, 拆附件码("338").ok], [false, false]);
|
||
eq("码AX/SHT合法(字母)", [拆附件码("AX").ok, 拆附件码("SHT").ok], [true, true]);
|
||
eq("码AB非法(字母白名单)", 拆附件码("AB").ok, false);
|
||
eq("码10Y合法(预付费)", 拆附件码("10Y").ok, true);
|
||
eq("码3300200非法(超5位)", 拆附件码("3300200").ok, false);
|
||
var p1 = parseSpecAll("CDM3S-250F/3300200A");
|
||
eq("3300200A唯一解=3300+200", p1.cands.map(function (c) { return c.code + "|" + c.dl; }), ["3300|200"]);
|
||
eq("3300200A壳架250分断F", [p1.base.kj, p1.base.fenhe], [250, "F"]);
|
||
var p2 = parseSpecAll("CDM3-63C/3200863A");
|
||
eq("3200863A无合法候选(码32008非法)", p2.cands.length, 0);
|
||
var x1 = parseSpecAll("XT4N250/3340 200A");
|
||
eq("ABB XT4N250 壳架=250", x1.base.kj, 250);
|
||
eq("XT /3340 码合法", x1.cands.map(function (c) { return c.code; }), ["3340"]);
|
||
var x2 = parseSpecAll("NSX100F TMD 100A 3P3D");
|
||
eq("施耐德 NSX100F 壳架=100(通用兜底)", x2.base.kj, 100);
|
||
eq("NSX 电流/极数(无码→fix 由 LLM 翻译)", [x2.cands[0].dl, x2.cands[0].js, x2.cands[0].code], [100, "3P", ""]);
|
||
eq("XT2N160 壳架=160", parseSpecAll("XT2N160 TMA 160 3p").base.kj, 160);
|
||
eq("壳架1000 支持", parseSpecAll("NM8-1000/3300 800A").base.kj, 1000);
|
||
eq("B曲线识别(施耐德微型)", (function () { var c = parseSpecAll("iC65N B16 1P").cands[0]; return [c.curve, c.dl]; })(), ["B", 16]);
|
||
eq("3200863A记录非法码", p2.invalid.map(function (x) { return x.code; }), ["32008"]);
|
||
var p3 = parseSpecAll("NXM-63S/33002 63A 板后接线");
|
||
eq("标准串33002+板后", p3.cands.map(function (c) { return c.code + "|" + c.dl + "|" + c.js; }), ["33002|63|3P"]);
|
||
eq("标准串接线后缀", p3.base.jiexian, "板后接线");
|
||
eq("标准串壳架", p3.base.kj, 63);
|
||
var p4 = parseSpecAll("CDM3S-160S/3300140A");
|
||
eq("3300140A=3300+140(140∈档位)", p4.cands.map(function (c) { return c.code + "|" + c.dl; }), ["3300|140"]);
|
||
var p5 = parseSpecAll("NXM-630S/33002500A");
|
||
eq("33002500A=33002+500(2500超壳架)", p5.cands.map(function (c) { return c.code + "|" + c.dl; }), ["33002|500"]);
|
||
var p6 = parseSpecAll("DZ47sLE 1P+N C16 30mA");
|
||
eq("微型无码串仍可解析", [p6.cands.length, p6.cands[0] && p6.cands[0].js, p6.cands[0] && p6.cands[0].dl], [1, "1P", 16]);
|
||
// 合成系列字典全维度匹配测试:33002 行不得被 3300 串命中
|
||
var props = [
|
||
{ propId: 1, propName: "壳架电流" }, { propId: 2, propName: "分断能力" }, { propId: 3, propName: "操作方式" },
|
||
{ propId: 4, propName: "极数" }, { propId: 5, propName: "脱扣器" }, { propId: 6, propName: "附件代号1" },
|
||
{ propId: 7, propName: "附件代号2" }, { propId: 8, propName: "用途" }, { propId: 9, propName: "额定电流" }, { propId: 10, propName: "接线方式" }
|
||
];
|
||
var opts = [
|
||
{ propId: 1, optId: 56, optName: "63A" }, { propId: 2, optId: 24, optName: "S 25kA" }, { propId: 3, optId: 22, optName: "手柄直接操作" },
|
||
{ propId: 4, optId: 15, optName: "3【3P】" }, { propId: 5, optId: 17, optName: "3【热磁式】" },
|
||
{ propId: 6, optId: 18, optName: "0【无附件】" }, { propId: 7, optId: 19, optName: "0【无附件】" },
|
||
{ propId: 8, optId: 74, optName: "【配电保护】" }, { propId: 8, optId: 75, optName: "2【电动机保护】" },
|
||
{ propId: 9, optId: 33, optName: "63A" }, { propId: 10, optId: 27, optName: "板前接线" }
|
||
];
|
||
var prices = [
|
||
{ optStr: "56-24-22-15-17-18-19-74-33-27", compPrice: "322" },
|
||
{ optStr: "56-24-22-15-17-18-19-75-33-27", compPrice: "322" }
|
||
];
|
||
var E = 展开价格行(props, opts, prices);
|
||
eq("合成行码=3300/33002", E.rows.map(function (r) { return r.code; }), ["3300", "33002"]);
|
||
var c3300 = parseSpecAll("NXM-63S/3300 63A").cands[0];
|
||
var c33002 = parseSpecAll("NXM-63S/33002 63A").cands[0];
|
||
eq("3300串命中3300行", E.rows.filter(function (r) { return !全维度匹配(c3300, r); }).map(function (r) { return r.code; }), ["3300"]);
|
||
eq("33002串命中33002行(不被3300行吞)", E.rows.filter(function (r) { return !全维度匹配(c33002, r); }).map(function (r) { return r.code; }), ["33002"]);
|
||
var c板后 = parseSpecAll("NXM-63S/3300 63A 板后接线").cands[0];
|
||
eq("接线不匹配被拒", E.rows.filter(function (r) { return !全维度匹配(c板后, r); }).length, 0);
|
||
// ===== v3 微型断路器:合成系列(极数含+N/曲线/电流/漏电,无码维度) =====
|
||
var mprops = [
|
||
{ propId: 1, propName: "极数" }, { propId: 2, propName: "曲线类型" },
|
||
{ propId: 3, propName: "额定电流" }, { propId: 4, propName: "剩余动作电流" }
|
||
];
|
||
var mopts = [
|
||
{ propId: 1, optId: 1, optName: "1P+N(N极可开闭)", nameStr: " 1P+N(N极可开闭)", nameSort: 2 },
|
||
{ propId: 1, optId: 2, optName: "2P", nameStr: " 2P", nameSort: 2 },
|
||
{ propId: 2, optId: 3, optName: "C型", nameStr: " C", nameSort: 3 },
|
||
{ propId: 2, optId: 4, optName: "D型", nameStr: " D", nameSort: 3 },
|
||
{ propId: 3, optId: 5, optName: "16A", nameStr: " C16", nameSort: 4 },
|
||
{ propId: 4, optId: 6, optName: "30mA", nameStr: "", nameSort: 5 },
|
||
{ propId: 4, optId: 7, optName: "300mA", nameStr: "", nameSort: 5 }
|
||
];
|
||
var mprices = [
|
||
{ optStr: "1-3-5-6", compPrice: "76.09" },
|
||
{ optStr: "1-4-5-6", compPrice: "76.84" },
|
||
{ optStr: "1-3-5-7", compPrice: "90" },
|
||
{ optStr: "2-3-5-6", compPrice: "60" }
|
||
];
|
||
var M = 展开价格行(mprops, mopts, mprices);
|
||
eq("微型行无码", M.rows.every(function (r) { return !r.code; }), true);
|
||
eq("微型行要素提取(极数N/曲线/mA)", [M.rows[0].poles, M.rows[0].curve, M.rows[0].ma, M.rows[0].dl], ["1P+N", "C", 30, 16]);
|
||
var m1 = parseSpecAll("DZ47PLE 1P+N C16 30mA").cands[0];
|
||
eq("微型解析(1P+N/C/30mA)", [m1.poles, m1.curve, m1.ma, m1.dl], ["1P+N", "C", 30, 16]);
|
||
var 乱码 = parseSpecAll("D747sLE 1P+N C16 300mA").cands[0];
|
||
eq("乱码行解析(300mA)", [乱码.poles, 乱码.curve, 乱码.ma], ["1P+N", "C", 300]);
|
||
eq("微型C16/30mA 命中76.09", M.rows.filter(function (r) { return !全维度匹配(m1, r); }).map(function (r) { return r.price; }), [76.09]);
|
||
var m2 = parseSpecAll("DZ47sLE 1P+N D16 30mA").cands[0];
|
||
eq("微型D型区分(命中76.84不串C型)", M.rows.filter(function (r) { return !全维度匹配(m2, r); }).map(function (r) { return r.price; }), [76.84]);
|
||
eq("微型300mA区分", M.rows.filter(function (r) { return !全维度匹配(乱码, r); }).map(function (r) { return r.price; }), [90]);
|
||
var m3 = parseSpecAll("DZ47 2P C16 30mA").cands[0];
|
||
eq("微型2P区分(不带N)", M.rows.filter(function (r) { return !全维度匹配(m3, r); }).map(function (r) { return r.price; }), [60]);
|
||
var m4 = parseSpecAll("DZ47 1P+N C16").cands[0];
|
||
eq("cand缺漏电档→匹配层放行(30/300mA行都过,交价格消歧)", M.rows.filter(function (r) { return !全维度匹配(m4, r); }).length, 2);
|
||
console.log(fail ? "\n自测失败 " + fail + " 项" : "\n自测全部通过");
|
||
process.exit(fail ? 1 : 0);
|
||
}
|
||
if (args.indexOf("--selftest") >= 0) selftest();
|
||
else main().catch(function (e) { console.error("脚本异常: " + (e && e.message || e)); process.exit(1); });
|