Skip to content

Commit dbd19c8

Browse files
committed
Rime-Wanxiang-Updater:fix:本地信息丢失问题
1 parent 82cd962 commit dbd19c8

3 files changed

Lines changed: 332 additions & 7 deletions

File tree

Rime-Wanxiang-Updater/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// File: index.tsx
22
import { Navigation, Script } from "scripting"
33
import { HomeView } from "./components/HomeView"
4+
import { runStorageMigration } from "./utils/storage_migration"
45

56
async function run() {
7+
runStorageMigration()
68
await Navigation.present({
79
element: <HomeView />,
810
})
@@ -11,4 +13,4 @@ async function run() {
1113
Script.exit()
1214
}
1315

14-
run()
16+
run()

Rime-Wanxiang-Updater/utils/meta.tsx

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,25 @@ function bookmarkKey(name?: string): string {
111111
return String(name ?? "").trim()
112112
}
113113

114+
function isRelatedRoot(a: string, b: string): boolean {
115+
const x = pathKey(a)
116+
const y = pathKey(b)
117+
if (!x || !y) return false
118+
if (x === y) return true
119+
const relX = new Set(relatedRoots(x).map(pathKey))
120+
if (relX.has(y)) return true
121+
const relY = new Set(relatedRoots(y).map(pathKey))
122+
return relY.has(x)
123+
}
124+
125+
function mergeMissingKinds(target: RootMetaRecords, source?: RootMetaRecords): RootMetaRecords {
126+
if (!source) return target
127+
if (!target.scheme && source.scheme) target.scheme = source.scheme
128+
if (!target.dict && source.dict) target.dict = source.dict
129+
if (!target.model && source.model) target.model = source.model
130+
return target
131+
}
132+
114133
function normalizeStore(raw: any): MetaStoreData {
115134
if (raw && typeof raw === "object" && raw.records && typeof raw.records === "object") {
116135
const records = raw.records && typeof raw.records === "object" ? (raw.records as MetaRecordsMap) : {}
@@ -212,18 +231,77 @@ function writeRecord(installRoot: string, kind: RecordKind, rec: RecordData, boo
212231
const root = normalizeRoot(installRoot)
213232
if (!root) return
214233
const data = loadStore()
215-
const canonical = pathKey(root)
216-
const bucket = data.records[canonical] ?? {}
234+
const bkey = bookmarkKey(bookmarkName)
235+
const canonicalFromPath = pathKey(root)
236+
const mappedBookmark = bkey ? pathKey(data.bookmarks[bkey] ?? "") : ""
237+
238+
let canonical = canonicalFromPath
239+
if (mappedBookmark) {
240+
if (isRelatedRoot(mappedBookmark, canonicalFromPath)) {
241+
canonical = mappedBookmark
242+
} else {
243+
// 同一书签切到新路径时,旧记录按覆盖语义清理
244+
if (data.records[mappedBookmark]) {
245+
delete data.records[mappedBookmark]
246+
}
247+
for (const aliasKey of Object.keys(data.aliases)) {
248+
const target = pathKey(data.aliases[aliasKey])
249+
if (target === mappedBookmark) delete data.aliases[aliasKey]
250+
}
251+
canonical = canonicalFromPath
252+
}
253+
} else {
254+
// 无书签名时,优先复用同路径家族已有记录
255+
for (const r of relatedRoots(canonicalFromPath)) {
256+
const rk = pathKey(r)
257+
if (rk && data.records[rk]) {
258+
canonical = rk
259+
break
260+
}
261+
const aliased = pathKey(data.aliases[rk] ?? "")
262+
if (aliased && data.records[aliased]) {
263+
canonical = aliased
264+
break
265+
}
266+
}
267+
}
268+
269+
let bucket: RootMetaRecords = { ...(data.records[canonical] ?? {}) }
270+
271+
// 合并同一路径家族中的碎片记录,避免仅更新方案/词库后丢失模型信息
272+
const candidateKeys = new Set<string>()
273+
for (const r of relatedRoots(canonicalFromPath)) {
274+
const rk = pathKey(r)
275+
if (rk) candidateKeys.add(rk)
276+
const aliased = pathKey(data.aliases[rk] ?? "")
277+
if (aliased) candidateKeys.add(aliased)
278+
}
279+
if (mappedBookmark) candidateKeys.add(mappedBookmark)
280+
for (const key of candidateKeys) {
281+
if (!key || key === canonical) continue
282+
if (data.records[key]) {
283+
bucket = mergeMissingKinds(bucket, data.records[key])
284+
delete data.records[key]
285+
}
286+
}
287+
217288
bucket[kind] = rec
218289
data.records[canonical] = bucket
290+
291+
// 清理失效 alias
292+
for (const aliasKey of Object.keys(data.aliases)) {
293+
const target = pathKey(data.aliases[aliasKey])
294+
if (!target || !data.records[target]) {
295+
delete data.aliases[aliasKey]
296+
}
297+
}
298+
219299
// 为同一路径的变体建立别名,便于通过“书签路径/实际rime路径”互相命中
220300
for (const key of relatedRoots(canonical)) {
221301
data.aliases[pathKey(key)] = canonical
222302
}
223-
const bkey = bookmarkKey(bookmarkName)
224-
if (bkey) {
225-
data.bookmarks[bkey] = canonical
226-
}
303+
data.aliases[canonicalFromPath] = canonical
304+
if (bkey) data.bookmarks[bkey] = canonical
227305
saveStore(data)
228306
}
229307

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import { Runtime } from "./runtime"
2+
3+
const CONFIG_KEY = "wanxiang_updater_config"
4+
const META_KEY = "wanxiang_meta_store"
5+
const EXTRACTED_KEY = "wanxiang_extracted_files"
6+
7+
const LEGACY_KEYS = [
8+
"wanxiang_updater_cfg_v2",
9+
"wanxiang_updater_cfg_v1",
10+
"wanxiang_meta_store_v1",
11+
"wanxiang_extracted_files_v1",
12+
]
13+
14+
const RIME_SUFFIXES = ["/RimeUserData/wanxiang", "/RIME/Rime", "/Rime"]
15+
16+
type AnyObj = Record<string, any>
17+
18+
function storage(): any {
19+
return (globalThis as any).Storage ?? Runtime.Storage
20+
}
21+
22+
function getRaw(st: any, key: string): string {
23+
const v = st?.get?.(key) ?? st?.getString?.(key)
24+
return typeof v === "string" ? v : ""
25+
}
26+
27+
function setRaw(st: any, key: string, value: string) {
28+
if (st?.set) st.set(key, value)
29+
else if (st?.setString) st.setString(key, value)
30+
}
31+
32+
function removeKey(st: any, key: string) {
33+
try {
34+
if (st?.remove) st.remove(key)
35+
} catch {}
36+
}
37+
38+
function normalizePath(p: string): string {
39+
return String(p ?? "").trim().replace(/\/+$/, "")
40+
}
41+
42+
function pathVariants(root: string): string[] {
43+
const n = normalizePath(root)
44+
if (!n) return []
45+
const out = new Set<string>([n])
46+
if (n.startsWith("/private/")) out.add(n.slice("/private".length))
47+
else if (n.startsWith("/")) out.add(`/private${n}`)
48+
return Array.from(out)
49+
}
50+
51+
function relatedRoots(root: string): string[] {
52+
const out = new Set<string>()
53+
for (const p of pathVariants(root)) {
54+
out.add(p)
55+
for (const s of RIME_SUFFIXES) {
56+
out.add(normalizePath(`${p}${s}`))
57+
if (p.endsWith(s)) out.add(normalizePath(p.slice(0, -s.length)))
58+
}
59+
}
60+
return Array.from(out).filter(Boolean)
61+
}
62+
63+
function isRelatedRoot(a: string, b: string): boolean {
64+
const x = normalizePath(a)
65+
const y = normalizePath(b)
66+
if (!x || !y) return false
67+
if (x === y) return true
68+
const s1 = new Set(relatedRoots(x))
69+
if (s1.has(y)) return true
70+
const s2 = new Set(relatedRoots(y))
71+
return s2.has(x)
72+
}
73+
74+
function parseJson(raw: string): any {
75+
try {
76+
return raw ? JSON.parse(raw) : undefined
77+
} catch {
78+
return undefined
79+
}
80+
}
81+
82+
function firstExistingRaw(st: any, keys: string[]): string {
83+
for (const key of keys) {
84+
const raw = getRaw(st, key)
85+
if (raw) return raw
86+
}
87+
return ""
88+
}
89+
90+
function mergeConfig(st: any) {
91+
const current = getRaw(st, CONFIG_KEY)
92+
if (current) return
93+
const legacy = firstExistingRaw(st, ["wanxiang_updater_cfg_v2", "wanxiang_updater_cfg_v1"])
94+
if (legacy) setRaw(st, CONFIG_KEY, legacy)
95+
}
96+
97+
function mergeExtracted(st: any) {
98+
const current = getRaw(st, EXTRACTED_KEY)
99+
if (current) return
100+
const legacy = getRaw(st, "wanxiang_extracted_files_v1")
101+
if (legacy) setRaw(st, EXTRACTED_KEY, legacy)
102+
}
103+
104+
function pickNewer(a: any, b: any): any {
105+
if (!a) return b
106+
if (!b) return a
107+
const ta = Date.parse(String(a?.apply_time ?? a?.update_time ?? ""))
108+
const tb = Date.parse(String(b?.apply_time ?? b?.update_time ?? ""))
109+
if (Number.isFinite(tb) && Number.isFinite(ta)) return tb >= ta ? b : a
110+
return b
111+
}
112+
113+
function normalizeMetaData(raw: any): { records: AnyObj; aliases: AnyObj; bookmarks: AnyObj } {
114+
if (raw && typeof raw === "object" && raw.records && typeof raw.records === "object") {
115+
return {
116+
records: raw.records as AnyObj,
117+
aliases: raw.aliases && typeof raw.aliases === "object" ? (raw.aliases as AnyObj) : {},
118+
bookmarks: raw.bookmarks && typeof raw.bookmarks === "object" ? (raw.bookmarks as AnyObj) : {},
119+
}
120+
}
121+
if (raw && typeof raw === "object") {
122+
return { records: raw as AnyObj, aliases: {}, bookmarks: {} }
123+
}
124+
return { records: {}, aliases: {}, bookmarks: {} }
125+
}
126+
127+
function cleanupMetaStore(raw: string): string {
128+
const parsed = normalizeMetaData(parseJson(raw))
129+
130+
const records: AnyObj = {}
131+
for (const [k, v] of Object.entries(parsed.records)) {
132+
const key = normalizePath(k)
133+
if (!key || !v || typeof v !== "object") continue
134+
const bucket: AnyObj = {}
135+
if ((v as AnyObj).scheme) bucket.scheme = (v as AnyObj).scheme
136+
if ((v as AnyObj).dict) bucket.dict = (v as AnyObj).dict
137+
if ((v as AnyObj).model) bucket.model = (v as AnyObj).model
138+
if (bucket.scheme || bucket.dict || bucket.model) records[key] = bucket
139+
}
140+
141+
const keys = Object.keys(records)
142+
const visited = new Set<string>()
143+
const newRecords: AnyObj = {}
144+
const newAliases: AnyObj = {}
145+
const newBookmarks: AnyObj = {}
146+
147+
const bookmarks = Object.entries(parsed.bookmarks).reduce((acc, [name, target]) => {
148+
const bk = String(name ?? "").trim()
149+
const tv = normalizePath(String(target ?? ""))
150+
if (bk && tv) acc[bk] = tv
151+
return acc
152+
}, {} as AnyObj)
153+
154+
for (const start of keys) {
155+
if (visited.has(start)) continue
156+
const cluster: string[] = []
157+
for (const k of keys) {
158+
if (!visited.has(k) && isRelatedRoot(start, k)) {
159+
visited.add(k)
160+
cluster.push(k)
161+
}
162+
}
163+
if (!cluster.length) continue
164+
165+
const bookmarkTarget = Object.values(bookmarks)
166+
.map((v) => normalizePath(String(v)))
167+
.find((target) => cluster.includes(target))
168+
const canonical =
169+
bookmarkTarget ??
170+
cluster
171+
.slice()
172+
.sort((a, b) => {
173+
const aPrivate = a.startsWith("/private/") ? 1 : 0
174+
const bPrivate = b.startsWith("/private/") ? 1 : 0
175+
if (aPrivate !== bPrivate) return aPrivate - bPrivate
176+
if (a.length !== b.length) return a.length - b.length
177+
return a.localeCompare(b)
178+
})[0]
179+
180+
const merged: AnyObj = {}
181+
for (const key of cluster) {
182+
const bucket = records[key] ?? {}
183+
merged.scheme = pickNewer(merged.scheme, bucket.scheme)
184+
merged.dict = pickNewer(merged.dict, bucket.dict)
185+
merged.model = pickNewer(merged.model, bucket.model)
186+
}
187+
if (merged.scheme || merged.dict || merged.model) {
188+
newRecords[canonical] = merged
189+
}
190+
191+
for (const key of cluster) {
192+
if (key !== canonical) newAliases[key] = canonical
193+
}
194+
for (const rel of relatedRoots(canonical)) {
195+
if (rel !== canonical) newAliases[rel] = canonical
196+
}
197+
198+
for (const [name, target] of Object.entries(bookmarks)) {
199+
if (cluster.includes(target)) newBookmarks[name] = canonical
200+
}
201+
}
202+
203+
for (const [name, target] of Object.entries(bookmarks)) {
204+
if (newBookmarks[name]) continue
205+
const t = normalizePath(String(target))
206+
if (!t) continue
207+
if (newRecords[t]) {
208+
newBookmarks[name] = t
209+
continue
210+
}
211+
const matched = Object.keys(newRecords).find((k) => isRelatedRoot(k, t))
212+
if (matched) newBookmarks[name] = matched
213+
}
214+
215+
return JSON.stringify({
216+
records: newRecords,
217+
aliases: newAliases,
218+
bookmarks: newBookmarks,
219+
})
220+
}
221+
222+
function mergeMeta(st: any) {
223+
const currentRaw = getRaw(st, META_KEY)
224+
const legacyRaw = getRaw(st, "wanxiang_meta_store_v1")
225+
const source = currentRaw || legacyRaw
226+
if (!source) return
227+
const cleaned = cleanupMetaStore(source)
228+
if (cleaned !== currentRaw) {
229+
setRaw(st, META_KEY, cleaned)
230+
}
231+
}
232+
233+
export function runStorageMigration() {
234+
const st = storage()
235+
if (!st) return
236+
237+
mergeConfig(st)
238+
mergeExtracted(st)
239+
mergeMeta(st)
240+
241+
for (const key of LEGACY_KEYS) {
242+
removeKey(st, key)
243+
}
244+
}
245+

0 commit comments

Comments
 (0)