本地优先、隐私优先的 DeepSeek Harness 桌面应用。
30.7/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/10 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/yumingtao/narwhal.git状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
生态应用
桌面端 / Web 外壳,不以 dsh plugin add 安装
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 25 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装仓库 package.json 标记 private,未发布到 npm,需从源码安装
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✓npm 包narwhal @ 0.0.2
✓Node 引擎要求 >=22 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/12 09:39:51
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 66 处,展示前 61 条)
- 读取本地文件src/main/config.ts:34
const legacyExists = existsSync(legacy) - 写入本地文件src/main/config.ts:36
await mkdir(target, { recursive: true, mode: 0o700 }) - 读取本地文件src/main/config.ts:40
const targetExists = existsSync(target) - 写入本地文件src/main/config.ts:49
await rename(legacy, target) - 写入本地文件src/main/config.ts:53
await mkdir(target, { recursive: true, mode: 0o700 }) - 读取本地文件src/main/config.ts:54
for (const entry of await readdir(legacy)) { - 写入本地文件src/main/config.ts:55
await rename(join(legacy, entry), join(target, entry)) - 删除本地文件递归src/main/config.ts:57
await rm(legacy, { recursive: true, force: true }) - 读取本地文件src/main/config.ts:82
const configExists = existsSync(configPath) - 读取本地文件src/main/config.ts:92
const raw = await readFile(configPath, 'utf-8') - 读取本地文件src/main/config.ts:140
if (!existsSync(configDir)) await mkdir(configDir, { recursive: true }) - 写入本地文件src/main/config.ts:140
if (!existsSync(configDir)) await mkdir(configDir, { recursive: true }) - 写入本地文件src/main/config.ts:141
await writeFile(configPath, JSON.stringify(cachedConfig, null, 2) + '\n', 'utf-8') - 读取本地文件src/main/dsh-sync.ts:70
if (existsSync(configPath)) { - 读取本地文件src/main/dsh-sync.ts:72
const raw = await readFile(configPath, 'utf-8') - 读取本地文件src/main/dsh-sync.ts:82
if (existsSync(dshSettingsPath)) { - 读取本地文件src/main/dsh-sync.ts:84
const raw = await readFile(dshSettingsPath, 'utf-8') - 读取本地文件src/main/dsh-sync.ts:99
if (!existsSync(configDir)) await mkdir(configDir, { recursive: true }) - 写入本地文件src/main/dsh-sync.ts:99
if (!existsSync(configDir)) await mkdir(configDir, { recursive: true }) - 写入本地文件src/main/dsh-sync.ts:101
await writeFile(configPath, JSON.stringify(validated, null, 2) + '\n', 'utf-8') - 读取本地文件src/main/dsh-sync.ts:272
if (!existsSync(dir)) await mkdir(dir, { recursive: true, mode: 0o700 }) - 写入本地文件src/main/dsh-sync.ts:272
if (!existsSync(dir)) await mkdir(dir, { recursive: true, mode: 0o700 }) - 写入本地文件src/main/dsh-sync.ts:273
await writeFile(path, content, 'utf-8') - 写入本地文件src/main/dsh-sync.ts:278
if (!existsSync(dir)) await mkdir(dir, { recursive: true }) - 写入本地文件src/main/dsh-sync.ts:279
await writeFile(path, content, 'utf-8') - 发起网络请求src/main/host-bridge.ts:602
response = await fetch(`${this.origin}/api/${endpoint}`, { - 发起网络请求src/main/host-supervisor.ts:78
const exchange = await fetch(readyUrl, { redirect: 'manual', signal: AbortSignal.timeout(5_000) }) - 发起网络请求src/main/host-supervisor.ts:85
const probe = await fetch(`${origin}/`, { headers: { Cookie: cookie }, redirect: 'manual', signal: AbortSignal.timeout(5_000) }) - 执行外部命令固定命令src/main/host-supervisor.ts:106
execFile('ps', ['-p', String(pid), '-o', 'args='], { windowsHide: true }, (error, stdout) => { - 读取本地文件src/main/host-supervisor.ts:121
const raw = (await readFile(pidfile, 'utf8')).trim() - 删除本地文件src/main/host-supervisor.ts:126
if (!isAlive(pid)) { await rm(pidfile, { force: true }); return } - 删除本地文件src/main/host-supervisor.ts:127
if (processMarker !== undefined && !(await commandLineContains(pid, processMarker))) { await rm(pidfile, { force: true }); return } - 删除本地文件src/main/host-supervisor.ts:136
await rm(pidfile, { force: true }) - 写入本地文件src/main/host-supervisor.ts:185
await writeFile(options.pidfile, String(child.pid), 'utf8').catch(() => { }) - 删除本地文件src/main/host-supervisor.ts:217
if (options.pidfile !== undefined) void rm(options.pidfile, { force: true }).catch(() => { }) - 删除本地文件src/main/host-supervisor.ts:237
if (options.pidfile !== undefined) await rm(options.pidfile, { force: true }).catch(() => { }) - 读取本地文件src/main/index.ts:57
return new Response(await readFile(filePath), { headers }) - 写入本地文件src/main/index.ts:127
await writeFile(temporary, JSON.stringify(store, null, 2), { encoding: 'utf8', mode: 0o600 }) - 写入本地文件src/main/index.ts:128
await rename(temporary, storePath) - 读取本地文件src/main/index.ts:132
try { const parsed = JSON.parse(await readFile(storePath, 'utf8')) as Store; if (parsed.version === 1 && Array.isArray(parsed.workspaces)) store = par - 执行外部命令固定命令src/main/index.ts:140
const child = spawn('git', args, { cwd, shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }) - 删除本地文件递归src/main/index.ts:172
await rm(sessionsDir, { recursive: true, force: true }) - 读取本地文件src/main/index.ts:177
if (existsSync(projcachePath)) { - 读取本地文件src/main/index.ts:179
const raw = await readFile(projcachePath, 'utf-8') - 写入本地文件src/main/index.ts:190
await writeFile(projcachePath, JSON.stringify(projcache), 'utf-8') - 读取本地文件src/main/index.ts:210
const path = await realpath(result.filePaths[0]); const now = new Date().toISOString() - 读取本地文件src/main/index.ts:226
const workspaceReal = await realpath(workspace.path) - 写入本地文件src/main/index.ts:252
if (reason) { await mkdir(app.getPath('logs'), { recursive: true, mode: 0o700 }); await appendFile(join(app.getPath('logs'), 'startup.log'), `${new Da - 写入本地文件src/main/index.ts:252
if (reason) { await mkdir(app.getPath('logs'), { recursive: true, mode: 0o700 }); await appendFile(join(app.getPath('logs'), 'startup.log'), `${new Da - 写入本地文件src/main/index.ts:371
try { await appendFile(join(app.getPath('logs'), 'startup.log'), msg, { encoding: 'utf8', mode: 0o600 }) } catch { } - 执行外部命令来自配置/变量src/main/index.ts:614
() => spawn(selectedRuntime!.nodeExecutable, [...selectedRuntime!.launchArguments, '--profile', 'web', '--host', '127.0.0.1', '--port', '0', '--no-ope - 执行外部命令来自配置/变量src/main/install-executor.ts:28
proc = spawn(cmd, args, { - 发起网络请求src/main/install-executor.ts:334
const req = https.get(url, { timeout: 15000 }, (res) => { - 读取本地文件src/main/runtime.ts:27
return JSON.parse(readFileSync(manifestPath, 'utf8')) as RuntimeManifest - 读取本地文件src/main/runtime.ts:39
if (!existsSync(configPath)) return undefined - 读取本地文件src/main/runtime.ts:40
const raw = JSON.parse(readFileSync(configPath, 'utf8')) - 读取本地文件src/main/runtime.ts:91
existsSync(join(r, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')) - 读取本地文件src/main/runtime.ts:92
|| existsSync(join(r, 'apps', 'cli', 'src', 'bin.ts')) - 读取本地文件src/main/runtime.ts:110
const hasDeployedBin = existsSync(join(runtimeRoot, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')) - 发起网络请求src/renderer/mock-bridge.ts:701
const res = await fetch(url, { headers: { Accept: 'application/json' } }) - 发起网络请求src/renderer/mock-bridge.ts:726
const res = await fetch(url, { headers: { Accept: 'application/json' } })
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 本地运行智能体
- 对话数据留存本机
- 项目与上下文本地存储
本地优先、隐私优先的 AI 智能体,对话、项目和工作上下文都留在你自己的机器上。
README
由 DeepSeek 最新模型翻译生成English · 简体中文
Narwhal
本地优先、隐私优先的 DeepSeek Harness 桌面应用。
基于 Electron 的桌面壳,在本地 loopback 上运行兼容的 DeepSeek Harness runtime。你自己的 AI agent 工作台、MCP servers、Cordis 插件(运行时扩展)和 skills — 全部本地运行,零云端锁定。
⚠️ 早期版本 — Narwhal 是一个个人开源项目。当前为首个公开发布版本:核心功能已可用,但仍有很多不足(Windows/Linux 打包、自动更新、高级模型预设、完整轨迹渲染等)。欢迎提交 Issue 和 PR,改进会逐步推出。路线图在 GitHub Issues 中跟踪。
截图
主工作台
工作区选择器、Agent 工作台、对话历史和 composer 控件 — 全部本地运行,全部 loopback。
Integrations 市场
Narwhal 的核心亮点:内置的 MCP servers、Runtime Extensions(Cordis 插件)和 Skills 市场。搜索、安装、管理全部在设置面板完成。
搜索公开 MCP registry,一键安装,或手动添加自研 MCP server(stdio 或 streamable-http 传输)。
展开完整文档(README 剩余 1 节)
English · 简体中文
Narwhal
本地优先、隐私优先的 DeepSeek Harness 桌面应用。
基于 Electron 的桌面壳,在本地 loopback 上运行兼容的 DeepSeek Harness runtime。你自己的 AI agent 工作台、MCP servers、Cordis 插件(运行时扩展)和 skills — 全部本地运行,零云端锁定。
⚠️ 早期版本 — Narwhal 是一个个人开源项目。当前为首个公开发布版本:核心功能已可用,但仍有很多不足(Windows/Linux 打包、自动更新、高级模型预设、完整轨迹渲染等)。欢迎提交 Issue 和 PR,改进会逐步推出。路线图在 GitHub Issues 中跟踪。
截图
主工作台
工作区选择器、Agent 工作台、对话历史和 composer 控件 — 全部本地运行,全部 loopback。
Integrations 市场
Narwhal 的核心亮点:内置的 MCP servers、Runtime Extensions(Cordis 插件)和 Skills 市场。搜索、安装、管理全部在设置面板完成。
搜索公开 MCP registry,一键安装,或手动添加自研 MCP server(stdio 或 streamable-http 传输)。
浏览并安装 DSH Cordis 插件包 — 框架级扩展,为 Agent runtime 添加新能力(沙箱 shell、凭证管理、MCP 协议客户端等)。
通过 GitHub raw URL 或 git 仓库添加 Markdown skill 包。已安装 skills 存放到 ~/.config/narwhal/skills/,并加载到 Agent 的 system prompt。
Narwhal 是什么?
Narwhal 是一个桌面应用,为你的机器带来本地优先、隐私优先的 AI agent。它在 loopback 上启动兼容的 DeepSeek Harness runtime,严格作为本地 Agent 侧车使用。桌面应用拥有每一个可见的对话、活动、项目和工作上下文 — 你永远不会接触上游产品的 UI。
Narwhal 适合任何希望使用 DeepSeek Harness 的强大能力,又不想牺牲本地控制、隐私或工作流自主权的人。
为什么叫 Narwhal?
Narwhal(独角鲸)。这个名字源于四个设计意图:
1. 同源 DeepSeek
Narwhal 是独角鲸,DeepSeek 的 Logo 也是鲸鱼。这个名字表达的是:Narwhal 不是一个独立的新物种 — 它是 DeepSeek 生态下的一个桌面端延伸。Logo、配色、整体视觉都跟 DeepSeek 的鲸系列一脉相承。
2. 深入(Deep)
独角鲸生活在北极海域,是最深潜的哺乳动物之一,能下到 1500 米。Narwhal 也希望如此:不是停留在表层的对话机器人,而是能深入到项目上下文、Git 仓库、MCP 工具链、runtime 插件 — 成为 Agent 真正的执行层。
3. 专注(Focus)
独角鲸的标志性长牙是单一的犬齿穿透上颌生长而成 — 尖锐、集中、不分叉。Narwhal 也是这样的产品:它就是 DeepSeek Harness 的桌面壳,不是通用 IDE,不是万能平台。一个目的,做好一件事。
4. 进取(Progress)
独角鲸不是被动潜水的动物 — 它们在冰层下穿行、探索、迁徙。Narwhal 同样想做一件事:把 DeepSeek Harness 的能力从浏览器里解放出来,推到更广阔的工作场景中,让 AI agent 真正进驻到开发者的日常桌面里。
核心特性
| 能力 | 说明 |
| 本地优先、隐私优先 | 原生工作区选择器、本地工作区记忆、项目级会话管理 — 全部本地存储,零云端锁定。 |
| Narwhal 自有对话 | 独立的 Work / Activity 视图,支持历史加载、prompt 取消、仅本地数据持久化。 |
| 安全任务执行 | 本地任务计划、可检查步骤、Git 分支/变更文件检查、安全交付物固定。 |
| 安全的 Loopback BFF | 仅主进程的 Backend-for-Frontend:固定 Host origin、允许列表内的会话 RPC、归一化事件流。渲染器无法获取 Host、选择 URL/cwd 或接收原始 host 配置。 |
| 狭窄类型化 IPC 桥 | 渲染器不暴露 Node、通用文件系统、shell、终端、原始 IPC 或任意路径 API。完整接口由 src/shared/desktop-contract.ts 中的严格类型契约定义。 |
| 零云端锁定 | V1 中无账号、无云端同步、无团队、无任务分配、无遥测、无自动外部上传。 |
V1 范围
已包含:
- 原生工作区选择器和本地工作区记忆
- 本地任务计划、可检查步骤、Git 分支/变更文件检查、安全交付物固定
- Narwhal 自有本地 Agent 对话,含 Work / Activity 视图、历史加载和 prompt 取消
- 仅主进程的 loopback BFF,固定 Host origin 和允许列表内的会话 RPC
- 狭窄类型化 IPC 桥,渲染器无 Node/shell/FS 访问
- Integrations 市场 — 搜索、安装、管理 MCP servers、Runtime Extensions(Cordis 插件)和 Skills。所有变更持久化到 config.json 并同步到 DSH runtime。Add Custom MCP 表单支持 stdio(自研 JS 进程)和 streamable-http(远程端点)。
延期(不在 V1 中):
- 文件/图片上传、审批和问答界面
- 高级模型或预设选择
- 对话分支/搜索、prompt 引导
- 上游产品的完整轨迹渲染器
架构
Narwhal 在渲染器(UI)和 runtime 之间采用严格的安全边界:
┌─────────────────────────────────────────────────────────────┐
│ Electron 主进程 │ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ Host Bridge │ │ Runtime │ │ BFF (loopback) │ │ │ │ (IPC handlers)│ │ Supervisor │ │ fixed origin │ │ │ └──────┬──────┘ └──────┬───────┘ └────────┬─────────┘ │ │ │ │ │ │ │ ┌──────┴────────────────┴───────────────────┴─────────┐ │ │ │ Typed IPC Bridge (preload) │ │ │ │ src/preload/index.cts — 狭窄、已审计的接口 │ │ │ └──────────────────────────┬───────────────────────────┘ │ │ │ │ │ ┌──────────────────────────┴───────────────────────────┐ │ │ │ Renderer (React + Vite) │ │ │ │ src/renderer/ — 无 Node、无 shell、无 FS 访问 │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
设计原则:
- 渲染器永远不能直接访问 Host runtime 或其配置
- 所有 Host 交互通过主进程的 loopback BFF,固定 origin
- IPC 桥仅暴露类型化、窄范围的操作
- 用户数据保留在桌面应用本地
平台支持
| 平台 | 状态 | 备注 |
| macOS arm64 | ✅ 可用 | 主要开发目标 |
| macOS x64 | 🚧 计划中 | Universal 构建 |
| Windows x64 | 🚧 计划中 | 在路线图上 |
| Linux | 🚧 计划中 | 在路线图上 |
快速开始
方式一 — 下载应用(普通用户)
Narwhal 是独立的桌面应用,DSH runtime 已打包在内 — 你的机器不需要安装 Node.js、pnpm 或任何其他依赖。
1. 从 GitHub Releases 下载最新的 .dmg 或 .zip(macOS arm64)
2. 双击 .dmg,将 Narwhal.app 拖入 /Applications
3. 打开 Narwhal。首次启动时 macOS 可能提示 "Narwhal 无法打开" — 右键 → 打开 → 再次确认
4. 打开 设置(左下角齿轮图标),添加 API Key,开始使用
macOS 未签名构建说明当前 GitHub Releases 尚未 notarize。浏览器下载后,macOS 会给 app 打 quarantine 标记,弹出:"Narwhal 已损坏,无法打开"
快速修复 — Finder 右键:右键 Narwhal.app → 打开 → 在安全对话框中再次点击 打开。macOS 会记住这次选择。
终端一行修复: xattr -d com.apple.quarantine /Applications/Narwhal.app
项目获得 Apple Developer ID 后会开启 notarization。
详见 docs/macos-release-checklist.md。
就这么简单 — 全部 loopback 运行,无账号、无云端配置、无遥测。
方式二 — 从源码构建(开发者)
前置条件:
- Node.js 22+
- pnpm
安装依赖
pnpm install
安装 DSH runtime(从 npm 获取指定版本的 @deepseek-ai/dsh)
pnpm stage:runtime
开发模式运行
pnpm dev
打包生产版 .app(macOS):
构建 + 安装 runtime + 在 release/ 生成 DMG + ZIP
pnpm package:mac
打包后的 .app 会将 DSH runtime 打包进 Contents/Resources/runtime/dsh/ — 从 DMG/ZIP 安装的最终用户不需要 Node.js 或任何工具链。
Runtime 发现优先级(开发模式):
1. config.json → runtime.root 覆盖
2. DSH_RUNTIME_ROOT 环境变量
3. runtime/dsh/(npm 安装 — 主路径,由 pnpm stage:runtime 生成)
4. ../DeepSeek-Harness/(源码 checkout — 供 Harness 核心开发)
当 Node 二进制不在 PATH 中时,设置 DSH_NODE_EXECUTABLE。参见 runtime/manifest.json 了解锁定的 runtime 版本(npm@0.1.1-rc.2)。
配置
Narwhal 使用单一权威配置文件,每次启动前同步到 DSH runtime。
配置文件路径: ~/.config/narwhal/config.json(可通过 NARWHAL_CONFIG_DIR 覆盖)
每次 Narwhal 启动时,从 config.json 重新生成两个 runtime 文件:
- dsh-home/settings.yaml — provider 路由、默认模型、权限预设
- dsh-home/profiles/web/cordis.patch.yml — MCP servers 和 skills
编辑 config.json(或使用应用内设置面板),然后重启 Narwhal 使变更生效。
完整示例
{
"version": 1,
"theme": "auto",
"defaultModel": {
"provider": "deepseek",
"model": "deepseek-chat"
},
"agent": {
"preset": "standard",
"permissionLevel": "workspace-write"
},
"providers": {
"deepseek": {
"displayName": "DeepSeek",
"api": "openai-completions",
"baseURL": "https://api.deepseek.com/v1",
"apiKeyEnv": "DEEPSEEK_API_KEY",
"models": [
{ "id": "deepseek-chat", "contextWindow": 128000, "reasoningEfforts": ["low", "medium", "high"] },
{ "id": "deepseek-reasoner", "contextWindow": 64000, "reasoningEfforts": ["low", "medium", "high"] }
]
},
"anthropic": {
"displayName": "Anthropic",
"api": "messages",
"baseURL": "https://api.anthropic.com",
"apiKeyEnv": "ANTHROPIC_API_KEY",
"headers": { "anthropic-version": "2023-06-01" },
"models": [
{ "id": "claude-sonnet-4-20250514", "contextWindow": 200000, "reasoningEfforts": ["low", "medium", "high"] }
]
}
},
"skills": {
"enabled": true,
"customDirs": ["~/.config/narwhal/skills"]
},
"bundlePlugins": ["@deepseek-ai/dsh-sandbox"],
"mcpServers": [
{
"serverName": "filesystem",
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "~/projects"],
"toolCallTimeoutMs": 60000,
"failOnStartupError": false
},
{
"serverName": "github",
"transport": "streamable-http",
"url": "https://mcp.example.com/github",
"headers": { "Authorization": "{env:GITHUB_TOKEN}" },
"toolCallTimeoutMs": 30000
}
]
}
Integrations:MCP · Runtime Extensions · Skills
Narwhal 有三层可扩展性,全部通过 Integrations 设置面板管理:
| 层级 | 是什么 | 存放位置 | 安装方式 |
| Runtime Extensions | Cordis 框架插件(npm 包 + JS)— 例如沙箱 shell、凭证管理器、MCP 协议客户端 | 安装到 <dsh-home>/profiles/web/node_modules/;symlink 到 <dsh-home>/profiles/node_modules/ 供 Node 解析 | 设置面板 Install 按钮 → config.json.bundlePlugins[] |
| MCP Servers | 向 Agent 暴露 JSON-RPC 工具的工具服务器 | 仅配置(不下载代码)— DSH 在运行时 spawn 它们 | Registry 搜索 或 Add Custom 表单 → config.json.mcpServers[] |
| Skills | Markdown 指导包(SKILL.md + 可选资源),用于引导 Agent 行为 | ~/.config/narwhal/skills/<name>/SKILL.md | URL 粘贴(GitHub raw 或 git 仓库)→ config.json.skills.customDirs[] + enabled=true |
每次变更写入 config.json,syncToDsh() 重新生成 cordis.patch.yml,DSH 下次启动时加载。
API Keys
Narwhal 从不将明文 API Key 存储在 config.json 中。Provider 凭证通过 apiKeyEnv 引用环境变量名:
"providers": {
"my-provider": {
"apiKeyEnv": "MY_PROVIDER_API_KEY"
}
}
MCP server headers 也可以使用 {env:VAR_NAME} 模式,将秘密排除在配置文件之外:
"headers": { "Authorization": "{env:GITHUB_TOKEN}" }
所需环境变量会自动传递给 DSH runtime 进程。在 shell profile、GUI session manager 中导出,或通过 Narwhal 设置面板设置(仍然存储在 OS keychain 中,从不存储在 config.json)。
MCP Servers
Narwhal 支持两种 MCP 传输。每个 server 在生成的 cordis.patch.yml 中对应一个 dsh-mcp-client 插件条目。
stdio 传输 — spawn 本地进程:
{
"serverName": "brave-search",
"transport": "stdio",
"command": "node",
"args": ["/path/to/brave-mcp-server/index.js"],
"env": { "BRAVE_API_KEY": "{env:BRAVE_API_KEY}" },
"cwd": "/path/to/brave-mcp-server",
"toolCallTimeoutMs": 120000,
"failOnStartupError": false
}
streamable-http 传输 — 连接远程 MCP 端点:
{
"serverName": "datadog",
"transport": "streamable-http",
"url": "https://mcp.datadoghq.com/v1",
"headers": { "DD_API_KEY": "{env:DD_API_KEY}" },
"toolCallTimeoutMs": 60000,
"failOnStartupError": true
}
| 选项 | 默认值 | 说明 |
| toolCallTimeoutMs | 60000 | 单次工具调用最大毫秒数 |
| failOnStartupError | false | 若为 true,server 启动失败时 Agent 中止;若为 false,静默跳过 |
Skills
Narwhal 的 skill 系统默认禁用。启用后,Agent 会从内置和自定义目录加载持久 skill 包:
"skills": {
"enabled": true,
"customDirs": ["~/.config/narwhal/skills", "./my-skills"],
"bundledDir": "/opt/narwhal/skills"
}
| 字段 | 说明 |
| enabled | 启用 skill 加载。若为 true,Narwhal 将 skill-filesystem DSH 插件条目写入 cordis.patch.yml。 |
| customDirs | 包含 skill 文件夹的目录绝对路径。每个文件夹根目录下有 SKILL.md。默认安装路径为 ~/.config/narwhal/skills(由 Integrations UI 写入)。 |
| bundledDir | 可选的内置 skill 目录路径,随 Narwhal 一起分发。 |
配置 Schema
Narwhal 使用 Zod 在每次加载时校验 config.json。权威 schema 见 src/shared/config-schema.ts,settings.yaml 和 cordis.patch.yml 的具体构建逻辑见 src/main/dsh-sync.ts。
开发
项目使用:
- React + Vite 渲染器 UI
- Electron 桌面壳
- TypeScript 全栈
类型检查
pnpm typecheck
仅构建
pnpm build
开发模式(热重载)
pnpm dev
锁定的 runtime 版本同时在 package.json.dshRuntime(npm@0.1.1-rc.2)和 runtime/manifest.json 中声明。pnpm stage:runtime 脚本将该精确版本的 @deepseek-ai/dsh 安装到 runtime/dsh/,启动时自动发现。
打包
pnpm package:mac 执行 build + stage:runtime + electron-builder。stage 脚本读取 package.json.dshRuntime(npm@0.1.1-rc.2)并将锁定的 @deepseek-ai/dsh 安装到 runtime/dsh/,electron-builder 随后将其复制到 app bundle 中。
构建 DMG + ZIP(使用锁定的 npm runtime)
pnpm package:mac
或从本地 DeepSeek-Harness 源码安装(供 Harness 开发者)
DSH_RUNTIME_SOURCE=/absolute/path/to/DeepSeek-Harness pnpm package:mac
当前仅提供 macOS arm64 构建。Windows 和 Linux 打包在路线图上。
构建目前未签名,待所有者提供签名和 notarization 凭证。详见 docs/macos-release-checklist.md。
项目结构
narwhal/
├── src/
│ ├── main/ # Electron 主进程(BFF、runtime 管理、IPC) │ ├── preload/ # Preload 脚本(类型化 IPC 桥) │ ├── renderer/ # React 渲染器(UI 组件、样式) │ ├── recovery/ # Recovery 模式 UI │ └── shared/ # 共享类型和契约 ├── runtime/ # 已安装的 Harness runtime ├── scripts/ # 构建工具 ├── docs/ # 文档 └── build/ # 图标和构建资源
runtime/dsh/package.json 是由 pnpm stage:runtime 生成的安装 manifest。它从本地 Harness checkout 锁定 @deepseek-ai/dsh- workspace 包,只能在该上游 workspace 内解析。请勿手动编辑或直接对它运行 pnpm install。
Runtime 边界与已知局限
Narwhal 将 DeepSeek Harness(@deepseek-ai/dsh)作为独立 Node.js 子进程启动。本节说明双方各自负责什么,以及已知的边界 workaround。
Narwhal 与 DSH 的职责划分
| 关注点 | Narwhal | DSH Runtime |
| 进程生命周期 | ✅ Spawn、监控、重启 DSH | ❌ 自行管理(无 supervisor) |
| UI 渲染 | ✅ 全部可见 UI | ❌ 纯 API 层,无 UI |
| Workspace 列表 | ✅ workbench.json(userData) | ❌ 不知道 workspace 存在 |
| Session CRUD | ❌ 只读(通过 session.list) | ✅ 权威存储(内存 + 磁盘) |
| Session 生命周期 | ⚠️ 仅 workaround*(见下方) | ✅ 没有 session.delete API |
| Provider CRUD | ✅ UI + config.json | ⚠️ DSH settings namespace(Narwhal 还必须清理 credentials) |
| Credentials(API Key) | ❌ 只读 UI 展示 | ✅ .credentials.yaml + credentials.set/unset API |
| Settings | ✅ config.json(权威源) | ⚠️ settings.yaml(每次启动时从 config.json 生成) |
| 插件加载 | ✅ Cordis 插件安装/移除 | ✅ 运行时执行插件 |
通信协议
所有交互均为 JSON-over-HTTP + 类型化 envelope: { "type": "client-request", "rpcId": "uuid", "method": "session.list", "payload": {} }
入站事件通过 WebSocket 推送(/api/events.mux): { "type": "agent-error", "text": "...", "time": 1234567890 }
已知边界裂缝与 workaround
以下是 DSH 没有提供 Narwhal 所需 API 的地方,我们不得不绕过干净的边界。DSH 发布新版本补齐后,应重新审视这些 workaround。
1. 删除 workspace(含 session)
DSH 没有 session.delete HTTP API。从磁盘删除 session 文件(~/.narwhal/sessions/<slug>/)并清理 projcache(storages/session_projcache.json)对正在运行的 DSH 进程无效 —— 它仍从内存缓存返回旧 session ID。
Workaround: 从磁盘删除后,Narwhal 调用 supervisor.stop() 再 supervisor.start() 重启 DSH 进程。重启时 DSH 从干净磁盘重新加载,旧 session 消失。
代码位置: src/main/index.ts → deleteDshSessionsForCwd() + delete-workspace IPC handler。
2. 删除 provider(含已设置的 API key)
settings.mutate {op:"unset"} 只从 DSH settings namespace 删除 provider 配置 — 不会自动清理 ~/.narwhal/.credentials.yaml 里的 credential 条目。DSH 要求显式调 credentials.unset {ref}。credential ref 由 provider 的 apiKeyEnv 字段推导(默认 <ID>_API_KEY)。
Workaround: 调 settings.mutate unset 之前,先从 DSH settings 读取 provider profile 计算 credential ref,然后调 credentials.describe 确认 key 存在,最后调 credentials.unset 清 key。
代码位置: src/main/host-bridge.ts → deleteProvider()。
3. supervisor.start() 在 DSH 存活时是空操作
supervisor.start() 在已有活跃 DSH 进程时立即返回(幂等设计 —— "确保 DSH 在跑")。这意味着任何需要真正重启 DSH(不是只确保它运行)的路径都必须先 supervisor.stop() 再 supervisor.start()。
已知陷阱: 天真的 startHost() 调用在 DSH 已运行时会被静默跳过。正确姿势永远是 stop() → start()。
与 DeepSeek Harness 的关系
Narwhal 是一个独立项目,依赖兼容的 DeepSeek Harness runtime。关键点:
- Narwhal 不修改或再分发 DeepSeek Harness 源码
- 它安装经过验证的兼容 runtime,仅供本地执行
- Narwhal 的桌面 UI、品牌和交互模式完全自有
- 分发已安装的 runtime 时必须保留上游许可证和声明
贡献
欢迎贡献!详见 CONTRIBUTING.md。
1. Fork 仓库
2. 创建特性分支(git checkout -b feature/amazing-feature)
3. 完成修改(确保 pnpm typecheck 和 pnpm build 通过)
4. 提交变更(git commit -m 'Add some amazing feature')
5. 推送分支(git push origin feature/amazing-feature)
6. 创建 Pull Request
许可证
本项目基于 MIT License — 详见 LICENSE 文件。
分发 Narwhal 时,请保留 runtime/dsh/ 中已安装 runtime 的上游许可证和声明。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
yumingtao/narwhal
本地优先、隐私优先的 DeepSeek Harness 桌面应用。
相关插件 · UI / 主题
破甲增强版JeffKeeping/dsh-purge-plus
dsh-purge 的非官方分支fork:适配 DeepSeek Harness 0.2.1-rc.2、破甲内核…
★ 387已汉化DeepSeek 客户端
71.9
无代码业务系统平台nocobase/nocobase
NocoBase 是什么
★ 24,804已汉化DSH
71.8
1Panel 服务器面板1Panel-dev/1Panel
全球 250 万+ 自托管用户共同喜爱。
★ 37,350已汉化DSH
71.5
工作状态行动态ccch1mneyyy/working-activity
作者:chimney@ccch1mneyyy。社区项目,非 DeepSeek 官方项目。
★ 664已汉化DSH
71.2
多模型配置管家farion1231/cc-switch
跨平台桌面一体化助手,支持 Claude Code、Codex、OpenCode、DeepSeek Harness 等工具,唯一官网 ccswitch.io
★ 151,506已汉化DSH
71.1
AI 矢量设计工具ZSeven-W/openpencil
全球首个 AI 原生开源矢量设计工具。
★ 7,027已汉化DSH
71.1
这是你的仓库?认领条目