claude-telegram-bot 0.3.19 β†’ 0.3.23

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 (2) hide show
  1. package/bot.mjs +92 -2
  2. package/package.json +1 -1
package/bot.mjs CHANGED
@@ -149,6 +149,8 @@ const STR = {
149
149
  compactOk: "πŸ—œοΈ Context compacted. The conversation continues with a summary.",
150
150
  compactFail: (m) => `⚠️ Compact failed: ${m}`,
151
151
  compactNoSession: "No active session to compact. Just send a message to start one.",
152
+ testFallbackDisabled: "⚠️ Ollama fallback is not enabled. Set `\"ollamaFallback\": true` in config.json.",
153
+ testFallbackFail: (m) => `⚠️ Ollama test failed: ${m}`,
152
154
  busy: "⏳ A previous task is still running. Please try again when it finishes.",
153
155
  queued: (n) => `⏳ Queued (#${n}). Will run when the current task finishes.`,
154
156
  stopOk: "πŸ›‘ Task stopped.",
@@ -287,6 +289,8 @@ const STR = {
287
289
  compactFail: (m) => `⚠️ compact μ‹€νŒ¨: ${m}`,
288
290
  compactNoSession: "μ••μΆ•ν•  ν™œμ„± μ„Έμ…˜μ΄ μ—†μŠ΅λ‹ˆλ‹€. λ©”μ‹œμ§€λ₯Ό 보내 μ„Έμ…˜μ„ μ‹œμž‘ν•˜μ„Έμš”.",
289
291
  contextTooLong: "⚠️ ν”„λ‘¬ν”„νŠΈκ°€ λ„ˆλ¬΄ κΉλ‹ˆλ‹€. `/compact` 둜 μ»¨ν…μŠ€νŠΈλ₯Ό μ••μΆ•ν•˜κ±°λ‚˜ `/new` 둜 μƒˆ μ„Έμ…˜μ„ μ‹œμž‘ν•˜μ„Έμš”.",
292
+ testFallbackDisabled: "⚠️ Ollama 폴백이 λΉ„ν™œμ„±ν™” μƒνƒœμž…λ‹ˆλ‹€. config.json에 `\"ollamaFallback\": true` λ₯Ό μΆ”κ°€ν•˜μ„Έμš”.",
293
+ testFallbackFail: (m) => `⚠️ Ollama ν…ŒμŠ€νŠΈ μ‹€νŒ¨: ${m}`,
290
294
  },
291
295
  };
292
296
  const t = (l, key, ...a) => {
@@ -536,6 +540,14 @@ function parseResetTime(raw) {
536
540
  return null;
537
541
  }
538
542
 
543
+ function isFallbackError(raw, code) {
544
+ const t = (raw || "").toLowerCase();
545
+ return t.includes("credit") || t.includes("balance") || t.includes("billing") || t.includes("payment")
546
+ || t.includes("rate_limit") || t.includes("rate limit") || t.includes("too many requests") || code === 429
547
+ || t.includes("usage limit") || t.includes("monthly limit")
548
+ || t.includes("overloaded") || code === 529;
549
+ }
550
+
539
551
  function classifyClaudeError(raw, code) {
540
552
  const t = raw.toLowerCase();
541
553
  if (t.includes("credit") || t.includes("balance") || t.includes("billing") || t.includes("payment"))
@@ -603,15 +615,34 @@ function runClaude(prompt, sessionId, opts = {}) {
603
615
  const rawErr = j.result ?? "";
604
616
  const text = j.is_error ? classifyClaudeError(rawErr, code) : (rawErr || "(empty response)");
605
617
  const resetAt = j.is_error ? parseResetTime(rawErr) : null;
606
- resolve({ ok: !j.is_error, text, sessionId: j.session_id, cost: j.total_cost_usd, resetAt });
618
+ const canFallback = j.is_error && isFallbackError(rawErr, code);
619
+ resolve({ ok: !j.is_error, text, sessionId: j.session_id, cost: j.total_cost_usd, resetAt, canFallback });
607
620
  } catch {
608
621
  const raw = (err || out || "no output").slice(0, 3500);
609
- resolve({ ok: false, text: classifyClaudeError(raw, code), resetAt: parseResetTime(raw) });
622
+ resolve({ ok: false, text: classifyClaudeError(raw, code), resetAt: parseResetTime(raw), canFallback: isFallbackError(raw, code) });
610
623
  }
611
624
  });
