deepseek-harness-desktop/packages/dsh-git-graph

156Star5Fork6Issue0Watching

为 DSH Web 界面在官方工作区胶囊旁加 git 分支选择器与 Git 图谱面板,分支切换在 host 进程真实执行,并加 loopback + 工作区门卫。

语言
TypeScript
License
BSD-3-Clause
分支
main
ai-agentai-coding-assistantcodexdeepseekdeepseek-harnessdesktop-appdshdsh-plugin

安装

$ dsh plugin --profile web add github:ningbainb/deepseek-harness-desktop/packages/dsh-git-graph

在终端中运行以上命令,通过 dsh CLI 安装此插件。可在右上角切换 Profile。 第一次用 dsh?看这篇新手教程

一句话定位

为 DeepSeek Harness Web 界面在官方工作区胶囊旁加一个 git 分支选择器,并在弹层里提供「创建并检出新分支」与「Git 图谱」面板,分支切换在 host 进程真实执行,浏览器只负责展示与点选。

核心能力

  • 在会话输入卡正上方的 chip 上显示当前分支名(detached HEAD 显示「分离 HEAD」),非 git 工作区自动隐藏,避免出现死控件
  • 打开弹层后可搜索本地分支、查看当前分支的勾选标记,并在底部看到「未提交的更改」摘要(dirty 文件数)
  • 切换分支前自动跑守卫检查(未解决冲突、进行中的合并/rebase/cherry-pick/revert/bisect、目标分支被其他 worktree 检出),守卫命中时切换会被拒绝并给出可读的中英错误提示,不会破坏磁盘工作树
  • 支持「创建并检出新分支」,先在前端镜像 git check-ref-format --branch 的命名规则即时反馈,再走 host 端权威校验和重名检查,最后执行 git switch -c <name>,成功后自动刷新 chip 与图谱
  • 提供只读 Git 图谱弹层:以拓扑顺序展示分支/标签/远端的 commit 列表,含等宽字体的泳道字符、相对时间(刚刚 / X 分钟前 / X 小时前 / X 天前)、ref 标签,分页加载(每页 100 条,初始 200 条)
  • host 端通过 /git/events SSE 推送外部 git 状态变化(订阅时每 30 秒轮询一次,单次探测 15 秒超时);chip 还会在 window focus 时刷新(5 秒节流),保证另开终端切换分支后 UI 能跟上

