中文 ▾
https://api.unrestrictedaiapi.com/v1uncensored2026-10-06
获取 API 密钥

在无审查 AI API 上构建 Discord 或 Telegram 机器人

聊天机器人是将无审查模型直接展示给用户的最快方式。本指南将构建一个 discord.py 机器人和一个 python-telegram-bot 机器人,它们共用一个处理 API 调用的模块,因此每个频道的历史记录、速率限制、重试机制和年龄限制只需编写一次即可复用。

更新于

要点

  1. 将 API 逻辑隔离在一个模块中,并保持每个平台适配器尽可能精简。
  2. 为每个频道或聊天保留独立的有界历史记录,确保对话不会混合。
  3. 三层节流:按用户冷却、每分钟 300 次请求的全局滑动窗口,以及并发限制。
  4. 年龄门槛:仅限标记为 NSFW 的 Discord 频道,并在 Telegram 上要求明确的 /adult 确认。

我们要构建什么

两个小型机器人,一个共享核心。名为 llm.py 的文件拥有所有涉及 API 的逻辑:人设、速率限制器和重试逻辑。Discord 机器人和 Telegram 机器人仅负责将平台事件转换为消息列表,再转换回响应。稍后添加第三个平台时,只需编写适配器。

使用 pip install httpx discord.py python-telegram-bot 安装依赖。从你的账户页面导出 API_KEY,以及从各平台获取 DISCORD_TOKEN 和/或 TELEGRAM_TOKEN。以下两个机器人均假设使用 Python 3.10 或更高版本。

请求格式为标准 OpenAI chat-completions 格式,详见多语言快速入门。此处无需使用 SDK。

共享核心:人设、限制器和重试

将关键部分放在一处。下面的模块维护请求时间戳的滑动窗口,当接近每分钟 240 次调用时等待,这在每密钥允许的每分钟 300 次调用限制下留有 deliberate 余量。信号量将并发限制为 4,防止繁忙频道同时触发 20 次请求。

# 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."

查看状态处理。429 或 503 错误会依次休眠 1、2 和 4 秒后重试。403 错误表示触发了内容过滤器,机器人将返回中性行而非重试。其他任何错误将抛出异常,由你的平台库记录。如果看到 402,说明余额为空或试用结束;请充值预付额度。

Discord 适配器

Discord 机器人需要在开发者门户和代码中开启 message-content 意图,否则 msg.content 将为空。机器人仅在被提及时回复,这使其不会读取繁忙服务器中的每条消息。

# 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"])

历史记录是一个以频道 ID 为键、maxlen=10 的 deque,因此每个频道都是独立的对话,旧消息会自动移除。在每个用户行前加上发言者的显示名称,使模型能在共享房间中区分用户。10 条消息是一个保守的默认值;上下文窗口为 100,000 token,因此你可以大幅提高此值。

Telegram 适配器

python-telegram-bot v20 及更高版本是异步的,这与共享核心自然配合。chat_data 是库为每个聊天维护的字典,因此历史记录和冷却状态无需额外的存储代码。

# 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() 是最简单的启动方式,无需公共 URL。请注意 chat_data 保存在内存中,因此重启会清除它。如果需要历史记录在重启后保留,请自行将 deque 内容存储到文件或数据库中。

年龄门槛和 NSFW 频道规则

此 API 仅限成人使用,你在其上构建的任何内容都继承这一责任。将年龄门槛视为功能,而非事后补充。

  • Discord: 仅在标记为年龄限制的公会频道中回复。检查 channel.is_nsfw() 无需成本,机器人会拒绝在其他地方(包括私信)回复。
  • Telegram: 在每个聊天中进行任何生成之前,要求明确的 /adult 确认,并在欢迎文本中说明规则。
  • 两者: 不要在面向年轻人的空间中提供机器人,如果发现用户未满 18 岁,请立即移除其访问权限。

确认命令是一个关卡,而非年龄证明。涉及未成年人的色情内容始终会被 API 以 403 状态码拦截,包括小说和角色扮演,你的机器人不应尝试绕过该响应。

