Created
April 18, 2026 17:41
-
-
Save lumpsoid/201e51a0759f64f5b04a68bd80fd398c to your computer and use it in GitHub Desktop.
openrouter-full-backup.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| async function fullBackup() { | |
| // --- find db --- | |
| const dbs = await indexedDB.databases(); | |
| const dbEntry = dbs.find(db => db.name.includes(':org_')) | |
| ?? dbs.find(db => db.name.startsWith('openrouter:playground:')); | |
| if (!dbEntry) { | |
| console.error('No OpenRouter database found. Make sure you are on openrouter.ai'); | |
| return; | |
| } | |
| // --- open --- | |
| const db = await new Promise((res, rej) => { | |
| const r = indexedDB.open(dbEntry.name); | |
| r.onsuccess = e => res(e.target.result); | |
| r.onerror = e => rej(e.target.error); | |
| }); | |
| // the object store name matches the db name | |
| const storeName = dbEntry.name; | |
| if (![...db.objectStoreNames].includes(storeName)) { | |
| console.error(`Object store "${storeName}" not found. Stores available:`, [...db.objectStoreNames]); | |
| db.close(); | |
| return; | |
| } | |
| // --- dump all records --- | |
| const all = await new Promise((res, rej) => { | |
| const tx = db.transaction(storeName, 'readonly'); | |
| const req = tx.objectStore(storeName).getAll(); | |
| req.onsuccess = () => res(req.result); | |
| req.onerror = e => rej(e.target.error); | |
| }); | |
| db.close(); | |
| // --- bucket by prefix --- | |
| const rooms = {}; | |
| const manifests = {}; | |
| const characters = {}; | |
| const messages = {}; | |
| const items = {}; | |
| let master = null; | |
| for (const { key, value } of all) { | |
| if (key.startsWith('v3:room:')) rooms[value.id] = value; | |
| else if (key.startsWith('v3:manifest:')) manifests[key.replace('v3:manifest:', '')] = value; | |
| else if (key.startsWith('v3:character:')) characters[value.id] = value; | |
| else if (key.startsWith('v3:message:')) messages[value.id] = value; | |
| else if (key.startsWith('v3:item:')) items[value.id] = value; | |
| else if (key.startsWith('v3:master:')) master = value; | |
| } | |
| console.log(`Loaded — rooms:${Object.keys(rooms).length} manifests:${Object.keys(manifests).length} chars:${Object.keys(characters).length} msgs:${Object.keys(messages).length} items:${Object.keys(items).length}`); | |
| // --- helpers --- | |
| function itemToText(item) { | |
| const content = item?.data?.content; | |
| if (!Array.isArray(content)) return null; | |
| return content.map(b => b.text ?? b.content ?? '').filter(Boolean).join('\n') || null; | |
| } | |
| // --- assemble --- | |
| const roomIds = master?.roomIds ?? Object.keys(rooms); | |
| const exportedRooms = roomIds.map(roomId => { | |
| const room = rooms[roomId]; | |
| const manifest = manifests[roomId]; | |
| if (!room || !manifest) return null; | |
| const roomChars = (manifest.characterIds ?? []) | |
| .map(cid => characters[cid]) | |
| .filter(Boolean) | |
| .map(c => ({ | |
| id: c.id, | |
| model: c.model, | |
| samplingParameters: c.samplingParameters, | |
| reasoning: c.reasoning ?? null, | |
| isRemoved: c.isRemoved ?? false, | |
| isDisabled: c.isDisabled ?? false, | |
| })); | |
| const charModelMap = Object.fromEntries(roomChars.map(c => [c.id, c.model])); | |
| const roomMessages = (manifest.messageIds ?? []) | |
| .map(mid => messages[mid]) | |
| .filter(Boolean) | |
| .map(msg => { | |
| const isUser = msg.characterId === 'USER'; | |
| const role = isUser ? 'user' : 'assistant'; | |
| const model = isUser ? null : (charModelMap[msg.characterId] ?? msg.characterId); | |
| const resolvedItems = (msg.items ?? []).map(ref => ({ | |
| type: ref.type, | |
| id: ref.id, | |
| text: itemToText(items[ref.id]), | |
| })); | |
| return { | |
| id: msg.id, | |
| role, | |
| model, | |
| createdAt: msg.createdAt, | |
| updatedAt: msg.updatedAt, | |
| parentMessageId: msg.parentMessageId ?? null, | |
| isEdited: msg.isEdited ?? false, | |
| context: msg.context ?? null, | |
| text: resolvedItems.filter(i => i.type === 'message') .map(i => i.text).filter(Boolean).join('\n') || null, | |
| reasoning: resolvedItems.filter(i => i.type === 'reasoning').map(i => i.text).filter(Boolean).join('\n') || null, | |
| metadata: msg.metadata ?? null, | |
| }; | |
| }); | |
| return { | |
| id: room.id, | |
| title: room.title, | |
| createdAt: room.createdAt, | |
| updatedAt: room.updatedAt, | |
| lastActivityAt: room.lastActivityAt ?? null, | |
| isPinned: room.isPinned ?? false, | |
| characters: roomChars, | |
| messages: roomMessages, | |
| }; | |
| }).filter(Boolean); | |
| // --- download --- | |
| const payload = { | |
| exportedAt: new Date().toISOString(), | |
| dbName, | |
| totalRooms: exportedRooms.length, | |
| totalMessages: exportedRooms.reduce((n, r) => n + r.messages.length, 0), | |
| rooms: exportedRooms, | |
| }; | |
| const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = `openrouter-backup-${Date.now()}.json`; | |
| a.click(); | |
| URL.revokeObjectURL(a.href); | |
| console.log(`✅ Exported ${payload.totalRooms} rooms, ${payload.totalMessages} messages`); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment