feat(opensource): README 四级回落 + 兜底 JSON 8 仓 + 项目可见性告警

- site-content.ts:README 取数加 AbortSignal.timeout(10s)(原无超时,
  慢端点可卡整条构建);四级回落矩阵逐级 console.warn 点名原因:
  新鲜网络(内容有变才回写缓存)→ token 缺位/HTTP 非 2xx/空/超时/异常
  时吃缓存 → 缓存亦缺回落 yaml blurb → 缓存回写失败(构建仓只读)仅告警
- README 图片加固:markdown 层给每个 img 补 loading=lazy + 缺省 alt
  (幂等;只动这两个属性,因为过 rehype-content-sanitize 白名单后其余
  属性会被剥);相对链接不重写,限制写入 JSDoc 与文档
- projects.ts:单源失败点名、双源全失败点名回落 JSON、mode=only 的
  include 未命中双向点名(双源无此仓 / 被 perSource 过滤掉)
- projects-fallback.json:1 → 8 条真实仓(blog / open-earth-data-guide★58
  / AI-CLI / open-earth-data-kit / pi-mesh / pi-board / pi-jobd /
  ai-code-style-kit,字段对齐 RepoCard)

车道:worker-one(T5);收编复验:E2E 37/37、构建 48 页 0 ERROR、
兜底 JSON 8 条断言过

验证: E2E 37/37 PASS;pnpm astro build 48 页 exit 0;fallback 8 条

Spec: 2026-11-02_blog-5g-pages (T5)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-02 18:13:47 +08:00
1 parent 59eda420b3
commit 6096a9dafe
3 files changed
+244 -25

No files matched your search

+41 -2
View File
@@ -74,6 +74,13 @@ export interface LoadProjectsOptions {
export async function loadProjects(options: LoadProjectsOptions = {}): Promise<RepoCard[]> {
const { pinned = PINNED_DEFAULT, mode = 'all', include = [], exclude = [], sort = 'pinned-first', perSource = 'both' } = options;
const [gitea, github] = await Promise.allSettled([fetchJson(GITEA_API), fetchJson(GITHUB_API)]);
// 单源失败不拦构建,但必须在日志里点名——否则“怪了,这个仓怎么没显示”要排查很久
if (gitea.status === 'rejected') {
console.warn(`[opensource] projects:gitea 源不可用(${(gitea.reason as Error).message}),仅用 GitHub 源`);
}
if (github.status === 'rejected') {
console.warn(`[opensource] projects:GitHub 源不可用(${(github.reason as Error).message}),仅用 gitea 源`);
}
const byName = new Map<string, RepoCard>();
for (const r of gitea.status === 'fulfilled' ? gitea.value : []) {
@@ -89,10 +96,42 @@ export async function loadProjects(options: LoadProjectsOptions = {}): Promise<R
}
if (byName.size === 0) {
console.warn('[opensource] projects:双源均无可用数据,改用仓内兜底 src/data/projects-fallback.json');
const fb = (await import('../data/projects-fallback.json')).default as RepoCard[];
return arrange(fb, pinned, mode, include, exclude, sort, perSource);
const out = arrange(fb, pinned, mode, include, exclude, sort, perSource);
warnMissedIncludes(out, fb, include, mode, perSource);
return out;
}
const merged = [...byName.values()];
const out = arrange(merged, pinned, mode, include, exclude, sort, perSource);
warnMissedIncludes(out, merged, include, mode, perSource);
return out;
}
/**
* mode=only 时点名 include 里“写了却没显示”的仓名——避开设了一个看不见的黑洞。
* 未命中分两类,日志措词区分:①双源结果里根本没有(拼写错 / 私有仓 / 该源刚失败);
* ②有,但被 perSource 过滤掉了(配置与仓来源不匹配)。
*/
function warnMissedIncludes(
showing: RepoCard[],
pool: RepoCard[],
include: string[],
mode: 'all' | 'only' | 'exclude',
perSource: 'both' | 'github' | 'gitea',
): void {
if (mode !== 'only' || include.length === 0) return;
const shown = new Set(showing.map((r) => r.name));
const available = new Map(pool.map((r) => [r.name, r.source]));
for (const name of include) {
if (shown.has(name)) continue;
const src = available.get(name);
console.warn(
src
? `[opensource] projects:include 里的 「${name}」命中 ${src} 源,但被 perSource=${perSource} 过滤掉——改 perSource=both 或把它移出 include`
: `[opensource] projects:include 里的 「${name}」在双源结果中没有命中(仓名拼写?私有仓?源刚失败?)——它不会出现在项目墙上`,
);
}
return arrange([...byName.values()], pinned, mode, include, exclude, sort, perSource);
}
/** 来源过滤 + 可见性过滤 + 排序(不截断:上限交给调用方) */