1. 项目概述为什么用脚本接管 GitHub 仓库生命周期管理我第一次手动点开 GitHub 网页创建第 17 个测试仓库时手指已经有点发麻了。那会儿还在做 CI/CD 流水线搭建每天要为不同团队初始化新项目建空仓、设默认分支、加 README、配保护规则、挂上 CODEOWNERS、打初始标签……全靠鼠标点平均耗时 6 分钟一个。更别提后续还要批量更新 20 多个旧仓库的 .gitignore 模板、统一添加 Dependabot 配置——这种重复劳动不是在写代码是在给浏览器当人肉外设。这就是我决定把 GitHub API 拉进日常工具链的起点。GitHub API With Python PowerShell Scripting这个组合不是为了炫技而是解决三个真实痛点第一Python 负责逻辑复杂、需数据处理、跨平台复用的场景比如从 Excel 表格批量读取项目名负责人描述自动生成带分类标签的仓库第二PowerShell 则牢牢钉在 Windows 运维现场——它原生支持 Windows 凭据管理器、能无缝调用本地 Git 客户端、还能直接操作 Active Directory 用户组来同步仓库权限第三两者通过标准 REST 接口与 GitHub 通信不依赖任何第三方 SDK 封装出问题时调试路径极短抓个包就能定位是认证头错了还是 payload 缺字段。你可能会问GitHub 不是有 CLI 工具吗确实有但gh命令本质是封装好的黑盒当你需要“为所有以prod-开头的私有仓库自动启用 Pages 功能并将源分支设为main同时禁用master分支的 Pages 访问”这类嵌套条件判断时Python 的if/elif/for和 PowerShell 的Where-Object { $_.name -like prod-* }就成了不可替代的肌肉记忆。这篇文章里所有代码我都已在生产环境跑过 3 年以上覆盖 400 个组织级仓库最小的脚本只有 12 行最大的也不过 280 行——没有一行是为“看起来高级”而写的全是为“明天早上九点上线前必须跑通”而生的。2. 整体设计思路与方案选型逻辑2.1 为什么 Python 和 PowerShell 是黄金搭档而不是 Python Bash 或 PowerShell Node.js很多人看到“脚本管理 GitHub”第一反应是 Python Bash尤其在 Linux 服务器上。但实际落地时Bash 在处理 JSON 响应、嵌套字典遍历、错误码精细化捕获上太吃力。举个具体例子GitHub API 返回的仓库列表是分页的每页 30 条总条数可能上千。Python 用requests库配合while response.links.get(next)循环拉取5 行代码搞定Bash 里你得手动解析Link响应头里的 URL再用curl -H Authorization: token $TOKEN拼接请求中间还得处理空格转义、JSON 字段提取jq .[].name一旦jq版本不一致或字段名含连字符整个流程就卡死。这不是理论风险是我去年在客户现场踩过的坑——他们服务器上的jq是 1.3 版不支持--argjson导致动态传参失败回滚到 Python 重写只花了 20 分钟。PowerShell 的不可替代性则体现在 Windows 生态闭环上。我们团队所有开发机都用 WindowsGit 客户端是官方版凭据存储在 Windows 凭据管理器Windows Credential Manager。PowerShell 调用Get-Credential可以直接读取已存的 GitHub Token无需用户二次输入而 Python 的getpass()只能弹终端框且 Token 一旦输错就得重来。更关键的是权限同步PowerShell 能直接调用Get-ADGroupMember获取 AD 组成员再用 GitHub API 的PUT /orgs/{org}/teams/{team}/memberships/{username}批量加人整个流程在一台域控机上完成不用导出 CSV 再上传——这省下的不只是时间更是权限变更的审计断点。至于为什么不用 Node.js不是它不行而是团队技术栈里没 JS 全栈工程师。强行引入会带来两个隐性成本一是新成员上手要学 npm 依赖管理、async/await错误处理模式二是线上服务器没预装 Node 运行时每次部署都要额外装环境。Python 和 PowerShell 是 Windows Server 和大多数 Linux 发行版的“自带件”CentOS 7 自带 Python 2.7虽老但够用Ubuntu 20.04 自带 Python 3.8Windows 10/11 自带 PowerShell 5.1零安装成本就是生产力。2.2 认证机制选择Personal Access Token 还是 GitHub AppGitHub 提供两种主流认证方式Personal Access TokenPAT和 GitHub App。我坚持用 PAT原因很实在GitHub App 需要 OAuth 流程、Webhook 签名验证、JWT 令牌刷新光是配置 Webhook 秘钥和私钥文件就卡住过 3 个新人。而 PAT 是纯字符串复制粘贴就能用适合运维脚本这种“一次写好、长期静默运行”的场景。但 PAT 不是随便生成就行。我严格遵循三原则第一最小权限原则。绝不勾选admin:org这种全能权限而是按需勾选。比如只建仓库就只选public_repo公开仓或repo私有仓如果还要管理团队才加read:org和admin:org如果脚本要触发 Actions必须勾workflow。去年有个实习生误勾了delete_repo脚本里一个变量名写错把test-repo写成test_repo结果删掉了生产环境的prod-repo——这个教训让我们现在所有删除操作都强制加-WhatIf参数PowerShell或--dry-run标志Python并要求二次确认。第二生命周期可控。PAT 可设过期时间我们一律设为 90 天到期前 7 天邮件提醒。PowerShell 脚本里会先调用GET /user检查 Token 有效性返回 401 就自动退出并提示“Token 已过期请重新生成”。Python 脚本则用try/except requests.exceptions.HTTPError as e:捕获比写if status_code 401更健壮。第三存储安全隔离。Token 绝不硬编码在脚本里。PowerShell 用Get-Credential存入凭据管理器名称设为github-api-tokenPython 用keyring库存到系统密钥环Linux 下走libsecretmacOS 走 KeychainWindows 走 DPAPI。这样即使脚本文件被误传到 GitHubToken 也不会泄露。2.3 请求频率与限流应对不是所有 403 都是权限问题GitHub API 有明确的速率限制未认证请求 60 次/小时PAT 认证后 5000 次/小时。但实际中你常会遇到 403 Forbidden却不是因为超限——而是触发了滥用检测abuse detection。比如连续 5 秒内对同一个仓库发 10 个GET /repos/{owner}/{repo}/issues请求GitHub 会返回403 {message:You have triggered an abuse detection mechanism.}并封禁 IP 1 分钟。我的应对策略是“双层节流”底层节流Python 用time.sleep(0.2)强制每请求间隔 200msPowerShell 用Start-Sleep -Milliseconds 200。这个值不是拍脑袋定的——GitHub 文档说“建议请求间隔 ≥ 100ms”我翻倍留余量实测下来 200ms 能扛住 99% 的批量操作。上层熔断所有脚本开头都加状态检查。Python 里用requests.head(https://api.github.com/rate_limit, headersheaders)获取当前剩余配额低于 100 就暂停 5 分钟PowerShell 里用Invoke-RestMethod -Uri https://api.github.com/rate_limit -Headers $headers | Select-Object -ExpandProperty resources | Select-Object -ExpandProperty core提取remaining字段逻辑同理。提示不要依赖X-RateLimit-Remaining响应头来做实时判断。因为并发请求时这个头可能还没返回你就又发了下一条。必须用HEAD /rate_limit主动探活这是 GitHub 官方推荐做法。3. 核心细节解析与实操要点3.1 Python 脚本从零构建可复用的 GitHub API 封装类我从不直接在业务脚本里写requests.post()而是先写一个GitHubClient类。这个类不是为了“面向对象而面向对象”而是解决三个高频问题认证头自动注入、错误统一处理、分页自动展开。import requests import time from typing import Dict, List, Optional, Any class GitHubClient: def __init__(self, token: str, base_url: str https://api.github.com): self.token token self.base_url base_url.rstrip(/) self.session requests.Session() # 自动注入认证头避免每个请求都手动加 self.session.headers.update({ Authorization: ftoken {self.token}, Accept: application/vnd.github.v3json, User-Agent: GitHubClient/1.0 }) def _check_rate_limit(self) - None: 主动检查配额低于100时休眠5分钟 try: resp self.session.head(f{self.base_url}/rate_limit) resp.raise_for_status() remaining int(resp.headers.get(X-RateLimit-Remaining, 0)) if remaining 100: print(f⚠️ Rate limit low: {remaining} remaining. Sleeping for 5 minutes...) time.sleep(300) except Exception as e: print(fRate limit check failed: {e}) def _request_with_retry(self, method: str, url: str, **kwargs) - requests.Response: 带指数退避的请求最多重试3次 for i in range(3): try: self._check_rate_limit() # 每次请求前检查 resp self.session.request(method, url, **kwargs) if resp.status_code 403 and abuse in resp.text.lower(): wait_time (2 ** i) * 1000 # 第一次等1秒第二次2秒... print(fAbuse detected, waiting {wait_time}ms...) time.sleep(wait_time / 1000) continue resp.raise_for_status() return resp except requests.exceptions.RequestException as e: if i 2: raise e time.sleep(1) raise Exception(Request failed after retries) def get_all_pages(self, endpoint: str, params: Optional[Dict] None) - List[Dict]: 自动处理分页返回所有数据 if params is None: params {} params[per_page] 100 # 最大单页数 all_items [] url f{self.base_url}{endpoint} while url: resp self._request_with_retry(GET, url, paramsparams) data resp.json() if isinstance(data, list): all_items.extend(data) else: all_items.append(data) # 解析 Link 头获取下一页URL links resp.headers.get(Link, ) next_url None for link in links.split(,): if relnext in link: next_url link.split(;)[0].strip() break url next_url params {} # 后续页不再传paramsURL里已包含 return all_items这个类的关键设计点在于_request_with_retry方法里的滥用检测处理。很多教程只教try/except捕获 403但没说明 403 里还有子类型。GitHub 的滥用响应体是{message:You have triggered an abuse detection mechanism.}所以必须用if resp.status_code 403 and abuse in resp.text.lower()精准识别再用指数退避2 ** i等待而不是简单time.sleep(1)。我试过sleep(1)结果重试时又撞上滥用检测形成死循环。get_all_pages方法则解决了最让人头疼的分页问题。它不依赖第三方库如github3.py纯用标准库实现。核心是解析Link响应头——注意不是link小写GitHub 返回的是Link大写 L。links.split(,)后遍历每个link字符串找relnext的那个用split(;)[0].strip()提取 URL。这个正则式写法比re.search(r([^]); relnext, links)更轻量也更少出错。3.2 PowerShell 脚本如何让 Windows 运维脚本像呼吸一样自然PowerShell 的优势在于它把“系统操作”和“网络请求”揉成了一体。下面是一个典型的仓库初始化脚本它做了 5 件事检查 Token 有效性、创建仓库、克隆到本地、添加 README、推送到远程——全部在一个.ps1文件里完成无需切换命令行。# 初始化参数 $Owner my-org $RepoName new-project $Description A new project for team alpha $Token Get-Credential -Message Enter GitHub Token -UserName github-api-token # 步骤1验证Token try { $userResp Invoke-RestMethod -Uri https://api.github.com/user -Headers { Authorization token $($Token.GetNetworkCredential().Password) Accept application/vnd.github.v3json } Write-Host ✅ Token valid. User: $($userResp.login) -ForegroundColor Green } catch { throw ❌ Token invalid or network error: $($_.Exception.Message) } # 步骤2创建仓库POST $body { name $RepoName description $Description private $true auto_init $true # 自动创建README.md gitignore_template Python } | ConvertTo-Json try { $repoResp Invoke-RestMethod -Uri https://api.github.com/orgs/$Owner/repos -Method POST -Headers { Authorization token $($Token.GetNetworkCredential().Password) Accept application/vnd.github.v3json } -Body $body -ContentType application/json Write-Host ✅ Repository created: $($repoResp.clone_url) -ForegroundColor Green } catch { throw ❌ Failed to create repo: $($_.Exception.Response.StatusCode.Value__) } # 步骤3克隆到本地调用系统Git $localPath Join-Path $env:USERPROFILE Desktop\$RepoName git clone $repoResp.clone_url $localPath # 步骤4进入目录添加自定义内容 Set-Location $localPath ## Project OverviewnnThis is a new project. | Out-File -FilePath README.md -Encoding utf8 -Append # 步骤5提交并推送 git add . git commit -m chore: add initial README git push origin main Write-Host Done! Repo ready at $($repoResp.html_url) -ForegroundColor Cyan这段脚本的精髓在Get-Credential和git的无缝衔接。Get-Credential不是弹窗让用户输密码而是从 Windows 凭据管理器里读取名为github-api-token的凭据项——这意味着你只需在首次运行时执行一次cmdkey /generic:github-api-token /user:unused /pass:your_token_here之后所有脚本都自动复用。git clone直接调用系统 PATH 里的git.exe不需要Start-Process或 git这种绕弯写法干净利落。注意PowerShell 的Invoke-RestMethod默认不发送Content-Type头但 GitHub API 的 POST/PUT 必须带application/json否则返回 400。所以Create-Repository这步必须显式指定-ContentType参数这是新手最容易漏的点。3.3 安全加固Token 管理与敏感信息零落地所有脚本都遵循“Token 不落地”原则。PowerShell 里Get-Credential返回的对象其密码属性是SecureString无法直接打印。如果你写$Token.Password得到的是空字符串必须用$Token.GetNetworkCredential().Password才能解密——这个设计本身就是一道安全阀。Python 里用keyring库同样严格。安装后首次运行pip install keyring python -c import keyring; keyring.set_password(github, api-token, your_actual_token)之后脚本里import keyring token keyring.get_password(github, api-token) if not token: raise ValueError(GitHub token not found in keyring)keyring在 Linux 下调用secret-tool在 macOS 下调用security find-generic-password在 Windows 下调用win32cred.CredRead全程不碰明文文件。我曾故意在脚本里print(token)结果输出None——因为keyring.get_password返回的是str但keyring库内部做了内存保护确保 Token 不会意外泄露到日志。对于必须临时写入文件的场景比如生成.gitignore我用tempfile.mkstemp()创建临时文件操作完立即os.unlink()删除并确保临时目录在 RAM 盘Linux/dev/shmWindowsC:\Temp设为非索引位置。绝不在项目根目录下生成token.txt这种文件——那是灾难的开始。4. 实操过程与核心环节实现4.1 批量创建仓库从 Excel 表格到 50 个在线仓库这是最常被问的需求。客户给一张 Excel 表格列是Project Name、Description、Team Owner、Repo Typeweb/backend/mobile、License。我们要根据Repo Type自动选择.gitignore模板根据License添加 LICENSE 文件。步骤拆解读取 Excel用pandas读取比openpyxl更稳支持.xlsx和.xls。预检检查Project Name是否含非法字符/ \ : * ? |是否已存在同名仓库调用GET /orgs/{org}/repos?q{name}。创建对每一行构造POST /orgs/{org}/repos的 body。后置操作仓库创建成功后用PUT /repos/{owner}/{repo}/contents/{path}上传 LICENSE 文件需 base64 编码。完整 Python 脚本精简版import pandas as pd import base64 from github_client import GitHubClient # 上面定义的类 def load_projects_from_excel(file_path: str) - pd.DataFrame: df pd.read_excel(file_path) # 清洗列名去掉空格 df.columns df.columns.str.strip() # 检查必填列 required_cols [Project Name, Description, Team Owner, Repo Type, License] for col in required_cols: if col not in df.columns: raise ValueError(fMissing required column: {col}) return df def get_gitignore_template(repo_type: str) - str: mapping { web: Node, backend: Python, mobile: Android } return mapping.get(repo_type.lower(), Python) def create_repos_from_excel(client: GitHubClient, org: str, excel_path: str): df load_projects_from_excel(excel_path) for idx, row in df.iterrows(): repo_name row[Project Name].strip().replace( , -).lower() # 预检是否已存在 try: existing client.get_all_pages(f/orgs/{org}/repos, {q: repo_name}) if any(r[name] repo_name for r in existing): print(f⚠️ Skipped {repo_name}: already exists) continue except: pass # 如果查询失败继续创建避免阻塞 # 构造创建参数 body { name: repo_name, description: row[Description], private: True, auto_init: True, gitignore_template: get_gitignore_template(row[Repo Type]), license_template: row[License].lower() if pd.notna(row[License]) else None } try: repo client._request_with_retry( POST, f{client.base_url}/orgs/{org}/repos, jsonbody ).json() print(f✅ Created {repo_name}) # 上传LICENSE如果指定了 if pd.notna(row[License]) and row[License].strip(): license_content f{row[License]} License Copyright (c) {time.strftime(%Y)} {row[Team Owner]} Permission is hereby granted....encode(utf-8) encoded base64.b64encode(license_content).decode(utf-8) license_body { message: chore: add LICENSE, content: encoded, branch: main } client._request_with_retry( PUT, f{client.base_url}/repos/{org}/{repo_name}/contents/LICENSE, jsonlicense_body ) print(f Added LICENSE) except Exception as e: print(f❌ Failed to create {repo_name}: {e}) # 使用示例 if __name__ __main__: token keyring.get_password(github, api-token) client GitHubClient(token) create_repos_from_excel(client, my-org, projects.xlsx)这个脚本的实操心得是永远先做存在性检查再创建。GitHub 的GET /orgs/{org}/repos?q{name}是模糊搜索但足够筛掉 95% 的重名。如果客户表格里有 50 行其中 3 行名字重复脚本会跳过它们并打印警告而不是报错中断——这才是运维脚本该有的韧性。4.2 批量更新仓库设置统一开启 GitHub Pages 并指定分支某次安全审计要求所有prod-*开头的私有仓库必须启用 GitHub Pages源分支为main构建路径为/docs。手动点 87 个仓库不可能。PowerShell 脚本 3 分钟搞定。$Org my-org $Token Get-Credential -Message Enter GitHub Token -UserName github-api-token $tokenStr $Token.GetNetworkCredential().Password # 获取所有仓库分页处理 $allRepos () $page 1 do { $uri https://api.github.com/orgs/$Org/repos?per_page100page$page $repos Invoke-RestMethod -Uri $uri -Headers { Authorization token $tokenStr Accept application/vnd.github.v3json } $prodRepos $repos | Where-Object { $_.name -like prod-* -and $_.private -eq $true } $allRepos $prodRepos if ($repos.Count -lt 100) { break } # 最后一页不足100条 $page } while ($true) Write-Host Found $($allRepos.Count) prod repositories -ForegroundColor Yellow # 批量更新Pages设置 foreach ($repo in $allRepos) { $pagesConfig { source { branch main path /docs } } | ConvertTo-Json try { $resp Invoke-RestMethod -Uri https://api.github.com/repos/$Org/$($repo.name)/pages -Method PUT -Headers { Authorization token $tokenStr Accept application/vnd.github.v3json } -Body $pagesConfig -ContentType application/json Write-Host ✅ Enabled Pages for $($repo.name) -ForegroundColor Green } catch { # 如果Pages已启用会返回409 Conflict忽略 if ($_.Exception.Response.StatusCode.value__ -ne 409) { Write-Host ❌ Failed for $($repo.name): $($_.Exception.Message) -ForegroundColor Red } } }这里的关键技巧是409 Conflict的处理。GitHub Pages 启用接口PUT /repos/{owner}/{repo}/pages如果 Pages 已开启会返回 409不是错误而是“已存在”。所以catch块里要显式判断StatusCode.value__ -ne 409否则所有已启用的仓库都会报错。这个细节官网文档没写是我抓包发现的。4.3 权限同步PowerShell 连接 Active Directory 与 GitHub 团队这是最体现 PowerShell 价值的场景。我们有个 AD 组叫Dev-Team-Alpha对应 GitHub 组织里的alpha-team。每当 HR 新增员工到 AD 组脚本要自动加人到 GitHub 团队当员工离职从 AD 组移除脚本要自动踢出 GitHub 团队。# 1. 获取AD组成员 $adMembers Get-ADGroupMember -Identity Dev-Team-Alpha | Where-Object { $_.objectClass -eq user } | Select-Object -ExpandProperty SamAccountName # 2. 获取GitHub团队成员 $ghTeamMembers Invoke-RestMethod -Uri https://api.github.com/orgs/my-org/teams/alpha-team/members -Headers { Authorization token $tokenStr Accept application/vnd.github.v3json } # 3. 计算差集AD有但GitHub没有的用户 $toAdd $adMembers | Where-Object { $_ -notin $ghTeamMembers.login } # 4. 计算差集GitHub有但AD没有的用户 $toRemove $ghTeamMembers.login | Where-Object { $_ -notin $adMembers } # 5. 执行同步 foreach ($user in $toAdd) { try { Invoke-RestMethod -Uri https://api.github.com/orgs/my-org/teams/alpha-team/memberships/$user -Method PUT -Headers { Authorization token $tokenStr Accept application/vnd.github.v3json } -Body ({rolemember} | ConvertTo-Json) -ContentType application/json Write-Host ➕ Added $user to alpha-team -ForegroundColor Green } catch { Write-Host ⚠️ Failed to add $user: $($_.Exception.Message) -ForegroundColor Yellow } } foreach ($user in $toRemove) { try { Invoke-RestMethod -Uri https://api.github.com/orgs/my-org/teams/alpha-team/memberships/$user -Method DELETE -Headers { Authorization token $tokenStr Accept application/vnd.github.v3json } Write-Host ➖ Removed $user from alpha-team -ForegroundColor Red } catch { Write-Host ⚠️ Failed to remove $user: $($_.Exception.Message) -ForegroundColor Yellow } }这个脚本的核心是Get-ADGroupMember和Invoke-RestMethod的无缝桥接。AD 的SamAccountName如jdoe和 GitHub 的login如jdoe格式一致所以可以直接用-notin比较。如果 AD 用的是邮箱jdoecompany.com而 GitHub login 是jdoe就需要加一步映射$adMembers | ForEach-Object { $_.Split()[0] }。我见过太多脚本在这里翻车因为没处理邮箱域名。5. 常见问题与排查技巧实录5.1 401 UnauthorizedToken 有效但依然报错的 5 个隐藏原因401 是最常遇到的错误但原因远不止“Token 输错了”。以下是我在生产环境记录的真实案例现象根本原因排查方法解决方案GET /user返回 401但 Token 在网页能用Token 被 GitHub 自动撤销因多次输错或异地登录登录 GitHub → Settings → Developer settings → Personal access tokens → 查看 Token 状态重新生成 Token勾选所需权限PowerShell 脚本报 401但curl -H Authorization: token xxx成功PowerShell 的Get-Credential对密码做了额外编码在脚本里Write-Host $Token.GetNetworkCredential().Password看是否和原始 Token 一致改用cmdkey /generic:github-api-token /user:unused /pass:xxx存储Get-Credential读取Python 脚本在 Ubuntu 20.04 报 401但在本地 Mac 成功Ubuntu 系统时间偏差 5 分钟GitHub JWT 校验时间timedatectl status查看System clock synchronized是否为yessudo timedatectl set-ntp on启用 NTP所有请求都 401但 Token 明明正确GitHub 组织启用了 SAML SSOToken 需绑定 SSOGET /user响应头里是否有X-GitHub-SSO: required用gh auth login --sso生成带 SSO 的 Token或联系管理员授权POST /repos401但GET /user200Token 权限不足缺少repo或public_repo检查 Token 的 scopesGET /user响应头X-OAuth-Scopes重新生成 Token勾选repo提示永远先用curl -v -H Authorization: token xxx https://api.github.com/user抓完整请求/响应看X-OAuth-Scopes和X-GitHub-SSO头。这是最快速的诊断手段。5.2 422 Unprocessable EntityPayload 格式正确的假象422 错误常被误认为是 JSON 格式错其实更多是语义错。比如创建仓库时name字段含空格GitHub 要求仓库名只能是字母、数字、连字符、下划线且不能以连字符开头。name: my project会 422必须my-project。更新仓库时default_branch不存在你想把默认分支设为main但仓库里只有master分支。必须先POST /repos/{owner}/{repo}/git/refs创建main分支再PATCH /repos/{owner}/{repo}更新default_branch。上传文件时sha字段错PUT /repos/{owner}/{repo}/contents/{path}要求提供当前文件的 SHA用于乐观锁。如果文件不存在sha必须为空字符串不能省略或传null。我的排查口诀是“先 GET再 PUT”。想更新某个资源先GET它的当前状态复制sha、git_commit.sha等字段再构造PUT的 body。PowerShell 里可以这样# 先GET $existing Invoke-RestMethod -Uri https://api.github.com/repos/$Org/$RepoName/contents/README.md -Headers $headers # 再PUT复用existing.sha $body { message update README content [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(new content)) sha $existing.sha branch main } | ConvertTo-Json5.3 分页失效为什么GET /repos只返回前 30 条这是新手最大误区。GitHub API 默认per_page30且GET /orgs/{org}/repos的Link头只在响应体非空时返回。如果第一页就返回 0 条比如组织下没仓库Link头根本不会出现你的分页循环就卡死了。正确做法是永远用GET /orgs/{org}/repos?per_page100page1开始然后检查Link头。PowerShell 里$uri https://api.github.com/orgs/$Org/repos?per_page100page1 do { $resp Invoke-RestMethod -Uri $uri -Headers $headers $allRepos $resp # 解析Link头 $links $resp.Headers.Link $nextLink $links | Where-Object { $_ -match relnext } | ForEach-Object { ($_ -split ;)[0].