中文EN

dsh-tool-mining

toolsnpmMIT

把 GitHub 仓库的原始计数变成一句能站住的判断 —— 贡献者集中度与 bus factor(附单作者文件)、合并时长中位数与均值并列(让离群值可见而不是被隐藏)、以及排掉 PR 与 draft 之后的 issue backlog。只用公开 REST API、不装 SDK,分页被截断时明确说明而非伪装完整。

下载量 / Downloads
452

安装

dsh plugin --profile web add dsh-tool-mining

沙箱测试

这个插件还没测过。提交测试 →

README

dsh-tool-mining

把一堆仓库数字变成一句能站住的判断 —— 先看谁在写代码、多久合一次,
再看这份工作有多依赖一个人,最后看 backlog 是不是在烂。

给 DeepSeek Harness 用的自建工具插件:读公开的 GitHub REST API,

不装任何 SDK,只用 fetch。

Compatibility: built and tested against dsh 0.2.0-rc.2 (preview).
The apply(ctx) plugin spec is stable; verify against your own dsh version if newer.

一行安装

dsh plugin --profile desktop add github:yuehancn/dsh-tool-mining

支持的 profile:desktop(桌面版)/ web(Web 版)。装完重启 dsh 即可用。


为什么需要它

GitHub 自己就把这些数字摆在那儿了 —— 贡献者页有排名,PR 列表有日期。

问题不是拿不到数字,而是这些数字很容易被读错:

  • 均值是有毒的。 一个 40 天没人理的 PR,能把一批 1~3 天合并的 PR 的均值

从 3 天拖到 10.8 天。而「这个仓库合 PR 很快」这句话,恰恰是由均值说出来的。

中位数顶着这一个离群值,说 3 天 —— 这才是真相。

  • 「贡献者 300 人」可能是废话。 里面可能有一半是 [bot],剩下的人里

一个人写了 60%。300 这个数看着很热闹,但对「这人要是休假了怎么办」这个问题,

答案是 1。

  • Issues API 会把 PR 混进来。 你用 /issues 数 backlog,数到的其实是

「issue + PR」。而 draft PR 只存在于 /pulls 端点 —— 想排掉它,得再问一次。

少问这一次,backlog 就会虚高。

  • 分页会悄悄截断。 不跟 Link: rel="next",你拿到的是第一页,

但你以为拿到了全部。这是最坏的一种错:答案看起来很正常,就是只有一半。

这个插件把「看清楚」和「下判断」分开:

mining_status(owner, repo)        →  API 通不通 / 还剩多少配额
mining_contributors(owner, repo)  →  70% 集中在一人 / bus factor 1
mining_velocity(owner, repo)      →  中位 3 天,均值被拖到 10.8 天(两个都报)
mining_busfactor(owner, repo)     →  4 个文件只有一个人碰过
mining_issues(owner, repo)        →  一半 issue 没人回,中位年龄 10 天

关键设计:mining_velocity 同时报中位数和均值。只报一个数就是替调用方

做了判断,而这两个数的分歧本身就是最有用的信息。


五个工具

mining_status

报 API 能不能通、token 配没配、这一天还剩多少请求配额、重置时间,

以及可用操作和分页上限。多页分析前先问一句 —— 匿名会话一小时只有 60 次,

读两次大仓库就见底了。

mining_contributors(owner, repo, limit?)

按提交数排名,并说明工作集中到什么程度:第一名占多少、前五名占多少、

需要几个人才能覆盖一半提交(halfOfWorkNeeds)。

参数 · 说明

limit · 单独列出多少人,默认全列

机器人和人是分开的。 份额算在人类提交里,不算 bot 的提交 ——

否则一个每天提 5 次的 dependabot 会把一个人的真实占比冲淡。

所以你会看到 topContributorShare 比「70/100」略高(是 70/98):

这是故意的,bot 不该稀释维护信号。

mining_velocity(owner, repo, weeks?, includeBots?)

从 PR 被开到被合,量出中位数和 p90,再加每周合并数的时间线 ——

让「变慢了」表现为趋势,而不是一个孤零零的数字。

返回值 · 说明

medianMergeDays · 主指标,扛得住离群值

meanMergeDays · 一起报,专门为了让两者的差距可见

p90MergeDays · 线性插值,不是取最近的样本

trend · 按周分桶,新的一周在最后

p90 用插值算:5 个样本 [1,2,3,8,40] 的 p90 位置是 0.9 × 4 = 3.6,

落在第 4 个和第 5 个之间 → 8 + (40−8) × 0.6 = 27.2。

如果按「取最近的样本」实现,这里会报 40 —— 那样 p90 就退化成最大值了。

mining_busfactor(owner, repo, commits?, sampleFiles?)

用一份 commit 列表同时回答两个问题:每个作者的提交数,

以及哪些文件只被一个人碰过。

比逐个文件问「谁改的」便宜得多 —— 后者是一个文件一次请求。

commit 列表本身带作者和文件列表,一遍扫完。

参数 · 说明

commits · 分析多少个近期提交,默认 300,上限 maxPages × perPage

sampleFiles · 最多列几个单作者文件,默认 10

判据是严格大于一半:一个人占正好 50% 不算够 —— 需要第二个作者。

risk 按 bus factor 给出 high / moderate / low,话术里写明判据

(「一个账号握着超过一半的近期工作」),而不是只给一个词。

