// 成套报价 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= 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); });