从 Git 贡献记录到人设画像报告:AI Stats Agent 的一次全栈实践
本文最后更新于 2026年7月9日 下午
DevStats 是我做的一个开发者统计卡片与 AI 画像服务。它最早只是 github-readme-stats 的 Go 重写版:输入用户名,吐一张 SVG 卡片,安静地躺在 GitHub Profile 里。
但一路做下来,它变成了一个更完整的开发者画像工坊:既能生成 GitHub / CNB 统计卡片,也能翻你的 Profile README、公开仓库、语言分布和贡献记录,然后给出一份带评分、标签、卡片配方和毒舌吐槽的开发者画像。
说人话就是:以前它像一个认真盖章的成绩单打印机;现在它会先查档案,再端着咖啡说:“来,解释一下你这个 final-final-v2 仓库。”
这也是我最想做出来的反差:用户以为自己只是来生成一张卡片,结果 Agent 已经开始翻公开仓库、看 README、读贡献记录了。

这篇文章不只记录“改了哪些代码”,也想聊聊这几个问题:
- 一个 README 卡片工具,为什么会演进成完整产品
- AI 辣评到底怎样从玩笑变成可复用的 Agent 链路
- 不同类型开发者画像,应该怎么被评分和表达
- 为什么要做独立画像页、缓存回放和分享传播
- 前端、Go 渲染引擎、安全边界分别踩了哪些坑
生产地址:
- DevStats: https://dev-stats.mintimate.cn
- GitHub 仓库: https://github.com/Mintimate/dev-stats
从卡片到画像
很多工具的演进,不是因为一开始就想做大,而是因为一个小功能用着用着,突然冒出了“它其实还能更好玩”的新可能。

DevStats 就是这样。
最早我只是想解决两个很现实的问题:原版服务偶发不可用、国内访问不够稳定。但当基础能力稳定下来以后,我发现单纯的统计卡片还是太“乖”了。Star、Commit、PR 这些数字当然有用,可它们只是骨架;真正让人愿意转发的,是“这个开发者到底是什么人”。
于是这次实践的方向慢慢变成了:保留卡片工具的实用性,再给它装一个会读材料、会吐槽、会出方案的 Agent。
原项目
故事的起点是 anuraghazra/github-readme-stats,一个在 GitHub 社区广泛使用的开发者卡片生成工具。它通过简单的 URL 参数,让任何人都能在 README 里嵌入自己的 GitHub 数据卡片:
1 | |
效果就是一张包含你的 Star 数、Commit 数、PR 数的动态 SVG 卡片,放在 GitHub Profile 里非常好看。但它使用 JavaScript 实现,托管在 Vercel 上。由于后来 Vercel 对开源项目的免费额度大幅收紧,导致原项目的公开服务经常因超出限制而偶发性不可用。

自建初版
正是因为原版服务偶发性不可用,加上国内访问较慢,我决定参考该项目,用 Go 语言重写了卡片渲染引擎并自建服务,托管在 EdgeOne Makers 上,同时新增了对 CNB 平台的数据源支持——原项目只支持 GitHub,CNB 用户没有等价的卡片工具。
这个版本以 Mintimate/github-readme-stats 的名字发布:
- Go 渲染引擎:用 Go Cloud Functions 实现 SVG 卡片渲染。这个服务最核心的诉求是“快、稳、便宜”,所以 Go 的冷启动和并发模型非常适合。
- EdgeOne Makers 托管:不用自己维护服务器,KV 存储、边缘 CDN、环境变量管理一体化。对这类静态页面 + Serverless API 的项目来说,体验很丝滑。
- CNB 数据源:在原有接口加入
platform=cnb参数,对 CNB 平台做专属数据抓取与渲染适配。GitHub 用户有漂亮门面,CNB 用户也应该有。