文件列表可能缺席。 commit 列表端点经常不回 files 数组。

这时候不是「没有单作者文件」,而是「这次查不出来」—— 两种结论天差地别,

所以 notes 里会明说,而不是让空数组自己暗示。

mining_issues(owner, repo, weeks?)

数 backlog:开了多少、中间那个等了多久、多少条一个回复都没有、

标签分布、以及每周新增的时间线。

做了三件过滤,每一件都写进 notes:

1. Issues API 会把 PR 混进来 → 按有没有 pull_request 字段排掉。

2. draft PR 只存在于 /pulls → 额外查一次开的 PR,把 draft 号收集起来排掉。

3. 仓库的 open_issues_count 是 issue + PR 合在一起的 → 明说这个数是

过滤后的 issue 数,和 GitHub 页面上显示的不是同一个数。


配置

# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: tool-mining
  config:
    token: ghp_xxxxxxxxxxxxxxxxxxxx
    maxPages: 5
    perPage: 100

字段 · 默认 · 说明

token · "" · GitHub PAT。强烈建议配:匿名一小时 60 次,多页分析几秒就耗尽。公开仓库不配也能用

apiBase · https://api.github.com · API 基地址,可指向企业版

maxPages · 5 · 每份列表最多翻几页

perPage · 100 · 每页多少条

timeoutMs · 600000 · 单次调用预算(10 分钟)

status/contributors/velocity/busfactor/issues · true · 按需关掉某个工具


安全说明

  • token 只进请求头,不进 URL、不进日志、不进错误信息。 401 的报错

只说「GitHub 拒绝了 token」,不会把 token 回显出来。

  • 只用 fetch,不落任何文件,不写磁盘。
  • owner / repo 一律 encodeURIComponent —— a/b c 会变成 a%2Fb%20c,

不会把路径拼歪(有专门的断言盯这一点)。

  • 错误按类型分辨,不糊成一句「请求失败」:401(token 无效)、

403 且配额为 0(配额耗尽,附重置时间)、404(仓库不存在,

或 token 看不见私有仓库)、非 JSON 响应(比如被网关挡了)。

  • 分页有上限,截断了就明说(truncated: true + 一句 note),

不给一个「看起来完整」的半份答案。


实现说明:四个值得说的地方

1. 分页的总数要读「广告里的」per_page,不是「我要的」。

Link 头的 rel="last" 只给页号,总数得自己乘。最初写的是

最后一页 × 我请求的 perPage —— 错。GitHub 会在头里把它实际用的

per_page 写出来,两个数不一致时(比如我请求 100、它给 2),

总数就会算成 300 而真相是 6。正确做法是从 Link 头里把 per_page 抠出来

再乘。这个 bug 是逻辑测试里断言「多页取满」时抓出来的。

2. 中位数和均值要一起报,因为分歧本身是信息。

测试数据 [1,2,3,8,40]:中位 3 天,均值 10.8 天。断言里专门写了一条

「一个巨大的离群值几乎不动中位数」,和一条「一个离群值把均值拖到 202」

(另一组数据),把这两种行为钉在测试里。哪天有人把中位数换成均值,

测试会红。

3. 严格大于一半。

estimateBusFactor 的循环条件是 covered / total > 0.5。

正好 50% 不算够 —— 一个人占一半,意味着另一半在别人手里,

这个人休假仓库不会停。测试里有一条断言就叫

「exactly 50% is not enough」,专门防这个边界被改宽。

4. 份额的分母排除 bot。

mining_contributors 的 topContributorShare 分母是人类提交总数,

不是全部提交。所以同一个 70/100 的仓库,配 2 次 bot 提交后报的是 0.7143

而不是 0.70。这是刻意选择:bot 提交量不反映「知识集中度」,

把它算进分母等于让机器人稀释风险信号。

测试方式:HTTP 全部走一个 githubRequest 助手,fetchImpl 可注入。

集成与 e2e 套件装上脚本化的假 fetch,请求 URL 全部被记录、被断言 ——

整个测试套件一次网络都不碰。而且每个 fixture 装完假 fetch 会立刻调

assertNoNetwork:如果请求没打到假实现(说明打到了真网络),直接抛错。

这条守卫不是装饰 —— 它抓到了一个真实的 bug:测试往一个对象上写 fetchImpl,

而插件读的是另一个对象。


跑测试

mkdir -p node_modules/@deepseek-ai
cp -r "$HOME/.dsh/profiles/desktop/node_modules/@deepseek-ai/." node_modules/@deepseek-ai/

node _test/run-all.mjs

三个套件,346 条断言全绿(对着真实 @deepseek-ai/dsh-tools 跑,不 mock):

套件 · 断言 · 内容

test-logic.mjs · 108 · Config 默认值与覆盖、注册开关、schema 归一化、isBot(8 种写法)、中位数(含「离群值几乎不动它」)、百分位插值、均值(含「离群值把它拖到 202」)、daysBetween、bus factor(7 种分布,含「正好 50% 不够」)、周分桶(含周日锚点)、错误映射(401 / 404 / 500 / 配额耗尽 / 网络 / 非 JSON / 无 fetch)、分页(多页、触顶截断、非列表响应)

test-integration.mjs · 99

…(truncated)

以上内容来自 npm 上的 README,我们只做格式化,不改内容。

版本

npm 上共发布:2,最后更新 2026-10-20

链接

GitHub · Homepage · npm

同类插件