@@ -0,0 +1,931 @@
// 成套报价 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 ) ; } ) ;