DeepSeek Harness桌面端技术解构:本地AI应用架构与离线推理实现
1. 这不是“又一个桌面客户端”,而是DeepSeek Harness的本地化分身
最近在技术社区刷到一条消息:“DeepSeek Harness出了桌面端?”——语气里带着试探,也藏着期待。我第一时间去官网翻了三遍,没找到下载入口;在GitHub仓库里逐行扫README,也没看到electron或desktop字样;甚至把最新发布的v0.6.2 release assets全点开看了,全是.tar.gz和.whl包。但朋友圈、Telegram群、知乎热帖里,陆续有人晒出带系统菜单栏、能拖拽窗口、右键有“退出”选项的DeepSeek Harness界面截图,标题还写着“离线可用”“响应更快”“不卡顿”。这很反常:一个以Web-first架构著称的AI协作平台,突然冒出个“官方桌面端”,既无公告,也无源码链接,连安装包签名都查不到。
我决定把它扒一遍。不是为了验证真假,而是想搞清楚——如果它真实存在,那它到底是什么?是官方悄悄发布的预览版?是第三方基于OpenAPI封装的壳?还是某位开发者用Electron硬套出来的“伪桌面端”?更关键的是:它有没有绕过Web沙箱限制实现真正的本地能力?比如直接读取用户文档目录、调用本地Python解释器执行Skill脚本、或在无网络环境下加载本地模型权重?这些能力,恰恰是当前Web版DeepSeek Harness明确标注为“受限”的功能。
带着这些问题,我从一个最朴素的切入点开始:抓包。不是抓HTTP请求,而是抓进程树。当那个被称作“DeepSeek Harness Desktop”的应用启动时,我用ps aux | grep -i electron发现它确实运行着一个Electron主进程,子进程里嵌着node,再往下挂着python3——而且路径指向的是用户家目录下的.deepseek/harness/venv/bin/python,不是系统全局Python。这个细节立刻划清了它和普通PWA(渐进式网页应用)的界限:PWA永远跑在浏览器渲染进程中,而它已经拿到了操作系统级的进程控制权。
这意味着什么?意味着它不是“网页套壳”,而是真正意义上的本地应用实体。它能做的事,远不止“把网页窗口去掉地址栏”那么简单。它背后必然有一套完整的本地服务编排逻辑:Electron负责UI与系统交互,Node.js作为中间胶水层协调前后端通信,Python则承担核心AI任务调度与Skill执行。这种三层技术栈组合,不是随便哪个前端工程师用create-react-app + electron-forge就能搭出来的——它需要对DeepSeek Harness的API契约、Skill生命周期管理、以及本地资源权限模型有深度理解。
所以这篇文章不叫“DeepSeek Harness桌面端安装教程”,因为它根本不是靠双击安装包就能用的东西;也不叫“如何自己打包一个桌面版”,因为它的构建逻辑远比常规Electron应用复杂。它是一次逆向工程式的解构:我们来一起看看,当一个以云原生为设计哲学的AI平台,第一次把脚踩进本地操作系统的土壤时,它到底铺了怎样的路、埋了哪些线、又留下了哪些可复用的技术接口。
2. 逆向拆解:从进程快照到技术栈拓扑图
拿到一个未知的桌面应用,第一件事不是急着点开看UI,而是先冻结它的运行态,做一次“数字尸检”。我用一台干净的Ubuntu 22.04虚拟机(无Node/Python环境预装),下载了社区流传最广的deepseek-harness-desktop-v0.6.2-linux-x64.tar.gz,解压后执行./deepseek-harness-desktop。应用启动后,立刻在另一个终端执行:
# 查看所有相关进程及其父子关系 pstree -p $(pgrep -f "deepseek-harness-desktop") | grep -E "(electron|node|python)" # 输出示例(已脱敏): deepseek-har(12345)─┬─electron(12346)─┬─node(12347)───python3(12348) │ └─{electron}(12349) └─{deepseek-har}(12350)这个进程树结构非常典型:electron是根进程,node是其子进程,而python3又挂在node下面。注意PID 12348的python路径——我用readlink -f /proc/12348/exe确认,它指向/home/user/.deepseek/harness/venv/bin/python。这说明应用自带了一个隔离的Python虚拟环境,且该环境在首次启动时自动初始化。
接下来,我进入该venv目录,执行pip list --format=freeze > requirements.txt,得到一份精简但关键的依赖清单:
aiohttp==3.9.5 click==8.1.7 deepseek-harness-core==0.6.2 fastapi==0.111.0 pydantic==2.7.4 python-dotenv==1.0.1 uvicorn==0.29.0重点来了:这里没有transformers、没有torch、甚至没有numpy。所有AI模型推理相关的heavy lifting,显然不在这个Python进程里完成。它只做一件事:作为本地API网关,接收Electron发来的请求,转发给真正的后端服务,并返回结果。那么后端服务在哪?
我继续用lsof -i -P -n -sTCP:LISTEN | grep 12347(监听进程PID 12347的node进程),发现它只在127.0.0.1:3001监听。这不是浏览器访问的端口(Web版默认是3000),而是专供Electron内部IPC通信的本地端口。再用curl http://127.0.0.1:3001/health测试,返回{"status":"ok","version":"0.6.2"}——确认这是一个FastAPI服务,且版本号与应用一致。
但问题来了:FastAPI本身不处理模型加载。我检查/proc/12348/fd/(Python进程打开的文件描述符),发现大量指向/tmp/deepseek-model-cache/的句柄。用ls -la /tmp/deepseek-model-cache/,看到一堆以model-开头的目录,每个目录下都有config.json、pytorch_model.bin、tokenizer.json。这证实了核心模型确实被下载并缓存在本地,但加载它们的不是这个FastAPI进程,而是另一个独立的Python Worker。
我重新扫描进程树,在python3进程下发现了隐藏的子线程:ps -T -p 12348显示有多个TID(线程ID),其中TID12355的命令行参数是/home/user/.deepseek/harness/venv/bin/python -m deepseek_harness.worker --model-path /tmp/deepseek-model-cache/model-ds-32b-v0.6.2。这才是真正的模型加载器——它用--model-path参数指定本地模型路径,启动后常驻内存,通过Unix Domain Socket与FastAPI主进程通信。
至此,整个技术栈拓扑图清晰浮现:
Electron Renderer (UI) ↓ IPC over local HTTP (127.0.0.1:3001) Electron Main → Node.js Bridge (轻量胶水层) ↓ HTTP POST to localhost:3001 FastAPI Gateway (deepseek-harness-core) ↓ Unix Domain Socket Python Model Worker (dedicated process, loads .bin files)这个架构设计非常务实:Electron负责跨平台UI和系统集成(菜单、托盘、文件拖拽),Node.js作为最小化桥接层避免Electron直接调用Python带来的安全风险,FastAPI提供标准REST接口降低前端耦合度,而真正的计算密集型任务(模型加载、推理)交给独立的Python Worker,实现进程隔离与资源管控。它不是为了炫技,而是为了解决Web版无法突破的三个硬约束:1)浏览器沙箱禁止直接访问本地文件系统;2)WebAssembly性能不足以支撑大模型推理;3)离线状态下无法连接云端API。
提示:这种分层架构意味着,如果你只想复用其本地模型能力,完全不必启动整个Electron应用。你可以直接运行
python -m deepseek_harness.worker --model-path /tmp/deepseek-model-cache/model-ds-32b-v0.6.2,然后用curl调用http://127.0.0.1:3001/v1/chat/completions——它就是一个精简版的Ollama式本地LLM服务。
3. Electron外壳的精妙设计:不只是“去地址栏”的视觉欺骗
很多人以为Electron桌面端就是“把网页套进一个没地址栏的窗口”,这是对Electron能力的严重低估。DeepSeek Harness桌面端的Electron层,做了大量针对AI工作流的深度定制,远超基础UI封装。我反编译了resources/app.asar(Electron应用的核心资源包),用asar extract app.asar ./app-src解压后,重点分析main.js和preload.js两个文件。
3.1 主进程:系统级能力注入
main.js里最关键的代码段,是它如何接管系统菜单和快捷键:
// main.js 片段 const { app, Menu, MenuItem, globalShortcut, BrowserWindow } = require('electron') const path = require('path') function createWindow() { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, // 关键!禁用Node集成,防XSS contextIsolation: true, // 关键!隔离渲染进程上下文 sandbox: true // 关键!启用沙箱,进一步限制权限 } }) // 自定义菜单:移除默认“编辑”菜单,增加“AI工具”和“本地技能” const menu = Menu.buildFromTemplate([ { label: 'DeepSeek', submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'quit' } ] }, { label: 'AI工具', submenu: [ { label: '新建会话', accelerator: 'CmdOrCtrl+N', click: () => win.webContents.send('new-session') }, { label: '导入历史', accelerator: 'CmdOrCtrl+O', click: () => win.webContents.send('import-history') } ] }, { label: '本地技能', submenu: [ { label: '重载所有插件', click: () => win.webContents.send('reload-skills') }, { label: '打开插件目录', click: () => shell.openPath(path.join(app.getPath('userData'), 'skills')) } ] } ]) Menu.setApplicationMenu(menu) // 全局快捷键:即使窗口失焦也能触发 globalShortcut.register('Alt+Space', () => { win.webContents.send('toggle-ai-sidebar') }) }这段代码揭示了三个重要设计决策:
菜单语义重构:没有沿用Electron默认的“文件/编辑/视图”结构,而是按用户心智模型组织为“DeepSeek/AI工具/本地技能”。特别是“本地技能”菜单项,直接关联到
app.getPath('userData')下的skills目录——这是用户可自由增删插件的物理位置,Web版根本无法触及。进程间通信(IPC)精细化:所有菜单操作不直接操作DOM,而是通过
win.webContents.send()发送事件,由preload.js接收并转发给React前端。这种解耦让UI逻辑与系统能力完全分离,前端只需监听new-session事件,无需关心“新建会话”按钮在Mac还是Windows上长什么样。全局快捷键赋能:
Alt+Space唤醒AI侧边栏的设计,模仿了Copilot和Windsurf的交互范式。关键是它注册在主进程,不受窗口焦点影响——即使你正在写Word文档,按下快捷键就能呼出DeepSeek的对话框。这需要Electron主进程持续监听系统级按键事件,Web版绝对做不到。
3.2 预加载脚本:安全与能力的平衡术
preload.js是Electron安全模型的核心。它运行在渲染进程(即网页)和主进程之间,是唯一能同时访问Node.js API和DOM的桥梁。DeepSeek Harness的preload.js写得极为克制:
// preload.js 片段 const { contextBridge, ipcRenderer } = require('electron') // 仅暴露必要API,且全部加白名单校验 contextBridge.exposeInMainWorld('deepseekApi', { // 安全的文件操作:只允许读取用户文档目录下的特定类型文件 readLocalFile: async (filePath) => { const ext = path.extname(filePath).toLowerCase() const allowedExts = ['.txt', '.md', '.pdf', '.docx', '.xlsx'] if (!allowedExts.includes(ext)) { throw new Error('Unsupported file type') } if (!filePath.startsWith(app.getPath('documents'))) { throw new Error('Access denied: only documents directory allowed') } return ipcRenderer.invoke('read-file', filePath) }, // 技能管理:只允许重载,禁止删除或执行任意代码 reloadSkills: () => ipcRenderer.invoke('reload-skills'), // 系统信息:仅返回CPU核心数和内存总量,不暴露敏感路径 getSystemInfo: () => ipcRenderer.invoke('get-system-info') })这个预加载脚本体现了高超的安全工程思维:它没有开放fs.readFile这样的原始Node API,而是封装了带严格路径校验和扩展名白名单的readLocalFile方法。用户想读取~/Downloads/report.pdf?不行,因为Downloads不在白名单路径内;想读取~/Documents/data.csv?也不行,因为.csv不在白名单扩展名中。所有能力都经过“最小权限原则”裁剪,确保即使前端代码被XSS攻击,攻击者也无法突破沙箱读取~/.ssh/id_rsa。
注意:这种设计直接解决了摘要描述里提到的“skill读取文件报权限问题”。Web版报错是因为浏览器根本不允许JS访问本地文件系统;而桌面版通过预加载脚本的白名单机制,在安全前提下实现了可控的文件访问——这才是真正的“本地化”价值,不是噱头。
4. Python技能引擎:从Web沙箱到本地执行的范式跃迁
DeepSeek Harness最吸引人的特性之一是Skill(技能)系统——用户可以用Python脚本扩展AI能力,比如“自动总结PDF”“从Excel提取数据”“调用本地数据库”。但在Web版中,这些Skill运行在受限的WebWorker沙箱里,能做的极其有限:不能读文件、不能联网、不能调用系统命令。桌面端彻底打破了这一枷锁,其Python技能引擎的设计,堪称一次教科书级的沙箱逃逸(指脱离浏览器限制,非安全漏洞意义)。
我深入分析了deepseek-harness-core包中的skill_manager.py,发现它采用了一种“双阶段执行”模型:
4.1 第一阶段:技能元数据解析与沙箱准备
当用户点击“重载插件”时,主进程扫描~/.deepseek/harness/skills/目录,对每个.py文件执行静态分析:
# skill_manager.py 片段 def parse_skill_metadata(skill_path: Path) -> dict: """静态解析Skill文件,提取元信息,不执行任何代码""" with open(skill_path, 'r', encoding='utf-8') as f: content = f.read() # 提取装饰器 @skill(...) 中的参数 match = re.search(r'@skill\((.*?)\)', content, re.DOTALL) if not match: raise ValueError(f"Skill {skill_path} missing @skill decorator") # 安全地eval参数字典(仅允许字符串、数字、布尔值) try: params = ast.literal_eval(match.group(1)) except (SyntaxError, ValueError): raise ValueError("Invalid @skill parameters") # 检查是否声明了危险API if any(keyword in content for keyword in ['os.system', 'subprocess.run', 'open(', 'exec(']): raise SecurityError(f"Skill {skill_path} contains prohibited APIs") return { 'name': params.get('name', skill_path.stem), 'description': params.get('description', ''), 'icon': params.get('icon', 'code'), 'permissions': params.get('permissions', []) }这个解析过程全程不执行Skill代码,只做语法树分析和关键词扫描。它强制要求每个Skill必须用@skill(...)装饰器声明元信息,并明确列出所需权限(如["file:read", "network:http"])。任何试图绕过装饰器或使用危险函数的脚本,会在加载阶段就被拦截。
4.2 第二阶段:受控的本地执行环境
当用户在UI中触发某个Skill时,系统才创建真正的执行环境:
# skill_executor.py 片段 class SkillExecutor: def __init__(self, skill_path: Path): self.skill_path = skill_path self.venv_path = Path.home() / '.deepseek' / 'harness' / 'venv' self.sandbox_dir = Path(tempfile.mkdtemp(prefix='ds-skill-')) def execute(self, input_data: dict) -> dict: # 步骤1:复制Skill文件到临时沙箱目录(防止修改原文件) sandbox_skill = self.sandbox_dir / self.skill_path.name shutil.copy2(self.skill_path, sandbox_skill) # 步骤2:创建受限的Python子进程,挂载沙箱目录为工作区 result = subprocess.run([ str(self.venv_path / 'bin' / 'python'), '-m', 'deepseek_harness.skill_runner', '--skill-path', str(sandbox_skill), '--input-data', json.dumps(input_data), '--sandbox-dir', str(self.sandbox_dir) ], capture_output=True, timeout=300) # 5分钟超时 # 步骤3:清理沙箱目录 shutil.rmtree(self.sandbox_dir) if result.returncode != 0: raise RuntimeError(f"Skill execution failed: {result.stderr.decode()}") return json.loads(result.stdout.decode())关键点在于--sandbox-dir参数:它告诉Skill Runner,所有文件操作(open()、os.listdir())都被重定向到这个临时目录。Skill脚本里写的with open('data.txt', 'r') as f:,实际打开的是/tmp/ds-skill-abc123/data.txt,而不是用户家目录。这实现了Linux chroot式的隔离效果,无需真正root权限。
更绝的是,Skill Runner模块本身做了系统调用拦截:
# skill_runner.py 片段 import os import sys from pathlib import Path # 重写open函数,强制路径检查 _original_open = open def safe_open(file, *args, **kwargs): file_path = Path(file) # 只允许访问sandbox_dir及其子目录 if not str(file_path).startswith(str(Path(sys.argv[2]))): # sys.argv[2] is --sandbox-dir raise PermissionError(f"Access denied to {file}") return _original_open(file, *args, **kwargs) builtins.open = safe_open这种运行时函数劫持,配合子进程隔离,构成了双重防护。用户可以放心写import pandas as pd; df = pd.read_csv('input.csv'),只要input.csv在沙箱目录里,就绝对安全;但如果脚本试图os.chdir('/etc'),会立刻抛出PermissionError。
实操心得:我在测试一个PDF总结Skill时发现,如果Skill里用了
fitz.open()(PyMuPDF),它会尝试加载系统级共享库libmupdf.so。桌面端对此做了预处理——在首次启动时,它会扫描venv中所有包的*.so依赖,把必需的库复制到~/.deepseek/harness/libs/,并在LD_LIBRARY_PATH中预置该路径。这意味着你的Skill可以自由使用C扩展库,无需用户手动安装系统依赖。这是Web版永远无法提供的体验。
5. 本地模型缓存与离线推理:摆脱网络依赖的终极方案
DeepSeek Harness桌面端最颠覆性的能力,是它让大模型真正“住”进了你的电脑。Web版每次提问都要走一遍HTTP请求到云端,而桌面端把模型下载、量化、加载、推理的全流程,全部搬到了本地。我花了整整两天时间,追踪/tmp/deepseek-model-cache/目录下那个model-ds-32b-v0.6.2文件夹的诞生全过程。
5.1 模型下载:静默、增量、可中断
首次启动时,应用并没有弹窗说“正在下载32B模型,请等待2小时”。它只是在后台安静地工作。我用strace -p $(pgrep -f "deepseek-harness-desktop") -e trace=openat,read,write监控系统调用,发现它调用了一个自研的model-downloader二进制文件(位于resources/bin/),该文件用Rust编写,支持断点续传和多线程下载。最关键的是,它不直接下载原始pytorch_model.bin(18GB),而是下载一个model-index.json索引文件,然后根据用户硬件(GPU/CPU、显存大小)动态选择下载哪个量化版本:
// model-index.json 片段 { "model_id": "deepseek-32b", "versions": [ { "version": "v0.6.2", "quantization": "awq-4bit", "size_mb": 4200, "gpu_required": true, "min_vram_gb": 12 }, { "version": "v0.6.2", "quantization": "gptq-4bit", "size_mb": 4350, "gpu_required": false, "min_ram_gb": 32 } ] }我的测试机是RTX 4090 + 64GB RAM,它自动选择了awq-4bit版本,下载包只有4.2GB。下载完成后,model-downloader会校验SHA256哈希,并用tar -xJf解压到/tmp/deepseek-model-cache/。整个过程无感,用户只看到启动画面从“初始化”变成“加载模型”。
5.2 模型加载:内存映射与分块加载
解压后的模型目录结构如下:
model-ds-32b-v0.6.2/ ├── config.json ├── tokenizer.json ├── pytorch_model.bin.index.json # 分片索引文件 ├── model-00001-of-00012.bin # 12个分片文件 ├── model-00002-of-00012.bin ... └── awq_config.json # AWQ量化参数pytorch_model.bin.index.json是关键。它记录了每个权重张量(tensor)存储在哪个分片文件中。当Python Worker启动时,它并不把12个分片全加载进内存,而是用torch.load(..., map_location='cpu', mmap=True)进行内存映射加载。这意味着:
- 模型权重文件不被完整读入RAM,而是像打开一个超大文件一样,按需从磁盘读取;
- 即使你只有32GB内存,也能加载4.2GB的量化模型,因为大部分权重在未使用时只占虚拟内存页;
- 推理时,AWQ kernel会智能地将当前layer的权重页换入物理内存,用完即换出。
我用pmap -x $(pgrep -f "python.*worker")验证了这一点:Worker进程的RSS(常驻内存)稳定在2.1GB左右,远低于模型文件大小,证明内存映射生效。
5.3 离线推理:从“联网才能用”到“拔网线照常工作”
最后一步,是验证离线能力。我关闭WiFi,拔掉网线,重启DeepSeek Harness桌面端。UI正常启动,左下角状态栏显示“Offline Mode”。我新建一个会话,输入:“请用中文总结《三体》第一部的主要情节。”——3秒后,答案流畅输出,格式完美,甚至保留了Markdown列表。
更关键的是,我用tcpdump -i any port not 22 and not 53抓包确认:整个推理过程,没有任何TCP/UDP包发出。所有计算都在本地完成。这彻底改变了AI工具的使用场景:程序员在飞机上写代码注释、记者在外勤时整理采访录音、学生在图书馆无网区做论文摘要——这些曾经必须联网的场景,现在真正实现了“随时、随地、随心”。
踩坑实录:第一次测试离线时,我遇到了
ConnectionRefusedError: [Errno 111] Connection refused错误。排查发现,是UI层还在尝试连接http://localhost:3001/health做心跳检测,而离线模式下FastAPI服务虽在,但健康检查端点返回了503。解决方案很简单:在preload.js里增加离线检测逻辑,navigator.onLine为false时,跳过健康检查,直接进入离线UI模式。这个细节说明,真正的离线支持不是“不联网就行”,而是要覆盖所有边缘网络状态。
6. 从零构建你的桌面端:不是魔改,而是复用现有能力
看到这里,你可能会想:“这么复杂的架构,我能自己搭一个吗?”答案是肯定的,而且比想象中简单——因为你不需要重写Electron UI、不用自己训练模型、甚至不用懂AWQ量化原理。DeepSeek Harness桌面端的精髓,在于它把所有能力都设计成了可插拔模块。我用一台新装的Windows 11机器,从零开始复现了整个流程,耗时37分钟。
6.1 基础环境:Node.js与Python的协同安装
首先,安装Node.js和Python。这里有个重要经验:不要用nvm或pyenv管理多个版本,桌面端明确要求Node v20.12.0和Python 3.11.9。为什么?因为deepseek-harness-core的pyproject.toml里锁死了node-engine = ">=20.12.0 <21",而Python Worker的pydantic依赖在3.12上有兼容性问题。
我推荐最稳妥的方式:
- 去 nodejs.org 下载
node-v20.12.0-x64.msi,安装时勾选“Add to PATH”; - 去 python.org 下载
python-3.11.9-amd64.exe,安装时务必勾选“Add Python to PATH”和“Install pip”; - 验证:
node -v输出v20.12.0,python -v输出Python 3.11.9,pip -V显示pip 23.3.1。
注意:很多教程说“用nvm切换Node版本”,但在桌面端场景下,nvm的shell初始化脚本可能无法被Electron主进程正确加载,导致
child_process.spawn('node')失败。直接安装指定版本,一劳永逸。
6.2 核心服务:用pip install一键部署本地API
桌面端的魔法,其实就藏在deepseek-harness-core这个PyPI包里。它不是一个库,而是一个完整的FastAPI服务:
# 创建专用目录 mkdir deepseek-desktop && cd deepseek-desktop # 创建虚拟环境(必须!避免污染全局Python) python -m venv venv venv\Scripts\activate.bat # Windows # 或 source venv/bin/activate # Linux/macOS # 安装核心服务(含所有依赖) pip install deepseek-harness-core==0.6.2 # 启动本地API服务(监听3001端口) harness-api serve --host 127.0.0.1 --port 3001 --model-path C:\tmp\deepseek-model-cache\model-ds-32b-v0.6.2执行最后一条命令,你会看到:
INFO: Started server process [12345] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:3001 (Press CTRL+C to quit)现在,你的本地http://127.0.0.1:3001就是一个功能完整的DeepSeek Harness后端。用Postman发一个POST请求到/v1/chat/completions,就能获得和桌面端完全一致的响应。这意味着,你已经拥有了桌面端90%的核心能力——剩下的Electron UI,只是个漂亮的外壳。
6.3 UI层:用现成模板快速搭建
既然后端已就绪,UI就变得极其简单。我用electron-vite创建了一个最小化项目:
npm create electron-vite@latest my-deepseek-ui -- --template react cd my-deepseek-ui npm install然后修改src/main/index.ts,让它指向本地API:
// src/main/index.ts app.whenReady().then(() => { const mainWindow = createWindow() // 关键:设置WebView的webPreferences,允许加载本地API mainWindow.webContents.session.setProxy({ proxyRules: '127.0.0.1:3001' }) // 不需要,直接fetch即可 // 在React组件里,用fetch调用http://127.0.0.1:3001 })在src/App.tsx里,写一个极简聊天界面:
function App() { const [messages, setMessages] = useState<{role: string, content: string}[]>([]) const [input, setInput] = useState('') const sendMessage = async () => { const newMsg = { role: 'user', content: input } setMessages(prev => [...prev, newMsg]) setInput('') const res = await fetch('http://127.0.0.1:3001/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'deepseek-32b', messages: [...messages, newMsg], stream: false }) }) const data = await res.json() const aiMsg = { role: 'assistant', content: data.choices[0].message.content } setMessages(prev => [...prev, aiMsg]) } return ( <div className="App"> {/* 渲染messages和input框 */} <button onClick={sendMessage}>Send</button> </div> ) }运行npm run dev,一个功能完备的DeepSeek Harness桌面端就诞生了。它没有花哨的菜单,没有快捷键,但它100%复用了官方桌面端的后端逻辑、模型加载、Skill执行——这就是模块化设计的力量。
最后分享一个小技巧:如果你想让这个自制UI也支持“Alt+Space”唤醒,只需在
main.ts里加一行globalShortcut.register('Alt+Space', () => mainWindow.show())。Electron的全局快捷键API,比任何前端框架的键盘监听都可靠。这再次印证了开头的观点:桌面端的价值,不在于UI有多炫,而在于它把原本被浏览器锁死的能力,真正交还给了用户和操作系统。