granclaw 0.0.1-beta.32 → 0.0.1-beta.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.
Files changed (70) hide show
  1. package/dist/backend/agent/process.js +14 -0
  2. package/dist/backend/agent/runner-pi.js +213 -1
  3. package/dist/backend/agent/telegram-adapter.js +36 -6
  4. package/dist/backend/assets/capmonster-extension/_locales/en/messages.json +226 -0
  5. package/dist/backend/assets/capmonster-extension/_locales/ru/messages.json +226 -0
  6. package/dist/backend/assets/capmonster-extension/_metadata/verified_contents.json +1 -0
  7. package/dist/backend/assets/capmonster-extension/background.js +2 -0
  8. package/dist/backend/assets/capmonster-extension/background.js.LICENSE.txt +29 -0
  9. package/dist/backend/assets/capmonster-extension/binanceInterceptor.js +1 -0
  10. package/dist/backend/assets/capmonster-extension/blsInterceptor.js +2 -0
  11. package/dist/backend/assets/capmonster-extension/blsInterceptor.js.LICENSE.txt +29 -0
  12. package/dist/backend/assets/capmonster-extension/content.js +2 -0
  13. package/dist/backend/assets/capmonster-extension/content.js.LICENSE.txt +29 -0
  14. package/dist/backend/assets/capmonster-extension/css/antd.variable.min.css +10 -0
  15. package/dist/backend/assets/capmonster-extension/css/content/solver.css +152 -0
  16. package/dist/backend/assets/capmonster-extension/css/popup/styles.css +110 -0
  17. package/dist/backend/assets/capmonster-extension/defaultSettings.json +62 -0
  18. package/dist/backend/assets/capmonster-extension/devtools/devtools.html +1 -0
  19. package/dist/backend/assets/capmonster-extension/devtools/devtools.js +1 -0
  20. package/dist/backend/assets/capmonster-extension/devtools/panel.html +11 -0
  21. package/dist/backend/assets/capmonster-extension/fonts/roboto/Roboto-Bold.ttf +0 -0
  22. package/dist/backend/assets/capmonster-extension/fonts/roboto/Roboto-Medium.ttf +0 -0
  23. package/dist/backend/assets/capmonster-extension/fonts/roboto/Roboto-Regular.ttf +0 -0
  24. package/dist/backend/assets/capmonster-extension/fonts/ubuntu/Ubuntu-B.ttf +0 -0
  25. package/dist/backend/assets/capmonster-extension/geetestInterceptor.js +1 -0
  26. package/dist/backend/assets/capmonster-extension/hcaptcha.js +2 -0
  27. package/dist/backend/assets/capmonster-extension/hcaptcha.js.LICENSE.txt +29 -0
  28. package/dist/backend/assets/capmonster-extension/hcaptchaInterceptor.js +1 -0
  29. package/dist/backend/assets/capmonster-extension/img/20x20_binance.svg +14 -0
  30. package/dist/backend/assets/capmonster-extension/img/20x20_bls.svg +9 -0
  31. package/dist/backend/assets/capmonster-extension/img/20x20_geetest.svg +6 -0
  32. package/dist/backend/assets/capmonster-extension/img/20x20_hcaptcha.svg +28 -0
  33. package/dist/backend/assets/capmonster-extension/img/20x20_recaptcha.svg +5 -0
  34. package/dist/backend/assets/capmonster-extension/img/20x20_text_captcha.svg +5 -0
  35. package/dist/backend/assets/capmonster-extension/img/20x20_turnstile.svg +11 -0
  36. package/dist/backend/assets/capmonster-extension/img/blue-cogs-animated.gif +0 -0
  37. package/dist/backend/assets/capmonster-extension/img/green-cogs.png +0 -0
  38. package/dist/backend/assets/capmonster-extension/img/icon.png +0 -0
  39. package/dist/backend/assets/capmonster-extension/img/logo.svg +20 -0
  40. package/dist/backend/assets/capmonster-extension/img/logo_icon.png +0 -0
  41. package/dist/backend/assets/capmonster-extension/img/red-cogs.png +0 -0
  42. package/dist/backend/assets/capmonster-extension/img/white-cogs.png +0 -0
  43. package/dist/backend/assets/capmonster-extension/manifest.json +107 -0
  44. package/dist/backend/assets/capmonster-extension/manifest_chrome.json +105 -0
  45. package/dist/backend/assets/capmonster-extension/manifest_firefox.json +120 -0
  46. package/dist/backend/assets/capmonster-extension/pageScript.js +1 -0
  47. package/dist/backend/assets/capmonster-extension/pageScriptHandler.js +2 -0
  48. package/dist/backend/assets/capmonster-extension/pageScriptHandler.js.LICENSE.txt +14 -0
  49. package/dist/backend/assets/capmonster-extension/panel.js +16 -0
  50. package/dist/backend/assets/capmonster-extension/panel.js.LICENSE.txt +90 -0
  51. package/dist/backend/assets/capmonster-extension/polyfills/browser-polyfill.js +1 -0
  52. package/dist/backend/assets/capmonster-extension/popup.html +13 -0
  53. package/dist/backend/assets/capmonster-extension/popup.js +222 -0
  54. package/dist/backend/assets/capmonster-extension/popup.js.LICENSE.txt +90 -0
  55. package/dist/backend/assets/capmonster-extension/recaptcha.js +2 -0
  56. package/dist/backend/assets/capmonster-extension/recaptcha.js.LICENSE.txt +29 -0
  57. package/dist/backend/assets/capmonster-extension/recaptcha2Interceptor.js +1 -0
  58. package/dist/backend/assets/capmonster-extension/recaptcha3Interceptor.js +1 -0
  59. package/dist/backend/assets/capmonster-extension/turnstileInterceptor.js +2 -0
  60. package/dist/backend/assets/capmonster-extension/turnstileInterceptor.js.LICENSE.txt +14 -0
  61. package/dist/backend/assets/stealth-extension/stealth.js +82 -37
  62. package/dist/backend/browser/stealth.js +82 -10
  63. package/dist/backend/lib/i18n-telegram.js +6 -0
  64. package/dist/frontend/assets/index-C89DY7ra.js +144 -0
  65. package/dist/frontend/assets/index-Cwyc7GKp.css +1 -0
  66. package/dist/frontend/index.html +2 -2
  67. package/package.json +1 -1
  68. package/templates/AGENT.onboarding.md +1 -1
  69. package/dist/frontend/assets/index-0VQ9YZWd.js +0 -144
  70. package/dist/frontend/assets/index-Dq3ELXQB.css +0 -1
