Yang kami bangun
Dua bot kecil, satu otak bersama. File bernama llm.py mengatur semua yang menyentuh API: persona, pembatas laju, logika pengulangan. Bot Discord dan bot Telegram kemudian hanya menerjemahkan peristiwa platform menjadi daftar pesan dan sebaliknya. Tambahkan platform ketiga nanti dan Anda hanya perlu menulis adaptor.
Instal dependensi dengan pip install httpx discord.py python-telegram-bot. Ekspor API_KEY dari halaman akun Anda, serta DISCORD_TOKEN dan/atau TELEGRAM_TOKEN dari pengaturan bot masing-masing platform. Kedua bot di bawah ini mengasumsikan Python 3.10 atau lebih baru.
Format permintaan adalah bentuk chat-completions OpenAI biasa, yang dibahas di panduan cepat multi-bahasa. Tidak ada di sini yang membutuhkan SDK.
Inti bersama: persona, pembatas, dan percobaan ulang
Letakkan bagian berbahaya di satu tempat. Modul di bawah ini mempertahankan jendela geser dari timestamp permintaan dan menunggu saat mendekati 240 panggilan per menit, margin yang disengaja di bawah 300 per menit yang diizinkan per kunci. Semaphore membatasi konkurensi menjadi empat sehingga saluran yang sibuk tidak dapat menembakkan dua puluh permintaan sekaligus.
# llm.py - shared by both bots
import asyncio
import os
import time
from collections import deque
import httpx
URL = "https://api.unrestrictedaiapi.com/v1/chat/completions"
PERSONA = "You are Pip, a sardonic bar-room storyteller. Keep replies under 150 words."
_stamps = deque() # timestamps of recent calls
_gate = asyncio.Semaphore(4) # at most 4 requests in flight
_client = httpx.AsyncClient(timeout=60.0)
async def _wait_for_slot(limit=240, window=60.0):
# Stay below the 300 requests/minute per-key limit with headroom.
while True:
now = time.monotonic()
while _stamps and now - _stamps[0] > window:
_stamps.popleft()
if len(_stamps) < limit:
_stamps.append(now)
return
await asyncio.sleep(window - (now - _stamps[0]) + 0.05)
async def chat(history):
"""history: list of {"role","content"} dicts, oldest first."""
messages = [{"role": "system", "content": PERSONA}] + list(history)
async with _gate:
for attempt in range(3):
await _wait_for_slot()
r = await _client.post(
URL,
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={"model": "uncensored", "messages": messages, "max_tokens": 400},
)
if r.status_code in (429, 503):
await asyncio.sleep(2 ** attempt)
continue
if r.status_code == 403:
return "I can't continue with that."
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
return "The service is busy. Try again in a moment."Lihat penanganan status. 429 atau 503 menunda selama 1, 2, lalu 4 detik dan mencoba lagi. 403 berarti filter konten aktif, jadi bot menjawab dengan baris netral daripada mencoba lagi. Kesalahan lain akan dilempar, yang akan dicatat oleh pustaka platform Anda. Jika Anda melihat 402, saldo Anda kosong atau uji coba telah berakhir; isi saldo prabayar.
Adapter Discord
Bot Discord memerlukan pengaktifan intent message-content di portal pengembang serta dalam kode, atau msg.content akan kosong. Bot hanya menjawab ketika disebutkan, yang mencegahnya membaca setiap pesan di server yang ramai.
# discord_bot.py
import os
import time
from collections import defaultdict, deque
import discord
from llm import chat
intents = discord.Intents.default()
intents.message_content = True # also enable it in the developer portal
bot = discord.Client(intents=intents)
HISTORY_TURNS = 10 # user+assistant messages kept per channel
history = defaultdict(lambda: deque(maxlen=HISTORY_TURNS))
last_use = {} # user id -> last request time
COOLDOWN = 5.0 # seconds between requests per user
def chunks(text, size=1900):
return [text[i:i + size] for i in range(0, len(text), size)] or [""]
@bot.event
async def on_message(msg: discord.Message):
if msg.author.bot or bot.user not in msg.mentions:
return
# Age gate: guild text channels must be flagged NSFW. No DMs.
if not isinstance(msg.channel, discord.TextChannel) or not msg.channel.is_nsfw():
await msg.reply("I only chat in channels marked age-restricted (NSFW).")
return
now = time.monotonic()
if now - last_use.get(msg.author.id, 0) < COOLDOWN:
await msg.add_reaction("\u23f3")
return
last_use[msg.author.id] = now
text = msg.clean_content.replace(f"@{bot.user.display_name}", "").strip()
if not text:
return
h = history[msg.channel.id]
h.append({"role": "user", "content": f"{msg.author.display_name}: {text}"})
async with msg.channel.typing():
answer = await chat(h)
h.append({"role": "assistant", "content": answer})
for part in chunks(answer):
await msg.channel.send(part)
bot.run(os.environ["DISCORD_TOKEN"])Riwayat adalah deque dengan maxlen=10 yang dikunci berdasarkan id saluran, sehingga setiap saluran adalah percakapan sendiri dan giliran lama akan jatuh secara otomatis. Memberi awalan setiap baris pengguna dengan nama tampilan pembicara memungkinkan model membedakan orang di ruangan bersama. Sepuluh pesan adalah default konservatif; jendela konteks adalah 100.000 token, sehingga Anda dapat meningkatkannya secara signifikan sebelum itu menjadi masalah.
Adapter Telegram
python-telegram-bot v20 dan yang lebih baru bersifat asinkron, yang berpasangan secara alami dengan inti bersama. chat_data adalah kamus per-obrolan yang disimpan perpustakaan untuk Anda, sehingga riwayat dan status cooldown tidak memerlukan kode penyimpanan tambahan.
# telegram_bot.py (python-telegram-bot v20+)
import os
import time
from collections import deque
from telegram import Update
from telegram.ext import (ApplicationBuilder, CommandHandler, ContextTypes,
MessageHandler, filters)
from llm import chat
async def adult(update: Update, context: ContextTypes.DEFAULT_TYPE):
context.chat_data["adult"] = True
await update.message.reply_text("Noted. Say something.")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"This bot produces adult fiction. Send /adult only if you are 18 or older."
)
async def talk(update: Update, context: ContextTypes.DEFAULT_TYPE):
cd = context.chat_data
if not cd.get("adult"):
await update.message.reply_text("Send /adult to confirm you are 18+.")
return
if time.monotonic() - cd.get("last", 0) < 4:
return # per-chat cooldown
cd["last"] = time.monotonic()
h = cd.setdefault("history", deque(maxlen=10))
h.append({"role": "user", "content": update.message.text})
await context.bot.send_chat_action(update.effective_chat.id, "typing")
answer = await chat(h)
h.append({"role": "assistant", "content": answer})
for i in range(0, len(answer), 4000): # Telegram limit is 4096 chars
await update.message.reply_text(answer[i:i + 4000])
app = ApplicationBuilder().token(os.environ["TELEGRAM_TOKEN"]).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("adult", adult))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, talk))
app.run_polling()run_polling() adalah cara termudah untuk memulai dan tidak memerlukan URL publik. Perlu diingat bahwa chat_data berada di memori, sehingga restart akan mengosongkannya. Jika Anda memerlukan riwayat agar bertahan dari restart, simpan isi deque sendiri ke dalam file atau basis data.
Pembatasan usia dan aturan saluran NSFW
API ini hanya untuk orang dewasa, dan apa pun yang Anda bangun di atasnya mewarisi tanggung jawab tersebut. Perlakukan pembatasan usia sebagai fitur, bukan sekadar tambahan.
- Discord: jawab hanya di saluran guild yang ditandai sebagai batasan usia. Pemeriksaan
channel.is_nsfw()tidak memerlukan biaya dan bot menolak di tempat lain, termasuk pesan langsung. - Telegram: memerlukan konfirmasi eksplisit
/adultper obrolan sebelum pembuatan apa pun, dan nyatakan aturan tersebut dalam teks sambutan. - Keduanya: jangan menawarkan bot di ruang yang ditujukan untuk anak muda, dan cabut akses segera jika Anda mengetahui pengguna berusia di bawah 18 tahun.
Perintah konfirmasi adalah gerbang, bukan bukti usia. Konten seksual yang melibatkan anak di bawah umur selalu diblokir oleh API dengan 403, termasuk fiksi dan roleplay, dan bot Anda tidak boleh mencoba mengatasi respons tersebut.
Menjalankannya dan menguji tanpa kerumunan
Mulai setiap bot di terminal sendiri dengan python discord_bot.py atau python telegram_bot.py. Untuk menjalankan pertama kali, buat server uji pribadi atau obrolan dengan hanya diri Anda sendiri. Ikuti daftar periksa singkat dan Anda akan menangkap hampir setiap bug pemula.
- Sebutkan bot di saluran non-NSFW dan konfirmasi bahwa ia menolak. Ini membuktikan bahwa gerbang berfungsi.
- Sebutkan di saluran yang ditandai dan kirim dua pesan dengan cepat. Yang kedua harus mendapatkan reaksi jam pasir, yang membuktikan bahwa waktu tunggu berfungsi.
- Pegang percakapan empat pesan, lalu merujuk kembali ke pesan pertama. Jika bot mengingat, riwayat terhubung dengan benar.
- Secara sementara atur kunci API menjadi nilai yang salah dan konfirmasi bahwa kegagalan muncul di log Anda daripada menghilang secara diam-diam.
- Tempel blok teks panjang dan perhatikan balasan terpecah menjadi beberapa pesan di bawah batas panjang platform.
Jika balasan terasa generik, persona biasanya menjadi masalahnya, bukan kodenya. Ubah satu kalimat dari PERSONA, mulai ulang, dan bandingkan. Karena persona berada di modul bersama, satu pengeditan mengubah kedua bot sekaligus, yang merupakan alasan mengapa struktur ini layak dengan file tambahan.
Ketika Anda siap untuk menerapkan, manajer proses always-on apa pun akan berhasil. Bot memegang koneksi berumur panjang ke platform mereka dan membuat panggilan HTTPS keluar pendek ke API, sehingga mereka tidak memerlukan port masuk dan hampir tidak ada memori. Tambahkan kebijakan restart sehingga kerusakan tidak meninggalkan komunitas Anda berbicara dengan bot yang diam.
Batas laju, ukuran riwayat, dan biaya
Tiga tombol mengontrol bagaimana bot berperilaku di bawah beban. Cooldown per pengguna menghentikan satu orang dari memonopoli kunci. Jendela global melindungi batas atas 300 permintaan per menit. Semaphore meratakan lonjakan. Jika Anda menjalankan beberapa proses bot pada satu kunci, mereka berbagi batas atas tersebut, sehingga turunkan limit setiap proses sesuai dengan itu.
Perkirakan pengeluaran sebelum Anda mengundang kerumunan. Asumsi: setiap panggilan mengirim sekitar 1.200 token input (persona ditambah sepuluh giliran) dan mengembalikan 250 token. Satu panggilan biaya 1.200 x $0,25 / 1.000.000 + 250 x $1,00 / 1.000.000 = $0,0003 + $0,00025 = $0,00055. Server yang menghasilkan 2.000 balasan sehari akan menghabiskan sekitar $1,10 sehari di bawah asumsi tersebut. Angka Anda akan berbeda, jadi log bidang usage selama seminggu dan hitung ulang.
Untuk menyetel persona itu sendiri, baca panduan prompting. Harga tercantum di halaman harga.
Penguatan sebelum Anda membagikan bot
Jauhkan token dari kontrol sumber; bacalah dari lingkungan seperti yang ditunjukkan. Batasi panjang pesan sebelum mengirimkannya ke API agar satu tempelan besar tidak menghabiskan saldo Anda. Log kesalahan tetapi bukan teks pesan, karena pengguna Anda secara wajar mengharapkan obrolan mereka tetap pribadi. Tambahkan perintah /reset yang mengosongkan deque saluran, yang merupakan perbaikan tercepat ketika percakapan terganggu. Terakhir, tambahkan catatan yang terlihat di profil bot yang menyatakan bahwa ia menulis fiksi dewasa dan bahwa balasan mereka dihasilkan.
Satu pilihan desain lagi layak satu kalimat: bot menunggu balasan lengkap alih-alih meng-streaming-nya. Mengedit pesan obrolan token demi token langsung bertemu dengan batas edit platform, dan untuk balasan yang dibatasi sekitar 150 kata, penundaannya singkat. Jika nanti Anda ingin pengetikan langsung di antarmuka web Anda sendiri, aktifkan stream: true di sana, di mana Anda mengendalikan transportasi. Sampai saat itu, indikator pengetikan yang sudah Anda kirim adalah cara termurah untuk menunjukkan bahwa bot sedang bekerja, dan itu menjaga kode tetap pendek sehingga dapat dibaca dalam satu kali duduk dan diaudit sebelum Anda mengundang siapa pun.