autoreg-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
110
skills/telegram-automation/references/lead-funnel-echogate.md
Normal file
110
skills/telegram-automation/references/lead-funnel-echogate.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# AI Lead Funnel — Echogate LLM + Telethon
|
||||
|
||||
Full runnable pattern for Telegram accounts that auto-respond to DMs using LLM.
|
||||
|
||||
## When to use
|
||||
|
||||
- User wants a Telegram account to auto-respond to incoming messages
|
||||
- Redirect leads to a target contact (@lordrax, etc.)
|
||||
- Need a human-like operator that filters and redirects
|
||||
|
||||
## Key design decisions
|
||||
|
||||
1. **Stage-gated dialog, not free-form LLM** — LLM is too eager to share contact immediately or too chatty. Use 3 hardcoded stages with separate system prompts.
|
||||
2. **Processing lock per user** — `asyncio.Event` or `set()` to prevent parallel handler invocations.
|
||||
3. **Typing indicator before every message** — `SetTypingRequest(SendMessageTypingAction())` + `asyncio.sleep(3-10s)`.
|
||||
4. **State persistence** — JSON file with `stage`, `history`, `done` per user. Survives restarts.
|
||||
|
||||
## LLM config
|
||||
|
||||
```python
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
llm = AsyncOpenAI(
|
||||
base_url="https://api.echogate.one/v1",
|
||||
api_key="[REDACTED]",
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
# gpt-5.5: ~3s response — fast enough for real-time
|
||||
# deepseek-v4-pro: ~21s — too slow, breaks the illusion
|
||||
```
|
||||
|
||||
## Stage prompts
|
||||
|
||||
```python
|
||||
STAGE_PROMPTS = {
|
||||
0: """Ты — оператор. Строгий, деловой, немногословный.
|
||||
Это ПЕРВОЕ сообщение в диалоге. Ответь коротко (1 предложение).
|
||||
Поприветствуй и спроси что интересует / по какому вопросу.
|
||||
НЕ давай контакты. НЕ упоминай @target. НЕ давай ссылки.
|
||||
НЕ используй эмодзи. Стиль: спокойный, уверенный, без лишней вежливости.""",
|
||||
|
||||
1: """Ты — оператор. Строгий, деловой.
|
||||
Это ВТОРОЕ сообщение. Ответь на вопрос пользователя коротко (1-2 предложения).
|
||||
Упомяни что детали, цены и наличие — у партнёра @target.
|
||||
НЕ давай прямую ссылку пока. НЕ используй эмодзи.
|
||||
Стиль: уверенный, деловой. Не извиняйся, не будь подобострастным.""",
|
||||
|
||||
2: """Ты — оператор. Строгий, деловой.
|
||||
Это ТРЕТЬЕ (финальное) сообщение. Дай прямую ссылку.
|
||||
Скажи обратиться туда, это всё. Закругляйся.
|
||||
1 предложение. НЕ продолжай диалог. НЕ используй эмодзи.""",
|
||||
}
|
||||
```
|
||||
|
||||
## Processing lock pattern
|
||||
|
||||
```python
|
||||
processing = set() # Track user_ids currently being processed
|
||||
|
||||
@client.on(events.NewMessage(incoming=True))
|
||||
async def handler(event):
|
||||
uk = str(event.sender_id)
|
||||
|
||||
if uk in processing:
|
||||
log(f"Already processing {uk}, skip")
|
||||
return
|
||||
|
||||
processing.add(uk)
|
||||
try:
|
||||
# ... generate and send reply ...
|
||||
finally:
|
||||
processing.discard(uk)
|
||||
```
|
||||
|
||||
## Race condition: why it happens
|
||||
|
||||
When two messages arrive within 2 seconds (e.g. user sends empty message then "привет"), the Telethon event handler fires twice in parallel. Both invocations see the same state (stage=0, our_count=0), both generate Stage 0 replies, and both send simultaneously. Result: two greeting messages back-to-back, no dialog.
|
||||
|
||||
The processing lock prevents this — the second invocation sees `uk in processing` and skips.
|
||||
|
||||
## Fallback messages
|
||||
|
||||
Always have hardcoded fallbacks per stage in case LLM times out:
|
||||
|
||||
```python
|
||||
FALLBACKS = {
|
||||
0: "Добрый. По какому вопросу?",
|
||||
1: "По наличию и ценам — обратись к @target",
|
||||
2: "Напиши @target: https://t.me/target",
|
||||
}
|
||||
```
|
||||
|
||||
## Full runnable script
|
||||
|
||||
See `C:\Users\User\Desktop\_PROJECTS\рассылка\lead_funnel.py` for the production version with:
|
||||
- echogate.one LLM integration
|
||||
- 3-stage dialog with processing lock
|
||||
- Typing indicator + random delays
|
||||
- JSON state persistence
|
||||
- @Tomitotototo → @lordrax funnel
|
||||
|
||||
## Pitfalls learned 2026-08-11
|
||||
|
||||
1. **JSON account metadata is stale**: `sessions/*.json` files say `status: "Active"`, `spamblock: "none"` but 90%+ of accounts are actually RESTRICTED/BANNED. Always verify by connecting and sending a test message to "me".
|
||||
2. **SpamBot "RESTRICTED" ≠ can't DM**: Account may be RESTRICTED for groups but perfectly fine for private messages. The real test is `client.send_message("me", "test")`.
|
||||
3. **Free-form LLM fails**: Without stage-gating, LLM either shares contact immediately (no dialog) or never shares it (endless chat). Hardcode stages.
|
||||
4. **No processing lock = duplicate messages**: Parallel handler invocations on rapid-fire messages produce duplicate greetings.
|
||||
5. **deepseek-v4-pro is 21s**: Too slow for real-time. gpt-5.5 is 3s. Pick fast models.
|
||||
6. **No skip phrases in lead funnel**: Respond to EVERYTHING — "ку", "привет", empty messages. No intent detection needed.
|
||||
@@ -0,0 +1,143 @@
|
||||
# MCP Fallback: Direct Telethon When `[REDACTED-NAME]` MCP Is Down
|
||||
|
||||
## Problem
|
||||
|
||||
The `[REDACTED-NAME]` MCP returns:
|
||||
```
|
||||
ERROR: Telethon not available or client not connected
|
||||
```
|
||||
|
||||
This happens when the Telethon client the MCP server wraps isn't authenticated or the MCP process itself is down. All MCP tools (`reform_messages`, `reform_dialogs`, `reform_me`, `reform_resolve`, `reform_search`) fail with the same error.
|
||||
|
||||
## Direct Telethon Bypass
|
||||
|
||||
The MCP server wraps a local SQLite session file. You can bypass the MCP entirely by connecting directly with the same session:
|
||||
|
||||
```python
|
||||
from telethon import TelegramClient
|
||||
from telethon.sessions import SQLiteSession
|
||||
|
||||
SESSION_PATH = r"C:\Users\User\Desktop\_PROJECTS\tg_content_agent\MAINTG1_fixed.session"
|
||||
API_ID = 2040
|
||||
API_HASH = "[REDACTED-KEY]"
|
||||
|
||||
async def main():
|
||||
client = TelegramClient(SQLiteSession(SESSION_PATH), API_ID, API_HASH)
|
||||
try:
|
||||
await asyncio.wait_for(client.start(), timeout=30)
|
||||
me = await client.get_me()
|
||||
print(f"Connected: @{me.username} (id={me.id})")
|
||||
|
||||
# Get dialogs (same as reform_dialogs)
|
||||
dialogs = await client.get_dialogs(limit=50)
|
||||
|
||||
# Get messages from a chat (same as reform_messages)
|
||||
msgs = await client.get_messages(chat_id, limit=30)
|
||||
|
||||
# Resolve username (same as reform_resolve)
|
||||
entity = await client.get_entity(username)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
print("ERROR: Connection timed out — session may be expired")
|
||||
finally:
|
||||
await client.disconnect()
|
||||
```
|
||||
|
||||
## Session Details
|
||||
|
||||
- **Session file**: `MAINTG1_fixed.session` (~630 KB, active)
|
||||
- **Account**: @[REDACTED-NAME] (id=[REDACTED-TGID])
|
||||
- **API credentials**: id=2040, hash `[REDACTED-KEY]`
|
||||
- **Auth flow**: Session is pre-authenticated — no phone/OTP needed
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **Session can expire** — if `client.start()` times out (30s+), the auth keys may be stale. Requires re-authentication via phone code.
|
||||
- **Write_file restrictions** — `skill_manage(write_file)` can write reference/template/script files into existing skills without file-system permission issues. Use this instead of `write_file` for session-sensitive scripts.
|
||||
- **No MCP tools available** — direct Telethon doesn't provide the convenience wrappers (`reform_search`, etc.). You have to implement search manually via `client.get_messages()` iteration.
|
||||
- **Timeouts** — `TelegramClient.start()` with a large session file can take 10-20s. Set `asyncio.wait_for(client.start(), timeout=30)`.
|
||||
- **Race with MCP** — if both direct client and MCP connect simultaneously with the same session file, one will get `Database is locked`. Don't overlap.
|
||||
|
||||
## Typical Cron Job Adaptation
|
||||
|
||||
When the cron job's MCP dependency is down, the full flow is:
|
||||
|
||||
1. **Detect** — call MCP tool, catch error
|
||||
2. **Fall back** — connect via direct `TelegramClient(SQLiteSession(...))`
|
||||
3. **Scan** — iterate dialogs, get messages per chat
|
||||
4. **Classify** — regex-based AI intel categorization (see below)
|
||||
5. **Dedup** — hash message text, compare with state file
|
||||
6. **Report** — format digest, deliver via cron output contract
|
||||
|
||||
## AI Intel Classification Pattern
|
||||
|
||||
For parsing Telegram messages for AI intelligence:
|
||||
|
||||
```python
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
CATEGORIES = {
|
||||
"api_keys": r"sk-[a-zA-Z0-9]{20,}|Bearer\s+[a-zA-Z0-9_\-\.]{30,}|api.?key[:\s][a-zA-Z0-9]{16,}",
|
||||
"jailbreak": r"jailbreak|bypass|prompt inj|uncensored|g0dm0d3|refusal",
|
||||
"ai_abuse": r"free api|free key|0元|unlimited|turbo charge",
|
||||
"ai_news": r"gpt-?5|claude|opus|sonnet|deepseek|grok|qwen|gemini|release|launch|new model",
|
||||
"traffic": r"traffic|seo|seeding|guerrilla|growth hack|virality",
|
||||
"funnels": r"funnel|lead gen|conversion|cpa|cpl|autofunnel|landing",
|
||||
"code_tips": r"claude code|codex|opencode|cursor|mcp|agent skill|workflow|template",
|
||||
}
|
||||
|
||||
def classify(text: str) -> dict:
|
||||
"""Score message text against all AI intel categories."""
|
||||
scores = defaultdict(float)
|
||||
textl = text.lower()
|
||||
for cat, pattern in CATEGORIES.items():
|
||||
matches = re.findall(pattern, textl, re.IGNORECASE)
|
||||
if matches:
|
||||
scores[cat] += len(matches) * 2
|
||||
|
||||
# Semantic boosters for weak signals
|
||||
if any(k in textl for k in ["api key", "token", "secret", "sk-", "bearer"]):
|
||||
scores["api_keys"] += 5
|
||||
if any(k in textl for k in ["claude code", "codex", "cursor", "opencode", "mcp server"]):
|
||||
scores["code_tips"] += 3
|
||||
if any(k in textl for k in ["free", "trial", "credit", "generat"]):
|
||||
scores["ai_abuse"] += 2
|
||||
if any(k in textl for k in ["release", "announce", "new model", "just launched"]):
|
||||
scores["ai_news"] += 2
|
||||
|
||||
return dict(scores)
|
||||
```
|
||||
|
||||
## Dedup via State File
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
STATE_PATH = "~/tg_intel_state.json"
|
||||
|
||||
def load_state() -> dict:
|
||||
try:
|
||||
with open(STATE_PATH) as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
return {"last_scan": None, "seen_hashes": []}
|
||||
|
||||
def hash_msg(text: str) -> str:
|
||||
return str(hash(text.strip()[:200]))
|
||||
|
||||
def update_state(state: dict, new_items: list) -> dict:
|
||||
seen = set(state.get("seen_hashes", []))
|
||||
for item in new_items:
|
||||
if item["hash"] not in seen:
|
||||
seen.add(item["hash"])
|
||||
state["seen_hashes"] = list(seen)[-5000:] # keep last 5000
|
||||
state["last_scan"] = datetime.now(timezone.utc).isoformat() + "Z"
|
||||
return state
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- `telegram-account-management` — for session audit and health patterns
|
||||
- `cron-job-execution-discipline` — for cron job execution contracts and formatting
|
||||
- `telegram-automation` — parent skill with Telethon repair patterns
|
||||
191
skills/telegram-automation/references/session-repair-recipes.md
Normal file
191
skills/telegram-automation/references/session-repair-recipes.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Telethon Session Repair — Full Recipes
|
||||
|
||||
Real repair scripts used in production. Copy-paste and run.
|
||||
|
||||
## Fix 7-column → 6-column (drop `number`)
|
||||
|
||||
Two 7-column variants exist. ALWAYS check actual column names first.
|
||||
|
||||
### Variant A: `number` column (Telethon v2, 2026+)
|
||||
Columns: `dc_id, server_address, port, auth_key, takeout_id, tmp_auth_key, number`
|
||||
|
||||
Drop the extra `number` column:
|
||||
|
||||
```python
|
||||
import sqlite3, shutil
|
||||
from pathlib import Path
|
||||
|
||||
SESSION_DIR = Path("sessions_clean")
|
||||
|
||||
for f in SESSION_DIR.glob("*.session"):
|
||||
db = sqlite3.connect(str(f))
|
||||
cur = db.execute("PRAGMA table_info(sessions)")
|
||||
cols = [r[1] for r in cur.fetchall()]
|
||||
db.close()
|
||||
|
||||
if len(cols) == 7 and "number" in cols:
|
||||
shutil.copy(str(f), str(f) + ".bak7cols")
|
||||
db = sqlite3.connect(str(f))
|
||||
db.execute("""
|
||||
CREATE TABLE sessions_new (
|
||||
dc_id INT PRIMARY KEY, server_address TEXT, port INT,
|
||||
auth_key BLOB, takeout_id INTEGER, tmp_auth_key BLOB
|
||||
)
|
||||
""")
|
||||
db.execute("INSERT INTO sessions_new SELECT dc_id, server_address, port, auth_key, takeout_id, tmp_auth_key FROM sessions")
|
||||
db.execute("DROP TABLE sessions")
|
||||
db.execute("ALTER TABLE sessions_new RENAME TO sessions")
|
||||
db.commit()
|
||||
db.close()
|
||||
print(f"Fixed: {f.name}")
|
||||
```
|
||||
|
||||
### Variant B: `auth_key_id` column (older)
|
||||
Columns: `dc_id, server_address, port, auth_key, auth_key_id, takeout_id, tmp_auth_key`
|
||||
|
||||
Drop the extra `auth_key_id` column:
|
||||
|
||||
```python
|
||||
import sqlite3, shutil
|
||||
from pathlib import Path
|
||||
|
||||
for f in Path("sessions_clean").glob("*.session"):
|
||||
db = sqlite3.connect(str(f))
|
||||
cur = db.execute("PRAGMA table_info(sessions)")
|
||||
cols = [r[1] for r in cur.fetchall()]
|
||||
db.close()
|
||||
|
||||
if len(cols) == 7 and "auth_key_id" in cols:
|
||||
shutil.copy(str(f), str(f) + ".bak7cols")
|
||||
db = sqlite3.connect(str(f))
|
||||
db.execute("""
|
||||
CREATE TABLE sessions_new (
|
||||
dc_id INT, server_address TEXT, port INT,
|
||||
auth_key BLOB, takeout_id INTEGER, tmp_auth_key BLOB
|
||||
)
|
||||
""")
|
||||
db.execute("INSERT INTO sessions_new SELECT dc_id, server_address, port, auth_key, takeout_id, tmp_auth_key FROM sessions")
|
||||
db.execute("DROP TABLE sessions")
|
||||
db.execute("ALTER TABLE sessions_new RENAME TO sessions")
|
||||
db.commit()
|
||||
db.close()
|
||||
print(f"Fixed: {f.name}")
|
||||
```
|
||||
|
||||
## Fix 5-column → 6-column
|
||||
|
||||
Two 5-column variants exist. ALWAYS check the actual column names before repairing.
|
||||
|
||||
### Variant A: missing `takeout_id` (cols: `dc_id, server_address, port, auth_key, auth_key_id`)
|
||||
|
||||
Older Telethon v7 sessions. Add `takeout_id` as NULL.
|
||||
|
||||
```python
|
||||
import sqlite3, shutil
|
||||
from pathlib import Path
|
||||
|
||||
for f in Path("sessions_clean").glob("*.session"):
|
||||
db = sqlite3.connect(str(f))
|
||||
cur = db.execute("PRAGMA table_info(sessions)")
|
||||
cols = [r[1] for r in cur.fetchall()]
|
||||
db.close()
|
||||
|
||||
if len(cols) == 5 and "auth_key_id" in cols:
|
||||
shutil.copy(str(f), str(f) + ".bak5cols")
|
||||
db = sqlite3.connect(str(f))
|
||||
db.execute("""
|
||||
CREATE TABLE sessions_new (
|
||||
dc_id INT, server_address TEXT, port INT,
|
||||
auth_key BLOB, auth_key_id BLOB, takeout_id INTEGER
|
||||
)
|
||||
""")
|
||||
db.execute("INSERT INTO sessions_new SELECT dc_id, server_address, port, auth_key, auth_key_id, NULL FROM sessions")
|
||||
db.execute("DROP TABLE sessions")
|
||||
db.execute("ALTER TABLE sessions_new RENAME TO sessions")
|
||||
db.commit()
|
||||
db.close()
|
||||
print(f"Fixed: {f.name}")
|
||||
```
|
||||
|
||||
### Variant B: missing `auth_key_id` (cols: `dc_id, server_address, port, auth_key, takeout_id`)
|
||||
|
||||
Encountered on MAINTG1.session (2026-07-28). This schema has `takeout_id` but no `auth_key_id`. Add `auth_key_id` as NULL.
|
||||
|
||||
```python
|
||||
# PROVEN on MAINTG1.session (C:\Users\User\Desktop\_PROJECTS\tg_content_agent\)
|
||||
import sqlite3, shutil
|
||||
|
||||
f = "MAINTG1.session"
|
||||
db = sqlite3.connect(f)
|
||||
cols = [r[1] for r in db.execute("PRAGMA table_info(sessions)")]
|
||||
print(f"Cols: {cols}")
|
||||
# Expected: ['dc_id', 'server_address', 'port', 'auth_key', 'takeout_id']
|
||||
|
||||
if len(cols) == 5 and "takeout_id" in cols and "auth_key_id" not in cols:
|
||||
shutil.copy(f, f + ".bak5cols_v2")
|
||||
db.execute("""
|
||||
CREATE TABLE sessions_new (
|
||||
dc_id INT, server_address TEXT, port INT,
|
||||
auth_key BLOB, auth_key_id BLOB, takeout_id INTEGER
|
||||
)
|
||||
""")
|
||||
db.execute("INSERT INTO sessions_new SELECT dc_id, server_address, port, auth_key, NULL, takeout_id FROM sessions")
|
||||
db.execute("DROP TABLE sessions")
|
||||
db.execute("ALTER TABLE sessions_new RENAME TO sessions")
|
||||
db.commit()
|
||||
# Verify
|
||||
new_cols = [r[1] for r in db.execute("PRAGMA table_info(sessions)")]
|
||||
row = db.execute("SELECT * FROM sessions").fetchone()
|
||||
assert len(new_cols) == 6, f"Expected 6 cols, got {len(new_cols)}"
|
||||
print(f"Fixed: {new_cols}, auth_key_len={len(row[3]) if row[3] else 0}")
|
||||
db.close()
|
||||
```
|
||||
|
||||
## Bulk session health check
|
||||
|
||||
```python
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
for d in ["sessions5", "sessions_clean", "session", "sessions_archive"]:
|
||||
sd = Path(d)
|
||||
if not sd.exists():
|
||||
continue
|
||||
for f in sorted(sd.glob("*.session")):
|
||||
try:
|
||||
db = sqlite3.connect(str(f))
|
||||
cur = db.execute("PRAGMA table_info(sessions)")
|
||||
cols = [r[1] for r in cur.fetchall()]
|
||||
cur = db.execute("SELECT * FROM sessions")
|
||||
row = cur.fetchone()
|
||||
db.close()
|
||||
status = "OK" if len(cols) == 6 and row and len(row) == 6 else f"BAD cols={len(cols)} row={len(row) if row else 0}"
|
||||
print(f"{d}/{f.stem}: {status}")
|
||||
except Exception as e:
|
||||
print(f"{d}/{f.stem}: ERROR {e}")
|
||||
```
|
||||
|
||||
## Check for corrupted auth_key (causes `TypeError: Strings must be encoded before hashing`)
|
||||
|
||||
```python
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
for d in ["sessions5", "sessions_clean"]:
|
||||
for f in Path(d).glob("*.session"):
|
||||
db = sqlite3.connect(str(f))
|
||||
cur = db.execute("SELECT auth_key FROM sessions")
|
||||
row = cur.fetchone()
|
||||
db.close()
|
||||
if row and row[0] is not None:
|
||||
ak = row[0]
|
||||
if isinstance(ak, str):
|
||||
print(f"BAD (string auth_key): {f}")
|
||||
elif isinstance(ak, bytes):
|
||||
if len(ak) < 100:
|
||||
print(f"SUSPECT (short auth_key {len(ak)}b): {f}")
|
||||
else:
|
||||
print(f"BAD (type {type(ak).__name__}): {f}")
|
||||
else:
|
||||
print(f"NULL auth_key: {f}")
|
||||
```
|
||||
Reference in New Issue
Block a user