feat(mira/feed): WikiStatusBadge — 顯示貼文 wiki 合成狀態 (leo 2026-05-17 反饋)

leo 反饋:「沒有符號顯示是否已建立 wiki,不知道是出錯了還是要等下一批」

新 component WikiStatusBadge 顯示在 PostCard header 來源/時間旁邊:
-  wiki    — tags 含 wiki-processed
-  處理中  — 貼 < 6 分鐘前(cron 5 分鐘一輪,可能還沒撈到)
- ○ 排隊    — 6-30 分鐘(等下一個 tick)
- ⚠️ 漏了? — > 30 分鐘還沒處理(可能 wiki_synthesis 失敗)

Mira 自己貼文(type=wiki-page from showMira)不顯示 — 它本身就是 wiki。
資料來源純 client-side:mainBlocksList[0].tags_json + doc.created_at。
未來可加 click → 跳對應 wiki page,或 hover 顯示 entity 預覽。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 09:53:19 +08:00
parent a7d36933e6
commit efdd75cbdc
+87
View File
@@ -1139,6 +1139,18 @@ function DocCard({
<SourceBadge source={postSource} /> <SourceBadge source={postSource} />
<span>·</span> <span>·</span>
<RelTime when={doc.updated_at} /> <RelTime when={doc.updated_at} />
{(() => {
const badge = (
<WikiStatusBadge
mainBlock={mainBlocksList[0]}
createdAt={doc.created_at}
showMira={showMira}
/>
);
return !showMira && mainBlocksList[0]
? (<><span>·</span>{badge}</>)
: null;
})()}
</div> </div>
</div> </div>
<MoreMenu items={cardMenu} /> <MoreMenu items={cardMenu} />
@@ -2046,6 +2058,81 @@ function SourceBadge({ source }: { source: string }) {
); );
} }
// WikiStatusBadge:顯示這篇 raw 是否已合成 wiki
// - ✅ 已合成(tags 含 wiki-processed
// - ⏳ 處理中(< 6 分鐘前貼,可能還沒被 cron 撈到 / wiki_synthesis 正在跑)
// - ○ 排隊中(> 6 分鐘但 < 30 分鐘,等下一個 cron tick
// - ⚠️ 可能漏了(> 30 分鐘還沒處理)
// 對應 leo 2026-05-17 反饋:「沒有符號顯示是否已建立 wiki」
function WikiStatusBadge({
mainBlock,
createdAt,
showMira,
}: {
mainBlock: KBDBBlock | undefined;
createdAt: number | string;
showMira: boolean;
}) {
// Mira 自己貼的(type=wiki-page)就是 wiki,不需要狀態
if (showMira) return null;
if (!mainBlock) return null;
let processed = false;
try {
const tags = JSON.parse(mainBlock.tags_json || '[]') as string[];
processed = Array.isArray(tags) && tags.includes('wiki-processed');
} catch {
// ignore
}
if (processed) {
return (
<span
className="wiki-status wiki-status-done"
title="已合成 wiki — click 看詳細 entity"
style={{ color: '#3a8a3a', fontSize: '0.85em' }}
>
wiki
</span>
);
}
const ageMs = Date.now() - toDate(createdAt).getTime();
const minutes = ageMs / 60_000;
if (minutes < 6) {
return (
<span
className="wiki-status wiki-status-pending"
title="處理中(mira_feed_watcher 每 5 分鐘掃一次)"
style={{ color: '#888', fontSize: '0.85em' }}
>
</span>
);
}
if (minutes < 30) {
return (
<span
className="wiki-status wiki-status-queued"
title="排隊中等下一個 cron tick"
style={{ color: '#aaa', fontSize: '0.85em' }}
>
</span>
);
}
return (
<span
className="wiki-status wiki-status-stuck"
title={`${Math.floor(minutes)} 分鐘未處理 — 可能失敗,看 wiki/ 確認或 trigger watcher`}
style={{ color: '#c66', fontSize: '0.85em' }}
>
</span>
);
}
function RelTime({ when }: { when: number | string }) { function RelTime({ when }: { when: number | string }) {
const d = toDate(when); const d = toDate(when);
const now = Date.now(); const now = Date.now();