从 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、读贡献记录了。

我只是想生成一张统计卡片,AI:先把你的公开仓库看一遍

这篇文章不只记录“改了哪些代码”,也想聊聊这几个问题:

  1. 一个 README 卡片工具,为什么会演进成完整产品
  2. AI 辣评到底怎样从玩笑变成可复用的 Agent 链路
  3. 不同类型开发者画像,应该怎么被评分和表达
  4. 为什么要做独立画像页、缓存回放和分享传播
  5. 前端、Go 渲染引擎、安全边界分别踩了哪些坑

生产地址:

从卡片到画像

很多工具的演进,不是因为一开始就想做大,而是因为一个小功能用着用着,突然冒出了“它其实还能更好玩”的新可能。

DevStats AI 分析台

DevStats 就是这样。

最早我只是想解决两个很现实的问题:原版服务偶发不可用、国内访问不够稳定。但当基础能力稳定下来以后,我发现单纯的统计卡片还是太“乖”了。Star、Commit、PR 这些数字当然有用,可它们只是骨架;真正让人愿意转发的,是“这个开发者到底是什么人”。

于是这次实践的方向慢慢变成了:保留卡片工具的实用性,再给它装一个会读材料、会吐槽、会出方案的 Agent。

原项目

故事的起点是 anuraghazra/github-readme-stats,一个在 GitHub 社区广泛使用的开发者卡片生成工具。它通过简单的 URL 参数,让任何人都能在 README 里嵌入自己的 GitHub 数据卡片:

