Files
ctbjrj/ai助手/zcode预置/skills/dqb-daoru-baojia/daoru.js
T

244 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 成套报价 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);
});