基线:全量源码首提(D1 版本控制落地,含第一轮优化 B1-B4/A1/A4/缩放修复)
This commit is contained in:
@@ -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, "&").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);
|
||||
});
|
||||
Reference in New Issue
Block a user