cc-viewer 1.6.32 → 1.6.34
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/dist/assets/{index-BlNBvL5K.css → index-BdC0zG2U.css} +2 -2
- package/dist/assets/{index-AD0x3eII.js → index-DE4AHbP5.js} +159 -159
- package/dist/index.html +2 -2
- package/i18n.js +0 -40
- package/interceptor.js +2 -2
- package/lib/delta-reconstructor.js +75 -3
- package/lib/log-management.js +20 -24
- package/lib/log-stream.js +192 -0
- package/lib/log-watcher.js +25 -8
- package/package.json +1 -1
- package/server.js +110 -140
- package/lib/translator.js +0 -84
package/server.js
CHANGED
|
@@ -43,10 +43,11 @@ import { uploadPlugins, installPluginFromUrl } from './lib/plugin-manager.js';
|
|
|
43
43
|
import { getUserProfile } from './lib/user-profile.js';
|
|
44
44
|
import { getGitDiffs } from './lib/git-diff.js';
|
|
45
45
|
import { CONTEXT_WINDOW_FILE, readModelContextSize, buildContextWindowEvent, getContextSizeForModel } from './lib/context-watcher.js';
|
|
46
|
-
import {
|
|
46
|
+
import { watchLogFile, startWatching, getWatchedFiles } from './lib/log-watcher.js';
|
|
47
47
|
import { isMainAgentEntry, extractCachedContent } from './lib/kv-cache-analyzer.js';
|
|
48
|
-
import { listLocalLogs,
|
|
49
|
-
import {
|
|
48
|
+
import { listLocalLogs, deleteLogFiles, mergeLogFiles } from './lib/log-management.js';
|
|
49
|
+
import { countLogEntries, streamRawEntriesAsync } from './lib/log-stream.js';
|
|
50
|
+
|
|
50
51
|
|
|
51
52
|
const PREFS_FILE = join(LOG_DIR, 'preferences.json');
|
|
52
53
|
|
|
@@ -348,7 +349,7 @@ async function handleRequest(req, res) {
|
|
|
348
349
|
if (url === '/api/resume-choice' && method === 'POST') {
|
|
349
350
|
let body = '';
|
|
350
351
|
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
351
|
-
req.on('end', () => {
|
|
352
|
+
req.on('end', async () => {
|
|
352
353
|
try {
|
|
353
354
|
const { choice } = JSON.parse(body);
|
|
354
355
|
if (choice !== 'continue' && choice !== 'new') {
|
|
@@ -371,12 +372,18 @@ async function handleRequest(req, res) {
|
|
|
371
372
|
client.write(`event: resume_resolved\ndata: ${resolvedData}\n\n`);
|
|
372
373
|
} catch { }
|
|
373
374
|
});
|
|
374
|
-
//
|
|
375
|
-
const
|
|
375
|
+
// 流式分段广播 full_reload,避免全量加载 OOM
|
|
376
|
+
const reloadTotal = countLogEntries(LOG_FILE);
|
|
376
377
|
clients.forEach(client => {
|
|
377
|
-
try {
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
try { client.write(`event: load_start\ndata: ${JSON.stringify({ total: reloadTotal, incremental: false })}\n\n`); } catch { }
|
|
379
|
+
});
|
|
380
|
+
await streamRawEntriesAsync(LOG_FILE, (raw) => {
|
|
381
|
+
clients.forEach(client => {
|
|
382
|
+
try { client.write('event: load_chunk\ndata: ['); client.write(raw); client.write(']\n\n'); } catch { }
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
clients.forEach(client => {
|
|
386
|
+
try { client.write(`event: load_end\ndata: {}\n\n`); } catch { }
|
|
380
387
|
});
|
|
381
388
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
382
389
|
res.end(JSON.stringify({ ok: true, logFile: result.logFile }));
|
|
@@ -388,58 +395,6 @@ async function handleRequest(req, res) {
|
|
|
388
395
|
return;
|
|
389
396
|
}
|
|
390
397
|
|
|
391
|
-
// 翻译 API
|
|
392
|
-
if (url === '/api/translate' && method === 'POST') {
|
|
393
|
-
let body = '';
|
|
394
|
-
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
395
|
-
req.on('end', async () => {
|
|
396
|
-
try {
|
|
397
|
-
const { text, from = 'en', to } = JSON.parse(body);
|
|
398
|
-
if (!text) {
|
|
399
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
400
|
-
res.end(JSON.stringify({ error: 'Missing "text" field' }));
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// 确定目标语言
|
|
405
|
-
const targetLang = to || detectTargetLang(PREFS_FILE);
|
|
406
|
-
|
|
407
|
-
// 获取 API Key(仅 x-api-key 认证,不复用 session token 避免上下文污染)
|
|
408
|
-
// 优先级: 环境变量 > 拦截缓存 > 从 authHeader 中提取 sk- 开头的 key
|
|
409
|
-
let apiKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || _cachedApiKey;
|
|
410
|
-
if (!apiKey && _cachedAuthHeader) {
|
|
411
|
-
const m = _cachedAuthHeader.match(/^Bearer\s+(sk-\S+)$/i);
|
|
412
|
-
if (m) apiKey = m[1];
|
|
413
|
-
}
|
|
414
|
-
if (!apiKey) {
|
|
415
|
-
res.writeHead(501, { 'Content-Type': 'application/json' });
|
|
416
|
-
res.end(JSON.stringify({ error: 'No API key available. Set ANTHROPIC_API_KEY or use x-api-key authentication.' }));
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const result = await translate({
|
|
421
|
-
text,
|
|
422
|
-
from,
|
|
423
|
-
to: targetLang,
|
|
424
|
-
apiKey,
|
|
425
|
-
baseUrl: process.env.ANTHROPIC_BASE_URL,
|
|
426
|
-
model: _cachedHaikuModel,
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
430
|
-
res.end(JSON.stringify(result));
|
|
431
|
-
} catch (err) {
|
|
432
|
-
const status = err.status ? 502 : 500;
|
|
433
|
-
const payload = err.status
|
|
434
|
-
? { error: 'Translation API failed', status: err.status, detail: err.detail }
|
|
435
|
-
: { error: 'Internal error', message: err.message };
|
|
436
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
437
|
-
res.end(JSON.stringify(payload));
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
398
|
// === Workspace API ===
|
|
444
399
|
|
|
445
400
|
// 目录浏览器
|
|
@@ -528,12 +483,18 @@ async function handleRequest(req, res) {
|
|
|
528
483
|
} catch {}
|
|
529
484
|
});
|
|
530
485
|
|
|
531
|
-
//
|
|
532
|
-
const
|
|
486
|
+
// 流式分段广播以刷新会话区域,避免全量加载 OOM
|
|
487
|
+
const wsReloadTotal = countLogEntries(LOG_FILE);
|
|
533
488
|
clients.forEach(client => {
|
|
534
|
-
try {
|
|
535
|
-
|
|
536
|
-
|
|
489
|
+
try { client.write(`event: load_start\ndata: ${JSON.stringify({ total: wsReloadTotal, incremental: false })}\n\n`); } catch {}
|
|
490
|
+
});
|
|
491
|
+
await streamRawEntriesAsync(LOG_FILE, (raw) => {
|
|
492
|
+
clients.forEach(client => {
|
|
493
|
+
try { client.write('event: load_chunk\ndata: ['); client.write(raw); client.write(']\n\n'); } catch {}
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
clients.forEach(client => {
|
|
497
|
+
try { client.write(`event: load_end\ndata: {}\n\n`); } catch {}
|
|
537
498
|
});
|
|
538
499
|
|
|
539
500
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -632,62 +593,48 @@ async function handleRequest(req, res) {
|
|
|
632
593
|
res.write(`event: resume_prompt\ndata: ${JSON.stringify({ recentFileName: _resumeState.recentFileName })}\n\n`);
|
|
633
594
|
}
|
|
634
595
|
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
let entriesToSend = entries;
|
|
640
|
-
let incremental = false;
|
|
641
|
-
if (since && cc > 0) {
|
|
642
|
-
const sinceMs = new Date(since).getTime();
|
|
643
|
-
if (!isNaN(sinceMs)) {
|
|
644
|
-
const delta = entries.filter(e => e.timestamp && new Date(e.timestamp).getTime() > sinceMs);
|
|
645
|
-
if (cc + delta.length === entries.length) {
|
|
646
|
-
entriesToSend = delta;
|
|
647
|
-
incremental = true;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
// 分段发送:先告知总数,再分块传输,让前端能显示真实加载进度
|
|
652
|
-
const CHUNK_SIZE = 50;
|
|
653
|
-
if (entriesToSend.length > CHUNK_SIZE) {
|
|
654
|
-
res.write(`event: load_start\ndata: ${JSON.stringify({ total: entriesToSend.length, incremental })}\n\n`);
|
|
655
|
-
for (let i = 0; i < entriesToSend.length; i += CHUNK_SIZE) {
|
|
656
|
-
const chunk = entriesToSend.slice(i, i + CHUNK_SIZE);
|
|
657
|
-
res.write(`event: load_chunk\ndata: ${JSON.stringify(chunk)}\n\n`);
|
|
658
|
-
}
|
|
659
|
-
res.write(`event: load_end\ndata: {}\n\n`);
|
|
660
|
-
} else if (incremental) {
|
|
661
|
-
// 增量模式:即使条目少也走 load_start/load_end 流程(可能 0 条新数据)
|
|
662
|
-
res.write(`event: load_start\ndata: ${JSON.stringify({ total: entriesToSend.length, incremental: true })}\n\n`);
|
|
663
|
-
if (entriesToSend.length > 0) {
|
|
664
|
-
res.write(`event: load_chunk\ndata: ${JSON.stringify(entriesToSend)}\n\n`);
|
|
665
|
-
}
|
|
666
|
-
res.write(`event: load_end\ndata: {}\n\n`);
|
|
667
|
-
} else {
|
|
668
|
-
res.write(`event: full_reload\ndata: ${JSON.stringify(entriesToSend)}\n\n`);
|
|
669
|
-
}
|
|
596
|
+
// 流式发送原始 delta 条目,客户端自行重建(避免 server OOM)
|
|
597
|
+
// 注:streamRawEntriesAsync 不支持 since 过滤,始终发送全量数据
|
|
598
|
+
const total = countLogEntries(LOG_FILE);
|
|
599
|
+
res.write(`event: load_start\ndata: ${JSON.stringify({ total, incremental: false })}\n\n`);
|
|
670
600
|
|
|
671
|
-
//
|
|
601
|
+
// 流式分段发送 + 追踪最新 MainAgent 的 KV-Cache 和 context_window
|
|
602
|
+
let latestKvCache = null;
|
|
603
|
+
let latestContextWindow = null;
|
|
672
604
|
let pushedContextWindow = false;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
605
|
+
|
|
606
|
+
await streamRawEntriesAsync(LOG_FILE, (raw) => {
|
|
607
|
+
// 直接发送原始 JSON 字符串,不做 parse/reconstruct/stringify
|
|
608
|
+
res.write('event: load_chunk\ndata: [');
|
|
609
|
+
res.write(raw);
|
|
610
|
+
res.write(']\n\n');
|
|
611
|
+
// 轻量追踪最新 MainAgent 的 KV-Cache 和 context_window(仅 regex 检测)
|
|
612
|
+
if (raw.includes('"mainAgent":true') || raw.includes('"mainAgent": true')) {
|
|
613
|
+
try {
|
|
614
|
+
const entry = JSON.parse(raw);
|
|
615
|
+
if (isMainAgentEntry(entry)) {
|
|
616
|
+
const cached = extractCachedContent(entry);
|
|
617
|
+
if (cached) latestKvCache = cached;
|
|
618
|
+
const usage = entry.response?.body?.usage;
|
|
619
|
+
if (usage) {
|
|
620
|
+
const contextSize = getContextSizeForModel(entry.body?.model);
|
|
621
|
+
const cw = buildContextWindowEvent(usage, contextSize);
|
|
622
|
+
if (cw) latestContextWindow = cw;
|
|
623
|
+
}
|
|
687
624
|
}
|
|
688
|
-
}
|
|
689
|
-
break;
|
|
625
|
+
} catch { }
|
|
690
626
|
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
res.write(`event: load_end\ndata: {}\n\n`);
|
|
630
|
+
|
|
631
|
+
// 发送最新 MainAgent 的 KV-Cache 和 context_window
|
|
632
|
+
if (latestKvCache) {
|
|
633
|
+
res.write(`event: kv_cache_content\ndata: ${JSON.stringify(latestKvCache)}\n\n`);
|
|
634
|
+
}
|
|
635
|
+
if (latestContextWindow) {
|
|
636
|
+
res.write(`event: context_window\ndata: ${JSON.stringify(latestContextWindow)}\n\n`);
|
|
637
|
+
pushedContextWindow = true;
|
|
691
638
|
}
|
|
692
639
|
// Fallback: no MainAgent in log (e.g. fresh session after -c), read context-window.json
|
|
693
640
|
if (!pushedContextWindow) {
|
|
@@ -718,9 +665,17 @@ async function handleRequest(req, res) {
|
|
|
718
665
|
|
|
719
666
|
// API endpoint
|
|
720
667
|
if (url === '/api/requests' && method === 'GET') {
|
|
721
|
-
|
|
668
|
+
// 异步流式 JSON 数组输出,不做 reconstruct,发原始条目
|
|
722
669
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
723
|
-
res.
|
|
670
|
+
res.write('[');
|
|
671
|
+
let first = true;
|
|
672
|
+
await streamRawEntriesAsync(LOG_FILE, (raw) => {
|
|
673
|
+
if (!first) res.write(',');
|
|
674
|
+
res.write(raw);
|
|
675
|
+
first = false;
|
|
676
|
+
});
|
|
677
|
+
res.write(']');
|
|
678
|
+
res.end();
|
|
724
679
|
return;
|
|
725
680
|
}
|
|
726
681
|
|
|
@@ -1325,24 +1280,17 @@ async function handleRequest(req, res) {
|
|
|
1325
1280
|
const stream = createReadStream(realPath);
|
|
1326
1281
|
stream.pipe(res);
|
|
1327
1282
|
} else {
|
|
1328
|
-
//
|
|
1329
|
-
const { readLocalLog } = await import('./lib/log-management.js');
|
|
1330
|
-
const entries = readLocalLog(LOG_DIR, file);
|
|
1331
|
-
// 清除 delta 元字段
|
|
1332
|
-
for (const entry of entries) {
|
|
1333
|
-
delete entry._deltaFormat;
|
|
1334
|
-
delete entry._totalMessageCount;
|
|
1335
|
-
delete entry._conversationId;
|
|
1336
|
-
delete entry._isCheckpoint;
|
|
1337
|
-
}
|
|
1338
|
-
const content = entries.map(e => JSON.stringify(e)).join('\n---\n') + '\n---\n';
|
|
1339
|
-
const buf = Buffer.from(content, 'utf-8');
|
|
1283
|
+
// 流式下载原始条目(不重建,保持 delta 格式),避免 OOM
|
|
1340
1284
|
res.writeHead(200, {
|
|
1341
1285
|
'Content-Type': 'application/octet-stream',
|
|
1342
1286
|
'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
|
|
1343
|
-
'
|
|
1287
|
+
'Transfer-Encoding': 'chunked',
|
|
1288
|
+
});
|
|
1289
|
+
await streamRawEntriesAsync(realPath, (raw) => {
|
|
1290
|
+
res.write(raw);
|
|
1291
|
+
res.write('\n---\n');
|
|
1344
1292
|
});
|
|
1345
|
-
res.end(
|
|
1293
|
+
res.end();
|
|
1346
1294
|
}
|
|
1347
1295
|
} catch (err) {
|
|
1348
1296
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
@@ -1368,13 +1316,35 @@ async function handleRequest(req, res) {
|
|
|
1368
1316
|
}
|
|
1369
1317
|
|
|
1370
1318
|
try {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1319
|
+
// 独立 SSE 流:直接向请求方返回 event-stream,不走 /events 广播
|
|
1320
|
+
const { validateLogPath } = await import('./lib/log-management.js');
|
|
1321
|
+
validateLogPath(LOG_DIR, file);
|
|
1322
|
+
const filePath = join(LOG_DIR, file);
|
|
1323
|
+
const total = countLogEntries(filePath);
|
|
1324
|
+
|
|
1325
|
+
res.writeHead(200, {
|
|
1326
|
+
'Content-Type': 'text/event-stream',
|
|
1327
|
+
'Cache-Control': 'no-cache',
|
|
1328
|
+
'Connection': 'keep-alive',
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
res.write(`event: load_start\ndata: ${JSON.stringify({ total, incremental: false })}\n\n`);
|
|
1332
|
+
await streamRawEntriesAsync(filePath, (raw) => {
|
|
1333
|
+
res.write('event: load_chunk\ndata: [');
|
|
1334
|
+
res.write(raw);
|
|
1335
|
+
res.write(']\n\n');
|
|
1336
|
+
});
|
|
1337
|
+
res.write(`event: load_end\ndata: {}\n\n`);
|
|
1338
|
+
res.end();
|
|
1374
1339
|
} catch (err) {
|
|
1375
|
-
|
|
1376
|
-
res.
|
|
1377
|
-
|
|
1340
|
+
// 如果 headers 未发送,返回 JSON 错误;否则关闭连接
|
|
1341
|
+
if (!res.headersSent) {
|
|
1342
|
+
const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'ACCESS_DENIED' ? 403 : 500;
|
|
1343
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
1344
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1345
|
+
} else {
|
|
1346
|
+
res.end();
|
|
1347
|
+
}
|
|
1378
1348
|
}
|
|
1379
1349
|
return;
|
|
1380
1350
|
}
|
package/lib/translator.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import { detectLanguage } from '../i18n.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Determine the target language for translation.
|
|
6
|
-
* Priority: explicit `to` param > prefs file lang > system locale.
|
|
7
|
-
* @param {string} prefsFile - Path to the preferences JSON file
|
|
8
|
-
* @returns {string} target language code
|
|
9
|
-
*/
|
|
10
|
-
export function detectTargetLang(prefsFile) {
|
|
11
|
-
let targetLang;
|
|
12
|
-
try {
|
|
13
|
-
if (prefsFile && existsSync(prefsFile)) {
|
|
14
|
-
const prefs = JSON.parse(readFileSync(prefsFile, 'utf-8'));
|
|
15
|
-
if (prefs.lang) targetLang = prefs.lang;
|
|
16
|
-
}
|
|
17
|
-
} catch { }
|
|
18
|
-
if (!targetLang) targetLang = detectLanguage();
|
|
19
|
-
return targetLang;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Translate text using the Claude API.
|
|
24
|
-
* @param {Object} opts
|
|
25
|
-
* @param {string|string[]} opts.text - Text or array of texts to translate
|
|
26
|
-
* @param {string} opts.from - Source language code
|
|
27
|
-
* @param {string} opts.to - Target language code
|
|
28
|
-
* @param {string} opts.apiKey - Anthropic API key
|
|
29
|
-
* @param {string} [opts.baseUrl='https://api.anthropic.com'] - API base URL
|
|
30
|
-
* @param {string} [opts.model='claude-haiku-4-5-20251001'] - Model to use
|
|
31
|
-
* @returns {Promise<{text: string|string[], from: string, to: string}>}
|
|
32
|
-
*/
|
|
33
|
-
export async function translate({ text, from, to, apiKey, baseUrl, model }) {
|
|
34
|
-
// Same language — no-op
|
|
35
|
-
if (from === to) {
|
|
36
|
-
return { text, from, to };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const effectiveBaseUrl = baseUrl || 'https://api.anthropic.com';
|
|
40
|
-
const effectiveModel = model || 'claude-haiku-4-5-20251001';
|
|
41
|
-
const inputText = Array.isArray(text) ? text.join('\n---SPLIT---\n') : text;
|
|
42
|
-
|
|
43
|
-
const reqHeaders = {
|
|
44
|
-
'Content-Type': 'application/json',
|
|
45
|
-
'anthropic-version': '2023-06-01',
|
|
46
|
-
'x-api-key': apiKey,
|
|
47
|
-
'x-cc-viewer-internal': '1',
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const apiRes = await fetch(`${effectiveBaseUrl}/v1/messages`, {
|
|
51
|
-
method: 'POST',
|
|
52
|
-
headers: reqHeaders,
|
|
53
|
-
body: JSON.stringify({
|
|
54
|
-
model: effectiveModel,
|
|
55
|
-
max_tokens: 32000,
|
|
56
|
-
tools: [],
|
|
57
|
-
system: [{
|
|
58
|
-
type: "text",
|
|
59
|
-
text: `You are a translator. Translate the following text from ${from} to ${to}. Output only the translated text, nothing else.`
|
|
60
|
-
}],
|
|
61
|
-
messages: [{ role: 'user', content: inputText }],
|
|
62
|
-
stream: false,
|
|
63
|
-
temperature: 1,
|
|
64
|
-
}),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
if (!apiRes.ok) {
|
|
68
|
-
const errBody = await apiRes.text();
|
|
69
|
-
const err = new Error(`Translation API failed (status ${apiRes.status}): ${errBody}`);
|
|
70
|
-
err.status = apiRes.status;
|
|
71
|
-
err.detail = errBody;
|
|
72
|
-
throw err;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const apiData = await apiRes.json();
|
|
76
|
-
let translated = apiData.content?.[0]?.text || '';
|
|
77
|
-
|
|
78
|
-
// If input was an array, split the result back into an array
|
|
79
|
-
if (Array.isArray(text)) {
|
|
80
|
-
translated = translated.split(/\n?---SPLIT---\n?/);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return { text: translated, from, to };
|
|
84
|
-
}
|