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);
}
}
}