1755 lines
78 KiB
C#
1755 lines
78 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net;
|
||
using System.Net.Sockets;
|
||
using System.Text;
|
||
using System.Threading;
|
||
|
||
namespace 成套报价软件
|
||
{
|
||
/// <summary>
|
||
/// AI助手本地接口(第二阶段:AI×选型页断路器选型替换)。
|
||
/// HttpListener 监听 127.0.0.1:18790(占用则顺延,记 shezhi\AI助手.config.xml 的 接口端口)。
|
||
/// 鉴权:查询参数 token == OpenClawHost.令牌(与网关同源,仅本机可用)。
|
||
/// 接口:
|
||
/// GET /api/xuanxing?token=… → 当前项目选型页元件清单 JSON(含分类字段 fenlei:"塑壳"|"微型"|null)
|
||
/// POST /api/replace?token=… → 替换元件(body=JSON:旧名称/旧规格定位,新值 SET)
|
||
/// ★★铁律:UPDATE 的 SET 列表绝不包含"数量"列;派生价按库里原数量 CAST(数量 AS REAL) 计算;
|
||
/// 替换前强制校验目标行分类 ∈ {塑壳, 微型}(分类断路器()),其他类型/识别不到的一律拒绝。
|
||
/// 替换后刷新选型页与报价页(UI 线程 Invoke)。
|
||
/// </summary>
|
||
public partial class MainForm
|
||
{
|
||
private System.Net.HttpListener _aiHttp;
|
||
private Thread _aiHttpThread;
|
||
private int _aiHttpPort = 18790;
|
||
|
||
// ---------- 生命周期 ----------
|
||
|
||
/// <summary>启动本地接口(MainForm.Load 挂调;后台线程,失败仅记日志不弹窗)开。summary 标签见上。</summary>
|
||
internal void 启动AI助手接口()
|
||
{
|
||
try
|
||
{
|
||
_aiHttpPort = 读取接口端口();
|
||
_aiHttp = new System.Net.HttpListener();
|
||
bool 绑定成功 = false;
|
||
for (int p = _aiHttpPort; p < _aiHttpPort + 10; p++)
|
||
{
|
||
try
|
||
{
|
||
_aiHttp.Prefixes.Clear();
|
||
_aiHttp.Prefixes.Add("http://127.0.0.1:" + p + "/");
|
||
_aiHttp.Start();
|
||
_aiHttpPort = p;
|
||
绑定成功 = true;
|
||
break;
|
||
}
|
||
catch { }
|
||
}
|
||
if (!绑定成功)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 端口 " + _aiHttpPort + " 起连续 10 个绑定失败,接口未启动");
|
||
return;
|
||
}
|
||
_aiHttpThread = new Thread(AI接口轮询);
|
||
_aiHttpThread.IsBackground = true;
|
||
_aiHttpThread.Start();
|
||
OpenClawHost.写日志("[AI接口] 已启动 http://127.0.0.1:" + _aiHttpPort + "/");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 启动失败: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>停止本地接口(FormClosing 挂调)。</summary>
|
||
internal void 停止AI助手接口()
|
||
{
|
||
try
|
||
{
|
||
if (_aiHttp != null)
|
||
{
|
||
_aiHttp.Stop();
|
||
_aiHttp.Close();
|
||
_aiHttp = null;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 停止接口失败: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
private int 读取接口端口()
|
||
{
|
||
try
|
||
{
|
||
string f = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "shezhi", "AI助手.config.xml");
|
||
if (File.Exists(f))
|
||
{
|
||
string xml = File.ReadAllText(f, Encoding.UTF8);
|
||
string 开 = "<接口端口>", 关 = "</接口端口>";
|
||
int i = xml.IndexOf(开, StringComparison.Ordinal);
|
||
if (i >= 0)
|
||
{
|
||
int j = xml.IndexOf(关, i + 开.Length, StringComparison.Ordinal);
|
||
if (j > i)
|
||
{
|
||
int p;
|
||
if (int.TryParse(xml.Substring(i + 开.Length, j - i - 开.Length).Trim(), out p)
|
||
&& p >= 1024 && p <= 65535) return p;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 读端口配置失败,用默认18790: " + ex.Message);
|
||
}
|
||
return 18790;
|
||
}
|
||
|
||
// ---------- 请求循环 ----------
|
||
|
||
private void AI接口轮询()
|
||
{
|
||
var listener = _aiHttp;
|
||
while (listener != null)
|
||
{
|
||
HttpListenerContext ctx;
|
||
try { ctx = listener.GetContext(); }
|
||
catch { break; } // Stop() 后退出
|
||
try
|
||
{
|
||
string path = ctx.Request.Url.AbsolutePath.TrimEnd('/');
|
||
string token = ctx.Request.QueryString["token"] ?? "";
|
||
if (!string.Equals(token, OpenClawHost.令牌, StringComparison.Ordinal))
|
||
{
|
||
回文(ctx, 403, "{\"ok\":false,\"error\":\"令牌无效\"}");
|
||
continue;
|
||
}
|
||
if (string.Equals(path, "/api/xuanxing", StringComparison.OrdinalIgnoreCase)
|
||
&& string.Equals(ctx.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string body = AI接口_选型清单();
|
||
回文(ctx, 200, body);
|
||
}
|
||
else if (string.Equals(path, "/api/replace", StringComparison.OrdinalIgnoreCase)
|
||
&& string.Equals(ctx.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string bodyJson;
|
||
using (var r = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
|
||
{
|
||
bodyJson = r.ReadToEnd();
|
||
}
|
||
string body = AI接口_替换元件(bodyJson);
|
||
回文(ctx, 200, body);
|
||
}
|
||
else if (string.Equals(path, "/api/page", StringComparison.OrdinalIgnoreCase)
|
||
&& string.Equals(ctx.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string bodyJson;
|
||
using (var r = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
|
||
{
|
||
bodyJson = r.ReadToEnd();
|
||
}
|
||
string body = AI接口_执行页面脚本(bodyJson);
|
||
回文(ctx, 200, body);
|
||
}
|
||
else if (string.Equals(path, "/api/replaceall", StringComparison.OrdinalIgnoreCase)
|
||
&& string.Equals(ctx.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string bodyJson;
|
||
using (var r = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
|
||
{
|
||
bodyJson = r.ReadToEnd();
|
||
}
|
||
string body = AI接口_网页式全量替换(bodyJson);
|
||
回文(ctx, 200, body);
|
||
}
|
||
else if (string.Equals(path, "/api/replacebatch", StringComparison.OrdinalIgnoreCase)
|
||
&& string.Equals(ctx.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string bodyJson;
|
||
using (var r = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
|
||
{
|
||
bodyJson = r.ReadToEnd();
|
||
}
|
||
string body = AI接口_批量替换元件(bodyJson);
|
||
回文(ctx, 200, body);
|
||
}
|
||
else if (string.Equals(path, "/api/selectselection", StringComparison.OrdinalIgnoreCase)
|
||
&& string.Equals(ctx.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string bodyJson;
|
||
using (var r = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
|
||
{
|
||
bodyJson = r.ReadToEnd();
|
||
}
|
||
string body = AI接口_选中选型行(bodyJson);
|
||
回文(ctx, 200, body);
|
||
}
|
||
else if (string.Equals(path, "/api/editselection", StringComparison.OrdinalIgnoreCase)
|
||
&& string.Equals(ctx.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string bodyJson;
|
||
using (var r = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
|
||
{
|
||
bodyJson = r.ReadToEnd();
|
||
}
|
||
string body = AI接口_编辑选型行(bodyJson);
|
||
回文(ctx, 200, body);
|
||
}
|
||
else if (string.Equals(path, "/api/importquote", StringComparison.OrdinalIgnoreCase)
|
||
&& string.Equals(ctx.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string bodyJson;
|
||
using (var r = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
|
||
{
|
||
bodyJson = r.ReadToEnd();
|
||
}
|
||
string body = AI接口_导入报价(bodyJson);
|
||
回文(ctx, 200, body);
|
||
}
|
||
else if (string.Equals(path, "/api/excelgrid", StringComparison.OrdinalIgnoreCase)
|
||
&& string.Equals(ctx.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string bodyJson;
|
||
using (var r = new StreamReader(ctx.Request.InputStream, Encoding.UTF8))
|
||
{
|
||
bodyJson = r.ReadToEnd();
|
||
}
|
||
string body = AI接口_Excel网格(bodyJson);
|
||
回文(ctx, 200, body);
|
||
}
|
||
else
|
||
{
|
||
回文(ctx, 404, "{\"ok\":false,\"error\":\"未知接口 " + path + "\"}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
try { 回文(ctx, 500, "{\"ok\":false,\"error\":\"" + Json转义(ex.Message) + "\"}"); } catch { }
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void 回文(HttpListenerContext ctx, int code, string body)
|
||
{
|
||
byte[] b = Encoding.UTF8.GetBytes(body);
|
||
ctx.Response.StatusCode = code;
|
||
ctx.Response.ContentType = "application/json; charset=utf-8";
|
||
ctx.Response.ContentLength64 = b.Length;
|
||
ctx.Response.OutputStream.Write(b, 0, b.Length);
|
||
ctx.Response.OutputStream.Close();
|
||
}
|
||
|
||
// ---------- GET /api/xuanxing:选型页元件清单(含分类) ----------
|
||
|
||
/// <summary>照 LoadSelectionSummary 的查询段重新读库聚合(不碰 UI 内存表,线程安全),
|
||
/// 输出 JSON:{ok, project, rows:[{mingcheng,guige,danwei,shuliang,biaojia,caigouxishu,baochuxishu,pinpai,xilie,leixing,fenlei,xianghaoshu}]}。</summary>
|
||
/// <summary>枚举"活"元件表(2026-09-21 删除残留根治配套):只返回图号表里仍存在、且所属箱号行
|
||
/// 仍存在的元件表——删除图号/箱号历史残留的孤儿表(表索引行+物理表没清干净)不再进入
|
||
/// AI 清单/替换范围。活键判定兼容两套登记:内部ID(主窗口)与显示名(扒图 Form3 老数据)。</summary>
|
||
private static List<string> 活元件表列表(System.Data.SQLite.SQLiteConnection conn)
|
||
{
|
||
var result = new List<string>();
|
||
try
|
||
{
|
||
// 1) 活图号键集合(内部ID+显示名都收)
|
||
var liveTh = new HashSet<string>(StringComparer.Ordinal);
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT 内部ID, 图号 FROM " + TuhaoTableName + ";", conn))
|
||
using (var rd = cmd.ExecuteReader())
|
||
{
|
||
while (rd.Read())
|
||
{
|
||
string id = rd.IsDBNull(0) ? "" : rd.GetString(0);
|
||
string nm = rd.IsDBNull(1) ? "" : rd.GetString(1);
|
||
if (!string.IsNullOrWhiteSpace(id)) liveTh.Add(id);
|
||
if (!string.IsNullOrWhiteSpace(nm)) liveTh.Add(nm);
|
||
}
|
||
}
|
||
if (liveTh.Count == 0) return result;
|
||
// 2) 各图号的活箱号键集合(箱号表行仍在=活;内部ID+显示名都算活)
|
||
// 只看图号键活着的箱号索引行,箱号行读其表收两键
|
||
var liveXhByTh = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT 图号, 实际表名 FROM 表索引 WHERE 类型='箱号';", conn))
|
||
using (var rd = cmd.ExecuteReader())
|
||
{
|
||
while (rd.Read())
|
||
{
|
||
string tk = rd.IsDBNull(0) ? "" : rd.GetString(0);
|
||
string xhTbl = rd.IsDBNull(1) ? "" : rd.GetString(1);
|
||
if (string.IsNullOrWhiteSpace(tk) || !liveTh.Contains(tk) || string.IsNullOrWhiteSpace(xhTbl)) continue;
|
||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||
using (var cmdX = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT 内部ID, 箱号 FROM \"" + xhTbl.Replace("\"", "") + "\";", conn))
|
||
using (var rdX = cmdX.ExecuteReader())
|
||
{
|
||
while (rdX.Read())
|
||
{
|
||
string xid = rdX.IsDBNull(0) ? "" : rdX.GetString(0);
|
||
string xnm = rdX.IsDBNull(1) ? "" : rdX.GetString(1);
|
||
if (!string.IsNullOrWhiteSpace(xid)) set.Add(xid);
|
||
if (!string.IsNullOrWhiteSpace(xnm)) set.Add(xnm);
|
||
}
|
||
}
|
||
liveXhByTh[tk] = set;
|
||
}
|
||
}
|
||
// 3) 元件索引行:图号活 + 箱号活 才收(孤儿表彻底挡在 AI 之外)
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT 图号, 箱号, 实际表名 FROM 表索引 WHERE 类型='元件';", conn))
|
||
using (var rd = cmd.ExecuteReader())
|
||
{
|
||
while (rd.Read())
|
||
{
|
||
string tk = rd.IsDBNull(0) ? "" : rd.GetString(0);
|
||
string xk = rd.IsDBNull(1) ? "" : rd.GetString(1);
|
||
string tbl = rd.IsDBNull(2) ? "" : rd.GetString(2);
|
||
if (string.IsNullOrWhiteSpace(tk) || string.IsNullOrWhiteSpace(tbl)) continue;
|
||
if (!liveTh.Contains(tk)) continue;
|
||
HashSet<string> set;
|
||
if (!liveXhByTh.TryGetValue(tk, out set) || set == null || !set.Contains(xk)) continue;
|
||
if (!result.Contains(tbl)) result.Add(tbl);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 收集元件表清单失败(AI端将看不到报价表): " + ex.Message);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private string AI接口_选型清单()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId))
|
||
return "{\"ok\":false,\"error\":\"当前没有打开项目,请先在项目列表双击进入项目\"}";
|
||
string dbPath = GetQuoteDbPath(_currentProjectId);
|
||
if (!File.Exists(dbPath))
|
||
return "{\"ok\":false,\"error\":\"项目库文件不存在\"}";
|
||
|
||
var merged = new Dictionary<string, Dictionary<string, object>>(StringComparer.Ordinal);
|
||
try
|
||
{
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
// ★ 只枚举活元件表(图号/箱号仍在):删除残留的孤儿表不进 AI 清单
|
||
var tblNames = 活元件表列表(conn);
|
||
foreach (string realTbl in tblNames)
|
||
{
|
||
string tbl = "\"" + realTbl.Replace("\"", "") + "\"";
|
||
string sql = "SELECT 元件名称,规格,单位,数量,表价,采购系数,报出系数,品牌,系列,类型 FROM " + tbl + ";";
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
|
||
using (var rd = cmd.ExecuteReader())
|
||
{
|
||
while (rd.Read())
|
||
{
|
||
string n = 取串(rd, "元件名称");
|
||
string s = 取串(rd, "规格");
|
||
double sl = 取数(rd, "数量");
|
||
double bj = 取数(rd, "表价");
|
||
string lx = 取串(rd, "类型");
|
||
string key = n + "\u0001" + s + "\u0001" + lx + "\u0001" + bj.ToString("0.######", System.Globalization.CultureInfo.InvariantCulture);
|
||
Dictionary<string, object> row;
|
||
if (!merged.TryGetValue(key, out row))
|
||
{
|
||
row = new Dictionary<string, object>();
|
||
row["mingcheng"] = n;
|
||
row["guige"] = s;
|
||
row["danwei"] = 取串(rd, "单位");
|
||
row["shuliang"] = sl;
|
||
row["biaojia"] = bj;
|
||
row["caigouxishu"] = 取数(rd, "采购系数");
|
||
row["baochuxishu"] = 取数(rd, "报出系数");
|
||
row["pinpai"] = 取串(rd, "品牌");
|
||
row["xilie"] = 取串(rd, "系列");
|
||
row["leixing"] = lx;
|
||
row["_biaoge"] = new HashSet<string>();
|
||
((HashSet<string>)row["_biaoge"]).Add(realTbl);
|
||
merged[key] = row;
|
||
}
|
||
else
|
||
{
|
||
row["shuliang"] = Convert.ToDouble(row["shuliang"]) + sl;
|
||
((HashSet<string>)row["_biaoge"]).Add(realTbl);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return "{\"ok\":false,\"error\":\"读选型数据失败: " + Json转义(ex.Message) + "\"}";
|
||
}
|
||
|
||
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
var root = new Dictionary<string, object>();
|
||
root["ok"] = true;
|
||
root["project"] = _currentProjectId;
|
||
var list = new List<Dictionary<string, object>>();
|
||
foreach (var kv in merged)
|
||
{
|
||
var r = kv.Value;
|
||
string fenlei = 分类断路器(Convert.ToString(r["mingcheng"]), Convert.ToString(r["guige"]), Convert.ToString(r["leixing"]));
|
||
var out2 = new Dictionary<string, object>();
|
||
out2["mingcheng"] = r["mingcheng"];
|
||
out2["guige"] = r["guige"];
|
||
out2["danwei"] = r["danwei"];
|
||
out2["shuliang"] = Math.Round(Convert.ToDouble(r["shuliang"]), 6);
|
||
out2["biaojia"] = r["biaojia"];
|
||
out2["caigouxishu"] = r["caigouxishu"];
|
||
out2["baochuxishu"] = r["baochuxishu"];
|
||
out2["pinpai"] = r["pinpai"];
|
||
out2["xilie"] = r["xilie"];
|
||
out2["leixing"] = r["leixing"];
|
||
out2["fenlei"] = fenlei; // "塑壳" | "微型" | null(不许替换)
|
||
out2["xianghaoshu"] = ((HashSet<string>)r["_biaoge"]).Count; // 影响箱号数
|
||
list.Add(out2);
|
||
}
|
||
root["rows"] = list;
|
||
root["count"] = list.Count;
|
||
return js.Serialize(root);
|
||
}
|
||
|
||
// ---------- POST /api/replace:替换元件(数量铁律) ----------
|
||
|
||
/// <summary>body=JSON:{jiu_mingcheng, jiu_guige, xin_mingcheng, xin_guige, xin_biaojia,
|
||
/// xin_caigouxishu, xin_pinpai, xin_xilie, xin_danwei?}。
|
||
/// 校验分类∈{塑壳,微型}→表索引白名单表→单事务参数化 UPDATE(SET 不含数量,派生价按原数量)
|
||
/// →刷新选型页/报价页。返回 {ok,rows,fenlei}。</summary>
|
||
private string AI接口_替换元件(string bodyJson)
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId))
|
||
return "{\"ok\":false,\"error\":\"当前没有打开项目\"}";
|
||
string dbPath = GetQuoteDbPath(_currentProjectId);
|
||
if (!File.Exists(dbPath))
|
||
return "{\"ok\":false,\"error\":\"项目库文件不存在\"}";
|
||
|
||
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
Dictionary<string, object> p;
|
||
try { p = js.Deserialize<Dictionary<string, object>>(bodyJson ?? ""); }
|
||
catch { return "{\"ok\":false,\"error\":\"请求体不是合法 JSON\"}"; }
|
||
if (p == null) return "{\"ok\":false,\"error\":\"请求体为空\"}";
|
||
|
||
string jiuMing = 取参(p, "jiu_mingcheng").Trim();
|
||
string jiuGui = 取参(p, "jiu_guige").Trim();
|
||
string xinMing = 取参(p, "xin_mingcheng").Trim();
|
||
string xinGui = 取参(p, "xin_guige").Trim();
|
||
if (!p.ContainsKey("jiu_guige"))
|
||
return "{\"ok\":false,\"error\":\"缺 jiu_guige 字段(名称+规格是联合定位键,原行规格为空也要显式传空串)\"}";
|
||
if (jiuMing.Length == 0 && jiuGui.Length == 0)
|
||
return "{\"ok\":false,\"error\":\"名称与规格不能同时为空(无法定位选型页行)\"}";
|
||
if (xinMing.Length == 0 && xinGui.Length == 0)
|
||
return "{\"ok\":false,\"error\":\"缺少新值(xin_mingcheng/xin_guige 至少一项)\"}";
|
||
if (取参(p, "xin_shuliang").Length > 0)
|
||
return "{\"ok\":false,\"error\":\"★禁止传 xin_shuliang:数量列绝对不允许改动\"}";
|
||
|
||
double xinBiaojia;
|
||
if (!double.TryParse(取参(p, "xin_biaojia"), out xinBiaojia)) xinBiaojia = 0;
|
||
double xinCaigou;
|
||
if (!double.TryParse(取参(p, "xin_caigouxishu"), out xinCaigou) || xinCaigou <= 0) xinCaigou = 1;
|
||
string xinPinpai = 取参(p, "xin_pinpai").Trim();
|
||
string xinXilie = 取参(p, "xin_xilie").Trim();
|
||
string xinDanwei = 取参(p, "xin_danwei").Trim();
|
||
// ★ 元件名称规则=元件库页面点击替换同款(用户要求 AI 替换结果与点击一致):
|
||
// 品名(xin_pinming)优先 → 系列名清理(去"XX系列"前缀) → xin_mingcheng → 旧值兜底
|
||
string 新元件名称 = 取参(p, "xin_pinming").Trim();
|
||
if (新元件名称.Length == 0 && xinXilie.Length > 0) 新元件名称 = 清理系列字样(xinXilie);
|
||
if (新元件名称.Length == 0) 新元件名称 = xinMing;
|
||
if (新元件名称.Length == 0) 新元件名称 = jiuMing;
|
||
|
||
// ★ 分类防线:目标行必须是塑壳/微型断路器,其他类型/识别不到一律拒绝
|
||
string dbLeixing = "";
|
||
// ★ 只枚举活元件表(图号/箱号仍在):孤儿残留表不参与分类判断与替换
|
||
var tblNames = new List<string>();
|
||
int 总行数 = 0;
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
tblNames = 活元件表列表(conn);
|
||
// 找到匹配行并读取其类型列(任一匹配行的类型参与分类判断)
|
||
foreach (string realTbl in tblNames)
|
||
{
|
||
string tbl = "\"" + realTbl.Replace("\"", "") + "\"";
|
||
string sqlFind = "SELECT 类型 FROM " + tbl + " WHERE 元件名称=@n AND 规格=@s LIMIT 1;";
|
||
using (var cmdFind = new System.Data.SQLite.SQLiteCommand(sqlFind, conn))
|
||
{
|
||
cmdFind.Parameters.AddWithValue("@n", jiuMing);
|
||
cmdFind.Parameters.AddWithValue("@s", jiuGui);
|
||
object o = cmdFind.ExecuteScalar();
|
||
if (o != null && o != DBNull.Value)
|
||
{
|
||
dbLeixing = o.ToString();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
string aiReason;
|
||
string fenlei = AI接口分类放行(jiuMing, jiuGui, dbLeixing, xinMing, xinGui, out aiReason);
|
||
if (fenlei == null)
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(aiReason) + "\"}";
|
||
// ★ 附件码语法域校验+表价防线(2026-09-25):不存在的型号(如32008)/无效表价一律拒绝
|
||
if (xinGui.Length > 0)
|
||
{
|
||
string 码误 = 校验断路器附件码(xinGui);
|
||
if (码误 != null)
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(码误) + "\"}";
|
||
}
|
||
if (xinBiaojia <= 0)
|
||
return "{\"ok\":false,\"error\":\"xin_biaojia 必须是大于0的数字(表价须来自元件库全维度命中行)\"}";
|
||
|
||
// ★ 替换落库:单事务参数化 UPDATE;SET 绝不含"数量";派生价按库里原数量重算
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
using (var tran = conn.BeginTransaction())
|
||
{
|
||
foreach (string realTbl in tblNames)
|
||
{
|
||
string tbl = "\"" + realTbl.Replace("\"", "") + "\"";
|
||
string sqlUpd =
|
||
"UPDATE " + tbl + " SET " +
|
||
"元件名称=@nn, 规格=@ng, 表价=@bj, 采购系数=@cgx, " +
|
||
"品牌=@pp, 系列=@xl" +
|
||
(xinDanwei.Length > 0 ? ", 单位=@dw" : "") +
|
||
", 采购单价 = @bj * @cgx, " +
|
||
"采购价 = (@bj * @cgx) * CAST(数量 AS REAL), " +
|
||
"报出单价 = (@bj * @cgx) * 报出系数, " +
|
||
"报出价 = ((@bj * @cgx) * 报出系数) * CAST(数量 AS REAL) " +
|
||
"WHERE 元件名称=@on AND 规格=@og;";
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(sqlUpd, conn, tran))
|
||
{
|
||
cmd.Parameters.AddWithValue("@nn", 新元件名称);
|
||
cmd.Parameters.AddWithValue("@ng", xinGui.Length > 0 ? xinGui : jiuGui);
|
||
cmd.Parameters.AddWithValue("@bj", xinBiaojia);
|
||
cmd.Parameters.AddWithValue("@cgx", xinCaigou);
|
||
cmd.Parameters.AddWithValue("@pp", xinPinpai);
|
||
cmd.Parameters.AddWithValue("@xl", xinXilie);
|
||
if (xinDanwei.Length > 0) cmd.Parameters.AddWithValue("@dw", xinDanwei);
|
||
cmd.Parameters.AddWithValue("@on", jiuMing);
|
||
cmd.Parameters.AddWithValue("@og", jiuGui);
|
||
总行数 += cmd.ExecuteNonQuery();
|
||
}
|
||
}
|
||
tran.Commit();
|
||
}
|
||
}
|
||
|
||
// ★ UI 刷新(接口线程 → UI 线程):清缓存 + 重载选型页/报价页 + 刷项目汇总
|
||
try
|
||
{
|
||
if (this.InvokeRequired)
|
||
{
|
||
this.Invoke((System.Windows.Forms.MethodInvoker)delegate { AI替换后刷新(); });
|
||
}
|
||
else
|
||
{
|
||
AI替换后刷新();
|
||
}
|
||
}
|
||
catch (Exception exUi) { OpenClawHost.写日志("[AI接口] 刷新UI失败: " + exUi.Message); }
|
||
|
||
OpenClawHost.写日志("[AI接口] 替换: " + jiuMing + "/" + jiuGui + " → " + xinMing + "/" + xinGui
|
||
+ " 表价=" + xinBiaojia + " 影响 " + 总行数 + " 行");
|
||
if (总行数 == 0)
|
||
return "{\"ok\":false,\"rows\":0,\"error\":\"库里没有匹配行(名称+规格定位不到,可能已被替换过)\"}";
|
||
return "{\"ok\":true,\"rows\":" + 总行数 + ",\"fenlei\":\"" + fenlei + "\"}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 替换异常: " + ex.Message);
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(ex.Message) + "\"}";
|
||
}
|
||
}
|
||
|
||
// ---------- POST /api/replacebatch:批量替换(最快流程,2026-09-22 用户要求) ----------
|
||
|
||
/// <summary>批量替换:body={"items":[{jiu_mingcheng,jiu_guige,xin_pinming,xin_guige,xin_biaojia,
|
||
/// xin_xishu,xin_pinpai,xin_xilie},...]}。单事务逐项按 /api/replace 完全同款规则
|
||
/// (品名链/分类防线/活表过滤/派生价重算,数量绝不改)UPDATE,分类不过的项跳过并回报原因,
|
||
/// 全部完成只做一次 UI 刷新——比逐行 selectselection+editselection 快一个量级。</summary>
|
||
private string AI接口_批量替换元件(string bodyJson)
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId))
|
||
return "{\"ok\":false,\"error\":\"当前没有打开项目\"}";
|
||
string dbPath = GetQuoteDbPath(_currentProjectId);
|
||
if (!File.Exists(dbPath))
|
||
return "{\"ok\":false,\"error\":\"项目库文件不存在\"}";
|
||
|
||
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
Dictionary<string, object> p;
|
||
try { p = js.Deserialize<Dictionary<string, object>>(bodyJson ?? ""); }
|
||
catch { return "{\"ok\":false,\"error\":\"请求体不是合法 JSON\"}"; }
|
||
if (p == null || !p.ContainsKey("items") || !(p["items"] is System.Collections.IEnumerable))
|
||
return "{\"ok\":false,\"error\":\"缺 items 数组\"}";
|
||
var items = new List<Dictionary<string, object>>();
|
||
foreach (object o in (System.Collections.IEnumerable)p["items"])
|
||
{
|
||
var d = o as Dictionary<string, object>;
|
||
if (d != null) items.Add(d);
|
||
}
|
||
if (items.Count == 0) return "{\"ok\":false,\"error\":\"items 为空\"}";
|
||
|
||
var tblNames = new List<string>();
|
||
var 结果 = new List<string>();
|
||
int 总行数 = 0, 成功项 = 0;
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
tblNames = 活元件表列表(conn);
|
||
using (var tran = conn.BeginTransaction())
|
||
{
|
||
foreach (var it in items)
|
||
{
|
||
string jiuMing = 取参(it, "jiu_mingcheng").Trim();
|
||
string jiuGui = 取参(it, "jiu_guige").Trim();
|
||
string xinMing = 取参(it, "xin_mingcheng").Trim();
|
||
string xinGui = 取参(it, "xin_guige").Trim();
|
||
double xinBiaojia;
|
||
if (!double.TryParse(取参(it, "xin_biaojia"), out xinBiaojia)) xinBiaojia = 0;
|
||
double xinXishu;
|
||
if (!double.TryParse(取参(it, "xin_xishu"), out xinXishu)) xinXishu = 1;
|
||
if (取参(it, "xin_caigouxishu").Length > 0)
|
||
double.TryParse(取参(it, "xin_caigouxishu"), out xinXishu);
|
||
if (xinXishu <= 0) xinXishu = 1;
|
||
string xinPinpai = 取参(it, "xin_pinpai").Trim();
|
||
string xinXilie = 取参(it, "xin_xilie").Trim();
|
||
// ★ 品名链=元件库页面点击替换同款:品名优先→系列名清理→xin_mingcheng→旧值
|
||
string 新元件名称 = 取参(it, "xin_pinming").Trim();
|
||
if (新元件名称.Length == 0 && xinXilie.Length > 0) 新元件名称 = 清理系列字样(xinXilie);
|
||
if (新元件名称.Length == 0) 新元件名称 = xinMing;
|
||
if (新元件名称.Length == 0) 新元件名称 = jiuMing;
|
||
|
||
if (jiuMing.Length == 0 && jiuGui.Length == 0)
|
||
{
|
||
结果.Add("{\"guige\":\"" + Json转义(jiuGui) + "\",\"rows\":0,\"error\":\"名称与规格同时为空\"}");
|
||
continue;
|
||
}
|
||
if (取参(it, "xin_shuliang").Length > 0)
|
||
{
|
||
结果.Add("{\"guige\":\"" + Json转义(jiuGui) + "\",\"rows\":0,\"error\":\"禁止传数量\"}");
|
||
continue;
|
||
}
|
||
// 分类防线:活表里找该行类型再放行(同单条版)
|
||
string dbLeixing = "";
|
||
foreach (string realTbl in tblNames)
|
||
{
|
||
string tblF = "\"" + realTbl.Replace("\"", "") + "\"";
|
||
using (var cmdFind = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT 类型 FROM " + tblF + " WHERE 元件名称=@n AND 规格=@s LIMIT 1;", conn, tran))
|
||
{
|
||
cmdFind.Parameters.AddWithValue("@n", jiuMing);
|
||
cmdFind.Parameters.AddWithValue("@s", jiuGui);
|
||
object o = cmdFind.ExecuteScalar();
|
||
if (o != null && o != DBNull.Value) { dbLeixing = o.ToString(); break; }
|
||
}
|
||
}
|
||
string aiReason;
|
||
string fenlei = AI接口分类放行(jiuMing, jiuGui, dbLeixing, xinMing, xinGui, out aiReason);
|
||
if (fenlei == null)
|
||
{
|
||
结果.Add("{\"guige\":\"" + Json转义(jiuGui) + "\",\"rows\":0,\"error\":\"" + Json转义(aiReason) + "\"}");
|
||
continue;
|
||
}
|
||
// ★ 附件码语法域校验+表价防线(2026-09-25):不存在的型号(如32008)/无效表价的项跳过并回报
|
||
if (xinGui.Length > 0)
|
||
{
|
||
string 码误 = 校验断路器附件码(xinGui);
|
||
if (码误 != null)
|
||
{
|
||
结果.Add("{\"guige\":\"" + Json转义(jiuGui) + "\",\"rows\":0,\"error\":\"" + Json转义(码误) + "\"}");
|
||
continue;
|
||
}
|
||
}
|
||
if (xinBiaojia <= 0)
|
||
{
|
||
结果.Add("{\"guige\":\"" + Json转义(jiuGui) + "\",\"rows\":0,\"error\":\"xin_biaojia 必须是大于0的数字(表价须来自元件库全维度命中行)\"}");
|
||
continue;
|
||
}
|
||
// 替换落库:逐活表参数化 UPDATE(SET 绝不含数量,派生价按原数量重算)
|
||
int itemRows = 0;
|
||
foreach (string realTbl in tblNames)
|
||
{
|
||
string tbl = "\"" + realTbl.Replace("\"", "") + "\"";
|
||
string sqlUpd =
|
||
"UPDATE " + tbl + " SET " +
|
||
"元件名称=@nn, 规格=@ng, 表价=@bj, 采购系数=@cgx, " +
|
||
"品牌=@pp, 系列=@xl, " +
|
||
"采购单价 = @bj * @cgx, " +
|
||
"采购价 = (@bj * @cgx) * CAST(数量 AS REAL), " +
|
||
"报出单价 = (@bj * @cgx) * 报出系数, " +
|
||
"报出价 = ((@bj * @cgx) * 报出系数) * CAST(数量 AS REAL) " +
|
||
"WHERE 元件名称=@on AND 规格=@og;";
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(sqlUpd, conn, tran))
|
||
{
|
||
cmd.Parameters.AddWithValue("@nn", 新元件名称);
|
||
cmd.Parameters.AddWithValue("@ng", xinGui.Length > 0 ? xinGui : jiuGui);
|
||
cmd.Parameters.AddWithValue("@bj", xinBiaojia);
|
||
cmd.Parameters.AddWithValue("@cgx", xinXishu);
|
||
cmd.Parameters.AddWithValue("@pp", xinPinpai);
|
||
cmd.Parameters.AddWithValue("@xl", xinXilie);
|
||
cmd.Parameters.AddWithValue("@on", jiuMing);
|
||
cmd.Parameters.AddWithValue("@og", jiuGui);
|
||
itemRows += cmd.ExecuteNonQuery();
|
||
}
|
||
}
|
||
// ★ 0 行命中=该项失败(2026-09-25:不再静默 ok,消灭假成功)
|
||
if (itemRows == 0)
|
||
{
|
||
结果.Add("{\"guige\":\"" + Json转义(jiuGui) + "\",\"rows\":0,\"error\":\"库里没有匹配行(名称+规格定位不到,可能已被替换过)\"}");
|
||
continue;
|
||
}
|
||
总行数 += itemRows;
|
||
成功项++;
|
||
结果.Add("{\"guige\":\"" + Json转义(jiuGui) + "\",\"rows\":" + itemRows + ",\"fenlei\":\"" + fenlei + "\"}");
|
||
}
|
||
tran.Commit();
|
||
}
|
||
}
|
||
|
||
// ★ 全部完成只做一次 UI 刷新(单条版每行刷一次,批量版砍掉 N-1 次)
|
||
try
|
||
{
|
||
if (this.InvokeRequired)
|
||
{
|
||
this.Invoke((System.Windows.Forms.MethodInvoker)delegate { AI替换后刷新(); });
|
||
}
|
||
else
|
||
{
|
||
AI替换后刷新();
|
||
}
|
||
}
|
||
catch (Exception exUi) { OpenClawHost.写日志("[AI接口] 刷新UI失败: " + exUi.Message); }
|
||
|
||
OpenClawHost.写日志("[AI接口] 批量替换: " + items.Count + " 项,成功 " + 成功项 + ",共影响 " + 总行数 + " 行");
|
||
var sb = new System.Text.StringBuilder();
|
||
sb.Append("{\"ok\":true,\"items\":").Append(items.Count)
|
||
.Append(",\"done\":").Append(成功项)
|
||
.Append(",\"rows\":").Append(总行数)
|
||
.Append(",\"results\":[");
|
||
for (int i = 0; i < 结果.Count; i++)
|
||
{
|
||
if (i > 0) sb.Append(",");
|
||
sb.Append(结果[i]);
|
||
}
|
||
sb.Append("]}");
|
||
return sb.ToString();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 批量替换异常: " + ex.Message);
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(ex.Message) + "\"}";
|
||
}
|
||
}
|
||
|
||
// ---------- POST /api/page:AI 的"鼠标"——在元件库页面里执行 JS(点击/读值) ----------
|
||
|
||
/// <summary>body=JSON:{js:"(async()=>{...})()"}。在元件库窗口(w4)的 WebView2 页面里执行脚本,
|
||
/// 返回 {ok, result}(result=页面返回值的 JSON 串)。AI 用它像人一样点选系列/壳架/极数/电流并读
|
||
/// 页面真实渲染的型号(.p-name)/价格/品名——所见即所得,不再从价格表拼。</summary>
|
||
private string AI接口_执行页面脚本(string bodyJson)
|
||
{
|
||
try
|
||
{
|
||
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
Dictionary<string, object> p;
|
||
try { p = js.Deserialize<Dictionary<string, object>>(bodyJson ?? ""); }
|
||
catch { return "{\"ok\":false,\"error\":\"请求体不是合法 JSON\"}"; }
|
||
string script = 取参(p, "js");
|
||
if (string.IsNullOrWhiteSpace(script))
|
||
return "{\"ok\":false,\"error\":\"缺 js 字段(要在元件库页面里执行的脚本)\"}";
|
||
if (script.Length > 20000)
|
||
return "{\"ok\":false,\"error\":\"脚本过长(>20KB)\"}";
|
||
|
||
// ExecuteScriptAsync 必须 UI 线程调用:整个访问(含就绪判断)放 BeginInvoke 里,
|
||
// ContinueWith 收结果,接口线程 Wait(非 UI 线程,安全)
|
||
var tcs = new System.Threading.Tasks.TaskCompletionSource<string>();
|
||
try
|
||
{
|
||
this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
||
{
|
||
try
|
||
{
|
||
var wv = this.webView21; // 控件别名:DockWindow4 的元件库 WebView2
|
||
if (wv == null || wv.CoreWebView2 == null)
|
||
{
|
||
tcs.TrySetResult(null); // 用 null 标记"页面未就绪"
|
||
return;
|
||
}
|
||
var task = wv.CoreWebView2.ExecuteScriptAsync(script);
|
||
task.ContinueWith(delegate(System.Threading.Tasks.Task<string> t)
|
||
{
|
||
if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerException);
|
||
else tcs.TrySetResult(t.Result);
|
||
});
|
||
}
|
||
catch (Exception exUi)
|
||
{
|
||
tcs.TrySetException(exUi);
|
||
}
|
||
});
|
||
}
|
||
catch (Exception exInv)
|
||
{
|
||
return "{\"ok\":false,\"error\":\"调度到界面线程失败: " + Json转义(exInv.Message) + "\"}";
|
||
}
|
||
if (!tcs.Task.Wait(8000))
|
||
return "{\"ok\":false,\"error\":\"页面脚本执行超时(8 秒)\"}";
|
||
string result = tcs.Task.Result; // 页面返回值的 JSON 编码串;null=页面未就绪
|
||
if (result == null)
|
||
return "{\"ok\":false,\"error\":\"元件库页面未就绪(软件刚启动或元件库窗口还没加载过页面;先打开一次元件库窗口再试)\"}";
|
||
return "{\"ok\":true,\"result\":" + (result ?? "null") + "}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 页面脚本异常: " + ex.Message);
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(ex.Message) + "\"}";
|
||
}
|
||
}
|
||
|
||
// ---------- POST /api/replaceall:网页"替换"按钮同款协议 → 全项目替换(数量铁律) ----------
|
||
|
||
/// <summary>body=JSON:{jiu_mingcheng, jiu_guige(定位旧行), name(页面完整型号→规格),
|
||
/// price(页面实算价→表价), brand, xishu(折扣→采购系数), xilie(系列), pinming(品名→元件名称)}。
|
||
/// 字段语义与页面"替换"按钮的 postMessage 协议完全同款(WebView21_WebMessageReceived→替换选中元件),
|
||
/// 但按旧规格替换全项目所有箱号的匹配行,且 ★数量列绝不写(页面按钮会覆盖数量,故替换动作用本接口而非真点按钮)。
|
||
/// 分类防线:目标行必须是塑壳/微型断路器。</summary>
|
||
private string AI接口_网页式全量替换(string bodyJson)
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId))
|
||
return "{\"ok\":false,\"error\":\"当前没有打开项目\"}";
|
||
string dbPath = GetQuoteDbPath(_currentProjectId);
|
||
if (!File.Exists(dbPath))
|
||
return "{\"ok\":false,\"error\":\"项目库文件不存在\"}";
|
||
|
||
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
Dictionary<string, object> p;
|
||
try { p = js.Deserialize<Dictionary<string, object>>(bodyJson ?? ""); }
|
||
catch { return "{\"ok\":false,\"error\":\"请求体不是合法 JSON\"}"; }
|
||
if (p == null) return "{\"ok\":false,\"error\":\"请求体为空\"}";
|
||
|
||
string jiuMing = 取参(p, "jiu_mingcheng").Trim();
|
||
string jiuGui = 取参(p, "jiu_guige").Trim();
|
||
string name = 取参(p, "name").Trim(); // 页面完整型号(.p-name)
|
||
string pinming = 取参(p, "pinming").Trim(); // 品名
|
||
string brand = 取参(p, "brand").Trim();
|
||
string xishu = 取参(p, "xishu").Trim(); // 折扣(字符串两位小数)
|
||
string xilie = 取参(p, "xilie").Trim(); // 系列名
|
||
double price;
|
||
if (!double.TryParse(取参(p, "price"), out price)) price = 0;
|
||
|
||
if (!p.ContainsKey("jiu_guige") || string.IsNullOrEmpty(name))
|
||
return "{\"ok\":false,\"error\":\"缺 jiu_guige / name(定位旧行与页面完整型号)\"}";
|
||
if (取参(p, "xin_shuliang").Length > 0 || 取参(p, "shuliang").Length > 0)
|
||
return "{\"ok\":false,\"error\":\"★禁止传数量字段:数量列绝对不允许改动\"}";
|
||
|
||
// 元件名称=品名非空?品名:系列名去系列字样(照 替换选中元件 同款规则)
|
||
string 元件名称 = string.IsNullOrWhiteSpace(pinming) ? 清理系列字样(xilie) : pinming;
|
||
double 系数 = 1.0;
|
||
double.TryParse(xishu, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out 系数);
|
||
if (系数 <= 0) 系数 = 1.0;
|
||
|
||
// 分类防线(照 /api/replace)
|
||
string dbLeixing = "";
|
||
// ★ 只枚举活元件表(图号/箱号仍在):孤儿残留表不参与分类判断与替换
|
||
var tblNames = new List<string>();
|
||
int 总行数 = 0;
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
tblNames = 活元件表列表(conn);
|
||
foreach (string realTbl in tblNames)
|
||
{
|
||
string tbl = "\"" + realTbl.Replace("\"", "") + "\"";
|
||
using (var cmdFind = new System.Data.SQLite.SQLiteCommand("SELECT 类型 FROM " + tbl + " WHERE 元件名称=@n AND 规格=@s LIMIT 1;", conn))
|
||
{
|
||
cmdFind.Parameters.AddWithValue("@n", jiuMing);
|
||
cmdFind.Parameters.AddWithValue("@s", jiuGui);
|
||
object o = cmdFind.ExecuteScalar();
|
||
if (o != null && o != DBNull.Value) { dbLeixing = o.ToString(); break; }
|
||
}
|
||
}
|
||
}
|
||
string fenlei = 分类断路器(jiuMing, jiuGui, dbLeixing);
|
||
if (string.Equals(fenlei, "排除"))
|
||
return "{\"ok\":false,\"error\":\"目标行是明确非断路器类(隔离开关/万能式/熔断器等),按规则不替换\"}";
|
||
if (!string.Equals(fenlei, "塑壳") && !string.Equals(fenlei, "微型"))
|
||
{
|
||
// 旧行识别不到(可能是打错的型号)→ 用 AI 给的新型号线再验
|
||
string 新类 = 分类断路器(pinming, name, dbLeixing);
|
||
if (!string.Equals(新类, "塑壳") && !string.Equals(新类, "微型"))
|
||
return "{\"ok\":false,\"error\":\"旧行与替换型号都无法识别为塑壳/微型断路器(打错的型号请先纠错成规范型号,如 DZ4/PLE→DZ47sLE)\"}";
|
||
fenlei = 新类;
|
||
}
|
||
// ★ 附件码语法域校验+表价防线(2026-09-25)
|
||
string 码误R = 校验断路器附件码(name);
|
||
if (码误R != null)
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(码误R) + "\"}";
|
||
if (price <= 0)
|
||
return "{\"ok\":false,\"error\":\"price 必须是大于0的数字(页面实算价)\"}";
|
||
|
||
// 全项目替换(SET 无数量列;派生价按原数量;报出系数保留原值)
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
using (var tran = conn.BeginTransaction())
|
||
{
|
||
foreach (string realTbl in tblNames)
|
||
{
|
||
string tbl = "\"" + realTbl.Replace("\"", "") + "\"";
|
||
string sqlUpd =
|
||
"UPDATE " + tbl + " SET " +
|
||
"元件名称=@mc, 规格=@gg, 表价=@bj, 采购系数=@xs, 品牌=@pp, 系列=@xl, " +
|
||
"采购单价 = @bj * @xs, " +
|
||
"采购价 = (@bj * @xs) * CAST(数量 AS REAL), " +
|
||
"报出单价 = (@bj * @xs) * 报出系数, " +
|
||
"报出价 = ((@bj * @xs) * 报出系数) * CAST(数量 AS REAL) " +
|
||
"WHERE 元件名称=@on AND 规格=@og;";
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(sqlUpd, conn, tran))
|
||
{
|
||
cmd.Parameters.AddWithValue("@mc", 元件名称);
|
||
cmd.Parameters.AddWithValue("@gg", name);
|
||
cmd.Parameters.AddWithValue("@bj", price);
|
||
cmd.Parameters.AddWithValue("@xs", 系数);
|
||
cmd.Parameters.AddWithValue("@pp", brand);
|
||
cmd.Parameters.AddWithValue("@xl", xilie);
|
||
cmd.Parameters.AddWithValue("@on", jiuMing);
|
||
cmd.Parameters.AddWithValue("@og", jiuGui);
|
||
总行数 += cmd.ExecuteNonQuery();
|
||
}
|
||
}
|
||
tran.Commit();
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
if (this.InvokeRequired) this.Invoke((System.Windows.Forms.MethodInvoker)delegate { AI替换后刷新(); });
|
||
else AI替换后刷新();
|
||
}
|
||
catch (Exception exUi) { OpenClawHost.写日志("[AI接口] 刷新UI失败: " + exUi.Message); }
|
||
|
||
OpenClawHost.写日志("[AI接口] 网页式替换: " + jiuMing + "/" + jiuGui + " → " + 元件名称 + "/" + name
|
||
+ " 表价=" + price + " 系数=" + 系数 + " 影响 " + 总行数 + " 行");
|
||
if (总行数 == 0)
|
||
return "{\"ok\":false,\"rows\":0,\"error\":\"库里没有匹配行(名称+规格定位不到,可能已被替换过)\"}";
|
||
return "{\"ok\":true,\"rows\":" + 总行数 + ",\"fenlei\":\"" + fenlei + "\"}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 网页式替换异常: " + ex.Message);
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(ex.Message) + "\"}";
|
||
}
|
||
}
|
||
|
||
// ---------- POST /api/selectselection:AI 选中选型页的行(可视化定位) ----------
|
||
|
||
/// <summary>body=JSON:{jiu_mingcheng, jiu_guige}。在选型页汇总表里找到匹配行并选中+滚动到可见,
|
||
/// 自然触发选中联动(下方明细表加载该元件分布)。返回 {ok,row}。</summary>
|
||
private string AI接口_选中选型行(string bodyJson)
|
||
{
|
||
try
|
||
{
|
||
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
Dictionary<string, object> p;
|
||
try { p = js.Deserialize<Dictionary<string, object>>(bodyJson ?? ""); }
|
||
catch { return "{\"ok\":false,\"error\":\"请求体不是合法 JSON\"}"; }
|
||
string jiuMing = 取参(p, "jiu_mingcheng").Trim();
|
||
string jiuGui = 取参(p, "jiu_guige").Trim();
|
||
if (!p.ContainsKey("jiu_guige"))
|
||
return "{\"ok\":false,\"error\":\"缺 jiu_guige(名称+规格是联合定位键,名称空传空串)\"}";
|
||
|
||
var tcs = new System.Threading.Tasks.TaskCompletionSource<string>();
|
||
this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
||
{
|
||
try
|
||
{
|
||
// 选型页未加载过(启动后没切到过)则先加载
|
||
if (_selectionSummaryTable == null) LoadSelectionSummary();
|
||
int found = -1;
|
||
if (_selectionSummaryTable != null)
|
||
{
|
||
for (int i = 0; i < _selectionSummaryTable.Rows.Count; i++)
|
||
{
|
||
DataRow r = _selectionSummaryTable.Rows[i];
|
||
string n = (r["元件名称"] ?? "").ToString().Trim();
|
||
string g = (r["规格"] ?? "").ToString().Trim();
|
||
if (string.Equals(n, jiuMing, StringComparison.Ordinal)
|
||
&& string.Equals(g, jiuGui, StringComparison.Ordinal))
|
||
{
|
||
found = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (found < 0) { tcs.TrySetResult("{\"ok\":false,\"error\":\"选型页没有该行(名称+规格与清单不一致)\"}"); return; }
|
||
var dgv = this.advancedDataGridView1;
|
||
if (dgv == null) { tcs.TrySetResult("{\"ok\":false,\"error\":\"选型页表格不可用\"}"); return; }
|
||
if (dgv.Rows.Count > found)
|
||
{
|
||
try { dgv.FirstDisplayedScrollingRowIndex = Math.Max(0, found - 3); } catch { }
|
||
dgv.CurrentCell = dgv.Rows[found].Cells.Count > 0 ? dgv.Rows[found].Cells[0] : null;
|
||
}
|
||
OpenClawHost.写日志("[AI接口] 已选中选型页第 " + (found + 1) + " 行: " + jiuMing + "/" + jiuGui);
|
||
tcs.TrySetResult("{\"ok\":true,\"row\":" + found + "}");
|
||
}
|
||
catch (Exception exUi) { tcs.TrySetResult("{\"ok\":false,\"error\":\"" + Json转义(exUi.Message) + "\"}"); }
|
||
});
|
||
if (!tcs.Task.Wait(5000)) return "{\"ok\":false,\"error\":\"选中操作超时\"}";
|
||
return tcs.Task.Result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(ex.Message) + "\"}";
|
||
}
|
||
}
|
||
|
||
// ---------- POST /api/editselection:走选型页编辑链路替换(零 SQL,复刻手改单元格) ----------
|
||
|
||
/// <summary>body=JSON:{jiu_mingcheng, jiu_guige(定位), xin_mingcheng?, xin_guige, xin_biaojia,
|
||
/// xin_xishu, xin_pinpai?, xin_xilie?}。复刻"用户在选型页手改单元格"的完整业务链:
|
||
/// 内存表行赋值 → RecalcSelectionRow 行内重算 → 置脏 → SyncSelectionChangesToDb()(现有方法,
|
||
/// 按名称+规格全项目同步,SET 天然不含数量列)→ RefreshQuotePageAfterSelectionEdit。
|
||
/// 全程不写任何新 SQL;数量列绝不触碰。</summary>
|
||
private string AI接口_编辑选型行(string bodyJson)
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId))
|
||
return "{\"ok\":false,\"error\":\"当前没有打开项目\"}";
|
||
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
Dictionary<string, object> p;
|
||
try { p = js.Deserialize<Dictionary<string, object>>(bodyJson ?? ""); }
|
||
catch { return "{\"ok\":false,\"error\":\"请求体不是合法 JSON\"}"; }
|
||
string jiuMing = 取参(p, "jiu_mingcheng").Trim();
|
||
string jiuGui = 取参(p, "jiu_guige").Trim();
|
||
string xinMing = 取参(p, "xin_mingcheng").Trim();
|
||
string xinGui = 取参(p, "xin_guige").Trim();
|
||
double xinBiaojia;
|
||
if (!double.TryParse(取参(p, "xin_biaojia"), out xinBiaojia)) xinBiaojia = 0;
|
||
double xinXishu;
|
||
if (!double.TryParse(取参(p, "xin_xishu"), out xinXishu) || xinXishu <= 0) xinXishu = 1;
|
||
string xinPinpai = 取参(p, "xin_pinpai").Trim();
|
||
string xinXilie = 取参(p, "xin_xilie").Trim();
|
||
// ★ 元件名称规则=元件库页面点击替换同款:品名(xin_pinming)优先 → 系列名清理
|
||
// (去"XX系列"前缀) → xin_mingcheng → 旧值兜底——AI 不传品名也能得到正确元件名称
|
||
string 新元件名称 = 取参(p, "xin_pinming").Trim();
|
||
if (新元件名称.Length == 0 && xinXilie.Length > 0) 新元件名称 = 清理系列字样(xinXilie);
|
||
if (新元件名称.Length == 0) 新元件名称 = xinMing;
|
||
if (新元件名称.Length == 0) 新元件名称 = jiuMing;
|
||
if (!p.ContainsKey("jiu_guige") || (xinMing.Length == 0 && xinGui.Length == 0))
|
||
return "{\"ok\":false,\"error\":\"缺定位键或新值(jiu_guige 必传;xin_mingcheng/xin_guige 至少一项)\"}";
|
||
if (取参(p, "xin_shuliang").Length > 0 || 取参(p, "shuliang").Length > 0)
|
||
return "{\"ok\":false,\"error\":\"★禁止传数量字段:数量列绝对不允许改动\"}";
|
||
|
||
// 分类防线:目标行必须是塑壳/微型断路器(选型页未加载时先在 UI 线程加载再查)
|
||
string leixing = "";
|
||
bool 行存在 = false;
|
||
System.Threading.Tasks.Task waitLoad = System.Threading.Tasks.Task.Factory.StartNew(delegate
|
||
{
|
||
this.Invoke((System.Windows.Forms.MethodInvoker)delegate
|
||
{
|
||
if (_selectionSummaryTable == null) LoadSelectionSummary();
|
||
});
|
||
});
|
||
try { waitLoad.Wait(15000); } catch { }
|
||
if (_selectionSummaryTable != null)
|
||
{
|
||
foreach (DataRow r in _selectionSummaryTable.Rows)
|
||
{
|
||
if (string.Equals((r["元件名称"] ?? "").ToString().Trim(), jiuMing, StringComparison.Ordinal)
|
||
&& string.Equals((r["规格"] ?? "").ToString().Trim(), jiuGui, StringComparison.Ordinal))
|
||
{
|
||
行存在 = true;
|
||
leixing = (r["类型"] ?? "").ToString();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!行存在)
|
||
return "{\"ok\":false,\"error\":\"选型页没有该行(名称+规格与清单不一致)\"}";
|
||
string fenlei = 分类断路器(jiuMing, jiuGui, leixing);
|
||
if (string.Equals(fenlei, "排除"))
|
||
return "{\"ok\":false,\"error\":\"目标行是明确非断路器类(隔离开关/万能式/熔断器等),按规则不替换\"}";
|
||
if (!string.Equals(fenlei, "塑壳") && !string.Equals(fenlei, "微型"))
|
||
{
|
||
// 旧行识别不到(打错的型号如 DZ4/PLE)→ 用 AI 纠错后的新型号再验(DZ47sLE→微型 ✓ 放行)
|
||
string 新类 = 分类断路器(xinMing, xinGui, leixing);
|
||
if (!string.Equals(新类, "塑壳") && !string.Equals(新类, "微型"))
|
||
return "{\"ok\":false,\"error\":\"旧行与纠错后型号都无法识别为塑壳/微型断路器(请先纠错成规范型号再替换)\"}";
|
||
fenlei = 新类;
|
||
}
|
||
// ★ 附件码语法域校验+表价防线(2026-09-25)
|
||
if (xinGui.Length > 0)
|
||
{
|
||
string 码误 = 校验断路器附件码(xinGui);
|
||
if (码误 != null)
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(码误) + "\"}";
|
||
}
|
||
if (xinBiaojia <= 0)
|
||
return "{\"ok\":false,\"error\":\"xin_biaojia 必须是大于0的数字(表价须来自元件库全维度命中行)\"}";
|
||
|
||
var tcs = new System.Threading.Tasks.TaskCompletionSource<string>();
|
||
this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
||
{
|
||
try
|
||
{
|
||
int found = -1;
|
||
if (_selectionSummaryTable != null)
|
||
{
|
||
for (int i = 0; i < _selectionSummaryTable.Rows.Count; i++)
|
||
{
|
||
DataRow r = _selectionSummaryTable.Rows[i];
|
||
if (string.Equals((r["元件名称"] ?? "").ToString().Trim(), jiuMing, StringComparison.Ordinal)
|
||
&& string.Equals((r["规格"] ?? "").ToString().Trim(), jiuGui, StringComparison.Ordinal))
|
||
{
|
||
found = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (found < 0) { tcs.TrySetResult("{\"ok\":false,\"error\":\"选型页没有该行(名称+规格与清单不一致)\"}"); return; }
|
||
|
||
// ★ 显示层:选型页汇总表行赋新值(数量列不碰)→ 行内重算(用户看到的就是改后的值)
|
||
DataRow row = _selectionSummaryTable.Rows[found];
|
||
if (新元件名称.Length > 0) row["元件名称"] = 新元件名称;
|
||
if (xinGui.Length > 0) row["规格"] = xinGui;
|
||
row["表价"] = xinBiaojia;
|
||
row["采购系数"] = xinXishu;
|
||
if (xinPinpai.Length > 0) row["品牌"] = xinPinpai;
|
||
if (xinXilie.Length > 0) row["系列"] = xinXilie;
|
||
RecalcSelectionRow(found);
|
||
|
||
// ★ 落库层:走软件"元件表编辑直写管线"(与手敲单元格落库同一条链,UPDATE WHERE rowid)——
|
||
// 不裸写 SQL;SyncSelectionChangesToDb 按"新名称+规格"匹配旧行永远 0 行(其边界=只能改价格类),故不用。
|
||
string dbPath2 = GetQuoteDbPath(_currentProjectId);
|
||
int 总行数2 = 0;
|
||
string newMing = 新元件名称.Length > 0 ? 新元件名称 : jiuMing;
|
||
string newGui = xinGui.Length > 0 ? xinGui : jiuGui;
|
||
// ★ 只枚举活元件表(图号/箱号仍在):孤儿残留表不参与直写管线替换
|
||
var tblNames2 = new List<string>();
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath2)))
|
||
{
|
||
conn.Open();
|
||
tblNames2 = 活元件表列表(conn);
|
||
// 逐表找匹配行(参数化),投喂软件直写队列(列值含派生价,按各行原数量/报出系数算;数量列绝不入队)
|
||
foreach (string tbl in tblNames2)
|
||
{
|
||
var hits = new List<object[]>();
|
||
using (var cmdF = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT rowid, 数量, 报出系数 FROM \"" + tbl.Replace("\"", "") + "\" WHERE 元件名称=@n AND 规格=@s;", conn))
|
||
{
|
||
cmdF.Parameters.AddWithValue("@n", jiuMing);
|
||
cmdF.Parameters.AddWithValue("@s", jiuGui);
|
||
using (var rdF = cmdF.ExecuteReader())
|
||
{
|
||
while (rdF.Read())
|
||
{
|
||
long rid = Convert.ToInt64(rdF["rowid"]);
|
||
double cnt, bcx;
|
||
double.TryParse((rdF["数量"] ?? "0").ToString(), out cnt);
|
||
double.TryParse((rdF["报出系数"] ?? "1").ToString(), out bcx);
|
||
if (bcx <= 0) bcx = 1;
|
||
hits.Add(new object[] { rid, cnt, bcx });
|
||
}
|
||
}
|
||
}
|
||
foreach (object[] h in hits)
|
||
{
|
||
long rid = (long)h[0];
|
||
double cnt = (double)h[1], bcx = (double)h[2];
|
||
lock (_待写项)
|
||
{
|
||
Action<string, string> 塞 = delegate(string col, string val)
|
||
{
|
||
_待写项[tbl + "|" + rid + "|" + col] = new[] { tbl, rid.ToString(), col, val };
|
||
};
|
||
塞("元件名称", newMing);
|
||
塞("规格", newGui);
|
||
塞("表价", xinBiaojia.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||
塞("采购系数", xinXishu.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||
if (xinPinpai.Length > 0) 塞("品牌", xinPinpai);
|
||
if (xinXilie.Length > 0) 塞("系列", xinXilie);
|
||
double dj = xinBiaojia * xinXishu;
|
||
塞("采购单价", dj.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||
塞("采购价", (dj * cnt).ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||
塞("报出单价", (dj * bcx).ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||
塞("报出价", (dj * bcx * cnt).ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||
}
|
||
总行数2++;
|
||
}
|
||
}
|
||
}
|
||
if (总行数2 == 0)
|
||
{
|
||
tcs.TrySetResult("{\"ok\":false,\"error\":\"库里没有匹配行(名称+规格定位不到,可能已被替换过)\"}");
|
||
return;
|
||
}
|
||
确保直写Timer().Stop();
|
||
直写队列强制落库(); // 软件直写管线同步提交(事务 UPDATE WHERE rowid+读回+刷新项目汇总)
|
||
|
||
RefreshQuotePageAfterSelectionEdit();
|
||
|
||
// 选型页保持新值显示
|
||
LoadSelectionSummary();
|
||
string newMing2 = newMing;
|
||
string newGui2 = newGui;
|
||
if (this.advancedDataGridView1 != null && _selectionSummaryTable != null)
|
||
{
|
||
for (int k = 0; k < _selectionSummaryTable.Rows.Count; k++)
|
||
{
|
||
DataRow rk = _selectionSummaryTable.Rows[k];
|
||
if (string.Equals((rk["元件名称"] ?? "").ToString().Trim(), newMing2, StringComparison.Ordinal)
|
||
&& string.Equals((rk["规格"] ?? "").ToString().Trim(), newGui2, StringComparison.Ordinal))
|
||
{
|
||
try { this.advancedDataGridView1.FirstDisplayedScrollingRowIndex = Math.Max(0, k - 3); } catch { }
|
||
if (this.advancedDataGridView1.Rows.Count > k && this.advancedDataGridView1.Rows[k].Cells.Count > 0)
|
||
this.advancedDataGridView1.CurrentCell = this.advancedDataGridView1.Rows[k].Cells[0];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
OpenClawHost.写日志("[AI接口] 选型页编辑替换(直写管线): " + jiuMing + "/" + jiuGui + " → "
|
||
+ newMing + "/" + newGui + " 表价=" + xinBiaojia + " 系数=" + xinXishu + " 影响 " + 总行数2 + " 行");
|
||
tcs.TrySetResult("{\"ok\":true,\"rows\":" + 总行数2 + ",\"fenlei\":\"" + fenlei + "\"}");
|
||
}
|
||
catch (Exception exUi) { tcs.TrySetResult("{\"ok\":false,\"error\":\"" + Json转义(exUi.Message) + "\"}"); }
|
||
});
|
||
if (!tcs.Task.Wait(30000)) return "{\"ok\":false,\"error\":\"选型页编辑超时(30 秒)\"}";
|
||
return tcs.Task.Result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 选型页编辑异常: " + ex.Message);
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(ex.Message) + "\"}";
|
||
}
|
||
}
|
||
|
||
/// <summary>替换落库后的界面刷新(UI 线程执行):先重算报价页/项目汇总(重型链),
|
||
/// 最后重载选型页并强制重绘,末尾写日志便于核验。</summary>
|
||
private void AI替换后刷新()
|
||
{
|
||
try
|
||
{
|
||
_xianghaoCache.Clear();
|
||
_yuanjianCache.Clear();
|
||
RefreshQuotePageAfterSelectionEdit(); // 报价页重算+重载+项目汇总
|
||
LoadSelectionSummary(); // 选型页重载(放最后,绑定不被后续重载链干扰)
|
||
if (this.advancedDataGridView1 != null)
|
||
{
|
||
this.advancedDataGridView1.Refresh(); // 强制重绘
|
||
}
|
||
OpenClawHost.写日志("[AI接口] 界面已刷新: 选型页 "
|
||
+ (_selectionSummaryTable != null ? _selectionSummaryTable.Rows.Count + " 行" : "空"));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] AI替换后刷新异常: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
// ---------- POST /api/importquote:AI 拖入甲方报价 xlsx → 走软件"导入报价"功能 ----------
|
||
|
||
/// <summary>body=JSON:{path:"xlsx 绝对路径", confirm:false|true, mapping:{...可选}}。
|
||
/// ★mapping(推荐,AI 先调 /api/excelgrid 看表后确定,适应任意表样):
|
||
/// {summarySheet:"汇总sheet名", detailSheet:"分项sheet名",
|
||
/// summary:{seq,cabinet,name,model,spec,unit,qty}, detail:{seq,name,spec,unit,qty,price,amount,vendor}}
|
||
/// (0 基列号;spec=-1 或省略=无该列;省略 mapping=软件自动识别表头,回退标准布局)
|
||
/// confirm 缺省/false=只解析不落库,返回图号/箱号/元件计数与分组预览(AI 先给用户确认);
|
||
/// confirm=true=解析后事务导入当前项目库,并在 UI 线程执行与"导入报价"按钮完全同款的
|
||
/// 保存→清缓存→EnterProjectCore 完整刷新。解析/落库零弹窗复用 导入报价.AI无界面执行。</summary>
|
||
private string AI接口_导入报价(string bodyJson)
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId))
|
||
return "{\"ok\":false,\"error\":\"当前没有打开项目,请先在项目列表双击进入项目再导入报价\"}";
|
||
|
||
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
Dictionary<string, object> p;
|
||
try { p = js.Deserialize<Dictionary<string, object>>(bodyJson ?? ""); }
|
||
catch { return "{\"ok\":false,\"error\":\"请求体不是合法 JSON\"}"; }
|
||
if (p == null) return "{\"ok\":false,\"error\":\"请求体为空\"}";
|
||
|
||
string xlsxPath = 取参(p, "path").Trim();
|
||
if (xlsxPath.Length == 0)
|
||
return "{\"ok\":false,\"error\":\"缺 path 字段(甲方报价 xlsx 的绝对路径;AI 收到拖入文件后用其本地路径)\"}";
|
||
bool confirm = false;
|
||
object cf;
|
||
if (p.TryGetValue("confirm", out cf) && cf != null)
|
||
{
|
||
if (cf is bool) confirm = (bool)cf;
|
||
else { string s = Convert.ToString(cf).Trim().ToLowerInvariant(); confirm = (s == "true" || s == "1" || s == "yes"); }
|
||
}
|
||
|
||
if (!File.Exists(xlsxPath))
|
||
return "{\"ok\":false,\"error\":\"文件不存在: " + Json转义(xlsxPath) + "\"}";
|
||
string ext = Path.GetExtension(xlsxPath).ToLowerInvariant();
|
||
if (ext != ".xlsx")
|
||
return "{\"ok\":false,\"error\":\"仅支持 .xlsx 报价文件(当前是 " + Json转义(ext) + ";.xls 老格式请先用 Excel 另存为 .xlsx)\"}";
|
||
|
||
string dbPath = GetQuoteDbPath(_currentProjectId);
|
||
if (!File.Exists(dbPath))
|
||
return "{\"ok\":false,\"error\":\"项目库文件不存在\"}";
|
||
|
||
// 落库前:UI 线程保存报价页未完成编辑(与"导入报价"按钮点击时同款,防编辑丢失)
|
||
if (confirm)
|
||
{
|
||
try
|
||
{
|
||
if (this.InvokeRequired)
|
||
this.Invoke((System.Windows.Forms.MethodInvoker)delegate { SaveAllQuoteTablesIfDirty(false); });
|
||
else
|
||
SaveAllQuoteTablesIfDirty(false);
|
||
}
|
||
catch (Exception exSv) { OpenClawHost.写日志("[AI接口] 导入前保存异常(继续导入): " + exSv.Message); }
|
||
}
|
||
|
||
// 解析(+confirm 时落库):复用导入报价窗体的无界面管线,接口线程执行,界面不卡顿
|
||
Dictionary<string, object> 映射 = null;
|
||
object mv;
|
||
if (p.TryGetValue("mapping", out mv) && mv is Dictionary<string, object>)
|
||
{
|
||
映射 = (Dictionary<string, object>)mv;
|
||
}
|
||
|
||
// ★执行顺序铁律:先只解析→查重→通过后才落库。
|
||
// 否则"重复拦截"返回时数据已经写进库,防重等于失效(还会让用户误以为没导成功)。
|
||
导入报价.AI导入结果 r;
|
||
if (confirm)
|
||
{
|
||
// 第一步:只解析不落库
|
||
r = 导入报价.AI无界面执行(_currentProjectId, dbPath, xlsxPath, false, 映射);
|
||
if (!r.成功)
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(r.错误 ?? "解析失败") + "\"}";
|
||
|
||
List<string> 重复 = 查项目同名图号(dbPath, r);
|
||
bool 允许重复 = false;
|
||
object ad;
|
||
if (p.TryGetValue("allow_duplicate", out ad) && ad != null)
|
||
{
|
||
string ads = Convert.ToString(ad).Trim().ToLowerInvariant();
|
||
允许重复 = (ads == "true" || ads == "1" || ads == "yes");
|
||
}
|
||
if (重复.Count > 0 && !允许重复)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] AI导入报价拦截(重复,未落库): " + Path.GetFileName(xlsxPath)
|
||
+ " 项目已有同名图号 " + 重复.Count + " 个");
|
||
return "{\"ok\":false,\"duplicate\":true,\"tuhao\":" + r.图号数 + ",\"xianghao\":" + r.箱号数
|
||
+ ",\"yuanjian\":" + r.元件数 + ",\"existing\":["
|
||
+ string.Join(",", 重复.Select(t => "\"" + Json转义(t) + "\""))
|
||
+ "],\"error\":\"项目里已存在同名图号 " + 重复.Count + " 个(" + Json转义(重复[0])
|
||
+ (重复.Count > 1 ? " 等" : "") + ")。本次文件导入是追加式,再导会产生双份数据。"
|
||
+ "请向用户说明:若是同一份报价重复导入,就不要再导;确属不同报价可改在软件中删除旧图号后再导,"
|
||
+ "或用户明确同意重复后带 allow_duplicate:true 重试。\"}";
|
||
}
|
||
|
||
// 第二步:查重通过,真正落库
|
||
var r2 = 导入报价.AI无界面执行(_currentProjectId, dbPath, xlsxPath, true, 映射);
|
||
if (!r2.成功)
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(r2.错误 ?? "导入失败") + "\"}";
|
||
r = r2;
|
||
}
|
||
else
|
||
{
|
||
// 预览:只解析不落库
|
||
r = 导入报价.AI无界面执行(_currentProjectId, dbPath, xlsxPath, false, 映射);
|
||
if (!r.成功)
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(r.错误 ?? "解析/导入失败") + "\"}";
|
||
}
|
||
|
||
// 预览模式附带 existing 供 AI 提前预警(落库后的 confirm 响应不需要)
|
||
List<string> 已存在图号 = confirm ? new List<string>() : 查项目同名图号(dbPath, r);
|
||
|
||
if (confirm)
|
||
{
|
||
// 落库后:UI 线程完整刷新(与 button18_Click 成功分支逐行同款:
|
||
// 清缓存+清内存表,再 EnterProjectCore 重算汇总+全表重载+选中联动)
|
||
Exception uiErr = null;
|
||
try
|
||
{
|
||
if (this.InvokeRequired)
|
||
this.Invoke((System.Windows.Forms.MethodInvoker)delegate
|
||
{
|
||
try
|
||
{
|
||
_isReloading = true;
|
||
try
|
||
{
|
||
_xianghaoCache.Clear();
|
||
_yuanjianCache.Clear();
|
||
if (_tuhaoTable != null) { _tuhaoTable.Clear(); _tuhaoTable.AcceptChanges(); }
|
||
if (_xianghaoTable != null) { _xianghaoTable.Clear(); _xianghaoTable.AcceptChanges(); }
|
||
if (_yuanjianTable != null) { _yuanjianTable.Clear(); _yuanjianTable.AcceptChanges(); }
|
||
}
|
||
finally { _isReloading = false; }
|
||
EnterProjectCore(_currentProjectId);
|
||
AI导入后强制重绘(); // ★防"报价页刚切换布局未完成导致 Invalidate 落空"
|
||
}
|
||
catch (Exception exInner) { uiErr = exInner; }
|
||
});
|
||
else
|
||
{
|
||
_isReloading = true;
|
||
try
|
||
{
|
||
_xianghaoCache.Clear();
|
||
_yuanjianCache.Clear();
|
||
if (_tuhaoTable != null) { _tuhaoTable.Clear(); _tuhaoTable.AcceptChanges(); }
|
||
if (_xianghaoTable != null) { _xianghaoTable.Clear(); _xianghaoTable.AcceptChanges(); }
|
||
if (_yuanjianTable != null) { _yuanjianTable.Clear(); _yuanjianTable.AcceptChanges(); }
|
||
}
|
||
finally { _isReloading = false; }
|
||
EnterProjectCore(_currentProjectId);
|
||
AI导入后强制重绘();
|
||
}
|
||
}
|
||
catch (Exception exInv) { uiErr = exInv; }
|
||
|
||
OpenClawHost.写日志("[AI接口] AI导入报价: " + Path.GetFileName(xlsxPath)
|
||
+ " 图号" + r.图号数 + " 箱号" + r.箱号数 + " 元件" + r.元件数
|
||
+ (uiErr != null ? " (界面刷新异常: " + uiErr.Message + ")" : ""));
|
||
if (uiErr != null)
|
||
return "{\"ok\":true,\"confirmed\":true,\"tuhao\":" + r.图号数 + ",\"xianghao\":" + r.箱号数
|
||
+ ",\"yuanjian\":" + r.元件数 + ",\"warning\":\"数据已导入,但界面刷新异常: "
|
||
+ Json转义(uiErr.Message) + "\"}";
|
||
return "{\"ok\":true,\"confirmed\":true,\"tuhao\":" + r.图号数 + ",\"xianghao\":" + r.箱号数
|
||
+ ",\"yuanjian\":" + r.元件数 + "}";
|
||
}
|
||
|
||
// 预览模式:返回分组明细供 AI 向用户汇报
|
||
OpenClawHost.写日志("[AI接口] AI导入报价预览: " + Path.GetFileName(xlsxPath)
|
||
+ " 图号" + r.图号数 + " 箱号" + r.箱号数 + " 元件" + r.元件数
|
||
+ (已存在图号.Count > 0 ? " [项目已有同名图号" + 已存在图号.Count + "个]" : ""));
|
||
var root = new Dictionary<string, object>();
|
||
root["ok"] = true;
|
||
root["confirmed"] = false;
|
||
root["file"] = xlsxPath;
|
||
root["tuhao"] = r.图号数;
|
||
root["xianghao"] = r.箱号数;
|
||
root["yuanjian"] = r.元件数;
|
||
root["existing"] = 已存在图号;
|
||
root["groups"] = r.分组预览;
|
||
return js.Serialize(root);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] 导入报价异常: " + ex.Message);
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(ex.Message) + "\"}";
|
||
}
|
||
}
|
||
|
||
/// <summary>★AI 导入后界面补刷:EnterProjectCore 已把三表数据读进内存并切到报价页,
|
||
/// 但 AI 导入时用户往往正停在 AI 面板——tabPage4 刚被切换、布局/句柄尚未就绪,
|
||
/// Reload 链里的 Invalidate() 可能落在控件真正显示之前而被丢弃(症状:报价页空白,
|
||
/// 手动切出再切回才显示)。这里延迟一拍(排队到 tab 切换/布局消息之后),
|
||
/// 用内存表重设行数并强制三个虚拟模式 grid 立即重绘;不重查数据库。</summary>
|
||
private void AI导入后强制重绘()
|
||
{
|
||
this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate
|
||
{
|
||
try
|
||
{
|
||
Action<System.Windows.Forms.DataGridView, System.Data.DataTable> 刷 = (grid, dt) =>
|
||
{
|
||
if (grid == null || dt == null) return;
|
||
int n = dt.Rows.Count;
|
||
if (grid.RowCount != n) grid.RowCount = n;
|
||
grid.Invalidate();
|
||
grid.Refresh(); // Invalidate+Update,强制立即重绘,触发 CellValueNeeded 重新供数
|
||
};
|
||
刷(this.advancedDataGridView9, _tuhaoTable);
|
||
刷(this.advancedDataGridView4, _xianghaoTable);
|
||
刷(this.advancedDataGridView5, _yuanjianTable);
|
||
OpenClawHost.写日志("[AI接口] 导入后补刷: 图号grid行="
|
||
+ (this.advancedDataGridView9 == null ? -1 : this.advancedDataGridView9.RowCount)
|
||
+ " 可见=" + (this.advancedDataGridView9 != null && this.advancedDataGridView9.Visible)
|
||
+ " 句柄=" + (this.advancedDataGridView9 != null && this.advancedDataGridView9.IsHandleCreated));
|
||
}
|
||
catch (Exception ex) { OpenClawHost.写日志("[AI接口] 导入后补刷异常: " + ex.Message); }
|
||
});
|
||
}
|
||
|
||
/// <summary>查当前项目图号表(advancedDataGridView9Table)里与本次解析结果同名的图号(防重复导入)。只读,异常时返回空表。</summary>
|
||
private List<string> 查项目同名图号(string dbPath, 导入报价.AI导入结果 r)
|
||
{
|
||
var res = new List<string>();
|
||
try
|
||
{
|
||
var names = new HashSet<string>();
|
||
foreach (var g in r.分组预览)
|
||
{
|
||
string n = Convert.ToString(g["tuhao"]) ?? "";
|
||
n = n.Trim();
|
||
if (n.Length > 0) names.Add(n);
|
||
}
|
||
if (names.Count == 0) return res;
|
||
using (var cn = new System.Data.SQLite.SQLiteConnection("Data Source=" + dbPath + ";Version=3;"))
|
||
{
|
||
cn.Open();
|
||
using (var cmd = cn.CreateCommand())
|
||
{
|
||
cmd.CommandText = "SELECT 图号 FROM advancedDataGridView9Table";
|
||
using (var rd = cmd.ExecuteReader())
|
||
{
|
||
while (rd.Read())
|
||
{
|
||
if (rd.IsDBNull(0)) continue;
|
||
string t = (Convert.ToString(rd.GetValue(0)) ?? "").Trim();
|
||
if (t.Length > 0 && names.Contains(t) && !res.Contains(t)) res.Add(t);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex) { OpenClawHost.写日志("[AI接口] 查同名图号异常: " + ex.Message); }
|
||
return res;
|
||
}
|
||
|
||
// ---------- POST /api/excelgrid:把 xlsx 表头区原样吐给 AI(LLM) 看表定列 ----------
|
||
|
||
/// <summary>body=JSON:{path:"xlsx 绝对路径", sheet:"sheet名(可选)", rows:30, cols:12}。
|
||
/// 不带 sheet → 返回 {ok, sheets:[{name,rows}]}(工作表清单);
|
||
/// 带 sheet → 返回 {ok, sheet, rows, grid:[[每行各列文本...]]}(公式取缓存结果)。
|
||
/// AI 看网格判断哪列是柜号/名称/型号/单位/数量后,把 mapping 交给 /api/importquote。</summary>
|
||
private string AI接口_Excel网格(string bodyJson)
|
||
{
|
||
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId))
|
||
return "{\"ok\":false,\"error\":\"尚未打开任何项目,请先在软件中进入一个项目\"}";
|
||
var p = js.Deserialize<Dictionary<string, object>>(bodyJson ?? "{}");
|
||
if (p == null) return "{\"ok\":false,\"error\":\"无效的 JSON 请求体\"}";
|
||
string 路径 = 取参(p, "path");
|
||
if (string.IsNullOrWhiteSpace(路径))
|
||
return "{\"ok\":false,\"error\":\"缺少 path 参数(xlsx 绝对路径)\"}";
|
||
if (!File.Exists(路径))
|
||
return "{\"ok\":false,\"error\":\"文件不存在: " + Json转义(路径) + "\"}";
|
||
if (!string.Equals(Path.GetExtension(路径), ".xlsx", StringComparison.OrdinalIgnoreCase))
|
||
return "{\"ok\":false,\"error\":\"只支持 .xlsx 格式\"}";
|
||
|
||
string sheet名 = 取参(p, "sheet");
|
||
int 最大行 = 30, 最大列 = 12;
|
||
object v;
|
||
if (p.TryGetValue("rows", out v)) { try { 最大行 = Math.Min(Convert.ToInt32(v), 200); } catch { } }
|
||
if (p.TryGetValue("cols", out v)) { try { 最大列 = Math.Min(Convert.ToInt32(v), 30); } catch { } }
|
||
|
||
string err;
|
||
object data = 导入报价.AI网格数据(路径, sheet名, 最大行, 最大列, out err);
|
||
if (data == null)
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(err ?? "读取失败") + "\"}";
|
||
var root = (Dictionary<string, object>)data;
|
||
root["ok"] = true;
|
||
root["file"] = 路径;
|
||
return js.Serialize(root);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
OpenClawHost.写日志("[AI接口] Excel网格异常: " + ex.Message);
|
||
return "{\"ok\":false,\"error\":\"" + Json转义(ex.Message) + "\"}";
|
||
}
|
||
}
|
||
|
||
// ---------- 断路器分类 ----------
|
||
|
||
/// <summary>断路器分类(只认塑壳/微型两类可替换):
|
||
/// 依据 类型列 → 名称关键词 → 规格前缀(国标类组+各品牌主流系列全家),
|
||
/// 返回 "塑壳" / "微型" / "排除"(明确非断路器:隔离开关/熔断器/转换开关/万能式) / null(识别不到)。
|
||
/// 识别不到(null)的行由 AI(LLM) 纠错出规范型号后再验一次——见 AI接口分类放行。</summary>
|
||
internal static string 分类断路器(string 名称, string 规格, string 类型)
|
||
{
|
||
string t = (类型 ?? "") + "|" + (名称 ?? "");
|
||
string g = (规格 ?? "").Trim().ToUpperInvariant().Replace(" ", "");
|
||
// 0) ★明确排除类:隔离开关/熔断器/转换开关/双电源/接触器/万能式——绝不允许替换
|
||
string[] 排除词 = { "隔离", "熔断", "转换开关", "双电源", "ATSE", "PC级", "CB级", "接触器",
|
||
"HGL", "HG-", "HR3", "HR5", "HR6", "QSA", "GLR", "LG-30", "NH40" };
|
||
string[] 万能前缀 = { "DW15", "DW16", "DW17", "DW45", "DW450", "CW1", "CW2", "CW3",
|
||
"NA1", "NA2", "HA1", "HSW1", "CW-", "DW-" };
|
||
// 1) 类型/名称列明示(优先)
|
||
if (t.Contains("塑壳")) return "塑壳";
|
||
if (t.Contains("微型") || t.Contains("小型")) return "微型";
|
||
if (t.Contains("万能式") || t.Contains("框架")) return "排除";
|
||
foreach (string k in 排除词) if (t.Contains(k)) return "排除";
|
||
// 2) 规格含排除词/万能前缀(明确不换)
|
||
foreach (string k in 排除词) if (g.Contains(k)) return "排除";
|
||
foreach (string k in 万能前缀) if (g.StartsWith(k, StringComparison.Ordinal)) return "排除";
|
||
// 3) 规格前缀:微型(终端/小型断路器,各品牌全家)
|
||
string[] 微型前缀 = {
|
||
// 国标类组
|
||
"DZ47", "DZ26", "DZ158", "DZ30", "DZL18",
|
||
// 正泰/德力西
|
||
"NB1", "NB2", "NB3", "NB7", "NB8", "NBH", "NXB", "NBHM", "NXBLE", "DZ47S",
|
||
// 施耐德
|
||
"IC65", "IEC65", "EA9", "DPN", "ISDP", "ISD", "C65", "ACT9", "K37",
|
||
// ABB
|
||
"SH2", "S2", "S20", "SN201", "E219", "E419",
|
||
// 西门子/伊顿/常熟等
|
||
"5SU", "5SJ", "5SL", "5SY", "5SB", "PZM", "PZ30", "XHB1", "TM30", "SB-", "3VU",
|
||
// 人民/良信/天正
|
||
"RDB", "NM8LE63", "NB1LE", "TXB1", "TIB1", "TDB1" };
|
||
// 4) 规格前缀:塑壳(各品牌全家)
|
||
string[] 塑壳前缀 = {
|
||
// 正泰
|
||
"NM1", "NM8", "NM10", "NM6", "NM7", "NM8S", "NM8L", "NM8N", "NM8NL", "NM8NDC", "NHM8", "NXM", "NXMS", "NXMF", "NXMN",
|
||
// 德力西
|
||
"CDM1", "CDM2", "CDM3", "CDM6", "CDK1", "CDW1", "DZ15", "DZ20", "DZ108", "DZL-", "DZ153",
|
||
// 施耐德
|
||
"NS", "NSX", "NSE", "NS2", "CVS", "EZD", "NSC",
|
||
// ABB
|
||
"XT1", "XT2", "XT4", "XT5", "XT6", "XT7", "TMAX", "ISOMAX", "SACE",
|
||
// 西门子
|
||
"3VA", "3VL", "3VT", "3PQ",
|
||
// 常熟/人民/良信/天正/环宇
|
||
"CM1", "CM2", "CM3", "CMZ", "RMM1", "RMM2", "RMW", "NDM2", "NDM3", "TGM", "TGM1", "HUM8", "HUM18", "HSM1", "SM1", "SM30", "XKM", "HSW-", "GSM1" };
|
||
foreach (string k in 微型前缀) if (g.StartsWith(k, StringComparison.Ordinal)) return "微型";
|
||
foreach (string k in 塑壳前缀) if (g.StartsWith(k, StringComparison.Ordinal)) return "塑壳";
|
||
// 5) 兜底:名称含断路器字样的再判一次
|
||
if (t.Contains("断路器"))
|
||
{
|
||
foreach (string k in 微型前缀) if (g.StartsWith(k, StringComparison.Ordinal)) return "微型";
|
||
foreach (string k in 塑壳前缀) if (g.StartsWith(k, StringComparison.Ordinal)) return "塑壳";
|
||
}
|
||
return null; // 识别不到 → 交给 AI 纠错后用新型号再验(见 AI接口分类放行)
|
||
}
|
||
|
||
/// <summary>AI 替换的分类放行(双通道):旧行本地分类 或 AI 纠错后的新型号本地分类
|
||
/// 任一 ∈ {塑壳, 微型} 即放行;旧行命中"排除"类(隔离开关/万能式等)则坚决拒绝。
|
||
/// 这样打错的型号(如 DZ4/PLE)由 AI 纠错成规范型号(DZ47sLE…)后能过防线,非断路器仍拦得住。</summary>
|
||
private static string AI接口分类放行(string jiuMing, string jiuGui, string leixing, string xinMing, string xinGui, out string 拒绝原因)
|
||
{
|
||
拒绝原因 = "";
|
||
string 旧 = 分类断路器(jiuMing, jiuGui, leixing);
|
||
if (string.Equals(旧, "排除"))
|
||
{
|
||
拒绝原因 = "目标行是明确非断路器类(隔离开关/万能式/熔断器等),按规则不替换";
|
||
return null;
|
||
}
|
||
if (string.Equals(旧, "塑壳") || string.Equals(旧, "微型")) return 旧;
|
||
// 旧行识别不到(null,可能是打错的型号)→ 用 AI 纠错后的新型号再验
|
||
string 新 = 分类断路器(xinMing, xinGui, leixing);
|
||
if (string.Equals(新, "排除"))
|
||
{
|
||
拒绝原因 = "替换目标型号是明确非断路器类,拒绝";
|
||
return null;
|
||
}
|
||
if (string.Equals(新, "塑壳") || string.Equals(新, "微型")) return 新 + "(按纠错型号)";
|
||
拒绝原因 = "旧行与纠错后型号都无法识别为塑壳/微型断路器(请先把规格纠错成规范型号再替换)";
|
||
return null;
|
||
}
|
||
|
||
// ---------- 附件码语法域校验(2026-09-25 用户要求:不存在的型号必须拒绝,如 32008) ----------
|
||
|
||
private static readonly HashSet<int> 标准电流档位表 = new HashSet<int> {
|
||
6, 10, 16, 20, 25, 32, 40, 50, 60, 63, 80, 100, 125, 140, 150, 160, 180, 200, 225,
|
||
250, 315, 350, 400, 500, 630, 700, 800, 1000, 1250, 1600, 2000, 2500 };
|
||
|
||
/// <summary>校验新规格里的附件码语法域。只拦"确证非法"的:
|
||
/// 4位码各位值域(极数2/3/4、脱扣2/3、附件十位0-7、个位0/8)、
|
||
/// 5位码第5位用途代号仅可为2(★用户确认:33002末位2=电动机保护,32008必不存在)、
|
||
/// 6位以上数字码。1/2/3位码与字母码(老NM1/NM2/NM8等系列制不同)交给技能侧系列字典判定,C#不硬拦。
|
||
/// 返回 null=通过;否则=拒绝原因。</summary>
|
||
internal static string 校验断路器附件码(string 规格)
|
||
{
|
||
string g = (规格 ?? "").Trim().ToUpperInvariant();
|
||
int slash = g.IndexOf('/');
|
||
if (slash < 0) return null;
|
||
var m = System.Text.RegularExpressions.Regex.Match(g.Substring(slash + 1), @"^[0-9A-Z]+");
|
||
if (!m.Success) return null;
|
||
string tok = m.Value;
|
||
if (!System.Text.RegularExpressions.Regex.IsMatch(tok, @"^\d+A?$")) return null; // 混合段(1P/C16等微型写法)不拦
|
||
bool glued = tok.EndsWith("A");
|
||
string digits = glued ? tok.Substring(0, tok.Length - 1) : tok;
|
||
if (!glued) return 数字码校验(digits);
|
||
// 尾部粘连电流(如 3300200A=码+电流):任一切分(码合法 且 电流∈标准档位)通过即可
|
||
if (数字码校验(digits) == null) return null;
|
||
for (int cl = 1; cl < digits.Length; cl++)
|
||
{
|
||
int cur;
|
||
if (!int.TryParse(digits.Substring(cl), out cur) || cur <= 0) continue;
|
||
if (!标准电流档位表.Contains(cur)) continue;
|
||
if (数字码校验(digits.Substring(0, cl)) == null) return null;
|
||
}
|
||
return "规格\"" + 规格 + "\"的码段无法解析成合法(附件码+标准档位电流)组合,型号不存在";
|
||
}
|
||
|
||
/// <summary>4/5位及超长数字码的语法域判定(与技能知识库同源)。返回 null=通过。</summary>
|
||
private static string 数字码校验(string code)
|
||
{
|
||
if (code.Length >= 6)
|
||
return "附件代号" + code + "非法:位数超过5位,不是合法附件码";
|
||
if (code.Length == 5)
|
||
{
|
||
if (code[4] != '2')
|
||
return "附件代号" + code + "非法:第5位用途代号仅可为空(配电保护)或2(电动机保护)——型号不存在";
|
||
code = code.Substring(0, 4);
|
||
}
|
||
if (code.Length == 4)
|
||
{
|
||
if ("234".IndexOf(code[0]) < 0) return "附件代号" + code + "非法:第1位极数须为2/3/4";
|
||
if ("23".IndexOf(code[1]) < 0) return "附件代号" + code + "非法:第2位脱扣器须为2(电磁式)/3(热磁式)";
|
||
if ("01234567".IndexOf(code[2]) < 0) return "附件代号" + code + "非法:第3位附件十位须为0-7";
|
||
if ("08".IndexOf(code[3]) < 0) return "附件代号" + code + "非法:第4位附件个位须为0(无)/8(报警触头)";
|
||
return null;
|
||
}
|
||
return null; // 1/2/3位码:系列制不同(老NM1/NM2等),交技能侧系列字典判定
|
||
}
|
||
|
||
|
||
// ---------- 小工具 ----------
|
||
|
||
private static string 取串(System.Data.SQLite.SQLiteDataReader rd, string 列)
|
||
{
|
||
try
|
||
{
|
||
object o = rd[列];
|
||
return o == null || o == DBNull.Value ? "" : o.ToString().Trim();
|
||
}
|
||
catch { return ""; }
|
||
}
|
||
|
||
private static double 取数(System.Data.SQLite.SQLiteDataReader rd, string 列)
|
||
{
|
||
try
|
||
{
|
||
object o = rd[列];
|
||
if (o == null || o == DBNull.Value) return 0;
|
||
double d;
|
||
double.TryParse(o.ToString(), out d);
|
||
return d;
|
||
}
|
||
catch { return 0; }
|
||
}
|
||
|
||
private static string 取参(Dictionary<string, object> p, string key)
|
||
{
|
||
object o;
|
||
if (p != null && p.TryGetValue(key, out o) && o != null) return o.ToString();
|
||
return "";
|
||
}
|
||
|
||
private static string Json转义(string s)
|
||
{
|
||
if (string.IsNullOrEmpty(s)) return "";
|
||
return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", " ").Replace("\n", " ");
|
||
}
|
||
}
|
||
}
|