Files
ctbjrj/ai助手/zcode预置/skills/dqb-xuanxing/xuanxing.js
T

932 lines
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 成套报价 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); });