升级改名
后来给项目加入了 AI Stats Agent:能对开发者画像进行多维评分、毒舌吐槽,并推荐最适合的卡片配方。
到这一步,github-readme-stats 这个名字已经有点装不下它了。它不再只是 GitHub 卡片,也不再只是 README 装饰;它更像一个开发者信息的“编译器”:把公开资料编译成画像,把画像编译成卡片,把卡片和吐槽一起编译成可分享的社交素材。
于是项目升级改名为 **dev-stats**。
flowchart LR
Ref["参考\nanuraghazra/github-readme-stats\nJS / Vercel"]
V1["Mintimate/github-readme-stats\nGo 重写 / EdgeOne Makers / CNB 支持"]
V2["Mintimate/dev-stats\n新增 AI 锐评 + 卡片推荐"]
Ref -->|参考实现| V1
V1 -->|升级改名| V2
classDef ref fill:#f8fafc,stroke:#64748b,stroke-width:2px,color:#334155
classDef v1 fill:#fff7ed,stroke:#f59e0b,stroke-width:2px,color:#7c2d12
classDef v2 fill:#eef2ff,stroke:#4f46e5,stroke-width:3px,color:#312e81
class Ref ref
class V1 v1
class V2 v2

整体架构
当前 DevStats 的技术架构分两层:
flowchart LR
User["👤 访客 / 开发者"] --> Web["🎨 React 前端"]
Web --> GoEngine["🚀 Go 卡片渲染引擎"]
Web --> NodeAgent["🤖 Node.js AI Stats Agent"]
GoEngine --> API["GitHub / CNB API"]
NodeAgent --> KV[("📦 EdgeOne KV/Blob")]
NodeAgent --> API
classDef user fill:#fff7ed,stroke:#f59e0b,stroke-width:2px,color:#7c2d12
classDef ui fill:#eef2ff,stroke:#4f46e5,stroke-width:2px,color:#312e81
classDef go fill:#ecfeff,stroke:#0891b2,stroke-width:2px,color:#164e63
classDef agent fill:#f0fdf4,stroke:#16a34a,stroke-width:2px,color:#14532d
classDef infra fill:#f8fafc,stroke:#64748b,stroke-width:2px,color:#334155
class User user
class Web ui
class GoEngine go
class NodeAgent agent
class API,KV infra
- Go 渲染引擎:处理
/api、/api/top-langs、/api/pin等 SVG 卡片接口,负责“稳定出图”。它兼容原项目常用参数,加platform=cnb即可切到 CNB 数据源。 - Node.js AI Stats Agent:处理
/agents/chat、/agents/leaderboard、/agents/profile等接口,负责“读材料、做判断、写人话”。它调用大模型生成开发者画像、辣评、README 草稿和卡片推荐。