运行与测试

在各自的终端中启动每个机器人:python discord_bot.py 或 python telegram_bot.py。首次运行时,创建一个私有测试服务器或仅包含你自己的聊天。通过简短的检查清单,你可以发现几乎所有初学者错误。

  1. 在非 NSFW 频道提及机器人并确认其拒绝。这证明门槛有效。
  2. 在标记频道中提及它并快速发送两条消息。第二条消息应获得沙漏反应,这证明冷却有效。
  3. 进行四轮对话,然后引用第一条消息。如果机器人记得,说明历史记录连接正确。
  4. 暂时将 API 密钥设置为错误值,并确认错误显示在日志中,而不是静默消失。
  5. 粘贴长文本块,观察回复在平台长度限制下拆分为多条消息。

如果回复感觉通用,通常是人设问题而非代码问题。更改 PERSONA 中的一句话,重启并比较。由于人设位于共享模块中,一次编辑会同时更改两个机器人,这正是该结构值得额外文件的原因。

准备部署时,任何常驻进程管理器均可使用。机器人与平台保持长连接,并向 API 发起短 HTTPS 调用,因此无需入站端口,几乎不占内存。添加重启策略,以防崩溃导致你的社区与沉默的机器人对话。

速率限制、历史记录大小与成本

三个参数控制机器人在高负载下的行为。用户级冷却时间可防止单人独占密钥。全局窗口保护每分钟 300 次请求的上限。信号量可平滑突发流量。如果你在一个密钥上运行多个机器人进程,它们共享该上限,因此请相应降低每个进程的 limit。

在邀请人群前估算支出。假设:每次调用发送约 1,200 个输入 token(人设加十轮对话),返回 250 个 token。一次调用成本为 1,200 x $0.25 / 1,000,000 + 250 x $1.00 / 1,000,000 = $0.0003 + $0.00025 = $0.00055。每天产生 2,000 条回复的服务器在这些假设下每天花费约 $1.10。你的数字会有所不同,因此记录 usage 字段一周后重新计算。

要调整人设本身,请阅读提示词指南。价格列在定价页面上。

发布前的加固

将 token 排除在源代码控制之外;如示例所示从环境变量中读取它们。在将消息发送到 API 之前限制消息长度,以防一条巨大的粘贴内容耗尽你的余额。记录错误但不记录消息文本,因为用户合理期望他们的聊天保持私密。添加一个 /reset 命令以清除频道的双端队列,这是在对话失控时最快的修复方法。最后,在机器人简介中添加一条可见说明,指出其撰写成人小说,且其回复由系统生成。

还有一个设计选择值得一提:机器人等待完整回复,而不是流式输出。逐 token 编辑聊天消息会直接触及平台编辑限制,而对于接近 150 字限制的回复,等待时间很短。如果稍后希望在自己的 Web 前端实现实时打字,请在那里开启 stream: true,由你控制传输。在此之前,你已发送的打字指示器是显示机器人正在工作的最便宜方式,且代码足够短,可以在一次阅读中阅读并在邀请任何人之前进行审计。

问答

一个 API 密钥能否同时供多个机器人使用?

可以,但它们共享该密钥每分钟 300 次请求的速率限制,因此请在各个进程间分配内部节流策略。

机器人重启后会保留对话记忆吗?

这些示例中未使用;历史记录保存在内存中。如果需要连续性,请将双端队列内容持久化到文件或数据库中。

如何防止未成年人访问?

将 Discord 回复限制在年龄受限频道,要求 Telegram 确认命令,并避免出现在面向青少年的空间。这些是门槛而非证明,因此需对任何举报迅速作出反应。

余额耗尽时会发生什么?

API 返回 402 状态码及 no_credit 错误。在核心模块中捕获该错误,并告知用户机器人已暂停。

只差一张表单,即可获得密钥

创建账户,复制密钥,更改 Base URL。这就是全部设置。

获取 API 密钥