feat: 启动进度提示、开机自启选项、应用内升级引导

- 启动等待时显示进度条+百分比+后端最新输出(等待上限 240s)
- 托盘菜单新增"开机自启服务"勾选项(注册表 Run + vbs 隐藏启动器)
- 启动后后台检查 DSH 最新版本,有新版弹窗引导升级(npm 后台完成,自动重启服务)
- StartDshService 禁用 DSH_TELEMETRY_DISABLED 遥测(可能小幅加速)
- 新增 start-dsh-service.vbs,更新 README
This commit is contained in:
Reasonix committed 2026-08-14 15:30:19 +08:00
1 parent f8c101d487
commit deba852624
4 files changed
+250 -18

No files matched your search

BIN
View File
Binary file not shown.
+233 -12
View File
@@ -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; }
}
}
+4
View File
@@ -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