这也是我这次实践里最明确的一条边界:卡片渲染要像基础设施一样无聊可靠,AI 分析可以尽量有戏。
前者不能抽风,后者必须有记忆点。
EdgeOne Makers Skills
现在 AI Coding 已经是主流开发方式,越来越多的工具在探索如何让大模型更稳定、更有针对性地完成任务。其中比较公认的一个方向就是 Skills:通过预定义的指令集、工具清单和知识库引用,把”大模型能做什么、怎么做”规范化地表达出来,而不是每次靠临场发挥。
让我惊喜的是,EdgeOne Makers 平台本身就内置了对 Skills 的支持:开发者可以在项目里直接定义 Skills 文件,平台会自动识别并在 Agent 运行时注入对应的指令与工具,不需要自己拼接 Prompt 或手动管理工具注册逻辑。
这对 DevStats 很关键。因为我不想做一个“输入用户名,模型自由发挥”的玩具;我想让它像一个有流程的审稿人:先查公开资料,再看用户自我介绍,接着整理证据,最后才开始夸、评、吐槽。
所以 Agent 的工作流被设计成一条小流水线:
1 | |
听起来像 CI,实际跑起来也很像 CI:每一步都有状态,每一步都有日志,失败了也知道卡在哪里。
为什么需要 Skills
纯粹的大语言模型就像”缸中之脑”:它掌握了海量的编程知识,但在被唤醒的那一刻,它无法感知真实世界——它不知道当前这个用户有多少 Star,也无法看到他的 Profile README 写了什么。
如果只给它一个用户名让它自由发挥,它最多能输出几段听起来有模有样但毫无根据的套话。
为了让 AI 成为真正能”拷打”开发者的助手,我们需要给它装上双眼和双手,也就是 Skills(工具箱)。
在 DevStats 里,AI Stats Agent 运行在 EdgeOne Makers 的 Node.js 运行时中,通过 @openai/agents SDK 定义了一组 Tool Calling 工具:
| 工具名 | 作用 |
|---|---|
browser_fetch |
抓取目标 URL 的渲染后文本,读取主页公开信号 |
inspect_github_user |
调用 GitHub API,获取仓库列表、语言分布、近期活跃度 |
inspect_cnb_user |
获取 CNB 平台项目、组织和公开履历 |
fetch_github_profile_readme |
读取用户 Profile 仓库中的 README 文本 |
compose_stats_recipe |
将分析结果映射为具体的 Stats 卡片配方 |
compose_readme_draft |
输出完整的 README Markdown 草稿,含辣评三段 |
有了这些 Skills,评估链路就变成:
flowchart LR
Input["访客填入用户名并触发评估"]
GH["inspect_github_user<br/>拉取仓库与活跃数据"]
README["fetch_github_profile_readme<br/>读取自我介绍"]
Browser["browser_fetch<br/>读取主页公开信号"]
Draft["compose_readme_draft<br/>生成辣评三段式报告"]
Recipe["compose_stats_recipe<br/>推荐卡片配方"]
Output["SSE 流式输出"]
Input --> GH
Input --> README
GH --> Draft
README --> Draft
Browser -.可选.-> Draft
Draft --> Recipe
Recipe --> Output
classDef input fill:#fff7ed,stroke:#f59e0b,stroke-width:2px,color:#7c2d12
classDef tool fill:#f0fdf4,stroke:#16a34a,stroke-width:2px,color:#14532d
classDef output fill:#eef2ff,stroke:#4f46e5,stroke-width:2px,color:#312e81
class Input input
class GH,README,Browser,Draft,Recipe tool
class Output output
AI 不再是凭空猜测,而是带着真实证据来”拷打”用户。
这也是 DevStats 趣味感的来源:它不是随机生成段子,而是把公开资料里的细节翻出来,再用一种更有戏剧性的语气讲给你听。被夸的时候会觉得“它懂我”,被吐槽的时候会觉得“它怎么连这个都看见了”。
辣评的灵感
DevStats 最有趣的部分——对开发者画像的”辣评”——灵感源自开源项目 hikariming/ghfind(在线体验:ghfind.com)。
ghfind 的核心魅力在于:它不只客观地统计 Star 数和 Commit 频率,还会用大模型生成带有毒舌色彩的开发者评价,让枯燥的数字变得有趣。
DevStats 的 AI Stats Agent 在这个思路上继续延伸:通过 browser_fetch 和 API 抓取大量细节数据,让模型生产出能真正”刺痛灵魂”的评价。
这里我做了一个取舍:辣评不能只是为了刻薄而刻薄。它必须满足三个条件:
- 有证据:吐槽要能对应到公开资料,比如仓库、README、语言分布、贡献项目。
- 有分寸:攻击代码和门面,不攻击身份、地域、性别等敏感属性。
- 有反差:先认真分析,再突然来一句“这个仓库命名方式很像凌晨三点的临时救火现场”,读者才会笑。

