@danieltmn/tamnora 0.5.1 → 0.6.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 +14 -0
- package/README.md +30 -0
- package/harness/packages/core/src/config/index.js +1 -0
- package/harness/packages/core/src/index.js +1 -0
- package/harness/packages/core/src/storage/sqlite.js +108 -2
- package/harness/packages/core/src/tools/labels.js +83 -0
- package/harness/packages/core/test/labels.test.js +34 -0
- package/harness/packages/server/public/app.js +527 -111
- package/harness/packages/server/public/index.html +36 -1
- package/harness/packages/server/public/styles.css +97 -4
- package/harness/packages/server/src/lib/channels/index.js +89 -0
- package/harness/packages/server/src/lib/channels/telegram.js +532 -0
- package/harness/packages/server/src/lib/http.js +3 -2
- package/harness/packages/server/src/lib/runManager.js +102 -0
- package/harness/packages/server/src/lib/tasks.js +126 -0
- package/harness/packages/server/src/routes/channels.js +47 -0
- package/harness/packages/server/src/routes/chat.js +166 -77
- package/harness/packages/server/src/routes/sessions.js +17 -0
- package/harness/packages/server/src/server.js +15 -16
- package/harness/packages/server/test/server.test.js +213 -1
- package/harness/packages/server/test/telegram.test.js +94 -0
- package/harness/tamnora.example.json +2 -1
- package/package.json +1 -1
- package/harness/packages/server/src/lib/telegram.js +0 -267
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
</head>
|
|
34
34
|
<body>
|
|
35
35
|
<header class="topbar">
|
|
36
|
-
<div class="brand"><span class="logo"><span class="logo-t">T</span><span class="logo-h">H</span></span> <span class="ver">v0.
|
|
36
|
+
<div class="brand"><span class="logo"><span class="logo-t">T</span><span class="logo-h">H</span></span> <span class="ver">v0.6.1</span></div>
|
|
37
37
|
<nav class="tabs">
|
|
38
38
|
<button class="tab active" data-tab="chat">chat</button>
|
|
39
39
|
<button class="tab" data-tab="providers">proveedores</button>
|
|
@@ -377,6 +377,41 @@
|
|
|
377
377
|
<div id="skill-import-status" class="summary"></div>
|
|
378
378
|
<ul id="skill-list" class="providers"></ul>
|
|
379
379
|
</div>
|
|
380
|
+
|
|
381
|
+
<div class="panel">
|
|
382
|
+
<div class="row between">
|
|
383
|
+
<h2>Telegram</h2>
|
|
384
|
+
<span id="tg-status" class="summary">cargando…</span>
|
|
385
|
+
</div>
|
|
386
|
+
<p class="muted verify-line">
|
|
387
|
+
Bot por long polling (no necesita URL pública). El token vive en <code>app.json</code>
|
|
388
|
+
(la casa) y nunca se muestra completo.
|
|
389
|
+
</p>
|
|
390
|
+
<label class="field"><span>token del bot</span>
|
|
391
|
+
<input id="tg-token" type="password" autocomplete="off" spellcheck="false"
|
|
392
|
+
placeholder="123456:ABC… (dejar vacío para no cambiar)" />
|
|
393
|
+
</label>
|
|
394
|
+
<label class="field"><span>chats autorizados (ids separados por coma)</span>
|
|
395
|
+
<input id="tg-chats" spellcheck="false" placeholder="123456789, -1001234567890" />
|
|
396
|
+
</label>
|
|
397
|
+
<label class="field"><span>modelo de transcripción (voz)</span>
|
|
398
|
+
<input id="tg-transcribe" spellcheck="false" placeholder="whisper-1" />
|
|
399
|
+
</label>
|
|
400
|
+
<details class="tg-help">
|
|
401
|
+
<summary>¿Cómo habilito mi chat?</summary>
|
|
402
|
+
<ol>
|
|
403
|
+
<li>Creá el bot con <b>@BotFather</b> (<code>/newbot</code>) y pegá el token acá.</li>
|
|
404
|
+
<li>Guardá y mandale cualquier mensaje al bot: si tu chat no está autorizado, te
|
|
405
|
+
responde tu <b>chat id</b>.</li>
|
|
406
|
+
<li>Agregá ese id a <strong>chats autorizados</strong> y volvé a guardar.</li>
|
|
407
|
+
</ol>
|
|
408
|
+
</details>
|
|
409
|
+
<div class="row">
|
|
410
|
+
<button id="tg-save" class="btn primary" type="button">guardar</button>
|
|
411
|
+
<button id="tg-test" class="btn" type="button">probar token</button>
|
|
412
|
+
<span id="tg-msg" class="summary"></span>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
380
415
|
</section>
|
|
381
416
|
|
|
382
417
|
<dialog id="folder-modal" class="modal folder-modal">
|
|
@@ -427,8 +427,38 @@ body.resizing * { cursor: col-resize !important; }
|
|
|
427
427
|
.msg-actions button.msg-act.danger:hover { color: var(--danger); border-color: var(--danger); }
|
|
428
428
|
.bubble .tools { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; }
|
|
429
429
|
|
|
430
|
+
.tools-actions { display: flex; align-items: center; gap: 8px; }
|
|
431
|
+
.tools-actions[hidden] { display: none; }
|
|
432
|
+
.tools-count { color: var(--muted); font-size: 12px; }
|
|
433
|
+
.tools-toggle {
|
|
434
|
+
background: none;
|
|
435
|
+
border: none;
|
|
436
|
+
color: var(--accent);
|
|
437
|
+
cursor: pointer;
|
|
438
|
+
font: 600 12px var(--sans);
|
|
439
|
+
padding: 2px 0;
|
|
440
|
+
}
|
|
441
|
+
.tools-toggle:hover { text-decoration: underline; }
|
|
442
|
+
|
|
430
443
|
.tool-card { font-size: 12.5px; border-left: 2px solid var(--border); padding-left: 10px; min-width: 0; }
|
|
431
|
-
.tool-card .tool-head {
|
|
444
|
+
.tool-card > .tool-head {
|
|
445
|
+
display: flex;
|
|
446
|
+
gap: 8px;
|
|
447
|
+
align-items: baseline;
|
|
448
|
+
min-width: 0;
|
|
449
|
+
cursor: pointer;
|
|
450
|
+
list-style: none;
|
|
451
|
+
touch-action: manipulation;
|
|
452
|
+
}
|
|
453
|
+
.tool-card > .tool-head::-webkit-details-marker { display: none; }
|
|
454
|
+
.tool-card > .tool-head::before {
|
|
455
|
+
content: '▸';
|
|
456
|
+
color: var(--muted);
|
|
457
|
+
flex: 0 0 auto;
|
|
458
|
+
font-size: 10px;
|
|
459
|
+
transition: transform 0.12s ease;
|
|
460
|
+
}
|
|
461
|
+
.tool-card[open] > .tool-head::before { transform: rotate(90deg); }
|
|
432
462
|
.tool-card .tool-glyph { color: var(--muted); flex: 0 0 auto; }
|
|
433
463
|
.tool-card.running .tool-glyph { color: var(--accent); animation: pulse 1s steps(2, start) infinite; }
|
|
434
464
|
.tool-card.done .tool-glyph { color: var(--ok); }
|
|
@@ -442,17 +472,78 @@ body.resizing * { cursor: col-resize !important; }
|
|
|
442
472
|
text-overflow: ellipsis;
|
|
443
473
|
white-space: nowrap;
|
|
444
474
|
}
|
|
445
|
-
.tool-card .tool-out {
|
|
475
|
+
.tool-card > .tool-out {
|
|
446
476
|
color: var(--muted);
|
|
447
477
|
margin: 4px 0 2px 18px;
|
|
448
478
|
padding-left: 10px;
|
|
449
479
|
border-left: 1px solid var(--border);
|
|
450
480
|
white-space: pre-wrap;
|
|
451
481
|
word-break: break-word;
|
|
452
|
-
max-height:
|
|
482
|
+
max-height: 320px;
|
|
453
483
|
overflow: auto;
|
|
454
484
|
}
|
|
455
|
-
.tool-card .tool-out:empty { display: none; }
|
|
485
|
+
.tool-card > .tool-out:empty { display: none; }
|
|
486
|
+
.tool-more {
|
|
487
|
+
display: inline-block;
|
|
488
|
+
margin: 2px 0 2px 18px;
|
|
489
|
+
background: none;
|
|
490
|
+
border: none;
|
|
491
|
+
color: var(--accent);
|
|
492
|
+
cursor: pointer;
|
|
493
|
+
font: 600 12px var(--sans);
|
|
494
|
+
padding: 0;
|
|
495
|
+
}
|
|
496
|
+
.tool-more[hidden] { display: none; }
|
|
497
|
+
.tool-more:hover { text-decoration: underline; }
|
|
498
|
+
|
|
499
|
+
.agent-card {
|
|
500
|
+
border: 1px solid var(--border);
|
|
501
|
+
border-radius: 7px;
|
|
502
|
+
padding: 6px 9px;
|
|
503
|
+
background: color-mix(in srgb, var(--accent) 4%, transparent);
|
|
504
|
+
min-width: 0;
|
|
505
|
+
}
|
|
506
|
+
.agent-card > .agent-head {
|
|
507
|
+
display: flex;
|
|
508
|
+
gap: 7px;
|
|
509
|
+
align-items: baseline;
|
|
510
|
+
cursor: pointer;
|
|
511
|
+
list-style: none;
|
|
512
|
+
touch-action: manipulation;
|
|
513
|
+
}
|
|
514
|
+
.agent-card > .agent-head::-webkit-details-marker { display: none; }
|
|
515
|
+
.agent-card > .agent-head::before {
|
|
516
|
+
content: '▸';
|
|
517
|
+
color: var(--muted);
|
|
518
|
+
font-size: 10px;
|
|
519
|
+
transition: transform 0.12s ease;
|
|
520
|
+
}
|
|
521
|
+
.agent-card[open] > .agent-head::before { transform: rotate(90deg); }
|
|
522
|
+
.agent-card .agent-glyph { color: var(--accent); flex: 0 0 auto; }
|
|
523
|
+
.agent-card.running .agent-glyph { animation: pulse 1s steps(2, start) infinite; }
|
|
524
|
+
.agent-card.done .agent-glyph { color: var(--ok); }
|
|
525
|
+
.agent-card.failed .agent-glyph { color: var(--danger); }
|
|
526
|
+
.agent-card .agent-title {
|
|
527
|
+
color: var(--text);
|
|
528
|
+
font-weight: 700;
|
|
529
|
+
flex: 1 1 auto;
|
|
530
|
+
min-width: 0;
|
|
531
|
+
overflow: hidden;
|
|
532
|
+
text-overflow: ellipsis;
|
|
533
|
+
white-space: nowrap;
|
|
534
|
+
}
|
|
535
|
+
.agent-card .agent-status { color: var(--muted); font-size: 11px; flex: 0 0 auto; }
|
|
536
|
+
.agent-card > .agent-body {
|
|
537
|
+
margin: 6px 0 2px 16px;
|
|
538
|
+
border-left: 1px solid var(--border);
|
|
539
|
+
padding-left: 10px;
|
|
540
|
+
display: flex;
|
|
541
|
+
flex-direction: column;
|
|
542
|
+
gap: 6px;
|
|
543
|
+
}
|
|
544
|
+
.agent-card .agent-text:empty { display: none; }
|
|
545
|
+
.agent-card .agent-tools { display: flex; flex-direction: column; gap: 6px; }
|
|
546
|
+
.agent-card .agent-tools:empty { display: none; }
|
|
456
547
|
.tool-out .diff-line { display: block; }
|
|
457
548
|
.tool-out .diff-add { color: var(--ok); }
|
|
458
549
|
.tool-out .diff-del { color: var(--danger); }
|
|
@@ -522,6 +613,8 @@ body.resizing * { cursor: col-resize !important; }
|
|
|
522
613
|
word-break: break-word;
|
|
523
614
|
}
|
|
524
615
|
.chip.err { border-left-color: var(--danger); color: var(--danger); }
|
|
616
|
+
.chip.ok { border-left-color: var(--ok); color: var(--ok); }
|
|
617
|
+
.chip.warn { border-left-color: var(--warn); color: var(--warn); }
|
|
525
618
|
details.reasoning { margin-top: 6px; }
|
|
526
619
|
details.reasoning > summary { color: var(--muted); cursor: pointer; font-size: 12.5px; }
|
|
527
620
|
details.reasoning .body { color: var(--muted); white-space: pre-wrap; margin-top: 4px; border-left: 1px solid var(--border); padding-left: 10px; }
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Capa de canales de mensajeria. Hoy implementa Telegram; la interfaz comun
|
|
2
|
+
// (`stop`, `notify`) permite sumar otros proveedores mas adelante. El manager
|
|
3
|
+
// permite recargar los canales en caliente tras cambiar la configuracion.
|
|
4
|
+
|
|
5
|
+
import { startTelegramChannel } from './telegram.js';
|
|
6
|
+
|
|
7
|
+
function tokenHint(token) {
|
|
8
|
+
const value = String(token ?? '');
|
|
9
|
+
if (!value) return '';
|
|
10
|
+
if (value.includes('•')) return value;
|
|
11
|
+
return value.length <= 6 ? '••••' : `••••${value.slice(-4)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseAllowed(raw) {
|
|
15
|
+
if (Array.isArray(raw)) return raw.map(String).filter(Boolean);
|
|
16
|
+
return String(raw ?? '')
|
|
17
|
+
.split(/[,\s]+/)
|
|
18
|
+
.map((s) => s.trim())
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Crea el manager de canales. `load()` arranca (o reinicia) los canales segun
|
|
24
|
+
* la config efectiva (app.json + tamnora.json + entorno), `status()` la resume
|
|
25
|
+
* y `stop()` los baja.
|
|
26
|
+
*/
|
|
27
|
+
export function createChannelsManager(ctx, { log = () => {}, enabled = true } = {}) {
|
|
28
|
+
let active = null;
|
|
29
|
+
|
|
30
|
+
// app.json (global) es la base; tamnora.json solo pisa con valores definidos
|
|
31
|
+
// (evita que sus defaults vacios borren lo guardado desde la consola).
|
|
32
|
+
const readConfig = () => {
|
|
33
|
+
const app = ctx.app?.telegram ?? {};
|
|
34
|
+
const cfg = ctx.config?.telegram ?? {};
|
|
35
|
+
const tg = { ...app };
|
|
36
|
+
if (cfg.token) tg.token = cfg.token;
|
|
37
|
+
if (Array.isArray(cfg.allowedChatIds) && cfg.allowedChatIds.length) tg.allowedChatIds = cfg.allowedChatIds;
|
|
38
|
+
if (cfg.transcribeModel) tg.transcribeModel = cfg.transcribeModel;
|
|
39
|
+
return tg;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function effective() {
|
|
43
|
+
const tg = readConfig();
|
|
44
|
+
const token = tg.token || process.env.TAMNORA_TELEGRAM_TOKEN || '';
|
|
45
|
+
const allowedChatIds = parseAllowed(tg.allowedChatIds ?? process.env.TAMNORA_TELEGRAM_CHAT_IDS ?? []);
|
|
46
|
+
return { token, allowedChatIds, transcribeModel: tg.transcribeModel };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stop() {
|
|
50
|
+
active?.unsubscribe?.();
|
|
51
|
+
for (const channel of active?.channels ?? []) channel.stop?.();
|
|
52
|
+
active = null;
|
|
53
|
+
ctx.telegram = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function load() {
|
|
57
|
+
stop();
|
|
58
|
+
if (!enabled) return { enabled: false, channels: [] };
|
|
59
|
+
const { token, allowedChatIds, transcribeModel } = effective();
|
|
60
|
+
const channels = [];
|
|
61
|
+
const telegram = startTelegramChannel(ctx, { token, allowedChatIds, transcribeModel, log });
|
|
62
|
+
if (telegram) channels.push(telegram);
|
|
63
|
+
const unsubscribe = ctx.runs?.subscribeAll?.((sessionId, event) => {
|
|
64
|
+
for (const channel of channels) {
|
|
65
|
+
try {
|
|
66
|
+
channel.notify?.(sessionId, event);
|
|
67
|
+
} catch {
|
|
68
|
+
// un canal caido no debe frenar el run
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
active = { channels, unsubscribe };
|
|
73
|
+
ctx.telegram = channels.length > 0;
|
|
74
|
+
return { enabled: ctx.telegram, channels };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function status() {
|
|
78
|
+
const { token, allowedChatIds, transcribeModel } = effective();
|
|
79
|
+
return {
|
|
80
|
+
tokenSet: Boolean(token),
|
|
81
|
+
tokenHint: tokenHint(token),
|
|
82
|
+
allowedChatIds,
|
|
83
|
+
transcribeModel: transcribeModel || 'whisper-1',
|
|
84
|
+
running: Boolean(active && active.channels.length),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { load, stop, status, readConfig };
|
|
89
|
+
}
|