基线:全量源码首提(D1 版本控制落地,含第一轮优化 B1-B4/A1/A4/缩放修复)
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user