浏览器抓取
精准辣评和泛泛而谈之间的差距,就在于 AI 拿到了多少真实、具体的细节。
单靠”用户有 10 个 Star,主用 Python”,模型只能产出类似”你的 Python 写得挺多的,继续加油哦”这种无聊废话。
这种评价的问题不是温柔,而是没有牙齿。它看起来像 AI 写的年终评语:安全、正确、没人想转发。
但当 browser_fetch 和 inspect_github_user 把这些细节塞进 AI 的上下文:
- 某个仓库命名为
test或final-final,里面只有两个空文件,却留下 20 次 Commit - Profile README 声称”全栈架构师、K8s 专家”,但公开仓库 95% 是纯 HTML/CSS 练习
- Commit 时间大量集中在凌晨 2-4 点或周末,工作日一片空白
- 大量 Fork 仓库堆砌,原创项目稀少
模型就有足够的弹药生成真正有杀伤力的评价了。
好玩的点也在这里:AI 的幽默不是凭空来的,而是来自“细节过于真实”。如果它只是说你“项目不够多”,你可能没感觉;但如果它说你“README 写得像融资 BP,仓库代码像周末补作业”,那就很难不沉默三秒。
三段式辣评
DevStats 的 AI 报告采用固定的三段式结构,这部分直接体现在 _prompt.ts 的系统提示词里:
| 段落 | 风格 | 内容 |
|---|---|---|
| promotional_summary | 促销文案 | 极尽夸张吹捧,用商业黑话包装用户特点 |
| objective_summary | 客观评价 | 基于 6 个维度评分(maturity / original_projects / contributions / influence / activity / community),各 1-20 分 |
| roast_summary | 毒舌吐槽 | 撕下吹捧,拿证据痛脚拷打,风格锋利幽默 |
综合评分不再只给一个冷冰冰的数字,而是顺手翻译成更像“开发者段位”的五档评级:
- 🔥 夯:
90–100,有代表作、有影响力,公开资料基本可以横着走。 - ⭐ 顶流:
80–89,已经能在某个技术圈层里稳定刷存在感。 - 💪 高级:
70–79,项目质量和活跃度都不错,只差一个更响亮的代表作。 - 😐 平庸:
50–69,能看出在写代码,但还没形成清晰的技术人设。 - 🌱 入门:
10–49,资料还比较薄,适合先把 README 和几个主项目认真养起来。
举个例子,对开源祖师爷 Linus Torvalds,其
roast_summary实际输出是这样的:“Linus Torvalds 的 GitHub 主页干净得像他写的 C 代码——没有 README,没有花里胡哨的 bio,连个星图都懒得搞。12 个仓库?对于一个拥有 31 万关注者的人来说,这仓库数量简直比他的脾气还稀少。但话说回来,人家发明了整个 Linux 和 Git,谁还需要 README 啊?"Talk is cheap, show me the code"——这话就是他说的,所以他的 README 就是一行代码:git push –force。”

几个典型 Case
为了避免 Agent 变成“见人就夸、见仓库就吹”的复读机,我专门挑了几组差异很大的样本来跑。下面这些结果来自当时的公开数据分析,随着用户仓库、Star、Fork、缓存时间变化,分数和文案也可能变化。
这里最容易踩的坑,是把“开发者价值”简化成 Star 数。Star 很重要,但它不是唯一答案;有些人的价值在生态推动,有些人在底层突破,有些人在工具被反复 Fork 和复制。

GitHub Case 1:tiann,Android 底层与 KernelSU 样本
tiann 的画像非常适合测试“硬核项目影响力”:KernelSU、epic、FreeReflection、hapi 这些项目都带有很强的技术标签,方向集中在 Android 底层、Hook、Root、AI 编程客户端。DevStats 给出的分数是 94.5,核心标签包括 #KernelSU大佬、#安卓Hook大神、#开源实干派。
这个 Case 的关键不在于“仓库多不多”,而在于能不能识别出项目的技术难度和社区影响力。比如 KernelSU 不是普通 Demo,而是 Android 内核级 Root 方案;epic 和 FreeReflection 也不是“写着玩的库”,而是长期被开发者引用的底层工具。吐槽也应该围绕项目气质来写,而不是泛泛地说“你很活跃”。

