feat(mira/feed): WikiStatusBadge 加手動重試按鈕

leo 2026-05-17 反饋:「漏了的要怎麼做?會自己慢慢完成還是要手動下令?」

自動行為解釋:
  mira_feed_watcher cron 每 5 min 掃 tags=[] 的 raw 自動重試。
  但若已被 mark wiki-processed (假處理),watcher 永遠跳過 → 需手動。

互動加:
  - ⚠️ 漏了 變按鈕,click → 清 tag + 立即 trigger wiki_synthesis
  -  wiki 旁加小 ↻ icon,給「tag 標完成但實際沒 wiki」的情況用
  - 點擊後 1 分鐘內顯示「 重試中」防 spam

UX 細節:
  - title hover 解釋每個狀態跟動作
  - retry 同時清 tag + 直接 trigger(不等下個 cron tick)
  - 失敗 fail silently (catch all),1 min 後可再試

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 11:26:24 +08:00
parent 475c95aaae
commit 175a290730
+64 -28
View File
@@ -1145,6 +1145,7 @@ function DocCard({
mainBlock={mainBlocksList[0]} mainBlock={mainBlocksList[0]}
createdAt={doc.created_at} createdAt={doc.created_at}
showMira={showMira} showMira={showMira}
apiKey={me.api_key}
/> />
); );
return !showMira && mainBlocksList[0] return !showMira && mainBlocksList[0]
@@ -2058,22 +2059,25 @@ function SourceBadge({ source }: { source: string }) {
); );
} }
// WikiStatusBadge:顯示這篇 raw 是否已合成 wiki // WikiStatusBadge:顯示這篇 raw 是否已合成 wiki + 「漏了」手動重試按鈕
// - ✅ 已合成(tags 含 wiki-processed // 對應 leo 2026-05-17 反饋:「漏了的要怎麼做?會自己慢慢完成還是要手動下令?」
// - ⏳ 處理中(< 6 分鐘前貼,可能還沒被 cron 撈到 / wiki_synthesis 正在跑) //
// - ○ 排隊中(> 6 分鐘但 < 30 分鐘,等下一個 cron tick // 自動行為:mira_feed_watcher cron 每 5 min 掃 tags=[] 的 raw,會無限重試
// - ⚠️ 可能漏了(> 30 分鐘還沒處理 // 手動入口:⚠️ 漏了 變按鈕,點擊 = 清 tag + 立即 trigger wiki_synthesis(不等 cron
// 對應 leo 2026-05-17 反饋:「沒有符號顯示是否已建立 wiki」
function WikiStatusBadge({ function WikiStatusBadge({
mainBlock, mainBlock,
createdAt, createdAt,
showMira, showMira,
apiKey,
}: { }: {
mainBlock: KBDBBlock | undefined; mainBlock: KBDBBlock | undefined;
createdAt: number | string; createdAt: number | string;
showMira: boolean; showMira: boolean;
apiKey: string;
}) { }) {
// Mira 自己貼的(type=wiki-page)就是 wiki,不需要狀態 const [retrying, setRetrying] = useState(false);
const [retriedAt, setRetriedAt] = useState<number | null>(null);
if (showMira) return null; if (showMira) return null;
if (!mainBlock) return null; if (!mainBlock) return null;
@@ -2085,51 +2089,83 @@ function WikiStatusBadge({
// ignore // ignore
} }
const ageMs = Date.now() - toDate(createdAt).getTime();
const minutes = ageMs / 60_000;
const recentlyRetried = retriedAt && Date.now() - retriedAt < 60_000;
// 手動重試:清 wiki-processed tag(保險:watcher 才會重撈)+ 立即 trigger wiki_synthesis
const handleRetry = useCallback(async () => {
if (retrying) return;
setRetrying(true);
try {
await fetch(`${KBDB_BASE}/blocks/${mainBlock.id}`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ tags: [] }),
}).catch(() => null);
await fetch(`${API_BASE}/webhooks/named/wiki_synthesis/trigger`, {
method: 'POST',
headers: { 'X-Arcrun-API-Key': apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify({ api_key: apiKey, raw_block_id: mainBlock.id }),
}).catch(() => null);
setRetriedAt(Date.now());
} finally {
setRetrying(false);
}
}, [retrying, mainBlock?.id, apiKey]);
if (processed) { if (processed) {
return ( return (
<span <span
className="wiki-status wiki-status-done" title="已合成 wiki — 若實際看不到 entity 可能是 fan-out 假處理,按 ↻ 重試"
title="已合成 wiki — click 看詳細 entity" style={{ color: '#3a8a3a', fontSize: '0.85em', display: 'inline-flex', gap: 4, alignItems: 'center' }}
style={{ color: '#3a8a3a', fontSize: '0.85em' }}
> >
wiki wiki
<button
onClick={handleRetry}
disabled={retrying || !!recentlyRetried}
title="重跑(適用 tag 已標但實際沒 wiki-page 的情況)"
style={{
background: 'transparent', border: 'none', color: '#888',
fontSize: '0.85em', padding: 0, cursor: 'pointer',
}}
>
{recentlyRetried ? '已重試' : (retrying ? '…' : '↻')}
</button>
</span> </span>
); );
} }
const ageMs = Date.now() - toDate(createdAt).getTime(); if (recentlyRetried) {
const minutes = ageMs / 60_000; return <span style={{ color: '#888', fontSize: '0.85em' }}> </span>;
}
if (minutes < 6) { if (minutes < 6) {
return ( return (
<span <span title="處理中(mira_feed_watcher 每 5 分鐘掃一次)" style={{ color: '#888', fontSize: '0.85em' }}>
className="wiki-status wiki-status-pending"
title="處理中(mira_feed_watcher 每 5 分鐘掃一次)"
style={{ color: '#888', fontSize: '0.85em' }}
>
</span> </span>
); );
} }
if (minutes < 30) { if (minutes < 30) {
return ( return (
<span <span title="排隊中等下一個 cron tick (5 分鐘一次)" style={{ color: '#aaa', fontSize: '0.85em' }}>
className="wiki-status wiki-status-queued"
title="排隊中等下一個 cron tick"
style={{ color: '#aaa', fontSize: '0.85em' }}
>
</span> </span>
); );
} }
return ( return (
<span <button
className="wiki-status wiki-status-stuck" onClick={handleRetry}
title={`${Math.floor(minutes)} 分鐘未處理 — 可能失敗,看 wiki/ 確認或 trigger watcher`} disabled={retrying}
style={{ color: '#c66', fontSize: '0.85em' }} title={`${Math.floor(minutes)} 分鐘未處理 — cron 應該已試多次。點擊強制重試一次`}
style={{
background: 'transparent', border: '1px solid #c66', color: '#c66',
fontSize: '0.85em', padding: '0 6px', borderRadius: 4, cursor: 'pointer',
}}
> >
{retrying ? '…' : '⚠️ 漏了 ↻'}
</span> </button>
); );
} }