@danieltmn/tamnora 0.22.0 → 0.23.1
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.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/harness/packages/cli/bin/tamnora.mjs +9 -1
- package/harness/packages/cli/src/commands/bridge.js +3 -3
- package/harness/packages/cli/src/commands/doctor.js +42 -4
- package/harness/packages/cli/src/commands/repl.js +1 -0
- package/harness/packages/cli/src/commands/run.js +1 -0
- package/harness/packages/cli/src/commands/tui.js +1 -0
- package/harness/packages/cli/src/commands/update.js +13 -2
- package/harness/packages/cli/src/lib/runtime.js +2 -0
- package/harness/packages/cli/src/tui/app.js +2 -0
- package/harness/packages/cli/test/bin-warning.test.js +27 -0
- package/harness/packages/cli/test/tunnel.test.js +49 -1
- package/harness/packages/core/src/browser/index.js +27 -13
- package/harness/packages/core/src/index.js +1 -1
- package/harness/packages/core/src/tts/edge.js +34 -4
- package/harness/packages/core/src/tunnel.js +32 -9
- package/harness/packages/core/test/agent.test.js +18 -0
- package/harness/packages/core/test/servicios-vivos.test.js +71 -0
- package/harness/packages/core/test/tts.test.js +81 -0
- package/harness/packages/server/bin/tamnora-server.mjs +9 -1
- package/harness/packages/server/public/app.js +4 -3
- package/harness/packages/server/public/cards.js +470 -0
- package/harness/packages/server/public/chat.js +1343 -1893
- package/harness/packages/server/public/chatFormat.js +156 -0
- package/harness/packages/server/public/chrome.js +5 -4
- package/harness/packages/server/public/icons/logo_dark.png +0 -0
- package/harness/packages/server/public/icons/logo_light.png +0 -0
- package/harness/packages/server/public/index.html +22 -23
- package/harness/packages/server/public/settings.js +4 -0
- package/harness/packages/server/public/styles.css +20 -50
- package/harness/packages/server/src/lib/channels/telegram/api.js +102 -0
- package/harness/packages/server/src/lib/channels/telegram/commands.js +309 -0
- package/harness/packages/server/src/lib/channels/telegram/media.js +64 -0
- package/harness/packages/server/src/lib/channels/telegram/permissions.js +0 -0
- package/harness/packages/server/src/lib/channels/telegram/projects.js +102 -0
- package/harness/packages/server/src/lib/channels/telegram/run.js +206 -0
- package/harness/packages/server/src/lib/channels/telegram/text.js +57 -0
- package/harness/packages/server/src/lib/channels/telegram.js +163 -933
- package/harness/packages/server/src/lib/procs.js +4 -1
- package/harness/packages/server/src/lib/scheduler.js +1 -0
- package/harness/packages/server/src/lib/tasks.js +1 -0
- package/harness/packages/server/src/routes/chat.js +1 -1
- package/harness/packages/server/src/routes/providers.js +2 -0
- package/harness/packages/server/src/server.js +6 -0
- package/harness/packages/server/test/chatFormat.test.js +119 -0
- package/harness/packages/server/test/consoleModules.test.js +301 -0
- package/harness/packages/server/test/consoleSmoke.test.js +34 -0
- package/harness/packages/server/test/helpers.js +15 -1
- package/harness/packages/server/test/ops.test.js +9 -5
- package/harness/packages/server/test/procs-manager.test.js +17 -0
- package/harness/packages/server/test/settings.test.js +18 -1
- package/harness/scripts/canary.mjs +24 -0
- package/harness/scripts/self-update.mjs +4 -1
- package/package.json +3 -2
|
@@ -376,6 +376,24 @@ test('runAgent propaga maxOutputTokens al proveedor', async () => {
|
|
|
376
376
|
assert.deepEqual(seen, [1234]);
|
|
377
377
|
});
|
|
378
378
|
|
|
379
|
+
test('runAgent propaga promptCaching al proveedor', async () => {
|
|
380
|
+
const seen = [];
|
|
381
|
+
const provider = {
|
|
382
|
+
async streamChat(_config, params) {
|
|
383
|
+
seen.push(params.promptCaching);
|
|
384
|
+
return { content: 'ok', reasoning: '', toolCalls: [] };
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
await runAgent({
|
|
388
|
+
provider,
|
|
389
|
+
config: {},
|
|
390
|
+
model: 'm',
|
|
391
|
+
messages: [{ role: 'user', content: 'x' }],
|
|
392
|
+
promptCaching: true,
|
|
393
|
+
});
|
|
394
|
+
assert.deepEqual(seen, [true]);
|
|
395
|
+
});
|
|
396
|
+
|
|
379
397
|
test('runAgent emite el ciclo de vida (run.start/end, tool.before/after)', async () => {
|
|
380
398
|
const life = [];
|
|
381
399
|
const provider = {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Canary de los servicios no oficiales (TTS de Edge y STT de Google). Son los
|
|
2
|
+
// unicos que dependen de un servicio externo sin contrato: si cambian el
|
|
3
|
+
// protocolo o la clave, el unico sintoma real es un 403. Los tests con mocks
|
|
4
|
+
// no lo ven, asi que estos prueban el camino real, pero solo con TAMNORA_LIVE=1
|
|
5
|
+
// (piden red y a servicios que pueden estar caidos o barrerte).
|
|
6
|
+
//
|
|
7
|
+
// TAMNORA_LIVE=1 node --test harness/packages/core/test/servicios-vivos.test.js
|
|
8
|
+
//
|
|
9
|
+
// El servicio de Edge solo devuelve MP3 (los formatos riff-/raw- cierran la
|
|
10
|
+
// conexion antes de mandar audio), asi que el round trip se hace con ffmpeg en
|
|
11
|
+
// el medio para convertir el MP3 a WAV.
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { generarSecMsGec, listarVoces, reconocerGoogle, sintetizar, VOZ_POR_DEFECTO } from '../src/index.js';
|
|
16
|
+
|
|
17
|
+
const LIVE = process.env.TAMNORA_LIVE === '1';
|
|
18
|
+
const liveOpts = LIVE ? {} : { skip: 'red real: correr con TAMNORA_LIVE=1' };
|
|
19
|
+
|
|
20
|
+
/** MP3 -> WAV 16 kHz mono con ffmpeg. `null` si no hay ffmpeg. */
|
|
21
|
+
async function mp3AWav(mp3) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const hijo = spawn(
|
|
24
|
+
'ffmpeg',
|
|
25
|
+
['-hide_banner', '-loglevel', 'error', '-i', 'pipe:0', '-f', 'wav', '-ar', '16000', '-ac', '1', 'pipe:1'],
|
|
26
|
+
{ stdio: ['pipe', 'pipe', 'ignore'], windowsHide: true },
|
|
27
|
+
);
|
|
28
|
+
const salida = [];
|
|
29
|
+
hijo.stdout.on('data', (b) => salida.push(b));
|
|
30
|
+
hijo.on('error', () => resolve(null));
|
|
31
|
+
hijo.on('close', (code) => resolve(code === 0 && salida.length ? Buffer.concat(salida) : null));
|
|
32
|
+
hijo.stdin.on('error', () => {});
|
|
33
|
+
hijo.stdin.end(mp3);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('sintetizar entrega un MP3 real (canary)', liveOpts, async () => {
|
|
38
|
+
const mp3 = await sintetizar('hola', { voice: VOZ_POR_DEFECTO });
|
|
39
|
+
assert.ok(Buffer.isBuffer(mp3), 'deberia devolver un Buffer');
|
|
40
|
+
assert.ok(mp3.length > 1000, `MP3 demasiado chico: ${mp3.length} bytes`);
|
|
41
|
+
assert.equal(mp3[0], 0xff, 'sin frame de MP3: el servicio cambio de formato');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('el catalogo de voces responde con la voz por defecto (canary)', liveOpts, async () => {
|
|
45
|
+
await esperar(3000);
|
|
46
|
+
const voces = await listarVoces('es-');
|
|
47
|
+
assert.ok(Array.isArray(voces) && voces.length > 0, 'el catalogo vino vacio');
|
|
48
|
+
const nombres = voces.map((v) => v.ShortName ?? v.name ?? v);
|
|
49
|
+
assert.ok(nombres.includes(VOZ_POR_DEFECTO), `falta ${VOZ_POR_DEFECTO} en ${JSON.stringify(nombres.slice(0, 3))}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** Espera ms antes de abrir otro socket: el servicio de Edge se queja (fallo de
|
|
53
|
+
* handshake) si se le abren varias conexiones seguidas. */
|
|
54
|
+
const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
|
+
|
|
56
|
+
test('ida y vuelta: el audio del TTS se entiende en el STT (canary)', liveOpts, async () => {
|
|
57
|
+
await esperar(3000);
|
|
58
|
+
const mp3 = await sintetizar('hola mundo de prueba', { voice: VOZ_POR_DEFECTO });
|
|
59
|
+
const wav = await mp3AWav(mp3);
|
|
60
|
+
if (!wav) {
|
|
61
|
+
// Sin ffmpeg no hay WAV con el que hacer el viaje: se probo arriba el camino
|
|
62
|
+
// del TTS, que es el que mas se rompe.
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const texto = await reconocerGoogle(wav, { lang: 'es-AR' });
|
|
66
|
+
assert.ok(/hola/.test(texto), `se entiende otra cosa: "${texto}"`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('el anti-abuso de Edge sigue generando tokens de 64 hex', () => {
|
|
70
|
+
assert.match(generarSecMsGec(), /^[0-9A-F]{64}$/);
|
|
71
|
+
});
|
|
@@ -39,3 +39,84 @@ test('sintetizar recorta el texto al tope del servicio', () => {
|
|
|
39
39
|
const largo = 'x'.repeat(MAX_TEXTO + 100);
|
|
40
40
|
assert.throws(() => sintetizar(largo, { signal: controller.signal }), /cancel/);
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
/** WebSocket falso: `guion` dice que hace cada intento ('error' o 'audio'). */
|
|
44
|
+
function mockWebSocket(guion) {
|
|
45
|
+
const original = globalThis.WebSocket;
|
|
46
|
+
let intentos = 0;
|
|
47
|
+
globalThis.WebSocket = class {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.listeners = new Map();
|
|
50
|
+
this.binaryType = 'arraybuffer';
|
|
51
|
+
const paso = guion[Math.min(intentos, guion.length - 1)];
|
|
52
|
+
intentos += 1;
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
// 'fallo' nunca llega a abrirse (fallo de transporte); 'error' abre la
|
|
55
|
+
// conexion y recien ahi falla (el servicio rechaza algo).
|
|
56
|
+
if (paso !== 'fallo') this.fire('open', {});
|
|
57
|
+
if (paso === 'error' || paso === 'fallo') {
|
|
58
|
+
this.fire('error', {});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.fire('message', new TextEncoder().encode('Path:audio\r\nFRAME').buffer);
|
|
62
|
+
this.fire('message', 'X-RequestId:1\r\nPath:turn.end\r\n');
|
|
63
|
+
}, 0);
|
|
64
|
+
}
|
|
65
|
+
addEventListener(type, fn) {
|
|
66
|
+
if (!this.listeners.has(type)) this.listeners.set(type, new Set());
|
|
67
|
+
this.listeners.get(type).add(fn);
|
|
68
|
+
}
|
|
69
|
+
removeEventListener() {}
|
|
70
|
+
send() {}
|
|
71
|
+
close() {
|
|
72
|
+
this.fire('close', {});
|
|
73
|
+
}
|
|
74
|
+
fire(type, arg) {
|
|
75
|
+
for (const fn of this.listeners.get(type) ?? []) fn({ data: arg, message: 'fake' });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
get intentos() {
|
|
80
|
+
return intentos;
|
|
81
|
+
},
|
|
82
|
+
restaurar: () => {
|
|
83
|
+
globalThis.WebSocket = original;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
test('sintetizar reintenta un fallo de conexion y devuelve el audio', async () => {
|
|
89
|
+
const mock = mockWebSocket(['fallo', 'audio']);
|
|
90
|
+
try {
|
|
91
|
+
const mp3 = await sintetizar('hola', { pausaReintentoMs: 5 });
|
|
92
|
+
assert.equal(mp3.toString(), 'FRAME');
|
|
93
|
+
assert.equal(mock.intentos, 2);
|
|
94
|
+
} finally {
|
|
95
|
+
mock.restaurar();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('sintetizar no reintenta cuando el fallo es del propio servicio', async () => {
|
|
100
|
+
// Con la conexion abierta el servicio ya decidio: volver a conectar sirve
|
|
101
|
+
// para un corte de red, no para un rechazo.
|
|
102
|
+
const mock = mockWebSocket(['error']);
|
|
103
|
+
try {
|
|
104
|
+
await assert.rejects(() => sintetizar('hola', { reintentos: 5, pausaReintentoMs: 5 }), /WebSocket/);
|
|
105
|
+
assert.equal(mock.intentos, 1);
|
|
106
|
+
} finally {
|
|
107
|
+
mock.restaurar();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('sintetizar no reintenta si el usuario cancela', async () => {
|
|
112
|
+
const mock = mockWebSocket(['fallo']);
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
try {
|
|
115
|
+
const enCurso = sintetizar('hola', { reintentos: 5, pausaReintentoMs: 5, signal: controller.signal });
|
|
116
|
+
controller.abort();
|
|
117
|
+
await assert.rejects(() => enCurso, /cancel/);
|
|
118
|
+
assert.ok(mock.intentos <= 1, `intentos: ${mock.intentos}`);
|
|
119
|
+
} finally {
|
|
120
|
+
mock.restaurar();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// node:sqlite sigue siendo "experimental" en Node 22, que avisa aunque ande. El
|
|
3
|
+
// aviso no le aporta nada al usuario, asi que se filtra aca. Se quita el
|
|
4
|
+
// impresor default de Node para que lo que si se imprime (lo que no sea
|
|
5
|
+
// ExperimentalWarning) salga una sola vez, no dos.
|
|
6
|
+
process.removeAllListeners('warning');
|
|
7
|
+
process.on('warning', (warning) => {
|
|
8
|
+
if (warning.name !== 'ExperimentalWarning') console.warn(warning.stack ?? warning);
|
|
9
|
+
});
|
|
10
|
+
const { serveCommand } = await import('../src/cli.js');
|
|
3
11
|
|
|
4
12
|
serveCommand(process.argv.slice(2))
|
|
5
13
|
.then((code) => {
|
|
@@ -122,7 +122,9 @@ import {
|
|
|
122
122
|
closeSkillForm,
|
|
123
123
|
importMcp,
|
|
124
124
|
importSkill,
|
|
125
|
+
limitsCachingEl,
|
|
125
126
|
limitsDurationEl,
|
|
127
|
+
limitsOutputEl,
|
|
126
128
|
limitsRoundsEl,
|
|
127
129
|
limitsTokensEl,
|
|
128
130
|
mcpForm,
|
|
@@ -201,9 +203,6 @@ import {
|
|
|
201
203
|
trTestBtn,
|
|
202
204
|
} from './voice.js';
|
|
203
205
|
|
|
204
|
-
document.querySelectorAll('.tab[data-tab]').forEach((t) =>
|
|
205
|
-
t.addEventListener('click', () => activateTab(t.dataset.tab))
|
|
206
|
-
);
|
|
207
206
|
moreTab.addEventListener('click', (event) => {
|
|
208
207
|
event.stopPropagation();
|
|
209
208
|
const open = moreMenu.hidden;
|
|
@@ -538,6 +537,8 @@ $('#limits-save')?.addEventListener('click', async () => {
|
|
|
538
537
|
maxRounds: Math.max(0, Number(limitsRoundsEl?.value) || 0),
|
|
539
538
|
maxTokens: Math.max(0, Number(limitsTokensEl?.value) || 0),
|
|
540
539
|
maxDurationMs: Math.max(0, Math.round((Number(limitsDurationEl?.value) || 0) * 1000)),
|
|
540
|
+
maxOutputTokens: Math.max(0, Number(limitsOutputEl?.value) || 0),
|
|
541
|
+
promptCaching: Boolean(limitsCachingEl?.checked),
|
|
541
542
|
};
|
|
542
543
|
try {
|
|
543
544
|
await api('/api/settings', { method: 'PUT', body });
|