GitHub Case 2:yyx990803,顶级生态创建者样本
Evan You 这个样本几乎是“开源影响力上限测试”:Vue Core、Vite、Rolldown 都是生态级项目。DevStats 给出的分数是 95.5,标签包括 #Vue之父、#Vite引擎师、#前端界顶流。
这类账号很容易被写成空泛神话,所以 Agent 需要尽量把神话拆回证据:Vue 是框架生态,Vite 是构建工具生态,Rolldown 又把 Rust 构建链路带进前端工具。它不是“Star 很多所以厉害”,而是一个人长期推动了多个开发范式的迁移。

GitHub Case 3:sxzz,生态贡献型开发者样本
sxzz 和前两个样本不同:他的个人仓库 Star 没有 Vue / Vite 那种压倒性数字,但在 Vue、Vite、UnJS、Rolldown/tsdown 这些生态里有大量实际贡献。DevStats 给出的分数是 85.2,标签包括 #Rust生态探索者、#前端构建工具链工程师、#Vue.js核心贡献者。
这个 Case 用来测试 Agent 是否能区分“项目 Owner 型影响力”和“生态 Contributor 型影响力”。有些人不是靠一个超级仓库出圈,而是长期在关键项目里补链路、修工具、接生态接口。这样的贡献如果只看个人仓库 Star,很容易被低估。
切到 CNB 时,评分逻辑还要再换一副眼镜。CNB 的主要侧重点在云原生、云构建、流水线和团队协作,平台上的 Star/Follower 文化没有 GitHub 那么强,公开语料和大模型先验也没有 GitHub 生态那么充足。所以 CNB 用户的分数不能直接和 GitHub 用户横向硬比:Agent 需要更看重 Fork、提交、PR、Code Review、插件、示例项目和流水线痕迹,也更依赖实时抓取到的平台数据来补足模型训练里的盲区。
CNB Case 1:youkun,平台生态基建样本
CNB 侧最有意思的是 youkun:仓库数量、Fork 数、PR、Code Review、官方插件和示例项目都非常夸张。DevStats 给出的分数是 88.5,标签包括 #CNB生态基建、#氛围编程第一人、#云原生实干派。
这个样本适合测试 CNB 数据源和 GitHub 数据源的差异。GitHub 上我们常看 Star、Follower、PR;CNB 上还要看平台内 Fork、示例项目、插件、流水线和团队协作痕迹。youkun 的价值不一定体现在某一个单仓大爆,而是体现在大量项目对平台生态的填充。

CNB Case 2:Mintimate,工具型内容创作者样本
CNB 上的 Mintimate 分数是 76.5,代表项目包括 oh-my-rime、oh-my-rime-cli、tencent-cloud-beginner 等。这个样本的特点是:平台 Stars 总量不算夸张,但 Fork 和提交频率明显,说明它更像“工具被复制使用”,而不是“项目被围观点赞”。
这种画像要避免只用 Star 判断价值。很多工具型项目的真实影响力不是点赞,而是用户拿走配置、Fork 后直接改成自己的生产资料。Rime 配置、CLI、教程型仓库都属于这种类型:它们不是最炫的项目,但很容易成为别人工作流的一部分。

CNB Case 3:Anye,勤奋型环境搭建者样本
Anye 的画像分数是 62.5,代表项目集中在 Ollama 一键包、Docker 镜像加速、默认开发环境、模型部署工具等方向。它的单仓 Star 不算高,但 Fork 数、提交数和活跃天数很可观。
这个样本适合测试“勤奋”和“影响力”之间的差异:高频提交和大量仓库说明这个账号非常能折腾,但如果缺少一个足够集中的代表项目,综合评分就不会被简单拉满。Agent 的评价要承认它的实用价值,也要指出影响力还比较分散。
这 6 个 Case 放在一起,DevStats 的评分逻辑就更清楚了:顶级生态创建者、底层技术专家、生态贡献者、平台基建者、工具型内容创作者、勤奋型环境搭建者,不能用同一把尺子简单比较。真正要做的是:拿到公开证据后,判断这个人主要通过什么方式产生影响。
通过 Skills 抓取 + LLM 语义提炼的闭环,DevStats 把一个冷冰冰的数据卡片生成器变成了一个有社交属性、让开发者自发传播的小工具。
这也是我对“AI 功能”的一点思路:不要只把 AI 塞进按钮里,而是让它改变产品的传播方式。统计卡片是工具属性,辣评画像是社交属性;工具解决“我要用”,社交解决“我想发给别人看”。

