fix(mira): wikilink autocomplete 5s cache + 開下拉時 refetch

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 13:37:03 +08:00
parent 7da1eb6d65
commit 1084e0102a
2 changed files with 22 additions and 5 deletions
+7 -1
View File
@@ -63,10 +63,16 @@ export function validateRelations(triplets: ParsedTriplet[]): void {
`「PIPE」已棄用,請改用「完成後」或「ON_SUCCESS」。`
);
}
// 容許 FOREACH iterator 命名變體:「對每個 paragraph」/「FOREACH item」
// graph-builder.ts 已支援這個 regexcommit 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」)`
);
}
}
+15 -4
View File
@@ -216,17 +216,21 @@ async function fetchAllEntityNames(apiKey: string): Promise<Set<string>> {
}
}
// Cache每個 session 只 fetch 一次 entity listautocomplete 用)
// Cachedebounce 短 TTL5s),同時 autocomplete 開啟時主動 refetch
// 30s 太久:wiki_synthesis 後台跑出新 entity(如 Claude 自己命名的「李飛飛的視界之旅」)autocomplete 撈不到
let cachedEntityNames: Set<string> | null = null;
let cachedEntityFetchedAt = 0;
async function getEntityNamesCached(apiKey: string): Promise<Set<string>> {
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(() => {