#!/usr/bin/env node // 生成 Star History SVG —— 自托管方案(不依赖 star-history 第三方服务) // 用法: GITHUB_TOKEN 可选; node scripts/gen-star-history.mjs // 输出: assets/star-history.svg 'use strict'; import fs from 'node:fs'; import path from 'node:path'; import { execSync } from 'node:child_process'; const REPO = 'dwgx/WindsurfAPI'; const OUT = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'docs', 'assets', 'star-history.svg'); async function fetchStars() { let token = process.env.GITHUB_TOKEN || ''; if (!token) { try { token = execSync('gh auth token', { encoding: 'utf8' }).trim(); } catch (e) { /* CI 无 gh 时忽略 */ } } const headers = { 'Accept': 'application/vnd.github.star+json', // starred_at 时间戳 'User-Agent': 'star-history-gen', ...(token ? { 'Authorization': 'Bearer ' + token } : {}), }; const stars = []; let page = 1; for (;;) { const url = `https://api.github.com/repos/${REPO}/stargazers?per_page=100&page=${page}`; const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`GitHub API ${res.status}: ${url}`); const batch = await res.json(); if (!Array.isArray(batch) || batch.length === 0) break; for (const s of batch) { if (s.starred_at) stars.push(s.starred_at); } // 检查是否还有下一页 const link = res.headers.get('link') || ''; if (!/rel="next"/.test(link)) break; page++; if (page > 200) break; // 安全阀 } return stars; } function makeSvg(stars) { const W = 800, H = 400, padL = 60, padR = 30, padT = 30, padB = 50; if (stars.length === 0) return null; const t0 = Date.parse(stars[0]); const t1 = Date.parse(stars[stars.length - 1]); const span = Math.max(t1 - t0, 1); const X = t => padL + (Date.parse(t) - t0) / span * (W - padL - padR); const Y = n => padT + (H - padT - padB) * (1 - n / stars.length); const points = stars.map((t, i) => `${X(t).toFixed(1)},${Y(i + 1).toFixed(1)}`).join(' '); const monthLabels = []; const seen = new Set(); for (const t of stars) { const d = new Date(t); const mk = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'); if (!seen.has(mk)) { seen.add(mk); monthLabels.push({ x: X(t), label: mk }); } } const finalStar = stars.length; const firstDate = stars[0].slice(0, 10); const lastDate = stars[stars.length - 1].slice(0, 10); const lines = []; lines.push(``); lines.push(``); lines.push(``); // 网格线(0/25/50/75/100%) for (const f of [0, .25, .5, .75, 1]) { const y = padT + (H - padT - padB) * (1 - f); lines.push(``); lines.push(`${Math.round(stars.length * f)}`); } // 基线 lines.push(``); // 辉光 + 主线 lines.push(``); lines.push(``); // 终点圆点 const lx = X(stars[stars.length - 1]), ly = Y(stars.length); lines.push(``); // 月份标签 for (const { x, label } of monthLabels) { lines.push(`${label}`); } // 信息 lines.push(`${finalStar.toLocaleString('en-US')} stars`); lines.push(`${firstDate} → ${lastDate} · ${REPO}`); lines.push(``); return lines.join('\n'); } (async () => { console.log('拉取 star 数据…'); const stars = await fetchStars(); console.log(`共 ${stars.length} 条 star 记录`); const svg = makeSvg(stars); if (!svg) { console.error('无数据'); process.exit(1); } fs.mkdirSync(path.dirname(OUT), { recursive: true }); fs.writeFileSync(OUT, svg); console.log('已写入', OUT); })().catch(e => { console.error('失败:', e.message); process.exit(1); });