前端组件化
原版 App.tsx 膨胀到 760 多行,把对话逻辑、卡片预览、排行榜、参数配置全部揉在一起。状态重绘范围大,改一处牵连全局,排查 bug 也很费力。
这次我把它彻底拆分:
1 | |
每个组件聚焦单一职责,状态只在最小必要范围内流动。
组件拆完以后,我顺手把前端气质也重新定了一遍:不要做成“配置表单”,而是做成“统计工坊”。
所以你会看到这些小心思:
- AI 分析台像任务控制台,左边派 Agent 出门,右边看排行榜和缓存结果。
- 手动配置面板故意做成代码编辑器风格,把参数选择写成
const WIDGET_MODULE = Widget.STATS这种伪代码。 - 按钮文案里保留一点开发者黑话,比如
git reset --hard、curl --open、Clipboard.copypasta,让它看起来不像普通后台表单。

这个方向其实很重要。DevStats 的受众不是普通表单用户,而是天天和 README、CI、Shell、配置文件打交道的开发者。给他们一个“像代码但不用写代码”的界面,会比堆一堆严肃输入框更贴合语境。
排行榜也顺手做了 UX 净化:去掉了大量 emoji(金牌、火箭、奖杯),在暗黑模式下不再显得嘈杂;加上每页 10 条的分页控件,避免数据增长后列表无限拉长。同时修复了用户名大小写冲突导致同一个人在 Blob 中产生两份数据的问题,现在统一以平台 API 返回的权威格式(profile.login / profile.username)为准。
这类小修小补看起来没有“AI 辣评”那么显眼,但它们决定了产品能不能被反复使用。梗只能吸引第一次点击,稳定和顺手才会让人回来。
画像分享页
原来没有独立分享链接。访客想看别人的报告必须重新触发一次 AI 评估,白白消耗大量 Token 和时间。

这个问题刚开始看只是“慢”,实际上会影响整个产品闭环:
- 用户自己跑一次评估,要等十几秒,这可以接受,因为等待本身就是体验的一部分。
- 朋友点分享链接还要再等十几秒,就很劝退;更糟的是,每次访问都在烧 Token。
- 如果分享页不稳定,辣评再好笑也传播不出去。
所以我把“跑分析”和“看结果”拆成了两个不同场景:前者可以慢一点、热闹一点;后者必须像打开一张图片一样快。
这一步本质上是在保护两件事:用户的耐心,以及我的 Token 余额。前者丢了,分享链路会断;后者丢了,项目会变成“每转发一次都心跳加速”的慈善行为。

这次新增了两个设计:
只读接口
agents/profile.ts:直接从 EdgeOne Makers Blob 存储读取已有的画像缓存,不触发任何 LLM 调用,响应时间从 10+ 秒缩短到 2s 以内。边缘路由中间件 middleware.js:DevStats 是 SPA,直接刷新
/u/github/Mintimate会 404。用 EdgeOne Makers 的 middleware 在边缘侧做透明重写:
1 | |

