本地 Git 仓库的可视化体检与维护中枢,专为 vibe coding 项目设计。
苹果风 · 深色 · 极简 · 零依赖 · 只读优先 · 可接 GitHub。
目录:快速开始 · 它解决什么 · 已实现 · 连接 GitHub · 安全模型 · 分支策略 · 目录结构 · API 一览 · 只读承诺 · 验证 · 桌面版 · 文档 · 已知限制
环境要求:Node ≥ 18 + 系统里已装 git。跑起来零依赖——不需要 npm install。
node server/index.js
# 打开 http://127.0.0.1:4599- 指定端口:
node server/index.js --port 5000 - 自动分配空闲端口:
node server/index.js --port 0 - 服务只绑定
127.0.0.1,不对外暴露
npm install # 只装 Electron 开发依赖;运行时依旧零依赖
npm run desktop # 开发态直接启动
npm run dist # 打包出安装程序 + 免安装版 → dist-desktop/桌面版本质是同一套代码:主进程用 Electron 二进制以纯 Node 方式拉起同一个本地服务,
窗口加载 127.0.0.1:<随机端口>。所以 server/ 与 web/ 一行都没为它改过 —— 详见 docs/03。
安装包是本地构建产物,不在仓库里(
dist-desktop/已 gitignore)。想用桌面版就自己跑一次npm run dist。
| 场景 | 目录 |
|---|---|
| Web 版 / 开发态桌面版 | 项目下的 data/ |
| 打包后的桌面版 | 系统的 userData 目录(绝不写安装目录) |
| 任意场景覆盖 | 环境变量 GM_DATA_DIR=/some/path |
里面存的是仓库列表、操作日志、加密凭据 —— 全部只在本机,已在 .gitignore 里排除。
vibe coding 的项目往往能跑但难维护:临时分支堆积、WIP 提交、stash 越攒越多、不知道 AI 在哪儿动过手、远端仓库和本地对不上。git-manager 把这些问题变成一眼能看懂的画面:
| 页面 | 回答的问题 |
|---|---|
| 概览 | 这个仓库现在健康吗?提交节奏如何? |
| 提交记录 | AI 参与了哪些提交?(多信号推断,每条给依据) |
| 分支与引用 | 哪些分支该清理?能安全地切过去、新建一个、或者删掉吗? |
| 操作日志 | 我(或 AI)对它做过什么?能撤销吗? |
| GitHub | 远端有哪些仓库?能不改文件就读到内容吗?能把本地仓库提交并推上去吗? 能安全地改一行并提交吗? |
- 原生文件夹选择器:点按钮直接弹 Windows 对话框选目录,不用手输路径
- 自动识别:选中后立刻判断「是否已初始化 Git」/「是否位于某个仓库内部」/「有没有子仓库」,并给出对应动作
- 磁盘扫描:选一个盘或任意目录,只读遍历找出其中所有 Git 仓库;深度可调、可随时取消、实时显示进度
- 仓库概览:提交总数、分支数、体积、贡献者、时间跨度
- 健康度评分(0–100)+ 可执行的问题清单
- 12 周提交热力图
- AI 协作痕迹(多信号推断,可解释、纯本地、不读文件内容)
- 半成品提交(WIP)占比
- 分支风险徽章:当前 / 领先 N / 落后 N / 未合并 / stale N 天 / 被工作树占用
- 分支的增删与恢复:界面直接新建分支、删除分支(正检出/唯一分支/被工作树占用会被拦下),删除前会展示影响范围;删掉的尖端存进
refs/gm/deleted/*,可在「可恢复的删除点」里一键找回
这一项最初只是「扫关键词」,结果一个完全由 AI 写的仓库会被判成 0% —— 等于摆设。现已重写为多信号推断,并对每条判定给出可见依据。
它只读 git 元数据(提交信息、作者、时间、改动行数)与文件路径名,不读文件内容、不联网、不上传。据此分三类证据打分:
| 证据类型 | 举例 | 说明 |
|---|---|---|
| 显式证据(单条即定性) | Co-Authored-By: Claude、🤖 Generated with ...、机器人作者、AI 作者名 |
命中即「高置信」 |
| 形态证据 | 正文结构化成文(标题+列表)、长正文、装饰字符、Conventional 前缀、超长标题 | 人类也可能这样写 |
| 过程证据 | 与相邻提交间隔极短、单次改动量远超该仓库常态 | 依赖同仓库基线 |
关键诚实约定:
- 只有形态/过程证据时,等级上限是「疑似」,永远不会自动升级成「高置信」 —— 因为严谨的人类开发者同样会写出规范提交。
- 同一仓库内样本不足 3 条时不做过程推断,避免拿噪声当规律。
- 界面明确标注「推断 · 非事实判定」,并提示:没检出痕迹 ≠ 没有 AI 参与。本仓库实测就有 2 条空正文提交无法追踪,面板会据此提示「实际参与度很可能高于估算」。
- 会额外识别 AI 工具痕迹文件(如
.workbuddy/、CLAUDE.md、.cursorrules、AGENTS.md),作为佐证一并列出。
本仓库自测:7 条提交 → 判定 5 条,占比 71.4%,依据逐条可见(极短间隔×7、Conventional 前缀×7、结构化正文×5 …)。验证脚本:node scripts/check-ai-signals.mjs(52 项)。
- ⌘K 可视化分支切换
- 切换前强制 Preflight 检查
- 工作区脏时自动快照,切换后工作区干净
- 一键撤销,改动完整恢复
- 新建分支 / 删除分支 / 恢复已删除的分支(删除前展示影响范围,删掉的尖端进
refs/gm/deleted/*回收站) - 全部操作进日志
读(只读)
- 账号身份条:头像、登录名、公开仓库数、当前 API 配额(含重置倒计时)
- 头像走本机代理(
/api/gh/avatar):页面 CSP 是img-src 'self',外域头像会被直接拦掉;而且浏览器直连avatars.githubusercontent.com在国内也常年失败。由服务端代取后回传,失败则回退到字形。
- 头像走本机代理(
- 仓库列表:搜索、公/私有过滤、按更新时间/星标/名称排序、分页
- 单仓库详情:默认分支、语言、体积、最后推送时间
- 文件树浏览 + 分支切换:目录逐层展开、文件内容只读预览、二进制文件自动识别并拒渲染
把本地仓库提交并推送(新增)
- 本地仓库视图顶栏与概览页都有「提交并推送」入口 —— 之前根本没有这个能力,用户只能跑命令行
- 向导:状态检查 → 选目标(新建 GitHub 仓库 或 推送到已有仓库)→ 勾选要一并提交的改动 → 提交信息 → 输入仓库名二次确认 → 执行
- 一次完成:
git add→git commit→remote add/set-url origin→git push -u origin <branch> - 新建仓库时不生成初始 README,否则远端先有一个本地没有的提交,推送会因历史分叉被拒
- 提交前自动打一个
refs/gm/prepush/*快照,随时可回退 - 仓库没配 git 提交身份时,用已连接的 GitHub 账号代署名(只写环境变量,不改任何配置文件)
写(需二次确认 + 乐观锁)
- 在线编辑任意文本文件(上限 1 MB),提交前先 dry-run 预演
- 文件列表里每个文件行都有显式「编辑」按钮;右上角有「上传本地文件…」与「新建文件…」
—— 上传时由服务端在用户刚选完文件的瞬间读回内容(刻意不做成「给路径就读」的通用接口) - 提交前必须输入仓库名作为确认语(服务端校验
confirm !== name即拒绝) - sha 乐观锁:预演时锁定远端 sha,提交时若不匹配则拒绝,防止覆盖别人的并发提交
- 支持删除文件(同样走预演 + 确认 + 乐观锁)
- 所有远端写操作落审计日志,可在 GitHub 页查看
克隆
- 粘贴任意 GitHub 地址(HTTPS /
git@/owner/repo短写均可)→ 先解析预览 → 再执行 - 可选目录层级、可选浅克隆深度、可选目标父目录(原生选择器)
- 克隆完成后自动接入本系统并独立复核,重复克隆会被拦下
- 地址白名单:只允许
github.com;..路径穿越、非白名单主机一律拒绝
网络自检
- 一键探测
api.github.com/github.com/codeload.github.com的 DNS 与 HTTPS 连通性 - 以真实 HTTPS 请求为唯一判据(透明代理会假握手,只测 TCP 会误报"正常")
- 检测到代理环境时给出说明,而不是笼统报"离线"
采用 OAuth 2.0 Device Flow(设备码流程),这是 GitHub 官方为 CLI / 桌面应用设计的流程。
关键结论:只需要
client_id,不需要client_secret。
因为 Device Flow 由 GitHub 直接发放短时效设备码,用户在浏览器里输码授权,客户端只用公开的 client_id 轮询换 token。client_id 是公开标识而非密钥,放在本地应用里没有任何问题。
反过来,回调式 OAuth 被明确排除:换取 token 时 GitHub 要求携带 client_secret,而本地应用无法安全保存 secret,任何"硬编码"都等于公开泄露。
你需要做的一次性事情(约 2 分钟):
- 打开 https://github.com/settings/applications/new
Application name随便填(如git-manager-local)Homepage URL填http://127.0.0.1:4599Authorization callback URL也填http://127.0.0.1:4599——⚠️ 这个字段必填,留空会报Callback URL can't be blank;但设备码流程根本不会用到它,随便填个合法地址即可,GitHub 明确允许http://127.0.0.1这类环回地址- 勾选
Enable Device Flow← 这一步必须有,否则授权会直接失败 - 点注册,复制生成的 Client ID,粘贴进本系统 GitHub 页的输入框并保存
- 点「使用 GitHub 授权」→ 页面显示设备码 → 点「打开验证页」→ 在 GitHub 页面输入设备码 → 确认
关于第 4 步:GitHub 官方文档写明,使用设备码流程时回调地址会被忽略("If your app uses device flow ... this field will be ignored");但注册表单仍强制要求填写。这是最常见的一个卡点,不是配置错误。
权限范围(scope)三档可选:
| 档位 | scope | 说明 |
|---|---|---|
| 只读 | read:user public_repo read:org |
只能读公开仓库,不会拿到任何写权限 |
| 读写 | read:user repo read:org |
可读写私有仓库(含提交文件) |
授权后如果你想撤销:https://github.com/settings/applications(页面上会给出这个链接)。
不想注册 OAuth App?直接用 PAT 也行,且细粒度 PAT 的权限比 OAuth 的 repo scope 更精确(可以只给某一个仓库的 Contents 读写权限)。在 GitHub 页展开「使用令牌(PAT)」填入即可。
| 环境 | 存储方式 |
|---|---|
| Windows | DPAPI 加密(ProtectedData,CurrentUser 作用域),密文落盘 |
| 其他平台 / 显式降级 | 明文存储,但页面上会明确标注安全等级,不假装安全 |
- DPAPI 密钥绑定当前 Windows 账户,别的账户或别的机器拿到文件也解不开
- 解密结果进程内缓存(DPAPI 单次开销约 800 ms,不缓存会拖垮每次请求)
- 凭据永远不回传前端:
/api/gh/status只返回hasToken: true/false与权限档位,不返回明文 - 克隆时的 token 不落盘到
.git/config,而是通过GIT_CONFIG_COUNT环境变量注入,进程结束即消失
详见 docs/04-GitHub集成与OAuth授权.md。
分支切换是 L1「可回滚写」;只要工作区有未提交内容,就按 L2 破坏性写流程处理。
选择分支 → Preflight 检查 → 脏则自动快照 → git switch(禁 force)→ 校验并记日志
- 一律使用
git switch,永不git checkout,永不--force - 禁止清单写在引擎层(
server/core/git.js的命令白名单),不写在 UI 层 - 未提交内容一律进快照,永不静默丢弃(所以 UI 上根本没有「放弃改动」这个选项)
- 快照存于
refs/gm/snapshots/*,不污染你的git stash list
新建分支是 L1(只是多一个指针,不覆盖任何文件);删除是 L2。
# 新建:branch <name> [<起点>]
# 删除:branch -d <name>(git 自己会拒绝未合并分支) 或 branch -D <name>(强制)
# 删前把尖端存进 refs/gm/deleted/<name>-<ts> —— 这就是回收站-d与-D的区别交给 git 自己挡,我们不自己实现合并判断去替代它(自己算会与 git 的 upstream 语义不一致而误拦)。所以「有未合并提交」不是阻塞项,只是一条要提醒你的事实- 强制删除必须把分支名原样再输一遍(
confirm === name),而且这个校验发生在任何 git 调用之前 —— 确认错了,仓库一个字节都不会动 - 删除失败(比如
-d被 git 拒绝)时会清掉刚建的回滚点,避免候选列表里留下一个「名字还在的幽灵删除点」 - 恢复分支 =
git branch <name> <refs/gm/deleted/…>,不改变任何文件 - 正检出的分支、仓库里唯一的本地分支、被其它工作树占用的分支,都删不掉(git 本身也不允许,我们提前讲清楚)
状态检查 → 选目标 → 输入仓库名二次确认 → 打快照 refs/gm/prepush/* → add → commit → remote → push → 记审计日志
git push 此前是整条熔断的。现在以窄化白名单方式开放,参数形态完全固定:
| 子命令 | 允许的形态 | 结构性排除 |
|---|---|---|
push |
git push [-u|--set-upstream] origin <单一分支名> |
--force / -f / +refspec / --mirror / --all / --tags / --delete / --prune / 通配 refspec —— 因为 refspec 只允许单个分支名,这些形态根本无法表达 |
remote |
remote(列表)/ remote -v / remote get-url origin / remote add|set-url origin <url> |
remote remove / rename / 操作 origin 以外的远端名 |
fetch |
git fetch origin [branch] |
非 origin、多余参数 |
branch |
branch / branch <只读flag…> / branch <name> [<起点>] / branch -d|-D <name> |
-m(改名)/ -f / --force 拼接 |
update-ref |
update-ref <refs/gm/…> <sha|HEAD> / update-ref -d <refs/gm/…> |
越出 refs/gm/** 一律拒绝 —— update-ref -d refs/heads/main 这种「用底层命令删掉用户分支」的写法根本写不出来 |
- 远端地址必须是
https://github.com/owner/name(.git);..在原始字符串上校验(URL 解析器会先把它归一化掉,事后查pathname查不到),http/ ssh / 非 github 主机一律拒绝 git gc/clean/reset/checkout/rebase/merge仍然整条熔断- 二次确认串校验发生在任何网络调用之前 —— 确认错了不会先去打一通 GitHub API
- 凭据仍走
GIT_CONFIG_COUNT环境注入,不写进.git/config、不进命令行
编辑 → dry-run 预演(拿 sha)→ 展示 diff → 输入仓库名确认 → 提交(校验 sha)→ 记审计日志
- 提交走 GitHub Contents API,不依赖本地 push 权限,也不需要配置 git 凭据
- 任何一步缺省都会被服务端拒绝(
confirm不对 → 400;sha过期 → 冲突报错) - 服务端不信任前端:确认、sha、路径合法性(禁绝对路径 / 禁
..)全部在服务端复核
- 主机白名单只放
github.com,其余一律拒绝 - 在原始字符串上检查
..(WHATWG URL 解析器会先把..归一化掉,事后查pathname查不到) - git 引擎层为
clone单独加了 11 条危险 flag 熔断:--upload-pack/--template/--separate-git-dir/--mirror/--bare/--config/--reference/--recurse-submodules/--remote-submodules/-c等 - 克隆前先做一次
probeRepo快速探测,但只有明确 404 才算"不存在";配额耗尽等"问不出来"的情况一律放行,绝不误拦
本仓库自己就是第一个用户,分支约定如下:
| 分支 / 标签 | 作用 | 状态 |
|---|---|---|
main |
主线,持续演进 | 活跃 |
pure-web |
纯 Web 版归档分支,只保留不依赖 Node 后端的静态实现 | 🔒 已封存,不再开发 |
seal/pure-web-v0.1 |
封存点的标注 tag | 指向封存基线 |
约定:新功能一律先落在 main(Web 版),验证通过后再同步到 pure-web;pure-web 只接受同步,不接受独立开发。
server/ 本地服务(纯 Node,零依赖)
index.js HTTP 入口:本机令牌鉴权 + 静态托管
api.js 路由 → 参数解析(不含业务逻辑)
core/
git.js ★ git 引擎:命令白名单 + 危险 flag 熔断 + 窄化校验(push/remote/fetch/branch/update-ref)
repo.js 仓库中途接入(零侵入探测)
analyzer.js 分析器:概览/分支/提交/热力图/健康度
ai-signals.js ★ AI 协作痕迹检测器(多信号 + 可解释 + 纯本地)
safety.js 安全模块:Preflight / 快照式切换 / 分支增删恢复 / 撤销 / 日志
dialog.js 原生文件夹/文件选择器(PowerShell 桥)
scanner.js 只读磁盘扫描(readdir + lstat)
secrets.js ★ 凭据仓库:DPAPI 加密落盘 + 进程内缓存
auth.js ★ OAuth Device Flow + PAT + 登出
github.js ★ REST 客户端 + 写操作验证层(预演/乐观锁/提交/删除)+ 建仓 + 头像代理
publish.js ★ 本地仓库「提交并推送」
clone.js ★ 克隆 + URL 白名单 + 凭据环境注入
netcheck.js ★ 网络自检(真实 HTTPS 为唯一判据)
store.js 本地存储(接口可换 SQLite)
util.js 工具函数 + 版本号单一来源
web/ 前端(纯静态,无构建步骤)
index.html
css/app.css 设计系统(苹果风深色)
js/api.js ★ 唯一传输边界 —— 桌面端只改了这一个文件里的两个方法
js/util.js 格式化与徽章
js/views.js 视图渲染(概览/提交/分支/日志/GitHub)
js/palette.js ⌘K 切换面板 + 安全切换 + 分支增删 + GitHub 各对话框
js/main.js 状态与路由
desktop/ 桌面端壳(Electron)
main.js 主进程:sidecar 启动 / 窗口 / 外链接管 / 原生对话框 / 自检
preload.cjs 唯一的桥(contextBridge)—— 不暴露 fs、不暴露任意 IPC
build/icon.ico 应用图标(由 scripts/make-icon.mjs 生成)
scripts/ 验证脚本(node scripts/check-*.mjs,全部自包含)
check-branch.mjs 分支增删 + 盘根扫描边界 + 接线契约
check-desktop.mjs 桌面端冒烟测试(真的把应用跑起来)
make-icon.mjs 程序化生成图标(无外部依赖)
docs/ 设计文档(见文末「文档」)
下面这些是本机专属的,已在 .gitignore 里排除,克隆下来不会有:
| 路径 | 是什么 | 为什么不入库 |
|---|---|---|
data/ |
仓库列表、操作日志、加密凭据 | 里面是你自己的仓库绝对路径 |
.workbuddy/ |
开发过程中的工作记忆 | 记录了本机路径与本机专属的上下文 |
node_modules/ |
依赖 | 跑 npm install 即可 |
dist-desktop/、dist-*/ |
打包产物 | 跑 npm run dist 即可 |
*.log、.DS_Store、.vscode/ |
编辑器/系统垃圾 | —— |
换句话说:仓库里只有项目本身的代码与文档,跑起来后产生的数据一律落在本机、不进仓库。
所有 /api/* 需要请求头 x-gm-token(页面通过 /gm-config.js 自动获取)。
服务仅绑定 127.0.0.1,且不发送 CORS 头,杜绝跨站读取。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/health |
健康检查 |
| GET | /api/repos |
已接入仓库列表 |
| POST | /api/repos/open |
接入仓库 { path } |
| DELETE | /api/repos/:id |
从列表移除(不动磁盘) |
| GET | /api/repos/:id/overview |
总览 |
| GET | /api/repos/:id/branches |
分支列表(含风险) |
| GET | /api/repos/:id/branches/removal?name= |
删除前评估:阻塞项、未合并提交数、尖端、远端同名分支 |
| GET | /api/repos/:id/branches/deleted |
可恢复的删除点(refs/gm/deleted/*) |
| POST | /api/repos/:id/branches |
新建分支 { name, from? } |
| POST | /api/repos/:id/branches/delete |
删除分支 { name, force?, confirm? }(force=true 时 confirm 必须是分支名本身) |
| POST | /api/repos/:id/branches/restore |
恢复已删除的分支 { name, ref } |
| GET | /api/repos/:id/commits?limit= |
提交记录 |
| GET | /api/repos/:id/activity?weeks= |
热力图数据 |
| GET | /api/repos/:id/health |
健康度报告 |
| GET | /api/repos/:id/log |
操作日志 |
| POST | /api/repos/:id/preflight |
切换前检查 { target } |
| POST | /api/repos/:id/switch |
安全切换 { target, strategy, message } |
| POST | /api/repos/:id/undo |
撤销上一次切换 |
| GET | /api/repos/:id/publish-status |
发布状态:分支/改动清单/origin/领先落后/提交身份 |
| POST | /api/repos/:id/publish |
提交并推送 { mode, repoName/owner+name, message, includeChanges, branch, confirm } |
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/fs/drives |
可用驱动器列表 + 是否支持原生对话框 |
| POST | /api/fs/pick |
弹出原生文件夹选择器,返回所选路径 |
| POST | /api/fs/pick-file |
弹出原生文件选择器;withContent 时由服务端在选中瞬间读回内容(不做成通用读文件接口) |
| POST | /api/fs/probe |
识别目录 { path }:是否仓库 / 是否在仓库内 / 有无子仓库 |
| POST | /api/scan |
启动只读扫描 { root, maxDepth } → { job } |
| GET | /api/scan/:id |
轮询扫描进度与结果 |
| POST | /api/scan/:id/cancel |
取消扫描 |
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/gh/status |
授权状态(不含明文凭据) |
| POST | /api/gh/client-id |
保存 OAuth App 的 client_id |
| DELETE | /api/gh/client-id |
清除 client_id(不动已授权 token) |
| POST | /api/gh/device/start |
发起设备码授权 { scopeMode } |
| GET | /api/gh/device/:id |
轮询授权状态 |
| POST | /api/gh/device/:id/cancel |
取消授权 |
| POST | /api/gh/pat |
备选通道:存 PAT { token } |
| POST | /api/gh/logout |
登出并清除 token |
| GET | /api/gh/rate |
当前 API 配额 |
| GET | /api/gh/repos |
仓库列表 ?sort=&visibility=&q=&page= |
| GET | /api/gh/repo |
单仓库详情 ?owner=&name= |
| GET | /api/gh/contents |
文件/目录内容 ?owner=&name=&path=&ref= |
| POST | /api/gh/preview |
只读预演,返回远端当前 sha(乐观锁) |
| POST | /api/gh/commit |
提交文件(需 confirm = 仓库名 + sha 匹配) |
| POST | /api/gh/delete |
删除文件(同样需确认 + 乐观锁) |
| GET | /api/gh/log |
远端写操作审计日志 |
| POST | /api/gh/parse |
解析仓库地址(预览将要克隆什么) |
| POST | /api/gh/clone |
克隆 { url, parentDir, dirName, depth } |
| POST | /api/gh/create-repo |
在 GitHub 新建空仓库 { name, private, description } |
| GET | /api/gh/avatar |
头像代理 ?login= 或 ?u=(主机白名单,防 SSRF);因 <img> 无法带请求头,仅此路由额外接受 ?t=<token> |
| GET | /api/net/probe |
探测 GitHub 各域名的 DNS + HTTPS 连通性 |
:id 用仓库 id;若要用绝对路径,需 encodeURIComponent 后再放进 URL。
「选择文件夹」和「磁盘扫描」是本系统里唯一会大规模接触你磁盘的操作,因此被刻意设计成结构上不可能造成破坏:
| 承诺 | 落地方式 |
|---|---|
| 不写任何文件 | 扫描器只调用 readdir + lstat 两个系统调用,代码里没有任何写操作;缓存只写进本项目的 data/ 目录 |
| 不读文件内容 | 判断是否为仓库只看 .git 是否存在,不解析其内容 |
| 不跟随符号链接 | 跳过所有 symlink / 目录联接,杜绝死循环和「跑到盘外」 |
| 不越界 | 每次入队都做路径包含性校验,结果严格落在所选根目录之内 |
| 不会失控 | 三重硬上限:深度 ≤ 10、目录数 ≤ 40 万、时间 ≤ 5 分钟;可随时取消 |
| 不碰系统区域 | 默认跳过 Windows、Program Files、AppData、$RECYCLE.BIN 等,以及 node_modules 等重量级缓存目录 |
| 可验证 | 已实测:扫描前后被扫目录的文件大小、修改时间、目录结构完全一致,未新增任何文件 |
会改动仓库的路径只有四条:本地分支切换、本地分支的新建/删除/恢复、本地提交并推送、GitHub 远端提交。
四者都要经过「预演/检查 → 快照/回收点/乐观锁 → 二次确认 → 审计日志」这一整套闸门。 前两条必须经过 Preflight + 快照(或refs/gm/deleted回收点)+ 日志;后两条必须经过预演 + 确认 + 乐观锁 + 审计日志。
node scripts/check-oauth.mjs # 设备码流程状态机(含失败分支)
node scripts/check-gh-poll.mjs # 授权面板的轮询循环(DOM 桩 + 手动定时器驱动)
node scripts/check-github-guard.mjs # 写操作守卫 + 路径穿越 + clone 危险 flag
node scripts/check-branch.mjs # 分支增删:窄化白名单 + 盘根路径边界 + 真实建/删/恢复 + HTTP 路由 + 前端契约
node scripts/check-publish.mjs # 提交并推送:push/remote 窄化熔断 + 守卫 + 真实 push 端到端
node scripts/check-clone-e2e.mjs # 真实克隆端到端(需网络)
node scripts/check-ai-signals.mjs # AI 协作痕迹检测器(显式/形态/过程证据 + 诚实边界)
node scripts/check-ui.mjs # 前端模块加载与渲染函数
node scripts/check-dpapi.mjs # 凭据仓库端到端:DPAPI 加密 / 跨进程冷启动解密 / 空值边界
node scripts/check-net.mjs # 网络自检判据
node scripts/check-dialog.mjs # 原生对话框桥
node scripts/check-desktop.mjs # 桌面端:真的把 Electron 应用跑起来自检(14 项)
node scripts/check-desktop.mjs --app dist-desktop/win-unpacked # 自检**打包产物**
check-clone-e2e.mjs需要可访问github.com,无网络时设GM_TEST_NET=1才执行真实克隆。
check-scan.mjs是探索性脚本而非自包含测试:需先自己起一个node server/index.js --port 4630,并把夹具目录与真实仓库路径作为参数传入。
为什么要有 check-desktop.mjs:桌面端的失败模式几乎都看不见 —— sidecar 起来了但窗口连不上、
preload 没注入、令牌没传对、渲染层 JS 报错导致白屏。静态检查一个都发现不了,必须真的把它跑起来。
它会启动应用、逐项断言、打印结果后退出,并且把数据指向临时目录,不碰真实的 data/store.json。
已经打包成 Windows 应用,Web 版与桌面版共用同一套后端与前端(server/ 与 web/ 一行都没改)。
GitManager.exe(Electron 主进程 · desktop/main.js)
│
├─ 用 ELECTRON_RUN_AS_NODE 把 server/index.js 拉成 sidecar 子进程
│ └─ 端口传 --port 0,由系统分配;真实端口与令牌从 stdout 的 GM_READY 行读回
│
└─ 窗口加载 http://127.0.0.1:<随机端口>/
└─ 页面照常走 HTTP + 令牌,前端零改动
几个刻意的选择:
| 决定 | 原因 |
|---|---|
sidecar 用 Electron 自带的 Node 跑(ELECTRON_RUN_AS_NODE=1) |
Electron 二进制本身就是 Node 运行时,不必再打一份进去,省约 40MB |
窗口加载 127.0.0.1 而不是 file:// |
CSP、相对路径、fetch 全部照常工作,web/** 零改动;file:// 下还得逐条放宽 CSP |
| 保留令牌机制 | 挡不住同源的注入脚本,但能挡住其它本机进程随意驱动 git 写操作 —— 这不因换了壳而失效 |
| 端口用 0 | 用户可能同时开着 Web 版(4599),写死端口会直接起不来 |
asar: false |
sidecar 要由 Node 从磁盘执行脚本,asar 打包后路径不可执行。代价是代码可读,对一个本地工具反而是透明性 |
| 数据放用户目录 | 安装目录可能只读,升级覆盖时会连用户数据一起抹掉 |
| 场景 | 目录 |
|---|---|
| 安装版 / 免安装版 | %APPDATA%\GitManager\(仓库列表、操作日志、加密凭据) |
开发态 npm run desktop |
项目里的 data/,与 Web 版共用,所以两边看到同一批仓库 |
| 自己指定 | 设环境变量 GM_DATA_DIR 即可覆盖(自检脚本就用它做隔离) |
菜单里「文件 → 打开数据目录」可以直接跳过去。
只有一处代码不同:web/js/api.js 里的 pickFolder / pickFile 在桌面端改走 Electron 原生对话框,
其余全部请求照旧走 HTTP。原生对话框返回结构与服务端的 PowerShell 版本完全一致
({ok, cancelled, path}),所以上层调用方不需要知道自己在哪个宿主里。
另外桌面端会自动接管外链(OAuth 设备码地址、仓库链接等),交给系统浏览器打开。
| 文档 | 内容 |
|---|---|
docs/00-产品构想与技术方案.md |
这个工具为什么长这样:问题定义、信息架构、技术选型 |
docs/01-前端视觉提示词-Google图像生成.md |
苹果风设计系统的视觉规格与生成提示词 |
docs/02-分支切换与安全模型.md |
★ 三级安全模型、Preflight 清单、窄化白名单、分支增删、只读扫描保证 |
docs/03-迁移契约与桌面端.md |
三层边界与迁移契约:Web → 桌面为什么只改一个文件 |
docs/04-GitHub集成与OAuth授权.md |
OAuth 设备码流程、凭据存储、写操作的三步验证、头像与 CSP |
诚实列一下现在做不了 / 做得不完美的地方:
| 限制 | 说明 |
|---|---|
| 只写本地,不碰远端历史 | 不提供 rebase / merge / reset / cherry-pick / force push。这些一律在引擎层熔断,宁可不做 |
| 不做 GitHub 侧的分支删除 / PR | 远端文件提交与建仓已支持;远端分支管理、PR、Issue 尚未接入 |
| AI 痕迹是推断,不是判定 | 启发式打分,没检出 ≠ 没有 AI 参与。只有显式证据(如 Co-Authored-By)才给「高置信」 |
| 扫描会跳过系统区域 | Windows、Program Files、AppData、node_modules 等默认不进入,所以那里面的仓库扫不到 |
| 快照不会自动清理 | refs/gm/snapshots/*、refs/gm/deleted/* 会一直留着。它们是纯本地 ref,想清理用 git update-ref -d <ref> |
| 只测过 Windows | 原生对话框走 PowerShell + WinForms;扫描与 DPAPI 都按 Windows 写的。macOS / Linux 未验证 |
| 前端 CSS 未做自动化验证 | 视图层是纯函数、有渲染冒烟测试,但布局只能靠人眼 |
本项目为个人项目,未附许可协议。如需引用或复用请先开 Issue 说明用途。