@@ -169,6 +169,20 @@ function main() {
169
169
  telegramAdapter.appendChunk(job.channelId, chunk.text);
170
170
  }
171
171
  }
172
+ if (chunk.type === 'takeover_requested') {
173
+ const takeoverUrl = chunk.takeoverUrl;
174
+ if (isTelegramJob) {
175
+ telegramAdapter.notifyTakeover(job.channelId, takeoverUrl);
176
+ }
177
+ else {
178
+ // For UI/WebSocket channels inject the URL as a text chunk so it
179
+ // appears inline in the streaming response.
180
+ broadcastToChannel(job.channelId, {
181
+ type: 'chunk',
182
+ chunk: { type: 'text', text: `\n\nšŸ”— **Takeover link:** ${takeoverUrl}` },
183
+ });
184
+ }
185
+ }
172
186
  if (chunk.type === 'tool_call') {
173
187
  const tcString = `${chunk.tool}(${JSON.stringify(chunk.input)})`;
174
188
  toolCallCount++;
@@ -26,6 +26,19 @@ const path_1 = __importDefault(require("path"));
26
26
  const fs_1 = __importDefault(require("fs"));
27
27
  const child_process_1 = require("child_process");
28
28
  const util_1 = require("util");
29
+ const node_html_markdown_1 = require("node-html-markdown");
30
+ const undici_1 = require("undici");
31
+ // Cache ProxyAgent instances by URL so the same connection pool (and exit IP)
32
+ // is reused across all fetch_website calls for the same proxy.
33
+ const proxyAgentCache = new Map();
34
+ function getProxyAgent(proxyUrl) {
35
+ let agent = proxyAgentCache.get(proxyUrl);
36
+ if (!agent) {
37
+ agent = new undici_1.ProxyAgent(proxyUrl);
38
+ proxyAgentCache.set(proxyUrl, agent);
39
+ }
40
+ return agent;
41
+ }
29
42
  const os_1 = require("os");
30
43
  const crypto_1 = require("crypto");
31
44
  const takeover_state_js_1 = require("../takeover-state.js");
@@ -255,6 +268,37 @@ function getLanIp() {
255
268
  }
256
269
  return 'localhost';
257
270
  }