这样一来,分享链接可以直接传播,访客秒级打开,不消耗任何 AI 资源。
分享页上也保留了“构建产物”的感觉:顶部显示 src cache built,报告提示“这份报告是缓存快照”,AI 路径则像一条小型 Pipeline。用户看到的不只是结论,还能看到它是怎么被生产出来的。
flowchart LR
Share["分享链接<br/>/u/github/Mintimate"]
Mid["EdgeOne 边缘中间件<br/>middleware.js"]
SPA["React SPA<br/>index.html"]
Profile["agents/profile 只读接口"]
KV[("EdgeOne Blob 缓存")]
Page["画像页极速加载<br/>雷达图 + 报告 + Timeline"]
Share --> Mid
Mid -->|透明重写| SPA
SPA --> Profile
Profile --> KV
KV --> Page
classDef entry fill:#fff7ed,stroke:#f59e0b,stroke-width:2px,color:#7c2d12
classDef edge fill:#ecfeff,stroke:#0891b2,stroke-width:2px,color:#164e63
classDef kv fill:#f8fafc,stroke:#64748b,stroke-width:2px,color:#334155
classDef out fill:#f0fdf4,stroke:#16a34a,stroke-width:2px,color:#14532d
class Share entry
class Mid,SPA edge
class Profile,KV kv
class Page out
Go 渲染引擎
DevStats 的 SVG 卡片渲染由 Go Cloud Functions 承担,在高并发下存在两个隐藏问题。

切片竞态
exclude_repo 切片被多个 Goroutine 共享时,存在底层数组竞态风险。修复方案是在操作前强制 Deep Copy:
1 | |
输入转义防注入
对所有的 username 输入强制做 url.QueryEscape(API 查询参数)和 url.PathEscape(路径段),防止特殊字符篡改后端请求语义:
1 | |
安全加固
Markdown 渲染防 XSS
DevStats 需要渲染大模型输出的 Markdown 和用户公开 Profile 文本。恶意用户可能在自己的 GitHub README 里写入 <img src="x" onerror="alert(1)">,如果原封不动渲染,就会在访客浏览器里执行非法脚本。
这类项目很容易在这里翻车:你越想保留 GitHub README 的展示效果,就越容易放进不该放的 HTML。尤其是 AI 画像页是公开分享页,一旦 XSS 漏出去,攻击面就从“我自己的页面”变成了“每个点开分享链接的人”。
所以 markdown.ts 里构建了三道防线:
关卡一:标签白名单
1 | |
关卡二:属性白名单 stripDangerousAttrs
即使是允许的标签,也只保留特定安全属性(href, src, alt, class 等),强制剥离所有 on* 事件监听器和 style 注入:
1 | |
关卡三:URL 协议防线 isSafeUrl
对所有 href 和 src 的 URL,只放行安全协议,阻断 javascript: 和 data: 伪协议:
1 | |
SSE 事件脱敏
这是一个容易被忽视的安全问题。
AI Agent 在流式对话时,SSE 事件中包含工具调用的详细入参和输出(如 GitHub API 原始响应)。如果直接将这些原始 SSE 帧持久化到 Blob,再在公开的 /agents/profile 接口中吐给访客,任何人通过浏览器 Network 面板就能看到这些内部数据。

处理方式是在写入 Blob 前做脱敏处理:
1 | |
访客在画像页能看到完整的 AI 分析路径时间线(”→ 检查 Profile → 读取 README → 生成报告”),但看不到任何工具调用的内部细节。
这里的取舍是:公开页面需要的是可解释性,不是原始数据转储。
Timeline 负责让用户知道 Agent 做了什么;脱敏负责确保它不会把“怎么做、拿到了什么细节”全部暴露出去。
Blob 缓存与 Token 轮转
Blob 缓存策略
/agents/profile 接口的核心逻辑是优先读 Blob,只有缓存不存在时才触发完整 AI 评估。每次评估完成后,结果写入 Blob 并设置过期时间。缓存期内的分享访问完全不消耗 Token。
缓存的核心数据结构(agents/_cache.ts):
1 | |