技术实现

  • 语言: TypeScript(host 与 browser 半区共享 src/core 纯逻辑;TypeScript 模块化构建,css-modules 走 lightningcss)
  • 关键依赖: @deepseek-ai/dsh-client-ui-conversation@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-client-runtime@deepseek-ai/dsh-client-locale(peer 注入面,皆 ^0.1.0-rc.7,package.json:60-70);host 侧依赖 @deepseek-ai/dsh-host-webserver@deepseek-ai/dsh-subprocess@deepseek-ai/dsh-workspace(package.json:61-70);@deepseek-ai/cordis 4.x 做 function plugin 装配
  • 架构模式: 双面 cordis 插件。host half(src/index.ts)通过 inject = ['webServer','subprocess','workspaceRegistry'] 挂载 GitService 与 /git/* 路由;client half(src/client/index.ts)通过 inject = ['slots','sessions','connection','locale','conversation'] 注入 git verbs(repoStatus / branches / switchBranch / createBranch / graph / subscribeChanges)。激活走 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }ui-git-graph row 注入 profile(cordis.patch.yml:1-12)
  • 入口文件: packages/dsh-git-graph/src/index.ts(host 半区 apply 入口)+ packages/dsh-git-graph/src/client/index.ts(browser 半区 apply 入口)。git 命令 argv 集中在 src/core/git-command.ts,纯分支名校验 mirror 在同一文件

适用场景

当你在 DeepSeek Harness Web 里处理多个 git 分支、经常需要从对话界面里直接切到别的分支去看历史/对比、又不想打开终端;同时你希望切换前有冲突保护,避免「切过去发现还有未解决的合并冲突」导致工作树脏掉。也适合作为只读 Git 图谱的轻量替代——你不必离开 dsh Web 就能看到全分支/标签/远端的拓扑结构与 ref 标签。

前置依赖与兼容性

依赖最低版本说明
DeepSeek Harness(web profile)^0.1.0-rc.7peerDependencies 全锁此版本(package.json:60-70);bundled patch 走 cordis.patch.yml(cordis.patch.yml:1-12)
Node.js^22.19.0 || >=24.0.0package.json:7-9 engines.node
平台macOS / Windows / Linux跨平台,无原生模块(package.json 仅声明 react ^18.2.0 peer,无 node-gyp 依赖)
系统 git 可执行必需host 半区通过 ctx.subprocess.spawn(['git',...])(Windows 强制 git.exe)真实执行 git 命令(src/host/git-service.ts:53-94);无 git 则所有操作以 internal 错误返回
已注册的工作区必需/git/* 路径门卫:请求的 path 必须 realpath 后命中 ctx.workspaceRegistry 里的某个工作区(src/index.ts:35-48);浏览器对未注册目录发起请求会得到 workspace-unknown
Loopback 客户端必需/git/* 与 /git/events SSE 都拒绝非 loopback socket/Host 请求(src/host/routes.ts:83-103),LAN 暴露的 dsh web 对外网客户端一律 403
DSH_HOME 环境变量可选未设置时回退到 ~/.dsh,host 半区把 worktree 目录放在 $DSH_HOME/worktrees/<repoHash>/<runId> 下(src/index.ts:82 / src/host/worktree-service.ts:212)

安装方式

dsh plugin --profile web add github:ningbainb/deepseek-harness-desktop/packages/dsh-git-graph

配置项

本插件无需额外配置。所有守卫、SSE 节奏、上下文挂载超时都在代码里固定写死,不开放用户级 schema。

配置类型说明默认值
DSH_HOME环境变量(可选)决定 worktree 子目录的存放位置;不设时使用 ~/.dsh(src/index.ts:82)~/.dsh

源码里其他可调参数(仅供二次开发参考,运行时不通过插件配置):

  • CONTEXT_FALLBACK_MS = 2000:等待 conversation.input.selector.context 槽位声明的超时,超时后 chip 回退到 conversation.input.dock(src/client/index.ts:108)
  • POLL_INTERVAL_MS = 30_000:host SSE 在有订阅者时轮询 workspace 状态的时间间隔(src/host/routes.ts:45)
  • STATUS_TIMEOUT_MS = 15_000:单次状态探测的硬超时,避免挂死的 git 子进程卡住推送流(src/host/routes.ts:57)
  • FOCUS_REFRESH_MIN_MS = 5_000:window focus 触发的 chip 重新拉取节流(src/client/chips/BranchChip.tsx:37)

常见问题

Q: 这个插件会改动 DeepSeek Harness 官方源码吗?

A: 不会。AGENTS.md:5-6 明确「主仓(sibling checkout)零改动;本仓是自包含的 cordis 插件包」;所有类型来源是 node_modules 里的 @deepseek-ai/* peerDependencies(package.json:60-70),不引入对 DSH 源码 checkout 的 tsconfig 引用。卸载即回到官方原生行为。

Q: 分支切换是真实的 git switch 吗?会影响其他会话吗?

A: 是真实的。src/host/git-service.ts:176-197 调用 git switch --no-guess <branch> 跑在 repoRoot 真实工作树上,并且会作用于该 workspace 下所有会话(不是单会话的 cwd 覆盖)。这意味着如果你在一个会话里切了分支,另一个会话来打开同一个工作区时也已经是新分支了;这是「工作区级」语义,不是「会话级」。

Q: 浏览器能不能借这个接口对任意目录跑 git?

A: 不能。src/index.ts:35-48 的 workspaceGate 把请求路径 realpath 后必须命中 ctx.workspaceRegistry 里某个已注册路径,否则返回 workspace-unknown;routes.ts:83-103 还会拒绝所有非 loopback 客户端(LAN 暴露的 dsh web 对外部客户端直接 403),/git/* 还强制 POST + application/json(routes.ts:215-229)。Worktree 路由更严:只接受不透明 ID,路径/base ref/argv 一律不接(README.md:74 / worktree-routes.ts:46-48)。

Q: 什么时候分支 chip 会隐藏?

A: 当它查不到 cwd(sessions list 里该 sessionId 的 cwd 为空)或者 status 返回 null(非 git 工作区)时整个 chip 不渲染。src/client/chips/BranchChip.tsx:253 写「repo === undefined || repo === null return null」;这种隐藏优于禁用,避免死控件,且工作区变仓库后 chip 会在下一次刷新时自动出现。

Q: 切换失败会给出什么样的错误?

A: 错误分两大类:守卫级(conflicts-present、operation-in-progress、branch-in-other-worktree、invalid-branch-name、branch-already-exists、target-branch-not-found、workspace-unknown)和切换时 Git 抛出的覆盖冲突(tracked/untracked-changes-would-be-overwritten,附前 2 个文件路径 + 溢出计数)。src/host/git-service.ts:294-311 的 guardBlock 跑前一种,src/core/git-command.ts:178-196 的 classifySwitchFailure 把 stderr 归到稳定 code;客户端在 src/client/chips/error-copy.ts:27-50 把 code 翻成中英双语可读句子。

Q: 跟官方分支管理有冲突吗?会被重复工作区选择器覆盖吗?

A: 工作区选择不在本插件里——官方工作区胶囊是唯一入口(src/client/index.ts:23-24 / ADR-001:36)。插件只补「分支」和「图谱」两个动作,对应 UI 上与官方胶囊并排的 28px 透明 chip。分支状态不会写进 session log,也不会进入模型可见面(src/index.ts:6-8),所以不会影响模型侧的对话历史。

Q: 需要 Git 在系统 PATH 里吗?

A: 需要。Host 侧的 GitService 用 subprocess 服务直接 spawn git(macOS/Linux)或 git.exe(Windows,src/host/git-service.ts:53-55 强制走原生可执行名避免 .cmd shim 解析问题);运行机器必须装好 git,否则所有 /git/* 接口都会以 internal 错误返回。Worktree 操作走同一个 spawn seam。

Q: 怎么卸载?

A: dsh plugin --profile web remove @linxin666/dsh-client-ui-git-graph(README.md:67)。包名是 npm 发布的 @linxin666/dsh-client-ui-git-graph,激活插件时挂的是 ui-git-graph 这个 cordis row 名(cordis.patch.yml:11)。卸载后下次 dsh web 启动就不再注册任何 /git/* 路由与浏览器 chip。

上手难度

入门 — 安装一行命令,重启 dsh web 即生效;无需配置;用户只需点击 chip、选择分支、查看错误提示。所有高级行为(守卫、SSE 节奏、loopback 限制)都由插件内置。

已知问题与限制

  • 仅支持本地分支:分支列表只走 git for-each-ref refs/heads(src/host/git-service.ts:151 / src/core/git-command.ts:19-23),不会列出远端分支。如果你在做 git fetch 后想看 origin/* 分支,需要在终端 fetch 后让 chip 重新拉取(挂载/弹层打开/focus 都会触发)
  • 切换窗口受工作区状态强约束:未解决合并冲突、进行中的 merge/rebase/cherry-pick/revert/bisect、目标分支被其他 worktree 检出,这三种情况都会被守卫拦下(src/host/git-service.ts:294-311),你需要先去终端完成或 abort 这些操作
  • LAN 暴露时不服务 /git/:所有 /git/ 接口强制 loopback + Host 校验(src/host/routes.ts:83-103),如果你把 dsh web 反向代理到公网,对应接口会返回 403;插件不打算为外部访问放宽这个限制(ADR-001:38)
  • Worktree 功能只在 host half 暴露(src/index.ts:62-87),浏览器侧 chip 不带 worktree 入口;这部分由桌面端 Task Board 这类插件另行消费 /git-worktree/* 路由
  • Worktree 子目录固定在 $DSH_HOME/worktrees/<repoHash>/<runId> 下(src/host/worktree-service.ts:212 / src/index.ts:82),且路径逃逸会被 TypeError 阻止(src/host/worktree-service.ts:214);如果你移动了 DSH_HOME,需要保证旧路径下的 worktree 已被清理或主动移除,否则会出现 orphan 记录
  • 浏览器 chip 与官方工作区胶囊是「会话级 vs workspace 级」分离的:会话切换不会清空 chip,但切到非 git cwd 时 chip 会自动隐藏(src/client/chips/BranchChip.tsx:253)
  • package.json:7-9 把 Node 锁到 ^22.19.0 || >=24.0.0,更早的 Node 不在测试矩阵内