diff --git a/README.md b/README.md index e64e52e..2c3ec0e 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,14 @@ - **独立窗口**:WebView2 渲染 `http://127.0.0.1:3080`,无浏览器地址栏/标签栏 - **自动拉起后端**:打开应用时若 dsh 服务未运行,自动静默启动(node 进程,无 CMD 窗口) +- **启动进度提示**:等待后端就绪时显示进度条 + 百分比 + 后端最新输出(首次冷启动较慢) - **系统托盘**: - 点窗口 `X` → 直接最小化到托盘(后端保持运行) - 托盘双击 / 右键「打开」→ 恢复窗口 - - 托盘右键「退出」→ 完全退出(**同时关闭后端服务**) + - 右键「**开机自启服务**」→ 勾选后开机自动后台启动后端(打开应用秒开) + - 右键「退出」→ 完全退出(**同时关闭后端服务**) +- **应用内升级引导**:启动时后台检查 DSH 最新版本,有新版本弹窗引导升级 + (npm 后台完成,无需打开浏览器/终端;升级后自动重启服务) - **DPI 感知**:PerMonitorV2,高分屏(250% 缩放)下界面清晰不模糊 - **DeepSeek 官方图标**:窗口、任务栏、快捷方式均为官方鲸鱼 logo @@ -24,6 +28,7 @@ DeepSeekHarness\ ├── .gitignore 排除运行时数据 └── app\ ├── dsh-app.exe 主程序(C# / WinForms / WebView2) + ├── start-dsh-service.vbs 开机自启用的隐藏启动器(wscript 调用) ├── Microsoft.Web.WebView2.Core.dll WebView2 托管组件 1.0.3719.77 ├── Microsoft.Web.WebView2.WinForms.dll 同上 ├── WebView2Loader.dll 原生加载器(x64) @@ -31,6 +36,7 @@ DeepSeekHarness\ │ ├── dsh-app.cs 全部源码 │ └── app.manifest DPI manifest(PerMonitorV2) ├── web.log dsh 服务日志(运行时生成) + ├── upgrade.log 升级日志(运行时生成) └── dsh-app.exe.WebView2\ WebView2 用户数据(运行时生成) ``` @@ -46,16 +52,16 @@ DeepSeekHarness\ ## 使用 1. 双击桌面(或 `C:\Users\VanuSystems-Mu\DeskBox\AI\`)的 **DeepSeek Harness** 快捷方式 -2. 首次打开等待几秒(后端自动启动),之后秒开 -3. 用完点 `X` 即最小化到托盘;想彻底退出用托盘右键 → **退出** +2. 首次打开需等待后端冷启动(**2~3 分钟**,窗口有进度提示);之后秒开 +3. 用完点 `X` 即最小化到托盘;彻底退出用托盘右键 → **退出** -> 后端服务日志:`app\web.log`;启动失败排查:`app\start-error.log`(如存在)。 +> **关于启动速度**:慢的根源是 dsh 自身(启动需加载 194 个插件包)。 +> 推荐在托盘菜单勾选「**开机自启服务**」——开机后台预热,之后任何时间打开应用都是秒开。 ## 从源码构建 ```sh cd app\src -# 需要 dsh.ico 与 app.manifest 位于 src 上级(或按需调整路径) csc /target:winexe /nologo \ /win32manifest:app.manifest \ /win32icon:..\..\dsh.ico \ @@ -74,7 +80,8 @@ csc 位于 `C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe`。 |---|---| | 界面模糊 | 确认 exe 已带 manifest(PerMonitorV2);重新构建 | | 弹 CMD 窗口 | 旧版本用 `cmd /c` 启动后端所致;当前版本直接启动 `node.exe`,无窗口 | -| 打开后一直"正在启动服务" | 查看 `web.log`;确认 `dsh` 已全局安装、端口 3080 未被占用 | +| 打开后一直"正在启动服务" | dsh 冷启动需 2~3 分钟,进度条会显示;若超 4 分钟查看 `web.log` | +| 升级失败 | 查看 `app\upgrade.log`;确认 npm 镜像源可用 | | 托盘图标消失 | 进程异常退出;重新双击快捷方式即可(会自动拉起后端) | ## 许可 diff --git a/app/dsh-app.exe b/app/dsh-app.exe index 5d28e78..f611ab8 100644 Binary files a/app/dsh-app.exe and b/app/dsh-app.exe differ diff --git a/app/src/dsh-app.cs b/app/src/dsh-app.cs index d79778c..a0af1b6 100644 --- a/app/src/dsh-app.cs +++ b/app/src/dsh-app.cs @@ -1,4 +1,4 @@ -// DeepSeek Harness — WebView2 独立桌面客户端(托盘常驻版) +// DeepSeek Harness — WebView2 独立桌面客户端 // 编译: csc /target:winexe /nologo /win32manifest:app.manifest /win32icon:dsh.ico // /out:dsh-app.exe /r:Microsoft.Web.WebView2.Core.dll /r:Microsoft.Web.WebView2.WinForms.dll dsh-app.cs using System; @@ -6,8 +6,10 @@ using System.Diagnostics; using System.Drawing; using System.IO; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using Microsoft.Win32; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; @@ -15,11 +17,21 @@ static class Program { private const int Port = 3080; private static readonly string WebUrl = "http://127.0.0.1:3080"; + private const int WaitSteps = 480; // 480 x 500ms = 240s 兜底 + private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; + private const string RunValueName = "DeepSeekHarnessService"; + + private const string NodeExe = @"C:\Program Files\nodejs\node.exe"; + private const string NpmCli = @"C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js"; private static Process serviceProcess; // 本应用拉起的 dsh 服务进程 private static bool startedByUs; // 服务是否由本应用启动 private static NotifyIcon trayIcon; private static Form mainForm; + private static WebView2 webView; + + private static readonly object outputLock = new object(); + private static string lastOutput; // 后端最新输出行(用于进度提示) [STAThread] static void Main() @@ -57,6 +69,14 @@ static class Program var menu = new ContextMenuStrip(); menu.Items.Add("打开 DeepSeek Harness", null, (s, e) => ShowMainWindow()); menu.Items.Add(new ToolStripSeparator()); + var autostart = new ToolStripMenuItem("开机自启服务") + { + CheckOnClick = true, + Checked = IsAutostartEnabled(), + }; + autostart.Click += (s, e) => SetAutostart(autostart.Checked); + menu.Items.Add(autostart); + menu.Items.Add(new ToolStripSeparator()); menu.Items.Add("退出", null, (s, e) => ExitApp()); trayIcon.ContextMenuStrip = menu; @@ -68,6 +88,16 @@ static class Program mainForm.Hide(); }; + // 启动进度界面:阶段文字 + 进度条 + var panel = new TableLayoutPanel + { + Dock = DockStyle.Fill, + RowCount = 2, + ColumnCount = 1, + }; + panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 36)); + var status = new Label { Dock = DockStyle.Fill, @@ -75,28 +105,51 @@ static class Program Font = new Font("Microsoft YaHei UI", 14), Text = "正在启动 DeepSeek Harness 服务…", }; - mainForm.Controls.Add(status); + var bar = new ProgressBar + { + Dock = DockStyle.Fill, + Minimum = 0, + Maximum = WaitSteps, + Value = 0, + Style = ProgressBarStyle.Continuous, + Margin = new Padding(120, 4, 120, 16), + }; + panel.Controls.Add(status, 0, 0); + panel.Controls.Add(bar, 0, 1); + mainForm.Controls.Add(panel); var web = new WebView2 { Dock = DockStyle.Fill }; mainForm.Shown += async (s, e) => { - // 等待服务就绪(最多 ~30 秒) + // 等待服务就绪(最多 ~240 秒,首次冷启动可能较慢),期间显示进度 bool ready = false; - for (int i = 0; i < 60; i++) + for (int i = 0; i < WaitSteps; i++) { await Task.Delay(500); + int pct = Math.Min(99, i * 100 / WaitSteps); + string line; + lock (outputLock) { line = lastOutput; } + bar.Value = Math.Min(bar.Maximum, i); + status.Text = "正在启动 DeepSeek Harness 服务… " + pct + "%\n" + + (string.IsNullOrEmpty(line) ? "正在初始化后端…" : line); if (IsPortOpen(Port)) { ready = true; break; } } if (!ready) { + bar.Value = bar.Maximum; status.Text = "无法连接 DeepSeek Harness 服务(端口 " + Port + ")。\n" + - "请查看日志:" + Path.Combine(AppDir(), "web.log"); + "请查看日志:" + Path.Combine(AppDir(), "web.log") + "\n" + + "或稍后从托盘重新打开窗口重试。"; return; } + bar.Value = bar.Maximum; + status.Text = "服务就绪,正在加载界面…"; + await Task.Delay(300); + mainForm.SuspendLayout(); - mainForm.Controls.Remove(status); + mainForm.Controls.Remove(panel); mainForm.Controls.Add(web); mainForm.ResumeLayout(); @@ -109,6 +162,7 @@ static class Program cp.UserDataFolder = userData; web.CreationProperties = cp; await web.EnsureCoreWebView2Async(); + webView = web; web.Source = new Uri(WebUrl); } catch (Exception ex) @@ -118,6 +172,9 @@ static class Program } }; + // 后台检查 DSH 版本更新(有新版才提示,不阻塞启动) + CheckUpdateAsync(); + Application.Run(mainForm); } @@ -146,13 +203,12 @@ static class Program string logFile = Path.Combine(appDir, "web.log"); string npmDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "npm"); - string nodeExe = @"C:\Program Files\nodejs\node.exe"; string binJs = Path.Combine(npmDir, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"); try { - if (!File.Exists(nodeExe) || !File.Exists(binJs)) return; + if (!File.Exists(NodeExe) || !File.Exists(binJs)) return; - var psi = new ProcessStartInfo(nodeExe) + var psi = new ProcessStartInfo(NodeExe) { UseShellExecute = false, CreateNoWindow = true, @@ -170,17 +226,28 @@ static class Program path = npmDir + ";" + path; psi.EnvironmentVariables["PATH"] = path; + // 禁用 dsh 启动遥测(可能小幅加速冷启动) + psi.EnvironmentVariables["DSH_TELEMETRY_DISABLED"] = "1"; + serviceProcess = Process.Start(psi); startedByUs = true; - // 输出写日志文件(防管道缓冲死锁) + // 输出写日志文件并记录最新行(供进度提示展示) serviceProcess.OutputDataReceived += (s, e) => { - if (e.Data != null) { try { File.AppendAllText(logFile, e.Data + "\r\n"); } catch { } } + if (e.Data != null) + { + lock (outputLock) { lastOutput = e.Data; } + try { File.AppendAllText(logFile, e.Data + "\r\n"); } catch { } + } }; serviceProcess.ErrorDataReceived += (s, e) => { - if (e.Data != null) { try { File.AppendAllText(logFile, e.Data + "\r\n"); } catch { } } + if (e.Data != null) + { + lock (outputLock) { lastOutput = e.Data; } + try { File.AppendAllText(logFile, e.Data + "\r\n"); } catch { } + } }; serviceProcess.BeginOutputReadLine(); serviceProcess.BeginErrorReadLine(); @@ -219,4 +286,158 @@ static class Program } Application.Exit(); } + + // ---- 开机自启 ---- + + static bool IsAutostartEnabled() + { + try + { + using (var key = Registry.CurrentUser.OpenSubKey(RunKeyPath)) + { + return key != null && key.GetValue(RunValueName) != null; + } + } + catch { return false; } + } + + static void SetAutostart(bool enable) + { + try + { + using (var key = Registry.CurrentUser.CreateSubKey(RunKeyPath)) + { + if (enable) + { + string vbs = Path.Combine(AppDir(), "start-dsh-service.vbs"); + key.SetValue(RunValueName, "wscript.exe \"" + vbs + "\""); + } + else + { + key.DeleteValue(RunValueName, false); + } + } + } + catch { } + } + + // ---- DSH 版本检查与升级引导(应用内完成,无需浏览器/终端)---- + + static void CheckUpdateAsync() + { + Task.Run(() => + { + try + { + Thread.Sleep(4000); // 等窗口稳定后再检查 + string local = GetLocalVersion(); + string latest = GetLatestVersion(); + if (string.IsNullOrEmpty(latest) || latest == local) return; + string msg = "发现 DSH 新版本 " + latest + "(当前 " + local + ")。\n" + + "是否现在升级?升级在后台完成,无需打开浏览器或终端。"; + mainForm.BeginInvoke((Action)(() => + { + var r = MessageBox.Show(mainForm, msg, "DeepSeek Harness 更新", + MessageBoxButtons.YesNo, MessageBoxIcon.Information); + if (r != DialogResult.Yes) return; + bool ok = RunNpmUpgrade(); + if (ok) + { + // 重启服务以加载新版本 + try { if (serviceProcess != null && !serviceProcess.HasExited) serviceProcess.Kill(); } catch { } + Thread.Sleep(1500); + startedByUs = false; + StartDshService(); + MessageBox.Show(mainForm, + "升级完成(" + local + " → " + latest + "),服务已重启。\n界面将自动刷新。", + "DeepSeek Harness", MessageBoxButtons.OK, MessageBoxIcon.Information); + try { if (webView != null) webView.Reload(); } catch { } + } + else + { + MessageBox.Show(mainForm, + "升级失败,请查看日志:" + Path.Combine(AppDir(), "upgrade.log"), + "DeepSeek Harness", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + })); + } + catch { } + }); + } + + static string GetLocalVersion() + { + try + { + string pkg = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "npm", "node_modules", "@deepseek-ai", "dsh", "package.json"); + if (File.Exists(pkg)) + { + var doc = new System.Text.RegularExpressions.Regex("\"version\"\\s*:\\s*\"([^\"]+)\""); + var m = doc.Match(File.ReadAllText(pkg)); + if (m.Success) return m.Groups[1].Value; + } + } + catch { } + return ""; + } + + static string GetLatestVersion() + { + try + { + string stdout = ""; + var psi = new ProcessStartInfo(NodeExe) + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + Arguments = "\"" + NpmCli + "\" view @deepseek-ai/dsh version", + }; + using (var p = Process.Start(psi)) + { + p.ErrorDataReceived += (s, e) => { }; + p.BeginErrorReadLine(); + stdout = p.StandardOutput.ReadToEnd(); + p.WaitForExit(60000); + } + return stdout.Trim(); + } + catch { return ""; } + } + + static bool RunNpmUpgrade() + { + string logFile = Path.Combine(AppDir(), "upgrade.log"); + try + { + var psi = new ProcessStartInfo(NodeExe) + { + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + RedirectStandardOutput = true, + RedirectStandardError = true, + Arguments = "\"" + NpmCli + "\" install -g @deepseek-ai/dsh@latest", + }; + using (var p = Process.Start(psi)) + { + p.OutputDataReceived += (s, e) => + { + if (e.Data != null) { try { File.AppendAllText(logFile, e.Data + "\r\n"); } catch { } } + }; + p.ErrorDataReceived += (s, e) => + { + if (e.Data != null) { try { File.AppendAllText(logFile, e.Data + "\r\n"); } catch { } } + }; + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + p.WaitForExit(180000); + return p.ExitCode == 0; + } + } + catch { return false; } + } } diff --git a/app/start-dsh-service.vbs b/app/start-dsh-service.vbs new file mode 100644 index 0000000..d765672 --- /dev/null +++ b/app/start-dsh-service.vbs @@ -0,0 +1,4 @@ +' Start dsh (DeepSeek Harness) service in background, no window. +' Used by "autostart" tray option (registry Run entry via wscript). +Set ws = CreateObject("WScript.Shell") +ws.Run "cmd /c """"C:\Program Files\nodejs\node.exe"" ""C:\Users\VanuSystems-Mu\AppData\Roaming\npm\node_modules\@deepseek-ai\dsh\lib\bin.js"" web > ""C:\Users\VanuSystems-Mu\AppData\Local\DeepSeekHarness\app\web.log"" 2>&1""", 0, False