1
![GitHub Stats](https://github-readme-stats.vercel.app/api?username=Mintimate&show_icons=true)

效果就是一张包含你的 Star 数、Commit 数、PR 数的动态 SVG 卡片,放在 GitHub Profile 里非常好看。但它使用 JavaScript 实现,托管在 Vercel 上。由于后来 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 用户也应该有。

基于 Go 并托管在 EdgeOne Makers 的自建版本

升级改名

后来给项目加入了 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

从 github-readme-stats 到 dev-stats

整体架构

当前 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
抓资料 -> 做画像 -> 出配方 -> 写 README -> 存缓存 -> 可分享

听起来像 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 抓取大量细节数据,让模型生产出能真正”刺痛灵魂”的评价。

这里我做了一个取舍:辣评不能只是为了刻薄而刻薄。它必须满足三个条件:

  1. 有证据:吐槽要能对应到公开资料,比如仓库、README、语言分布、贡献项目。
  2. 有分寸:攻击代码和门面,不攻击身份、地域、性别等敏感属性。
  3. 有反差:先认真分析,再突然来一句“这个仓库命名方式很像凌晨三点的临时救火现场”,读者才会笑。

DevStats 独立画像页

浏览器抓取

精准辣评和泛泛而谈之间的差距,就在于 AI 拿到了多少真实、具体的细节。

单靠”用户有 10 个 Star,主用 Python”,模型只能产出类似”你的 Python 写得挺多的,继续加油哦”这种无聊废话。

这种评价的问题不是温柔,而是没有牙齿。它看起来像 AI 写的年终评语:安全、正确、没人想转发。

但当 browser_fetchinspect_github_user 把这些细节塞进 AI 的上下文:

  • 某个仓库命名为 testfinal-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 和复制。

别只数 Star,先看这个人到底解决了什么问题

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 也不是“写着玩的库”,而是长期被开发者引用的底层工具。吐槽也应该围绕项目气质来写,而不是泛泛地说“你很活跃”。

tiann 的毒舌画像片段

GitHub Case 2:yyx990803,顶级生态创建者样本

Evan You 这个样本几乎是“开源影响力上限测试”:Vue Core、Vite、Rolldown 都是生态级项目。DevStats 给出的分数是 95.5,标签包括 #Vue之父#Vite引擎师#前端界顶流

这类账号很容易被写成空泛神话,所以 Agent 需要尽量把神话拆回证据:Vue 是框架生态,Vite 是构建工具生态,Rolldown 又把 Rust 构建链路带进前端工具。它不是“Star 很多所以厉害”,而是一个人长期推动了多个开发范式的迁移。

yyx990803 的开发者画像

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 的价值不一定体现在某一个单仓大爆,而是体现在大量项目对平台生态的填充。

youkun 的 CNB 开发者画像

CNB Case 2:Mintimate,工具型内容创作者样本

CNB 上的 Mintimate 分数是 76.5,代表项目包括 oh-my-rime、oh-my-rime-cli、tencent-cloud-beginner 等。这个样本的特点是:平台 Stars 总量不算夸张,但 Fork 和提交频率明显,说明它更像“工具被复制使用”,而不是“项目被围观点赞”。

这种画像要避免只用 Star 判断价值。很多工具型项目的真实影响力不是点赞,而是用户拿走配置、Fork 后直接改成自己的生产资料。Rime 配置、CLI、教程型仓库都属于这种类型:它们不是最炫的项目,但很容易成为别人工作流的一部分。

Mintimate 的 CNB 开发者画像

CNB Case 3:Anye,勤奋型环境搭建者样本

Anye 的画像分数是 62.5,代表项目集中在 Ollama 一键包、Docker 镜像加速、默认开发环境、模型部署工具等方向。它的单仓 Star 不算高,但 Fork 数、提交数和活跃天数很可观。

这个样本适合测试“勤奋”和“影响力”之间的差异:高频提交和大量仓库说明这个账号非常能折腾,但如果缺少一个足够集中的代表项目,综合评分就不会被简单拉满。Agent 的评价要承认它的实用价值,也要指出影响力还比较分散。

这 6 个 Case 放在一起,DevStats 的评分逻辑就更清楚了:顶级生态创建者、底层技术专家、生态贡献者、平台基建者、工具型内容创作者、勤奋型环境搭建者,不能用同一把尺子简单比较。真正要做的是:拿到公开证据后,判断这个人主要通过什么方式产生影响。

通过 Skills 抓取 + LLM 语义提炼的闭环,DevStats 把一个冷冰冰的数据卡片生成器变成了一个有社交属性、让开发者自发传播的小工具。

这也是我对“AI 功能”的一点思路:不要只把 AI 塞进按钮里,而是让它改变产品的传播方式。统计卡片是工具属性,辣评画像是社交属性;工具解决“我要用”,社交解决“我想发给别人看”。

Anye 的 CNB 开发者画像

前端组件化

原版 App.tsx 膨胀到 760 多行,把对话逻辑、卡片预览、排行榜、参数配置全部揉在一起。状态重绘范围大,改一处牵连全局,排查 bug 也很费力。

这次我把它彻底拆分:

1
2
3
4
5
6
7
8
9
10
11
12
src/components/
├── TopBar.tsx # 页头导航与暗黑模式切换
├── PlatformSegment.tsx # GitHub / CNB 切换控件
├── AgentPanel.tsx # AI 预设任务按钮面板
├── AgentResultPanel.tsx # SSE 流式输出终端回放
├── ManualOptions.tsx # 卡片参数手动配置
├── PreviewPanel.tsx # 实时 SVG 卡片预览
├── RadarChart.tsx # 能力雷达图
├── ReadmeReport.tsx # 报告渲染卡片(首页+分享页共用)
├── ShareModal.tsx # 分享浮窗与防抖二维码
├── UserPage.tsx # 独立只读画像分享页
└── Footer.tsx

每个组件聚焦单一职责,状态只在最小必要范围内流动。

组件拆完以后,我顺手把前端气质也重新定了一遍:不要做成“配置表单”,而是做成“统计工坊”。

所以你会看到这些小心思:

  • AI 分析台像任务控制台,左边派 Agent 出门,右边看排行榜和缓存结果。
  • 手动配置面板故意做成代码编辑器风格,把参数选择写成 const WIDGET_MODULE = Widget.STATS 这种伪代码。
  • 按钮文案里保留一点开发者黑话,比如 git reset --hardcurl --openClipboard.copypasta,让它看起来不像普通后台表单。

手动配置参数编译器

这个方向其实很重要。DevStats 的受众不是普通表单用户,而是天天和 README、CI、Shell、配置文件打交道的开发者。给他们一个“像代码但不用写代码”的界面,会比堆一堆严肃输入框更贴合语境。

排行榜也顺手做了 UX 净化:去掉了大量 emoji(金牌、火箭、奖杯),在暗黑模式下不再显得嘈杂;加上每页 10 条的分页控件,避免数据增长后列表无限拉长。同时修复了用户名大小写冲突导致同一个人在 Blob 中产生两份数据的问题,现在统一以平台 API 返回的权威格式(profile.login / profile.username)为准。

这类小修小补看起来没有“AI 辣评”那么显眼,但它们决定了产品能不能被反复使用。梗只能吸引第一次点击,稳定和顺手才会让人回来。

画像分享页

原来没有独立分享链接。访客想看别人的报告必须重新触发一次 AI 评估,白白消耗大量 Token 和时间。

快速加载分享页面

这个问题刚开始看只是“慢”,实际上会影响整个产品闭环:

  • 用户自己跑一次评估,要等十几秒,这可以接受,因为等待本身就是体验的一部分。
  • 朋友点分享链接还要再等十几秒,就很劝退;更糟的是,每次访问都在烧 Token。
  • 如果分享页不稳定,辣评再好笑也传播不出去。

所以我把“跑分析”和“看结果”拆成了两个不同场景:前者可以慢一点、热闹一点;后者必须像打开一张图片一样快。

这一步本质上是在保护两件事:用户的耐心,以及我的 Token 余额。前者丢了,分享链路会断;后者丢了,项目会变成“每转发一次都心跳加速”的慈善行为。

访客点开分享链接,Blob:我来,Token 你先歇着

这次新增了两个设计:

  • 只读接口 agents/profile.ts:直接从 EdgeOne Makers Blob 存储读取已有的画像缓存,不触发任何 LLM 调用,响应时间从 10+ 秒缩短到 2s 以内。

  • 边缘路由中间件 middleware.js:DevStats 是 SPA,直接刷新 /u/github/Mintimate 会 404。用 EdgeOne Makers 的 middleware 在边缘侧做透明重写:

1
2
3
4
5
6
7
8
9
// middleware.js
export default async function middleware(request, context) {
const url = new URL(request.url);
if (url.pathname.startsWith('/u/')) {
// 透明重写到 index.html,浏览器 URL 不变,由 React Router 接管
return context.rewrite('/index.html');
}
return context.next();
}

独立分享页面加载中

这样一来,分享链接可以直接传播,访客秒级打开,不消耗任何 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 承担,在高并发下存在两个隐藏问题。

Go 渲染引擎前端

切片竞态

exclude_repo 切片被多个 Goroutine 共享时,存在底层数组竞态风险。修复方案是在操作前强制 Deep Copy:

1
2
3
4
5
6
// 修复 exclude_repo 切片被调用方复用时的底层数组竞态风险
var cleanExclude []string
if len(query.ExcludeRepos) > 0 {
cleanExclude = make([]string, len(query.ExcludeRepos))
copy(cleanExclude, query.ExcludeRepos)
}

输入转义防注入

对所有的 username 输入强制做 url.QueryEscape(API 查询参数)和 url.PathEscape(路径段),防止特殊字符篡改后端请求语义:

1
2
3
// 防止特殊字符通过 username 篡改 GitHub API 查询语义
escapedUser := url.QueryEscape(username)
commitQuery := fmt.Sprintf("author:%s", escapedUser)

安全加固

Markdown 渲染防 XSS

DevStats 需要渲染大模型输出的 Markdown 和用户公开 Profile 文本。恶意用户可能在自己的 GitHub README 里写入 <img src="x" onerror="alert(1)">,如果原封不动渲染,就会在访客浏览器里执行非法脚本。

这类项目很容易在这里翻车:你越想保留 GitHub README 的展示效果,就越容易放进不该放的 HTML。尤其是 AI 画像页是公开分享页,一旦 XSS 漏出去,攻击面就从“我自己的页面”变成了“每个点开分享链接的人”。

所以 markdown.ts 里构建了三道防线:

关卡一:标签白名单

1
2
3
4
5
const ALLOWED_TAGS = new Set([
"div", "p", "img", "a", "br", "span", "sub", "sup",
"h1", "h2", "h3", "hr", "details", "summary", "ul", "ol", "li",
]);
// 不在白名单的标签一律转义为纯文本显示

关卡二:属性白名单 stripDangerousAttrs

即使是允许的标签,也只保留特定安全属性(href, src, alt, class 等),强制剥离所有 on* 事件监听器和 style 注入:

1
2
3
4
5
6
7
8
function stripDangerousAttrs(tag: string): string {
const SAFE_ATTRS_BY_TAG: Record<string, string[]> = {
a: ["href", "target", "rel", "title"],
img: ["src", "alt", "title"],
div: ["align", "class"],
};
// 非白名单属性直接剥除,包括 onclick、onerror、style 等
}

关卡三:URL 协议防线 isSafeUrl

对所有 hrefsrc 的 URL,只放行安全协议,阻断 javascript:data: 伪协议:

1
2
3
4
function isSafeUrl(url: string) {
const trimmed = String(url || "").trim();
return /^(https?:\/\/|mailto:|\/|#|\.\/|\.\.\/)/i.test(trimmed);
}

SSE 事件脱敏

这是一个容易被忽视的安全问题。

AI Agent 在流式对话时,SSE 事件中包含工具调用的详细入参和输出(如 GitHub API 原始响应)。如果直接将这些原始 SSE 帧持久化到 Blob,再在公开的 /agents/profile 接口中吐给访客,任何人通过浏览器 Network 面板就能看到这些内部数据。

SSE 渲染到 HTML 上面

处理方式是在写入 Blob 前做脱敏处理:

1
2
3
4
5
6
7
8
9
10
// agents/_cache.ts
export function sanitizeEventsForPublicReplay(events: any[]) {
return events.map(e => {
if (e.type === "tool_call" || e.type === "tool_result") {
// 只保留工具名,供 Timeline 渲染,剥除所有入参和执行细节
return { type: e.type, name: e.name };
}
return e;
});
}

访客在画像页能看到完整的 AI 分析路径时间线(”→ 检查 Profile → 读取 README → 生成报告”),但看不到任何工具调用的内部细节。

这里的取舍是:公开页面需要的是可解释性,不是原始数据转储。

Timeline 负责让用户知道 Agent 做了什么;脱敏负责确保它不会把“怎么做、拿到了什么细节”全部暴露出去。

Blob 缓存与 Token 轮转

Blob 缓存策略

/agents/profile 接口的核心逻辑是优先读 Blob,只有缓存不存在时才触发完整 AI 评估。每次评估完成后,结果写入 Blob 并设置过期时间。缓存期内的分享访问完全不消耗 Token。

缓存的核心数据结构(agents/_cache.ts):

1
2
3
4
5
6
7
8
9
export const CACHE_STORE_NAME = 'stats-agent-analysis-cache';
export const CACHE_SCHEMA_VERSION = 'v4';
export const DEFAULT_ANALYSIS_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours

export interface CacheEntry {
cachedAt: number;
expiresAt?: number;
events: string[]; // 原始 SSE 事件行,脱敏后写入
}

EdgeOne Makers 后台使用 Blob 存储做的缓存

缓存 key 按 analysis/v4/:platform/:username/:mode.json 的规则构造(L213-L221),CNB 用户名大小写敏感,GitHub 用户名统一小写,从根上避免大小写冲突导致的重复条目问题。

读取时先命中精确 key,命中失败再遍历 blob store 做兼容性模糊匹配(L39-L75):

1
2
3
4
5
6
7
8
9
10
11
12
// 精确命中失败后,遍历 blob store 寻找大小写兼容的旧条目
const { blobs } = await store.list({ consistency: 'strong' });
const candidates = blobs
.map((blob: any) => blob.key)
.filter((key: string) => key !== currentKey)
.map((key: string) => ({ key, parsed: parseAnalysisCacheKey(key) }))
.filter(({ parsed }) =>
parsed &&
parsed.platform === safePlatform &&
parsed.mode === safeMode &&
parsed.username === safeUsername
);

多 Token 轮转

GitHub 对匿名请求有严格限流(每小时 60 次)。DevStats 支持配置 GITHUB_TOKEN_1GITHUB_TOKEN_2 等多个凭据,在排行榜全量重建等高频场景下轮换使用,避免触发 anonymous 限流:

1
2
3
4
5
6
7
8
9
10
// 从环境变量中轮询可用的 GitHub Token
export function getGitHubToken(
env?: Record<string, string | undefined>
): string | undefined {
for (let i = 1; i <= 10; i++) {
const token = env?.[`GITHUB_TOKEN_${i}`];
if (token) return token;
}
return env?.GITHUB_TOKEN;
}

应用后的效果

最终,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 分析。

直接展示缓存的画像页面

现在它的使用路径大概是这样:

  1. 你打开首页,输入 GitHub 或 CNB 用户名。
  2. Agent 开始翻公开资料,像 CI 日志一样逐步输出分析过程。
  3. 结果生成后,你得到一份开发者画像、雷达图、标签、明星项目、客观评价和毒舌吐槽。
  4. 想认真经营 README,可以复制 AI 生成的草稿和卡片配方。
  5. 想发给朋友看,就直接甩分享页链接,让别人围观你的“开发者体检报告”。

从产品角度看,这次产品化真正打通的是两条路径:

  • 工具路径:生成卡片、复制 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 真的该重写了。



从 Git 贡献记录到人设画像报告:AI Stats Agent 的一次全栈实践
https://www.mintimate.cn/2026/07/08/devStatsRefactor/
作者
Mintimate
发布于
2026年7月8日
许可协议