@@ -18,9 +18,9 @@ dotenv.config({ path: [".env.local", ".env"] });
1818import * as PrismaModule from "../generated/prisma/client.ts" ;
1919const PrismaClient =
2020 PrismaModule . default ?. PrismaClient || PrismaModule . PrismaClient ;
21- import pg from "pg" ;
22- const { Pool } = pg ;
2321import { PrismaPg } from "@prisma/adapter-pg" ;
22+ import pg from "pg" ;
23+ // 我们使用 node:fs/promises 的 fs 即可
2424
2525const __filename = fileURLToPath ( import . meta. url ) ;
2626const __dirname = path . dirname ( __filename ) ;
@@ -35,49 +35,140 @@ async function ensureParentDir(filePath) {
3535
3636async function main ( ) {
3737 if ( ! process . env . DATABASE_URL ) {
38- console . error ( "[generate-leaderboard] No DATABASE_URL found. Skipping." ) ;
38+ console . error (
39+ "[generate-leaderboard] 未找到 DATABASE_URL,跳过生成排行榜。 | No DATABASE_URL found. Skipping." ,
40+ ) ;
3941 process . exit ( 0 ) ;
4042 }
4143
44+ const rawData = await fs . readFile (
45+ path . join ( __dirname , "../.source/index.ts" ) ,
46+ "utf-8" ,
47+ ) ;
48+ const docsMap = { } ;
49+
50+ // 正则提取所有类似 { info: {"path":"...","absolutePath":"..."}, data: docs_0 } 的节点数据。
51+ // 在 .source/index.ts 文件底部有一行类似于:
52+ // export const docs = _runtime.docs<typeof _source.docs>([{ info: ... }, { info: ... }])
53+ //
54+ // 正则解析:
55+
56+ // - `/s`: 允许 `.` 匹配换行符,防备代码被格式化成多行
57+
58+ // - `export const docs.*=\s*.*?docs>\(\[`: 匹配由 Fumadocs 自动生成的固定代码开头,直到方括号 `[`
59+ const pagesInfoMatch = rawData . match (
60+ / e x p o r t c o n s t d o c s .* = \s * .* ?d o c s > \( \[ ( .* ?) \] \) / s,
61+ ) ;
62+
63+ // pagesInfoMatch[1] 代表我们在上方的正则中用括号 (.*?) 提取出来的具体内容(也就是那一大串 {info: ...} 数组字符串)
64+ if ( pagesInfoMatch && pagesInfoMatch [ 1 ] ) {
65+ const pagesRaw = pagesInfoMatch [ 1 ] ;
66+ // 我们利用简单解析获取所有的 path 和大致提取对应的导入行
67+ const pageItems = pagesRaw . split ( "}, {" ) ;
68+ for ( const item of pageItems ) {
69+ // - `(.*?)`: 捕获方括号内部的所有的对象内容(非贪婪匹配),这一部分就是所有文章的配置数组
70+ const pathMatch = item . match ( / " p a t h " : " ( .* ?) " / ) ;
71+ if ( pathMatch && pathMatch [ 1 ] ) {
72+ const docPath = pathMatch [ 1 ] ;
73+ let title = docPath . replace ( / \. m d x ? $ / , "" ) ;
74+ const url = `/docs/${ title } ` ;
75+
76+ let docIdFromFm = null ;
77+ // 为了获取确切的 title 和 docId,我们需要打开实际的文件获取 frontmatter,
78+ try {
79+ const absolutePathMatch = item . match ( / " a b s o l u t e P a t h " : " ( .* ?) " / ) ;
80+ if ( absolutePathMatch && absolutePathMatch [ 1 ] ) {
81+ const content = await fs . readFile ( absolutePathMatch [ 1 ] , "utf-8" ) ;
82+
83+ // 提取 title
84+ const titleMatch =
85+ content . match ( / ^ t i t l e : \s * (?: ' | " ) ? ( .* ?) (?: ' | " ) ? $ / m) ||
86+ content . match ( / ^ # \s + ( .* ) $ / m) ;
87+ if ( titleMatch && titleMatch [ 1 ] ) {
88+ title = titleMatch [ 1 ] . trim ( ) ;
89+ }
90+
91+ // 提取 docId
92+ const docIdMatch = content . match (
93+ / ^ d o c I d : \s * (?: ' | " ) ? ( .* ?) (?: ' | " ) ? $ / m,
94+ ) ;
95+ if ( docIdMatch && docIdMatch [ 1 ] ) {
96+ docIdFromFm = docIdMatch [ 1 ] . trim ( ) ;
97+ }
98+ }
99+ } catch ( e ) {
100+ console . error ( e ) ;
101+ }
102+
103+ // 优先使用 frontmatter 中的 docId 作为键(与数据库中存储的 CUID 对应)
104+ // 否则回退使用文件路径作为键
105+ const key = docIdFromFm || docPath . replace ( / \. m d x ? $ / , "" ) ;
106+ docsMap [ key ] = { title, url } ;
107+ }
108+ }
109+ }
110+
42111 const connectionString = `${ process . env . DATABASE_URL } ` ;
43- const pool = new Pool ( { connectionString } ) ;
112+ const pool = new pg . Pool ( { connectionString } ) ;
44113 const adapter = new PrismaPg ( pool ) ;
45114 const prisma = new PrismaClient ( { adapter } ) ;
46115
47116 console . log (
48- "[generate-leaderboard] Connecting to database to aggregate contributions..." ,
117+ "[generate-leaderboard] 连接数据库以聚合贡献数据... | Connecting to database to aggregate contributions..." ,
49118 ) ;
50119
51120 try {
52- // 聚合每一个 github_id 的总贡献量
53- const aggregations = await prisma . doc_contributors . groupBy ( {
54- by : [ "github_id" ] ,
55- _sum : {
56- contributions : true ,
57- } ,
58- } ) ;
121+ // 聚合每一个 github_id 的总贡献量和所有贡献的文章
122+ const allRecords = await prisma . doc_contributors . findMany ( ) ;
123+
124+ const grouped = { } ;
125+ for ( const record of allRecords ) {
126+ const gid = record . github_id . toString ( ) ;
127+ if ( ! grouped [ gid ] ) {
128+ grouped [ gid ] = {
129+ contributions : 0 ,
130+ docs : new Set ( ) ,
131+ } ;
132+ }
133+ grouped [ gid ] . contributions += record . contributions ;
134+ grouped [ gid ] . docs . add ( record . doc_id ) ;
135+ }
59136
60137 // 格式化输出榜单
61- const leaderboard = aggregations
62- . filter ( ( item ) => item . _sum . contributions && item . _sum . contributions > 0 )
63- . map ( ( item ) => {
64- const points = Number ( item . _sum . contributions ) * 10 ; // 每个 commit 暂定 10 分
65- const githubId = item . github_id . toString ( ) ;
138+ const leaderboard = Object . entries ( grouped )
139+ . filter ( ( [ id , data ] ) => data . contributions > 0 )
140+ . map ( ( [ id , data ] ) => {
141+ const points = data . contributions * 10 ; // 每个 commit 暂定 10 分
142+ const githubId = id ;
143+
144+ const contributedDocsInfo = Array . from ( data . docs ) . map ( ( dbDocId ) => {
145+ // dbDocId 对应数据库里的 CUID (如 psc0xf6oa1m7g8s9wfwiojkf)
146+ // 或之前的路径 (如 path/to/doc.mdx 需要去除后缀匹配)
147+ const key = dbDocId . replace ( / \. m d x ? $ / , "" ) ;
148+ const mappedInfo = docsMap [ key ] ;
149+
150+ return {
151+ id : dbDocId ,
152+ title : mappedInfo ? mappedInfo . title : dbDocId , // 若没有匹配到页面,回退显示 docId
153+ url : mappedInfo ? mappedInfo . url : `/docs/${ key } ` ,
154+ } ;
155+ } ) ;
66156
67157 return {
68158 id : githubId ,
69159 // 暂时没有办法直接从表中获取 login_name,我们就以此格式保留并前端展示默认占位符或者使用 github username API 换取 (如果需要完全离线,则只展示ID)
70160 name : `GitHub User ${ githubId } ` ,
71161 points : points ,
72- commits : item . _sum . contributions ,
162+ commits : data . contributions ,
73163 avatarUrl : `https://avatars.githubusercontent.com/u/${ githubId } ` ,
164+ contributedDocs : contributedDocsInfo ,
74165 } ;
75166 } )
76167 . sort ( ( a , b ) => b . points - a . points ) ;
77168
78169 // 为前排用户附带一下 Github API 拉取详细昵称信息,由于只是少数人(例如前100),可以接受构建时拉取。
79170 console . log (
80- `[generate-leaderboard] Aggregated ${ leaderboard . length } users. Annotating top 100 with Github API...` ,
171+ `[generate-leaderboard] 已聚合 ${ leaderboard . length } 名用户,正在通过 GitHub API 获取前 100 名的详细信息... | Aggregated ${ leaderboard . length } users. Annotating top 100 with Github API...` ,
81172 ) ;
82173
83174 // 我们在此取前 100 名抓取 Github Name 防止 Rate Limit
@@ -104,11 +195,11 @@ async function main() {
104195 await fs . writeFile ( outputAbs , JSON . stringify ( leaderboard , null , 2 ) , "utf8" ) ;
105196
106197 console . log (
107- `[generate-leaderboard] Successfully wrote leaderboard to ${ OUTPUT } ` ,
198+ `[generate-leaderboard] 排行榜数据已成功写入至 ${ OUTPUT } | Successfully wrote leaderboard to ${ OUTPUT } ` ,
108199 ) ;
109200 } catch ( error ) {
110201 console . error (
111- "[generate-leaderboard] Failed to generate leaderboard:" ,
202+ "[generate-leaderboard] 生成排行榜失败: | Failed to generate leaderboard:" ,
112203 error ,
113204 ) ;
114205 process . exit ( 1 ) ;
0 commit comments