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