缓存 key 按 analysis/v4/:platform/:username/:mode.json 的规则构造(L213-L221),CNB 用户名大小写敏感,GitHub 用户名统一小写,从根上避免大小写冲突导致的重复条目问题。
读取时先命中精确 key,命中失败再遍历 blob store 做兼容性模糊匹配(L39-L75):
1 | |
多 Token 轮转
GitHub 对匿名请求有严格限流(每小时 60 次)。DevStats 支持配置 GITHUB_TOKEN_1、GITHUB_TOKEN_2 等多个凭据,在排行榜全量重建等高频场景下轮换使用,避免触发 anonymous 限流:
1 | |
应用后的效果
最终,DevStats 的整体形态变成了:
| 层级 | 负责的事情 |
|---|---|
| Go 卡片渲染引擎 | SVG 卡片生成,支持 10+ 种卡片类型,兼容原项目所有参数,毫秒级响应 |
| Node.js AI Stats Agent | 开发者画像分析、三段式辣评、排行榜维护 |
| React 前端 | 卡片参数配置、SSE 流式回放、独立画像分享页 |
| EdgeOne Makers 边缘层 | 路由重写、Blob 缓存、CDN 加速、全球就近访问 |
打开 dev-stats.mintimate.cn 后,主要有两个入口:
Agent 面板(AI Stats Agent):填入 GitHub / CNB 用户名,点击预设按钮触发评估。AI 会依次调用 Skills 工具链,通过 SSE 实时展示”思考过程 → 工具调用 → 生成报告”的完整路径。评估完成后可以直接获取个人分享页链接。
手动配置面板:支持实时选择主题、布局、显示参数,防抖自动刷新卡片预览,并生成可直接粘贴的 Markdown / HTML 代码。同时兼容原 github-readme-stats 的所有参数,原有链接无需修改,加 platform=cnb 即可切到 CNB 数据源。
对于已经跑过 AI 评估的用户,他人打开其分享链接 /u/github/:username 时,由于边缘中间件 + Blob 只读缓存的组合,响应几乎是即时的,完全不需要重新等待 AI 分析。

现在它的使用路径大概是这样:
- 你打开首页,输入 GitHub 或 CNB 用户名。
- Agent 开始翻公开资料,像 CI 日志一样逐步输出分析过程。
- 结果生成后,你得到一份开发者画像、雷达图、标签、明星项目、客观评价和毒舌吐槽。
- 想认真经营 README,可以复制 AI 生成的草稿和卡片配方。
- 想发给朋友看,就直接甩分享页链接,让别人围观你的“开发者体检报告”。
从产品角度看,这次产品化真正打通的是两条路径:
- 工具路径:生成卡片、复制 Markdown、嵌入 README。
- 传播路径:生成画像、缓存结果、分享链接、排行榜沉淀。
前者让项目有用,后者让项目好玩。
END
这次实践最大的收获,是把从 github-readme-stats 迁移过来的这套基础,真正扩展成了一个有独立功能特色的全栈项目:
- 原项目 Vercel 调用限制的问题,用 EdgeOne Makers Go 渲染引擎从根上解决了
- CNB 数据源让国内开发者不再只能展示 GitHub 数据
- 边缘路由中间件解决了 SPA 分享链接 404 的问题
- Blob 只读缓存把重复 AI 评估的 Token 成本降到了零
- SSE 脱敏和 Markdown XSS 白名单把开放平台的安全边界补齐
关于 AI Skills 的使用体会:AI 能产出什么质量的结果,很大程度取决于你给了它多少真实的上下文。browser_fetch + inspect_github_user + fetch_github_profile_readme 这一组组合下来,模型的辣评准确度比单纯给它一个用户名高出了不止一个量级。
也顺便印证了我对 AI 产品的一点判断:AI 不一定要替用户完成一个严肃任务,它也可以把原本无聊的数据变成一个值得分享的故事。
DevStats 最早只是为了让 README 多一张卡片;现在它会认真看你的资料,给你打分,给你配卡,再礼貌地补上一刀。这个方向我挺喜欢,因为它既保留了工具的确定性,也留下了玩具的惊喜感。
DevStats 的分享和代码都是公开的:
如果你也想用 EdgeOne Makers 搭一个类似的 AI 全栈项目,欢迎参考这个仓库的结构,也欢迎去仓库提 Issue 或者 PR 一起完善。
当然,也可以先去跑一下自己的画像。万一被吐槽到了,不要急着生气,先看看是不是 README 真的该重写了。