612
625
  });
613
626
  }
614
627
 
628
+ // ── Ollama 폴백 μ‹€ν–‰ ──────────────────────────────────────────────────────
629
+ async function runOllama(prompt, lang = "en") {
630
+ const header = lang === "ko"
631
+ ? "πŸŒ™ Claudeκ°€ μž μ‹œ 쉬고 μžˆμ–΄μš”. μ œκ°€ λŒ€μ‹  λ„μ™€λ“œλ¦΄κ²Œμš”. (μ„Έμ…˜μ€ 이어지지 μ•Šμ•„μš”)\n\n"
632
+ : "πŸŒ™ Claude is resting right now. I'll help in the meantime. (Session won't continue)\n\n";
633
+ const model = cfg.ollamaModel || "phi3:mini";
634
+ const r = await fetch("http://localhost:11434/api/generate", {
635
+ method: "POST",
636
+ headers: { "Content-Type": "application/json" },
637
+ body: JSON.stringify({ model, prompt, stream: false }),
638
+ signal: AbortSignal.timeout(60_000),
639
+ });
640
+ if (!r.ok) return { ok: false, text: `Ollama HTTP ${r.status}` };
641
+ const j = await r.json();
642
+ const text = (j.response || "").trim();
643
+ return text ? { ok: true, text: header + text } : { ok: false, text: "no response" };
644
+ }
645
+
615
646
  // ── 크둠 μŠ€μΌ€μ€„λŸ¬ ─────────────────────────────────────────────────────────
616
647
  // ν‘œμ€€ cron 5ν•„λ“œ "λΆ„ μ‹œ 일 μ›” μš”μΌ"을 μ˜μ‘΄μ„± 0 μœ μ§€λ₯Ό μœ„ν•΄ μ΅œμ†Œ νŒŒμ„œλ‘œ 직접 κ΅¬ν˜„.
617
648
  // 지원: * Β· λͺ©λ‘(1,3,5) Β· λ²”μœ„(1-5) Β· μŠ€ν…(*/15, 9-17/2). μš”μΌ 0Β·7 = μΌμš”μΌ.
@@ -846,6 +877,44 @@ async function downloadAttachment(att) {
846
877
  return { dest, name };
847
878
  }
848
879
 
880
+ // ── ν…”λ ˆκ·Έλž¨ λ©”μ‹œμ§€ 메타데이터 μΆ”μΆœ ──────────────────────────────────────
881
+ function buildMsgMeta(msg) {
882
+ const parts = [];
883
+
884
+ // ν¬μ›Œλ“œ 좜처 (μ‹ κ·œ API: forward_origin, ꡬ버전 폴백: forward_from / forward_from_chat)
885
+ const fo = msg.forward_origin;
886
+ if (fo) {
887
+ if (fo.type === "user" && fo.sender_user)
888
+ parts.push(`[Forwarded from: ${fo.sender_user.first_name}${fo.sender_user.username ? ` (@${fo.sender_user.username})` : ""}]`);
889
+ else if (fo.type === "channel" && fo.chat)
890
+ parts.push(`[Forwarded from channel: ${fo.chat.title}${fo.chat.username ? ` (@${fo.chat.username})` : ""}]`);
891
+ else if (fo.type === "chat" && fo.sender_chat)
892
+ parts.push(`[Forwarded from: ${fo.sender_chat.title}]`);
893
+ else if (fo.type === "hidden_user" && fo.sender_user_name)
894
+ parts.push(`[Forwarded from: ${fo.sender_user_name} (hidden)]`);
895
+ else
896
+ parts.push(`[Forwarded message]`);
897
+ } else if (msg.forward_from) {
898
+ const u = msg.forward_from;
899
+ parts.push(`[Forwarded from: ${u.first_name}${u.username ? ` (@${u.username})` : ""}]`);
900
+ } else if (msg.forward_from_chat) {
901
+ const c = msg.forward_from_chat;
902
+ parts.push(`[Forwarded from: ${c.title}${c.username ? ` (@${c.username})` : ""}]`);
903
+ }
904
+
905
+ // λ¦¬ν”ŒλΌμ΄ μ»¨ν…μŠ€νŠΈ
906
+ const reply = msg.reply_to_message;
907
+ if (reply) {
908
+ const replyText = (reply.text || reply.caption || "").trim().slice(0, 300);
909
+ const replyFrom = reply.from?.first_name || "unknown";
910
+ parts.push(replyText
911
+ ? `[Replying to ${replyFrom}: "${replyText}${replyText.length >= 300 ? "…" : ""}"]`
912
+ : `[Replying to ${replyFrom}'s message]`);
913
+ }
914
+
915
+ return parts.join("\n");
916
+ }
917
+
849
918
  // ── λ©”μ‹œμ§€ 처리 ───────────────────────────────────────────────────────────
