基线:全量源码首提(D1 版本控制落地,含第一轮优化 B1-B4/A1/A4/缩放修复)

This commit is contained in:
dqb-dev
2026-10-01 22:15:22 +08:00
commit 1e65792b09
499 changed files with 204579 additions and 0 deletions
@@ -0,0 +1,115 @@
---
name: dqb-daoru-baojia
description: 成套报价软件"导入报价"技能——把用户拖进 AI 助手对话框的甲方报价 Excel(.xlsx)由 AI 看表确定列含义后导入当前项目。当用户说"导入报价""把这个报价表/Excel导进去""解析这份报价单并导入""拖给你的报价文件帮我导一下""这是甲方报价,导入软件"等,或用户直接拖入一个报价 .xlsx 文件要求处理时使用。AI 负责:看表(excelgrid)→确定每列是柜号/名称/型号/单位/数量(mapping)→预览→用户确认后导入;软件负责按映射确定性解析、事务落库和界面刷新(与软件"导入报价"按钮同源)。
---
# 报价表拖入导入(AI 看表定列 → 软件同源解析落库)
不同甲方的报价表样千差万别(列位漂移、sheet 名不同、有无规格列……)。
本技能让 **AI(LLM) 看表判断结构**,软件按 AI 给出的列映射**确定性解析+事务落库**——
数据不经过 AI 转写,不会错位。软件端与手工点"导入报价"按钮 100% 同一条管线。
## 铁律
1. **必须先预览,用户明确确认后才 `--exec` 真导入**;禁止拿到文件直接导入。
2. **路径必须用用户拖入文件的真实绝对路径**(会话中附件给出的本地路径,原样使用;含空格/中文要加引号)。
3. **只支持 .xlsx**;.xls 老格式让用户先用 Excel 另存为 .xlsx。
4. **不要自己读 Excel 内容/手工转写数据**——AI 只看表头定列,行数据全部由软件按映射解析。
5. **导入是追加,不是覆盖,软件有防重复拦截**:预览响应里的 `existing` 非空 = 项目里已有同名图号
(这份文件很可能导入过)。此时**必须先把 existing 清单亮给用户**,说明"再导会产生双份数据",
并问用户怎么办:同一份报价就不要再导 / 用户在软件里删掉旧图号后再导 / 用户明确同意重复才可加
`--allow-duplicate`。**软件在重名时会直接拒绝 `--exec`(duplicate 拦截)**,不要反复重试。
6. 不执行任何直接 SQL;一切走脚本(已封装 /api/excelgrid 与 /api/importquote)。
## 前置自检(第一步必做)
```bash
TOKEN=$(cat ~/.zcode/chengtao/token.txt)
curl -s "http://127.0.0.1:18790/api/xuanxing?token=$TOKEN"
```
- 返回 `ok:false` 且含"没有打开项目"→ **请用户先在软件项目列表双击进入项目**。
- 读不到令牌/端口不通 → 软件没运行,请用户先打开成套报价软件(daoru.js 会自动扫 18790~18799)。
## 工作流(四步)
### ① 看表:列出工作表 → 看表头网格
```bash
node "<本技能目录>/daoru.js" "<拖入xlsx绝对路径>" --grid
node "<本技能目录>/daoru.js" "<同一个xlsx绝对路径>" --grid "屏柜汇总表" 14
node "<本技能目录>/daoru.js" "<同一个xlsx绝对路径>" --grid "屏柜分项表" 14
```
输出带行号/列号的网格文本。**AI 据表头文字判断**:哪个 sheet 是箱柜汇总、哪个是元件明细、
每列是什么(柜号/箱柜名称/型号/单位/数量…)、数据从哪行开始。
### ② 定列:写 mapping.json
根据①看到的表头写映射文件(0 基列号;该表没有的列写 -1):
```json
{
"summarySheet": "屏柜汇总表",
"detailSheet": "屏柜分项表",
"summary": { "seq": 0, "cabinet": 1, "name": 2, "model": 3, "spec": -1, "unit": 4, "qty": 5 },
"detail": { "seq": 0, "name": 1, "spec": 2, "unit": 3, "qty": 4, "price": 5, "amount": 6, "vendor": 7 }
}
```
- summary 键:seq序号 cabinet柜号 name箱柜名称 model型号 spec规格 unit单位 qty数量
- detail 键:seq序号 name元件名称 spec型号规格 unit单位 qty数量 price单价 amount金额 vendor厂家
- **逐列核对**:以表头文字为准,不要套用默认列号(例:有的表"单位"在 E 列=4,有的在 F 列=5)。
- **特例免 mapping**:sheet 名为"设备汇总表/设备明细表"(正泰识图格式)时软件自动识别,直接走③。
### ③ 预览(不落库)
```bash
node "<本技能目录>/daoru.js" "<xlsx绝对路径>" --mapping "<mapping.json路径>"
```
输出:图号数、箱号数、元件总数,每个图号下的箱柜清单(柜号、箱名、型号、单位、数量、元件条数)。
**AI 要抽查核对**:拿网格里看到的某台柜(如 G1 高压进线柜 单位=台 数量=1)与预览输出对得上才算数;
对不上说明列映射错了,回①重新看表改 mapping。
### ④ 用户确认后真导入
用户明确回复"导入/确认/可以"后:
```bash
node "<本技能目录>/daoru.js" "<同一个xlsx绝对路径>" --mapping "<mapping.json路径>" --exec
```
成功输出:`✅ 报价已导入当前项目:图号 N 个、箱号 N 个、元件 N 条;软件界面已自动刷新。`
导入完成后软件自动保存未完成编辑、清缓存并重算汇总,**不需要再做任何刷新操作**。
## 汇报格式建议
```
📄 已看表并解析报价表:<文件名>
列映射:汇总表"单位"=E列、"数量"=F列(按实际表头判断)
识别到:图号 N 个 / 箱柜 N 台 / 元件 N 条
1. <图号名>(M 台)
- G1 高压进线柜 HXGN-12(1台,元件 n 条)
- …
⚠️ 导入是追加式,重复导入会产生重复数据。
尚未导入,确认导入吗?
```
完成:
```
✅ 已导入:图号 N 个、箱柜 N 台、元件 N 条,软件界面已自动刷新。
```
## 排错
| 现象 | 处理 |
|---|---|
| 文件不存在 | 确认拖入文件路径是否可访问,请用户重新拖入 |
| 仅支持 .xlsx | .xls → Excel 另存为 .xlsx;其他格式(csv/pdf/图片)不走本技能 |
| 当前没有打开项目 | 请用户双击进入项目后再说"继续导入" |
| 预览 0 图号/箱号 或 数量明显不对 | 列映射错位——回①重新看表头,逐列核对 mapping 后再预览 |
| 预览/导入出现 existing 或 ⛔duplicate | 项目里已有同名图号(文件可能导过)——按铁律5:亮出清单问用户,绝不默默重复导入 |
| sheet 名不含"汇总/分项"字样 | 看表判断哪个 sheet 是箱柜清单、哪个是元件明细,把真实 sheet 名写进 mapping |
| 找不到接口 | 软件未运行;启动软件并进入项目后重试 |
@@ -0,0 +1,243 @@
// 成套报价 AI 拖入报价表 → 看表定列 → 调软件"导入报价"功能(v2,2026-09-29)
// ★v2 核心:不同甲方的报价表样千差万别(列位漂移、sheet 名不同、无规格列等)。
// 解析列位由 AI(LLM) 看表后确定(--grid 看网格 → 写 mapping.json → --mapping 交给软件),
// 软件按映射确定性解析+事务落库(与手工点"导入报价"按钮同一条管线),数据零转写不错位。
//
// 用法(四步流程):
// ① node daoru.js <xlsx> --grid # 列出工作表清单(名+行数)
// ② node daoru.js <xlsx> --grid <sheet名> [行数] # 看某表前 N 行网格(默认 14 行,AI 据此判断列含义)
// ③ node daoru.js <xlsx> --mapping <mapping.json> # 按 AI 定好的列映射预览(不落库)
// ④ node daoru.js <xlsx> --mapping <mapping.json> --exec # 用户确认后真实导入
// 兜底:node daoru.js <xlsx> # 不带 mapping,软件自动识别表头(识别不了再走①-④)
//
// mapping.json 结构(0 基列号;-1 或省略=该表没有此列):
// {
// "summarySheet": "屏柜汇总表", // 汇总 sheet 名(箱柜清单)
// "detailSheet": "屏柜分项表", // 明细 sheet 名(每柜元件)
// "summary": { "seq":0, "cabinet":1, "name":2, "model":3, "spec":-1, "unit":4, "qty":5 },
// "detail": { "seq":0, "name":1, "spec":2, "unit":3, "qty":4, "price":5, "amount":6, "vendor":7 }
// }
// summary 键含义: seq序号 cabinet柜号 name箱柜名称 model型号 spec规格 unit单位 qty数量
// detail 键含义: seq序号 name元件名称 spec型号规格 unit单位 qty数量 price单价 amount金额 vendor厂家
// ★若 sheet 名是"设备汇总表/设备明细表"(正泰识图格式),软件自动识别,无需 mapping 直接预览。
//
// ★安全铁律:默认只预览;必须先把预览结果给用户看,用户明确确认后才允许 --exec。
// ★导入是追加式:同一文件重复导入会产生重复数据。
var http = require("http");
var fs = require("fs");
var path = require("path");
var os = require("os");
var args = process.argv.slice(2);
var EXEC = args.indexOf("--exec") >= 0;
var ALLOW_DUP = args.indexOf("--allow-duplicate") >= 0;
var XLSX = "";
var SHEET = "";
var GRIDROWS = 14;
var MAPPING_FILE = "";
// 参数解析:<xlsx> [--grid [sheet] [rows]] [--mapping file] [--exec]
for (var i = 0; i < args.length; i++) {
var a = args[i];
if (a === "--exec") continue;
if (a === "--grid") {
if (i + 1 < args.length && args[i + 1].indexOf("--") !== 0) { SHEET = args[++i]; }
if (i + 1 < args.length && args[i + 1].indexOf("--") !== 0 && /^\d+$/.test(args[i + 1])) { GRIDROWS = parseInt(args[++i], 10); }
continue;
}
if (a === "--mapping") { if (i + 1 < args.length) MAPPING_FILE = args[++i]; continue; }
if (a.indexOf("--") !== 0 && !XLSX) XLSX = a;
}
if (!XLSX) {
console.error("用法: node daoru.js <报价xlsx绝对路径> [--grid [sheet名] [行数]] [--mapping mapping.json] [--exec]");
process.exit(1);
}
XLSX = path.resolve(XLSX);
// ================= HTTP 小工具(与 dqb-xuanxing/xuanxing.js 同款令牌+端口发现) =================
function get(url) {
return new Promise(function (res, rej) {
var req = http.get(url, 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.setTimeout(5000, function () { req.destroy(new Error("timeout")); });
});
}
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", "Content-Length": Buffer.byteLength(body) }
}, 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);
// 大报价表解析+落库+界面完整刷新可能较久,给 5 分钟
req.setTimeout(300000, function () { req.destroy(new Error("请求超时(300s)")); });
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~18799),请确认软件已运行");
process.exit(1);
}
function 转义(s) { return (s || "").replace(/&/g, "&amp;").replace(/\|/g, "|"); }
// ================= 输出 =================
// 打印网格:带列号/行号,AI 看着判断每列含义
function 打印网格(r) {
var grid = r.grid || [];
var cols = 0;
grid.forEach(function (row) { cols = Math.max(cols, row.length); });
console.log("【工作表 " + r.sheet + "】共 " + r.rows + " 行,下面显示前 " + grid.length + " 行 × " + cols + " 列:");
var head = " ";
for (var c = 0; c < cols; c++) head += "|" + (c < 10 ? " " : "") + c + " ";
console.log(head);
grid.forEach(function (row, i) {
var line = "行" + (i + 1 < 10 ? " " : "") + (i + 1) + ": ";
for (var c = 0; c < cols; c++) {
var v = (row[c] || "").replace(/\s+/g, " ").trim();
if (v.length > 12) v = v.slice(0, 11) + "…";
while (v.length < 6) v += " ";
line += "|" + v;
}
console.log(line);
});
console.log("");
console.log("↑ 请根据表头文字判断列含义,写 mapping.json 后用 --mapping 预览:");
console.log(' {"summarySheet":"<汇总表sheet名>","detailSheet":"<分项表sheet名>",');
console.log(' "summary":{"seq":序号列,"cabinet":柜号列,"name":名称列,"model":型号列,"spec":规格列或-1,"unit":单位列,"qty":数量列},');
console.log(' "detail":{"seq":序号列,"name":元件名列,"spec":型号规格列,"unit":单位列,"qty":数量列,"price":单价列,"amount":金额列,"vendor":厂家列}}');
console.log("(以上列号均为 0 基;没有的列写 -1。若 sheet 名是 设备汇总表/设备明细表(正泰格式)则无需 mapping 直接预览。)");
}
function 打印预览(r) {
console.log("【报价表解析预览】" + (r.file || XLSX));
console.log("共识别:图号 " + r.tuhao + " 个、箱号 " + r.xianghao + " 个、元件 " + r.yuanjian + " 条");
console.log("");
(r.groups || []).forEach(function (g, i) {
console.log("图号" + (i + 1) + ":" + g.tuhao + "(" + g.xianghaoshu + " 台箱柜)");
(g.xianghaos || []).forEach(function (x) {
var 型号段 = x.xinghao ? " " + x.xinghao : "";
var 量段 = (x.danwei || x.shuliang) ? " " + (x.shuliang || "?") + (x.danwei || "") : "";
console.log(" " + x.xianghao + " " + x.xiangming + 型号段 + 量段 + "(元件 " + x.yuanjian + " 条)");
});
});
console.log("");
console.log("以上为预览,尚未导入。请把预览给用户确认;确认后执行:node daoru.js \"" + XLSX + "\""
+ (MAPPING_FILE ? " --mapping \"" + MAPPING_FILE + "\"" : "") + " --exec");
}
// ================= 主流程 =================
async function main() {
if (!fs.existsSync(XLSX)) {
console.error("文件不存在: " + XLSX);
process.exit(1);
}
if (path.extname(XLSX).toLowerCase() !== ".xlsx") {
console.error("仅支持 .xlsx 报价文件(" + path.extname(XLSX) + ");.xls 老格式请先用 Excel 另存为 .xlsx");
process.exit(1);
}
var token = readToken();
var port = await findAppPort(token);
var api = "http://127.0.0.1:" + port;
// ① --grid:看表(AI 判断列含义用)
if (process.argv.indexOf("--grid") >= 0) {
var body = { path: XLSX };
if (SHEET) { body.sheet = SHEET; body.rows = GRIDROWS; body.cols = 12; }
var raw;
try { raw = await postJson(api + "/api/excelgrid?token=" + encodeURIComponent(token), body); }
catch (e) { console.error("请求软件接口失败: " + e.message); process.exit(1); }
var r;
try { r = JSON.parse(raw); } catch (e) { console.error("软件返回无法解析: " + raw.slice(0, 500)); process.exit(1); }
if (!r.ok) { console.error("✗ " + (r.error || "未知错误")); process.exit(1); }
if (SHEET) { 打印网格(r); }
else {
console.log("【工作表清单】" + XLSX);
(r.sheets || []).forEach(function (s) { console.log(" " + s.name + "(" + s.rows + " 行)"); });
console.log("");
console.log("下一步:node daoru.js \"" + XLSX + "\" --grid <sheet名> 14 逐个看表头定列。");
}
return;
}
// ② --mapping:读映射文件
var 映射 = null;
if (MAPPING_FILE) {
try {
映射 = JSON.parse(fs.readFileSync(path.resolve(MAPPING_FILE), "utf-8"));
} catch (e) {
console.error("读 mapping 文件失败: " + e.message);
process.exit(1);
}
}
// ③ 预览 / ④ 导入(confirm)
var raw;
try {
raw = await postJson(api + "/api/importquote?token=" + encodeURIComponent(token),
{ path: XLSX, confirm: EXEC, allow_duplicate: ALLOW_DUP, mapping: 映射 });
} catch (e) {
console.error("请求软件接口失败: " + e.message);
process.exit(1);
}
var r;
try { r = JSON.parse(raw); }
catch (e) { console.error("软件返回了无法解析的内容: " + raw.slice(0, 500)); process.exit(1); }
if (!r.ok) {
if (r.duplicate) {
// 软件防重复拦截:项目里已有同名图号。必须原样转告用户,不要重试、不要绕过
console.error("⛔ 防止重复导入:项目里已存在同名图号 " + (r.existing || []).length + " 个:");
(r.existing || []).forEach(function (t) { console.error(" - " + t); });
console.error("导入是追加式,再导会产生双份数据。");
console.error("→ 若是同一份报价重复导入:停止,不要再导。");
console.error("→ 若用户明确说仍要导入(如覆盖旧数据):先让用户在软件里删除旧图号,或用户确认后加 --allow-duplicate 重试。");
} else {
console.error("✗ " + (r.error || "未知错误"));
}
process.exit(1);
}
if (r.confirmed) {
console.log("✅ 报价已导入当前项目:图号 " + r.tuhao + " 个、箱号 " + r.xianghao
+ " 个、元件 " + r.yuanjian + " 条;软件界面已自动刷新到最新数据。");
if (r.warning) console.log("⚠️ " + r.warning);
return;
}
// 预览:若项目已有同名图号,必须先向用户亮出来
if ((r.existing || []).length > 0) {
console.log("⚠️ 注意:当前项目里已存在同名图号 " + r.existing.length + " 个:");
r.existing.forEach(function (t) { console.log(" - " + t); });
console.log("这份文件可能之前已导入过。重复导入会产生双份数据——先向用户确认处理方式,再决定是否继续。");
console.log("");
}
打印预览(r);
}
main().catch(function (e) {
console.error("执行异常: " + (e && e.message ? e.message : e));
process.exit(1);
});
@@ -0,0 +1,200 @@
---
name: dqb-xuanxing
description: 成套报价软件选型页断路器筛选修正与替换技能。当用户要求"检查/筛选选型页异常的断路器规格""把异常型号修正/替换成规范型号""用XX系列替换断路器""帮我选型"等时使用。工作方式=一次拉清单+一次 LLM 批量判断→**确定性引擎(xuanxing.js)按《规格完全解析知识库》全维度匹配定价**→一次批量接口替换。只处理塑壳断路器和微型断路器;数量列绝对不许改动;**匹配不上就不替换**。
---
# 成套报价选型页断路器筛选修正(解析走知识库、匹配定价走确定性脚本)
## ★★塑壳断路器规格完全解析知识库(解析/纠错/匹配前必须逐位读懂)
### 型号语法总图(以 NXM-63S/33002 63A 板后接线 为例,从左到右)
| 段 | 实例 | 含义 | 说明 |
|---|---|---|---|
| 系列前缀 | NXM | 系列 | 与品牌共同定位目标系列 |
| 壳架 | -63 | 壳架等级电流 | 标准档 16/20/32/63/100/125/140/160/250/400/630/800/1250/1600 |
| 分断代号 | S | 分断能力等级字母 | S/H/J/C…(紧贴壳架数字后),基准价主要决定项 |
| 附件码 | /33002 | 极数+脱扣+附件+用途 | 逐位见下表,**位数因系列而异** |
| 额定电流 | 63A | 整定电流 | 必须 ≤ 壳架 |
| 接线后缀 | 板后接线 | 接线方式 | 板前=无后缀(默认);**价差大**,见接线附件表 |
### 附件码逐位语义表(★用户 2026-09-25 确认:**第 5 位是用途代号,不是脱扣方式**)
| 位数 | 名称 | 取值域 | 含义 |
|---|---|---|---|
| 第1位 | 极数 | 2/3/4 | 2P/3P/4P(进码不打印"P"字) |
| 第2位 | 脱扣器方式 | 2=电磁式 / 3=热磁(NM1B 叫复式脱扣器) | 电子式**不进码**=独立系列名(NXMS/NXMSF 等电子式塑壳) |
| 第3位 | 附件·十位 | 0无 / 1分励脱扣器 / 2辅助触头 / 3欠压脱扣器 / 4分励+辅助 / 5分励+欠压 / 6二组辅助触头 / 7辅助+欠压 | 附件代号主体 |
| 第4位 | 附件·个位 | 0无 / 8=叠报警触头 | Y=预付费电表专用变体(10Y/48Y…) |
| 第5位 | **用途代号** | **空(不写)=配电保护 / 2=电动机保护——仅此两种,不加价** | ★第5位出现 0/1/3~9 均为**不存在的型号** |
### 各系列附件码位数制(先判系列再用对应制解读)
| 位数制 | 代表系列 | 结构 |
|---|---|---|
| 4 位(主流) | NXM/NM1/CDM/DZ20 | 极数+脱扣+附件十位+附件个位(3300/2300/3200/3320/3308/4300…) |
| 5 位 | 同上带用途 | 4 位+用途代号(实证存在:33002/23002=…电动机保护) |
| 1 位 | 老 NM10/NM1 | 0~8 附件码(极数脱扣另见规格他处) |
| 3 位 | NM2 | 300/308/320/328 = 极数+附件十位+附件个位 |
| 字母码 | NM8/NM3DC/NM3FC/NXA | AX辅助 AL报警 AXL辅助报警 SHT分励 SHTA分励+辅助 SHTB分励+辅助报警 UVT欠压 UVTD助吸欠压延时 ASUVT自吸欠压瞬时 MO电操机构 OF辅助 CC闭合电磁铁 KL钥匙锁——**整串比对,不拆位** |
### 操作附件 / 接线附件 / 电压类附件(都在价格维度里,必须核对)
- **操作附件**:手柄直接操作=无后缀(默认)|Z=转动手柄操作|P=电动操作(另有"电操电压"独立维度,选 P 才有意义)。
- **接线附件**:板前接线=无后缀(默认)|板后接线(后缀"板后接线")|插入式(后缀"插入式")。**价差大**:NXM-63S/3300 63A 板前322 / 板后555 / 插入式821。
- **电压类附件**:分励电压/欠压电压/分励欠压电压=独立选型维度,影响组合价格但**不进型号串**(附件码含 1/3/5/7 分励欠压时才有意义)。
- **附件变价实例**:3300→3320(辅助触头)+41元;3300→3308(报警触头)+41元;**用途位不加价**(3300 与 33002 同价)。
### 实例判读库(正反例,匹配时就照这样判)
| 码 | 判读 | 结论 |
|---|---|---|
| 33002 | 3P+热磁+无附件+**电动机保护** | ✓ 真实型号 |
| 32008 | 第5位=8,用途代号仅可为空/2 | ✗ **型号不存在** |
| 3300 | 3P+热磁+无附件(配电保护) | ✓ |
| 3320 | 3P+热磁+辅助触头 | ✓ |
| 3308 | 3P+热磁+报警触头 | ✓ |
| 3200 | 3P+电磁式脱扣 | ✓ |
| 23002 | 2P+热磁+无附件+电动机保护 | ✓ |
### 各品牌型号语法速查(纠错识别通吃塑壳+微型,不局限于德力西/正泰)
| 品牌系 | 型号形态 | 识别要点 |
|---|---|---|
| 国标码系(正泰 NM/NXM/NM8、德力西 CDM/CDK、常熟 CM、人民 RMM、良信 NDM、天正 TGM、环宇 HUM、DZ20) | `NM8-125S/33002 100A` | 数字附件码 1~5 位;壳架在 `-` 后;引擎原生支持 |
| ABB Tmax XT | `XT4N250/3340 200A`、`XT2N160 TMA 160 3p` | `/3340` 同国标码位语义;壳架在型号尾部(XT4N**250**,引擎通用兜底已认);TMA/TMD=热磁脱扣单元 |
| 施耐德 NSX/Compact、iC65/EA9 | `NSX100F TMD 100A 3P3D`、`iC65N B16 1P` | 壳架 NSX**100**F;TMD/MP=热磁、ETU/EtP=电子;3P3D=3极3保护(中性线带保护);微型曲线 B/C/D(B=照明防护) |
| 西门子 3VA/3VL、5SU | `3VA1113-4EE32-0AA0` | 订单号式:`3VA1`+框架规格码+`-`+结构码(极数/附件)+`-`+电压码——纠错时按西门样本翻译成 极数/电流/附件 再喂引擎 |
| 微型全系(DZ47 家族/NB/NXB/SH/iC65/5SJ/TXB…) | `DZ47sLE 1P+N C16 30mA` | 极数(含1P+N/3P+N)、曲线 C/D(施耐德系加 B)、电流、漏电 mA——引擎 v3 原生全维度匹配 |
**纠错翻译规则(阶段① LLM 把非国标格式译成标准形再喂 --fix)**:
- TMD/TMA/TM/D→热磁脱扣;ETU/EtP/E/Micrologic/电子→电子式脱扣(电子式≠附件码,进系列名)
- 3P3D/4P4D=极数+中性线带保护→译成 3P/4P(若目标系列有 N 极可开闭选项,译 3P+N 由用户确认)
- 订单号(西门子/ABB 老式)→查样本取 极数/In/附件,译成 `系列-壳架/码 电流` 形式;译不出把握的项**留空不要编**
- OCR 噪声(D747s/e30mA/*/斜杠粘连)→按特征还原(引擎已内建噪声容忍,--fix 仅需处理品牌级错误)
## ★多型号批量编排(快速替换的通用形态,不局限一两种)
一键/批量任务的标准编排(AI 负责,引擎逐组跑):
1. 拉清单 → 按行的**品牌+类型**分组(ABB 塑壳一组、德力西漏电微型一组、正泰普通微型一组…)
2. 每组选目标系列:同品牌优先(元件库 `get_data_search2` 搜系列名,德力西=factId 1/正泰=2);库里没有的品牌组 → 默认问用户替换成哪家(会话里已指定则直接用)
3. 逐组 `node xuanxing.js <品牌> <系列> --dry`(几秒一组)→ **合并各组方案表**给用户(含组名=目标系列)
4. 用户确认后逐组 `--exec`;跨品牌替换(ABB→德力西等)按知识库映射分断等级(N 36kA≈S 35kA 同级),映射不确定的行标注请用户确认
5. 库里搜不到的系列/未定价系列:该组列"未定价"跳过,不阻塞其他组
### 粘连串拆分(CAD 扒图常见:码与电流无空格粘连)
规则:`/` 后整段数字,**枚举全部切分**,电流段必须 ∈ 标准档位表 且 ≤ 壳架,码段必须通过上表语法域——切分不唯一或全部非法都按跳过处理:
| 粘连串 | 切分枚举 | 唯一解 |
|---|---|---|
| /3300200A(壳架250) | 3300+200A✓|33002+00A✗(00A非法电流) | 3300 200A |
| /3200863A(壳架63) | 3200+863A✗(863超壳架非档位)|32008+63A 切分合法但**码32008非法** | 无有效解→整行跳过"型号不存在" |
| /3300140A(壳架160) | 3300+140A✓(140∈档位) | 3300 140A |
| /33002500A(壳架630) | 3300+2500A✗(超壳架)|33002+500A✓ | 33002 500A |
## ★匹配判定铁律(用户 2026-09-25 要求,违反即事故)
1. **匹配=全维度一致**:壳架/分断代号/极数/脱扣方式/附件码(含用途位)/额定电流/接线方式,逐一命中才算匹配;**表价必须取全维度命中行的价格**。
2. **码必须能被目标系列选项字典解释**(spe_opt 选项标签是"码【含义】"双写格式,如 `2【电磁式】`、`20【辅助触头】`)——字典里没有该码=该系列无此型号→**跳过不替换,禁止就近改成相近码**(如 32008 改成 3300 去匹配)。
3. **禁止硬选**:不许电流上取一档、不许壳架升档、不许换到别的有价系列凑数。
4. **匹配不上就不替换**:跳过行必须在汇报里列明原因(如"32008 型号不存在""附件码 3320 不在 CDM3S 系列选项表")。
5. 解析不出唯一合法解的行(粘连多解且都合法、乱码)→ 跳过并报告,不提交猜测。
## ★一键入口(AI助手面板的 ⚡ 按钮)
聊天面板右下角有"⚡ 读取选型规格并替换"按钮,点击会发送固定指令:
> **读取当前选型界面的规格并且帮我替换这些规格**
收到这句(或用户手动输入同义语)= **直接执行下面完整三阶段流程**,并遵守默认范围:
- **默认只替换 塑壳断路器 和 微型断路器**;
- **其他任何类型一律不替换**(隔离开关/万能式/接触器/熔断器/互感器/浪涌/风机等非断路器,仅识别后列进跳过汇报);
- 打错/OCR 错的型号**必须先按上方知识库识别纠正**——纠错后确认属于塑壳/微断才替换;**纠错后仍认不出归属的同样不替换**;
- 替换前仍按铁律 4 给《方案表》等用户说"执行"。
**阶段①** 一次拉清单 + 一次 LLM 批量判断(禁逐行问模型)
**阶段②③** 跑确定性引擎 `node xuanxing.js <品牌> <系列> --dry` 出《方案表》(引擎内部按本知识库全维度匹配,结果稳定不随机)→ 用户确认 → `--exec` 真跑
## ★替换规则速查表(与元件库页面"替换"按钮逐条一致)
| 列 | 取值规则 |
|---|---|
| **元件名称** | `xin_pinming` = 该系列树节点的 **DustryName**(同一系列所有规格共用)。★禁止把完整型号当品名传;实在拿不到 DustryName 可省略——软件自动按系列名去"XX系列"前缀补 |
| 规格 | `xin_guige` = 规范完整型号(**壳架数字+分断字母+/附件码 电流[ 接线后缀]**,如 `CDM3S-250S/33002 200A`、`NXM-63S/3300 63A 板后接线`)——附件码绝不可丢 |
| 表价 | `xin_biaojia` = **全维度命中行**的价格(引擎已按铁律匹配;手工路径也必须逐维度核对,禁止只对三元组) |
| 采购系数 | `xin_xishu` = 该系列**在元件库页面已保存的本体折扣**÷100(localStorage 键 `__dqb折扣@<系列名>` 的 `bt` 字段;无存档=100→系数1)。★**每次任务现读,禁止沿用上次/记忆里的折扣** |
| 品牌 | `xin_pinpai` = 系列(树/搜索)返回的厂家名 |
| 系列 | `xin_xilie` = 树节点 **SeriesName 原文**(如 `CDM3S系列塑壳断路器`) |
| **数量** | ★**绝不传、绝不改**——接口会直接拒绝 |
## ★铁律
1. 只处理 fenlei 为 "塑壳"/"微型" 的行;隔离开关 HGL/万能式/接触器/熔断器/fenlei=null 一律跳过并汇报(fenlei=null 但确是打错的断路器型号→按知识库纠错后再验,认出即放行)。
2. **数量绝对不许动**。
3. 不执行任何直接 SQL;替换一律走 `/api/replacebatch`(或回落 `/api/replace`)。
4. 修改前先给用户看《方案表》,用户说"执行/替换"再跑阶段②③。
5. **解析与定价一律交给确定性引擎 xuanxing.js(v3,塑壳+微型都支持)**;接口层已接入附件码校验/表价>0/0行显式失败防线。每行只需:`node xuanxing.js 品牌 系列 --dry` → 核对 → `--exec`,禁止手工拼平表/逆向页面 JS。
## 前置
- 成套报价软件运行中且已进项目(引擎/接口随软件自动起)。
- **开始前自检(第一步必做)**:`curl -s "http://127.0.0.1:18790/api/xuanxing?token=$TOKEN"`——若返回 `ok:false` 且含"没有打开项目",**先请用户在软件项目列表双击进入项目再继续**,不要自己重试或绕过。
- 若 2c 读折扣时 `/api/page` 返回"元件库页面未就绪"——请用户先点开软件的「元件库」页签(页面加载一次即可),再重试。
- 令牌:`cat ~/.zcode/chengtao/token.txt`(主程序启动时自动生成,无需解析配置)
- 主程序接口 http://127.0.0.1:18790(全部 ?token=);元件库数据 http://127.0.0.1:8080。
## 阶段①:筛选异常断路器行(秒级)
```bash
curl -s "http://127.0.0.1:18790/api/xuanxing?token=$TOKEN"
```
**1a. 正则快筛可疑特征**(缩小范围,不用逐行思考):
- 电流粘连:`/3300` 或 `/4300` 后紧跟数字无空格(如 `CDM3S-400F/3300400A`)
- 壳架≥2000 或非标准档
- 极数缺失或写法混乱(无 P 也无 /3xxx /4xxx)
- 前缀大小写混乱(CDM3s/CDM3S)、多余中文后缀、空格异常、乱字符(`DZ4/PLE`、`e30mA`)
- **fenlei=null 但含断路器特征**(DZ/NM/NB/CM/NS 碎段、[1-4]P、C/D+数字、数字A、30mA 漏电标识)
**1b. LLM 批量理解判断+纠错**(按知识库逐位解读,禁逐行问模型):
对每个可疑行回答:①是不是塑壳/微型断路器 ②异常类型 ③**纠错出规范完整型号**(按语法总图+逐位语义表拆:极数/脱扣/附件/用途逐位写清,如 `DZ4/PLE 1P+N C16A e30mA`→德力西漏电微型 `DZ47sLE 1P+N C16 30mA`)④壳架/极数/电流。
★附件码按"粘连串拆分"节枚举切分,切不出唯一合法解就标记跳过,不许猜。
**纠错后的规范型号必须能被软件分类表认出**——认不出说明纠错了,别提交。
产出方案给用户确认:
```
| 原规格 | 判断 | 异常 | 纠错后型号(逐位解读) |
```
## 阶段②③:确定性引擎匹配定价 + 批量替换(主路径)
**先跑 dry 出《方案表》**(引擎 v3 **原生支持塑壳+微型断路器**:塑壳按附件码全维度;微型按 极数(含1P+N/N极可开闭)/脱扣曲线(C·D)/额定电流/漏电mA 全维度,规范型号串按元件库页面 nameStr 规则自动拼装,含 OCR 噪声(e/*/斜杠)行与 fenlei=null 推断行;多配置价差自动取"无附件/标准"档并标注):
```bash
node "<本技能目录>/xuanxing.js" <品牌关键词> <系列关键词> --dry
# 例:node ~/.zcode/skills/../.zcode/skills/dqb-xuanxing/xuanxing.js 德力西 CDM3S --dry
# 微型(漏电/普通)系列同样直接跑:… 德力西 DZ47PLE --dry / … 德力西 DZ47s --dry
# 可选:--only "CDM3S-250F/3300200A" 只看一行;--fix 修正.json({"旧规格":"纠错后规格",...})
```
引擎输出:`| 旧规格 | 要素解读 | 新型号 | 表价 | 系数 | 匹配明细 |` + 跳过清单(含逐条原因:型号不存在/无全维度匹配行/多价歧义/未定价)。
**AI 逐行核对 dry 方案表**(要素解读与表价是否合理)→ 给用户看 → 用户说"执行"后:
```bash
node "<本技能目录>/xuanxing.js" <品牌关键词> <系列关键词> --exec
```
引擎 --exec 内部走一次 `POST /api/replacebatch`(单事务、折扣现读、失败项回落逐行 /api/replace)。**不要再手工拼字典/逆向页面 JS——微型与塑壳都由引擎一条命令完成,手工路径已废弃。**
提交仍走 /api/replacebatch(`xin_pinming` 必传=DustryName,协议同速查表)。
**可视化模式(可选,仅当用户要求"看着一行行替换")**:逐行 `POST /api/selectselection` + `POST /api/editselection`——慢一个量级,默认不用。
## 汇报格式
```
✅ 已批量修正 N 项(影响 M 个库行):
| 原规格(异常) | 新型号 | 码位解读 | 品名 | 表价 | 系数 | 影响 |
⏭️ 跳过/失败 K 项:| 规格 | 原因(型号不存在(码非法)/系列选项表无此码/无全维度匹配行/多解歧义/未定价系列/非断路器/接口拒绝原因) |
```
软件侧日志:`<软件目录>/logs/ai助手_gateway.log`(每次批量替换有一行汇总)。
@@ -0,0 +1,26 @@
// dqb 接口响应解码器(复刻页面 deStr+de:两字符一组 → 数值 → LZW 解压)
// spe 接口=500×(c1-903)+(c2-903);acc 接口=550×(c1-809)+(c2-809)
// 用法: cat 响应 | node dqb_decode.js (spe,默认)
// cat 响应 | node dqb_decode.js acc (acc 附件)
function deStr(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 de(t) { // LZW 解压(字典从 256 起)
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;
}
var fs = require("fs");
var isAcc = (process.argv[2] === "acc");
var input = fs.readFileSync(0, "utf-8");
var text = de(deStr(input.trim(), isAcc ? 809 : 903, isAcc ? 550 : 500));
process.stdout.write(text || "(解码失败)");
@@ -0,0 +1,45 @@
// dqb 规格接口签名生成器(复刻自 dqb 页面 chunk-e94d3322 的 nt 函数)
// 用法: node dqb_sign.js <SeriesId> → 输出完整的 s 参数值
function shift(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 sign(t, e) { // t=随机串"AAAAAA.BBBBBB", e=SeriesId 字符串
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; // 页面 gtk 分支不触发(t 非 null)
var 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;
var C = "+-a^+6", S = "+-3^+b+-f";
for (var w = 0; w < v.length; w++) { b += v[w]; b = shift(b, C); }
b = shift(b, S);
b ^= f;
if (b < 0) b = 2147483648 + (b & 2147483647);
b %= 1e6;
return b.toString() + "." + (b ^ m);
}
function rnd6() {
var d = "0123456789", r = "";
for (var i = 0; i < 6; i++) r += d.charAt(Math.floor(Math.random() * d.length));
return r;
}
var sid = process.argv[2];
var ab = rnd6() + "." + rnd6();
console.log(sid + "." + ab + "." + sign(ab, String(sid)));
@@ -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); });