packages/dsh-git-graph
zhu1090093659/dsh-web输入框上方提供 Git 分支选择器,并把分支泳道与提交历史画成图谱,沿着时间线找到任意变更。
DSH 网页界面的代码管理面板,五步搞定 Git 日常:① 环境检查自动检测 git/gh/ssh,缺工具一键安装(gh 支持镜像下载源加速,Windows 也可用)并带检查更新;② SSH ed25519 密钥生成/连接测试 + 一键写 443 配置绕过 22 端口封锁,Gitee 私人令牌在此配置;③ 查看仓库状态、并排 diff、暂存提交、切换分支、查看/回滚历史,一键新建仓库并推送,自动忽略超 100MB 文件;④ 列出账号名下所有远程仓库一键克隆;⑤ 五步发布 npm 包。支持 GitHub / Gitee 双平台,界面中英文跟随 DSH 设置实时切换。
English | 中文
版本:v1.25.0 | 更新日志见文末「版本历史」
界面语言跟随 DSH 设置实时切换:面板与 host 端消息自动使用 DSH 的语言(设置 → 通用 → 语言),中文 ↔ 英文即时生效,无需重启。
DSH Web GUI 源代码管理插件:一个「代码管理」面板把 ① 环境检查 → ② SSH 密钥与连接 → ③ 代码管理 → ④ 克隆仓库 → ⑤ 发布 npm 包 全流程串起来,GitHub / Gitee 双平台通用。环境检查自动检测 Git / GitHub CLI / SSH,缺工具可一键安装(按平台自适应、尽量免 sudo;gh 支持镜像下载源加速,Windows 亦可用),并带「检查更新」跟进新版本。
入口默认注册进 DSH 自带的右侧栏(不再保留单独的按钮):在侧边栏「开始」页上,「代码管理」是与 工作区文件 / 新建终端 / 浏览器 / 壁纸引擎 并列的一张卡片,点开即在该栏里以页面形式打开面板。老宿主自动降级:装了 dsh-better-sidebar 时注册成它的侧边栏 Tab;两者都没有时(旧版宿主)才回退到右上角的 header 入口(有会话时放在「Session 日志」旁;无会话/新对话时钉在右上角,与有会话时逐像素同高)。不会占用左边栏。
集成入口(按可用性自动选择,无需手动切换;任何时刻只有一个入口):
DSH 自带右侧栏(默认,harness ≥ 0.1.5):「代码管理」作为侧边栏「开始」页(guide)上的一张卡片 + 该栏里的一个页面。点卡片即在右侧栏打开面板(官方行为:openTab(kind, { replaceTab: true }),替换掉「开始」页本身,像浏览器起始页那样)。卡片顺序排在内置三张(工作区文件 10 / 新建终端 20 / 浏览器 30)与其它插件(壁纸引擎 100)之后 = 110。
sidebar.right.pane.tab 存在即代表宿主自带右侧栏,此时插件完全不注册 header 入口 —— 这正是「去掉单独按钮」的落点。title: () => t("代码管理")),由宿主每次渲染时求值 ⇒ 切语言无需重新注册。variant: "tab":填满该栏、不画自己的 ✕(关闭由宿主的 tab 栏负责)。已安装 dsh-better-sidebar(旧宿主降级):「代码管理」注册为它侧边栏的一个新 Tab 页面,点击侧边栏 Tab 直接打开面板;
两者都没有(旧宿主降级):入口只注册在会话 header 里(两处严格互斥,任何时刻只有一个「代码管理」可见)——
conversation.session.header.utilities 槽位放在「Session 日志」旁(同款胶囊、间距 8px 一致);shell.overlay 槽注册一个常驻按钮,钉在右上角、且与「有会话时」那枚按钮完全同高。
.header 的 padding-top: 10px + .titleRow 的 min-height: 30px(居中)⇒ 标题行占 y=10..40、中心 25px;按钮高 32px ⇒ top = 10 + (30 - 32)/2 = 9px。空态按钮取同一个 9px,所以两种形态切换时按钮不跳位。桌面壳里再叠加 --dsh-windows-titlebar-height(普通浏览器为 0),写法是 calc(var(--dsh-windows-titlebar-height, 0px) + 9px)。right: 48px,让开空态 header 右上角已有的「右侧栏展开按钮」:空态下 .headerBlank .headerCorner 带 margin-left: auto,把 ui-sidebar-right 的 ExpandButton(28×28 圆钮)顶到最右,其盒子位于视口右侧 12px~40px(.header 的 padding-right: 28px 减去 .headerCorner 的 margin-right: -16px 得到右缘 12px)。48 = 40 + 8(8px 与 header 内其它控件的间距一致),整个按钮落在圆钮左侧、互不相交。降级入口点击都打开同一个 dsh-better-sidebar 外观的右侧集成面板(内容放同一面板),并把主内容区往左推挤。
座位「晚到」有宽限窗口(600ms):
sidebar.right.pane.tab座位属于另一个包(ui-sidebar-right),加载器不保证顺序。若座位比本插件晚声明,这段时间里先不挂兜底按钮 —— 否则官方宿主上会先闪一个「代码管理」按钮、随后才被侧边栏卡片取代。窗口内座位出现 → 永远不挂;窗口耗尽仍没有 → 才按老宿主降级(老宿主本来就吃这条路径,晚 0.6s 出现按钮不可见)。全程零 I/O,只是一个计时器。
关闭方式:右侧栏形态由宿主的 tab 栏负责(点 ✕ / 切页);降级面板有三种 —— 面板右上角 ✕(标题行固定不滚动,内容滚到哪都在视口内)、按 Esc、或再次点「代码管理」按钮。
降级面板顶部会让开窗口控件那条带(
--dsh-frame-chrome-top):桌面壳里 ─ □ ✕ 是 OS 绘制的覆盖层,任何贴到右上角的网页按钮都会被它压住、点不到(v1.24.1 修的正是这个「打开后关不掉」),所以面板整体从该带下方开始;原生全屏时该变量归零,面板仍用满整屏。
刻意不注册进 DSH 自带左边栏(
sidebar.footer.action)——那个入口会挤在左边栏「设置」上方,与侧栏自身的项混在一起。也刻意不去抢 DSH 自己的单占位槽:
conversation.session.header.corner已被自带右侧栏开关(ui-sidebar-right 的 ExpandButton)占用,conversation.session.header.leading被自带侧栏开关(ui-sidebar 的 HeaderLeadingControls)占用;sidebar.panellist的每一项 id 必须是真实主面板 id(会交给layout.selectPanel),本插件没有主面板,注册进去只会得到点不动的死按钮。
检测只是激活时两次内存读取(座位声明 +
ctx.get('sidebarRightTabs')),零 I/O、零网络,不影响 DSH 启动速度。
面板分五步(①②③ 为核心三步,④⑤ 为新扩展板块,默认折叠按需展开):
Windows / macOS / Linux,对应底层 Node 平台标识 win32 / darwin / linux)git version 2.55.0、gh version 2.97.0)ssh 即显示「已找到」)~/.local/bin/gh(目录不存在会自动创建,全程不需要管理员权限);失败时自动回退系统包管理器。sudo -n;先探测 sudo 是否免密,需要密码时不再盲目执行,而是给出「请在终端手动执行」的指引 + 完整命令)。Git(macOS):brew,无 brew 时走 Xcode 命令行工具(xcode-select --install,系统自带安装,含 git/ssh)。~/.local/bin)时,弹窗提示并显示「重启 DSH」按钮——点击后 host 通过 detached 辅助进程按原启动命令自动拉起新进程(机制同 dsh-update),页面短暂断开后刷新即可;若 host 已能识别(如设置了 DSH_SCM_GH 或 ~/.local/bin 在 PATH),则不打扰直接完成。~/.ssh/*.pub 中已存在的 ed25519 公钥——优先用 id_ed25519;否则用找到的第一个(支持任意命名的密钥,如 github_ed25519);都没有则默认名 id_ed25519。状态行显示实际检测到的密钥文件名,SSH config 的 IdentityFile 也用它github.com → ssh.github.com:443;Gitee 写 gitee.com:443。用于绕过封锁 22 端口的网络。github.com → github.com:22(标准端点)。家宽 / 办公室 / VPS 通常直连即可。~/.ssh/config 里对应的 Host 块(不会留下重复块,也不动其它 Host);状态行显示的是实际配置的端口(host 端解析 ~/.ssh/config 得出),不再是写死的 (443)。ssh -T -p <port>),失败时提示「若是网络封锁 22 端口,请改用 443」。ssh -T -p <port> git@<host>(GitHub 走 ssh.github.com / github.com,Gitee 走 gitee.com)gh 是否已登录及账号443 与「是否有代理」无关(常见误解)。 普通 HTTP 代理只对 git-over-HTTPS / curl 生效;OpenSSH 完全无视
HTTP_PROXY/HTTPS_PROXY(要走代理得配ProxyCommand)。所以「我开了代理」并不等于「22 端口通了」——该不该用 443 取决于你的网络是否封锁 22 端口,因此这里交给用户自己选,而不是由插件猜测。海外/正常网络需要 443 吗?——不需要。 「写 SSH config」本身是可选的(只有你点按钮才写
~/.ssh/config)。GitHub 标准端点就是git@github.com走 22 端口,正常网络开箱即用:生成密钥 → 把公钥贴到 GitHub → 测试连接 → 推送,全程 22 端口。443(Host github.com → HostName ssh.github.com, Port 443)是 GitHub 官方支持的兜底方案,典型场景是国内网络、部分公司/校园网;只有当 443 也被封锁时才会反而不通(极少数网络)。Gitee 是国内平台,海外用户基本只会用到 GitHub。
gh CLI,Gitee 走 Gitee OpenAPI)projects 权限)→ 保存在本机 ~/.dsh/storages/source-code-mgmt-gitee.json(0600,不写入插件目录、不回传到浏览器/日志);可一键清除;令牌无效会自动清掉~/.dsh/storages/source-code-mgmt-dirs.json,不写入插件目录,开源不泄漏个人路径),下次打开无需重新选择;手动添加的目录会以自定义目录徽标显示,末端带 ✕ 可一键删除该下拉记录(只删记录,不删实际文件夹)git status 的 XY 状态区分 staged/unstaged),并显示「已暂存 / 未暂存」标记checkoutgh 账号如 Zhucy123 名下,Gitee 平台 = Gitee 令牌账号如 Zhucy2100 名下)。这样:
deepseek-ai/deepseek-harness)不会被当作「用户自己的远程」显示,ahead/behind 也不对它计算;git init(+设默认身份),不拉取不推送,由用户自行决定下一步是拉取还是推送git add + 生成一个初始提交再创建,避免推送时报 "no commits found"
gh repo create --private|--public --source=. --pushPOST /user/repos 建仓,再设置 SSH 远程 git@gitee.com:<owner>/<name>.git 并 git push(走 ② 已配的 SSH 密钥).gitignore 的不重复添加,并显示「未上传原因」gh,Gitee 走 OpenAPI)名下所有远程仓库列出来,并自动标记本地是否已有同名 git 仓库(默认按「默认工作区 + 已登记工作区 + 自定义目录」判定)git@github.com:… / git@gitee.com:…),成功后自动加入自定义目录列表,③ 里可直接选中npm config get registry → ② 查看认证配置 npm config list(自动脱敏 token/auth/password)→ ③ 验证身份 npm whoami → ④ 预览打包 npm pack --dry-run(先看会发布哪些文件,不真正打包发布)→ ⑤ 发布 npm publishhttps://registry.npmjs.org:即使你的全局 npm 配置是镜像源(如 registry.npmmirror.com——镜像只同步、不接受发布),whoami / npm login / npm publish 也统一带 --registry=https://registry.npmjs.org;状态区在检测到镜像配置时会黄色提示「发布将使用官方源」。konsole / gnome-terminal / xterm 系等),再 spawn(并挂 error 监听兜底)——修复了旧版 spawn 不存在二进制时异步 ENOENT 未捕获、直接把 dsh host 进程打崩的问题;KDE 下用 konsole --separate 开独立窗口,不与宿主所在终端实例纠缠。找不到终端时明确返回手动命令指引,不再假装成功。DSH 打开时不联网同步仓库,只预取静态的环境/SSH/工作区列表。打开插件、切换工作区、刷新状态、以及推送/拉取/暂存/提交等操作后,都会联网获取对应工作区的最新仓库状态,并显示「⟳ 刷新中…」提示——避免打开/重开/切换时显示可能过期的旧数据(如旧的「无改动」)。关闭面板再打开也会重新同步,不会停留在旧状态。
本插件以 Profile Bundle 形态分发:
package.json声明了dsh.bundle(携带cordis.patch.yml配置层),所以dsh plugin --profile web add一条命令装完即自动激活——无需手动编辑任何配置文件。
最省事的方式——只需一条命令,且不用先进 profile 目录,推荐给普通使用者。
在任意目录执行:
# 用 DSH 自带的插件命令(自动定位/初始化 web profile)
dsh plugin --profile web add source-code-mgmt
该命令在 web profile 目录里执行
pnpm add,成功后对账插件层:检测到本插件声明dsh.bundle,会自动把它追加进dsh.profile.bundles(见~/.dsh/profiles/web/package.json)并注册进 Cordis loader 树,一步装完即用。
装完完全重启 dsh web(不是刷新页面,而是要停掉旧进程后重新启动),然后浏览器 F5 刷新,「代码管理」入口即出现(已装 dsh-better-sidebar 时为侧边栏 Tab,未装时为右上角 Session log 旁的「代码管理」按钮 + 右侧集成面板)。
Windows (PowerShell):
# 用 DSH 插件命令安装本地源码(link: 协议,符号链接,改源码即生效)
dsh plugin --profile web add link:C:/path/to/source-code-mgmt
dsh web
Linux / macOS:
dsh plugin --profile web add link:/home/yourname/path/to/source-code-mgmt
dsh web
Windows / Linux / macOS 通用:
dsh plugin --profile web add git+https://github.com/Zhucy123/source-code-mgmt.git
dsh web
git 安装会把源码实际拷贝到 node_modules,改动源码需重新
dsh plugin --profile web add ...拉取(不像link:是符号链接、改源码即生效)。
安装并重启后,可以核对以下几点:
~/.dsh/profiles/web/package.json 的 dependencies 里应有 source-code-mgmt。~/.dsh/profiles/web/package.json 的 dsh.profile.bundles 列表里应有 source-code-mgmt(dsh plugin add 自动写入,无需手动编辑)。link: 方式):~/.dsh/profiles/web/node_modules/source-code-mgmt 指向源码目录(Windows 显示为 Junction)。| 现象 | 原因 / 处理 |
|---|---|
已 dsh plugin add 并重启,但按钮不出现 | 最常见:装完没有完全重启(不是刷新)。停掉旧 dsh web 进程再启动(旧进程还占着 3080 端口时,新实例起不来)。 |
| 安装时提示「declares no dsh.bundle」 | 装到的版本缺少 bundle 声明(旧版或打包遗漏 cordis.patch.yml)。确认版本 ≥ 1.9.0 后重新安装/更新。 |
| 出现「Failed to load plugins」 | 插件 host 端 index.js 启动报错(多为依赖解析问题)。查看启动日志,确认 node_modules 依赖已装齐。 |
| 路由 | 方法 | 说明 |
|---|---|---|
/api/source-code-mgmt/env | GET | 环境检查(git/gh 版本) |
/api/source-code-mgmt/install-tool | POST | 一键安装缺失工具(body tool: git/gh/ssh;gh 可另传 source: official 或 gh-proxy.com/ghfast.top/ghproxy.net 等镜像 id,按平台自适应:gh 在 Linux/macOS 免 sudo 装到 ~/.local/bin;git/ssh 走系统包管理器 / brew / Xcode CLT / winget;响应为 NDJSON 事件流:step/out/result,供进度弹窗实时展示) |
/api/source-code-mgmt/install-command | POST | 按工具与下载源返回可复制的安装命令(body tool + 可选 source;与「安装」同源逻辑) |
/api/source-code-mgmt/check-update | POST | 检查 gh / git 是否有可用更新(body tool;返回 {current, latest, hasUpdate}) |
/api/source-code-mgmt/ssh | GET | SSH 密钥 / config / gh 登录状态 |
/api/source-code-mgmt/gh/login | POST | 打开终端运行 gh auth login(交互式登录需用户在弹出终端完成;找不到终端时返回手动命令指引) |
/api/source-code-mgmt/gen-key | POST | 生成 ed25519 密钥 |
/api/source-code-mgmt/write-config | POST | 写入 SSH config(body provider: github 默认 / gitee) |
/api/source-code-mgmt/ssh-test | POST | 测试 SSH 连接(body provider: github 默认 / gitee) |
/api/source-code-mgmt/default-dir | GET | 当前工作区目录 |
/api/source-code-mgmt/workspaces | GET | 列出所有工作区目录 + 自定义目录集合(DSH 工作区 + 插件自定义目录) |
/api/source-code-mgmt/pick-dir | POST | 宿主端弹出原生文件夹选择对话框,返回选中的路径 |
/api/source-code-mgmt/add-workspace | POST | 校验目录存在并持久化加入插件自定义目录列表,返回合并后的工作区列表 |
/api/source-code-mgmt/remove-workspace | POST | 仅删除自定义目录的下拉记录(不删实际文件夹),返回更新后的列表 |
/api/source-code-mgmt/align | POST | 强制对齐:git fetch + git reset --hard origin/<branch>,本地完全重置为远程状态 |
/api/source-code-mgmt/init-git | POST | 仅 git init + 设置默认身份,不拉取不推送(由用户决定下一步) |
/api/source-code-mgmt/repo-exists | POST | 检测同名仓库是否存在(body provider: github/gitee) |
/api/source-code-mgmt/repo?dir= | GET | 获取仓库状态(query provider: github/gitee) |
/api/source-code-mgmt/repo-diff?dir=&path= | GET | 按需返回单个改动文件的 unified diff 文本 |
/api/source-code-mgmt/stage | POST | 暂存改动(body dir、path;path 空=全部) |
/api/source-code-mgmt/unstage | POST | 取消暂存(body dir、path;path 空=全部) |
/api/source-code-mgmt/commit | POST | 用自定义信息提交(body dir、message、paths?;沿用 pushFlow 的无身份兜底逻辑) |
/api/source-code-mgmt/branches | POST | 列出分支(当前分支在前) |
/api/source-code-mgmt/checkout | POST | 切换分支(body dir、branch) |
/api/source-code-mgmt/log | POST | 最近提交历史(body dir、count?,返回 hash/subject/author/date) |
/api/source-code-mgmt/revert | POST | revert 某提交(body dir、hash) |
/api/source-code-mgmt/cherrypick | POST | cherry-pick 某提交(body dir、hash) |
/api/source-code-mgmt/commit-diff | POST | 返回某提交的完整 patch(body dir、hash) |
/api/source-code-mgmt/push | POST | 提交并推送(git 操作,平台无关) |
/api/source-code-mgmt/pull | POST | 从远程拉取更新(git pull --ff-only,已最新/成功/冲突反馈) |
/api/source-code-mgmt/merge-push | POST | 拉取并推送(git pull --rebase + git push,本地有更改且远程有更新时合并推送) |
/api/source-code-mgmt/force-push | POST | 强制推送(git push --force,覆盖远程为本地状态) |
/api/source-code-mgmt/force-pull | POST | 强制拉取(git pull --force,拉入远程更新) |
/api/source-code-mgmt/create | POST | 新建仓库并推送(body provider;GitHub 走 gh repo create,Gitee 走 OpenAPI + SSH push) |
/api/source-code-mgmt/set-visibility | POST | 修改仓库可见性(body provider;GitHub 走 gh repo edit,Gitee 走 PATCH /repos/{owner}/{repo}) |
/api/source-code-mgmt/gitee-token | GET/POST | GET:令牌是否已配置 + 账号;POST:保存({token})或清除({clear:true})Gitee 令牌 |
所有路由均为 loopback-only(sec-fetch-site + Origin 校验),仅本机浏览器可访问,LAN/手机来源一律 403——与控制面板同一策略。
process.platform 检测平台~/.ssh 通过 homedir() 定位(Windows: C:\Users\用户名\ .ssh,Linux/macOS: /home/用户名/.ssh 或 /Users/用户名/.ssh).exe)→(仅 Windows)③Git 自带目录(usr\bin / bin)回退,最后兜底用裸命令名。因此只要装了 Git,即使 ssh 不在 PATH 里也能正常工作,换设备无需额外配置。DSH_SCM_GIT / DSH_SCM_GH / DSH_SCM_SSH / DSH_SCM_SSH_KEYGENssh.exe(usr\bin\ssh.exe)在被 detached/agent 进程调用时可能报 couldn't create signal pipe, Win32 error 5,导致 git push/git pull 失败。插件执行 git 远程命令时会自动注入 GIT_SSH 指向解析到的可用 ssh(通常为系统 OpenSSH C:\Windows\System32\OpenSSH\ssh.exe),避免该问题。core.quotepath 会把含非 ASCII 字节的路径输出成八进制转义的引号串(显示为乱码)。插件对所有 git 调用统一注入 -c core.quotepath=false(直接输出原始 UTF-8 路径),并对仍带引号转义的路径做 parseGitPath() 反转义兜底——改动列表 / diff 里的中文文件名显示正常~/.local/bin/gh,失败回退系统包管理器);git/ssh 在 Linux 走 apt-get / dnf / pacman(先探测 sudo 免密,需要密码时给出手动命令指引而非静默失败),macOS 走 brew / Xcode 命令行工具,Windows 走 winget / 内置功能(回退 choco/scoop)。安装过程有实时进度弹窗,需要重启时提供一键重启 dsh。SSH 密钥探测、本地 Git 工作流、并排 diff 等在三个平台行为一致;「浏览目录」的原生选择器仅 Windows 可用,macOS/Linux 上请在输入框直接填路径(可手动输入粘贴)。git clone https://github.com/Zhucy123/source-code-mgmt.git
cd source-code-mgmt
# 在本地 DSH 测试(安装到 web profile,自动激活)
dsh plugin --profile web add link:$(pwd)
lib/client.js(浏览器端)→ 刷新页面即生效lib/index.js(host/Node 端)→ 刷新页面即可让面板重新检测(toolInstalled 按当前 PATH / ~/.local/bin 实测);如 host 端 API 仍为旧版,重启 dsh web 进程一次变更:入口默认注册进 DSH 自带的右侧栏,「代码管理」不再是一个单独的按钮:
sidebar.right.pane.tab 座位 + sidebarRightTabs.register({ guide })),并不是 sidebar.footer.action(左边栏底部)——左边栏早在 v1.1.2 起就明确不碰。slots.inject('sidebar.right.pane.tab', …) —— 座位声明了就是这个宿主自带右侧栏,回调跑没跑即天然的能力门(不嗅探版本号,少一个漂移源)。回调里:
ctx.get('sidebarRightTabs').register({ id: 'source-code-mgmt/sidebar', kind: 'source-code-mgmt', title: () => t('代码管理'), guide: [{ id: 'panel', order: 110, title, description, icon: RepoIcon }] }) —— 这张 guide 条目就是「开始」页上的卡片;slots.register({ name: 'sidebar.right.pane.tab', key: 'source-code-mgmt/sidebar' }, () => h(ScmPanel, { variant: 'tab' })) —— 卡片点开后在该栏里渲染的页面。key 必须等于 tab 类型的 id:宿主 TabSlot 用 definition.id 作 entryKey 派发 tab 本体,写错就会「卡片点开了但面板空白」。order: 110:内置三张是 10 / 20 / 30,壁纸引擎(dsh-plugin-wallpaper-engine)是 100,所以本卡片排在其后、落在列表末尾。title / description 是函数,宿主每次渲染都求值 ⇒ 切语言不需要重新注册(旧的 better-sidebar 路径因为收的是字符串,才需要 subscribe 后重注册)。sidebar.right.pane.tab 属于另一个包,可能比本插件晚声明。新增 SCM_FALLBACK_GRACE_MS = 600:这段窗口内先不挂兜底按钮(否则官方宿主上会先闪一个按钮再被卡片取代),窗口内座位出现 → 永远不挂,窗口耗尽才降级。全程零 I/O。sidebarRightTabs 服务已就绪,故 ctx.get 短轮询(250ms × 40 ≈ 10s)后才放弃并回落 ③;tab 本体注册包在 ctx.effect 里,让 DSH 在插件卸载时自动注销(避免 fiber 已 dispose 时抛 INACTIVE_EFFECT)。verify-entry-runtime.test.mjs(32 项,三段分支 + 卡片形状 + 图标尺寸 + 流体布局 + 卸载清理)、verify-sidebar-entry.test.mjs(16 项,「忠实宿主模型」:座位按宣告纪元重跑注入、keyed 座位拒绝重复 key、ctx.effect 按注入作用域回收、卸载按 LIFO / FIFO 两种顺序都验、注册抛异常路径,以及「兜底只能从路由里挂」的结构断言)、verify-sidebar-adversarial.test.mjs(16 项对抗式:能不能让官方宿主冒出独立按钮 / 能不能把用户搞成「零入口」)、verify-sidebar-postfix.test.mjs(3 项,独立复核者所写)。这些测试文件已在本版发布前删除(见下方「变更范围」),此处保留它们的结论作为改动依据的记录。app.asar 逐条核对宿主契约(座位名/guide 排序/entryKey = definition.id/图标 size 22|26/openTab(kind,{replaceTab:true})/无 patterns 也能按 kind 打开/keepMounted 确实有语义),并用对抗用例查出 4 个真实缺陷,随后全部修掉:
betterEntryActive(入口归属)与 hasBetterSidebar(是否安装,面板据此隐藏自带 Git 工作流)两个概念,官方成功路径同时 teardownBetterTab()。stoppedByHost=true,恢复逻辑被跳过 ⇒ 一个入口都不剩。修法:座位回收不再设该标记,改为走统一的 routeToFallback() 恢复;stoppedByHost 只留给「插件卸载」与「② 已接管」。[500,500,750,1000,1500,2000,3000,5000×7] 因此挤在头 5s 内烧完(注释却写「约 44s」)。后果:官方座位在、服务 10s 后才发布的宿主永远停在兜底按钮上。修法:改为累加调度(retryAt += interval,末级 ≈44s),并让重试在服务真正就绪时重新尝试官方注册。officialGaveUp 曾被当作永久放弃。修法:它只表示「这一轮 40×250ms 探测结束」,重试阶梯看到服务已就绪会把官方卡片换上、拆掉兜底。
另修一处口径错误:installHeaderFallback 原本用 hasBetterSidebar 当门,而该标记在官方入口赢下后仍为 true ⇒ 兜底永远挂不上(与缺陷 2 同类的不可达)。tabs.register 会抛)的 catch 分支直接调 installHeaderFallback(),绕过了「② 已接管入口」的判断 —— 若此刻 ② 的 Tab 正挂着,屏幕上就是「Tab + header 按钮」两个入口。修法:该分支改走 routeToFallback(true)。(第一轮我曾以为 installHeaderFallback 里的 betterEntryActive 门是冗余防御,这个判断是错的:它在「抛异常」这条路径上是唯一的拦截点。)
6. 死状态:officialGaveUp 在改成「非永久」之后所有读取都被移除,只剩写入 ⇒ 是误导下一个读者的死变量(相关注释还在描述已不存在的门)。修法:整个删除,重入只由 officialCleanup/officialInstalled 两个条件表达。betterEntryActive 门)下用例确实失败 —— 说明该不变量被测到。ctx.effect 归属与 carriers() 短路),并复跑了受影响结论。installHeaderFallback 的 betterEntryActive 门与「catch 走路由」这两处,单独改任一处都无法从行为上观察到(路由先拦住)。除了保留双变异用例,另加一条结构断言:全文件里 installHeaderFallback( 只能有 1 处调用、且必须在 routeToFallback 内部 —— 这样将来有人在别处直调(正是第二轮那个重复入口 bug 的形态)会立刻失败。实测:单独把 catch 改回直调,这条断言即失败。*.test.mjs + smoke-test.mjs + verify-en-dict.mjs)。保留 tools/ 下的 i18n 工具链(i18n-apply.mjs、i18n-extract.mjs、client-en.json、host-en.json)—— 它们是生成 lib/client.js 内联英文词典的脚本与数据源,删掉后词典就只能手改。上面的测试结论与变异测试结果作为本轮改动的历史依据保留。lib/client.js(桌面版无需操作,自动热重载;host 端未改)、tools/client-en.json、README.md、README.en.md、package.json,以及删除 tools/ 下 15 个测试文件。版本号 1.24.4 → 1.25.0(新功能用次版本号)。修复:面板打开后,窗口顶栏仍然有一条白缝(v1.24.3 的补带只补了面板那一段):
panelW(跟面板同宽),但白缝的位置并不都在面板宽度内 —— 面板宽度随拖拽变化,补带宽度跟着变就总会留下没补到的段。left: 0; right: 0 横贯整个窗口宽度。左半部分本来就是同色的原生顶栏,重复铺同色无副作用;右半部分把缺的那段补齐。不再依赖面板宽度 —— 拖拽改宽、开合面板都不会在顶栏留下任何缝。顺带删掉了为同步宽度而加的 chromeRef/getChromeEl(不再需要)。left: 0; right: 0 横贯整宽(变异测试验证过:改回 panelW 宽会失败)。lib/client.js(桌面版无需操作,自动热重载)、tools/verify-close-button.test.mjs、README.md、README.en.md、package.json。版本号 1.24.3 → 1.24.4。修复:面板打开后,窗口顶栏那一段出现明显色差(白条):
#f9fafb —— 中间在面板左缘形成一条硬缝。.BynINW_frame(及其 :before)用 --dsw-specific-sidebar-fill(= --dsw-static-neutral-bluish-50,#f9fafb)画出来的,而 frame 在 #root 里。本插件为了让位,用 .LAYOUT_CSS 把 #root 收窄了 panelW:#root { margin-right: var(--scm-push); width: calc(100% - var(--scm-push)) }。顶栏跟着 #root 一起被收窄,于是在面板左缘戛然而止;面板上方那一段没人画,露出页面底色(--dsw-alias-bg-layer-1 = #ffffff)。不是面板盖住了顶栏,而是顶栏被推挤截断了。var(--dsw-specific-sidebar-fill, transparent) 填充,顶栏于是横贯整个窗口宽度、看不出接缝。
pointerEvents: "none" —— 这条带只负责上色,鼠标事件照旧穿透给底下的原生拖拽区域,不改变顶栏原有的拖动行为;SCM_CHROME_TOP(普通浏览器下为 0px → 高度 0、整块不可见,与改造前一致);原生全屏下同样归零,不会多出一条带;makeResizeHandler 相应多接一个 getChromeEl,逐帧同时写面板与色带的 width。tools/verify-close-button.test.mjs,9 项,本次新增 1 项):断言 portal 里必须存在这条带、用的是 --dsw-specific-sidebar-fill、高度等于 SCM_CHROME_TOP、pointerEvents: "none"、带 ref 且 makeResizeHandler 拿到了它(并用变异测试验证:去掉色带的背景后该断言确实失败)。lib/client.js(刷新页面即生效)、tools/verify-close-button.test.mjs、README.md、README.en.md、package.json。版本号 1.24.2 → 1.24.3。修复:面板顶部那条灰带盖住了说明文字(v1.24.1 引入的回归):
✕ 常驻,把标题行做成了 position: sticky 并给了它一层底色(--dsw-alias-bg-layer-1)。sticky 行是浮在内容之上的——它不会把后续内容推开,只是悬停在那里,于是底色正好压住从下面滚上来