850
919
  let busy = false;
851
920
  const msgQueue = []; // { msg, receivedAt } β€” busy 쀑 μˆ˜μ‹  λ©”μ‹œμ§€ λŒ€κΈ°μ—΄
@@ -958,6 +1027,18 @@ async function handle(msg) {
958
1027
  }
959
1028
  return;
960
1029
  }
1030
+ if (text === "/testfallback") {
1031
+ if (!cfg.ollamaFallback) { await send(chatId, t(l, "testFallbackDisabled")); return; }
1032
+ await send(chatId, "πŸ§ͺ Ollama μ—°κ²° ν…ŒμŠ€νŠΈ 쀑…");
1033
+ try {
1034
+ const res = await runOllama("Reply with exactly one sentence: Ollama fallback is working.", l);
1035
+ if (res.ok) await send(chatId, res.text);
1036
+ else await send(chatId, t(l, "testFallbackFail", res.text));
1037
+ } catch (e) {
1038
+ await send(chatId, t(l, "testFallbackFail", e.message));
1039
+ }
1040
+ return;
1041
+ }
961
1042
  if (text === "/new") {
962
1043
  state.sessionId = undefined;
963
1044
  saveState(state);
@@ -1069,6 +1150,8 @@ async function handle(msg) {
1069
1150
  await send(chatId, t(l, "attachFail", e.message));
1070
1151
  }
1071
1152
  }
1153
+ const meta = buildMsgMeta(msg);
1154
+ if (meta) prompt = prompt ? `${meta}\n\n${prompt}` : meta;
1072
1155
  prevSessionId = state.sessionId; // /stop --reset 볡원 λŒ€μƒ μ €μž₯
1073
1156
  const res = await runClaude(prompt, state.sessionId, { modelHint: true, trackChild: true, injectMemory: true });
1074
1157
  if (res.sessionId) {
@@ -1080,6 +1163,13 @@ async function handle(msg) {
1080
1163
  // 레이트 리밋이고 리셋 μ‹œκ°„μ„ μ•Œλ©΄ /reserve 힌트 μΆ”κ°€
1081
1164
  const hint = res.resetAt ? t(l, "reserveHint") : "";
1082
1165
  rateLimitState = res.resetAt ? { prompt, resetAt: res.resetAt } : null;
1166
+ // Ollama 폴백: λ ˆμ΄νŠΈλ¦¬λ°‹Β·ν¬λ ˆλ”§ μ—λŸ¬μ΄κ³  ollamaFallback 켜져 있으면 Ollama둜 μž¬μ‹œλ„
1167
+ if (cfg.ollamaFallback && res.canFallback && !stopping) {
1168
+ try {
1169
+ const oRes = await runOllama(prompt, l);
1170
+ if (oRes.ok) { await send(chatId, oRes.text); return; }
1171
+ } catch {}
1172
+ }
1083
1173
  const errMsg = res.text === "contextTooLong" ? t(l, "contextTooLong") : `⚠️ ${res.text}${hint}`;
1084
1174
  if (!stopping) await send(chatId, errMsg);
1085
1175
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-telegram-bot",
3
- "version": "0.3.19",
3
+ "version": "0.3.23",
4
4
  "description": "Drive Claude Code from Telegram β€” messages run headless `claude -p` in a project dir and replies come back to the chat. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {