// 成套报价 AI 拖入报价表 → 看表定列 → 调软件"导入报价"功能(v2,2026-09-29) // ★v2 核心:不同甲方的报价表样千差万别(列位漂移、sheet 名不同、无规格列等)。 // 解析列位由 AI(LLM) 看表后确定(--grid 看网格 → 写 mapping.json → --mapping 交给软件), // 软件按映射确定性解析+事务落库(与手工点"导入报价"按钮同一条管线),数据零转写不错位。 // // 用法(四步流程): // ① node daoru.js --grid # 列出工作表清单(名+行数) // ② node daoru.js --grid [行数] # 看某表前 N 行网格(默认 14 行,AI 据此判断列含义) // ③ node daoru.js --mapping # 按 AI 定好的列映射预览(不落库) // ④ node daoru.js --mapping --exec # 用户确认后真实导入 // 兜底:node daoru.js # 不带 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 = ""; // 参数解析: [--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, "&").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 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); });