File size: 3,763 Bytes
494c9e4
1f66b27
494c9e4
 
 
28147a1
 
312e591
494c9e4
 
1f66b27
494c9e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f66b27
 
494c9e4
 
 
 
 
 
 
 
28147a1
494c9e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28147a1
 
 
 
 
 
 
 
 
494c9e4
 
 
 
 
 
 
 
1f66b27
 
494c9e4
 
 
 
 
 
1f66b27
 
494c9e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
 * 语义分析结果缓存:以 text + query + kind(relevance|keywords)的 hash 为索引。
 * 持久化到 localStorage,刷新后保留。删除查询历史时需调用 removeByQuery 清理对应缓存。
 */

import { lsGet, lsSetCatch } from '../storage/localStorageHelpers';

const MAX_SIZE = 100;
const STORAGE_KEY = 'info_radar_semantic_result_cache';

/** 缓存条目:relevance / keywords / hybrid 任一形态(字段按 kind 选用) */
export type SemanticCacheResult = {
    success: boolean;
    model?: string;
    token_attention?: Array<{ offset: [number, number]; raw: string; score: number }>;
    debug_info?: { abbrev?: string; topk_tokens?: string[]; topk_probs?: number[] };
    full_match_degree?: number;
    message?: string;
};

type StoredEntry = SemanticCacheResult & { _query?: string };

function simpleHash(s: string): string {
    let h = 0;
    for (let i = 0; i < s.length; i++) {
        h = ((h << 5) - h + s.charCodeAt(i)) | 0;
    }
    return (h >>> 0).toString(36);
}

function buildKey(text: string, query: string, kind?: string): string {
    const parts = [text, query, kind ?? ''];
    return simpleHash(parts.join('\0'));
}

const cache = new Map<string, StoredEntry>();
let keyOrder: string[] = [];

function load(): void {
    try {
        const raw = lsGet(STORAGE_KEY);
        if (!raw) return;
        const parsed = JSON.parse(raw) as { entries?: Record<string, StoredEntry>; keyOrder?: string[] };
        if (!parsed?.entries || typeof parsed.entries !== 'object') return;
        cache.clear();
        for (const [k, v] of Object.entries(parsed.entries)) {
            if (v && typeof v === 'object') cache.set(k, v);
        }
        keyOrder = Array.isArray(parsed.keyOrder)
            ? parsed.keyOrder.filter((k) => cache.has(k)).slice(-MAX_SIZE)
            : [...cache.keys()];
    } catch {
        cache.clear();
        keyOrder = [];
    }
}

load();

function persist(): void {
    const entries: Record<string, StoredEntry> = {};
    for (const [k, v] of cache) entries[k] = v;
    const err = lsSetCatch(STORAGE_KEY, JSON.stringify({ entries, keyOrder }));
    if (err === undefined) return;
    const reason =
        err instanceof DOMException && err.name === 'QuotaExceededError'
            ? 'localStorage 配额已满(Chrome 约 5MB/域名),建议减少 MAX_SIZE 或清理其他站点数据'
            : String(err);
    console.warn('[semanticResultCache] 持久化失败,刷新后缓存可能丢失。原因:', reason);
}

function evictOne(): void {
    if (keyOrder.length < MAX_SIZE) return;
    const oldest = keyOrder.shift()!;
    cache.delete(oldest);
}

export function get(text: string, query: string, kind?: string): SemanticCacheResult | undefined {
    const key = buildKey(text, query, kind);
    const entry = cache.get(key);
    if (!entry) return undefined;
    const { _query, ...rest } = entry as SemanticCacheResult & { _query?: string };
    return rest;
}

export function set(text: string, query: string, result: SemanticCacheResult, kind?: string): void {
    const key = buildKey(text, query, kind);
    if (cache.has(key)) {
        const idx = keyOrder.indexOf(key);
        if (idx >= 0) keyOrder.splice(idx, 1);
    }
    evictOne();
    cache.set(key, { ...result, _query: query });
    keyOrder.push(key);
    persist();
}

export function removeByQuery(query: string): void {
    const keysToRemove: string[] = [];
    for (const [key, entry] of cache) {
        if (entry._query === query) keysToRemove.push(key);
    }
    for (const key of keysToRemove) {
        cache.delete(key);
        const idx = keyOrder.indexOf(key);
        if (idx >= 0) keyOrder.splice(idx, 1);
    }
    if (keysToRemove.length) persist();
}