@danieltmn/openbridge 0.6.1 → 0.6.3
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 +28 -0
- package/README.md +10 -0
- package/package.json +1 -1
- package/src/bridge/bridge.js +107 -14
- package/src/store/index.js +24 -2
- package/src/web/routes.js +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ Todos los cambios relevantes de OpenBridge. Formato basado en
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y
|
|
5
5
|
[Versionado Semantico](https://semver.org/lang/es/).
|
|
6
6
|
|
|
7
|
+
## [0.6.3] - 2026-09-15
|
|
8
|
+
|
|
9
|
+
### Agregado
|
|
10
|
+
|
|
11
|
+
- **Sincronizacion casi en tiempo real**: el puente vigila la base de opencode
|
|
12
|
+
(`opencode.db`/`-wal`) y dispara un barrido tras ~8 s de calma, en vez de
|
|
13
|
+
esperar 15 min (que queda como respaldo cada 3 min).
|
|
14
|
+
|
|
15
|
+
### Corregido
|
|
16
|
+
|
|
17
|
+
- El barrido ya **no se pierde** cuando opencode esta ocupado: se marca pendiente
|
|
18
|
+
y corre al terminar el mensaje (antes se salteaba el tick entero).
|
|
19
|
+
- **Mensajes del TUI en sesiones web**: las sesiones vinculadas a la web ya no
|
|
20
|
+
quedan solo con refresco de tokens; se importan los mensajes nuevos.
|
|
21
|
+
- **Sin duplicados**: el merge deduplica por el id de mensaje de opencode
|
|
22
|
+
(`oc_msg`) y "adopta" el mensaje optimista que publico la web. Paridad en el
|
|
23
|
+
hub Node (`store.sessionImport`) y en el PHP.
|
|
24
|
+
|
|
25
|
+
## [0.6.2] - 2026-09-14
|
|
26
|
+
|
|
27
|
+
### Corregido
|
|
28
|
+
|
|
29
|
+
- **Tunel en Windows**: los shims `.cmd` (p. ej. `tmole`) se lanzaban con doble
|
|
30
|
+
cita (`cmd.exe /d /s /c "\"ruta\""`) y fallaban con "no se reconoce como un
|
|
31
|
+
comando". Ahora se usa `windowsVerbatimArguments` y, si `tmole` falla, el
|
|
32
|
+
puente **reintenta con `npx --yes tunnelmole`** (antes solo lo intentaba si
|
|
33
|
+
`tmole` no estaba en el PATH). Timeout del tunel: 85 s.
|
|
34
|
+
|
|
7
35
|
## [0.6.1] - 2026-09-14
|
|
8
36
|
|
|
9
37
|
### Agregado
|
package/README.md
CHANGED
|
@@ -145,6 +145,16 @@ Ejemplo concreto:
|
|
|
145
145
|
El puente remoto solo necesita salida a internet hacia el hub; no abre puertos ni
|
|
146
146
|
túnel propio. Para que arranque solo en cada PC: `openbridge autostart install`.
|
|
147
147
|
|
|
148
|
+
## Sincronización (opencode es la fuente)
|
|
149
|
+
|
|
150
|
+
La conversación vive en **opencode** (tu PC); la web la refleja. Los mensajes que
|
|
151
|
+
pasás por la web se guardan al instante; los que hacés en el **TUI de opencode**
|
|
152
|
+
se importan por un barrido que ahora corre **casi en tiempo real**: el puente
|
|
153
|
+
vigila la base de opencode (`opencode.db`) y sincroniza a los pocos segundos
|
|
154
|
+
(respaldo cada 3 min). Si la sesión empezó en la web y la seguís en el TUI (o al
|
|
155
|
+
revés), los mensajes nuevos se agregan sin duplicarse: el merge identifica cada
|
|
156
|
+
mensaje por su id de opencode (`oc_msg`).
|
|
157
|
+
|
|
148
158
|
## Hub en hosting PHP (URL fija, sin túnel)
|
|
149
159
|
|
|
150
160
|
Además del hub Node, OpenBridge puede correr su hub en un **hosting PHP** (cPanel),
|
package/package.json
CHANGED
package/src/bridge/bridge.js
CHANGED
|
@@ -540,6 +540,7 @@ function streamCli(args, opts, onPartial) {
|
|
|
540
540
|
let settled = false;
|
|
541
541
|
let timer = null;
|
|
542
542
|
let sessionID = null;
|
|
543
|
+
let assistantMsgID = null;
|
|
543
544
|
let errorText = '';
|
|
544
545
|
const texts = [];
|
|
545
546
|
const reasons = [];
|
|
@@ -577,6 +578,7 @@ function streamCli(args, opts, onPartial) {
|
|
|
577
578
|
killed: false,
|
|
578
579
|
canceled: false,
|
|
579
580
|
sessionID: sessionID,
|
|
581
|
+
assistantMsgID: assistantMsgID,
|
|
580
582
|
errorText: errorText,
|
|
581
583
|
}, extra || {});
|
|
582
584
|
}
|
|
@@ -636,6 +638,9 @@ function streamCli(args, opts, onPartial) {
|
|
|
636
638
|
if (ev.part && typeof ev.part.text === 'string') {
|
|
637
639
|
if (ev.type === 'text') texts.push(ev.part.text);
|
|
638
640
|
else if (ev.type === 'reasoning') reasons.push(ev.part.text);
|
|
641
|
+
// Id del mensaje de opencode (msg_...): identifica la respuesta
|
|
642
|
+
// para que el hub no la duplique al importar del TUI.
|
|
643
|
+
if (typeof ev.messageID === 'string' && ev.messageID) assistantMsgID = ev.messageID;
|
|
639
644
|
}
|
|
640
645
|
if (ev.type === 'error') {
|
|
641
646
|
errorText = errorMessage(ev);
|
|
@@ -1369,6 +1374,7 @@ async function exportAndImport(folder, sess, target) {
|
|
|
1369
1374
|
ts: ts,
|
|
1370
1375
|
agent: role === 'assistant' ? lastAgent : (info.agent || ''),
|
|
1371
1376
|
};
|
|
1377
|
+
if (info.id) out.oc_msg = String(info.id);
|
|
1372
1378
|
if (reasoning) out.reasoning = reasoning;
|
|
1373
1379
|
msgs.push(out);
|
|
1374
1380
|
}
|
|
@@ -1447,18 +1453,27 @@ async function sweepTarget(t, opts) {
|
|
|
1447
1453
|
if (!sessions.length) continue;
|
|
1448
1454
|
const fstate = target.folders[folder] || (target.folders[folder] = {});
|
|
1449
1455
|
for (const s of sessions) {
|
|
1450
|
-
|
|
1456
|
+
// Sin cambios desde el último barrido (el marcador es el "updated"
|
|
1457
|
+
// de la lista, con precision de minuto). Con `force` (watcher) se
|
|
1458
|
+
// re-exportan las sesiones tocadas en los últimos 30 min para no
|
|
1459
|
+
// perder cambios dentro del mismo minuto.
|
|
1460
|
+
const unchanged = fstate[s.id] === s.updated;
|
|
1461
|
+
const forceRecent = !!(opts && opts.force) && listUpdatedRecent(s.updated, 30 * 60 * 1000);
|
|
1462
|
+
if (unchanged && !forceRecent) { seen.add(s.id); continue; }
|
|
1451
1463
|
if (seen.has(s.id)) { fstate[s.id] = s.updated; continue; } // ya atendida en esta pasada
|
|
1452
1464
|
seen.add(s.id);
|
|
1453
1465
|
if (knownOc.includes(s.id)) {
|
|
1454
|
-
// Chat web vinculado:
|
|
1455
|
-
//
|
|
1466
|
+
// Chat web vinculado: el hub ya tiene lo que publicó la web,
|
|
1467
|
+
// pero el TUI puede haber agregado mensajes. Se importa igual
|
|
1468
|
+
// (el merge deduplica por oc_msg) y el hub lo marca `importada`,
|
|
1469
|
+
// con lo que sale de known_oc y sigue sincronizando.
|
|
1456
1470
|
try {
|
|
1457
|
-
await
|
|
1471
|
+
await exportAndImport(folder, s, t);
|
|
1458
1472
|
fstate[s.id] = s.updated;
|
|
1459
1473
|
saveSyncState();
|
|
1474
|
+
imported++;
|
|
1460
1475
|
} catch (e) {
|
|
1461
|
-
log('aviso: no pude
|
|
1476
|
+
log('aviso: no pude sincronizar ' + s.id + ': ' + e.message);
|
|
1462
1477
|
}
|
|
1463
1478
|
continue;
|
|
1464
1479
|
}
|
|
@@ -1496,6 +1511,61 @@ async function sweepTarget(t, opts) {
|
|
|
1496
1511
|
return imported;
|
|
1497
1512
|
}
|
|
1498
1513
|
|
|
1514
|
+
// ---------------------------------------------------------------------------
|
|
1515
|
+
// Sincronizacion casi en tiempo real. opencode persiste en su base SQLite
|
|
1516
|
+
// (opencode.db / -wal / -shm); un watcher liviano mira mtime+tamano y dispara
|
|
1517
|
+
// un barrido tras un periodo de calma, en vez de esperar 15 min.
|
|
1518
|
+
// ---------------------------------------------------------------------------
|
|
1519
|
+
let sweepPending = false;
|
|
1520
|
+
let lastSweepAt = 0;
|
|
1521
|
+
|
|
1522
|
+
function scheduleSweep(reason) {
|
|
1523
|
+
if (busy || sweepRunning) { sweepPending = true; return; }
|
|
1524
|
+
if (Date.now() - lastSweepAt < 30000) { sweepPending = true; return; }
|
|
1525
|
+
lastSweepAt = Date.now();
|
|
1526
|
+
syncSessions({ silent: true, force: reason === 'watcher' }).catch(handleError);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function opencodeDataDirs() {
|
|
1530
|
+
const dirs = [];
|
|
1531
|
+
if (process.env.XDG_DATA_HOME) dirs.push(path.join(process.env.XDG_DATA_HOME, 'opencode'));
|
|
1532
|
+
dirs.push(path.join(os.homedir(), '.local', 'share', 'opencode'));
|
|
1533
|
+
dirs.push(path.join(os.homedir(), 'AppData', 'Local', 'opencode'));
|
|
1534
|
+
return dirs;
|
|
1535
|
+
}
|
|
1536
|
+
function opencodeDbSignature() {
|
|
1537
|
+
let sig = '';
|
|
1538
|
+
for (const d of opencodeDataDirs()) {
|
|
1539
|
+
for (const f of ['opencode.db', 'opencode.db-wal', 'opencode.db-shm']) {
|
|
1540
|
+
try {
|
|
1541
|
+
const st = fs.statSync(path.join(d, f));
|
|
1542
|
+
sig += f + ':' + Math.round(st.mtimeMs) + ':' + st.size + ';';
|
|
1543
|
+
} catch (e) { /* no existe en esa ruta */ }
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return sig;
|
|
1547
|
+
}
|
|
1548
|
+
// `updated` de `session list` es "HH:MM · D/M/YYYY" (precision de minuto).
|
|
1549
|
+
function listUpdatedRecent(updated, windowMs) {
|
|
1550
|
+
const m = String(updated || '').match(/^(\d{1,2}):(\d{2})\s*·\s*(\d{1,2})\/(\d{1,2})\/(\d{4})/);
|
|
1551
|
+
if (!m) return false;
|
|
1552
|
+
const ts = new Date(Number(m[5]), Number(m[4]) - 1, Number(m[3]), Number(m[1]), Number(m[2])).getTime();
|
|
1553
|
+
return ts > 0 && (Date.now() - ts) <= windowMs;
|
|
1554
|
+
}
|
|
1555
|
+
function startSyncWatcher() {
|
|
1556
|
+
let last = opencodeDbSignature();
|
|
1557
|
+
let timer = null;
|
|
1558
|
+
setInterval(() => {
|
|
1559
|
+
const sig = opencodeDbSignature();
|
|
1560
|
+
if (sig === '' || sig === last) return;
|
|
1561
|
+
last = sig;
|
|
1562
|
+
if (timer) clearTimeout(timer);
|
|
1563
|
+
timer = setTimeout(() => scheduleSweep('watcher'), 8000);
|
|
1564
|
+
}, 4000);
|
|
1565
|
+
// Respaldo por si el watcher no ve el cambio (otra ruta o filesystem).
|
|
1566
|
+
setInterval(() => scheduleSweep('periodico'), 3 * 60 * 1000);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1499
1569
|
// ---------------------------------------------------------------------------
|
|
1500
1570
|
// Archivos: listado y lectura (read-only) resueltos por el puente,
|
|
1501
1571
|
// siempre dentro del workspace y sin recorrer basura (node_modules, .git...).
|
|
@@ -1700,10 +1770,24 @@ function tunnelSpawn(port, onLine, onFail, onUrls, triedNpx = false) {
|
|
|
1700
1770
|
return;
|
|
1701
1771
|
}
|
|
1702
1772
|
const args = (spec.pre || []).concat(triedNpx ? ['--yes', 'tunnelmole', String(port)] : [String(port)]);
|
|
1773
|
+
let settled = false;
|
|
1774
|
+
// Si `tmole` existe pero falla (shim roto, sin PATH, etc.), reintentamos con
|
|
1775
|
+
// `npx --yes tunnelmole` en vez de darnos por vencidos.
|
|
1776
|
+
const retryNpx = () => {
|
|
1777
|
+
if (settled) return;
|
|
1778
|
+
settled = true;
|
|
1779
|
+
log('túnel: tmole falló, reintentando con npx tunnelmole…');
|
|
1780
|
+
tunnelSpawn(port, onLine, onFail, onUrls, true);
|
|
1781
|
+
};
|
|
1703
1782
|
let proc;
|
|
1704
1783
|
try {
|
|
1705
|
-
proc = spawn(spec.bin, args, {
|
|
1784
|
+
proc = spawn(spec.bin, args, {
|
|
1785
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1786
|
+
windowsHide: true,
|
|
1787
|
+
windowsVerbatimArguments: !!spec.verbatim,
|
|
1788
|
+
});
|
|
1706
1789
|
} catch (e) {
|
|
1790
|
+
if (!triedNpx) { retryNpx(); return; }
|
|
1707
1791
|
onFail('no se pudo iniciar tunnelmole (' + e.message + '). Instalalo con: npm i -g tunnelmole');
|
|
1708
1792
|
return;
|
|
1709
1793
|
}
|
|
@@ -1711,14 +1795,17 @@ function tunnelSpawn(port, onLine, onFail, onUrls, triedNpx = false) {
|
|
|
1711
1795
|
const feed = (d) => {
|
|
1712
1796
|
buf += String(d);
|
|
1713
1797
|
const urls = parseTunnelUrls(buf);
|
|
1714
|
-
if (urls.https || urls.http) onUrls(urls);
|
|
1798
|
+
if (urls.https || urls.http) { settled = true; onUrls(urls); }
|
|
1715
1799
|
};
|
|
1716
1800
|
proc.stdout.on('data', feed);
|
|
1717
1801
|
proc.stderr.on('data', feed);
|
|
1718
1802
|
proc.on('error', (e) => {
|
|
1803
|
+
if (!triedNpx) { retryNpx(); return; }
|
|
1719
1804
|
onFail('no se pudo iniciar tunnelmole (' + e.message + '). Instalalo con: npm i -g tunnelmole');
|
|
1720
1805
|
});
|
|
1721
1806
|
proc.on('exit', (code) => {
|
|
1807
|
+
if (settled) return;
|
|
1808
|
+
if (!triedNpx) { retryNpx(); return; }
|
|
1722
1809
|
onFail('tmole terminó (código ' + code + ')' + (buf ? ': ' + buf.slice(0, 160).trim() : ''));
|
|
1723
1810
|
});
|
|
1724
1811
|
onLine(proc);
|
|
@@ -1756,10 +1843,10 @@ async function tunnelStart(cmd) {
|
|
|
1756
1843
|
};
|
|
1757
1844
|
entry = { port, proc: null, urls: { https: '', http: '' }, startedAt: Date.now() };
|
|
1758
1845
|
tunnels.set(port, entry);
|
|
1759
|
-
//
|
|
1846
|
+
// 85 s: con el fallback de npx, la primera vez descarga tunnelmole.
|
|
1760
1847
|
timer = setTimeout(() => {
|
|
1761
|
-
finish(false, 'tunnelmole no devolvió URLs (
|
|
1762
|
-
},
|
|
1848
|
+
finish(false, 'tunnelmole no devolvió URLs (85 s). ¿Está instalado? npm i -g tunnelmole');
|
|
1849
|
+
}, 85000);
|
|
1763
1850
|
tunnelSpawn(
|
|
1764
1851
|
port,
|
|
1765
1852
|
(proc) => { entry.proc = proc; },
|
|
@@ -1913,7 +2000,9 @@ function procResolveBin(token, allowSet) {
|
|
|
1913
2000
|
const found = procFindOnPath(bare);
|
|
1914
2001
|
if (!found) throw new Error('no se encontró "' + token + '" en el PATH');
|
|
1915
2002
|
if (found.ext.toLowerCase() === '.exe') return { bin: found.p, pre: [] };
|
|
1916
|
-
|
|
2003
|
+
// Shim .cmd/.bat: se ejecuta con cmd.exe. `verbatim` evita que Node vuelva a
|
|
2004
|
+
// citar el argumento (que ya viene entre comillas) y cmd reciba "\"ruta\"".
|
|
2005
|
+
return { bin: 'cmd.exe', pre: ['/d', '/s', '/c', '"' + found.p + '"'], verbatim: true };
|
|
1917
2006
|
}
|
|
1918
2007
|
|
|
1919
2008
|
async function procDone(cmd, ok, payload, error) {
|
|
@@ -2000,6 +2089,7 @@ async function procStart(cmd) {
|
|
|
2000
2089
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2001
2090
|
env: process.env,
|
|
2002
2091
|
windowsHide: true,
|
|
2092
|
+
windowsVerbatimArguments: !!spec.verbatim,
|
|
2003
2093
|
});
|
|
2004
2094
|
} catch (e) {
|
|
2005
2095
|
await procDone(cmd, false, null, 'no se pudo iniciar: ' + e.message);
|
|
@@ -2301,6 +2391,7 @@ async function tick(opts) {
|
|
|
2301
2391
|
text: r.text,
|
|
2302
2392
|
reasoning: r.reasoning || '',
|
|
2303
2393
|
opencode_session: r.opencodeSession || '',
|
|
2394
|
+
oc_msg: r.assistantMsgID || '',
|
|
2304
2395
|
canceled: !!r.canceled,
|
|
2305
2396
|
};
|
|
2306
2397
|
try {
|
|
@@ -2334,6 +2425,9 @@ async function tick(opts) {
|
|
|
2334
2425
|
activeMsgTarget = null;
|
|
2335
2426
|
busy = false;
|
|
2336
2427
|
for (const t of involvedTargets) notifyBusy(t, null);
|
|
2428
|
+
// El watcher pudo pedir un barrido mientras opencode estaba ocupado:
|
|
2429
|
+
// se corre ahora que terminó (en vez de perderse).
|
|
2430
|
+
if (sweepPending) { sweepPending = false; scheduleSweep('pendiente'); }
|
|
2337
2431
|
}
|
|
2338
2432
|
return 'trabajo';
|
|
2339
2433
|
}
|
|
@@ -2396,8 +2490,7 @@ process.on('SIGINT', () => {
|
|
|
2396
2490
|
// Historial único: primer poll hecho (lastKnownOc cargado), importamos
|
|
2397
2491
|
// en segundo plano y repetimos cada 15 minutos.
|
|
2398
2492
|
syncSessions().catch(handleError);
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
}, 15 * 60 * 1000);
|
|
2493
|
+
// Watcher de la base de opencode: sincroniza en segundos, no cada 15 min.
|
|
2494
|
+
startSyncWatcher();
|
|
2402
2495
|
scheduleTick(POLL_QUICK_MS);
|
|
2403
2496
|
})();
|
package/src/store/index.js
CHANGED
|
@@ -198,9 +198,14 @@ async function sessionImport(ocSession, name, folder, model, agent, updatedTs, m
|
|
|
198
198
|
let added = 0;
|
|
199
199
|
await jsonfile.update(paths.messagesFile(sid), { messages: [], nextId: 1 }, (data) => {
|
|
200
200
|
const known = {};
|
|
201
|
-
|
|
201
|
+
const knownOc = {};
|
|
202
|
+
const byText = {};
|
|
203
|
+
data.messages.forEach((m, i) => {
|
|
202
204
|
known[String(m.role || '') + '|' + String(m.ts || '') + '|' + md5(mbSubstr(m.text || '', 0, 400))] = true;
|
|
203
|
-
|
|
205
|
+
const ocId = String(m.oc_msg || '').trim();
|
|
206
|
+
if (ocId !== '') knownOc[ocId] = true;
|
|
207
|
+
else byText[String(m.role || '') + '|' + md5(mbSubstr(String(m.text || '').trim(), 0, 400))] = i;
|
|
208
|
+
});
|
|
204
209
|
for (const m of (Array.isArray(messages) ? messages : [])) {
|
|
205
210
|
if (!m || typeof m !== 'object') continue;
|
|
206
211
|
const role = String(m.role || '');
|
|
@@ -210,12 +215,29 @@ async function sessionImport(ocSession, name, folder, model, agent, updatedTs, m
|
|
|
210
215
|
if (Array.from(text).length > 50000) text = mbSubstr(text, 0, 50000);
|
|
211
216
|
let ts = String(m.ts || '');
|
|
212
217
|
if (ts === '' || isNaN(Date.parse(ts))) ts = nowIso();
|
|
218
|
+
const ocMsg = String(m.oc_msg || '').trim();
|
|
219
|
+
// Identidad de opencode: si ya esta, es el mismo mensaje.
|
|
220
|
+
if (ocMsg !== '' && knownOc[ocMsg]) continue;
|
|
213
221
|
const key = role + '|' + ts + '|' + md5(mbSubstr(text, 0, 400));
|
|
214
222
|
if (known[key]) continue;
|
|
223
|
+
// Adopcion: llego de opencode (con oc_msg) y existe uno de la web
|
|
224
|
+
// con el mismo rol+texto y sin id. Se le asigna el id.
|
|
225
|
+
if (ocMsg !== '') {
|
|
226
|
+
const tk = role + '|' + md5(mbSubstr(text, 0, 400));
|
|
227
|
+
if (byText[tk] !== undefined) {
|
|
228
|
+
data.messages[byText[tk]].oc_msg = ocMsg;
|
|
229
|
+
knownOc[ocMsg] = true;
|
|
230
|
+
known[key] = true;
|
|
231
|
+
delete byText[tk];
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
215
235
|
known[key] = true;
|
|
236
|
+
if (ocMsg !== '') knownOc[ocMsg] = true;
|
|
216
237
|
const id = data.nextId;
|
|
217
238
|
data.nextId = id + 1;
|
|
218
239
|
const msg = { id, role, text, ts, status: 'done' };
|
|
240
|
+
if (ocMsg !== '') msg.oc_msg = ocMsg;
|
|
219
241
|
const reasoning = String(m.reasoning || '').trim();
|
|
220
242
|
if (reasoning !== '') msg.reasoning = Array.from(reasoning).length > 50000 ? mbSubstr(reasoning, 0, 50000) : reasoning;
|
|
221
243
|
if (m.agent) msg.agent = mbSubstr(String(m.agent), 0, 40);
|
package/src/web/routes.js
CHANGED
|
@@ -627,6 +627,7 @@ async function handleApi(ctx) {
|
|
|
627
627
|
const userId = parseInt(body.user_id, 10) || 0;
|
|
628
628
|
const text = String(body.text || '').trim();
|
|
629
629
|
const reasoning = String(body.reasoning || '').trim();
|
|
630
|
+
const ocMsg = String(body.oc_msg || '').trim();
|
|
630
631
|
if (sid <= 0 || userId <= 0) return ok({ ok: false, error: 'session_id y user_id son obligatorios' }, 400);
|
|
631
632
|
if (Array.from(text).length > 50000 || Array.from(reasoning).length > 50000) return ok({ ok: false, error: 'Respuesta demasiado larga' }, 400);
|
|
632
633
|
if (text === '' && reasoning === '') return ok({ ok: false, error: 'Nada para publicar' }, 400);
|
|
@@ -639,6 +640,7 @@ async function handleApi(ctx) {
|
|
|
639
640
|
if (draftIdx >= 0) {
|
|
640
641
|
data.messages[draftIdx].text = text;
|
|
641
642
|
if (reasoning !== '') data.messages[draftIdx].reasoning = reasoning;
|
|
643
|
+
if (ocMsg !== '') data.messages[draftIdx].oc_msg = ocMsg;
|
|
642
644
|
data.messages[draftIdx].ts = store.nowIso();
|
|
643
645
|
} else {
|
|
644
646
|
const aid = data.nextId;
|
|
@@ -648,6 +650,7 @@ async function handleApi(ctx) {
|
|
|
648
650
|
draft_for: userId, agent: store.messageAgentOf(data, userId),
|
|
649
651
|
};
|
|
650
652
|
if (reasoning !== '') draft.reasoning = reasoning;
|
|
653
|
+
if (ocMsg !== '') draft.oc_msg = ocMsg;
|
|
651
654
|
data.messages.push(draft);
|
|
652
655
|
}
|
|
653
656
|
});
|
|
@@ -664,6 +667,7 @@ async function handleApi(ctx) {
|
|
|
664
667
|
if (!exists) return ok({ ok: false, error: 'Sesion no encontrada' }, 404);
|
|
665
668
|
const oc = body.opencode_session ? String(body.opencode_session).trim() : '';
|
|
666
669
|
const reasoning = String(body.reasoning || '').trim();
|
|
670
|
+
const ocMsg = String(body.oc_msg || '').trim();
|
|
667
671
|
const clearSession = !!body.clear_session;
|
|
668
672
|
const canceled = !!body.canceled;
|
|
669
673
|
await store.sessionsUpdate((sd) => {
|
|
@@ -706,6 +710,7 @@ async function handleApi(ctx) {
|
|
|
706
710
|
m.answered_ts = store.nowIso();
|
|
707
711
|
m.ts = store.nowIso();
|
|
708
712
|
if (reasoning !== '') m.reasoning = reasoning; else delete m.reasoning;
|
|
713
|
+
if (ocMsg !== '') m.oc_msg = ocMsg;
|
|
709
714
|
if (canceled) m.canceled = true; else delete m.canceled;
|
|
710
715
|
if (!m.agent) m.agent = store.messageAgentOf(data, userId, (sess && sess.agent) || '');
|
|
711
716
|
if (author && !m.author) m.author = author;
|
|
@@ -715,6 +720,7 @@ async function handleApi(ctx) {
|
|
|
715
720
|
data.nextId = aid + 1;
|
|
716
721
|
const nm = { id: aid, role: 'assistant', text, ts: store.nowIso(), status: 'done', agent: store.messageAgentOf(data, userId, (sess && sess.agent) || '') };
|
|
717
722
|
if (reasoning !== '') nm.reasoning = reasoning;
|
|
723
|
+
if (ocMsg !== '') nm.oc_msg = ocMsg;
|
|
718
724
|
if (canceled) nm.canceled = true;
|
|
719
725
|
if (author) nm.author = author;
|
|
720
726
|
data.messages.push(nm);
|