zerochat 7.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. zerochat-7.3.0.dist-info/METADATA +70 -0
  2. zerochat-7.3.0.dist-info/RECORD +130 -0
  3. zerochat-7.3.0.dist-info/WHEEL +5 -0
  4. zerochat-7.3.0.dist-info/entry_points.txt +2 -0
  5. zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
  6. zerochat-7.3.0.dist-info/top_level.txt +2 -0
  7. zerochat.py +2004 -0
  8. zerochat_runtime/__init__.py +1 -0
  9. zerochat_runtime/assets/css/base.css +199 -0
  10. zerochat_runtime/assets/css/components/composer.css +1258 -0
  11. zerochat_runtime/assets/css/components/debug.css +777 -0
  12. zerochat_runtime/assets/css/components/header.css +97 -0
  13. zerochat_runtime/assets/css/components/icons.css +39 -0
  14. zerochat_runtime/assets/css/components/markdown.css +685 -0
  15. zerochat_runtime/assets/css/components/messages.css +473 -0
  16. zerochat_runtime/assets/css/components/modals.css +1319 -0
  17. zerochat_runtime/assets/css/components/sidebar.css +622 -0
  18. zerochat_runtime/assets/css/components/tools.css +1223 -0
  19. zerochat_runtime/assets/css/layout.css +171 -0
  20. zerochat_runtime/assets/css/print.css +120 -0
  21. zerochat_runtime/assets/css/styles.css +18 -0
  22. zerochat_runtime/assets/css/theme-overrides.css +555 -0
  23. zerochat_runtime/assets/css/tokens.css +165 -0
  24. zerochat_runtime/assets/help/architecture.html +247 -0
  25. zerochat_runtime/assets/help/composer.html +134 -0
  26. zerochat_runtime/assets/help/conversations.html +105 -0
  27. zerochat_runtime/assets/help/debug.html +108 -0
  28. zerochat_runtime/assets/help/en/architecture.html +247 -0
  29. zerochat_runtime/assets/help/en/composer.html +134 -0
  30. zerochat_runtime/assets/help/en/conversations.html +104 -0
  31. zerochat_runtime/assets/help/en/debug.html +105 -0
  32. zerochat_runtime/assets/help/en/gemini-free.html +39 -0
  33. zerochat_runtime/assets/help/en/index.html +191 -0
  34. zerochat_runtime/assets/help/en/learning.html +71 -0
  35. zerochat_runtime/assets/help/en/mcp.html +143 -0
  36. zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
  37. zerochat_runtime/assets/help/en/profiles.html +173 -0
  38. zerochat_runtime/assets/help/en/rag.html +130 -0
  39. zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
  40. zerochat_runtime/assets/help/en/tools-agent.html +133 -0
  41. zerochat_runtime/assets/help/en/webllm.html +355 -0
  42. zerochat_runtime/assets/help/gemini-free.html +39 -0
  43. zerochat_runtime/assets/help/help.css +604 -0
  44. zerochat_runtime/assets/help/help.js +51 -0
  45. zerochat_runtime/assets/help/index.html +194 -0
  46. zerochat_runtime/assets/help/learning.html +71 -0
  47. zerochat_runtime/assets/help/mcp.html +143 -0
  48. zerochat_runtime/assets/help/openrouter-free.html +39 -0
  49. zerochat_runtime/assets/help/profiles.html +174 -0
  50. zerochat_runtime/assets/help/rag.html +130 -0
  51. zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
  52. zerochat_runtime/assets/help/tools-agent.html +133 -0
  53. zerochat_runtime/assets/help/webllm.html +359 -0
  54. zerochat_runtime/assets/js/agent-core.js +1553 -0
  55. zerochat_runtime/assets/js/api.js +844 -0
  56. zerochat_runtime/assets/js/app.js +2558 -0
  57. zerochat_runtime/assets/js/attachments.js +220 -0
  58. zerochat_runtime/assets/js/charts.js +338 -0
  59. zerochat_runtime/assets/js/chat-engine.js +566 -0
  60. zerochat_runtime/assets/js/config-store.js +207 -0
  61. zerochat_runtime/assets/js/context-manager.js +584 -0
  62. zerochat_runtime/assets/js/conversation-service.js +484 -0
  63. zerochat_runtime/assets/js/cookies.js +688 -0
  64. zerochat_runtime/assets/js/data-reset-service.js +136 -0
  65. zerochat_runtime/assets/js/debug.js +508 -0
  66. zerochat_runtime/assets/js/defaults.js +16 -0
  67. zerochat_runtime/assets/js/export.js +179 -0
  68. zerochat_runtime/assets/js/file-parser.js +2088 -0
  69. zerochat_runtime/assets/js/generation-controller.js +417 -0
  70. zerochat_runtime/assets/js/i18n.js +1483 -0
  71. zerochat_runtime/assets/js/icons.js +156 -0
  72. zerochat_runtime/assets/js/ingestionEngine.js +436 -0
  73. zerochat_runtime/assets/js/markdown.js +588 -0
  74. zerochat_runtime/assets/js/mcp.js +1271 -0
  75. zerochat_runtime/assets/js/message-turns.js +62 -0
  76. zerochat_runtime/assets/js/profile-backup.js +118 -0
  77. zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
  78. zerochat_runtime/assets/js/profile-repository.js +213 -0
  79. zerochat_runtime/assets/js/providers-webllm.js +486 -0
  80. zerochat_runtime/assets/js/providers.js +1560 -0
  81. zerochat_runtime/assets/js/rag-index.js +295 -0
  82. zerochat_runtime/assets/js/rag-service.js +530 -0
  83. zerochat_runtime/assets/js/rag-ui.js +997 -0
  84. zerochat_runtime/assets/js/ragStorage.js +676 -0
  85. zerochat_runtime/assets/js/sandbox.js +449 -0
  86. zerochat_runtime/assets/js/state.js +704 -0
  87. zerochat_runtime/assets/js/storage-db.js +144 -0
  88. zerochat_runtime/assets/js/tool-cards.js +330 -0
  89. zerochat_runtime/assets/js/tool-security.js +653 -0
  90. zerochat_runtime/assets/js/tools/README.md +50 -0
  91. zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
  92. zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
  93. zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
  94. zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
  95. zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
  96. zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
  97. zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
  98. zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
  99. zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
  100. zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
  101. zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
  102. zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
  103. zerochat_runtime/assets/js/ui-composer.js +319 -0
  104. zerochat_runtime/assets/js/ui-conversation.js +655 -0
  105. zerochat_runtime/assets/js/ui-dialogs.js +132 -0
  106. zerochat_runtime/assets/js/ui-generation-status.js +119 -0
  107. zerochat_runtime/assets/js/ui-inspector.js +804 -0
  108. zerochat_runtime/assets/js/ui-mcp.js +604 -0
  109. zerochat_runtime/assets/js/ui-profiles.js +824 -0
  110. zerochat_runtime/assets/js/ui-reasoning.js +146 -0
  111. zerochat_runtime/assets/js/ui-settings.js +825 -0
  112. zerochat_runtime/assets/js/ui-shell.js +215 -0
  113. zerochat_runtime/assets/js/ui-sidebar.js +429 -0
  114. zerochat_runtime/assets/js/ui-telemetry.js +404 -0
  115. zerochat_runtime/assets/js/ui-transfer.js +262 -0
  116. zerochat_runtime/assets/js/utils.js +216 -0
  117. zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
  118. zerochat_runtime/assets/js/web-browser.js +569 -0
  119. zerochat_runtime/assets/js/web-search.js +519 -0
  120. zerochat_runtime/assets/manifest.webmanifest +20 -0
  121. zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
  122. zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
  123. zerochat_runtime/assets/services/lsp/installer.json +9 -0
  124. zerochat_runtime/assets/services/lsp/service.json +22 -0
  125. zerochat_runtime/assets/services/memory/installer.json +9 -0
  126. zerochat_runtime/assets/services/memory/service.json +24 -0
  127. zerochat_runtime/assets/services/playwright/installer.json +10 -0
  128. zerochat_runtime/assets/services/playwright/service.json +39 -0
  129. zerochat_runtime/assets/sw.js +164 -0
  130. zerochat_runtime/assets/zerochat.html +671 -0
