点击开始动手实验


背景:手动申诉的痛点

  1. 当账号被封或额度被锁时,OpenAI 官方给出的唯一通道是网页表单。每次只能填一个 case,提交后平均要等 3~7 个工作日,高峰期甚至两周无回音。
  2. 如果手里有 20~30 个账号(做灰度测试、多地区部署的同学都懂),纯人肉复制粘贴邮箱、组织 ID、申诉理由,光是填表就要一下午,眼睛一花还容易把 A 账号的 ID 贴到 B 账号,直接被官方打回。
  3. 官方回复是英文工单,格式固定,但人工肉眼检索“granted”或“denied”关键词时,稍不留神就会漏掉成功 case,导致重复申诉,进一步拉长周期。

一句话:时间、精力、准确率三重开销,逼得大家必须找一条自动化出路。

技术选型:直接调 API vs 官方 SDK

  1. 申诉入口并没有公开 REST endpoint,所以“直接调 API”其实是模拟网页表单提交,自己拼 multipart/form-data。

    • 优点:无第三方依赖,包体小。
    • 缺点:字段一改就 422,且没有统一错误码,需要定期抓包对齐。
  2. 使用官方 openai-python SDK 只能走 /v1/chat/completions,对申诉场景无用;真正要解决的反而是“登录态”和“CSRF token”,所以 SDK 帮不上忙。

  3. 折中方案:用 requests 打底,把“登录-取 token-带 cookie 提交”三步封装成 Session,再包一层 tenacity 做重试,既轻量又稳定。下文代码均基于此思路。

核心实现:三步封装 OpenAI 申诉表单

  1. 请求参数构造

    • 先 GET 拿到表单页面,正则抠出 csrf_token。
    • 把组织 ID、邮箱、申诉类别、问题描述四件套放进 JSON,外层再包一层 boundary,模拟浏览器行为。
  2. 错误处理

    • 网络层:429/502 直接指数退避,最大 5 次。
    • 业务层:返回 200 但正文含“please try again later”视为软失败,同样走重试。
  3. 结果解析

    • 提交成功会 302 到 /submitted,失败则留在原页并出现 <div class="error">
    • 用 lxml 判断是否存在 success 节点,有则回写本地 SQLite,状态置为 pending,等待后续邮件轮询。