271
+ /**
272
+ * Stable hash of an agent ID string → non-negative integer.
273
+ * Used so proxy assignment is deterministic by agent ID, not config order.
274
+ */
275
+ function hashAgentId(id) {
276
+ let h = 0;
277
+ for (const c of id)
278
+ h = (Math.imul(31, h) + c.charCodeAt(0)) | 0;
279
+ return Math.abs(h);
280
+ }
281
+ /**
282
+ * Resolve the proxy for an agent.
283
+ * Priority: agent.proxy config → GRANCLAW_PROXY_LIST (stable hash by agent ID) → undefined.
284
+ * Hash-based assignment means the same agent always gets the same proxy even if
285
+ * agents.config.json is reordered or new agents are added — so the Chrome daemon
286
+ * always restarts on the same IP.
287
+ * Only applies when GRANCLAW_PROXY_ENABLE=true or agent.proxy is explicitly set.
288
+ */
289
+ function resolveAgentProxy(agentId, configProxy) {
290
+ if (configProxy)
291
+ return configProxy;
292
+ if (process.env.GRANCLAW_PROXY_ENABLE !== 'true')
293
+ return undefined;
294
+ const proxyList = process.env.GRANCLAW_PROXY_LIST;
295
+ if (!proxyList)
296
+ return undefined;
297
+ const proxies = proxyList.split(',').map(p => p.trim()).filter(Boolean);
298
+ if (proxies.length === 0)
299
+ return undefined;
300
+ return proxies[hashAgentId(agentId) % proxies.length];
301
+ }
258
302
  async function runAgent(agent, message, onChunk, options) {
259
303
  const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir);
260
304
  const channelId = options?.channelId ?? 'ui';
