feat: 启动进度提示、开机自启选项、应用内升级引导
- 启动等待时显示进度条+百分比+后端最新输出(等待上限 240s) - 托盘菜单新增"开机自启服务"勾选项(注册表 Run + vbs 隐藏启动器) - 启动后后台检查 DSH 最新版本,有新版弹窗引导升级(npm 后台完成,自动重启服务) - StartDshService 禁用 DSH_TELEMETRY_DISABLED 遥测(可能小幅加速) - 新增 start-dsh-service.vbs,更新 README
This commit is contained in:
1 parent
f8c101d487
commit
deba852624
4 files changed
+250
-18
No files matched your search
Binary file not shown.
+233
-12
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in new issue
Block a user