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.
- zerochat-7.3.0.dist-info/METADATA +70 -0
- zerochat-7.3.0.dist-info/RECORD +130 -0
- zerochat-7.3.0.dist-info/WHEEL +5 -0
- zerochat-7.3.0.dist-info/entry_points.txt +2 -0
- zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
- zerochat-7.3.0.dist-info/top_level.txt +2 -0
- zerochat.py +2004 -0
- zerochat_runtime/__init__.py +1 -0
- zerochat_runtime/assets/css/base.css +199 -0
- zerochat_runtime/assets/css/components/composer.css +1258 -0
- zerochat_runtime/assets/css/components/debug.css +777 -0
- zerochat_runtime/assets/css/components/header.css +97 -0
- zerochat_runtime/assets/css/components/icons.css +39 -0
- zerochat_runtime/assets/css/components/markdown.css +685 -0
- zerochat_runtime/assets/css/components/messages.css +473 -0
- zerochat_runtime/assets/css/components/modals.css +1319 -0
- zerochat_runtime/assets/css/components/sidebar.css +622 -0
- zerochat_runtime/assets/css/components/tools.css +1223 -0
- zerochat_runtime/assets/css/layout.css +171 -0
- zerochat_runtime/assets/css/print.css +120 -0
- zerochat_runtime/assets/css/styles.css +18 -0
- zerochat_runtime/assets/css/theme-overrides.css +555 -0
- zerochat_runtime/assets/css/tokens.css +165 -0
- zerochat_runtime/assets/help/architecture.html +247 -0
- zerochat_runtime/assets/help/composer.html +134 -0
- zerochat_runtime/assets/help/conversations.html +105 -0
- zerochat_runtime/assets/help/debug.html +108 -0
- zerochat_runtime/assets/help/en/architecture.html +247 -0
- zerochat_runtime/assets/help/en/composer.html +134 -0
- zerochat_runtime/assets/help/en/conversations.html +104 -0
- zerochat_runtime/assets/help/en/debug.html +105 -0
- zerochat_runtime/assets/help/en/gemini-free.html +39 -0
- zerochat_runtime/assets/help/en/index.html +191 -0
- zerochat_runtime/assets/help/en/learning.html +71 -0
- zerochat_runtime/assets/help/en/mcp.html +143 -0
- zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
- zerochat_runtime/assets/help/en/profiles.html +173 -0
- zerochat_runtime/assets/help/en/rag.html +130 -0
- zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
- zerochat_runtime/assets/help/en/tools-agent.html +133 -0
- zerochat_runtime/assets/help/en/webllm.html +355 -0
- zerochat_runtime/assets/help/gemini-free.html +39 -0
- zerochat_runtime/assets/help/help.css +604 -0
- zerochat_runtime/assets/help/help.js +51 -0
- zerochat_runtime/assets/help/index.html +194 -0
- zerochat_runtime/assets/help/learning.html +71 -0
- zerochat_runtime/assets/help/mcp.html +143 -0
- zerochat_runtime/assets/help/openrouter-free.html +39 -0
- zerochat_runtime/assets/help/profiles.html +174 -0
- zerochat_runtime/assets/help/rag.html +130 -0
- zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
- zerochat_runtime/assets/help/tools-agent.html +133 -0
- zerochat_runtime/assets/help/webllm.html +359 -0
- zerochat_runtime/assets/js/agent-core.js +1553 -0
- zerochat_runtime/assets/js/api.js +844 -0
- zerochat_runtime/assets/js/app.js +2558 -0
- zerochat_runtime/assets/js/attachments.js +220 -0
- zerochat_runtime/assets/js/charts.js +338 -0
- zerochat_runtime/assets/js/chat-engine.js +566 -0
- zerochat_runtime/assets/js/config-store.js +207 -0
- zerochat_runtime/assets/js/context-manager.js +584 -0
- zerochat_runtime/assets/js/conversation-service.js +484 -0
- zerochat_runtime/assets/js/cookies.js +688 -0
- zerochat_runtime/assets/js/data-reset-service.js +136 -0
- zerochat_runtime/assets/js/debug.js +508 -0
- zerochat_runtime/assets/js/defaults.js +16 -0
- zerochat_runtime/assets/js/export.js +179 -0
- zerochat_runtime/assets/js/file-parser.js +2088 -0
- zerochat_runtime/assets/js/generation-controller.js +417 -0
- zerochat_runtime/assets/js/i18n.js +1483 -0
- zerochat_runtime/assets/js/icons.js +156 -0
- zerochat_runtime/assets/js/ingestionEngine.js +436 -0
- zerochat_runtime/assets/js/markdown.js +588 -0
- zerochat_runtime/assets/js/mcp.js +1271 -0
- zerochat_runtime/assets/js/message-turns.js +62 -0
- zerochat_runtime/assets/js/profile-backup.js +118 -0
- zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
- zerochat_runtime/assets/js/profile-repository.js +213 -0
- zerochat_runtime/assets/js/providers-webllm.js +486 -0
- zerochat_runtime/assets/js/providers.js +1560 -0
- zerochat_runtime/assets/js/rag-index.js +295 -0
- zerochat_runtime/assets/js/rag-service.js +530 -0
- zerochat_runtime/assets/js/rag-ui.js +997 -0
- zerochat_runtime/assets/js/ragStorage.js +676 -0
- zerochat_runtime/assets/js/sandbox.js +449 -0
- zerochat_runtime/assets/js/state.js +704 -0
- zerochat_runtime/assets/js/storage-db.js +144 -0
- zerochat_runtime/assets/js/tool-cards.js +330 -0
- zerochat_runtime/assets/js/tool-security.js +653 -0
- zerochat_runtime/assets/js/tools/README.md +50 -0
- zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
- zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
- zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
- zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
- zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
- zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
- zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
- zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
- zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
- zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
- zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
- zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
- zerochat_runtime/assets/js/ui-composer.js +319 -0
- zerochat_runtime/assets/js/ui-conversation.js +655 -0
- zerochat_runtime/assets/js/ui-dialogs.js +132 -0
- zerochat_runtime/assets/js/ui-generation-status.js +119 -0
- zerochat_runtime/assets/js/ui-inspector.js +804 -0
- zerochat_runtime/assets/js/ui-mcp.js +604 -0
- zerochat_runtime/assets/js/ui-profiles.js +824 -0
- zerochat_runtime/assets/js/ui-reasoning.js +146 -0
- zerochat_runtime/assets/js/ui-settings.js +825 -0
- zerochat_runtime/assets/js/ui-shell.js +215 -0
- zerochat_runtime/assets/js/ui-sidebar.js +429 -0
- zerochat_runtime/assets/js/ui-telemetry.js +404 -0
- zerochat_runtime/assets/js/ui-transfer.js +262 -0
- zerochat_runtime/assets/js/utils.js +216 -0
- zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
- zerochat_runtime/assets/js/web-browser.js +569 -0
- zerochat_runtime/assets/js/web-search.js +519 -0
- zerochat_runtime/assets/manifest.webmanifest +20 -0
- zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
- zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
- zerochat_runtime/assets/services/lsp/installer.json +9 -0
- zerochat_runtime/assets/services/lsp/service.json +22 -0
- zerochat_runtime/assets/services/memory/installer.json +9 -0
- zerochat_runtime/assets/services/memory/service.json +24 -0
- zerochat_runtime/assets/services/playwright/installer.json +10 -0
- zerochat_runtime/assets/services/playwright/service.json +39 -0
- zerochat_runtime/assets/sw.js +164 -0
- zerochat_runtime/assets/zerochat.html +671 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Pure conversation-turn rules shared by state, engine, service and views. */
|
|
2
|
+
(function (root, factory) {
|
|
3
|
+
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
4
|
+
module.exports = factory();
|
|
5
|
+
} else {
|
|
6
|
+
root.ChatMessageTurns = factory();
|
|
7
|
+
}
|
|
8
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
function extractBaseId(id) {
|
|
12
|
+
if (!id || typeof id !== 'string') return '';
|
|
13
|
+
return id.replace(/(?:_turn_\d+_(?:assistant|tool.*)|_final)$/, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isDateTimeInitialTurn(message) {
|
|
17
|
+
if (!message || message.role !== 'user') return false;
|
|
18
|
+
const content = typeof message.content === 'string' ? message.content : (message.content?.[0]?.text || '');
|
|
19
|
+
return content.startsWith('La fecha y hora actual es:') ||
|
|
20
|
+
content.startsWith('Fecha y hora actual:') ||
|
|
21
|
+
content.startsWith('The current date and time is:') ||
|
|
22
|
+
content.startsWith('Current date and time:');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Callers normalize their public selection contracts before reaching this rule.
|
|
26
|
+
function removeSelectedTurn(history, { msgId = '', baseId = '', explicitIds = [] } = {}) {
|
|
27
|
+
if (!Array.isArray(history)) return [];
|
|
28
|
+
const ids = new Set(Array.isArray(explicitIds) || explicitIds instanceof Set ? explicitIds : []);
|
|
29
|
+
if (msgId) ids.add(msgId);
|
|
30
|
+
if (baseId) ids.add(baseId);
|
|
31
|
+
const isTarget = message => {
|
|
32
|
+
const id = message?.id;
|
|
33
|
+
return Boolean(id && (ids.has(id) ||
|
|
34
|
+
(baseId && id.startsWith(`${baseId}_`)) ||
|
|
35
|
+
(msgId && id.startsWith(`${msgId}_`))));
|
|
36
|
+
};
|
|
37
|
+
const deletedCalls = new Set();
|
|
38
|
+
for (const message of history) {
|
|
39
|
+
if (!isTarget(message)) continue;
|
|
40
|
+
if (message.role === 'assistant' && Array.isArray(message.tool_calls)) {
|
|
41
|
+
for (const call of message.tool_calls) if (call?.id) deletedCalls.add(call.id);
|
|
42
|
+
}
|
|
43
|
+
if (message.role === 'tool' && message.tool_call_id) deletedCalls.add(message.tool_call_id);
|
|
44
|
+
}
|
|
45
|
+
const remaining = history.filter(message => message && !isTarget(message) &&
|
|
46
|
+
!(message.role === 'tool' && message.tool_call_id && deletedCalls.has(message.tool_call_id)));
|
|
47
|
+
const sanitized = [];
|
|
48
|
+
for (const message of remaining) {
|
|
49
|
+
if (message.role === 'tool') {
|
|
50
|
+
const previous = sanitized[sanitized.length - 1];
|
|
51
|
+
const matches = previous?.role === 'assistant' && Array.isArray(previous.tool_calls) &&
|
|
52
|
+
previous.tool_calls.some(call => call && (call.id === message.tool_call_id ||
|
|
53
|
+
(call.function && call.function.name === message.name)));
|
|
54
|
+
if (!matches) continue;
|
|
55
|
+
}
|
|
56
|
+
sanitized.push(message);
|
|
57
|
+
}
|
|
58
|
+
return sanitized;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { extractBaseId, isDateTimeInitialTurn, removeSelectedTurn };
|
|
62
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable, encrypted backups for connection-profile definitions.
|
|
3
|
+
* The embedded key deliberately provides transport/privacy protection only:
|
|
4
|
+
* anyone with the ZeroChat source can recover it.
|
|
5
|
+
*/
|
|
6
|
+
(function (root, factory) {
|
|
7
|
+
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
8
|
+
module.exports = factory(require('node:crypto').webcrypto);
|
|
9
|
+
} else {
|
|
10
|
+
root.ChatProfileBackup = factory(root.crypto);
|
|
11
|
+
}
|
|
12
|
+
}(typeof self !== 'undefined' ? self : this, function (cryptoApi) {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const FORMAT = 'zerochat-profile-backup';
|
|
16
|
+
const VERSION = 1;
|
|
17
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
18
|
+
const MASTER_KEY = 'ZeroChat profile transfer key v1';
|
|
19
|
+
|
|
20
|
+
function cryptoOrThrow() {
|
|
21
|
+
if (!cryptoApi?.subtle || typeof cryptoApi.getRandomValues !== 'function') {
|
|
22
|
+
throw new Error('El navegador no permite cifrar copias de perfiles.');
|
|
23
|
+
}
|
|
24
|
+
return cryptoApi;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function encodeBase64(bytes) {
|
|
28
|
+
if (typeof Buffer !== 'undefined') return Buffer.from(bytes).toString('base64');
|
|
29
|
+
let binary = '';
|
|
30
|
+
bytes.forEach(byte => { binary += String.fromCharCode(byte); });
|
|
31
|
+
return btoa(binary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decodeBase64(value) {
|
|
35
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
36
|
+
throw new Error('El archivo de perfiles cifrado no es válido.');
|
|
37
|
+
}
|
|
38
|
+
if (typeof Buffer !== 'undefined') return new Uint8Array(Buffer.from(value, 'base64'));
|
|
39
|
+
const binary = atob(value);
|
|
40
|
+
return Uint8Array.from(binary, char => char.charCodeAt(0));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function getKey() {
|
|
44
|
+
const c = cryptoOrThrow();
|
|
45
|
+
const material = new TextEncoder().encode(MASTER_KEY);
|
|
46
|
+
const digest = await c.subtle.digest('SHA-256', material);
|
|
47
|
+
return c.subtle.importKey('raw', digest, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateProfiles(profiles) {
|
|
51
|
+
if (!Array.isArray(profiles) || profiles.length > 100) throw new Error('La copia contiene una lista de perfiles no válida.');
|
|
52
|
+
profiles.forEach(profile => {
|
|
53
|
+
if (!profile || typeof profile !== 'object' || Array.isArray(profile) ||
|
|
54
|
+
typeof profile.id !== 'string' || typeof profile.name !== 'string' ||
|
|
55
|
+
!profile.name.trim() || !profile.settings || typeof profile.settings !== 'object' || Array.isArray(profile.settings)) {
|
|
56
|
+
throw new Error('La copia contiene un perfil no válido.');
|
|
57
|
+
}
|
|
58
|
+
if (profile.id.length > 256 || profile.name.length > 256 || String(profile.description || '').length > 10000) {
|
|
59
|
+
throw new Error('La copia contiene campos de perfil demasiado largos.');
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return profiles;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function encryptProfiles(profiles) {
|
|
66
|
+
validateProfiles(profiles);
|
|
67
|
+
const c = cryptoOrThrow();
|
|
68
|
+
const plaintext = new TextEncoder().encode(JSON.stringify({ format: FORMAT, version: VERSION, profiles }));
|
|
69
|
+
if (plaintext.byteLength > MAX_FILE_BYTES) throw new Error('La copia de perfiles supera el tamaño permitido.');
|
|
70
|
+
const iv = c.getRandomValues(new Uint8Array(12));
|
|
71
|
+
const ciphertext = await c.subtle.encrypt({ name: 'AES-GCM', iv }, await getKey(), plaintext);
|
|
72
|
+
return JSON.stringify({ format: FORMAT, version: VERSION, algorithm: 'AES-GCM', iv: encodeBase64(iv), ciphertext: encodeBase64(new Uint8Array(ciphertext)) });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function encryptApiKey(value) {
|
|
76
|
+
const c = cryptoOrThrow();
|
|
77
|
+
const iv = c.getRandomValues(new Uint8Array(12));
|
|
78
|
+
const ciphertext = await c.subtle.encrypt({ name: 'AES-GCM', iv }, await getKey(), new TextEncoder().encode(String(value || '')));
|
|
79
|
+
return { format: 'zerochat-profile-secret', version: VERSION, algorithm: 'AES-GCM', iv: encodeBase64(iv), ciphertext: encodeBase64(new Uint8Array(ciphertext)) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateApiKeySecret(secret) {
|
|
83
|
+
if (!secret || typeof secret !== 'object' || Array.isArray(secret) ||
|
|
84
|
+
secret.format !== 'zerochat-profile-secret' || secret.version !== VERSION || secret.algorithm !== 'AES-GCM') {
|
|
85
|
+
throw new Error('La API key cifrada no es válida.');
|
|
86
|
+
}
|
|
87
|
+
const iv = decodeBase64(secret.iv);
|
|
88
|
+
const ciphertext = decodeBase64(secret.ciphertext);
|
|
89
|
+
if (iv.length !== 12 || ciphertext.length < 16) throw new Error('La API key cifrada no es válida.');
|
|
90
|
+
return { iv, ciphertext };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function decryptApiKey(secret) {
|
|
94
|
+
const { iv, ciphertext } = validateApiKeySecret(secret);
|
|
95
|
+
const plaintext = await cryptoOrThrow().subtle.decrypt({ name: 'AES-GCM', iv }, await getKey(), ciphertext);
|
|
96
|
+
return new TextDecoder().decode(plaintext);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function decryptProfiles(serialized) {
|
|
100
|
+
if (typeof serialized !== 'string' || serialized.length > MAX_FILE_BYTES * 2) throw new Error('El archivo de perfiles supera el tamaño permitido.');
|
|
101
|
+
let envelope;
|
|
102
|
+
try { envelope = JSON.parse(serialized); } catch (_) { throw new Error('El archivo de perfiles no es JSON válido.'); }
|
|
103
|
+
if (!envelope || envelope.format !== FORMAT || envelope.version !== VERSION || envelope.algorithm !== 'AES-GCM') {
|
|
104
|
+
throw new Error('El formato de copia de perfiles no es compatible.');
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const plaintext = await cryptoOrThrow().subtle.decrypt({ name: 'AES-GCM', iv: decodeBase64(envelope.iv) }, await getKey(), decodeBase64(envelope.ciphertext));
|
|
108
|
+
const document = JSON.parse(new TextDecoder().decode(plaintext));
|
|
109
|
+
if (!document || document.format !== FORMAT || document.version !== VERSION) throw new Error('invalid');
|
|
110
|
+
return validateProfiles(document.profiles);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error?.message?.startsWith('La copia')) throw error;
|
|
113
|
+
throw new Error('No se pudo descifrar o validar la copia de perfiles.');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { FORMAT, VERSION, MAX_FILE_BYTES, encryptProfiles, decryptProfiles, encryptApiKey, decryptApiKey, validateApiKeySecret };
|
|
118
|
+
}));
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generador de HTML autónomo para importación de perfiles via postMessage.
|
|
3
|
+
* Sistema único sin fallbacks: el HTML exportado abre zerochat.html en modo importación
|
|
4
|
+
* y transfiere los perfiles cifrados mediante postMessage cross-origin.
|
|
5
|
+
*/
|
|
6
|
+
(function (root, factory) {
|
|
7
|
+
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
8
|
+
module.exports = factory();
|
|
9
|
+
} else {
|
|
10
|
+
root.ChatProfileExportBundle = factory();
|
|
11
|
+
}
|
|
12
|
+
}(typeof self !== 'undefined' ? self : this, function () {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detecta la URL base de zerochat.html según el contexto actual
|
|
17
|
+
*/
|
|
18
|
+
function getZeroChatURL() {
|
|
19
|
+
if (typeof window === 'undefined') return './zerochat.html';
|
|
20
|
+
|
|
21
|
+
const protocol = window.location.protocol;
|
|
22
|
+
const host = window.location.host;
|
|
23
|
+
const pathname = window.location.pathname;
|
|
24
|
+
const basePath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
|
|
25
|
+
|
|
26
|
+
// Si estamos en http/https, construir URL absoluta
|
|
27
|
+
if (protocol.startsWith('http')) {
|
|
28
|
+
return `${protocol}//${host}${basePath}zerochat.html`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Si estamos en file://, usar URL relativa
|
|
32
|
+
// Asumimos que zerochat.html está en el mismo directorio
|
|
33
|
+
return 'zerochat.html';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Genera el HTML autónomo con el bundle cifrado embebido
|
|
38
|
+
* @param {string} encryptedPayload - JSON string del payload cifrado
|
|
39
|
+
* @param {object} metadata - Metadatos (profileCount, exportDate)
|
|
40
|
+
* @returns {string} HTML completo listo para descargar
|
|
41
|
+
*/
|
|
42
|
+
function generateExportHTML(encryptedPayload, metadata) {
|
|
43
|
+
const zeroChatURL = getZeroChatURL();
|
|
44
|
+
const exportDate = new Date().toLocaleString('es-ES', {
|
|
45
|
+
year: 'numeric',
|
|
46
|
+
month: '2-digit',
|
|
47
|
+
day: '2-digit',
|
|
48
|
+
hour: '2-digit',
|
|
49
|
+
minute: '2-digit'
|
|
50
|
+
});
|
|
51
|
+
const profileCount = metadata?.profileCount || 0;
|
|
52
|
+
|
|
53
|
+
return `<!DOCTYPE html>
|
|
54
|
+
<html lang="es">
|
|
55
|
+
<head>
|
|
56
|
+
<meta charset="UTF-8">
|
|
57
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
58
|
+
<meta name="description" content="ZeroChat - Archivo de importación de perfiles cifrados">
|
|
59
|
+
<title>ZeroChat - Importar ${profileCount} Perfil${profileCount !== 1 ? 'es' : ''}</title>
|
|
60
|
+
<style>
|
|
61
|
+
* {
|
|
62
|
+
margin: 0;
|
|
63
|
+
padding: 0;
|
|
64
|
+
box-sizing: border-box;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
body {
|
|
68
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
69
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
70
|
+
min-height: 100vh;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
padding: 20px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.container {
|
|
78
|
+
background: white;
|
|
79
|
+
border-radius: 16px;
|
|
80
|
+
padding: 24px;
|
|
81
|
+
max-width: 600px;
|
|
82
|
+
width: 100%;
|
|
83
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
84
|
+
text-align: center;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@media (min-width: 600px) {
|
|
88
|
+
.container {
|
|
89
|
+
padding: 32px;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.icon {
|
|
94
|
+
font-size: 44px;
|
|
95
|
+
margin-bottom: 16px;
|
|
96
|
+
animation: pulse 2s ease-in-out infinite;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@keyframes pulse {
|
|
100
|
+
0%, 100% { transform: scale(1); }
|
|
101
|
+
50% { transform: scale(1.05); }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
h1 {
|
|
105
|
+
color: #333;
|
|
106
|
+
font-size: 28px;
|
|
107
|
+
margin-bottom: 10px;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.subtitle {
|
|
112
|
+
color: #666;
|
|
113
|
+
font-size: 14px;
|
|
114
|
+
margin-bottom: 30px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.info-box {
|
|
118
|
+
background: #f8f9fa;
|
|
119
|
+
border-left: 4px solid #667eea;
|
|
120
|
+
padding: 16px 18px;
|
|
121
|
+
margin: 16px 0;
|
|
122
|
+
text-align: left;
|
|
123
|
+
border-radius: 8px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.info-box h2 {
|
|
127
|
+
font-size: 16px;
|
|
128
|
+
color: #333;
|
|
129
|
+
margin-bottom: 12px;
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.info-box p {
|
|
134
|
+
font-size: 14px;
|
|
135
|
+
color: #555;
|
|
136
|
+
line-height: 1.6;
|
|
137
|
+
margin: 8px 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.import-button {
|
|
141
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
142
|
+
color: white;
|
|
143
|
+
border: none;
|
|
144
|
+
padding: 16px 32px;
|
|
145
|
+
font-size: 16px;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
border-radius: 8px;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
margin: 20px 0;
|
|
150
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
151
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.import-button:hover:not(:disabled) {
|
|
155
|
+
transform: translateY(-2px);
|
|
156
|
+
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.import-button:active:not(:disabled) {
|
|
160
|
+
transform: translateY(0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.import-button:disabled {
|
|
164
|
+
opacity: 0.6;
|
|
165
|
+
cursor: not-allowed;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.status {
|
|
169
|
+
padding: 20px;
|
|
170
|
+
border-radius: 12px;
|
|
171
|
+
margin: 20px 0;
|
|
172
|
+
font-size: 16px;
|
|
173
|
+
line-height: 1.6;
|
|
174
|
+
font-weight: 500;
|
|
175
|
+
transition: all 0.3s ease;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.status.loading {
|
|
179
|
+
background: #e3f2fd;
|
|
180
|
+
color: #1565c0;
|
|
181
|
+
border: 2px solid #1976d2;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.status.success {
|
|
185
|
+
background: #e8f5e9;
|
|
186
|
+
color: #2e7d32;
|
|
187
|
+
border: 2px solid #4caf50;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.status.error {
|
|
191
|
+
background: #ffebee;
|
|
192
|
+
color: #c62828;
|
|
193
|
+
border: 2px solid #f44336;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.spinner {
|
|
197
|
+
display: inline-block;
|
|
198
|
+
width: 18px;
|
|
199
|
+
height: 18px;
|
|
200
|
+
border: 3px solid #1976d2;
|
|
201
|
+
border-top-color: transparent;
|
|
202
|
+
border-radius: 50%;
|
|
203
|
+
animation: spin 0.8s linear infinite;
|
|
204
|
+
margin-right: 10px;
|
|
205
|
+
vertical-align: middle;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@keyframes spin {
|
|
209
|
+
to { transform: rotate(360deg); }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.meta {
|
|
213
|
+
margin-top: 30px;
|
|
214
|
+
padding-top: 20px;
|
|
215
|
+
border-top: 2px solid #f0f0f0;
|
|
216
|
+
font-size: 13px;
|
|
217
|
+
color: #666;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.meta-row {
|
|
221
|
+
display: flex;
|
|
222
|
+
justify-content: space-between;
|
|
223
|
+
margin: 8px 0;
|
|
224
|
+
padding: 8px 12px;
|
|
225
|
+
background: #f8f9fa;
|
|
226
|
+
border-radius: 6px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.meta-label {
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
color: #555;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.meta-value {
|
|
235
|
+
color: #666;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.help-text {
|
|
239
|
+
font-size: 12px;
|
|
240
|
+
color: #999;
|
|
241
|
+
margin-top: 20px;
|
|
242
|
+
line-height: 1.5;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.warning-box {
|
|
246
|
+
background: #fff3cd;
|
|
247
|
+
border-left: 4px solid #ffc107;
|
|
248
|
+
padding: 16px;
|
|
249
|
+
margin-top: 20px;
|
|
250
|
+
text-align: left;
|
|
251
|
+
border-radius: 8px;
|
|
252
|
+
font-size: 13px;
|
|
253
|
+
color: #856404;
|
|
254
|
+
line-height: 1.5;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.warning-box strong {
|
|
258
|
+
display: block;
|
|
259
|
+
margin-bottom: 8px;
|
|
260
|
+
color: #856404;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#status.hidden {
|
|
264
|
+
display: none;
|
|
265
|
+
}
|
|
266
|
+
</style>
|
|
267
|
+
</head>
|
|
268
|
+
<body>
|
|
269
|
+
<div class="container">
|
|
270
|
+
<div class="icon">🔐</div>
|
|
271
|
+
<h1>ZeroChat</h1>
|
|
272
|
+
<div class="subtitle">Importación de Perfiles Cifrados</div>
|
|
273
|
+
|
|
274
|
+
<div class="info-box">
|
|
275
|
+
<h2>📦 Contenido del paquete</h2>
|
|
276
|
+
<p><strong>${profileCount}</strong> perfil${profileCount !== 1 ? 'es' : ''} de conexión cifrado${profileCount !== 1 ? 's' : ''}</p>
|
|
277
|
+
<p>Exportado: <strong>${exportDate}</strong></p>
|
|
278
|
+
<p>Formato: <strong>AES-GCM</strong></p>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div class="info-box">
|
|
282
|
+
<h2>🔄 Proceso de importación</h2>
|
|
283
|
+
<p>Al hacer clic en el botón, se abrirá ZeroChat en una nueva pestaña donde podrás importar ${profileCount !== 1 ? 'estos perfiles' : 'este perfil'} de forma segura.</p>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<button id="importButton" class="import-button">
|
|
287
|
+
🚀 Importar Perfil${profileCount !== 1 ? 'es' : ''} en ZeroChat
|
|
288
|
+
</button>
|
|
289
|
+
|
|
290
|
+
<div id="status" class="status loading hidden">
|
|
291
|
+
<span class="spinner"></span>
|
|
292
|
+
Abriendo ZeroChat y transfiriendo perfiles...
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div class="warning-box">
|
|
296
|
+
<strong>⚠️ Nota sobre permisos del navegador</strong>
|
|
297
|
+
Es posible que tu navegador muestre un aviso solicitando permiso para abrir una nueva pestaña.
|
|
298
|
+
Esto es normal por seguridad. Si aparece, autoriza la acción para continuar con la importación.
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<script>
|
|
303
|
+
(function() {
|
|
304
|
+
'use strict';
|
|
305
|
+
|
|
306
|
+
// Bundle cifrado embebido (generado en tiempo de exportación)
|
|
307
|
+
const ENCRYPTED_PAYLOAD = ${JSON.stringify(encryptedPayload)};
|
|
308
|
+
const TARGET_URL = ${JSON.stringify(zeroChatURL + '#mode=import')};
|
|
309
|
+
const METADATA = ${JSON.stringify(metadata)};
|
|
310
|
+
const TIMEOUT_MS = 20000; // 20 segundos
|
|
311
|
+
|
|
312
|
+
// Referencias DOM
|
|
313
|
+
const statusEl = document.getElementById('status');
|
|
314
|
+
const importButton = document.getElementById('importButton');
|
|
315
|
+
|
|
316
|
+
// Estado
|
|
317
|
+
let targetWindow = null;
|
|
318
|
+
let timeoutId = null;
|
|
319
|
+
let messageReceived = false;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Actualizar el estado visual
|
|
323
|
+
*/
|
|
324
|
+
function updateStatus(message, type) {
|
|
325
|
+
statusEl.className = 'status ' + type;
|
|
326
|
+
statusEl.classList.remove('hidden');
|
|
327
|
+
statusEl.innerHTML = message;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Manejar mensajes desde zerochat.html
|
|
332
|
+
*/
|
|
333
|
+
function handleMessage(event) {
|
|
334
|
+
// Mensaje de "estoy listo" desde zerochat
|
|
335
|
+
if (event.data === 'zerochat_ready') {
|
|
336
|
+
console.log('[Export] ZeroChat ready, sending payload...');
|
|
337
|
+
|
|
338
|
+
if (targetWindow && !targetWindow.closed) {
|
|
339
|
+
// Enviar el payload cifrado
|
|
340
|
+
targetWindow.postMessage({
|
|
341
|
+
type: 'import_profiles',
|
|
342
|
+
version: 1,
|
|
343
|
+
payload: ENCRYPTED_PAYLOAD,
|
|
344
|
+
metadata: METADATA
|
|
345
|
+
}, '*');
|
|
346
|
+
|
|
347
|
+
updateStatus(
|
|
348
|
+
'<span class="spinner"></span>Datos enviados, esperando confirmación...',
|
|
349
|
+
'loading'
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
updateStatus(
|
|
353
|
+
'❌ La ventana de ZeroChat se cerró inesperadamente.',
|
|
354
|
+
'error'
|
|
355
|
+
);
|
|
356
|
+
importButton.disabled = false;
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Resultado de la importación
|
|
362
|
+
if (event.data && event.data.type === 'import_result') {
|
|
363
|
+
messageReceived = true;
|
|
364
|
+
clearTimeout(timeoutId);
|
|
365
|
+
|
|
366
|
+
if (event.data.success) {
|
|
367
|
+
const result = event.data.result || {};
|
|
368
|
+
updateStatus(
|
|
369
|
+
'✅ Perfiles importados correctamente<br>' +
|
|
370
|
+
'<small style="font-size: 14px;">' +
|
|
371
|
+
(result.added || 0) + ' añadido(s), ' +
|
|
372
|
+
(result.replaced || 0) + ' actualizado(s)' +
|
|
373
|
+
'</small><br><br>' +
|
|
374
|
+
'<small style="font-size: 13px; opacity: 0.8;">Esta ventana se cerrará en 3 segundos...</small>',
|
|
375
|
+
'success'
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// Auto-cerrar después de 3 segundos
|
|
379
|
+
setTimeout(() => {
|
|
380
|
+
window.close();
|
|
381
|
+
}, 3000);
|
|
382
|
+
} else {
|
|
383
|
+
const errorMsg = event.data.error || 'Error desconocido';
|
|
384
|
+
updateStatus(
|
|
385
|
+
'❌ Error al importar perfiles<br>' +
|
|
386
|
+
'<small style="font-size: 14px;">' + errorMsg + '</small>',
|
|
387
|
+
'error'
|
|
388
|
+
);
|
|
389
|
+
importButton.disabled = false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Inicializar el proceso de importación (llamado desde el botón)
|
|
396
|
+
*/
|
|
397
|
+
function startImport() {
|
|
398
|
+
console.log('[Export] Starting import process...');
|
|
399
|
+
console.log('[Export] Target URL:', TARGET_URL);
|
|
400
|
+
console.log('[Export] Payload size:', ENCRYPTED_PAYLOAD.length, 'bytes');
|
|
401
|
+
|
|
402
|
+
// Deshabilitar el botón
|
|
403
|
+
importButton.disabled = true;
|
|
404
|
+
updateStatus(
|
|
405
|
+
'<span class="spinner"></span>Abriendo ZeroChat...',
|
|
406
|
+
'loading'
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
// Abrir zerochat.html en nueva pestaña con parámetro mode=import
|
|
411
|
+
// Ejecutado desde user gesture (click) para mejorar compatibilidad
|
|
412
|
+
targetWindow = window.open(TARGET_URL, 'zerochat_import');
|
|
413
|
+
|
|
414
|
+
// Verificar si el popup fue bloqueado
|
|
415
|
+
if (!targetWindow || targetWindow.closed || typeof targetWindow.closed === 'undefined') {
|
|
416
|
+
console.error('[Export] Popup blocked');
|
|
417
|
+
updateStatus(
|
|
418
|
+
'❌ No se pudo abrir ZeroChat<br><br>' +
|
|
419
|
+
'<small style="font-size: 14px;">' +
|
|
420
|
+
'Tu navegador bloqueó la apertura de la nueva pestaña. ' +
|
|
421
|
+
'Por favor, <strong>permite ventanas emergentes</strong> para este sitio ' +
|
|
422
|
+
'y haz clic de nuevo en el botón.' +
|
|
423
|
+
'</small>',
|
|
424
|
+
'error'
|
|
425
|
+
);
|
|
426
|
+
importButton.disabled = false;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
console.log('[Export] Window opened successfully');
|
|
431
|
+
|
|
432
|
+
// Escuchar mensajes desde zerochat
|
|
433
|
+
window.addEventListener('message', handleMessage);
|
|
434
|
+
|
|
435
|
+
// Timeout de seguridad
|
|
436
|
+
timeoutId = setTimeout(() => {
|
|
437
|
+
if (!messageReceived) {
|
|
438
|
+
console.error('[Export] Timeout waiting for response');
|
|
439
|
+
updateStatus(
|
|
440
|
+
'⏱️ Timeout: ZeroChat no respondió en ' + (TIMEOUT_MS / 1000) + ' segundos<br><br>' +
|
|
441
|
+
'<small style="font-size: 14px;">' +
|
|
442
|
+
'Verifica que la pestaña de ZeroChat se abrió correctamente. ' +
|
|
443
|
+
'Si es necesario, haz clic de nuevo en el botón.' +
|
|
444
|
+
'</small>',
|
|
445
|
+
'error'
|
|
446
|
+
);
|
|
447
|
+
importButton.disabled = false;
|
|
448
|
+
}
|
|
449
|
+
}, TIMEOUT_MS);
|
|
450
|
+
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error('[Export] Error opening window:', error);
|
|
453
|
+
updateStatus(
|
|
454
|
+
'❌ Error al abrir ZeroChat<br><br>' +
|
|
455
|
+
'<small style="font-size: 14px;">' + error.message + '</small>',
|
|
456
|
+
'error'
|
|
457
|
+
);
|
|
458
|
+
importButton.disabled = false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Conectar el botón al proceso de importación
|
|
463
|
+
importButton.addEventListener('click', startImport);
|
|
464
|
+
|
|
465
|
+
// Limpiar al cerrar
|
|
466
|
+
window.addEventListener('beforeunload', () => {
|
|
467
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
468
|
+
window.removeEventListener('message', handleMessage);
|
|
469
|
+
});
|
|
470
|
+
})();
|
|
471
|
+
</script>
|
|
472
|
+
</body>
|
|
473
|
+
</html>`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Exportar API pública
|
|
477
|
+
return {
|
|
478
|
+
generateExportHTML,
|
|
479
|
+
getZeroChatURL
|
|
480
|
+
};
|
|
481
|
+
}));
|