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:
@@ -63,10 +63,16 @@ export function validateRelations(triplets: ParsedTriplet[]): void {
|
|||||||
`「PIPE」已棄用,請改用「完成後」或「ON_SUCCESS」。`
|
`「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)) {
|
if (!VALID_RELATIONS.has(t.relation)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`未知關係詞「${t.relation}」。\n` +
|
`未知關係詞「${t.relation}」。\n` +
|
||||||
`合法關係詞:${[...VALID_RELATIONS].join('、')}`
|
`合法關係詞:${[...VALID_RELATIONS].join('、')}\n` +
|
||||||
|
`(FOREACH 支援 iterator 命名:「對每個 X」/「FOREACH X」)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,17 +216,21 @@ async function fetchAllEntityNames(apiKey: string): Promise<Set<string>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache:每個 session 只 fetch 一次 entity list(autocomplete 用)
|
// Cache:debounce 短 TTL(5s),同時 autocomplete 開啟時主動 refetch
|
||||||
|
// 30s 太久:wiki_synthesis 後台跑出新 entity(如 Claude 自己命名的「李飛飛的視界之旅」)autocomplete 撈不到
|
||||||
let cachedEntityNames: Set<string> | null = null;
|
let cachedEntityNames: Set<string> | null = null;
|
||||||
let cachedEntityFetchedAt = 0;
|
let cachedEntityFetchedAt = 0;
|
||||||
async function getEntityNamesCached(apiKey: string): Promise<Set<string>> {
|
async function getEntityNamesCached(apiKey: string): Promise<Set<string>> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// 30s TTL — leo 持續寫貼文時新建的 entity 也能很快被 autocomplete 看到
|
if (cachedEntityNames && now - cachedEntityFetchedAt < 5_000) return cachedEntityNames;
|
||||||
if (cachedEntityNames && now - cachedEntityFetchedAt < 30_000) return cachedEntityNames;
|
|
||||||
cachedEntityNames = await fetchAllEntityNames(apiKey);
|
cachedEntityNames = await fetchAllEntityNames(apiKey);
|
||||||
cachedEntityFetchedAt = now;
|
cachedEntityFetchedAt = now;
|
||||||
return cachedEntityNames;
|
return cachedEntityNames;
|
||||||
}
|
}
|
||||||
|
function invalidateEntityCache() {
|
||||||
|
cachedEntityNames = null;
|
||||||
|
cachedEntityFetchedAt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// 簡單 type 推測:《X》 → 書;http(s)://X → URL;其他 → 概念
|
// 簡單 type 推測:《X》 → 書;http(s)://X → URL;其他 → 概念
|
||||||
function guessEntityType(name: string): 'book' | 'url' | 'concept' {
|
function guessEntityType(name: string): 'book' | 'url' | 'concept' {
|
||||||
@@ -664,10 +668,17 @@ function WikilinkAutocomplete({
|
|||||||
const [matchInfo, setMatchInfo] = useState<{ start: number; query: string } | null>(null);
|
const [matchInfo, setMatchInfo] = useState<{ start: number; query: string } | null>(null);
|
||||||
const [selectedIdx, setSelectedIdx] = useState(0);
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
||||||
|
|
||||||
// 載入 entity 清單(cached 30s)
|
// 載入 entity 清單(5s cache + 每次 `[[` 開啟時 refetch 保持新鮮)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getEntityNamesCached(apiKey).then(set => setEntities(Array.from(set).sort()));
|
getEntityNamesCached(apiKey).then(set => setEntities(Array.from(set).sort()));
|
||||||
}, [apiKey]);
|
}, [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
|
// 監聽 textarea 變化 / cursor 移動 → 重算 matchInfo
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user