@iaforged/context-code 1.0.57 → 1.0.59
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.
|
@@ -118,6 +118,55 @@ function truncate(text, max) {
|
|
|
118
118
|
return text;
|
|
119
119
|
return `${text.slice(0, max - 1)}…`;
|
|
120
120
|
}
|
|
121
|
+
/** Escapa caracteres especiales de HTML para envío seguro a Telegram con parse_mode HTML. */
|
|
122
|
+
export function escapeHTML(text) {
|
|
123
|
+
return text
|
|
124
|
+
.replace(/&/g, '&')
|
|
125
|
+
.replace(/</g, '<')
|
|
126
|
+
.replace(/>/g, '>')
|
|
127
|
+
.replace(/"/g, '"');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Convierte Markdown básico a HTML compatible con Telegram.
|
|
131
|
+
* Soporta: bloques de código, código inline, negritas, itálicas, tachado.
|
|
132
|
+
* El texto fuera de esos patrones se escapa para evitar errores de parseo.
|
|
133
|
+
*/
|
|
134
|
+
export function markdownToTelegramHTML(text) {
|
|
135
|
+
const result = [];
|
|
136
|
+
// Procesa bloque a bloque para no escapar lo que ya es HTML
|
|
137
|
+
const codeBlockRe = /```(\w*)\n?([\s\S]*?)```/g;
|
|
138
|
+
let lastIndex = 0;
|
|
139
|
+
let match;
|
|
140
|
+
while ((match = codeBlockRe.exec(text)) !== null) {
|
|
141
|
+
// Texto antes del bloque de código
|
|
142
|
+
if (match.index > lastIndex) {
|
|
143
|
+
result.push(inlineMarkdownToHTML(text.slice(lastIndex, match.index)));
|
|
144
|
+
}
|
|
145
|
+
const code = match[2] ?? '';
|
|
146
|
+
result.push(`<pre><code>${escapeHTML(code.trimEnd())}</code></pre>`);
|
|
147
|
+
lastIndex = match.index + match[0].length;
|
|
148
|
+
}
|
|
149
|
+
// Resto del texto
|
|
150
|
+
if (lastIndex < text.length) {
|
|
151
|
+
result.push(inlineMarkdownToHTML(text.slice(lastIndex)));
|
|
152
|
+
}
|
|
153
|
+
return result.join('');
|
|
154
|
+
}
|
|
155
|
+
function inlineMarkdownToHTML(text) {
|
|
156
|
+
// Escape HTML primero, luego restauramos los marcadores de formato
|
|
157
|
+
let out = escapeHTML(text);
|
|
158
|
+
// Código inline: `...`
|
|
159
|
+
out = out.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
160
|
+
// Negrita: **texto** o __texto__
|
|
161
|
+
out = out.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
162
|
+
out = out.replace(/__(.+?)__/g, '<b>$1</b>');
|
|
163
|
+
// Itálica: *texto* o _texto_ (no conflicta con negrita ya procesada)
|
|
164
|
+
out = out.replace(/\*(.+?)\*/g, '<i>$1</i>');
|
|
165
|
+
out = out.replace(/_(.+?)_/g, '<i>$1</i>');
|
|
166
|
+
// Tachado: ~~texto~~
|
|
167
|
+
out = out.replace(/~~(.+?)~~/g, '<s>$1</s>');
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
121
170
|
/** Divide un texto en trozos ≤ `max` chars, cortando en newline cuando se puede. */
|
|
122
171
|
export function splitForChannel(text, max) {
|
|
123
172
|
if (text.length <= max)
|
|
@@ -5,6 +5,7 @@ import { getOpenAICompatibleAccessToken } from '../utils/auth.js';
|
|
|
5
5
|
import { getConfiguredProviderBaseUrl } from '../utils/model/providerBaseUrls.js';
|
|
6
6
|
import { getStoredActiveProviderPreference, getStoredLastModelForProvider, setStoredActiveProviderPreference, setStoredLastModelForProvider, } from '../utils/model/providerProfilesDb.js';
|
|
7
7
|
import { setMirrorBot } from './mirror.js';
|
|
8
|
+
import { markdownToTelegramHTML, escapeHTML } from '../mirrors/shared.js';
|
|
8
9
|
let activeBotInstance = null;
|
|
9
10
|
let inboundHandler = null;
|
|
10
11
|
export function setTelegramInboundHandler(handler) {
|
|
@@ -159,14 +160,14 @@ export async function startTelegramBridge() {
|
|
|
159
160
|
}
|
|
160
161
|
const model = getActiveModel();
|
|
161
162
|
const provider = getActiveProvider();
|
|
162
|
-
await ctx.reply(`🚀
|
|
163
|
-
`📡 Proveedor:
|
|
164
|
-
`🤖 Modelo:
|
|
163
|
+
await ctx.reply(`🚀 <b>Context Code Bridge Activo</b>\n\n` +
|
|
164
|
+
`📡 Proveedor: <b>${escapeHTML(provider)}</b>\n` +
|
|
165
|
+
`🤖 Modelo: <b>${escapeHTML(model)}</b>\n\n` +
|
|
165
166
|
`Escríbeme cualquier pregunta y la enviaré al modelo.\n\n` +
|
|
166
|
-
|
|
167
|
+
`<b>Comandos disponibles:</b>\n` +
|
|
167
168
|
`/status - Ver estado actual\n` +
|
|
168
|
-
`/model
|
|
169
|
-
`/provider
|
|
169
|
+
`/model <nombre> - Cambiar modelo\n` +
|
|
170
|
+
`/provider <nombre> - Cambiar proveedor`, { parse_mode: 'HTML' });
|
|
170
171
|
});
|
|
171
172
|
// Comando /status
|
|
172
173
|
bot.command('status', async (ctx) => {
|
|
@@ -176,10 +177,10 @@ export async function startTelegramBridge() {
|
|
|
176
177
|
return;
|
|
177
178
|
const model = getActiveModel();
|
|
178
179
|
const provider = getActiveProvider();
|
|
179
|
-
await ctx.reply(`📊
|
|
180
|
-
`📡 Proveedor:
|
|
181
|
-
`🤖 Modelo:
|
|
182
|
-
`🔗 Bridge: ✅ Conectado`, { parse_mode: '
|
|
180
|
+
await ctx.reply(`📊 <b>Estado de Context Code</b>\n\n` +
|
|
181
|
+
`📡 Proveedor: <b>${escapeHTML(provider)}</b>\n` +
|
|
182
|
+
`🤖 Modelo: <b>${escapeHTML(model)}</b>\n` +
|
|
183
|
+
`🔗 Bridge: ✅ Conectado`, { parse_mode: 'HTML' });
|
|
183
184
|
});
|
|
184
185
|
// Comando /model
|
|
185
186
|
bot.command('model', async (ctx) => {
|
|
@@ -190,15 +191,15 @@ export async function startTelegramBridge() {
|
|
|
190
191
|
const newModel = ctx.match?.trim();
|
|
191
192
|
if (!newModel) {
|
|
192
193
|
const model = getActiveModel();
|
|
193
|
-
await ctx.reply(`🤖 Modelo actual:
|
|
194
|
-
parse_mode: '
|
|
194
|
+
await ctx.reply(`🤖 Modelo actual: <b>${escapeHTML(model)}</b>`, {
|
|
195
|
+
parse_mode: 'HTML',
|
|
195
196
|
});
|
|
196
197
|
return;
|
|
197
198
|
}
|
|
198
199
|
const provider = getActiveProvider();
|
|
199
200
|
setStoredLastModelForProvider(provider, newModel);
|
|
200
|
-
await ctx.reply(`✅ Modelo cambiado a
|
|
201
|
-
parse_mode: '
|
|
201
|
+
await ctx.reply(`✅ Modelo cambiado a <b>${escapeHTML(newModel)}</b>`, {
|
|
202
|
+
parse_mode: 'HTML',
|
|
202
203
|
});
|
|
203
204
|
});
|
|
204
205
|
// Comando /provider
|
|
@@ -210,8 +211,8 @@ export async function startTelegramBridge() {
|
|
|
210
211
|
const newProvider = ctx.match?.trim();
|
|
211
212
|
if (!newProvider) {
|
|
212
213
|
const provider = getActiveProvider();
|
|
213
|
-
await ctx.reply(`📡 Proveedor actual:
|
|
214
|
-
parse_mode: '
|
|
214
|
+
await ctx.reply(`📡 Proveedor actual: <b>${escapeHTML(provider)}</b>`, {
|
|
215
|
+
parse_mode: 'HTML',
|
|
215
216
|
});
|
|
216
217
|
return;
|
|
217
218
|
}
|
|
@@ -229,8 +230,8 @@ export async function startTelegramBridge() {
|
|
|
229
230
|
return;
|
|
230
231
|
}
|
|
231
232
|
setStoredActiveProviderPreference(newProvider.toLowerCase());
|
|
232
|
-
await ctx.reply(`✅ Proveedor cambiado a
|
|
233
|
-
parse_mode: '
|
|
233
|
+
await ctx.reply(`✅ Proveedor cambiado a <b>${escapeHTML(newProvider)}</b>`, {
|
|
234
|
+
parse_mode: 'HTML',
|
|
234
235
|
});
|
|
235
236
|
});
|
|
236
237
|
// Mensajes de texto libre
|
|
@@ -266,13 +267,15 @@ export async function startTelegramBridge() {
|
|
|
266
267
|
const provider = getActiveProvider();
|
|
267
268
|
await ctx.replyWithChatAction('typing');
|
|
268
269
|
const response = await queryModel(ctx.message.text, model, provider);
|
|
269
|
-
const
|
|
270
|
+
const htmlResponse = markdownToTelegramHTML(response);
|
|
271
|
+
const chunks = splitMessage(htmlResponse);
|
|
270
272
|
for (const chunk of chunks) {
|
|
271
273
|
try {
|
|
272
|
-
await ctx.reply(chunk, { parse_mode: '
|
|
274
|
+
await ctx.reply(chunk, { parse_mode: 'HTML' });
|
|
273
275
|
}
|
|
274
276
|
catch {
|
|
275
|
-
|
|
277
|
+
// Fallback: texto plano sin formato
|
|
278
|
+
await ctx.reply(chunk.replace(/<[^>]+>/g, ''));
|
|
276
279
|
}
|
|
277
280
|
}
|
|
278
281
|
});
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { getPrimaryChatId } from './config.js';
|
|
2
|
-
import { consumeInjectedMarkIfSameOrigin, markInjected, splitForChannel, } from '../mirrors/shared.js';
|
|
2
|
+
import { consumeInjectedMarkIfSameOrigin, markInjected, markdownToTelegramHTML, escapeHTML, splitForChannel, } from '../mirrors/shared.js';
|
|
3
3
|
const PREFIX = {
|
|
4
4
|
user: '👤',
|
|
5
5
|
assistant: '🤖',
|
|
6
6
|
tool: '🔧',
|
|
7
7
|
};
|
|
8
|
+
function formatMirrorMessage(role, text) {
|
|
9
|
+
const icon = PREFIX[role];
|
|
10
|
+
if (role === 'assistant') {
|
|
11
|
+
// Convierte Markdown del modelo a HTML
|
|
12
|
+
return `${icon} ${markdownToTelegramHTML(text)}`;
|
|
13
|
+
}
|
|
14
|
+
if (role === 'tool') {
|
|
15
|
+
// Herramientas en bloque de código inline para distinguirlas
|
|
16
|
+
return `${icon} <code>${escapeHTML(text)}</code>`;
|
|
17
|
+
}
|
|
18
|
+
// Usuario: texto plano escapado
|
|
19
|
+
return `${icon} <b>${escapeHTML(text)}</b>`;
|
|
20
|
+
}
|
|
8
21
|
const TELEGRAM_MAX = 4000;
|
|
9
22
|
const SEND_INTERVAL_MS = 150;
|
|
10
23
|
const MAX_QUEUE = 200;
|
|
@@ -49,14 +62,20 @@ async function drain() {
|
|
|
49
62
|
const chatId = getPrimaryChatId();
|
|
50
63
|
if (!chatId)
|
|
51
64
|
break;
|
|
52
|
-
const
|
|
65
|
+
const formatted = formatMirrorMessage(item.role, item.text);
|
|
66
|
+
const chunks = splitForChannel(formatted, TELEGRAM_MAX);
|
|
53
67
|
for (const chunk of chunks) {
|
|
54
68
|
try {
|
|
55
|
-
await botRef.api.sendMessage(chatId, chunk);
|
|
69
|
+
await botRef.api.sendMessage(chatId, chunk, { parse_mode: 'HTML' });
|
|
56
70
|
}
|
|
57
71
|
catch {
|
|
58
|
-
//
|
|
59
|
-
|
|
72
|
+
// Fallback sin formato si el HTML falla (e.g. tag roto por corte de chunk)
|
|
73
|
+
try {
|
|
74
|
+
await botRef.api.sendMessage(chatId, `${PREFIX[item.role]} ${item.text}`.slice(0, TELEGRAM_MAX));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Si el send falla (token inválido, user bloqueó, etc.) no reintentamos
|
|
78
|
+
}
|
|
60
79
|
break;
|
|
61
80
|
}
|
|
62
81
|
await sleep(SEND_INTERVAL_MS);
|
|
@@ -17,6 +17,9 @@ function getDatabasePath() {
|
|
|
17
17
|
function getKeyPath() {
|
|
18
18
|
return join(getClaudeConfigHomeDir(), KEY_FILENAME);
|
|
19
19
|
}
|
|
20
|
+
function getLegacyCredentialsPath() {
|
|
21
|
+
return join(getClaudeConfigHomeDir(), '.credentials.json');
|
|
22
|
+
}
|
|
20
23
|
let _db = null;
|
|
21
24
|
let _legacyMigrationAttempted = false;
|
|
22
25
|
let _cachedEncryptionKey = null;
|
|
@@ -81,6 +84,20 @@ function savePayload(db, plainText) {
|
|
|
81
84
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
82
85
|
`).run('main_storage', encryptPayload(plainText));
|
|
83
86
|
}
|
|
87
|
+
function getStoredPayload() {
|
|
88
|
+
const db = getDb();
|
|
89
|
+
if (!db)
|
|
90
|
+
return null;
|
|
91
|
+
try {
|
|
92
|
+
const row = db
|
|
93
|
+
.prepare('SELECT value FROM secure_storage WHERE key = ?')
|
|
94
|
+
.get('main_storage');
|
|
95
|
+
return typeof row?.value === 'string' ? row.value : null;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
84
101
|
function getDb() {
|
|
85
102
|
if (_db)
|
|
86
103
|
return _db;
|
|
@@ -177,3 +194,24 @@ export const sqliteStorage = {
|
|
|
177
194
|
}
|
|
178
195
|
}
|
|
179
196
|
};
|
|
197
|
+
export function getSecureStorageDbPath() {
|
|
198
|
+
return getDatabasePath();
|
|
199
|
+
}
|
|
200
|
+
export function getSecureStorageKeyPath() {
|
|
201
|
+
return getKeyPath();
|
|
202
|
+
}
|
|
203
|
+
export function getLegacyCredentialsFilePath() {
|
|
204
|
+
return getLegacyCredentialsPath();
|
|
205
|
+
}
|
|
206
|
+
export function hasLegacyCredentialsFile() {
|
|
207
|
+
try {
|
|
208
|
+
return getFsImplementation().existsSync(getLegacyCredentialsPath());
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export function isSecureStorageEncrypted() {
|
|
215
|
+
const payload = getStoredPayload();
|
|
216
|
+
return payload ? payload.startsWith(ENCRYPTED_PREFIX) : false;
|
|
217
|
+
}
|
package/dist/src/utils/status.js
CHANGED
|
@@ -20,6 +20,7 @@ import { checkInstall } from './nativeInstaller/index.js';
|
|
|
20
20
|
import { getProxyUrl } from './proxy.js';
|
|
21
21
|
import { SandboxManager } from './sandbox/sandbox-adapter.js';
|
|
22
22
|
import { getSecureStorage } from './secureStorage/index.js';
|
|
23
|
+
import { getLegacyCredentialsFilePath, getSecureStorageDbPath, getSecureStorageKeyPath, hasLegacyCredentialsFile, isSecureStorageEncrypted, } from './secureStorage/sqliteStorage.js';
|
|
23
24
|
import { getSettingsWithAllErrors } from './settings/allErrors.js';
|
|
24
25
|
import { getEnabledSettingSources, getSettingSourceDisplayNameCapitalized } from './settings/constants.js';
|
|
25
26
|
import { getManagedFileSettingsPresence, getPolicySettingsOrigin, getSettingsForSource } from './settings/settings.js';
|
|
@@ -313,8 +314,30 @@ export function buildAPIProviderProperties() {
|
|
|
313
314
|
});
|
|
314
315
|
properties.push({
|
|
315
316
|
label: 'Credenciales providers',
|
|
316
|
-
value: getSecureStorage().name === 'sqlite'
|
|
317
|
+
value: getSecureStorage().name === 'sqlite'
|
|
318
|
+
? 'SQLite (cifrado local)'
|
|
319
|
+
: getSecureStorage().name,
|
|
317
320
|
});
|
|
321
|
+
if (getSecureStorage().name === 'sqlite') {
|
|
322
|
+
properties.push({
|
|
323
|
+
label: 'Credenciales DB',
|
|
324
|
+
value: getSecureStorageDbPath(),
|
|
325
|
+
});
|
|
326
|
+
properties.push({
|
|
327
|
+
label: 'Clave cifrado',
|
|
328
|
+
value: getSecureStorageKeyPath(),
|
|
329
|
+
});
|
|
330
|
+
properties.push({
|
|
331
|
+
label: 'Estado cifrado credenciales',
|
|
332
|
+
value: isSecureStorageEncrypted() ? 'Cifrado activo' : 'Sin payload o pendiente',
|
|
333
|
+
});
|
|
334
|
+
properties.push({
|
|
335
|
+
label: 'Legacy credentials file',
|
|
336
|
+
value: hasLegacyCredentialsFile()
|
|
337
|
+
? `Presente (${getLegacyCredentialsFilePath()})`
|
|
338
|
+
: 'No presente',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
318
341
|
if (apiProvider !== 'firstParty') {
|
|
319
342
|
const providerLabels = {
|
|
320
343
|
bedrock: 'AWS Bedrock',
|
|
@@ -180,6 +180,15 @@ export async function startLoginWithQr(options) {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
closeSocketSafe(sock);
|
|
183
|
+
// Force-save credentials before closing so they are on disk even if Baileys
|
|
184
|
+
// hasn't emitted a creds.update yet. This is the only explicit save point that
|
|
185
|
+
// guarantees the JID is available to startWhatsAppBridge on the next launch.
|
|
186
|
+
try {
|
|
187
|
+
await saveCreds();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// non-fatal: continue and let readSelfIdentity report what was persisted
|
|
191
|
+
}
|
|
183
192
|
const identity = await readSelfIdentity(authDir);
|
|
184
193
|
if (identity.jid) {
|
|
185
194
|
setSelfIdentity(identity.jid, identity.e164);
|