重构 DevStats:用 EdgeOne Makers 与 AI Skills 打造开发者画像与辣评生成器

本文最后更新于 2026年7月8日 晚上

DevStats 是我做的一个开发者统计卡片与 AI 画像服务,脱胎自开源项目 github-readme-stats,用 Go 重写了渲染引擎,迁移到 EdgeOne Makers 托管,新增了 CNB 平台数据源,并融入了 AI 锐评和卡片推荐功能后改名为 DevStats。

最近对它做了一次全面重构,这篇文章记录整个过程,按几条主线展开:

  1. DevStats 从何而来
  2. “辣评”灵感来自哪里,浏览器抓取在其中扮演了什么角色
  3. 各个关键链路的构建思路与最终效果

生产地址:

DevStats 的来历

原项目

故事的起点是 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 卡片渲染,冷启动更低,并发性能更好,彻底告别了 Vercel 免费额度的限制和频繁不可用的尴尬
  • EdgeOne Makers 托管:不用自己维护服务器,KV 存储、边缘 CDN、环境变量管理一体化
  • CNB 数据源:在原有接口加入 platform=cnb 参数,对 CNB 平台做专属数据抓取与渲染适配

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

升级改名

后来给项目加入了 AI Stats Agent:能对开发者画像进行多维评分、毒舌吐槽,并推荐最适合的卡片配方。功能扩展后,github-readme-stats 这个名字已经不能准确描述项目,于是升级改名为 **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 等接口,调用大模型生成开发者画像和辣评

最后的开发者画像页

EdgeOne Makers Skills

现在 AI Coding 已经是主流开发方式,越来越多的工具在探索如何让大模型更稳定、更有针对性地完成任务。其中比较公认的一个方向就是 Skills:通过预定义的指令集、工具清单和知识库引用,把”大模型能做什么、怎么做”规范化地表达出来,而不是每次靠临场发挥。

让我惊喜的是,EdgeOne Makers 平台本身就内置了对 Skills 的支持:开发者可以在项目里直接定义 Skills 文件,平台会自动识别并在 Agent 运行时注入对应的指令与工具,不需要自己拼接 Prompt 或手动管理工具注册逻辑。

为什么需要 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 最有趣的部分——对开发者画像的”辣评”——灵感源自开源项目 hikariming/ghfind(在线体验:ghfind.com)。

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

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

浏览器抓取

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

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

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

  • 某个仓库命名为 testfinal-final,里面只有两个空文件,却留下 20 次 Commit
  • Profile README 声称”全栈架构师、K8s 专家”,但公开仓库 95% 是纯 HTML/CSS 练习
  • Commit 时间大量集中在凌晨 2-4 点或周末,工作日一片空白
  • 大量 Fork 仓库堆砌,原创项目稀少

模型就有足够的弹药生成真正有杀伤力的评价了。

三段式辣评

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

举个例子,对开源祖师爷 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。”

实际跑完后的效果

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

前端组件化

原版 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

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

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

画像分享页

原来没有独立分享链接。访客想看别人的报告必须重新触发一次 AI 评估,白白消耗大量 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 资源。

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 渲染的

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

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 → 生成报告”),但看不到任何工具调用的内部细节。

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 分析。

直接展示缓存的画像页面

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 这一组组合下来,模型的辣评准确度比单纯给它一个用户名高出了不止一个量级。

DevStats 的分享和代码都是公开的:

如果你也想用 EdgeOne Makers 搭一个类似的 AI 全栈项目,欢迎参考这个仓库的结构,也欢迎去仓库提 Issue 或者 PR 一起完善。



重构 DevStats:用 EdgeOne Makers 与 AI Skills 打造开发者画像与辣评生成器
https://www.mintimate.cn/2026/07/08/devStatsRefactor/
作者
Mintimate
发布于
2026年7月8日
许可协议