GitHub 镜像站这个词在开发团队里几乎天天提到。我最早接触它是因为每次 CI 构建都要从 GitHub 拉一堆依赖时不时的超时、断流让构建时间变成玄学后来帮运维搭内网代码仓库发现把 GitHub 上的开源项目同步到内网再配合一套统一的下载入口能省掉大量重复等待。这篇文章就从实际需求出发把镜像站的三种常见形态——仓库全量同步、下载缓存代理、定时资产分发——的选型逻辑、搭建步骤和踩坑记录完整过一遍。适用对象是手里有服务器、想在团队内部或自己机器上搭建一个稳定下载入口的开发者我会尽量把每一步写成可以直接抄作业的配置。1. 镜像站能解决什么先想清楚再动手1.1 镜像站不是只有一个形态很多人一上来就问“镜像站用什么软件搭”其实这是个伪问题。镜像站是一类需求的统称不同场景要解决的问题完全不同软件的差异只是表象。第一类是代码仓库镜像。团队内网需要绕过公网波动稳定拿到某个开源仓库的代码或者希望给 GitLab 外部项目做一份容灾备份这类需求的核心是“把一个 Git 仓库变成内网的一个只读副本”。典型工具是 Gitea、GitLab 的 Pull Mirror或者干脆用裸仓库加定时任务。它实时性不需要太高同步间隔几小时甚至一天都能接受但仓库数据必须完整分支和标签不能丢。第二类是下载缓存代理。开发者从 GitHub 拉 Release 文件和仓库压缩包非常频繁这些东西体积动辄几十 MB 到几 GB直接从源站拉会占用大量时间。这类需求的核心是“在服务器上缓存高频访问的大文件下次来取直接本地回包”。它比仓库镜像实时性要求更高但缓存的是静态产物只要版本和 URL 不变缓存就能一直命中。第三类是静态资产定时分发。有些成熟项目的二进制其实变化很慢比如 kubelet、helm、protoc、hadoop 生态的一堆 jar。你完全可以定时把需要的版本拉到自己服务器上用内网链接统一分发。这种方式不追求“镜像所有内容”而是精确控制要同步哪些仓库、哪些 tag、哪些文件存储开销可控最容易被团队接受。这三类形态不是互斥的。我见过很多团队先搭仓库同步发现 CI 依然要下载依赖包再补一个下载缓存最后把固定版本挪到静态目录。你也可以从任何一个形态开始关键是先搞清楚你的痛点是“代码拉不下来”还是“大文件下载慢”。1.2 三种形态怎么选一张表说清楚形态存储开销实时性部署难度核心价值仓库级只读镜像中等仓库 .git 体积累积低分钟到小时级同步低Gitea 界面点几下就行内网稳定 clone 代码容灾备份下载缓存代理中等缓存可设置淘汰策略高第一次回源后立即命中中需写一点脚本逻辑解决 Release / 压缩包下载慢静态资产定时分发可控只保留指定版本低依赖定时任务低脚本加静态目录固定版本工具链内网分发选型的判断标准其实就两条。需要访问完整分支历史、打标签的人选仓库镜像。只是拿发布产物、不关心历史版本的人选缓存代理或静态分发。如果你的场景两者都有仓库镜像打底下载缓存铺在它前面当入口这是最舒服的组合。2. 形态一仓库级只读镜像用 Gitea 同步 GitHub2.1 为什么我选 Gitea 而不是 GitLabGitLab 的 Pull Mirror 功能同样好用但做纯粹的只读镜像仓库Gitea 更轻。GitLab 一个实例光内存就至少要 4GB跑起来之后后台还有一堆 Runner、Prometheus、PostgreSQL 组件在消耗资源Gitea 是个单体程序配合 SQLite 就能跑512MB 内存的机器照样顺畅这对很多只有一台小服务器的团队非常友好。更重要的是 Gitea 原生支持“镜像仓库”的概念新建仓库时直接选“镜像仓库”填入源地址就能开始同步之后界面里能看同步状态、同步时间还能手动立即同步。GitLab 虽然也能配 remote mirror但整个界面和权限模型要重很多为了一个只读副本不值得。2.2 服务端搭建docker-compose 是最快的路我习惯用 Docker 部署升级和回滚都方便。下面这份 docker-compose.yml 是最小可用版本实际使用只需要把 git.example.com 换成你的域名。services: gitea: image: gitea/gitea:latest container_name: gitea restart: unless-stopped environment: - USER_UID1000 - USER_GID1000 - GITEA__server__DOMAINgit.example.com - GITEA__server__ROOT_URLhttps://git.example.com/ - GITEA__server__DISABLE_SSHfalse - GITEA__database__DB_TYPEsqlite3 - GITEA__database__PATH/data/gitea/gitea.db volumes: - ./gitea:/data ports: - 3000:3000启动之后浏览器访问 http://服务器IP:3000进入安装页面。这里的关键点是 ROOT_URL 要填最终对外域名否则生成的克隆地址会是 IP 加端口别人拿到链接也用不了。数据库直接用 SQLite 就行不需要单独起 MySQL少一个进程少一分运维负担。如果你不想用 Docker官方也提供 Linux 二进制包解压后直接运行./gitea web也能跑起来。区别只是更新方式二进制包升级要手动替换文件Docker 一条命令搞定更推荐 Docker 方式。2.3 建立镜像仓库的关键配置在 Gitea 登录后点右上角“新建仓库”页面底部能看到一个“镜像仓库”的开关选中它仓库类型自动变成“只读的镜像仓库”。然后在“拉取地址”填 GitHub 仓库的地址比如https://github.com/kubernetes/kubernetes.git如果仓库是公开的这个地址就够了如果是私有仓库需要在地址里带上 Tokenhttps://你的GitHubTokengithub.com/yourname/private-repo.git同步间隔我建议设在 180 分钟到 12 小时之间。间隔太短容易触发 GitHub 的频率限制太长又会让代码不够新。默认的 8 小时其实挺合理如果有紧急需求手动点一次“立即同步”就行。还要注意两个开关“同步时删除远端不存在的分支”建议打开避免内网残余一堆已被源仓库清掉的老分支“镜像仓库同步时使用本地用户”保持默认。创建完成后Gitea 会在后台跑首次同步大仓库可能要等几分钟到几十分钟可以在“站点管理 → 后台任务”里观察进度。2.4 同步原理和几个实际坑Gitea 的 Pull Mirror 本质上是定期执行git fetch。Git 的对象存储是内容寻址的每个对象以 SHA-1 为文件名内容相同的对象不会重复存储所以增量同步时第二次以后很轻快只有新提交产生的对象需要传输。你可以在服务器上进入仓库目录运行git count-objects -vH看实际的大小和松散对象数量。实际使用中这几个坑最常遇到第一次同步大型仓库经常超时。GitHub 上的 monorepo 仓库 .git 目录动辄几个 GB首次传输时间可能超过 Gitea 默认的超时配置。建议在首次同步前临时把服务器上的网络超时参数调大或者选择半夜带宽空闲时创建镜像不要在工作时间启动。私有仓库同步提示认证失败。除了在 URL 里带 Token还要确认 Token 具备repo权限。GitHub 的个人访问令牌默认有很多权限如果你自定义过 scope务必检查repo是否勾选。镜像仓库里没有 LFS 文件。Gitea 的 pull mirror 只是同步 Git 对象不自动同步 Git LFS 存储。项目如果依赖 LFS需要在源服务器用git lfs fetch --all拉完再想办法传到镜像侧。我的建议是如果团队确实依赖 LFS 文件不要完全依赖仓库镜像把 LFS 文件单独放进对象存储或 Nextcloud 分发会更稳。磁盘 inode 被占满。Git 仓库小文件极多一亿个对象拖垮的可能不是容量而是 inode。部署前用df -i看一眼服务器inode 使用率超过 80% 就要小心最好把 Gitea 数据目录放在 XFS 或 ext4 这类支持大量小文件的文件系统上。3. 形态二下载缓存代理Python 小服务吃掉远端的 3023.1 为什么不用纯 Nginx 反代很多人第一反应是“用 nginx 反代 github.com 不就行了吗”我最初也这么试过结果很快就发现一个关键问题GitHub 的 Release 下载和仓库压缩包下载服务端都会返回 302 跳转浏览器会跟随跳转到对象存储节点比如objects.githubusercontent.com或codeload.github.com。nginx 的反向代理基本不会替客户端跟随这些重定向它只是把 302 响应原样返回给客户端。客户端拿到跳转地址后仍然要直连那些对象存储域名下载速度没任何改善。而且很多响应里的 URL 是绝对路径指向的域名并不在你代理的范围内修改proxy_redirect也处理不了跨域名的跳转。所以纯 nginx 做 GitHub 下载加速很难做完整。解决思路是用一个服务端脚本去请求目标 URL主动跟随所有 302把最终文件下载到本地磁盘再作为一个完整的响应交给客户端。这样对客户端来说是透明的它只跟你的服务器通信速度快、链路单一。3.2 目录设计与接口约定我做的缓存代理对外只暴露两个路径保证服务不会被滥用成任意 URL 抓取器https://mirror.example.com/github/{owner}/{repo}/archive/refs/heads/{branch}.zip https://mirror.example.com/github/{owner}/{repo}/releases/download/{tag}/{asset}第一条用来拉仓库源码压缩包第二条用来拉 Release 附件。客户端使用的时候把原来的https://github.com/前缀替换成https://mirror.example.com/github/即可比如wget https://mirror.example.com/github/prometheus/prometheus/archive/refs/heads/main.zip wget https://mirror.example.com/github/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz接口后面我加了一层白名单校验只允许上述两种路径其他一律返回 404。这一步很重要否则一旦服务器不小心暴露在公网就可能被人拿来做免费代理下载任意内容流量和信誉都会被打爆。3.3 核心代码与运行方式下面是我实际在用的简化版本Flask 加 requests 就够没有额外依赖。import os import hashlib from urllib.parse import urlparse import requests from flask import Flask, abort, send_file app Flask(__name__) CACHE_DIR /data/github-mirror/cache UPSTREAM https://github.com # 只允许跟随这些域名下的跳转 ALLOWED_NETLOCS { github.com, codeload.github.com, objects.githubusercontent.com, raw.githubusercontent.com, github-releases.githubusercontent.com, } MAX_REDIRECTS 8 def is_allowed(url): netloc urlparse(url).netloc.lower() return netloc in ALLOWED_NETLOCS def cache_file(key): digest hashlib.sha256(key.encode()).hexdigest() return os.path.join(CACHE_DIR, digest[:2], digest[2:]) def resolve_and_download(target): current target for _ in range(MAX_REDIRECTS): if not is_allowed(current): abort(403) with requests.get( current, streamTrue, allow_redirectsFalse, timeout(10, 90) ) as resp: if resp.status_code in (301, 302, 303, 307, 308): location resp.headers.get(Location) if not location: abort(502) if location.startswith(http): current location else: current https:// urlparse(current).netloc location continue if resp.status_code 400: abort(resp.status_code) return current, resp abort(502) def stream_to_path(resp, path): os.makedirs(os.path.dirname(path), exist_okTrue) tmp_path path .part try: with open(tmp_path, wb) as f: for chunk in resp.iter_content(chunk_size256 * 1024): if chunk: f.write(chunk) os.replace(tmp_path, path) finally: resp.close() if os.path.exists(tmp_path): os.remove(tmp_path) app.route(/github/path:target_path) def mirror(target_path): path_name / target_path if ( /releases/download/ not in path_name and /archive/ not in path_name and /raw/ not in path_name ): abort(404) final_url f{UPSTREAM}/{target_path} local_path cache_file(final_url) if os.path.exists(local_path): return send_file(local_path, conditionalTrue, max_age86400) _, resp resolve_and_download(final_url) stream_to_path(resp, local_path) return send_file(local_path, conditionalTrue, max_age86400) if __name__ __main__: app.run(host127.0.0.1, port8000)几个关键点解释一下。resolve_and_download函数手动处理重定向每跳一步都检查域名是否在白名单里这是安全底线。stream_to_path先写临时文件再原子重命名避免下载到一半的脏文件被后续请求读到。缓存文件名是目标 URL 的 SHA-256 摘要天然按仓库、tag、文件名区分不会碰撞。生产环境用 gunicorn 启动建议 4 个 worker前面再用 nginx 做 443 和 TLS 终结pip install flask requests gunicorn gunicorn -w 4 -b 127.0.0.1:8000 app:appnginx 配置只需要一个标准的反向代理节点把/github/路径转发到 8000 端口。整个服务只监听内网或加 Basic Auth不要裸奔在公网。3.4 缓存过期与一致性这个服务的缓存策略是“URL 不变就永不重新拉取”。因为 GitHub 的 Release 文件地址带 tag发布之后内容基本不可变压缩包地址带 commit 或分支名同一个 URL 对应内容也是固定的所以缓存命中的文件可以放心用。唯一要注意的是分支类 URL。比如你请求archive/refs/heads/main.zip分支更新后相同 URL 的内容会变缓存就可能旧了。我的处理方式很简单在服务器上加一个定时任务定期删除超过 7 天没有被访问的缓存文件find /data/github-mirror/cache -type f -mtime 7 -delete这样热门文件一直留在磁盘上冷门文件过期淘汰下一次请求自动回源重新拉取。对于内网团队这种规模完全够用。4. 形态三静态 Release 分发定时器拉取固定清单4.1 用 GitHub API 获取资产列表如果团队只需要固定几个工具链静态分发是最省心的方案。它的思路是你不再等用户来触发而是让服务器定时去向 GitHub API 查询指定仓库的最新 Release然后自动把附件下载到本地再用一个简单的索引页面让团队自助获取。GitHub API 的 Release 接口长这样GET https://api.github.com/repos/{owner}/{repo}/releases/latest未认证状态每小时 60 次请求限制但拉取几十个仓库足够用。如果你管理的仓库多或者希望抓取多个历史版本建议创建一个 GitHub Token在请求头里带上认证信息这样配额会提升到每小时 5000 次基本不用考虑限制。4.2 增量下载脚本下面这个脚本负责定时同步指定仓库的最新 Release核心特点是有校验和增量判断文件已经存在且大小一致就直接跳过。import os import requests TOKEN os.environ[GH_TOKEN] DEST_ROOT /data/release HEADERS { Authorization: ftoken {TOKEN}, Accept: application/vnd.githubjson, } REPOS [ kubernetes/kubernetes, helm/helm, prometheus/prometheus, goharbor/harbor, ] def safe_path(owner, repo, tag, name): return os.path.join(DEST_ROOT, owner, repo, tag, name) def sync_release(owner, repo): url fhttps://api.github.com/repos/{owner}/{repo}/releases/latest resp requests.get(url, headersHEADERS, timeout20) resp.raise_for_status() data resp.json() tag data[tag_name] for asset in data.get(assets, []): name asset[name] size asset[size] path safe_path(owner, repo, tag, name) if os.path.exists(path) and os.path.getsize(path) size: continue os.makedirs(os.path.dirname(path), exist_okTrue) dl requests.get( asset[browser_download_url], headersHEADERS, streamTrue, timeout(10, 120), ) dl.raise_for_status() tmp path .part with open(tmp, wb) as f: for chunk in dl.iter_content(chunk_size256 * 1024): f.write(chunk) os.replace(tmp, path) print(fdownloaded {owner}/{repo} {tag} {name}) for item in REPOS: owner, repo item.split(/) sync_release(owner, repo)把这段脚本放进 cron每天凌晨跑一次0 2 * * * cd /opt/release-mirror GH_TOKENxxx python3 sync_releases.py /var/log/release-mirror.log 21下载过程中请求会自动跟随重定向GitHub 的 Release 下载 URL 最终会跳到对象存储requests 默认行为就是跟随的这里无需额外处理。4.3 索引页和权限控制文件都下载到/data/release之后最简单的方式是 nginx 直接把这个目录暴露成静态站点并开启 autoindexserver { listen 443 ssl; server_name mirror.example.com; root /data/release; autoindex on; autoindex_exact_size off; location / { auth_basic Restricted; auth_basic_user_file /etc/nginx/.htpasswd; } }团队访问https://mirror.example.com/kubernetes/kubernetes/v1.30.0/就能看到文件列表直接点击下载。加 Basic Auth 是为了防止目录暴露到公网被陌生人消耗流量如果你只在公司内网使用也可以把 location 里的allow和deny改成只允许内网网段。4.4 静态分发适合什么场景我观察下来静态分发最受欢迎的场景是给 CI 构建提供固定版本依赖。比如公司内部要统一使用某个版本的 kubectl 或 helm与其让每台构建机临时去 GitHub 拉不如让运维把校验好的二进制放到内网地址流水线里直接写死内网 URL。版本升级也就是改一下脚本里的仓库清单确定性强还能避免公网波动影响发布流程。5. 通用运维事项不然后面全是坑5.1 存储与带宽估算镜像站最容易被低估的是存储和带宽。仓库同步模式下每个仓库的 .git 体积会随历史增长一个活跃仓库轻松到 1~2GB。Release 资产更夸张单个安装包几百 MB 很常见。我建议在规划目录时预留两倍以上缓冲比如你打算镜像 100 个仓库先按每个 1GB 算再留 100GB 余量磁盘至少 300GB 起步。带宽估算有个简单公式单日预期下载量除以白天可用时长再乘一个高峰冗余系数。假设团队每天通过镜像下载 20GB 文件主要发生在工作日的 6 小时内平均速度就是20GB / 6h ≈ 0.9MB/s听起来不高但高峰时刻可能是平均值的十倍所以出口带宽至少按 200Mbps 规划比较稳。内网服务器之间走万兆没压力真正要留意的是如果镜像站放在公网云服务器带宽很容易被打满。5.2 监控与告警镜像站最怕两件事磁盘满了和同步失败了。磁盘满会导致 Gitea 无法写入、缓存代理无法存储新文件同步失败会让内网镜像持续落后用户拉到的代码越来越旧。我用的是最简单的监控组合Prometheus 加 node_exporter 盯磁盘在告警规则里配置使用率超过 80% 就通知。另外写一个定时脚本检查 Gitea 最近的同步日志find /data/gitea/log -type f -mmin -30 | xargs grep -i mirror | grep -i error如果有错误输出就报警。下载代理进程存活也可以直接用 systemd 管理服务重启策略设为always再配合 uptime 监控基本够用。5.3 安全与合规镜像站是一个内部基础设施但它面向 GitHub 的数据做二次分发还是有几个安全底线要守住。只镜像公开仓库并且优先选择可明确看到开源许可证的项目。镜像站本质上分发的是他人项目不做侵权分发是底线。页面上保留指向原仓库的地址方便使用者溯源。不要轻易把镜像站暴露到公网。如果一定要公网对外开放至少加 Basic Auth 或 IP 白名单否则你的服务器带宽会变成别人的免费下载节点遇到恶意刷流量会造成大额账单。定期更新 HTTPS 证书。Git 客户端和下载工具对证书校验很严格证书过期会导致 clone 直接失败。用 certbot 自动续期是最省事的路别手动维护。5.4 备份与恢复镜像站的数据虽然大多是从 GitHub 重新拉取的内容但重新拉一遍的成本依然不低尤其是大仓库和大量 Release 文件。备份策略我认为不需要太复杂把下面的目录做增量备份即可/data/gitea/ # Gitea 的仓库和数据 /data/github-mirror # 缓存代理的缓存文件 /data/release # 静态分发目录用 restic 备份是一个不错的选择它做增量备份高效、支持对象存储和本地目录恢复时直接把文件还原回去。恢复之后如果 Gitea 服务起不来可以先检查数据目录权限和 SQLite 文件是否完整大概率没什么问题。6. 常见问题速查现象可能原因解决办法Gitea 镜像同步一直卡住首次同步数据量大或 GitHub Token 失效查看后台任务日志更新 Token点手动同步clone 内网仓库时提示 403匿名访问频率受限在 Gitea 中创建用户用账号密码或 SSH Key 访问镜像仓库里 LFS 文件为空pull mirror 不自动同步 LFS单独用git lfs fetch --all同步 LFS 文件下载代理返回 502重定向次数超限或目标域名不在白名单检查是否新出现了 GitHub 对象存储域名加入白名单下载到的文件是 HTML 而不是压缩包请求被 GitHub 重定向到了登录页或限制页检查 URL 是否包含私有仓库路径代理服务只应处理公开内容缓存目录越来越大没有设置淘汰策略加find ... -mtime 7 -delete定时任务静态分发目录权限混乱脚本运行用户和 nginx 用户不一致统一用同一个系统用户运行文件权限设为 644证书过期导致下载失败忘记自动续期配置 certbot renew 定时任务或改用 DNS 验证方式团队反映还是慢请求没走镜像域名直连了源站检查 cli 配置或文档里的下载链接确认前缀已替换7. 我的实际体会这三类镜像方案我自己都搭过也都在真实团队里跑过。我个人的落地顺序是先上 Gitea 仓库同步把代码 clone 的稳定性问题解决掉等团队开始频繁下载 Release 文件再补一个下载缓存代理等到有固定版本需要统一管控时才把常驻资产挪到静态分发目录。个人使用的话仓库同步其实是性价比最高的第一站Gitea 装好、点几下鼠标、配好磁盘告警就能解决大半问题。下载缓存代理虽然要写点代码但它能同时服务多个仓库的 Release 文件对 CI 体系的稳定性帮助最明显。最后一个小建议是所有路径里的访问地址都直接用 HTTPS 域名别用 IP 加端口后期一切扩展都方便。
企业数字化 ERP 产品动态
相关推荐
第259篇_连锁超市会员折扣与促销海报采集 【Python爬虫实战】第259篇:会员价满减券海报OCR一把梭——连锁超市会员折扣与促销海报采集实战 所属专栏:【Python爬虫实战】从零到企业级爬虫工程师(CSDN 付费专栏) 本篇篇目:第 259 篇(垂直行业爬虫本地零售专场) 难度等级:中级,接触过图片下载与正则即可 阅读时长… · 2026/9/26 13:26:01
LeetCode 1004:滑动窗口与双指针巧解最大连续1的个数 1. 题目解读与核心思路 先说结论:LeetCode 1004. Max Consecutive Ones III(最大连续1的个数 III)是一道非常经典的滑动窗口题目,也是面试中高频出现的“变种双指针”问题。题目本身不复杂,但很多人在第一次做的时候会… · 2026/9/26 13:26:01
MySQL 综合应用实战:用 Python+Html 搭一套带 TaoToken 配置的 Flask 数据看板 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 13:25:55
全栈工程师项目练习记录:用 TaoToken 统一 Key 打通前后端联调配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 14:03:47
MiniMax-H3 模型全家桶详解:ComfyUI 三分钟部署与常见坑位排查 1. 项目概述与部署思路1.1 MiniMax-H3 到底是什么,为什么值得关注我是在一次本地工作流改造里第一次接触 MiniMax-H3 的,当时朋友丢给我一个压缩包,说"你把这个跑起来看看",我打开一看,里面整整一组模型文件… · 2026/9/26 14:03:41
Spring Boot健康饮食系统源码拆解:数据库设计与推荐算法落地 拿到一个Spring Boot智能健康饮食系统的源码包(编号05961),第一件事别急着跑起来,先花十分钟把这套东西的结构和设计意图摸清楚。这种系统在课程设计、毕业设计里出镜率极高,但绝大多数人只会照着README把项目启动&… · 2026/9/26 14:03:41
本地n8n添加图片完全指南:二进制、Docker与自动化流程实战 本地跑 n8n 的人,大概率迟早会卡在同一个问题上:我的工作流里需要一张图片,但找了半天没找到“上传图片”按钮。这个困惑我一开始也有,因为 n8n 的界面布局跟普通软件不太一样,图片不是贴上去的,而是作为数… · 2026/9/26 14:03:41
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46