首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从 Git 贡献记录到人设画像报告:AI Stats Agent 的一次全栈实践

从 Git 贡献记录到人设画像报告:AI Stats Agent 的一次全栈实践

原创
作者头像
Mintimate
发布2026-07-09 21:44:03
发布2026-07-09 21:44:03
600
举报
文章被收录于专栏:Mintimate's BlogMintimate's Blog

DevStats 是我做的一个开发者统计卡片与 AI 画像服务。它最早只是 github-readme-stats 的 Go 重写版:输入用户名,吐一张 SVG 卡片,安静地躺在 GitHub Profile 里。

但一路做下来,它变成了一个更完整的开发者画像工坊:既能生成 GitHub / CNB 统计卡片,也能翻你的 Profile README、公开仓库、语言分布和贡献记录,然后给出一份带评分、标签、卡片配方和毒舌吐槽的开发者画像。

说人话就是:以前它像一个认真盖章的成绩单打印机;现在它会先查档案,再端着咖啡说:“来,解释一下你这个 final-final-v2 仓库。”

这也是我最想做出来的反差:用户以为自己只是来生成一张卡片,结果 Agent 已经开始翻公开仓库、看 README、读贡献记录了。

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

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

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

生产地址:

从卡片到画像

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

DevStats AI 分析台
DevStats AI 分析台

DevStats 就是这样。

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

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

原项目

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

代码语言:markdown
复制
![GitHub Stats](https://github-readme-stats.vercel.app/api?username=Mintimate&show_icons=true)

效果就是一张包含你的 Star 数、Commit 数、PR 数的动态 SVG 卡片,放在 GitHub Profile 里非常好看。但它使用 JavaScript 实现,托管在 Vercel 上。由于后来 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 的自建版本
基于 Go 并托管在 EdgeOne Makers 的自建版本

升级改名

后来给项目加入了 AI Stats Agent:能对开发者画像进行多维评分、毒舌吐槽,并推荐最适合的卡片配方。

到这一步,github-readme-stats 这个名字已经有点装不下它了。它不再只是 GitHub 卡片,也不再只是 README 装饰;它更像一个开发者信息的“编译器”:把公开资料编译成画像,把画像编译成卡片,把卡片和吐槽一起编译成可分享的社交素材。

于是项目升级改名为 dev-stats

项目发展
项目发展
从 github-readme-stats 到 dev-stats
从 github-readme-stats 到 dev-stats

整体架构

当前 DevStats 的技术架构分两层:

DevStats 的技术架构分两层
DevStats 的技术架构分两层
  • 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 的工作流被设计成一条小流水线:

代码语言:txt
复制
抓资料 -> 做画像 -> 出配方 -> 写 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,评估链路就变成:

评估链路
评估链路

AI 不再是凭空猜测,而是带着真实证据来"拷打"用户。

这也是 DevStats 趣味感的来源:它不是随机生成段子,而是把公开资料里的细节翻出来,再用一种更有戏剧性的语气讲给你听。被夸的时候会觉得“它懂我”,被吐槽的时候会觉得“它怎么连这个都看见了”。

辣评的灵感

DevStats 最有趣的部分——对开发者画像的"辣评"——灵感源自开源项目 hikariming/ghfind(在线体验:ghfind.com)。

ghfind 的核心魅力在于:它不只客观地统计 Star 数和 Commit 频率,还会用大模型生成带有毒舌色彩的开发者评价,让枯燥的数字变得有趣。

DevStats 的 AI Stats Agent 在这个思路上继续延伸:通过 browser_fetch 和 API 抓取大量细节数据,让模型生产出能真正"刺痛灵魂"的评价。

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

  1. 有证据:吐槽要能对应到公开资料,比如仓库、README、语言分布、贡献项目。
  2. 有分寸:攻击代码和门面,不攻击身份、地域、性别等敏感属性。
  3. 有反差:先认真分析,再突然来一句“这个仓库命名方式很像凌晨三点的临时救火现场”,读者才会笑。
DevStats 独立画像页
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,先看这个人到底解决了什么问题
别只数 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 的毒舌画像片段
tiann 的毒舌画像片段

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

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

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

yyx990803 的开发者画像
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 开发者画像
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 开发者画像
Mintimate 的 CNB 开发者画像

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

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

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

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

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

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

Anye 的 CNB 开发者画像
Anye 的 CNB 开发者画像

前端组件化

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

这次我把它彻底拆分:

代码语言:txt
复制
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 你先歇着
访客点开分享链接,Blob:我来,Token 你先歇着

这次新增了两个设计:

  • 只读接口 agents/profile.ts:直接从 EdgeOne Makers Blob 存储读取已有的画像缓存,不触发任何 LLM 调用,响应时间从 10+ 秒缩短到 2s 以内。
  • 边缘路由中间件 middleware.js:DevStats 是 SPA,直接刷新 /u/github/Mintimate 会 404。用 EdgeOne Makers 的 middleware 在边缘侧做透明重写:
代码语言:javascript
复制
// 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。用户看到的不只是结论,还能看到它是怎么被生产出来的。

分享页上也保留了“构建产物”的感觉
分享页上也保留了“构建产物”的感觉

Go 渲染引擎

DevStats 的 SVG 卡片渲染由 Go Cloud Functions 承担,在高并发下存在两个隐藏问题。

Go 渲染引擎前端
Go 渲染引擎前端

切片竞态

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

代码语言:go
复制
// 修复 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(路径段),防止特殊字符篡改后端请求语义:

代码语言:go
复制
// 防止特殊字符通过 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 里构建了三道防线:

关卡一:标签白名单

代码语言:typescript
复制
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 注入:

代码语言:typescript
复制
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: 伪协议:

代码语言:typescript
复制
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 上面
SSE 渲染到 HTML 上面

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

代码语言:typescript
复制
// 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):

代码语言:typescript
复制
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 存储做的缓存
EdgeOne Makers 后台使用 Blob 存储做的缓存

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

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

代码语言:typescript
复制
// 精确命中失败后,遍历 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 限流:

代码语言:typescript
复制
// 从环境变量中轮询可用的 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 真的该重写了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从卡片到画像
    • 原项目
    • 自建初版
    • 升级改名
    • 整体架构
  • EdgeOne Makers Skills
    • 为什么需要 Skills
  • 辣评的灵感
    • 浏览器抓取
    • 三段式辣评
    • 几个典型 Case
  • 前端组件化
  • 画像分享页
  • Go 渲染引擎
    • 切片竞态
    • 输入转义防注入
  • 安全加固
    • Markdown 渲染防 XSS
    • SSE 事件脱敏
  • Blob 缓存与 Token 轮转
    • Blob 缓存策略
    • 多 Token 轮转
  • 应用后的效果
  • END
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档