docs: record this round's fixes in AGENTS.md; add download-stats script; plan M11 in ROADMAP

This commit is contained in:
Bill committed 2026-10-07 20:44:46 +08:00
1 parent 36627ee4b7
commit f5acf26d1f
3 files changed
+179 -4

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
// Download stats: Gitea releases (domestic channel) + GitHub releases (intl).
// Usage: node scripts/download-stats.cjs
// Credentials from .env (repo root, gitignored): GIT_TOKEN, GH_TOKEN.
// GitHub traffic honors HTTPS_PROXY / HTTP_PROXY (env or .env); Gitea is
// always direct — same policy as scripts/release.cjs.
//
// Caveats the printed report also states:
// - The auto-update channel (Gitea generic package `openterminal-update`)
// does not expose download counts, so updater traffic is not included.
// - Counts reflect currently existing release assets only; re-uploaded
// assets restart from zero.
const fs = require('node:fs')
const path = require('node:path')
const ROOT = path.join(__dirname, '..')
const env = Object.fromEntries(
fs.readFileSync(path.join(ROOT, '.env'), 'utf8').split('\n')
.map((l) => l.trim())
.filter((l) => l.includes('=') && !l.startsWith('#'))
.map((l) => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()] })
)
const proxy = process.env.HTTPS_PROXY || process.env.https_proxy || env.HTTPS_PROXY || env.PROXY
// Fallback for shells without the env set: the local proxy documented in
// AGENTS.md is how GitHub traffic gets out on this network; a direct call
// dies with ECONNRESET.
const ghAgent = (p) => (p ? new (require('undici').ProxyAgent)(p) : undefined)
const GH_FALLBACK_PROXY = 'http://127.0.0.1:7897'
const GITEA = 'https://git.codingplan.site/api/v1/repos/admin/OpenTerminal'
const GITHUB = 'https://api.github.com/repos/billowliu2/OpenTerminal'
/** Sum download counts per release, split by exe/msi/other. */
function tally(releases) {
let total = 0, exe = 0, msi = 0
const rows = []
for (const r of releases) {
let n = 0, e = 0, m = 0
for (const a of r.assets || []) {
n += a.download_count || 0
if (a.name.endsWith('.exe')) e += a.download_count || 0
else if (a.name.endsWith('.msi')) m += a.download_count || 0
}
rows.push({ tag: r.tag_name || r.name, total: n, exe: e, msi: m })
total += n; exe += e; msi += m
}
rows.sort((a, b) => b.total - a.total)
return { rows, total, exe, msi }
}
async function giteaReleases() {
// Paginated: Gitea caps `limit` at 50.
const all = []
for (let page = 1; ; page++) {
const resp = await fetch(`${GITEA}/releases?limit=50&page=${page}`, {
headers: { Authorization: `token ${env.GIT_TOKEN}` }
})
if (!resp.ok) throw new Error(`gitea HTTP ${resp.status}`)
const batch = await resp.json()
all.push(...batch)
if (batch.length < 50) return all
}
}
async function githubFetch(url) {
const headers = {
Accept: 'application/vnd.github+json',
'User-Agent': 'openterminal-download-stats',
...(env.GH_TOKEN ? { Authorization: `Bearer ${env.GH_TOKEN}` } : {})
}
const attempts = [{}, { dispatcher: ghAgent(proxy) }, { dispatcher: ghAgent(GH_FALLBACK_PROXY) }]
let lastErr
for (const extra of attempts) {
try {
const resp = await fetch(url, { headers, ...extra })
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
return resp.json()
} catch (e) { lastErr = e }
}
throw lastErr
}
async function githubReleases() {
const all = []
for (let page = 1; ; page++) {
const batch = await githubFetch(`${GITHUB}/releases?per_page=100&page=${page}`)
all.push(...batch)
if (batch.length < 100) return all
}
}
function print(source, t) {
console.log(`\n== ${source} ==`)
for (const r of t.rows) {
if (!r.total) continue
console.log(`${r.tag.padEnd(12)} total ${String(r.total).padStart(5)} exe ${String(r.exe).padStart(5)} msi ${String(r.msi).padStart(4)}`)
}
console.log(` 小计 ${t.total}(exe ${t.exe} / msi ${t.msi} / 其他 ${t.total - t.exe - t.msi})`)
}
async function main() {
let gh, gt
const fails = []
try { gt = tally(await giteaReleases()) } catch (e) { fails.push(`Gitea: ${e.message}`) }
try { gh = tally(await githubReleases()) } catch (e) { fails.push(`GitHub: ${e.message}`) }
if (gt) print('国内 · Gitea Releases', gt)
if (gh) print('国外 · GitHub Releases', gh)
if (gt && gh) {
const total = gt.total + gh.total
console.log(`\n合计(可计数部分): ${total}`)
}
console.log('\n口径说明:')
console.log('- 应用内自动更新走 Gitea 通用包通道(openterminal-update),该通道无下载计数接口,未计入。')
console.log('- 计数只反映当前存活的 release 资产,重传过的资产从零累计。')
if (fails.length) {
console.error('\n部分来源失败:')
for (const f of fails) console.error(' -', f)
process.exitCode = 1
}
}
main()