using System; using System.IO; using System.Threading.Tasks; using System.Windows.Forms; using WeifenLuo.WinFormsUI.Docking; namespace 成套报价软件 { /// 停靠窗口:AI助手。WebView2 内嵌 ZCode Web 面板(zai-org/ZCode 开源版,--web 本机服务)。 /// 引擎由 ai助手\启动引擎.cmd 常驻拉起(本软件不含进程启动逻辑), /// 窗口只做:配置就绪 → 健康探测 → 带令牌导航;未启动时显示引导占位页。 public partial class DockWindow10 : DockContent { private bool _初始化已开始; private System.Windows.Forms.Timer _重试Timer; private int _重试次数; public DockWindow10() { InitializeComponent(); this.BackColor = System.Drawing.Color.White; Text = "AI助手"; TabText = "AI助手"; // 与右侧工具区其他窗口(元件库/用户库/母排等)一致:锁定 DockRight + 禁止关闭(常驻) this.DockAreas = WeifenLuo.WinFormsUI.Docking.DockAreas.DockRight; this.CloseButtonVisible = false; // ★ G2-3(2026-10-01):原工具栏 btn设置/btn重试 已移到功能区「设置」页 AI助手组, // 对应逻辑:AI设置→new AI助手设置() 窗体;重连→本类 重新连接() this.VisibleChanged += DockWindow10_VisibleChanged; } /// ★ G2-3(2026-10-01):供功能区「AI重连」按钮调用—— /// 停止后台重试并立即重新探测/连接 AI 引擎(原工具栏"重新连接"按钮逻辑)。 public async void 重新连接() { 停止重试(); await 连接(); } private async void DockWindow10_VisibleChanged(object sender, EventArgs e) { if (!this.Visible || _初始化已开始) return; _初始化已开始 = true; await 连接(); } // ★ G2-3(2026-10-01):btn重试_Click/btn设置_Click 已删(按钮移功能区);逻辑并入 重新连接() 与功能区路由 /// 初始化 WebView2(独立用户数据目录 aiui数据,与元件库隔离)→ 探测网关 → /// 就绪则带令牌导航 Control UI,否则显示引导页并后台重试(引擎可能正在启动,90 秒内就绪即自动进入)。 private async Task 连接() { try { if (this.webView101.CoreWebView2 == null) { string 用户数据 = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "aiui数据"); var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(null, 用户数据); await this.webView101.EnsureCoreWebView2Async(env); // ★ 默认浅色主题(2026-09-26 用户要求):ZCode 前端主题存 localStorage 'zcode-theme', // 无值时前端默认 zai-dark——这里在文档创建前把无值状态预置为 zai-light; // 用户在面板里主动切换过(键已有值)则不动,尊重其选择。 try { await this.webView101.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync( "try{if(!localStorage.getItem('zcode-theme'))localStorage.setItem('zcode-theme','zai-light')}catch(e){}"); } catch (Exception exT) { OpenClawHost.写日志("[AI助手] 默认浅色主题注入失败: " + exT.Message); } // ★聊天页导航成功后注入"一键替换选型规格"提示按钮(2026-09-22 用户要求: // 按钮在 AI助手 里,点击即把固定指令发给 AI 开跑替换流程) this.webView101.CoreWebView2.NavigationCompleted += (s, e) => { if (e.IsSuccess) 安装提示按钮(); }; } } catch (Exception ex) { OpenClawHost.写日志("[AI助手] WebView2 初始化失败: " + ex.Message); MessageBox.Show(this, "WebView2 初始化失败:" + ex.Message, "AI助手", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } bool 就绪 = await Task.Run(() => OpenClawHost.网关就绪()); if (就绪) { 导航面板(); } else { 显示引导页(); 启动重试(); } } /// 聊天页内注入悬浮提示按钮:点击把固定指令填进聊天输入框并自动发送。 /// ★2026-09-26 按 ZCode Web 实测 DOM 重写(playwright 无头探查结论): /// 输入框=[contenteditable=true][role=textbox](富文本编辑器,非 textarea); /// 写入=focus→selectAll→execCommand insertText(实测编辑器接受且框架 state 同步, /// 用户随后打字正常追加——旧版误走 textarea 分支导致打字被覆盖的根治); /// 发送=button[aria-label="发送"](实测存在),未就绪/禁用时轮询等待再点。 /// ★按钮可拖动:按住拖到任意位置自动记住(localStorage),下次注入恢复; /// 拖动(位移>6px)不触发点击。脚本自带重试等待编辑器挂载,window 标记防重复注入。 private async void 安装提示按钮() { try { string js = @"(function(){ if (window.__dqbTipInstalled) return 'already'; window.__dqbTipInstalled = true; var PROMPT = '读取当前选型界面的规格并且帮我替换这些规格'; var POS_KEY = '__dqbTipPos'; function 找输入框(){ return document.querySelector('[contenteditable=true][role=textbox]') || document.querySelector('[contenteditable=true]') || document.querySelector('textarea:not([disabled])'); } function 写入(t, text){ t.focus(); if (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT') { var proto = t.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; var set = Object.getOwnPropertyDescriptor(proto, 'value').set; set.call(t, text); t.dispatchEvent(new Event('input', { bubbles: true })); } else { document.execCommand('selectAll', false, null); document.execCommand('insertText', false, text); } } function 找发送钮(){ var b = document.querySelector('button[aria-label=""发送""]'); if (b && !b.disabled) return b; var btns = document.querySelectorAll('button'); for (var i = 0; i < btns.length; i++) { var s = btns[i]; var 语义 = (s.getAttribute('aria-label') || '') + ' ' + (s.title || '') + ' ' + (s.className || ''); if (!s.disabled && /send|submit|发送|提交/i.test(语义)) return s; } return null; } function 发送(){ var tries = 0; (function 试(){ tries++; var b = 找发送钮(); if (b) { b.click(); return; } if (tries < 16) { setTimeout(试, 500); return; } // 最多等 8 秒(编辑器有内容后发送钮才出现) var t = 找输入框(); if (t) { // 末路兜底:Enter 键序(合成事件对部分编辑器无效,仅一试) var o = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true }; t.dispatchEvent(new KeyboardEvent('keydown', o)); t.dispatchEvent(new KeyboardEvent('keyup', o)); } })(); } var tries = 0; function 安装(){ tries++; if (!document.body) { if (tries < 60) setTimeout(安装, 1000); return; } if (document.getElementById('__dqbTipBtn')) return; var b = document.createElement('button'); b.id = '__dqbTipBtn'; b.type = 'button'; b.textContent = '⚡ 读取选型规格并替换'; b.title = '一键发送:读取当前选型界面的规格并且帮我替换这些规格(默认只替换塑壳/微型断路器)。按住可拖动,位置自动记住。'; b.style.cssText = 'position:fixed;right:18px;bottom:130px;z-index:99999;border:none;cursor:grab;' + 'background:linear-gradient(135deg,#4f7cff,#6a5cff);color:#fff;padding:8px 14px;border-radius:18px;' + 'font:13px sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.25);user-select:none;touch-action:none;'; // 恢复记住的位置(并夹回视口内) try { var p = localStorage.getItem(POS_KEY); if (p) { var xy = p.split(','); var x = Math.max(4, Math.min(innerWidth - 60, parseInt(xy[0], 10))); var y = Math.max(4, Math.min(innerHeight - 40, parseInt(xy[1], 10))); b.style.left = x + 'px'; b.style.top = y + 'px'; b.style.right = 'auto'; b.style.bottom = 'auto'; } } catch (e) { } // 拖动+点击合一:pointer 按下记录起点,移动>6px=拖动(实时跟手),松手存位;未拖动=执行点击动作 var 拖动中 = false, 已拖 = false, sx = 0, sy = 0; b.addEventListener('pointerdown', function(e){ if (e.button !== 0) return; 拖动中 = true; 已拖 = false; sx = e.clientX; sy = e.clientY; var r = b.getBoundingClientRect(); b.__ox = e.clientX - r.left; b.__oy = e.clientY - r.top; b.setPointerCapture(e.pointerId); b.style.cursor = 'grabbing'; e.preventDefault(); }); b.addEventListener('pointermove', function(e){ if (!拖动中) return; var dx = e.clientX - sx, dy = e.clientY - sy; if (!已拖 && Math.abs(dx) < 6 && Math.abs(dy) < 6) return; 已拖 = true; b.style.left = Math.max(0, Math.min(innerWidth - b.offsetWidth, e.clientX - b.__ox)) + 'px'; b.style.top = Math.max(0, Math.min(innerHeight - b.offsetHeight, e.clientY - b.__oy)) + 'px'; b.style.right = 'auto'; b.style.bottom = 'auto'; }); b.addEventListener('pointerup', function(e){ if (!拖动中) return; 拖动中 = false; b.style.cursor = 'grab'; try { b.releasePointerCapture(e.pointerId); } catch (ex) { } if (已拖) { var r = b.getBoundingClientRect(); try { localStorage.setItem(POS_KEY, Math.round(r.left) + ',' + Math.round(r.top)); } catch (ex) { } return; // 拖动不触发点击 } var t = 找输入框(); if (!t) return; 写入(t, PROMPT); setTimeout(发送, 250); }); document.body.appendChild(b); } 安装(); return 'ok'; })()"; await this.webView101.CoreWebView2.ExecuteScriptAsync(js); } catch (Exception ex) { OpenClawHost.写日志("[AI助手] 提示按钮注入失败: " + ex.Message); } } // (2026-09-28/29 的拖放桥接自定义代码已整体移除——拖入文件回归 ZCode 原生附件机制) /// 带令牌导航 ZCode Web 面板(token 走 URL query,服务端命中后设 Cookie)。 private void 导航面板() { string url = "http://127.0.0.1:" + OpenClawHost.端口 + "/?token=" + Uri.EscapeDataString(OpenClawHost.令牌); this.webView101.CoreWebView2.Navigate(url); OpenClawHost.写日志("[AI助手] 打开面板 http://127.0.0.1:" + OpenClawHost.端口); } /// 引擎未就绪时每 3 秒探测一次,90 秒内就绪自动进入面板(配合启动器的引擎慢启动)。 private void 启动重试() { if (this.IsDisposed) return; if (_重试Timer == null) { 重试TimerInitializer(); } _重试次数 = 0; _重试Timer.Start(); } private void 重试TimerInitializer() { _重试Timer = new System.Windows.Forms.Timer(); _重试Timer.Interval = 3000; _重试Timer.Tick += 重试Timer_Tick; } private void 重试Timer_Tick(object sender, EventArgs e) { if (this.IsDisposed || !this.IsHandleCreated) { 停止重试(); return; } _重试次数++; if (_重试次数 > 30) { 停止重试(); return; } // 90 秒后放弃(引擎大概没在启动) try { if (OpenClawHost.健康检查(OpenClawHost.端口)) { 停止重试(); 导航面板(); } } catch (Exception ex) { OpenClawHost.写日志("[AI助手] 引擎健康检查重试异常: " + ex.Message); } } private void 停止重试() { if (_重试Timer != null) _重试Timer.Stop(); } /// 引擎未运行时的引导占位页(软件已自动发起启动,等就绪即可)。 private void 显示引导页() { string html = "
" + "

AI 引擎正在启动…

" + "软件已自动拉起 AI 引擎(ZCode,首次启动约需 10~30 秒),就绪后本页自动进入聊天面板。
" + "长时间未进入可点上方「重新连接」。" + "

引擎随软件自动启动/停止;也可在 ai助手 目录手动 启动引擎.cmd / 停止引擎.cmd

" + "
"; this.webView101.CoreWebView2.NavigateToString(html); } } }