zames_pro 2.9.0 → 2.9.2
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/agent-loop.js +1 -1
- package/dist/attachments.js +64 -18
- package/dist/browser.js +30 -1
- package/dist/i18n.js +6 -2
- package/dist/index.js +5 -3
- package/dist/path-token.js +71 -0
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -171,7 +171,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
171
171
|
trimmed.length < 2 ||
|
|
172
172
|
/^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i.test(trimmed) ||
|
|
173
173
|
(trimmed.length <= 200 &&
|
|
174
|
-
/(messages? too frequent|too many requests|rate limit|слишком часто|try again later)/i.test(trimmed));
|
|
174
|
+
/(messages? too frequent|too many requests|rate limit|server (is )?busy|service (is )?unavailable|слишком часто|try again later)/i.test(trimmed));
|
|
175
175
|
if (looksService && stallRetries < MAX_STALL_RETRIES) {
|
|
176
176
|
stallRetries++;
|
|
177
177
|
transcript?.log('stall_retry', {
|
package/dist/attachments.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
|
+
import { extractPathToken } from './path-token.js';
|
|
6
|
+
export { extractPathToken };
|
|
5
7
|
export const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico'];
|
|
6
8
|
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
7
9
|
function sanitize(name) {
|
|
@@ -182,6 +184,15 @@ export async function saveToTemp(tmpDir, name, data) {
|
|
|
182
184
|
await fs.writeFile(target, data);
|
|
183
185
|
return target;
|
|
184
186
|
}
|
|
187
|
+
// True if any OS clipboard tool is available on this machine.
|
|
188
|
+
export function hasClipboardTool() {
|
|
189
|
+
if (process.platform === 'win32' || process.platform === 'darwin')
|
|
190
|
+
return true;
|
|
191
|
+
if (process.platform === 'linux') {
|
|
192
|
+
return Boolean(tryCommand('xclip', ['-version']) || tryCommand('xsel', ['--version']) || tryCommand('wl-paste', ['--version']));
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
185
196
|
// Read an image from the OS clipboard. Tries, in order, every known tool for
|
|
186
197
|
// the current platform. Returns the raw bytes plus a short note about which
|
|
187
198
|
// tool was used (or the reason nothing was found) — the caller shows that note
|
|
@@ -189,38 +200,53 @@ export async function saveToTemp(tmpDir, name, data) {
|
|
|
189
200
|
// True when running inside WSL. There the X11 clipboard is a SEPARATE
|
|
190
201
|
// clipboard from the Windows one: the user copies an image in Windows, so we
|
|
191
202
|
// must read it through powershell.exe, not xclip.
|
|
203
|
+
export function windowsReachable() {
|
|
204
|
+
if (process.platform === 'win32')
|
|
205
|
+
return true;
|
|
206
|
+
if (process.platform !== 'linux')
|
|
207
|
+
return false;
|
|
208
|
+
try {
|
|
209
|
+
return require('fs').existsSync('/mnt/c/Windows');
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
192
215
|
export function isWsl() {
|
|
193
216
|
if (process.platform !== 'linux')
|
|
194
217
|
return false;
|
|
218
|
+
let kernelWsl = false;
|
|
195
219
|
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
220
|
+
kernelWsl = true;
|
|
221
|
+
if (!kernelWsl) {
|
|
222
|
+
try {
|
|
223
|
+
const rel = os.release().toLowerCase();
|
|
224
|
+
if (rel.includes('microsoft') || rel.includes('wsl'))
|
|
225
|
+
kernelWsl = true;
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
201
228
|
}
|
|
202
|
-
|
|
203
|
-
return false;
|
|
229
|
+
return kernelWsl && windowsReachable();
|
|
204
230
|
}
|
|
205
|
-
// PowerShell
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
'$img.Save($ms,[System.Drawing.Imaging.ImageFormat]::Png); ' +
|
|
210
|
-
'[Console]::OpenStandardOutput().Write($ms.ToArray(),0,$ms.Length)}';
|
|
231
|
+
// PowerShell image grabber, embedded as a UTF-16LE base64 script and run
|
|
232
|
+
// via powershell.exe -EncodedCommand: it saves the clipboard image to a
|
|
233
|
+
// Windows temp .png and prints the Windows path; we read it from WSL.
|
|
234
|
+
const PS_IMAGE_ENCODED = 'QQBkAGQALQBUAHkAcABlACAALQBBAHMAcwBlAG0AYgBsAHkATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwAsAFMAeQBzAHQAZQBtAC4ARAByAGEAdwBpAG4AZwAKACQAaQBtAGcAPQBbAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwAuAEMAbABpAHAAYgBvAGEAcgBkAF0AOgA6AEcAZQB0AEkAbQBhAGcAZQAoACkACgBpAGYAKAAkAGkAbQBnACkAewAkAHAAPQBKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBUAEUATQBQACAAKAAiAHoAYQBtAGUAcwAtAGMAbABpAHAALQAiACsAWwBHAHUAaQBkAF0AOgA6AE4AZQB3AEcAdQBpAGQAKAApAC4AVABvAFMAdAByAGkAbgBnACgAKQArACIALgBwAG4AZwAiACkAOwAkAGkAbQBnAC4AUwBhAHYAZQAoACQAcAAsAFsAUwB5AHMAdABlAG0ALgBEAHIAYQB3AGkAbgBnAC4ASQBtAGEAZwBpAG4AZwAuAEkAbQBhAGcAZQBGAG8AcgBtAGEAdABdADoAOgBQAG4AZwApADsAVwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIAAkAHAAfQAKAA==';
|
|
211
235
|
// PowerShell one-liner: write the clipboard file paths (CF_HDROP) to stdout.
|
|
212
236
|
const PS_GET_FILE_DROP = 'Add-Type -AssemblyName System.Windows.Forms; ' +
|
|
213
237
|
'$d=[System.Windows.Forms.Clipboard]::GetDataObject(); ' +
|
|
214
238
|
'if($d -and $d.GetDataPresent([System.Windows.Forms.DataFormats]::FileDrop)){' +
|
|
215
239
|
'$f=$d.GetData([System.Windows.Forms.DataFormats]::FileDrop); $f -join [Environment]::NewLine}';
|
|
216
|
-
export function readClipboardImageDetailed() {
|
|
240
|
+
export async function readClipboardImageDetailed() {
|
|
217
241
|
if (process.platform === 'linux' || process.platform === 'freebsd' || process.platform === 'openbsd') {
|
|
218
242
|
const wayland = process.env.WAYLAND_DISPLAY;
|
|
219
243
|
const x11 = process.env.DISPLAY;
|
|
220
244
|
const attempts = [];
|
|
221
245
|
// On WSL the Windows clipboard is what the user copies into — read it first.
|
|
222
246
|
if (isWsl()) {
|
|
223
|
-
|
|
247
|
+
const win = await readWindowsClipboardImage();
|
|
248
|
+
if (win)
|
|
249
|
+
return { data: win, via: 'powershell.exe' };
|
|
224
250
|
}
|
|
225
251
|
if (wayland || !x11)
|
|
226
252
|
attempts.push({ bin: 'wl-paste', args: ['--type', 'image/png', '--no-newline'], via: 'wl-paste' });
|
|
@@ -261,8 +287,8 @@ export function readClipboardImageDetailed() {
|
|
|
261
287
|
return { data: null, via: 'unsupported-platform' };
|
|
262
288
|
}
|
|
263
289
|
// Backwards-compatible wrapper used elsewhere.
|
|
264
|
-
export function readClipboardImage() {
|
|
265
|
-
return readClipboardImageDetailed().data;
|
|
290
|
+
export async function readClipboardImage() {
|
|
291
|
+
return (await readClipboardImageDetailed()).data;
|
|
266
292
|
}
|
|
267
293
|
function tryCommand(bin, args) {
|
|
268
294
|
try {
|
|
@@ -303,7 +329,7 @@ export async function findFileByName(workdir, name) {
|
|
|
303
329
|
export function readWindowsClipboardFiles() {
|
|
304
330
|
if (!isWsl())
|
|
305
331
|
return [];
|
|
306
|
-
const out = tryCommand('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', PS_GET_FILE_DROP]);
|
|
332
|
+
const out = tryCommand('powershell.exe', ['-STA', '-NoProfile', '-NonInteractive', '-Command', PS_GET_FILE_DROP]);
|
|
307
333
|
if (!out)
|
|
308
334
|
return [];
|
|
309
335
|
const text = out.toString('utf-8');
|
|
@@ -330,3 +356,23 @@ export function winPathToWsl(winPath) {
|
|
|
330
356
|
const rest = p.slice(3).split('\\').join('/');
|
|
331
357
|
return '/mnt/' + drive + '/' + rest;
|
|
332
358
|
}
|
|
359
|
+
// Run the embedded PowerShell script and return the clipboard image bytes.
|
|
360
|
+
export async function readWindowsClipboardImage() {
|
|
361
|
+
if (!isWsl())
|
|
362
|
+
return null;
|
|
363
|
+
const out = tryCommand('powershell.exe', ['-STA', '-NoProfile', '-NonInteractive', '-EncodedCommand', PS_IMAGE_ENCODED]);
|
|
364
|
+
if (!out)
|
|
365
|
+
return null;
|
|
366
|
+
const first = out.toString('utf-8').trim().split(String.fromCharCode(10))[0].trim();
|
|
367
|
+
if (!first)
|
|
368
|
+
return null;
|
|
369
|
+
const wslPath = winPathToWsl(first);
|
|
370
|
+
if (!wslPath)
|
|
371
|
+
return null;
|
|
372
|
+
try {
|
|
373
|
+
return await fs.readFile(wslPath);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
package/dist/browser.js
CHANGED
|
@@ -40,9 +40,20 @@ const STOP_SELECTORS = [
|
|
|
40
40
|
const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i;
|
|
41
41
|
// DeepSeek's answer when the rate limit is exceeded.
|
|
42
42
|
const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate limit|слишком часто|повторите позже|try again later)/i;
|
|
43
|
+
// Transient server-side hiccups (DeepSeek overloaded / hiccup). Unlike the
|
|
44
|
+
// rate limit, these usually clear in seconds, so we retry quickly instead of
|
|
45
|
+
// waiting minutes. "Server busy", 503, "temporarily unavailable", etc.
|
|
46
|
+
const SERVER_BUSY_RE = /(server (is )?busy|server error|service (is )?unavailable|temporarily unavailable|internal server error|502|503|504|server overloaded|сервер занят|сервер перегружен|сервис недоступен|внутренняя ошибка|попробуйте позже)/i;
|
|
43
47
|
export function isRateLimitText(text) {
|
|
44
48
|
return RATE_LIMIT_RE.test(String(text || ''));
|
|
45
49
|
}
|
|
50
|
+
export function isServerBusyText(text) {
|
|
51
|
+
const t = String(text || '');
|
|
52
|
+
// Rate limit takes priority (it needs the long wait).
|
|
53
|
+
if (RATE_LIMIT_RE.test(t))
|
|
54
|
+
return false;
|
|
55
|
+
return SERVER_BUSY_RE.test(t);
|
|
56
|
+
}
|
|
46
57
|
// Normalize text for comparison: collapse whitespace so that tiny DOM
|
|
47
58
|
// differences (nbsp, trailing spaces) don't count as "a new answer".
|
|
48
59
|
export function normText(s) {
|
|
@@ -59,6 +70,13 @@ export class RateLimitError extends Error {
|
|
|
59
70
|
this.name = 'RateLimitError';
|
|
60
71
|
}
|
|
61
72
|
}
|
|
73
|
+
// DeepSeek transient server error (Server busy).
|
|
74
|
+
export class ServerBusyError extends Error {
|
|
75
|
+
constructor(detail) {
|
|
76
|
+
super('Server busy. Try again later. ' + detail);
|
|
77
|
+
this.name = 'ServerBusyError';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
62
80
|
// ---------- profile cleanup ----------
|
|
63
81
|
async function killStaleChrome() {
|
|
64
82
|
if (process.platform !== 'win32')
|
|
@@ -105,6 +123,8 @@ export class DeepSeekBrowser {
|
|
|
105
123
|
minSendIntervalMs;
|
|
106
124
|
rateLimitWaitMs;
|
|
107
125
|
maxRateLimitRetries;
|
|
126
|
+
maxServerBusyRetries;
|
|
127
|
+
serverBusyWaitMs;
|
|
108
128
|
_lastSentAt;
|
|
109
129
|
_abort;
|
|
110
130
|
// The user pressed Esc/Ctrl+C — a "stop" for the WHOLE current batch of
|
|
@@ -119,7 +139,7 @@ export class DeepSeekBrowser {
|
|
|
119
139
|
_netSniff;
|
|
120
140
|
_netSniffLimit;
|
|
121
141
|
_netHookInstalled;
|
|
122
|
-
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, } = {}) {
|
|
142
|
+
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, } = {}) {
|
|
123
143
|
this.headless = headless;
|
|
124
144
|
this.debug = debug;
|
|
125
145
|
this.channel = channel;
|
|
@@ -130,6 +150,8 @@ export class DeepSeekBrowser {
|
|
|
130
150
|
this.minSendIntervalMs = minSendIntervalMs;
|
|
131
151
|
this.rateLimitWaitMs = rateLimitWaitMs;
|
|
132
152
|
this.maxRateLimitRetries = maxRateLimitRetries;
|
|
153
|
+
this.maxServerBusyRetries = maxServerBusyRetries;
|
|
154
|
+
this.serverBusyWaitMs = serverBusyWaitMs;
|
|
133
155
|
this._lastSentAt = 0;
|
|
134
156
|
this._abort = false;
|
|
135
157
|
this._stopped = false;
|
|
@@ -420,6 +442,7 @@ export class DeepSeekBrowser {
|
|
|
420
442
|
let lastErr = null;
|
|
421
443
|
let attempt = 0;
|
|
422
444
|
let rateLimitRetries = 0;
|
|
445
|
+
let serverBusyRetries = 0;
|
|
423
446
|
while (attempt < this.askRetries) {
|
|
424
447
|
attempt++;
|
|
425
448
|
try {
|
|
@@ -629,6 +652,9 @@ export class DeepSeekBrowser {
|
|
|
629
652
|
const pageText = await this._readPageText();
|
|
630
653
|
if (isRateLimitText(pageText)) {
|
|
631
654
|
throw new RateLimitError(pageText.slice(0, 300));
|
|
655
|
+
if (isServerBusyText(pageText)) {
|
|
656
|
+
throw new ServerBusyError(pageText.slice(0, 300));
|
|
657
|
+
}
|
|
632
658
|
}
|
|
633
659
|
const cur = await this._readLastAnswerTextClean().catch(() => '');
|
|
634
660
|
const bodyLen = await this.page
|
|
@@ -672,6 +698,9 @@ export class DeepSeekBrowser {
|
|
|
672
698
|
const pageText = await this._readPageText();
|
|
673
699
|
if (isRateLimitText(pageText)) {
|
|
674
700
|
throw new RateLimitError(pageText.slice(0, 300));
|
|
701
|
+
if (isServerBusyText(pageText)) {
|
|
702
|
+
throw new ServerBusyError(pageText.slice(0, 300));
|
|
703
|
+
}
|
|
675
704
|
}
|
|
676
705
|
}
|
|
677
706
|
const cur = await this._readLastAnswerTextClean().catch(() => '');
|
package/dist/i18n.js
CHANGED
|
@@ -150,8 +150,12 @@ const CATALOG = {
|
|
|
150
150
|
en: '⚠ No image found in the clipboard (tried: {via}).',
|
|
151
151
|
},
|
|
152
152
|
'msg.clip_hint': {
|
|
153
|
-
ru: '
|
|
154
|
-
en: '
|
|
153
|
+
ru: ' Проще всего: вставьте путь к файлу (например tmp/pic.png) — файл приложится к сообщению. Для клипборда нужна утилита (Linux/X11 — xclip или xsel, Wayland — wl-clipboard; Windows — PowerShell, macOS — pngpaste).',
|
|
154
|
+
en: ' Easiest: paste a path to a file (e.g. tmp/pic.png) — it will be attached to the message. Clipboard paste needs a tool (Linux/X11 — xclip or xsel, Wayland — wl-clipboard; Windows — PowerShell, macOS — pngpaste).',
|
|
155
|
+
},
|
|
156
|
+
'msg.clip_container': {
|
|
157
|
+
ru: ' Похоже, вы в контейнере/Dev Container: клипборд Windows отсюда недоступен. Вставьте путь к файлу (например tmp/pic.png) — он приложится к сообщению.',
|
|
158
|
+
en: ' Looks like a container/Dev Container: the Windows clipboard is not reachable from here. Paste a path to a file (e.g. tmp/pic.png) — it will be attached to the message.',
|
|
155
159
|
},
|
|
156
160
|
'msg.from_queue': { ru: '▶ Из очереди: ', en: '▶ From queue: ' },
|
|
157
161
|
'msg.agent_error': { ru: '✖ Ошибка агента:', en: '✖ Agent error:' },
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createTools } from './tools.js';
|
|
|
8
8
|
import { runAgentLoop } from './agent-loop.js';
|
|
9
9
|
import { createSpinner } from './spinner.js';
|
|
10
10
|
import { LineEditor, expandPastes, pasteReplacement } from './input.js';
|
|
11
|
-
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, } from './attachments.js';
|
|
11
|
+
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, } from './attachments.js';
|
|
12
12
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
13
13
|
import { runConfigMenu } from './config-menu.js';
|
|
14
14
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
@@ -995,7 +995,7 @@ async function main() {
|
|
|
995
995
|
return att;
|
|
996
996
|
};
|
|
997
997
|
ed.onClipboard = async () => {
|
|
998
|
-
const res = readClipboardImageDetailed();
|
|
998
|
+
const res = await readClipboardImageDetailed();
|
|
999
999
|
if (!res.data || !res.data.length) {
|
|
1000
1000
|
// On WSL the user may have copied a FILE in Windows (not an image):
|
|
1001
1001
|
// the Windows clipboard holds its path - attach it directly.
|
|
@@ -1021,7 +1021,9 @@ async function main() {
|
|
|
1021
1021
|
clipboardWarned = true;
|
|
1022
1022
|
ed.printAbove(theme.warn(t('msg.clip_empty', { via: res.via }) +
|
|
1023
1023
|
String.fromCharCode(10) +
|
|
1024
|
-
|
|
1024
|
+
(process.platform === 'linux' && !hasClipboardTool()
|
|
1025
|
+
? t('msg.clip_container')
|
|
1026
|
+
: t('msg.clip_hint'))));
|
|
1025
1027
|
}
|
|
1026
1028
|
return null;
|
|
1027
1029
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Find a single file path inside arbitrary text (a pasted path surrounded by
|
|
2
|
+
// words, e.g. a quoted path followed by a question). Returns the cleaned path
|
|
3
|
+
// plus the text around it, or null. Implemented WITHOUT regular expressions so
|
|
4
|
+
// the source has no escaping pitfalls.
|
|
5
|
+
export function extractPathToken(text) {
|
|
6
|
+
const s = String(text || '');
|
|
7
|
+
const chars = Array.from(s);
|
|
8
|
+
const n = chars.length;
|
|
9
|
+
let i = 0;
|
|
10
|
+
while (i < n) {
|
|
11
|
+
const c = chars[i];
|
|
12
|
+
if (c === ' ' || c === ' ') {
|
|
13
|
+
i++;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
let quote = '';
|
|
17
|
+
let start = i;
|
|
18
|
+
if (c === String.fromCharCode(39) || c === String.fromCharCode(34)) {
|
|
19
|
+
quote = c;
|
|
20
|
+
start = i + 1;
|
|
21
|
+
i = start;
|
|
22
|
+
while (i < n && chars[i] !== quote)
|
|
23
|
+
i++;
|
|
24
|
+
const tok = chars.slice(start, i).join('');
|
|
25
|
+
if (looksLikeToken(tok)) {
|
|
26
|
+
const before = s.slice(0, start - 1);
|
|
27
|
+
const after = i < n ? s.slice(i + 1) : '';
|
|
28
|
+
return { path: tok, before, after };
|
|
29
|
+
}
|
|
30
|
+
if (i < n)
|
|
31
|
+
i++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
i = start;
|
|
35
|
+
while (i < n && chars[i] !== ' ' && chars[i] !== ' ')
|
|
36
|
+
i++;
|
|
37
|
+
const tok = chars.slice(start, i).join('');
|
|
38
|
+
const cleaned = stripPunct(tok);
|
|
39
|
+
if (looksLikeToken(cleaned)) {
|
|
40
|
+
const before = s.slice(0, start);
|
|
41
|
+
const after = s.slice(start + tok.length);
|
|
42
|
+
return { path: cleaned, before, after };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function stripPunct(tok) {
|
|
48
|
+
let t = tok;
|
|
49
|
+
while (t.length && ',.!?;:)]'.includes(t[t.length - 1]))
|
|
50
|
+
t = t.slice(0, -1);
|
|
51
|
+
while (t.length && '(['.includes(t[0]))
|
|
52
|
+
t = t.slice(1);
|
|
53
|
+
return t;
|
|
54
|
+
}
|
|
55
|
+
function looksLikeToken(tok) {
|
|
56
|
+
const t = String(tok || '');
|
|
57
|
+
if (!t || t.length > 500)
|
|
58
|
+
return false;
|
|
59
|
+
const dot = t.lastIndexOf('.');
|
|
60
|
+
if (dot <= 0 || dot >= t.length - 1)
|
|
61
|
+
return false;
|
|
62
|
+
const ext = t.slice(dot + 1);
|
|
63
|
+
if (ext.length > 8)
|
|
64
|
+
return false;
|
|
65
|
+
for (const ch of ext) {
|
|
66
|
+
const ok = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9');
|
|
67
|
+
if (!ok)
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|