Files
ctbjrj/成套报价软件/DockWindows/DockWindow10/DockWindow10.cs
T

298 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using WeifenLuo.WinFormsUI.Docking;
namespace 成套报价软件
{
/// <summary>停靠窗口:AI助手。WebView2 内嵌 ZCode Web 面板(zai-org/ZCode 开源版,--web 本机服务)。
/// 引擎由 ai助手\启动引擎.cmd 常驻拉起(本软件不含进程启动逻辑),
/// 窗口只做:配置就绪 → 健康探测 → 带令牌导航;未启动时显示引导占位页。</summary>
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;
}
/// <summary>★ G2-3(2026-10-01):供功能区「AI重连」按钮调用——
/// 停止后台重试并立即重新探测/连接 AI 引擎(原工具栏"重新连接"按钮逻辑)。</summary>
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 已删(按钮移功能区);逻辑并入 重新连接() 与功能区路由
/// <summary>初始化 WebView2(独立用户数据目录 aiui数据,与元件库隔离)→ 探测网关 →
/// 就绪则带令牌导航 Control UI,否则显示引导页并后台重试(引擎可能正在启动,90 秒内就绪即自动进入)。</summary>
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
{
显示引导页();
启动重试();
}
}
/// <summary>聊天页内注入悬浮提示按钮:点击把固定指令填进聊天输入框并自动发送。
/// ★2026-09-26 按 ZCode Web 实测 DOM 重写(playwright 无头探查结论):
/// 输入框=[contenteditable=true][role=textbox](富文本编辑器,非 textarea);
/// 写入=focus→selectAll→execCommand insertText(实测编辑器接受且框架 state 同步,
/// 用户随后打字正常追加——旧版误走 textarea 分支导致打字被覆盖的根治);
/// 发送=button[aria-label="发送"](实测存在),未就绪/禁用时轮询等待再点。
/// ★按钮可拖动:按住拖到任意位置自动记住(localStorage),下次注入恢复;
/// 拖动(位移>6px)不触发点击。脚本自带重试等待编辑器挂载,window 标记防重复注入。</summary>
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 原生附件机制)
/// <summary>带令牌导航 ZCode Web 面板(token 走 URL query,服务端命中后设 Cookie)。</summary>
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.端口);
}
/// <summary>引擎未就绪时每 3 秒探测一次,90 秒内就绪自动进入面板(配合启动器的引擎慢启动)。</summary>
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();
}
/// <summary>引擎未运行时的引导占位页(软件已自动发起启动,等就绪即可)。</summary>
private void 显示引导页()
{
string html = "<!doctype html><html><head><meta charset='utf-8'><style>" +
"body{font-family:'Microsoft YaHei',sans-serif;background:#f6f7fb;color:#333;margin:0;" +
"display:flex;align-items:center;justify-content:center;height:95vh}" +
"div{max-width:560px;text-align:center;line-height:2;font-size:14px}" +
"h2{margin-bottom:6px}" +
"</style></head><body><div>" +
"<h2>AI 引擎正在启动…</h2>" +
"软件已自动拉起 AI 引擎(ZCode,首次启动约需 10~30 秒),就绪后本页自动进入聊天面板。<br>" +
"长时间未进入可点上方「重新连接」。" +
"<p style='color:#8a8f99;font-size:12px'>引擎随软件自动启动/停止;也可在 ai助手 目录手动 启动引擎.cmd / 停止引擎.cmd</p>" +
"</div></body></html>";
this.webView101.CoreWebView2.NavigateToString(html);
}
}
}