From 1084e0102abbe988f349118ddf2cb96ec1505067 Mon Sep 17 00:00:00 2001 From: richblack Date: Sat, 16 May 2026 13:37:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(mira):=20wikilink=20autocomplete=205s=20cac?= =?UTF-8?q?he=20+=20=E9=96=8B=E4=B8=8B=E6=8B=89=E6=99=82=20refetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit leo 反饋:30s TTL 太久,wiki_synthesis 後台跑出新 entity,autocomplete 撈不到。 - TTL 30s → 5s - WikilinkAutocomplete 在 matchInfo 從 null → 有值時主動 invalidate refetch - 順手把 yaml-parser 對 FOREACH iterator relation 命名變體(「對每個 X」/「FOREACH X」)放行,graph-builder 早就支援,validate 卻擋掉 Co-Authored-By: Claude Opus 4.7 --- cli/src/lib/yaml-parser.ts | 8 +++++++- landing/app/mira/feed/page.tsx | 19 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/cli/src/lib/yaml-parser.ts b/cli/src/lib/yaml-parser.ts index 17ae5ea..693ea13 100644 --- a/cli/src/lib/yaml-parser.ts +++ b/cli/src/lib/yaml-parser.ts @@ -63,10 +63,16 @@ export function validateRelations(triplets: ParsedTriplet[]): void { `「PIPE」已棄用,請改用「完成後」或「ON_SUCCESS」。` ); } + // 容許 FOREACH iterator 命名變體:「對每個 paragraph」/「FOREACH item」 + // graph-builder.ts 已支援這個 regex(commit e8fca33 2026-05-07) + const foreachMatch = t.relation.match(/^(?:對每個|FOREACH)\s+\w+$/i); + if (foreachMatch) continue; + if (!VALID_RELATIONS.has(t.relation)) { throw new Error( `未知關係詞「${t.relation}」。\n` + - `合法關係詞:${[...VALID_RELATIONS].join('、')}` + `合法關係詞:${[...VALID_RELATIONS].join('、')}\n` + + `(FOREACH 支援 iterator 命名:「對每個 X」/「FOREACH X」)` ); } } diff --git a/landing/app/mira/feed/page.tsx b/landing/app/mira/feed/page.tsx index a9f19b5..0028dfe 100644 --- a/landing/app/mira/feed/page.tsx +++ b/landing/app/mira/feed/page.tsx @@ -216,17 +216,21 @@ async function fetchAllEntityNames(apiKey: string): Promise> { } } -// Cache:每個 session 只 fetch 一次 entity list(autocomplete 用) +// Cache:debounce 短 TTL(5s),同時 autocomplete 開啟時主動 refetch +// 30s 太久:wiki_synthesis 後台跑出新 entity(如 Claude 自己命名的「李飛飛的視界之旅」)autocomplete 撈不到 let cachedEntityNames: Set | null = null; let cachedEntityFetchedAt = 0; async function getEntityNamesCached(apiKey: string): Promise> { const now = Date.now(); - // 30s TTL — leo 持續寫貼文時新建的 entity 也能很快被 autocomplete 看到 - if (cachedEntityNames && now - cachedEntityFetchedAt < 30_000) return cachedEntityNames; + if (cachedEntityNames && now - cachedEntityFetchedAt < 5_000) return cachedEntityNames; cachedEntityNames = await fetchAllEntityNames(apiKey); cachedEntityFetchedAt = now; return cachedEntityNames; } +function invalidateEntityCache() { + cachedEntityNames = null; + cachedEntityFetchedAt = 0; +} // 簡單 type 推測:《X》 → 書;http(s)://X → URL;其他 → 概念 function guessEntityType(name: string): 'book' | 'url' | 'concept' { @@ -664,10 +668,17 @@ function WikilinkAutocomplete({ const [matchInfo, setMatchInfo] = useState<{ start: number; query: string } | null>(null); const [selectedIdx, setSelectedIdx] = useState(0); - // 載入 entity 清單(cached 30s) + // 載入 entity 清單(5s cache + 每次 `[[` 開啟時 refetch 保持新鮮) useEffect(() => { getEntityNamesCached(apiKey).then(set => setEntities(Array.from(set).sort())); }, [apiKey]); + // matchInfo 從 null → 有值 = 剛打開 autocomplete → 重抓 + useEffect(() => { + if (!matchInfo) return; + invalidateEntityCache(); + getEntityNamesCached(apiKey).then(set => setEntities(Array.from(set).sort())); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [matchInfo !== null, apiKey]); // 監聽 textarea 變化 / cursor 移動 → 重算 matchInfo useEffect(() => {