init: DeepSeek Harness WebView2 桌面客户端
- dsh-app.cs: C#/WinForms/WebView2 主程序(托盘常驻、自动拉起后端、DPI 感知) - app.manifest: PerMonitorV2 DPI 声明 - dsh.ico / logo-512.png: DeepSeek 官方鲸鱼图标 - README.md: 使用与构建文档
This commit is contained in:
commit
8c90a1709b
10 files changed
+330
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
# 运行时数据(不要提交)
|
||||||
|
*.WebView2/
|
||||||
|
web.log
|
||||||
|
start-error.log
|
||||||
|
|
||||||
|
# 临时文件
|
||||||
|
*.tmp
|
||||||
|
*.log.*
|
||||||
@@ -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
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="DeepSeekHarness.dsh-app" type="win32"/>
|
||||||
|
<description>DeepSeek Harness</description>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
</assembly>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Reference in new issue
Block a user