commit f8c101d48767fecc9bfeb7c19cac7f618d3a4f44 Author: Reasonix Date: Fri Aug 14 15:11:41 2026 +0800 init: DeepSeek Harness WebView2 桌面客户端 - dsh-app.cs: C#/WinForms/WebView2 主程序(托盘常驻、自动拉起后端、DPI 感知) - app.manifest: PerMonitorV2 DPI 声明 - dsh.ico / logo-512.png: DeepSeek 官方鲸鱼图标 - README.md: 使用与构建文档 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..042cb69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# 运行时数据(不要提交) +*.WebView2/ +web.log +start-error.log + +# 临时文件 +*.tmp +*.log.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..e64e52e --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# DeepSeek Harness 桌面客户端 + +一个把 DeepSeek Harness(dsh)Web UI 包装成**独立桌面应用**的本地客户端。 +使用系统 **WebView2** 渲染,不依赖 Vivaldi / Edge 等完整浏览器,双击即用。 + +## 功能 + +- **独立窗口**:WebView2 渲染 `http://127.0.0.1:3080`,无浏览器地址栏/标签栏 +- **自动拉起后端**:打开应用时若 dsh 服务未运行,自动静默启动(node 进程,无 CMD 窗口) +- **系统托盘**: + - 点窗口 `X` → 直接最小化到托盘(后端保持运行) + - 托盘双击 / 右键「打开」→ 恢复窗口 + - 托盘右键「退出」→ 完全退出(**同时关闭后端服务**) +- **DPI 感知**:PerMonitorV2,高分屏(250% 缩放)下界面清晰不模糊 +- **DeepSeek 官方图标**:窗口、任务栏、快捷方式均为官方鲸鱼 logo + +## 目录结构 + +``` +DeepSeekHarness\ +├── README.md 本文件 +├── dsh.ico DeepSeek 图标(多尺寸,快捷方式/窗口使用) +├── logo-512.png 图标 512px 源图(由官方 favicon.svg 生成) +├── .gitignore 排除运行时数据 +└── app\ + ├── dsh-app.exe 主程序(C# / WinForms / WebView2) + ├── Microsoft.Web.WebView2.Core.dll WebView2 托管组件 1.0.3719.77 + ├── Microsoft.Web.WebView2.WinForms.dll 同上 + ├── WebView2Loader.dll 原生加载器(x64) + ├── src\ + │ ├── dsh-app.cs 全部源码 + │ └── app.manifest DPI manifest(PerMonitorV2) + ├── web.log dsh 服务日志(运行时生成) + └── dsh-app.exe.WebView2\ WebView2 用户数据(运行时生成) +``` + +## 环境依赖 + +| 依赖 | 说明 | +|---|---| +| Node.js ≥ 22.19 | dsh 运行环境(当前机器:v24.19.0,winget 安装) | +| `@deepseek-ai/dsh` | 全局安装:`npm i -g @deepseek-ai/dsh`(镜像源:registry.npmmirror.com) | +| WebView2 Runtime | Windows 11 自带;旧系统需安装 | +| .NET Framework 4.8 | Windows 10/11 自带(构建时才需要) | + +## 使用 + +1. 双击桌面(或 `C:\Users\VanuSystems-Mu\DeskBox\AI\`)的 **DeepSeek Harness** 快捷方式 +2. 首次打开等待几秒(后端自动启动),之后秒开 +3. 用完点 `X` 即最小化到托盘;想彻底退出用托盘右键 → **退出** + +> 后端服务日志:`app\web.log`;启动失败排查:`app\start-error.log`(如存在)。 + +## 从源码构建 + +```sh +cd app\src +# 需要 dsh.ico 与 app.manifest 位于 src 上级(或按需调整路径) +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 \ + /r:System.Windows.Forms.dll /r:System.Drawing.dll \ + dsh-app.cs +``` + +csc 位于 `C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe`。 + +## 排障 + +| 现象 | 处理 | +|---|---| +| 界面模糊 | 确认 exe 已带 manifest(PerMonitorV2);重新构建 | +| 弹 CMD 窗口 | 旧版本用 `cmd /c` 启动后端所致;当前版本直接启动 `node.exe`,无窗口 | +| 打开后一直"正在启动服务" | 查看 `web.log`;确认 `dsh` 已全局安装、端口 3080 未被占用 | +| 托盘图标消失 | 进程异常退出;重新双击快捷方式即可(会自动拉起后端) | + +## 许可 + +- 本包装代码:MIT(可自行修改 `app\src\dsh-app.cs` 后重新构建) +- DeepSeek Harness:由 [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) 提供,MIT +- WebView2 组件:Microsoft 分发,MIT diff --git a/app/Microsoft.Web.WebView2.Core.dll b/app/Microsoft.Web.WebView2.Core.dll new file mode 100644 index 0000000..a39b59c Binary files /dev/null and b/app/Microsoft.Web.WebView2.Core.dll differ diff --git a/app/Microsoft.Web.WebView2.WinForms.dll b/app/Microsoft.Web.WebView2.WinForms.dll new file mode 100644 index 0000000..07d8bd6 Binary files /dev/null and b/app/Microsoft.Web.WebView2.WinForms.dll differ diff --git a/app/WebView2Loader.dll b/app/WebView2Loader.dll new file mode 100644 index 0000000..ea25db8 Binary files /dev/null and b/app/WebView2Loader.dll differ diff --git a/app/dsh-app.exe b/app/dsh-app.exe new file mode 100644 index 0000000..5d28e78 Binary files /dev/null and b/app/dsh-app.exe differ diff --git a/app/src/app.manifest b/app/src/app.manifest new file mode 100644 index 0000000..ea39ecf --- /dev/null +++ b/app/src/app.manifest @@ -0,0 +1,16 @@ + + + + DeepSeek Harness + + + + + + + + true/pm + PerMonitorV2 + + + diff --git a/app/src/dsh-app.cs b/app/src/dsh-app.cs new file mode 100644 index 0000000..d79778c --- /dev/null +++ b/app/src/dsh-app.cs @@ -0,0 +1,222 @@ +// 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; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.Web.WebView2.Core; +using Microsoft.Web.WebView2.WinForms; + +static class Program +{ + private const int Port = 3080; + private static readonly string WebUrl = "http://127.0.0.1:3080"; + + private static Process serviceProcess; // 本应用拉起的 dsh 服务进程 + private static bool startedByUs; // 服务是否由本应用启动 + private static NotifyIcon trayIcon; + private static Form mainForm; + + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + // 确保 dsh 服务在运行(若端口未监听则后台拉起) + if (!IsPortOpen(Port)) + { + StartDshService(); + } + + mainForm = new Form + { + Text = "DeepSeek Harness", + ClientSize = new Size(1440, 900), + StartPosition = FormStartPosition.CenterScreen, + MinimumSize = new Size(800, 600), + }; + + // 窗口标题栏/任务栏使用 DeepSeek 图标 + try + { + mainForm.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); + } + catch { } + + // 系统托盘常驻图标 + trayIcon = new NotifyIcon(); + trayIcon.Icon = mainForm.Icon; + trayIcon.Text = "DeepSeek Harness"; + trayIcon.Visible = true; + trayIcon.DoubleClick += (s, e) => ShowMainWindow(); + var menu = new ContextMenuStrip(); + menu.Items.Add("打开 DeepSeek Harness", null, (s, e) => ShowMainWindow()); + menu.Items.Add(new ToolStripSeparator()); + menu.Items.Add("退出", null, (s, e) => ExitApp()); + trayIcon.ContextMenuStrip = menu; + + // 点 X:直接最小化到系统托盘继续后台运行;完全退出请用托盘右键菜单 + mainForm.FormClosing += (s, e) => + { + if (e.CloseReason != CloseReason.UserClosing) return; + e.Cancel = true; + mainForm.Hide(); + }; + + var status = new Label + { + Dock = DockStyle.Fill, + TextAlign = ContentAlignment.MiddleCenter, + Font = new Font("Microsoft YaHei UI", 14), + Text = "正在启动 DeepSeek Harness 服务…", + }; + mainForm.Controls.Add(status); + + var web = new WebView2 { Dock = DockStyle.Fill }; + + mainForm.Shown += async (s, e) => + { + // 等待服务就绪(最多 ~30 秒) + bool ready = false; + for (int i = 0; i < 60; i++) + { + await Task.Delay(500); + if (IsPortOpen(Port)) { ready = true; break; } + } + if (!ready) + { + status.Text = "无法连接 DeepSeek Harness 服务(端口 " + Port + ")。\n" + + "请查看日志:" + Path.Combine(AppDir(), "web.log"); + return; + } + + mainForm.SuspendLayout(); + mainForm.Controls.Remove(status); + mainForm.Controls.Add(web); + mainForm.ResumeLayout(); + + try + { + string userData = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "DeepSeekHarness", "EBWebView"); + var cp = new CoreWebView2CreationProperties(); + cp.UserDataFolder = userData; + web.CreationProperties = cp; + await web.EnsureCoreWebView2Async(); + web.Source = new Uri(WebUrl); + } + catch (Exception ex) + { + MessageBox.Show("WebView2 初始化失败:\n" + ex.Message, "DeepSeek Harness", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + }; + + Application.Run(mainForm); + } + + static string AppDir() + { + return Path.GetDirectoryName(Application.ExecutablePath); + } + + static bool IsPortOpen(int port) + { + try + { + using (var client = new TcpClient()) + { + var task = client.ConnectAsync("127.0.0.1", port); + return task.Wait(500) && client.Connected; + } + } + catch { return false; } + } + + // 直接启动 node.exe 运行 dsh 入口(不经 cmd/批处理,避免弹出 CMD 窗口) + static void StartDshService() + { + string appDir = AppDir(); + 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; + + var psi = new ProcessStartInfo(nodeExe) + { + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + psi.Arguments = "\"" + binJs + "\" web"; + + // 确保 node 与 npm 命令可用(dsh 插件可能用到) + string path = psi.EnvironmentVariables["PATH"] ?? ""; + if (path.IndexOf("nodejs", StringComparison.OrdinalIgnoreCase) < 0) + path = @"C:\Program Files\nodejs;" + path; + if (path.IndexOf("npm", StringComparison.OrdinalIgnoreCase) < 0) + path = npmDir + ";" + path; + psi.EnvironmentVariables["PATH"] = path; + + serviceProcess = Process.Start(psi); + startedByUs = true; + + // 输出写日志文件(防管道缓冲死锁) + serviceProcess.OutputDataReceived += (s, e) => + { + if (e.Data != null) { 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 { } } + }; + serviceProcess.BeginOutputReadLine(); + serviceProcess.BeginErrorReadLine(); + } + catch (Exception ex) + { + try { File.WriteAllText(Path.Combine(AppDir(), "start-error.log"), ex.ToString()); } catch { } + } + } + + static void ShowMainWindow() + { + if (mainForm == null) return; + mainForm.Show(); + if (mainForm.WindowState == FormWindowState.Minimized) + mainForm.WindowState = FormWindowState.Normal; + mainForm.Activate(); + } + + // 完全退出:关闭后端服务 + 托盘 + 应用 + static void ExitApp() + { + try + { + if (startedByUs && serviceProcess != null && !serviceProcess.HasExited) + { + serviceProcess.Kill(); + } + } + catch { } + if (trayIcon != null) + { + trayIcon.Visible = false; + trayIcon.Dispose(); + trayIcon = null; + } + Application.Exit(); + } +} diff --git a/dsh.ico b/dsh.ico new file mode 100644 index 0000000..b748985 Binary files /dev/null and b/dsh.ico differ diff --git a/logo-512.png b/logo-512.png new file mode 100644 index 0000000..630f532 Binary files /dev/null and b/logo-512.png differ