@@ -638,6 +682,50 @@ async function runAgent(agent, message, onChunk, options) {
638
682
  //
639
683
  // Privileged commands (record, close, tab close for session 0,
640
684
  // session management) are rejected — the runtime owns those.
685
+ /**
686
+ * After a navigation command, check if a CAPTCHA is blocking the page.
687
+ * Waits up to 30s for the CapMonster extension to auto-solve it.
688
+ * Returns 'clear' (no captcha / solved), 'unsolved' (still blocked after timeout).
689
+ */
690
+ async function waitForCaptchaResolution(bin, sessionId) {
691
+ const CAPTCHA_JS = `(function() {
692
+ var patterns = [
693
+ 'iframe[src*="captcha-delivery"]',
694
+ 'iframe[src*="geo.captcha"]',
695
+ 'iframe[src*="recaptcha"]',
696
+ 'iframe[src*="hcaptcha"]',
697
+ 'iframe[src*="challenges.cloudflare"]',
698
+ '.g-recaptcha',
699
+ '.h-captcha',
700
+ '[class*="captcha"]',
701
+ ];
702
+ return patterns.some(function(s) {
703
+ return !!document.querySelector(s);
704
+ }) ? 'captcha' : 'clear';
705
+ })()`;
706
+ const evalArgv = (id) => ['--session', id, 'eval', CAPTCHA_JS];
707
+ const check = async () => {
708
+ try {
709
+ const { stdout } = await execFileAsync(bin, evalArgv(sessionId), { timeout: 8_000 });
710
+ return stdout.includes('"captcha"') || stdout.includes("'captcha'") || stdout.trim() === 'captcha';
711
+ }
712
+ catch {
713
+ return false;
714
+ }
715
+ };
716
+ // Initial check — short delay to let page settle
717
+ await new Promise(r => setTimeout(r, 1500));
718
+ if (!await check())
719
+ return 'clear';
720
+ // CAPTCHA detected — wait up to 30s for auto-solve
721
+ const deadline = Date.now() + 30_000;
722
+ while (Date.now() < deadline) {
723
+ await new Promise(r => setTimeout(r, 2_000));
724
+ if (!await check())
725
+ return 'clear';
726
+ }
727
+ return 'unsolved';
728
+ }
641
729
  extensionFactories.push((pi) => {
642
730
  const agentBrowserBin = process.env.AGENT_BROWSER_BIN ?? 'agent-browser';
643
731
  const profileDir = path_1.default.join(workspaceDir, '.browser-profile');
@@ -660,12 +748,15 @@ async function runAgent(agent, message, onChunk, options) {
660
748
  'or `session`, they are rejected. Every turn is recorded as a single WebM video visible in the dashboard Browser view.',
661
749
  promptSnippet: 'Control a headless browser — navigate, click, fill, snapshot, extract data',
662
750
  promptGuidelines: [
751
+ 'Use browser for: real-time navigation, login flows, write/post/update operations (LinkedIn, Reddit, social media), pages requiring JS interaction, and multi-step forms.',
752
+ 'Do NOT use browser to just read a webpage — use fetch_website instead (faster, lighter, no screenshot overhead).',
663
753
  'Core loop: open → snapshot → interact → re-snapshot. Refs from snapshot are invalidated by navigation.',
664
754
  'For visual reasoning use `snapshot --annotate -i` (annotated screenshot with numbered refs).',
665
755
  'For data extraction use `snapshot` (plain accessibility tree) or `text --ref <ref>`.',
666
756
  'Do not call `record start`, `record stop`, `close`, or `session` — the runtime manages those.',
667
757
  'You do not need to screenshot for audit — the whole session is recorded as video automatically.',
668
758
  'Saved logins persist automatically when the user has set up a profile via the dashboard Browser view.',
759
+ 'CAPTCHA handling: if a page returns a CAPTCHA, wait — the browser has an automatic solver extension. If it is not resolved after ~30 seconds, use request_human_browser_takeover to let the user solve it.',
669
760
  'Examples: {"command":"open","args":["https://example.com"]}, {"command":"click","args":["--ref","e12"]}, {"command":"fill","args":["--ref","e5","Alice"]}',
670
761
  ],
671
762
  parameters: {
@@ -718,7 +809,11 @@ async function runAgent(agent, message, onChunk, options) {
718
809
  if (fs_1.default.existsSync(profileDir)) {
719
810
  argv.push('--profile', profileDir);
720
811
  }
721
- argv.push(...(0, stealth_js_1.stealthArgv)());
812
+ const stealthOpts = {
813
+ proxy: resolveAgentProxy(agent.id, agent.proxy),
814
+ capmonsterKey: agent.capmonsterKey,
815
+ };
816
+ argv.push(...(0, stealth_js_1.stealthArgv)(stealthOpts));
722
817
  argv.push(command, ...args);
723
818
  try {
724
819
  const { stdout, stderr } = await execFileAsync(agentBrowserBin, argv, {
@@ -728,6 +823,20 @@ async function runAgent(agent, message, onChunk, options) {
728
823
  });
729
824
  (0, session_manager_js_1.appendCommand)(browserState.handle, `${command} ${args.join(' ')}`.trim());
730
825
  const out = stdout.trim() || stderr.trim() || 'ok';
826
+ // After navigation, check for CAPTCHA and wait up to 30s for the
827
+ // CapMonster extension to auto-solve it before returning to the agent.
828
+ if (command === 'open' || command === 'reload') {
829
+ const captchaResult = await waitForCaptchaResolution(agentBrowserBin, agent.id);
830
+ if (captchaResult === 'unsolved') {
831
+ return {
832
+ content: [{
833
+ type: 'text',
834
+ text: out + '\n\nāš ļø CAPTCHA detected and not auto-solved after 30s. ' +
835
+ 'Use request_human_browser_takeover to let the user solve it, or try a different URL.',
836
+ }],
837
+ };
838
+ }
839
+ }
731
840
  return { content: [{ type: 'text', text: out }] };
732
841
  }
733
842
  catch (err) {
@@ -826,6 +935,7 @@ async function runAgent(agent, message, onChunk, options) {
826
935
  'Use for any question about recent events, current prices, live data, or information past your training cutoff.',
827
936
  'Prefer specific, focused queries over vague ones.',
828
937
  'You can call this tool multiple times to refine results.',
938
+ 'After getting results, verify URLs with fetch_website before sharing with the user — confirms the page loads and is not paywalled or broken.',
829
939
  ],
830
940
  parameters: {
831
941
  type: 'object',
@@ -862,6 +972,108 @@ async function runAgent(agent, message, onChunk, options) {
862
972
  });
863
973
  });
864
974
  }
975
+ // fetch_website tool: fetches a URL and returns clean trimmed markdown.
976
+ // Normal mode: plain HTTP GET. Unblocker mode: Bright Data Web Unlocker API.
977
+ // Always registered — no key required for basic usage.
978
+ extensionFactories.push((pi) => {
979
+ const nhm = new node_html_markdown_1.NodeHtmlMarkdown();
980
+ function htmlToMarkdown(html, maxChars = 4000) {
981
+ let md = nhm.translate(html).replace(/\n{3,}/g, '\n\n').trim();
982
+ if (md.length > maxChars) {
983
+ md = md.slice(0, maxChars) + `\n\n[...truncated at ${maxChars} chars]`;
984
+ }
985
+ return md;
986
+ }
987
+ pi.registerTool({
988
+ name: 'fetch_website',
989
+ label: 'Fetch Website',
990
+ description: 'Fetch a webpage and return its content as trimmed markdown. ' +
991
+ 'Use to read web pages, verify URLs from search results, or scrape public content. ' +
992
+ 'Set unblocker=true if the site blocks normal requests (bot-detection, Cloudflare, DataDome). ' +
993
+ 'Prefer this over browser for read-only operations — it is faster and uses less context.',
994
+ promptSnippet: 'Fetch and read a webpage as markdown',
995
+ promptGuidelines: [
996
+ 'Use fetch_website (not browser) when you only need to read a page — faster and no screenshot overhead.',
997
+ 'Use fetch_website to verify URLs from web_search results before sharing with the user.',
998
+ 'Set unblocker=true only after being blocked (captcha/403) on the same domain with unblocker=false.',
999
+ 'Output is truncated at 4000 chars. For interactive or login-gated pages, use browser instead.',
1000
+ ],
1001
+ parameters: {
1002
+ type: 'object',
1003
+ properties: {
1004
+ url: { type: 'string', description: 'The URL to fetch' },
1005
+ unblocker: {
1006
+ type: 'boolean',
1007
+ description: 'Route through Bright Data Web Unlocker to bypass bot protection (default: false)',
1008
+ },
1009
+ },
1010
+ required: ['url'],
1011
+ },
1012
+ async execute(_toolCallId, params) {
1013
+ const useUnblocker = !!params.unblocker;
1014
+ if (useUnblocker) {
1015
+ const unblockerKey = process.env.BRIGHTDATA_UNBLOCKER_KEY;
1016
+ const unblockerEnabled = process.env.BRIGHTDATA_UNBLOCKER_ENABLED !== 'false';
1017
+ if (!unblockerKey || !unblockerEnabled) {
1018
+ return {
1019
+ content: [{
1020
+ type: 'text',
1021
+ text: 'fetch_website: Bright Data unblocker not configured. Set BRIGHTDATA_UNBLOCKER_KEY and BRIGHTDATA_UNBLOCKER_ENABLED=true.',
1022
+ }],
1023
+ };
1024
+ }
1025
+ try {
1026
+ const res = await fetch('https://api.brightdata.com/request', {
1027
+ method: 'POST',
1028
+ headers: {
1029
+ 'Content-Type': 'application/json',
1030
+ 'Authorization': `Bearer ${unblockerKey}`,
1031
+ },
1032
+ body: JSON.stringify({ zone: 'web_unlocker1', url: params.url, format: 'raw' }),
1033
+ signal: AbortSignal.timeout(30_000),
1034
+ });
1035
+ if (!res.ok) {
1036
+ return { content: [{ type: 'text', text: `fetch_website (unblocker): HTTP ${res.status} ${res.statusText}` }] };
1037
+ }
1038
+ const html = await res.text();
1039
+ const md = htmlToMarkdown(html);
1040
+ return { content: [{ type: 'text', text: md }] };
1041
+ }
1042
+ catch (err) {
1043
+ return { content: [{ type: 'text', text: `fetch_website (unblocker) error: ${err instanceof Error ? err.message : String(err)}` }] };
1044
+ }
1045
+ }
1046
+ // Plain HTTP fetch — route through proxy if configured
1047
+ try {
1048
+ const agentProxy = resolveAgentProxy(agent.id, agent.proxy);
1049
+ const fetchOpts = {
1050
+ headers: {
1051
+ 'User-Agent': 'Mozilla/5.0 (compatible; GranClaw/1.0; +https://granclaw.com)',
1052
+ 'Accept': 'text/html,application/xhtml+xml,*/*;q=0.9',
1053
+ },
1054
+ signal: AbortSignal.timeout(15_000),
1055
+ redirect: 'follow',
1056
+ ...(agentProxy ? { dispatcher: getProxyAgent(agentProxy) } : {}),
1057
+ };
1058
+ const res = await (0, undici_1.fetch)(params.url, fetchOpts);
1059
+ if (!res.ok) {
1060
+ return { content: [{ type: 'text', text: `fetch_website: HTTP ${res.status} ${res.statusText}. Try unblocker=true if blocked.` }] };
1061
+ }
1062
+ const contentType = res.headers.get('content-type') ?? '';
1063
+ const body = await res.text();
1064
+ if (!contentType.includes('html')) {
1065
+ const out = body.length > 4000 ? body.slice(0, 4000) + '\n\n[...truncated]' : body;
1066
+ return { content: [{ type: 'text', text: out }] };
1067
+ }
1068
+ const md = htmlToMarkdown(body);
1069
+ return { content: [{ type: 'text', text: md }] };
1070
+ }
1071
+ catch (err) {
1072
+ return { content: [{ type: 'text', text: `fetch_website error: ${err instanceof Error ? err.message : String(err)}` }] };
1073
+ }
1074
+ },
1075
+ });
1076
+ });
865
1077
  // Build resource loader. Must call reload() before passing to createAgentSession —
866
1078
  // when the sdk receives a pre-built resourceLoader it skips reload().
867
1079
  const resourceLoader = new DefaultResourceLoader({
@@ -22,8 +22,12 @@
22
22
  * stay under Telegram's per-chat rate limit. Periodic typing indicators are
23
23
  * re-sent every 4 s while the job is running.
24
24
  */
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
25
28
  Object.defineProperty(exports, "__esModule", { value: true });
26
29
  exports.TelegramAdapter = void 0;
30
+ const telegramify_markdown_1 = __importDefault(require("telegramify-markdown"));
27
31
  const telegram_http_client_js_1 = require("./telegram-http-client.js");
28
32
  const agent_db_js_1 = require("../agent-db.js");
29
33
  const i18n_telegram_js_1 = require("../lib/i18n-telegram.js");
@@ -181,6 +185,17 @@ class TelegramAdapter {
181
185
  return;
182
186
  state.responseBuffer += text;
183
187
  }
188
+ /** Called from process.ts when the agent emits a takeover_requested chunk. */
189
+ notifyTakeover(channelId, takeoverUrl) {
190
+ const state = this.chats.get(channelId);
191
+ if (!state)
192
+ return;
193
+ this.bot
194
+ .sendMessage(state.chatId, `Takeover link: ${takeoverUrl}`)
195
+ .catch((err) => {
196
+ console.warn(`[agent:${this.agentId}] Telegram takeover notify failed:`, err.message);
197
+ });
198
+ }
184
199
  // ── Finalize + flush ─────────────────────────────────────────────────────
185
200
  /**
186
201
  * Edit the live status message one last time with a "Done" footer, then
@@ -235,15 +250,30 @@ class TelegramAdapter {
235
250
  }
236
251
  async sendReply(chatId, text) {
237
252
  const MAX = 4000;
238
- if (text.length <= MAX) {
239
- await this.bot.sendMessage(chatId, text, { parse_mode: 'Markdown' }).catch(() => this.bot.sendMessage(chatId, text));
253
+ // Convert standard markdown → Telegram MarkdownV2. Fall back to plain
254
+ // text if conversion throws (e.g. deeply malformed LLM output).
255
+ let body;
256
+ let parseMode;
257
+ try {
258
+ body = (0, telegramify_markdown_1.default)(text, 'escape');
259
+ parseMode = 'MarkdownV2';
260
+ }
261
+ catch {
262
+ body = text;
263
+ parseMode = undefined;
264
+ }
265
+ const send = (chunk) => this.bot
266
+ .sendMessage(chatId, chunk, parseMode ? { parse_mode: parseMode } : undefined)
267
+ .catch(() => this.bot.sendMessage(chatId, chunk, {})); // plain-text fallback
268
+ if (body.length <= MAX) {
269
+ await send(body);
240
270
  return;
241
271
  }
242
- let remaining = text;
272
+ let remaining = body;
243
273
  while (remaining.length > 0) {
244
- const chunk = remaining.slice(0, MAX);
245
- const splitAt = chunk.lastIndexOf('\n') > MAX / 2 ? chunk.lastIndexOf('\n') : MAX;
246
- await this.bot.sendMessage(chatId, remaining.slice(0, splitAt)).catch(() => { });
274
+ const slice = remaining.slice(0, MAX);
275
+ const splitAt = slice.lastIndexOf('\n') > MAX / 2 ? slice.lastIndexOf('\n') : MAX;
276
+ await send(remaining.slice(0, splitAt));
247
277
  remaining = remaining.slice(splitAt).trimStart();
248
278
  }
249
279
  }
@@ -0,0 +1,226 @@
1
+ {
2
+ "extName": {
3
+ "message": "CapMonster Cloud — automated captcha solver",
4
+ "description": "The title of the application, displayed in the web store."
5
+ },
6
+ "extDesc": {
7
+ "message": "Online service for automated captcha solving",
8
+ "description": "The description of the application, displayed in the web store."
9
+ },
10
+ "extNameFirefox": {
11
+ "message": "CapMonster Cloud — automated captcha solver",
12
+ "description": "The title of the application, displayed in the web store."
13
+ },
14
+ "extDescFirefox": {
15
+ "message": "Online service for automated captcha solving",
16
+ "description": "The description of the application, displayed in the web store."
17
+ },
18
+ "extShortName": {
19
+ "message": "CapMonster Cloud",
20
+ "description": ""
21
+ },
22
+ "on": {
23
+ "message": "On",
24
+ "description": ""
25
+ },
26
+ "off": {
27
+ "message": "Off",
28
+ "description": ""
29
+ },
30
+ "support": {
31
+ "message": "Support",
32
+ "description": ""
33
+ },
34
+ "apiKeyPlaceholder": {
35
+ "message": "Enter API key",
36
+ "description": ""
37
+ },
38
+ "getKey": {
39
+ "message": "Get a key",
40
+ "description": ""
41
+ },
42
+ "balance": {
43
+ "message": "Balance",
44
+ "description": ""
45
+ },
46
+ "topUp": {
47
+ "message": "Add funds",
48
+ "description": ""
49
+ },
50
+ "wrongKey": {
51
+ "message": "Wrong key",
52
+ "description": ""
53
+ },
54
+ "emptyKey": {
55
+ "message": "Empty key",
56
+ "description": ""
57
+ },
58
+ "automaticCaptchaSolving": {
59
+ "message": "Automated captcha solving",
60
+ "description": ""
61
+ },
62
+ "settings": {
63
+ "message": "Settings",
64
+ "description": ""
65
+ },
66
+ "repeatSolveAttempts": {
67
+ "message": "Repeat captcha solving in case of an error, attempts",
68
+ "description": ""
69
+ },
70
+ "proxy": {
71
+ "message": "Proxy",
72
+ "description": ""
73
+ },
74
+ "proxyType": {
75
+ "message": "Proxy type",
76
+ "description": ""
77
+ },
78
+ "port": {
79
+ "message": "Port",
80
+ "description": ""
81
+ },
82
+ "login": {
83
+ "message": "Login",
84
+ "description": ""
85
+ },
86
+ "password": {
87
+ "message": "Password",
88
+ "description": ""
89
+ },
90
+ "blacklistControl": {
91
+ "message": "Blacklist control",
92
+ "description": ""
93
+ },
94
+ "blacklistDescription": {
95
+ "message": "Captcha solving on added websites will be disabled",
96
+ "description": ""
97
+ },
98
+ "add": {
99
+ "message": "Add",
100
+ "description": ""
101
+ },
102
+ "manualRecognition": {
103
+ "message": "Manual recognition",
104
+ "description": ""
105
+ },
106
+ "textCaptcha": {
107
+ "message": "Text captcha",
108
+ "description": ""
109
+ },
110
+ "textCaptchaHelpLink": {
111
+ "message": "https://docs.capmonster.cloud/docs/captchas/image-to-text",
112
+ "description": ""
113
+ },
114
+ "markImageAsCaptcha": {
115
+ "message": "Mark image as captcha",
116
+ "description": ""
117
+ },
118
+ "selectForSubmit": {
119
+ "message": "Select element for submit",
120
+ "description": ""
121
+ },
122
+ "selectInputForCaptchaResult": {
123
+ "message": "Select an input for the captcha result",
124
+ "description": ""
125
+ },
126
+ "help": {
127
+ "message": "Help",
128
+ "description": ""
129
+ },
130
+ "rcInfoText": {
131
+ "message": "Only click",
132
+ "description": ""
133
+ },
134
+ "close_button": {
135
+ "message": "Close",
136
+ "description": ""
137
+ },
138
+ "delayStartCount": {
139
+ "message": "Delay between start solve captcha clicks",
140
+ "description": ""
141
+ },
142
+ "textCaptchaSaveOnSite": {
143
+ "message": "Save selected captcha position for sites",
144
+ "description": ""
145
+ },
146
+ "delayAfterLoadPage": {
147
+ "message": "Delay before start solve saved captcha",
148
+ "description": ""
149
+ },
150
+ "autoClick": {
151
+ "message": "Auto click",
152
+ "description": ""
153
+ },
154
+ "autoSolve": {
155
+ "message": "Auto solve",
156
+ "description": ""
157
+ },
158
+ "globalVariableSetting": {
159
+ "message": "Global variable",
160
+ "description": ""
161
+ },
162
+ "manualRecognitionDescription": {
163
+ "message": "This feature allows starting captcha solving using the \"Token\" method not automatically but by clicking the \"Solve\" button.",
164
+ "description": ""
165
+ },
166
+ "textCaptchaModule": {
167
+ "message": "Text module",
168
+ "description": ""
169
+ },
170
+ "enterModuleName": {
171
+ "message": "Enter text module",
172
+ "description": ""
173
+ },
174
+ "addModuleText": {
175
+ "message": "Add module",
176
+ "description": ""
177
+ },
178
+ "autoModule": {
179
+ "message": "Auto",
180
+ "description": ""
181
+ },
182
+ "delayBetweenClickValue": {
183
+ "message": "Delay between click (ms)",
184
+ "description": ""
185
+ },
186
+ "delayBetweenClickEnabled": {
187
+ "message": "Delay between click",
188
+ "description": ""
189
+ },
190
+ "apiKeyTitle": {
191
+ "message": "API-key",
192
+ "description": ""
193
+ },
194
+ "globalObjectDescription": {
195
+ "message": "Name of the field for interaction with the extension via a global object. More information about the extension settings can be found",
196
+ "description": ""
197
+ },
198
+ "globalObjectDescriptionLink": {
199
+ "message": "in the article",
200
+ "description": ""
201
+ },
202
+ "autoSubmitText": {
203
+ "message": "Element for auto submit",
204
+ "description": ""
205
+ },
206
+ "removeAutoSubmit": {
207
+ "message": "Remove",
208
+ "description": ""
209
+ },
210
+ "emptyDetectedCaptchaMessage": {
211
+ "message": "CAPTCHA may require activation - simply refreshing the page might not be enough. Please complete the CAPTCHA manually to load all necessary parameters.",
212
+ "description": ""
213
+ },
214
+ "detectedCaptchas": {
215
+ "message": "Captchas detected",
216
+ "description": ""
217
+ },
218
+ "copySuccessMessage": {
219
+ "message": "Copied to clipboard!",
220
+ "description": ""
221
+ },
222
+ "copyFailedMessage": {
223
+ "message": "Failed to copy",
224
+ "description": ""
225
+ }
226
+ }