基线:全量源码首提(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,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);
});