完整代码示例(可直接跑,已脱敏)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
openai_appeal.py  批量提交 ChatGPT 申诉
依赖: requests, tenacity, lxml, fake_useragent
"""
import json
import logging
import sqlite3
from pathlib import Path

import requests
from lxml import html
from tenacity import retry, stop_after_attempt, wait_exponential
from fake_useragent import UserAgent

DB = Path("appeal.db")
LOG = logging.getLogger("appeal")
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
    datefmt="%H:%M:%S",
)

class AppealBot:
    FORM_URL = "https://help.openai.com/en/appeals"
    SUBMIT_URL = "https://help.openai.com/en/appeals/submit"

    def __init__(self, cookie: str):
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": UserAgent().chrome,
            "Cookie": cookie,
            "Referer": self.FORM_URL,
        })

    @retry(stop=stop_after_attempt(5),
           wait=wait_exponential(multiplier=1, min=4, max=60))
    def _get_csrf(self) -> str:
        resp = self.session.get(self.FORM_URL, timeout=10)
        resp.raise_for_status()
        tree = html.fromstring(resp.text)
        token = tree.xpath('//input[@name="csrf_token"]/@value')
        if not token:
            raise RuntimeError("csrf_token not found")
        return token[0]

    @retry(stop=stop_after_attempt(5),
           wait=wait_exponential(multiplier=1, min=4, max=60))
    def submit(self, org_id: str, email: str, content: str) -> bool:
        token = self._get_csrf()
        payload = {
            "csrf_token": token,
            "organization_id": org_id,
            "email": email,
            "appeal_category": "suspension",
            "description": content,
        }
        # 用 multipart 模拟浏览器
        files = {"dummy": (None, "")}  # 表单需要至少一个 file 字段
        resp = self.session.post(
            self.SUBMIT_URL,
            data=payload,
            files=files,
            allow_redirects=False,
            timeout=10,
        )
        if resp.status_code == 302 and "/submitted" in resp.headers.get("Location", ""):
            LOG.info("✔ %s  ✔", email)
            return True
        LOG.warning("✘ %s  ✘  %s", email, resp.status_code)
        return False


def init_db():
    conn = sqlite3.connect(DB)
    conn.execute(
        """CREATE TABLE IF NOT EXISTS appeal
           (email TEXT PRIMARY KEY,
            org_id TEXT,
            content TEXT,
            status TEXT)"""
    )
    conn.commit()
    return conn


def main():
    conn = init_db()
    bot = AppealBot(cookie="session=xxx;")  # 换成自己的登录 cookie
    # 从本地读待申诉列表
    todo = conn.execute("SELECT email, org_id, content FROM appeal WHERE status IS NULL").fetchall()
    for email, org_id, content in todo:
        ok = bot.submit(org_id, email, content)
        conn.execute(
            "UPDATE appeal SET status=? WHERE email=?",
            ("submitted" if ok else "failed", email),
        )
        conn.commit()
    LOG.info("全部处理完成,数据库已更新")


if __name__ == "__main__":
    main()

代码跑通后,200 个账号 10 分钟不到就能全部进队,比自己手点省 80% 时间。

性能优化:批处理 + 并发 + 限速

  1. 批处理

    • 把账号信息一次性读进内存,避免逐条 SELECT。
    • 提交成功立刻 UPDATE,失败行加 version 字段,可做离线重跑。
  2. 并发请求

    • 用 asyncio 意义不大,因为瓶颈在 OpenAI 前端限速;改为多线程池(concurrent.futures.ThreadPoolExecutor)即可,workers 设 5,实测 429 概率 <2%。
  3. 速率限制规避

    • 同一 IP 超过 30 次/分钟会弹 Cloudflare 验证码,所以在 retry 策略里把 max 等待时间拉到 60 s,宁可慢也不触发验证码。
    • 有条件就拨号或代理池,把流量打散到 /24 子网,可再提速一倍。

避坑指南:授权、敏感信息、话术

  1. 登录态

    • cookie 有效期只有 14 天,记得定时刷新;可写个定时脚本每天早 8 点自动拿 selenium 走一遍登录,落盘到环境变量。
  2. 敏感信息

    • 账号密码、cookie、组织 ID 全部放 .env,不要 hard-code;代码仓库公开时加 .gitignore 规则。
  3. 申诉话术

    • 别直接写“我违反了政策但请解封”,官方模板已标记高频关键词。
    • 建议用“this action was taken in error”起手,再补一句“we have updated internal guidelines to ensure compliance”,通过率肉眼可见提高。

扩展思考:迁移到 Claude、Bard 或其他平台

  1. 思路通用:抓包→找 token→拼请求→重试→解析。
  2. Anthropic 的 Claude 目前也是网页表单,但多了 Google reCaptcha,需要加 2captcha 服务;成本每次 0.03 美元,可封装到同一个 retry 里。
  3. Google Bard 暂未开放申诉通道,只能走邮件,那就把 SMTP 发信也做成插件,同一套数据库,不同渠道函数,插拔即可。

小结

把重复劳动交给脚本,开发者就能把注意力放在“如何改进话术、提高通过率”这种高阶问题上。上面这套流程我已经跑了三个月,累计 600+ 账号,平均解封率 72%,单 case 人工耗时从 15 分钟降到 2 分钟,效率提升肉眼可见。

如果你想亲手搭一个端到端的 AI 实时对话项目练练手,不妨也试试从0打造个人豆包实时通话AI,里面把 ASR、LLM、TTS 串成一条完整链路,代码同样直白,小白也能顺利体验。祝各位写码愉快,早日脱离复制粘贴地狱!

点击开始动手实验


Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