File size: 3,637 Bytes
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
/**
 * 语义分析结果缓存:以 text + query + submode 的 hash 为索引。
 * 持久化到 localStorage,刷新后保留。删除查询历史时需调用 removeByQuery 清理对应缓存。
 */

const MAX_SIZE = 50;
const STORAGE_KEY = 'info_radar_semantic_result_cache';

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, submode?: string): string {
    const parts = [text, query, submode ?? ''];
    return simpleHash(parts.join('\0'));
}

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

function load(): void {
    try {
        const raw = localStorage.getItem(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 {
    try {
        const entries: Record<string, StoredEntry> = {};
        for (const [k, v] of cache) entries[k] = v;
        localStorage.setItem(STORAGE_KEY, JSON.stringify({ entries, keyOrder }));
    } catch (e) {
        const reason =
            e instanceof DOMException && e.name === 'QuotaExceededError'
                ? 'localStorage 配额已满(Chrome 约 5MB/域名),建议减少 MAX_SIZE 或清理其他站点数据'
                : String(e);
        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, submode?: string): SemanticCacheResult | undefined {
    const key = buildKey(text, query, submode);
    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, submode?: string): void {
    const key = buildKey(text, query, submode);
    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();
}