Bundle
@ptfm/dsh-github
GitHub 账号接入:设置里登录 GitHub(PAT / OAuth Device Flow / 纯 token CLI),凭据安全落盘,agent 通过 github-* 工具以该账号真实调用 GitHub API
- Source
- w32394045-dotcom
- License
- MIT
- Updated
- Updated 7 days ago
Readme
# @ptfm/dsh-github
把 GitHub 接进 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness):**设置里登录一次,agent 就能以你的账号真实干活**——读仓库、翻 issue 与 PR、查 Actions、发评论、管分支与发布,以及调用 GitHub 的云计算服务。
和其他「GitHub MCP」类方案最大的区别是:**token 不经过模型上下文**。凭据落在本机文件里,插件在宿主内直接调用 GitHub API,模型只看到结构化结果。agent 通过 shell 跑 `git` / `gh` 时也能拿到登录态(`DSH_GITHUB_TOKEN`),行为等同你自己在终端里操作。
```
┌─────────────┐ 设置页/CLI 登录一次 ┌──────────────────────────────┐
│ DSH Web UI │ ─────────────────────▶ │ 插件宿主半边 │
│ 设置→GitHub│ │ · 凭据:$DSH_HOME/github.json │
└─────────────┘ ◀───────────────────── │ · 12 个 github-* 工具 │
│ · /api/github 精确路由 │
agent ──── github-* 工具 ───────▶ │ · DSH_GITHUB_TOKEN 注入 │
└───────────┬──────────────────┘
▼
api.github.com(你的权限)
```
## 快速开始
**Windows(PowerShell):**
```powershell
git clone https://github.com/w32394045-dotcom/dsh-github.git
cd dsh-github
powershell -ExecutionPolicy Bypass -File scripts\install.ps1
```
**macOS / Linux:**
```bash
git clone https://github.com/w32394045-dotcom/dsh-github.git
cd dsh-github
bash scripts/install.sh
```
安装器会(幂等地)把插件放进 `$DSH_HOME/plugins/dsh-github`,在目标 profile 里登记 `link:` 依赖与 bundle,再用 DSH 自带的 pnpm 装好链接。装完打开 DSH → **设置 → GitHub** → 粘贴 token 即可;若插件没立刻出现,重启一次 DSH。
> 手动安装也可以:在 profile 的 `package.json` 里把 `"@ptfm/dsh-github": "link:<插件目录>"` 加进 `dependencies` 和 `dsh.profile.bundles`,然后 `pnpm install`。
## 三条登录路径
1. **Personal Access Token** —— 设置页粘贴,先拿 `/user` 真实校验,通过才落盘。
2. **OAuth Device Flow** —— 不需要 `client_secret`。你在 GitHub 建一个 OAuth App(勾选 *Enable Device Flow*),把 `client_id` 填进设置页;点「开始授权」后显示 8 位码,浏览器确认即完成。插件只保存 `client_id`,它本来就不是秘密。
3. **纯 token CLI** —— 不依赖 DSH 运行,读写同一个凭据文件:
```bash
node lib/cli.mjs status # 状态 + 实时校验,永不打印明文 token
node lib/cli.mjs login --token <token> # 也支持 --stdin / 交互式隐藏输入
node lib/cli.mjs login --device --client-id <id>
node lib/cli.mjs whoami --json # 机器可读
node lib/cli.mjs token # 唯一输出明文的命令(stdout)
node lib/cli.mjs logout
```
## 12 个 agent 工具
| 工具 | 用途 |
| --- | --- |
| `github-search` | 搜索 issue / PR / 仓库 / 代码 / 提交(**最常用**,见下) |
| `github-status` | 登录状态、账号、token 真实 scope、速率限制 |
| `github-account` | 用户资料 |
| `github-repo` | 仓库信息、目录树;`action=local` 读本地 `.git/config` 反推仓库(不消耗 API) |
| `github-issues` | 列 / 读 / 建 / 评论 / 改状态 |
| `github-pulls` | 列 / 读 / 建 / 评论 / review / merge |
| `github-branches` | 分支列表、HEAD、建/删、**compare**(建 PR 前确认 base) |
| `github-actions` | 工作流运行、job 与步骤、日志尾部、手动触发 |
| `github-releases` | Release 列表 / 详情 / 创建 / 修改 / 删除 / 资产 |
| `github-notifications` | 通知中心:列未读、标记已读、归档、订阅 |
| `github-cloud` | **云计算服务**:Codespaces、自托管运行器、Actions 配额、Pages、Packages、Models,以及成本报告与闲置回收 |
| `github-api` | 直通任意 REST 端点(其它工具覆盖不到时的逃生口) |
`repo` 参数支持三级推断:**显式传入 → 当前工作目录所属的 git 仓库 → 设置里的默认仓库**。所以在某个仓库目录里说「这个仓库的 open issue」就够了。
### `github-search` 为什么单独做厚
搜索是这套工具里调用最频繁的一个,也是 GitHub 限流最紧的一个(**认证用户 30 次/分钟**,不是普通的 5000/小时):
- **分页与总数**:`page` / `limit`,输出明确区分「还有下一页」和「以上是全部 N 条」;
- **查询增强**:`@me` 展开成真实登录名;`days: 7` → `updated:>=日期`;查询没写 `repo:`/`user:`/`org:` 时**自动限定到当前目录所属仓库**;
- **90 秒短时缓存**:同一查询不重复花额度;
- **错误不静默**:422 回显 GitHub 原话 + 实际发出的查询 + 语法排查建议;限流会点明是搜索接口的限制并给替代方案;`incomplete_results` 会被标注;
- **输出为「读」设计**:命中总数、`issue`/`PR` 前缀、`open`/`closed`/`merged`、标签、评论数、时间、URL,以及下一步该调哪个工具。
## 云端成本:忘关 vs 误关
Codespaces 按运行时长计费,「忘了关」和「被误关」是同一枚硬币的两面。规则集中在一个模块里(`lib/lifecycle.mjs`),设置页与工具共用:
**四道防线,从最可靠到最兜底**
1. **创建时就把空闲自动停止设短**(默认 15 分钟)——由 GitHub 自己停机,**不依赖任何人记得关**,这是唯一「忘了也没事」的一道;
2. **计费动作前自动预检**:`create` 之前列出现有环境、累计花费与闲置告警,提醒「别让两台一起烧」;
3. **闲置告警与批量回收**:`action=reap`(支持 `dryRun=true` 预演);
4. **绝不误关**(优先级高于省钱):`last_used_at` 在安全窗口内、时间戳不可信、或持有 DSH 活跃租约时一律不动;`manual` 模式完全不动手。另外 `stop`/`delete` 发现该环境最近仍有活动会**拒绝执行**,必须显式 `force=true`。
**三种模式**(设置 → GitHub → 云端成本,点一下即生效):
| 模式 | 提醒 | 可回收 | 安全窗口 |
| --- | --- | --- | --- |
| 平衡(默认) | 闲置 30 分钟 | 2 小时 | 15 分钟 |
| 极致省钱 | 闲置 15 分钟 | 45 分钟 | 10 分钟 |
| 手动 | 闲置 2 小时 | 不自动回收 | 30 分钟 |
`github-cloud action=cost` 给出成本报告与**省钱顺序**(停机 ≠ 免费,存储照收 → 空闲超时设短 → 能本地跑就别上云 → 长任务用 Actions → 批量 `reap`)。
## 权限与安全
- **权限完全跟随 token 自身**:插件不做二次裁剪,也不会给 agent 比 token 更大的能力。
- **token 不经过模型**:只存 `$DSH_HOME/github.json`(0600、原子写、读时剥 BOM)。`statusOf()` 是唯一给界面/日志用的出口,只返回掩码预览;`readStore()` 会返回明文,插件内部绝不对浏览器序列化它。
- **环境变量优先**:`DSH_GITHUB_TOKEN` / `GITHUB_TOKEN` 一旦设置就覆盖本地存储,方便临时切身份。
- **scope 缺失时给路**:Codespaces / Packages / Models 需要各自的 scope。工具在 403/404 时会明确告诉你缺哪个、去哪儿补,而不是丢一句失败。
- **计费动作需确认**:`create` Codespace、`dispatch` 工作流、推镜像属于会产生费用的操作,工具描述里要求先征求用户同意。
## 结构
```
lib/
main.mjs 入口薄壳(见「开发须知」)
index.js 宿主半边:/api/github 路由、工具注册、DSH_GITHUB_TOKEN 注入
client.js 浏览器半边:设置 → GitHub 页
store.mjs 凭据与偏好存储(原子写、BOM 容错、脱敏)
lifecycle.mjs 云端成本策略:闲置判定、安全边界、租约、花费估算
rest.mjs GitHub REST 调用层(错误归一化、scope/速率回传)
device-flow.mjs OAuth 设备流
tools.mjs 12 个工具的实现(agent 工具与 HTTP 共用同一份)
cli.mjs 命令行
scripts/
install.ps1 / install.sh 一键安装
upload-via-api.mjs 备用上传:走 Git Data API,绕过被墙/被重置的 git 传输域
test/
lifecycle-unit.mjs 成本策略与安全边界 80 项
tools-unit.mjs 工具表与 .git/config 解析 77 项
cloud-unit.mjs 云计算端点与权限诊断 70 项
host-load.mjs 宿主挂载与脱敏 50 项
smoke.mjs 存储/设备流/REST/CLI 47 项
search-unit.mjs 搜索行为(stub fetch) 40 项
search-live.mjs 真实联网搜索冒烟 12 项
cloud-live.mjs 逐个云 action 的真实展示
acceptance.mjs 端到端验收条目(默认关闭)
```
**零运行时依赖**:不 import 任何 `@deepseek-ai/*` 或第三方包,只用 Node 内置模块与 DSH 的服务接口,因此不受插件 peer 版本检查影响。
跑测试(除两个 `*-live` 外全部离线,不需要 token):
```bash
node test/lifecycle-unit.mjs && node test/tools-unit.mjs && node test/cloud-unit.mjs
node test/host-load.mjs && node test/smoke.mjs && node test/search-unit.mjs
node test/search-live.mjs # 需要已登录,会真实调用 GitHub
```
## 开发须知(踩过的坑)
这几条是 DSH/Cordis 的硬约束,改这个插件前值得先看:
1. **`connection.fetch.register` 的路径必须是精确路径**:以 `/api/` 开头、每段只允许 `[A-Za-z0-9_$.-]`、**不能有尾斜杠**(`/api/github/tool/` 会让 `register` 抛错,进而整块插件激活失败),也不支持通配符;方法只支持 `GET/HEAD/POST`。所以「退出登录」也是 `action=logout` 的 POST。
2. **`tools.register` 的 `parameters` 是原样透传给模型的原生 JSON Schema**,`required` 是顶层数组;逐参数 `required: true` 属于 `defineTool` DSL,而 profile 插件目录里解析不到 `@deepseek-ai/*`,用不了 `defineTool`。
3. **取服务一律 `ctx.get(name)`**,不要写 `ctx.tools` 这种属性读取:未声明 `inject` 的属性读取会抛错,而且那条报错的属性名可能是**上一次**非法访问的残留——真凶与文案里的名字未必一致,排障以栈里的行号为准。
4. **Codespaces 列表端点是 `/user/codespaces`**,不是 `/users/{login}/codespaces`(后者 404,看着像权限问题其实是路径错);`default_workflow_permissions` 在 `/actions/permissions/workflow`,不在 `/actions/permissions`。
另外:`main.mjs` 之所以存在,是因为 Cordis 缓存的是**模块 specifier**——一旦某个版本的 `apply` 抛过错,改 `index.js` 内容不会被重新求值,disable/enable 只会回放旧报错。换入口文件名就换了一个 specifier。**宿主半边一旦处于 `fiberPhase: failed`,运行时无法恢复,必须重启 DSH。**
## 卸载
从目标 profile 的 `package.json` 里删掉 `dependencies` 与 `dsh.profile.bundles` 中的 `@ptfm/dsh-github`,重跑 `pnpm install`。可选再删 `$DSH_HOME/plugins/dsh-github` 与 `$DSH_HOME/github.json`。
## License
[MIT](LICENSE)
Install
dsh plugin --profile web add github:w32394045-dotcom/dsh-github
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install ptfm-dsh-github from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.