@@ -0,0 +1,688 @@
1
+ /**
2
+ * Módulo de almacenamiento y persistencia de configuración y conversaciones (ChatStorage).
3
+ * - Configuración síncrona en localStorage / cookies (preferencias ligeras y API keys).
4
+ * - Persistencia estructurada y asíncrona de conversaciones, mensajes y adjuntos en IndexedDB (ZeroChatDB).
5
+ * - Carga bajo demanda de mensajes para optimizar el consumo de memoria.
6
+ * - Fallback transparente para entornos sin soporte de IndexedDB.
7
+ */
8
+
9
+ (function (root, factory) {
10
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
11
+ module.exports = factory(require('./storage-db.js'));
12
+ } else {
13
+ root.ChatStorage = factory(root.ZeroChatDB);
14
+ }
15
+ })(typeof self !== 'undefined' ? self : this, function (Database) {
16
+ 'use strict';
17
+
18
+ const DB_NAME = Database?.DB_NAME || 'ZeroChatDB';
19
+ const DB_VERSION = Database?.DB_VERSION || 2;
20
+ const STORE_CONVERSATIONS = Database?.STORES?.conversations || 'conversations';
21
+ const STORE_MESSAGES = Database?.STORES?.messages || 'messages';
22
+ const STORE_ATTACHMENTS = Database?.STORES?.attachments || 'attachments';
23
+
24
+ const STORAGE_PREFIX = 'zerochat_';
25
+ const DEFAULT_EXPIRY_DAYS = 365;
26
+ const BACKEND_SESSION_COOKIE = 'zerochat_backend_session_v1';
27
+ const BACKEND_SESSION_MAX_AGE_SECONDS = 26 * 60 * 60;
28
+ const memoryStorage = new Map();
29
+ const memoryConversations = new Map();
30
+ const memoryMessages = new Map();
31
+ let lastClearAllStorageError = '';
32
+
33
+ function isLocalStorageAvailable() {
34
+ try {
35
+ const testKey = '__zerochat_test__';
36
+ localStorage.setItem(testKey, testKey);
37
+ localStorage.removeItem(testKey);
38
+ return true;
39
+ } catch (e) {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ const hasLocalStorage = isLocalStorageAvailable();
45
+
46
+ function setStorageItem(name, value, days = DEFAULT_EXPIRY_DAYS) {
47
+ const key = `${STORAGE_PREFIX}${name}`;
48
+ const strVal = value ?? '';
49
+
50
+ if (hasLocalStorage) {
51
+ try {
52
+ localStorage.setItem(key, String(strVal));
53
+ } catch (e) {}
54
+ }
55
+
56
+ if (typeof document !== 'undefined' && location.protocol !== 'file:') {
57
+ try {
58
+ const d = new Date();
59
+ d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
60
+ const expires = 'expires=' + d.toUTCString();
61
+ document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(strVal)};${expires};path=/;SameSite=Lax`;
62
+ } catch (e) {}
63
+ }
64
+
65
+ memoryStorage.set(key, String(strVal));
66
+ }
67
+
68
+ function getStorageItem(name) {
69
+ const key = `${STORAGE_PREFIX}${name}`;
70
+
71
+ if (hasLocalStorage) {
72
+ try {
73
+ const item = localStorage.getItem(key);
74
+ if (item !== null) return item;
75
+ } catch (e) {}
76
+ }
77
+
78
+ if (typeof document !== 'undefined' && location.protocol !== 'file:') {
79
+ try {
80
+ const nameEQ = encodeURIComponent(key) + '=';
81
+ const ca = document.cookie.split(';');
82
+ for (let i = 0; i < ca.length; i++) {
83
+ let c = ca[i];
84
+ while (c.charAt(0) === ' ') c = c.substring(1, c.length);
85
+ if (c.indexOf(nameEQ) === 0) {
86
+ return decodeURIComponent(c.substring(nameEQ.length, c.length));
87
+ }
88
+ }
89
+ } catch (e) {}
90
+ }
91
+
92
+ if (memoryStorage.has(key)) {
93
+ return memoryStorage.get(key);
94
+ }
95
+
96
+ return null;
97
+ }
98
+
99
+ function deleteStorageItem(name) {
100
+ const key = `${STORAGE_PREFIX}${name}`;
101
+ if (hasLocalStorage) {
102
+ try {
103
+ localStorage.removeItem(key);
104
+ } catch (e) {}
105
+ }
106
+ if (typeof document !== 'undefined' && location.protocol !== 'file:') {
107
+ try {
108
+ document.cookie = `${encodeURIComponent(key)}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;SameSite=Lax`;
109
+ } catch (e) {}
110
+ }
111
+ memoryStorage.delete(key);
112
+ }
113
+
114
+ // Compatibilidad para las claves MCP previas a la centralización. Ningún
115
+ // módulo de dominio debe acceder directamente a estas claves heredadas.
116
+ function migrateLegacyStorageItem(name) {
117
+ if (typeof name !== 'string' || !/^[a-z0-9_]+$/i.test(name)) return null;
118
+ const current = getStorageItem(name);
119
+ if (current !== null || !hasLocalStorage) return current;
120
+ try {
121
+ const legacy = localStorage.getItem(name);
122
+ if (legacy === null) return null;
123
+ setStorageItem(name, legacy);
124
+ localStorage.removeItem(name);
125
+ return legacy;
126
+ } catch (_) {
127
+ return null;
128
+ }
129
+ }
130
+
131
+ function normalizeBackendSession(session) {
132
+ if (!session || typeof session !== 'object' || Array.isArray(session)) return null;
133
+ const token = typeof session.token === 'string' ? session.token : '';
134
+ const host = typeof session.host === 'string' ? session.host.toLowerCase() : '';
135
+ const port = Number(session.port);
136
+ if (!token || token.length > 512 || token.trim() !== token || /[\u0000-\u001f\u007f]/.test(token)) return null;
137
+ if (!['127.0.0.1', 'localhost'].includes(host)) return null;
138
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
139
+ return { token, host, port };
140
+ }
141
+
142
+ function getCookieValue(name) {
143
+ if (typeof document === 'undefined' || location.protocol === 'file:') return null;
144
+ try {
145
+ const prefix = `${encodeURIComponent(name)}=`;
146
+ const cookie = document.cookie.split(';').map(part => part.trim()).find(part => part.startsWith(prefix));
147
+ return cookie ? decodeURIComponent(cookie.slice(prefix.length)) : null;
148
+ } catch (_) {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function setBackendSession(session, maxAgeSeconds = BACKEND_SESSION_MAX_AGE_SECONDS) {
154
+ const normalized = normalizeBackendSession(session);
155
+ const maxAge = Number(maxAgeSeconds);
156
+ if (!normalized || !Number.isInteger(maxAge) || maxAge < 1 || typeof document === 'undefined' || location.protocol === 'file:') return false;
157
+ try {
158
+ const secure = location.protocol === 'https:' ? ';Secure' : '';
159
+ document.cookie = `${encodeURIComponent(BACKEND_SESSION_COOKIE)}=${encodeURIComponent(JSON.stringify(normalized))};Max-Age=${maxAge};Path=/;SameSite=Strict${secure}`;
160
+ return getBackendSession() !== null;
161
+ } catch (_) {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ function getBackendSession() {
167
+ const raw = getCookieValue(BACKEND_SESSION_COOKIE);
168
+ if (!raw) return null;
169
+ try {
170
+ return normalizeBackendSession(JSON.parse(raw));
171
+ } catch (_) {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ function deleteBackendSession() {
177
+ if (typeof document === 'undefined' || location.protocol === 'file:') return;
178
+ try {
179
+ const secure = location.protocol === 'https:' ? ';Secure' : '';
180
+ document.cookie = `${encodeURIComponent(BACKEND_SESSION_COOKIE)}=;Max-Age=0;Path=/;SameSite=Strict${secure}`;
181
+ } catch (_) {}
182
+ }
183
+
184
+ function loadRuntimeConfigV2() {
185
+ const raw = getStorageItem('runtime_config_v2');
186
+ if (!raw) return null;
187
+ try {
188
+ const parsed = JSON.parse(raw);
189
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
190
+ ? JSON.parse(JSON.stringify(parsed))
191
+ : null;
192
+ } catch (_) {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ function saveRuntimeConfigV2(config) {
198
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
199
+ throw new Error('La configuración operativa debe ser un objeto.');
200
+ }
201
+ setStorageItem('runtime_config_v2', JSON.stringify(config));
202
+ return true;
203
+ }
204
+
205
+ async function clearAllStorage() {
206
+ const failures = [];
207
+ lastClearAllStorageError = '';
208
+ if (hasLocalStorage) {
209
+ try {
210
+ localStorage.clear();
211
+ } catch (e) { failures.push(e); }
212
+ }
213
+
214
+ try {
215
+ if (typeof sessionStorage !== 'undefined') {
216
+ sessionStorage.clear();
217
+ }
218
+ } catch (e) { failures.push(e); }
219
+
220
+ if (typeof document !== 'undefined') {
221
+ try {
222
+ const cookies = document.cookie.split(';');
223
+ for (let i = 0; i < cookies.length; i++) {
224
+ const cookie = cookies[i];
225
+ const eqPos = cookie.indexOf('=');
226
+ const name = eqPos > -1 ? cookie.slice(0, eqPos).trim() : cookie.trim();
227
+ if (name) {
228
+ document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;SameSite=Lax`;
229
+ document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=;SameSite=Lax`;
230
+ }
231
+ }
232
+ } catch (e) { failures.push(e); }
233
+ }
234
+
235
+ memoryStorage.clear();
236
+ memoryConversations.clear();
237
+ memoryMessages.clear();
238
+
239
+ const operations = [
240
+ ['IndexedDB de ZeroChat', () => Database?.deleteOriginDatabases?.()],
241
+ ['Cache Storage', async () => {
242
+ if (typeof caches === 'undefined') return;
243
+ const results = await Promise.allSettled((await caches.keys()).map(name => caches.delete(name)));
244
+ if (results.some(result => result.status === 'rejected')) throw new Error('Cache Storage deletion failed.');
245
+ }],
246
+ ['OPFS', async () => {
247
+ if (typeof navigator === 'undefined' || !navigator.storage?.getDirectory) return;
248
+ try {
249
+ const root = await navigator.storage.getDirectory();
250
+ const removals = [];
251
+ for await (const name of root.keys()) removals.push(root.removeEntry(name, { recursive: true }));
252
+ const results = await Promise.allSettled(removals);
253
+ if (results.some(result => result.status === 'rejected')) throw new Error('OPFS deletion failed.');
254
+ } catch (error) {
255
+ // Chromium exposes OPFS on some file:// contexts but rejects access to it.
256
+ // ZeroChat never writes there, so an unavailable OPFS cannot retain its data.
257
+ if (!['SecurityError', 'NotSupportedError', 'InvalidStateError'].includes(error?.name)) throw error;
258
+ }
259
+ }],
260
+ ['Service Workers', async () => {
261
+ if (typeof navigator === 'undefined' || !navigator.serviceWorker?.getRegistrations) return;
262
+ try {
263
+ await Promise.all((await navigator.serviceWorker.getRegistrations()).map(registration => registration.unregister()));
264
+ } catch (error) {
265
+ // Chromium rejects Service Worker queries for the opaque file:// origin.
266
+ // ZeroChat cannot register a worker there, so there is nothing to remove.
267
+ if (!['SecurityError', 'NotSupportedError', 'InvalidStateError'].includes(error?.name)
268
+ && !/origin .*not supported|URL protocol .*not supported/i.test(error?.message || '')) throw error;
269
+ }
270
+ }]
271
+ ];
272
+ const results = await Promise.allSettled(operations.map(([, run]) => run()));
273
+ const operationFailures = results
274
+ .map((result, index) => result.status === 'rejected'
275
+ ? `${operations[index][0]}: ${result.reason?.message || result.reason || 'unknown error'}`
276
+ : '')
277
+ .filter(Boolean);
278
+ const allFailures = [...failures, ...operationFailures];
279
+ if (allFailures.length) {
280
+ lastClearAllStorageError = allFailures.map(error => error?.message || String(error)).join(' · ');
281
+ console.warn('[ZeroChat] Complete local-data reset failed:', lastClearAllStorageError);
282
+ return false;
283
+ }
284
+ return true;
285
+ }
286
+
287
+ function getLastClearAllStorageError() {
288
+ return lastClearAllStorageError;
289
+ }
290
+
291
+ // ==========================================================================
292
+ // Capa de Persistencia en IndexedDB para Conversaciones y Mensajes
293
+ // ==========================================================================
294
+
295
+ function isIndexedDBAvailable() {
296
+ return Database?.isAvailable ? Database.isAvailable() : (typeof indexedDB !== 'undefined' && indexedDB !== null);
297
+ }
298
+
299
+ /**
300
+ * Abre o inicializa la base de datos IndexedDB.
301
+ * @returns {Promise<IDBDatabase|null>}
302
+ */
303
+ function openDatabase() {
304
+ if (!isIndexedDBAvailable()) return Promise.resolve(null);
305
+ return Database?.openDatabase ? Database.openDatabase() : Promise.resolve(null);
306
+ }
307
+
308
+ /**
309
+ * Obtiene la lista de cabeceras de conversaciones (sin cargar todos los mensajes en memoria).
310
+ * @param {string} filterText - Filtro opcional por título
311
+ * @returns {Promise<Array<{ id: string, title: string, createdAt: number, updatedAt: number, messageCount: number }>>}
312
+ */
313
+ async function getConversationsList(filterText = '') {
314
+ const db = await openDatabase();
315
+ const filter = filterText.toLowerCase().trim();
316
+
317
+ if (!db) {
318
+ // Fallback en memoria / localStorage
319
+ const results = [];
320
+ for (const conv of memoryConversations.values()) {
321
+ if (!filter || (conv.title && conv.title.toLowerCase().includes(filter))) {
322
+ results.push({ ...conv });
323
+ }
324
+ }
325
+ return results.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
326
+ }
327
+
328
+ return new Promise((resolve) => {
329
+ try {
330
+ const tx = db.transaction([STORE_CONVERSATIONS], 'readonly');
331
+ const store = tx.objectStore(STORE_CONVERSATIONS);
332
+ const index = store.index('by_updatedAt');
333
+ const results = [];
334
+
335
+ // Iterar en orden descendente de updatedAt
336
+ const request = index.openCursor(null, 'prev');
337
+
338
+ request.onsuccess = function (event) {
339
+ const cursor = event.target.result;
340
+ if (cursor) {
341
+ const val = cursor.value;
342
+ if (!filter || (val.title && val.title.toLowerCase().includes(filter))) {
343
+ results.push({
344
+ id: val.id,
345
+ title: val.title || 'Nueva conversación',
346
+ createdAt: val.createdAt,
347
+ updatedAt: val.updatedAt,
348
+ messageCount: val.messageCount || 0,
349
+ model: val.model || '',
350
+ pinned: Boolean(val.pinned),
351
+ summary: val.summary || ''
352
+ });
353
+ }
354
+ cursor.continue();
355
+ } else {
356
+ resolve(results);
357
+ }
358
+ };
359
+
360
+ request.onerror = function () {
361
+ resolve([]);
362
+ };
363
+ } catch (e) {
364
+ console.warn('ChatStorage: Error al leer lista de conversaciones:', e);
365
+ resolve([]);
366
+ }
367
+ });
368
+ }
369
+
370
+ /**
371
+ * Carga una conversación completa con sus mensajes ordenados cronológicamente.
372
+ * @param {string} sessionId
373
+ * @returns {Promise<{ id: string, title: string, createdAt: number, updatedAt: number, history: Array } | null>}
374
+ */
375
+ async function getConversation(sessionId) {
376
+ if (!sessionId) return null;
377
+ const db = await openDatabase();
378
+
379
+ if (!db) {
380
+ const conv = memoryConversations.get(sessionId);
381
+ if (!conv) return null;
382
+ const msgs = (memoryMessages.get(sessionId) || []).sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
383
+ return { ...conv, history: msgs };
384
+ }
385
+
386
+ return new Promise((resolve) => {
387
+ try {
388
+ const tx = db.transaction([STORE_CONVERSATIONS, STORE_MESSAGES], 'readonly');
389
+ const convStore = tx.objectStore(STORE_CONVERSATIONS);
390
+ const msgStore = tx.objectStore(STORE_MESSAGES);
391
+ const msgIndex = msgStore.index('by_conversationId');
392
+
393
+ const convReq = convStore.get(sessionId);
394
+
395
+ convReq.onsuccess = function () {
396
+ const conv = convReq.result;
397
+ if (!conv) {
398
+ resolve(null);
399
+ return;
400
+ }
401
+
402
+ const msgsReq = msgIndex.getAll(sessionId);
403
+ msgsReq.onsuccess = function () {
404
+ const msgs = msgsReq.result || [];
405
+ msgs.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
406
+ resolve({
407
+ ...conv,
408
+ history: msgs
409
+ });
410
+ };
411
+ msgsReq.onerror = function () {
412
+ resolve({ ...conv, history: [] });
413
+ };
414
+ };
415
+
416
+ convReq.onerror = function () {
417
+ resolve(null);
418
+ };
419
+ } catch (e) {
420
+ console.warn('ChatStorage: Error al obtener conversación:', e);
421
+ resolve(null);
422
+ }
423
+ });
424
+ }
425
+
426
+ /**
427
+ * Guarda o actualiza una conversación y sus mensajes asociados atómicamente.
428
+ * @param {Object} sessionMeta - { id, title, createdAt, updatedAt, ... }
429
+ * @param {Array} messages - Array de mensajes del chat
430
+ * @returns {Promise<boolean>}
431
+ */
432
+ async function saveConversation(sessionMeta, messages = []) {
433
+ if (!sessionMeta || !sessionMeta.id) return false;
434
+ const db = await openDatabase();
435
+ const sessionId = sessionMeta.id;
436
+ const now = Date.now();
437
+
438
+ const conversationRecord = {
439
+ id: sessionId,
440
+ title: sessionMeta.title || 'Nueva conversación',
441
+ createdAt: sessionMeta.createdAt || now,
442
+ updatedAt: sessionMeta.updatedAt || now,
443
+ messageCount: messages.length,
444
+ model: sessionMeta.model || '',
445
+ summary: sessionMeta.summary || '',
446
+ tags: sessionMeta.tags || [],
447
+ pinned: Boolean(sessionMeta.pinned),
448
+ metadata: sessionMeta.metadata || {}
449
+ };
450
+
451
+ if (!db) {
452
+ memoryConversations.set(sessionId, conversationRecord);
453
+ memoryMessages.set(sessionId, [...messages]);
454
+ return true;
455
+ }
456
+
457
+ return new Promise((resolve) => {
458
+ try {
459
+ const tx = db.transaction([STORE_CONVERSATIONS, STORE_MESSAGES], 'readwrite');
460
+ const convStore = tx.objectStore(STORE_CONVERSATIONS);
461
+ const msgStore = tx.objectStore(STORE_MESSAGES);
462
+ const msgIndex = msgStore.index('by_conversationId');
463
+
464
+ convStore.put(conversationRecord);
465
+
466
+ // Borrar mensajes existentes de esta conversación y re-insertar actualizados
467
+ const delReq = msgIndex.openCursor(sessionId);
468
+ delReq.onsuccess = function (e) {
469
+ const cursor = e.target.result;
470
+ if (cursor) {
471
+ cursor.delete();
472
+ cursor.continue();
473
+ } else {
474
+ // Insertar mensajes actualizados garantizando IDs únicos por mensaje
475
+ const seenMsgIds = new Set();
476
+ messages.forEach((m, idx) => {
477
+ let msgId = m.id;
478
+ if (!msgId || seenMsgIds.has(msgId)) {
479
+ msgId = `msg_${sessionId}_${idx}_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
480
+ }
481
+ seenMsgIds.add(msgId);
482
+
483
+ const msgRecord = {
484
+ id: msgId,
485
+ conversationId: sessionId,
486
+ role: m.role || 'user',
487
+ content: m.content !== undefined ? m.content : '',
488
+ tool_calls: m.tool_calls || null,
489
+ tool_call_id: m.tool_call_id || null,
490
+ name: m.name || null,
491
+ stats: m.stats || null,
492
+ createdAt: m.createdAt || (conversationRecord.createdAt + idx * 10)
493
+ };
494
+ if (m.contextDateAnchor) msgRecord.contextDateAnchor = m.contextDateAnchor;
495
+ if (m.images) msgRecord.images = m.images;
496
+ msgStore.put(msgRecord);
497
+ });
498
+ }
499
+ };
500
+
501
+ tx.oncomplete = function () {
502
+ resolve(true);
503
+ };
504
+
505
+ tx.onerror = function (e) {
506
+ console.warn('ChatStorage: Error en transacción saveConversation:', e.target.error);
507
+ resolve(false);
508
+ };
509
+ } catch (e) {
510
+ console.warn('ChatStorage: Excepción al guardar conversación:', e);
511
+ resolve(false);
512
+ }
513
+ });
514
+ }
515
+
516
+ /**
517
+ * Elimina una conversación y todos sus mensajes y adjuntos asociados.
518
+ * @param {string} sessionId
519
+ * @returns {Promise<boolean>}
520
+ */
521
+ async function deleteConversation(sessionId) {
522
+ if (!sessionId) return false;
523
+ const db = await openDatabase();
524
+
525
+ if (!db) {
526
+ memoryConversations.delete(sessionId);
527
+ memoryMessages.delete(sessionId);
528
+ return true;
529
+ }
530
+
531
+ return new Promise((resolve) => {
532
+ try {
533
+ const tx = db.transaction([STORE_CONVERSATIONS, STORE_MESSAGES, STORE_ATTACHMENTS], 'readwrite');
534
+ const convStore = tx.objectStore(STORE_CONVERSATIONS);
535
+ const msgStore = tx.objectStore(STORE_MESSAGES);
536
+ const msgIndex = msgStore.index('by_conversationId');
537
+
538
+ convStore.delete(sessionId);
539
+
540
+ const delMsgs = msgIndex.openCursor(sessionId);
541
+ delMsgs.onsuccess = function (e) {
542
+ const cursor = e.target.result;
543
+ if (cursor) {
544
+ cursor.delete();
545
+ cursor.continue();
546
+ }
547
+ };
548
+
549
+ tx.oncomplete = function () {
550
+ resolve(true);
551
+ };
552
+
553
+ tx.onerror = function (e) {
554
+ console.warn('ChatStorage: Error al eliminar conversación:', e.target.error);
555
+ resolve(false);
556
+ };
557
+ } catch (e) {
558
+ console.warn('ChatStorage: Excepción al eliminar conversación:', e);
559
+ resolve(false);
560
+ }
561
+ });
562
+ }
563
+
564
+ /**
565
+ * Elimina todas las conversaciones y mensajes guardados.
566
+ * @returns {Promise<boolean>}
567
+ */
568
+ async function deleteAllConversations() {
569
+ const db = await openDatabase();
570
+
571
+ if (!db) {
572
+ memoryConversations.clear();
573
+ memoryMessages.clear();
574
+ return true;
575
+ }
576
+
577
+ return new Promise((resolve) => {
578
+ try {
579
+ const tx = db.transaction([STORE_CONVERSATIONS, STORE_MESSAGES, STORE_ATTACHMENTS], 'readwrite');
580
+ tx.objectStore(STORE_CONVERSATIONS).clear();
581
+ tx.objectStore(STORE_MESSAGES).clear();
582
+ tx.objectStore(STORE_ATTACHMENTS).clear();
583
+
584
+ tx.oncomplete = function () {
585
+ resolve(true);
586
+ };
587
+
588
+ tx.onerror = function (e) {
589
+ console.warn('ChatStorage: Error al vaciar base de datos:', e.target.error);
590
+ resolve(false);
591
+ };
592
+ } catch (e) {
593
+ console.warn('ChatStorage: Excepción al vaciar conversaciones:', e);
594
+ resolve(false);
595
+ }
596
+ });
597
+ }
598
+
599
+ /**
600
+ * Renombra una conversación.
601
+ * @param {string} sessionId
602
+ * @param {string} newTitle
603
+ * @returns {Promise<boolean>}
604
+ */
605
+ async function renameConversation(sessionId, newTitle) {
606
+ if (!sessionId || !newTitle) return false;
607
+ const db = await openDatabase();
608
+
609
+ if (!db) {
610
+ const conv = memoryConversations.get(sessionId);
611
+ if (conv) {
612
+ conv.title = newTitle.trim();
613
+ conv.updatedAt = Date.now();
614
+ return true;
615
+ }
616
+ return false;
617
+ }
618
+
619
+ return new Promise((resolve) => {
620
+ try {
621
+ const tx = db.transaction([STORE_CONVERSATIONS], 'readwrite');
622
+ const store = tx.objectStore(STORE_CONVERSATIONS);
623
+ const req = store.get(sessionId);
624
+
625
+ req.onsuccess = function () {
626
+ const conv = req.result;
627
+ if (conv) {
628
+ conv.title = newTitle.trim();
629
+ conv.updatedAt = Date.now();
630
+ store.put(conv);
631
+ }
632
+ };
633
+
634
+ tx.oncomplete = function () {
635
+ resolve(true);
636
+ };
637
+
638
+ tx.onerror = function () {
639
+ resolve(false);
640
+ };
641
+ } catch (e) {
642
+ resolve(false);
643
+ }
644
+ });
645
+ }
646
+
647
+ /** Inicializa el almacenamiento IndexedDB. */
648
+ async function initDB() {
649
+ return openDatabase();
650
+ }
651
+
652
+ return {
653
+ // Configuración Síncrona (localStorage / cookies)
654
+ setStorageItem,
655
+ getStorageItem,
656
+ deleteStorageItem,
657
+ migrateLegacyStorageItem,
658
+ // Alias heredados para compatibilidad hacia atrás en consola de depuración (@deprecated)
659
+ setCookie: setStorageItem,
660
+ getCookie: getStorageItem,
661
+ deleteCookie: deleteStorageItem,
662
+ // Sesión efímera del backend local. Se mantiene separada de localStorage.
663
+ normalizeBackendSession,
664
+ setBackendSession,
665
+ getBackendSession,
666
+ deleteBackendSession,
667
+ loadRuntimeConfigV2,
668
+ saveRuntimeConfigV2,
669
+ clearAllStorage,
670
+ getLastClearAllStorageError,
671
+
672
+ // Persistencia Asíncrona en IndexedDB (Conversaciones & Mensajes)
673
+ initDB,
674
+ getConversationsList,
675
+ getConversation,
676
+ saveConversation,
677
+ deleteConversation,
678
+ deleteAllConversations,
679
+ renameConversation,
680
+
681
+ // Constantes de esquema exportadas
682
+ DB_NAME,
683
+ DB_VERSION,
684
+ STORE_CONVERSATIONS,
685
+ STORE_MESSAGES,
686
+ STORE_ATTACHMENTS
687
+ };
688
+ });