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 成套报价软件
{
///
/// 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)。
///
public partial class MainForm
{
private System.Net.HttpListener _aiHttp;
private Thread _aiHttpThread;
private int _aiHttpPort = 18790;
// ---------- 生命周期 ----------
/// 启动本地接口(MainForm.Load 挂调;后台线程,失败仅记日志不弹窗)开。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);
}
}
/// 停止本地接口(FormClosing 挂调)。
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:选型页元件清单(含分类) ----------
/// 照 LoadSelectionSummary 的查询段重新读库聚合(不碰 UI 内存表,线程安全),
/// 输出 JSON:{ok, project, rows:[{mingcheng,guige,danwei,shuliang,biaojia,caigouxishu,baochuxishu,pinpai,xilie,leixing,fenlei,xianghaoshu}]}。
/// 枚举"活"元件表(2026-09-21 删除残留根治配套):只返回图号表里仍存在、且所属箱号行
/// 仍存在的元件表——删除图号/箱号历史残留的孤儿表(表索引行+物理表没清干净)不再进入
/// AI 清单/替换范围。活键判定兼容两套登记:内部ID(主窗口)与显示名(扒图 Form3 老数据)。
private static List 活元件表列表(System.Data.SQLite.SQLiteConnection conn)
{
var result = new List();
try
{
// 1) 活图号键集合(内部ID+显示名都收)
var liveTh = new HashSet(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>(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(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 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>(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 row;
if (!merged.TryGetValue(key, out row))
{
row = new Dictionary();
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();
((HashSet)row["_biaoge"]).Add(realTbl);
merged[key] = row;
}
else
{
row["shuliang"] = Convert.ToDouble(row["shuliang"]) + sl;
((HashSet)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();
root["ok"] = true;
root["project"] = _currentProjectId;
var list = new List>();
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();
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)r["_biaoge"]).Count; // 影响箱号数
list.Add(out2);
}
root["rows"] = list;
root["count"] = list.Count;
return js.Serialize(root);
}
// ---------- POST /api/replace:替换元件(数量铁律) ----------
/// 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}。
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 p;
try { p = js.Deserialize>(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();
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 用户要求) ----------
/// 批量替换:body={"items":[{jiu_mingcheng,jiu_guige,xin_pinming,xin_guige,xin_biaojia,
/// xin_xishu,xin_pinpai,xin_xilie},...]}。单事务逐项按 /api/replace 完全同款规则
/// (品名链/分类防线/活表过滤/派生价重算,数量绝不改)UPDATE,分类不过的项跳过并回报原因,
/// 全部完成只做一次 UI 刷新——比逐行 selectselection+editselection 快一个量级。
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 p;
try { p = js.Deserialize>(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>();
foreach (object o in (System.Collections.IEnumerable)p["items"])
{
var d = o as Dictionary;
if (d != null) items.Add(d);
}
if (items.Count == 0) return "{\"ok\":false,\"error\":\"items 为空\"}";
var tblNames = new List();
var 结果 = new List();
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(点击/读值) ----------
/// body=JSON:{js:"(async()=>{...})()"}。在元件库窗口(w4)的 WebView2 页面里执行脚本,
/// 返回 {ok, result}(result=页面返回值的 JSON 串)。AI 用它像人一样点选系列/壳架/极数/电流并读
/// 页面真实渲染的型号(.p-name)/价格/品名——所见即所得,不再从价格表拼。
private string AI接口_执行页面脚本(string bodyJson)
{
try
{
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
Dictionary p;
try { p = js.Deserialize>(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();
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 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:网页"替换"按钮同款协议 → 全项目替换(数量铁律) ----------
/// body=JSON:{jiu_mingcheng, jiu_guige(定位旧行), name(页面完整型号→规格),
/// price(页面实算价→表价), brand, xishu(折扣→采购系数), xilie(系列), pinming(品名→元件名称)}。
/// 字段语义与页面"替换"按钮的 postMessage 协议完全同款(WebView21_WebMessageReceived→替换选中元件),
/// 但按旧规格替换全项目所有箱号的匹配行,且 ★数量列绝不写(页面按钮会覆盖数量,故替换动作用本接口而非真点按钮)。
/// 分类防线:目标行必须是塑壳/微型断路器。
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 p;
try { p = js.Deserialize>(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();
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 选中选型页的行(可视化定位) ----------
/// body=JSON:{jiu_mingcheng, jiu_guige}。在选型页汇总表里找到匹配行并选中+滚动到可见,
/// 自然触发选中联动(下方明细表加载该元件分布)。返回 {ok,row}。
private string AI接口_选中选型行(string bodyJson)
{
try
{
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
Dictionary p;
try { p = js.Deserialize>(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();
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,复刻手改单元格) ----------
/// body=JSON:{jiu_mingcheng, jiu_guige(定位), xin_mingcheng?, xin_guige, xin_biaojia,
/// xin_xishu, xin_pinpai?, xin_xilie?}。复刻"用户在选型页手改单元格"的完整业务链:
/// 内存表行赋值 → RecalcSelectionRow 行内重算 → 置脏 → SyncSelectionChangesToDb()(现有方法,
/// 按名称+规格全项目同步,SET 天然不含数量列)→ RefreshQuotePageAfterSelectionEdit。
/// 全程不写任何新 SQL;数量列绝不触碰。
private string AI接口_编辑选型行(string bodyJson)
{
try
{
if (string.IsNullOrWhiteSpace(_currentProjectId))
return "{\"ok\":false,\"error\":\"当前没有打开项目\"}";
var js = new System.Web.Script.Serialization.JavaScriptSerializer();
Dictionary p;
try { p = js.Deserialize>(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();
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();
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath2)))
{
conn.Open();
tblNames2 = 活元件表列表(conn);
// 逐表找匹配行(参数化),投喂软件直写队列(列值含派生价,按各行原数量/报出系数算;数量列绝不入队)
foreach (string tbl in tblNames2)
{
var hits = new List