一个脚本,把人批量邀请进 GitHub 组织,顺手把人加进 Team。
默认不会发任何东西,加 --execute 才真正发送。
1. 你得是这个组织的 owner(或至少是该 Team 的 maintainer)。不是 owner 会回 404 / 403。
2. 关掉系统代理(Clash / v2ray 之类的「系统代理」开关)。
实测结论:走系统代理成功率 42%,直连 100%。 代理开着时典型症状是满屏重试,或者
SSL: UNEXPECTED_EOF_WHILE_READING。
3. 建一个 token,把 token 直接写进脚本同目录的 token.txt 第一行,其他什么都不要有。
去 https://github.com/settings/tokens 建token:
| token 类型 | 需要的权限 |
|---|---|
| 细粒度(推荐) | Organization permissions → Members → Read and write |
| 经典 | admin:org |
细粒度 token 还要组织那边批准一下:组织 Settings → Personal access tokens → Pending requests。 没批准的话调用会一直失败。 (如果你是owner似乎不需要批准?蛮看一下吧)
token.txt已被.gitignore挡住,不会误提交。
环境:Python 3.10+,不需要装任何第三方库。
脚本只收一个表格文件(.csv 或 .xlsx)。
因为要发出去的一组人必须同时装得下「邮箱」和「GitHub用户名」两项
注:有用户名才能可靠邀请至 Team; 只有邮箱时, 仅可邀请至组织。
示例文件都在 docs/examples/,照着改就行。
推荐的表结构(列顺序任意,脚本按表头名字认列):
| 姓名 | 邮箱 | GitHub用户名 |
|---|---|---|
| 张三 | zhangsan@example.com | zhangsan |
| 李四 | lisi@example.com | lisi-2026 |
| 王五 | wangwu@example.com | |
| 赵六 | zhaoliu |
示例文件:收集表示例.csv · 收集表示例.xlsx
- 姓名:只用来在结果里显示是谁,不参与匹配账号
- 邮箱 / GitHub用户名:至少要有一个。两个都有最稳
- 表里可以只填邮箱(示例
只有姓名和邮箱.csv)—— 能发,但只有邮箱时对方必须已把该邮箱验证到自己账号上才点得动邀请, 而且这只能做到邀请到组织,无法实现邀请到team
列名认这些:
| 用途 | 认得的表头 |
|---|---|
| 姓名 | 姓名 名字 昵称 name nickname |
| 邮箱 | 邮箱 电子邮箱 邮件 email mail |
| GitHub用户名 | GitHub用户名 用户名 账号 username login |
列顺序完全不用管,只要表头名字在上面这张表里就认得出。比如这样也对
(示例 列顺序不一样.csv):
| 提交时间 | GitHub用户名 | 邮箱 | 姓名 | 备注 |
|---|---|---|---|---|
| 2026-03-01 | zhangsan | zhangsan@example.com | 张三 | 组长 |
先看一眼组织现状(只读,不改任何东西):
python github_inviter.py --org <组织名> --status邀请至组织:
# 1. 先看计划(不发任何东西)
python github_inviter.py --org <组织名> "<收集表.xlsx>"
# 2. 试跑 3 个(这一步会真发邀请)
python github_inviter.py --org <组织名> "<收集表.xlsx>" --execute --limit 3
# 3. 确认没问题,全量
python github_inviter.py --org <组织名> "<收集表.xlsx>" --execute同时把人加进 Team,加一个 --team:
python github_inviter.py --org <组织名> "<收集表.xlsx>" --team "<Team名>" --execute表头认不出来时(比如列名写的是「QQ」「微信」),用 --cols 按位置说明,
位置从 1 开始数,不用的列写 -:
--cols "name,email,username" # 第1列姓名、第2列邮箱、第3列用户名
--cols "username,email" # 第1列用户名、第2列邮箱
--cols "-,-,-,username,email,name" # 跳过前三列Note
--cols 只负责「哪一列是什么」,表头行照旧会被自动跳过。 表头认得出来时,
加了 --cols 也不用再写别的:
# 表头是 提交时间 | GitHub用户名 | 邮箱 | 姓名 | 备注 —— 认得出来, 第1行自动跳过
python github_inviter.py --org <组织名> "<收集表.xlsx>" \
--cols "-,username,email,name"
只有当表头也认不出来时(比如列名是「QQ」「微信」),脚本没法判断第 1 行是不是表头。 这时有两条路,随便哪条都行:
办法一:让它问你(最省事,在终端里直接跑就会问)
直接运行上面「邀请至组织」的第 1 条命令(不带 --execute),它会停下来问你:
表格前 3 行(共 3 行):
第 1 行: 日期 | QQ | 微信 | 怎么称呼 | 备注
第 2 行: 2026-03-01 | zhangsan | zhangsan@example.com | 张三 | 组长
第 3 行: 2026-03-01 | lisi-2026 | lisi@example.com | 李四 |
这一行数据长这样(共 5 列):
第 1 列: 日期
第 2 列: QQ
...
我没认出哪一列是邮箱、哪一列是用户名。请按位置告诉我, 和 --cols 一个写法:
第1列姓名、第2列用户名、第3列邮箱 就写 name,username,email
不想用的列写 - 就写 -,username,email,name
列的顺序(直接回车 = 按老约定: 第1列用户名、第2列邮箱): -,username,email,name
数据行范围(例如 2 或 2-10, 写 0 表示全是数据): 2
注意:这种表不能在脚本或管道里非交互地跑 —— 脚本会明说"没法向你提问"并停下, 而不是静默按错的列发出去。要自动化就用下面的办法二。
办法二:在命令里直接说清楚
# 表是: 日期 | QQ | 微信 | 怎么称呼 | 备注 —— 一个列名都认不出
python github_inviter.py --org <组织名> "<收集表.xlsx>" \
--cols "-,username,email,name,-" --skip-rows 1关于 --cols 的几点:
- 项数不必等于列数,写到有用的最后一列就行。例如第 5 列不用时,
--cols "-,username,email,name"和--cols "-,username,email,name,-"效果完全一样 - 但如果末尾要跳过的列后面还有你要用的列,就得把
-写齐,例如--cols "-,username,-,email"(第 1、3 列跳过,第 2 列用户名、第 4 列邮箱)
表里没有表头(第一行就是数据)时,用 --no-header,或者直接让脚本问你:
python github_inviter.py --org <组织名> 名单.csv --no-header --cols "name,email,username"鄙人认为的最佳方法, 就是先按
办法一走, 直接跑一次, 然后看脚本是否报错, 报错就按提示走, 之后再输入就按办法二走
| 选项 | 说明 |
|---|---|
--org |
必填,组织名(URL 里那个 slug) |
--execute |
真正发送。不加就是 dry-run |
--limit N |
本次只处理表里第 1~N 行,首次建议 3~5 |
--team "<名字>" |
同时把人加进这个 Team |
--status |
只读:看组织成员数、待处理邀请、失败邀请及原因 |
--cols |
表头认不出来时,按位置指定数据列 |
--skip-rows N |
跳过表格开头 N 行(比如认不出的表头行) |
--no-header |
表里没有表头,第一行就是数据 |
--sheet <名字> |
xlsx 指定工作表,默认第一个 |
--delay S |
每条间隔秒数,默认 2.0。别调小,会触发限流 |
--resolve-emails |
先用搜索把邮箱反查成用户名,命中更可靠(慢一些) |
--no-resume |
忽略历史结果重跑全部(会重复发送) |
直接再跑一次同样的命令就行。脚本会自动跳过上次已经成功的人,不会重复打扰。
想强制全部重来加 --no-resume(会重复发邮件,慎用)。
先是逐行结果(按姓名列出来谁成功、谁失败):
== 逐行结果 (4 人) ==
姓名 邮箱 用户名 组织邀请 Team 说明
---- -------------------- ---------- -------------- ---------------- ------------------
张三 zhangsan@example.com zhangsan 组织邀请已发出 已加入 team 已按用户名邀请
李四 lisi@example.com lisi-2026 组织邀请已发出 待对方接受组织邀请 已按用户名邀请
王五 wangwu@example.com — 组织邀请已发出 缺用户名 已按邮箱邀请
赵六 — zhaoliu 已是组织成员 待对方接受组织邀请 在组织成员列表里
后面还有汇总、失败明细(带处理措施)、以及「待重跑名单」。
汇总那一行(组织邀请: …)里每个数字都有明确含义,末尾的「合计」就是上面的人数:
- 第一个数字跟着模式走:真发时是「已发出邀请」,dry-run 是「计划邀请」,
加了
--no-preflight则是「状态未知(未预检)」—— 没查过就老老实实说没查过; - 具名的那几个桶相加不到「合计」时,差额在下面的明细行里:缺联系方式、表里重复、
被
--limit截断 —— 都是跟对方无关的行。
顺带一提:
--no-preflight的定义是「只看表格解析结果」,所以它优先于--execute—— 两个一起给时一封都不会发,脚本会明确告诉你(不会不声不响)。
同时在 output/ 下留档:
| 文件 | 内容 |
|---|---|
results-<时间戳>.csv |
逐行状态、失败原因、处理措施,Excel 直接打开 |
results-<时间戳>.jsonl |
机器可读,续跑就是靠它记住谁已成功 |
plan-<时间戳>.csv |
dry-run 时生成的计划 |
| 现象 | 原因 | 怎么办 |
|---|---|---|
满屏重试、SSL: UNEXPECTED_EOF_WHILE_READING |
系统代理 | 关掉系统代理 |
404 |
组织名写错,或 token 不是该组织 owner | 核对组织 slug 和 token |
403 权限不足 |
权限没给够,或细粒度 token 没被组织批准 | 补 Members 写权限 / 去组织批准 |
找不到 team: xxx |
Team 名字写错 | 报错里会列出全部 Team |
预检失败 |
token 无效或没权限 | 先用 --status 单独测一下 |
| 现象 | 原因 | 怎么办 |
|---|---|---|
已是组织成员 |
人本来就在组织里 | 不用管,接口也不会给他发邮件 |
已有待处理邀请 |
之前发过还没接受 | 不用重发,等对方接受 |
| 同上,但对方说没收到 | 邀请邮件进了垃圾邮件 | 让对方搜一下 GitHub 的邮件;7 天内有效 |
用户名不存在 |
表里那个用户名在 GitHub 上查不到 | 找本人核对拼写。先试相邻字母打反:user_name 被打成 u5er_na3e 这种情况 |
一批人 缺用户名,加不了 team |
表里只有邮箱 —— Team 接口不认邮箱 | 补一列 GitHub 用户名再重跑 |
一批人 待对方接受组织邀请 |
人还不是组织成员,现在加不进 Team | 等对方接受,再跑一次同样的命令就会自动补进 Team |
| 对方说已经进组织了,还显示待接受 | 表里用户名可能填错了 | 让对方把自己的 GitHub 用户名发来核对 |
跳过(上次已成功) |
续跑记忆生效了 | 不用管。想重发加 --no-resume |
跳过(--limit 截断) |
被 --limit 拦住了,不是失败 |
不带 --limit 再跑一次 |
这是邮箱邀请的硬限制:只有该邮箱是对方 GitHub 账号上已验证的邮箱,他才点得动。
没验证时接口照样返回成功,所以事后要用 --status 对一遍:
python github_inviter.py --org <组织名> --status里面列出的「失败邀请」就是对方没接成的。处理办法二选一:
- 让对方把该邮箱加到自己的 GitHub 账号并验证;
- 改用 GitHub 用户名邀请(表里补上用户名列)——这条路可靠得多。
看到 Invitation expired. User did not accept this invite for 7 days 表示
组织邀请 7 天过期了,需要重新发一次。
先跑一次 dry-run,看输出里的「列映射」那一行,确认 邮箱=<...> 用户名=<...> 姓名=<...>
指对了列。指错了就停下来用 --cols 改,不要在列映射不对的时候加 --execute。
| 现象 | 怎么办 |
|---|---|
| 列映射指错了列 | 用 --cols "name,email,username" 按位置指定 |
没法向你提问 |
表头认不出来,但当前不是在终端里跑(脚本/管道/CI)。改用 --cols + --skip-rows 1(或 --no-header) |
用了 --cols,结果里混进了表头那一行 |
加上 --skip-rows 1 |
| 解析出的行数比想象中少 | 完全空白的行会被忽略;重复的人只算一次 |
| 中文乱码 | 脚本会自动试 UTF-8 / GB18030,一般不会;仍乱码就把表另存为 UTF-8 |
| 只想要其中几个人 | 先用 --limit,或者另存一份只含这几个人的表 |
- 按邮箱邀请要求该邮箱是对方 GitHub 账号上已验证的邮箱,否则他点不动。 能拿到用户名, 就优先用用户名邀请(脚本默认就这么做),这条路可靠得多。
- 组织邀请 7 天过期,到期前催一下没接受的人。
- Team 接口只收用户名,不认邮箱,而且对方得先是组织成员。 所以收集表最好带一列 GitHub 用户名。
设计取舍、接口细节、踩坑记录见 ARCHITECTURE.md(面向维护者)。