零基础也能看懂!手把手教你搭建B站自动回复机器人
本教程从「什么是Python」讲起,带你一步步理解整个项目的每一行代码。
全文包含完整代码展示和中文逐行注释。
由于输入法原因,流程图放在文章最后


目录

  1. 项目概览
  2. 环境准备
  3. 文件结构
  4. B站API
  5. AI服务
  6. 人设管理
  7. 消息处理
  8. 轮询器
  9. 数据持久化
  10. Web界面
  11. 运行全流程
  12. 常见问题
  13. 附录

1. 项目概览

1.1 这个项目是做什么的?

用大白话讲: 你是B站UP主,每天有很多粉丝在评论区@你。你不可能24小时在线回复。
这个机器人帮你自动回复——粉丝@你,它判断是不是粉丝、是不是要求总结视频,然后自动回复。

它能做4件事:

功能效果
自动回复@评论粉丝评论@你时,自动按人设风格回复
AI总结视频有人说「总结作品,私信我」,AI生成总结私信发送
切换人设风格8种回复语气(沙雕/傲娇/治愈/佛系等)自动轮换
Web管理页面浏览器打开一个页面就能控制一切

1.2 技术栈

技术用途
Python 3.12+编程语言
Flask 3.0+Web框架,提供API接口
RequestsHTTP库,跟B站和DeepSeek通信
DeepSeek APIAI大模型,生成回复和总结
B站API获取通知、发评论、发私信
JSON文件存储配置和日志

1.3 整体架构

渲染错误: Mermaid 渲染失败: Parse error on line 2: ...t TB U[B站用户] -->|@评论| N[B站通知中心] ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

2.2 安装Python

  1. 打开 https://www.python.org/downloads/
  2. 点击「Download Python 3.12.x」
  3. 安装时务必勾选 Add Python to PATH(否则后面会报错)
  4. 一路Next完成

验证安装:打开命令提示符(Win+R → cmd → 回车)

`ash
python --version


看到 Python 3.12.x 就成功了。

### 2.3 pip换源(加速下载)

pip是Python的包管理器。换源后下载更快:

`ash
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

2.4 安装依赖

`ash
cd BiliReplyWeb
pip install -r requirements.txt


依赖只有3个:
- flask -- Web框架
- flask-cors -- 跨域支持
- requests -- HTTP请求

### 2.5 验证启动

`ash
python app.py

看到 Web UI 地址输出即成功。


3. 项目文件结构逐层解析

3.1 目录树

BiliReplyWeb/
  app.py              -- 程序入口(双击运行的文件)
  config.py           -- 所有配置集中管理
  requirements.txt    -- 依赖清单
  core/               -- 核心业务逻辑
    bilibili_api.py   -- B站API封装
    ai_service.py     -- DeepSeek AI服务
    persona_manager.py-- 人设管理
    poller.py         -- 轮询器
    processor.py      -- 消息处理器
  storage/            -- 数据持久化
    config_manager.py -- 配置读写
    account_manager.py-- 账户管理
    log_manager.py    -- 日志管理
  web/                -- Web管理界面
    server.py         -- Flask服务器
    static/           -- 前端文件
  data/               -- 运行生成的数据
    config.json       -- 配置
    cookies.json      -- 登录凭证
    logs.json         -- 日志

核心文件概览:

文件一句话说明
app.py程序入口,启动Web服务器
config.py集中管理所有配置和数据文件路径
bilibili_api.py封装所有B站接口的「电话总机」
ai_service.py调用DeepSeek AI的「大脑接线员」
persona_manager.py管理8种人设的「人格仓库」
poller.py定时检查B站消息的「哨兵」
processor.py处理消息、判断粉丝、分发任务的「指挥官」
storage/config_manager.py读写配置文件的「管家」
storage/account_manager.py安全保存登录凭证的「保险箱」
storage/log_manager.py记录所有操作的「黑匣子」
web/server.py提供Web API的「前台服务员」

3.2 app.py:程序入口

# -*- coding: utf-8 -*-
# """哔哩哔哩智能回复助手 - 入口文件"""
import sys
import os

# 处理Windows控制台中文乱码
if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
    try:
        sys.stdout.reconfigure(encoding='utf-8', errors='replace')
    except:
        pass

# 把项目根目录加入Python搜索路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

# 导入Web服务器的启动函数
from web.server import run_server

if __name__ == '__main__':
    print('=' * 46)
    print('  BiliReplyBot - Bilibili Smart Reply Assistant')
    print('  Web UI: http://127.0.0.1:8005')
    print('  Press Ctrl+C to stop')
    print('=' * 46)
    run_server()

逐行解释:

  • 第8-12行:如果命令行输出编码不是UTF-8,强制改成UTF-8,防止中文乱码
  • 第15行:把项目根目录加入Python搜索路径,让import能找到其他模块
  • 第18行:从web/server.py导入run_server函数(只导入,不立即执行)
  • 第20行:if name == ‘main’ 确保只有直接运行app.py时才执行下面代码
  • 第21-26行:打印启动信息,然后调用run_server()启动Web服务器

3.3 config.py:配置中心

# -*- coding: utf-8 -*-
# """配置文件"""
import os
from pathlib import Path

BASE_DIR = Path(__file__).parent
DATA_DIR = BASE_DIR / 'data'
DATA_DIR.mkdir(exist_ok=True)

WEB_HOST = '127.0.0.1'
WEB_PORT = 8005

BILI_API_BASE = 'https://api.bilibili.com'
BILI_PASSPORT_BASE = 'https://passport.bilibili.com'
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Safari/537.36'

POLL_INTERVAL_MIN = 8
POLL_INTERVAL_MAX = 10

DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions'
SUMMARY_MAX_TOKENS = 1000
SUMMARY_TIMEOUT = 60
SUMMARY_RETRY = 1

MAX_LOG_ENTRIES = 500
PERSONAS_COUNT = 9
PERSONAS_FILE = BASE_DIR / 'web' / 'static' / 'personas.json'

DEFAULT_CONFIG = {
    'api_key': '',              # DeepSeek API密钥
    'poll_interval': 10,        # 轮询间隔
    'daily_reply_limit': 100,   # 每日回复上限
    'keyword_enabled': True,    # 是否启用关键词检测
    'public_reply_template': '已私信你啦~快去查看吧!',
    'current_persona_id': None, # 当前人设ID
    'manual_persona_date': None,# 手动选择人设的日期
    'cookies': None,            # B站登录Cookie
    'poller_running': False     # 轮询器是否正在运行
}

CONFIG_FILE = DATA_DIR / 'config.json'
COOKIES_FILE = DATA_DIR / 'cookies.json'
LOGS_FILE = DATA_DIR / 'logs.json'

config.py要点:

  • Path(file).parent 获取config.py所在目录的父目录,即项目根
  • BASE_DIR / ‘data’ 使用pathlib拼接路径,等价于 BASE_DIR + ‘/data’
  • DEFAULT_CONFIG 是配置模板,首次启动时写入data/config.json
  • 所有「固定配置」集中在这里,方便修改

4. B站API封装 (core/bilibili_api.py)

4.1 什么是API?

API(应用程序接口)是程序与服务之间的「通信协议」。

  • 你的程序问B站:『最近有人@我吗?』
  • B站API返回:『有,请看这些数据』

4.2 Session和Cookie

  • Session:一次对话。程序跟B站建立Session后,多次请求共享状态
  • Cookie:身份凭证。登录成功B站给你一个Cookie,下次请求带上它,B站就知道你是谁了

4.3 基础设置

import json
import time
import requests
from config import BILI_API_BASE, BILI_PASSPORT_BASE, USER_AGENT

SESSION = requests.Session()
SESSION.headers.update({
    'User-Agent': USER_AGENT,
    'Referer': 'https://www.bilibili.com',
    'Origin': 'https://www.bilibili.com'
})

逐行解释:

  • requests.Session() 创建一个会话对象,自动管理Cookie
  • headers.update(…) 设置请求头,模拟浏览器访问
  • User-Agent:告诉B站「我是一个正常浏览器」,防止被拦截
  • Referer/Origin:HTTP协议中的来源标识,B站检查这些字段

4.4 扫码登录

在这里插入图片描述

def get_qrcode() -> dict:
    """获取二维码登录信息"""
    url = f"{BILI_PASSPORT_BASE}/x/passport-login/web/qrcode/generate"
    r = _req("GET", url)
    return r.json()

def poll_qrcode(qrcode_key: str) -> dict:
    """轮询扫码状态,返回 {json, cookies}"""
    url = f"{BILI_PASSPORT_BASE}/x/passport-login/web/qrcode/poll"
    r = _req("GET", url, params={"qrcode_key": qrcode_key})
    return {
        "json": r.json(),
        "cookies": dict(r.cookies.get_dict())
    }

扫码登录原理:

  1. get_qrcode() 调用B站接口,获取一个二维码图片URL和唯一key
  2. 前端显示二维码,用户用B站App扫码
  3. poll_qrcode(key) 不断轮询B站:用户扫码了吗?
  4. 扫码成功后,B站返回cookies,保存即可

4.5 消息轮询

def get_notifications(cookies: dict, page=1) -> dict:
    """获取@我的通知"""
    s = requests.Session()
    _set_cookies(s, cookies)
    params = {"type": 1, "platform": "web", "pn": page, "ps": 20}
    r = _req("GET", f"{BILI_API_BASE}/x/msgfeed/at", session=s, params=params)
    return r.json()

参数说明:type=1获取@通知,pn页码,ps每页条数

4.6 粉丝判断

def check_follow(cookies: dict, target_uid: int) -> bool:
    """判断是否关注了我(遍历分页)"""
    nav = get_nav_info(cookies)
    if nav.get("code") != 0:
        return False
    my_uid = nav['data']['mid']

    s = requests.Session()
    _set_cookies(s, cookies)
    pn = 1
    while True:
        params = {"vmid": target_uid, "psize": 50, "pn": pn}
        r = _req("GET", f"{BILI_API_BASE}/x/relation/followings", session=s, params=params)
        data = r.json()
        if data.get("code") != 0:
            return False
        items = data.get("data", {}).get("list", [])
        for item in items:
            if item.get("mid") == my_uid:
                return True
        if len(items) < 50:
            break
        pn += 1
        if pn > 10:
            break
    return False

原理:翻对方关注列表找自己UID,最多查10页*50人=500人

4.7 回复评论

def reply_comment(cookies, oid, content, type_id=1, root=0, parent=0):
    """回复评论/动态"""
    s = requests.Session()
    _set_cookies(s, cookies)
    s.headers.update({"Content-Type": "application/x-www-form-urlencoded"})
    csrf_token = cookies.get("bili_jct", "")

    data = {
        "oid": oid,
        "type": type_id,
        "message": content,
        "root": root,
        "parent": parent,
        "csrf": csrf_token
    }
    r = _req("POST", f"{BILI_API_BASE}/x/v2/reply/add", session=s, data=data)
    return r.json()

CSRF是安全令牌。B站要求每个写操作都带上bili_jct。

4.8 发送私信

def send_private_msg(cookies, receiver_uid, content):
    """发送私信"""
    s = requests.Session()
    _set_cookies(s, cookies)
    s.headers.update({"Content-Type": "application/x-www-form-urlencoded"})
    csrf_token = cookies.get("bili_jct", "")

    data = {
        "msg_sender_uid": 0,
        "receiver_id": receiver_uid,
        "receiver_type": 1,
        "msg_type": 1,
        "content": json.dumps({"content": content}, ensure_ascii=False),
        "csrf": csrf_token
    }
    try:
        r = _req("POST", f"{BILI_API_BASE}/x/session/send", session=s, data=data)
        result = r.json()
        if result.get("code") == 0:
            return result
    except:
        pass
    try:
        r2 = _req("POST", f"{BILI_API_BASE}/x/msgfeed/send", session=s, data=data)
        return r2.json()
    except:
        pass
    return {"code": -1, "message": "私信发送失败"}

关键点:content需json.dumps包装成JSON字符串,这是B站API格式要求

4.9 字幕获取

def get_video_subtitle(cookies, bvid):
    """获取B站AI字幕文本"""
    s = requests.Session()
    _set_cookies(s, cookies)
    r = _req("GET", f"{BILI_API_BASE}/x/web-interface/view",
             session=s, params={"bvid": bvid})
    data = r.json()
    if data.get("code") != 0:
        return ''
    sub_urls = data.get("data",{}).get("subtitle",{}).get("subtitles",[])
    if not sub_urls:
        return ''
    sub_url = sub_urls[0].get('subtitle_url', '')
    if sub_url.startswith('//'):
        sub_url = 'https:' + sub_url
    try:
        r2 = _req('GET', sub_url)
        sub_data = r2.json()
        parts = sub_data.get("body", [])
        text = ' '.join([p.get('content', '') for p in parts])
        return text[:2000]
    except:
        return ''

两步过程:1)查视频信息找字幕链接 2)下载字幕拼接成文本


5. AI智能回复 (core/ai_service.py)

5.1 什么是大模型API?

大模型(如DeepSeek)像一个超级聪明的AI。
你给它一段文字,它返回一段有逻辑的回答。

  • 你问:「总结这个视频内容」
  • AI返回:「本视频讲述了……的核心要点……」

要使用需要先注册DeepSeek账号,获取API Key:
https://platform.deepseek.com/

5.2 核心调用函数

import requests, json, time
from config import DEEPSEEK_API_URL, SUMMARY_MAX_TOKENS, SUMMARY_TIMEOUT

def _call_deepseek(api_key, messages, temperature=0.7, max_tokens=1000):
    """调用DeepSeek API的核心函数"""
    if not api_key:
        return ''

    headers = {
        "Authorization": f"Bearer {api_key}",  # API认证
        "Content-Type": "application/json"
    }
    payload = {
        "model": "deepseek-chat",              # 模型名
        "messages": messages,                   # 对话历史
        "temperature": temperature,             # 创造力(0-2)
        "max_tokens": max_tokens                # 最大回复长度
    }
    try:
        r = requests.post(DEEPSEEK_API_URL,
                          headers=headers,
                          json=payload,
                          timeout=SUMMARY_TIMEOUT)
        data = r.json()
        if "choices" in data and len(data["choices"]) > 0:
            return data["choices"][0]["message"]["content"].strip()
        return ''
    except Exception as e:
        raise Exception(f"DeepSeek API调用失败: {str(e)}")

参数解释:

  • Authorization: Bearer + API Key 是标准的API认证方式
  • temperature=0.7:0=保守稳定,1=有创意,2=天马行空
  • max_tokens=1000:限制AI生成的最大字数(中文约600-800字)
  • messages:对话结构 [{“role”:“system”,“content”:“指令”}, {“role”:“user”,“content”:“问题”}]

5.3 生成视频总结

def generate_summary(api_key, source_text, persona_style):
    """生成带人设风格的视频总结"""
    system_prompt = f'''你是一个B站UP主"龙哥"的AI智能回复助手。
    请根据提供的视频内容,生成一段作品总结。
    要求:
    1. 用第三人称客观总结作品核心内容
    2. 包含作品链接引导
    3. 语气风格符合以下人设:{persona_style}
    4. 字数控制在1000字以内
    '''

    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": f"请总结:\n\n{source_text}"}
    ]
    return _call_deepseek(api_key, messages, temperature=0.8)

Prompt工程: 给AI的「指令」很关键。这里告诉AI:

  • 你是谁(UP主龙哥的助手)
  • 你要做什么(总结作品)
  • 你的风格(按人设)
  • 有什么限制(1000字以内)

5.4 按人设生成回复

def generate_persona_reply(api_key, user_message, persona_name, persona_desc):
    """用AI按人设生成一条回复"""
    system_prompt = f'''你是一个B站UP主"龙哥"的AI智能回复助手。
    你的回复风格人设是:
    【名称】{persona_name} 【描述】{persona_desc}
    要求:
    1. 回复要符合上述人设风格
    2. 必须针对用户评论具体内容回应
    3. 语气自然,有互动感
    4. 字数控制在50字以内
    '''
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": f"用户评论:{user_message}\n\n请回复:"}
    ]
    return _call_deepseek(api_key, messages, temperature=0.9, max_tokens=200)

5.5 测试API连接

def test_api_connection(api_key):
    """测试API连接是否正常"""
    if not api_key or len(api_key) < 10:
        return {"success": False, "message": "API Key 无效"}
    try:
        start = time.time()
        result = _call_deepseek(api_key, [
            {"role": "user", "content": "回复连接测试四个字"}
        ], temperature=0, max_tokens=20)
        elapsed = int((time.time() - start) * 1000)
        if result:
            return {"success": True, "message": f"连接成功!延迟{elapsed}ms"}
        return {"success": False, "message": "API返回为空"}
    except Exception as e:
        return {"success": False, "message": str(e)}

6. 人设管理系统 (core/persona_manager.py)

6.1 什么是人设?

人设就是回复的语气风格。同一个意思用不同方式说出来:

风格效果
沙雕风哈哈哈哈这波操作太下饭了!
傲娇风才、才不是在蹲你直播呢!
治愈风慢慢来,你已经很棒了~
佛系风放平心态,时间会给你答案。

6.2 8种预置人设

编号名称描述
1落井下石前女友风互损督促学习
2幽默活泼沙雕风欢乐整活
3傲娇二次元风嘴硬心软
4文艺清新治愈风诗意疗愈
5抽象整活乐子风网络热梗
6萌系黏人治愈风软萌可爱
7沉稳成熟御系风温暖导师
8冷淡佛系松弛风佛系不摆烂
9自定义人设你来定义

每种人设都预置了20条回复话术,存储在 web/static/personas.json

6.3 核心代码

import json, random, threading
from datetime import date
from config import PERSONAS_FILE

class PersonaManager:
    """人设管理器:自动轮换、手动覆盖、自定义编辑"""

    def __init__(self, config_manager):
        self._config = config_manager
        self._lock = threading.Lock()
        self._personas = []
        self._load_personas()

    def _load_personas(self):
        """从配置文件加载人设"""
        if PERSONAS_FILE.exists():
            with open(PERSONAS_FILE, "r", encoding="utf-8") as f:
                self._personas = json.load(f)
        else:
            self._personas = self._default_personas()
            self._save_personas()

6.4 人设轮换机制

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

def get_current_persona(self):
    """获取当前生效的人设"""
    today = date.today().isoformat()
    manual_date = self._config.get("manual_persona_date")
    manual_id = self._config.get("current_persona_id")

    # 如果今天手动选择过人设,用选中的
    if manual_date == today and manual_id:
        p = self.get_persona(manual_id)
        if p:
            return p

    # 自动轮换:按一年中的第几天计算
    count = self.persona_count()
    if count == 0:
        return self._default_personas()[0]
    day_of_year = date.today().timetuple().tm_yday
    auto_id = ((day_of_year - 1) % count) + 1
    return self.get_persona(auto_id) or self._personas[0]

自动轮换原理:

  1. 获取今天是今年的第几天(1月1日=1,12月31日=365)
  2. 用这个数字取模人设数量:(day-1) % 总人数 + 1
  3. 结果:365天循环一遍,每天自动切换一个人设

6.5 手动覆盖

def set_manual_persona(self, persona_id):
    """手动选择当天人设"""
    today = date.today().isoformat()
    self._config.set_multi({
        "current_persona_id": persona_id,
        "manual_persona_date": today
    })

手动选择后记录日期。当天内保持手动人设,次日自动恢复轮换。

6.6 更新人设

def update_persona(self, persona_id, data):
    """更新一个人设(名称、描述、回复列表等)"""
    with self._lock:
        for i, p in enumerate(self._personas):
            if p["id"] == persona_id:
                p.update(data)
                self._personas[i] = p
                self._save_personas()
                return True
        return False

6.7 随机回复

def get_random_reply(self, persona_id=None):
    """从预置回复里随机选一条"""
    p = self.get_persona(persona_id) if persona_id else self.get_current_persona()
    if p and p.get("replies"):
        return random.choice(p["replies"])
    return "谢谢你的评论~"

没有API Key时,程序从预置话术中随机选一条回复。每套人设20条。


7. 消息处理流水线 (core/processor.py)

这是整个项目最核心的文件——消息处理器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

处理流程:

收到@消息
    |
解析消息(谁@的?说了什么?在哪个视频下?)
    |
判断是不是粉丝(查对方关注列表,5分钟缓存)
    |      |
非粉丝->忽略    粉丝->检测关键词
                    |        |
            包含"总结"-> AI总结流程
            不包含    -> 普通回复流程

7.1 构造函数

class MessageProcessor:
    def __init__(self, config_mgr, account_mgr, persona_mgr, log_mgr):
        self.config = config_mgr    # 配置管理器
        self.account = account_mgr  # 账户管理器
        self.persona = persona_mgr  # 人设管理器
        self.log = log_mgr          # 日志管理器
        self._processed_ids = set() # 已处理消息ID去重
        self._fan_cache = {}         # 粉丝缓存

    def cleanup_cache(self):
        """清理超过5分钟的粉丝缓存"""
        now = time.time()
        expired = [uid for uid, (_, ts) in self._fan_cache.items()
                   if now - ts > 300]
        for uid in expired:
            del self._fan_cache[uid]

构造函数接收4个管理器(依赖注入)。cleanup_cache 每轮轮询后调用一次。

7.2 消息解析

def process_at_message(self, at_item):
    """处理一条@消息"""
    msg_id = at_item.get("id")
    if msg_id in self._processed_ids:
        return {"action": "ignored", "reason": "已处理过"}
    self._processed_ids.add(msg_id)

    cookies = self.account.get_cookies()
    if not cookies:
        return {"action": "failed", "reason": "未登录"}

    # 从@消息中提取关键信息
    user_info = at_item.get("user", {})
    item_obj = at_item.get("item", {})
    uid = user_info.get("mid", 0)
    uname = user_info.get("nickname", "未知用户")
    raw_text = item_obj.get("source_content", "")
    if not raw_text:
        raw_text = at_item.get("content", {}).get("message", "")

    # 获取视频/动态ID
    oid = item_obj.get("subject_id", 0)
    if not oid or not str(oid).isdigit():
        oid = item_obj.get("source_id", 0)
    try: oid = int(oid)
    except: oid = 0

    type_id = item_obj.get("business_id", 1)
    return self._route_message(cookies, uid, uname, oid, type_id,
                               raw_text, at_item)

解析的5个关键字段:

  1. uid:用户B站UID
  2. uname:用户名
  3. raw_text:评论内容
  4. oid:作品ID
  5. type_id:类型(1=视频, 12=动态)

7.3 路由分发

def _route_message(self, cookies, uid, uname, oid, type_id, raw_text, at_item):
    # 1. 粉丝判断
    if not self._check_is_fan(cookies, uid):
        log_msg = {"t": time.strftime("%H:%M:%S"),
                   "u": uname, "m": raw_text[:50],
                   "s": "已忽略(非粉丝)", "type": "info"}
        self.log.add(log_msg)
        return {"action": "ignored", "reason": "非粉丝"}

    # 2. 关键词检测
    if self._is_summary_keyword(raw_text):
        return self._handle_summary_request(...)

    # 3. 普通回复
    return self._handle_normal_reply(...)

7.4 粉丝缓存

def _check_is_fan(self, cookies, uid):
    """检查是否是粉丝,带5分钟缓存"""
    now = time.time()
    if uid in self._fan_cache:
        is_fan, ts = self._fan_cache[uid]
        if now - ts < 300:  # 5分钟内不重复查
            return is_fan
    try:
        is_fan = check_follow(cookies, uid)
    except:
        is_fan = False
    self._fan_cache[uid] = (is_fan, now)
    return is_fan

缓存设计:同用户5分钟内多次@,只查一次粉丝状态,减少B站API调用。

7.5 关键词检测

_SUMMARY_KEYWORDS = [
    "总结", "总结作品", "总结视频", "总结一下",
    "私信我", "私聊我"
]

def _is_summary_keyword(self, text):
    if not self.config.get("keyword_enabled", True):
        return False
    for kw in self._SUMMARY_KEYWORDS:
        if kw in text:
            return True
    return False

7.6 普通回复

def _handle_normal_reply(self, cookies, uid, uname, oid,
                          type_id, raw_text, comment_rpid):
    api_key = self.config.get("api_key")
    p = self.persona.get_current_persona()

    # 有API Key -> AI生成
    reply_text = ""
    if api_key:
        try:
            reply_text = generate_persona_reply(
                api_key, raw_text,
                p.get("name", "默认"),
                p.get("desc", ""))
        except:
            pass

    # 失败或没Key -> 随机话术
    if not reply_text:
        reply_text = self.persona.get_random_reply()

    # 发评论回复
    try:
        result = reply_comment(
            cookies, oid, reply_text, type_id,
            root=comment_rpid, parent=comment_rpid)
        if result.get("code") == 0:
            self.log.add({"type": "success", "s": "回复成功"})
            return {"action": "replied"}
    except:
        pass
    self.log.add({"type": "fail", "s": "回复失败"})
    return {"action": "failed"}

7.7 AI总结流程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

def _handle_summary_request(self, cookies, uid, uname, oid,
                              type_id, raw_text, at_item, comment_rpid):
    api_key = self.config.get("api_key")
    p = self.persona.get_current_persona()

    # Step 1: 从@消息提取BV号
    item_uri = at_item.get("item", {}).get("uri", "")
    bvid = ""
    if "BV" in item_uri:
        import re
        bv_match = re.search(r"BV[a-zA-Z0-9]+", item_uri)
        if bv_match:
            bvid = bv_match.group()

    # Step 2: 获取视频内容(字幕)
    source_text = ""
    if type_id == 1:  # 视频
        video_info = get_video_info(cookies, bvid)
        sub_text = get_video_subtitle(cookies, bvid)
        if sub_text:
            title = video_info.get("title", "")
            source_text = f"标题:{title} 内容:{sub_text}"

    # Step 3: AI生成总结(重试1次)
    summary_text = ""
    for attempt in range(2):
        try:
            style = f"名称:{p.get(chr(39)+chr(39)+name+chr(39)+chr(39))}"
            summary_text = generate_summary(api_key, source_text, style)
            if summary_text: break
        except:
            if attempt == 0: time.sleep(1)

    # Step 4: 私信发送 + 公开提示
    if summary_text:
        link = "https://www.bilibili.com/video/" + bvid
        dm = send_private_msg(cookies, uid, "作品总结:" + summary_text)
        if dm.get("code") == 0:
            hint = random.choice(p.get("replies", ["已私信你啦~"]))
            reply_comment(cookies, oid, hint, type_id)
            return {"action": "summarized"}

    # 失败处理
    reply_comment(cookies, oid, "抱歉,总结遇到点问题~", type_id)
    return {"action": "failed"}

AI总结4个步骤:

  1. 提取BV号:从@消息的URI中提取视频ID
  2. 获取视频内容:通过B站API获取字幕/标题/简介
  3. AI总结:调用DeepSeek分析内容生成总结(最多重试1次)
  4. 私信发送:总结通过私信发送,评论区回复提示

8. 后台轮询器 (core/poller.py)

8.1 什么是线程?

线程让程序同时做多件事。就像你一边听歌一边写作业:

  • 主线程:Flask Web服务器(提供网页服务)
  • 后台线程:Poller(定时检查B站消息)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

8.2 完整代码

import time, random, threading
from core.bilibili_api import get_notifications
from core.processor import MessageProcessor

class Poller:
    """后台轮询器"""

    def __init__(self, config_mgr, account_mgr, processor, log_mgr):
        self.config = config_mgr
        self.account = account_mgr
        self.processor = processor
        self.log = log_mgr
        self._running = False
        self._thread = None
        self._last_at_time = 0        # 最后处理的消息时间戳
        self._processed_ids = set()    # 已处理消息ID
        self._stats = {
            "start_time": None,
            "total_processed": 0,    # 轮询总次数
            "total_replied": 0,      # 已回复数
            "total_summarized": 0,   # 已总结数
            "total_failed": 0,       # 失败数
            "total_ignored": 0       # 忽略数
        }

8.3 启动与停止

def start(self):
    """启动轮询线程"""
    if self._running:
        return False
    self._running = True
    self._stats["start_time"] = time.time()

    # 从配置文件恢复上次处理时间戳
    saved = self.config.get("last_at_time", 0)
    if saved > 0:
        self._last_at_time = saved  # 重启:处理关机期间来的消息
    else:
        self._last_at_time = time.time()  # 首次:只处理启动后的消息

    # 创建守护线程(主程序退出时自动结束)
    self._thread = threading.Thread(
        target=self._run, daemon=True, name="poller"
    )
    self._thread.start()
    return True

def stop(self):
    """停止轮询"""
    self._running = False

daemon=True 表示守护线程——主程序退出时自动结束,不会残留。

8.4 主循环

def _run(self):
    """轮询主循环"""
    while self._running:
        sleep_time = 10
        try:
            cookies = self.account.get_cookies()
            if not cookies:
                time.sleep(5)
                continue

            # 随机抖动:8-12秒不等,更像真人
            interval = self.config.get("poll_interval", 10)
            jitter = random.uniform(-2, 2)
            sleep_time = max(5, interval + jitter)

            # 获取@消息
            result = get_notifications(cookies)
            if result.get("code") == 0:
                items = result.get("data", {}).get("items", [])
                self._stats["total_processed"] += 1

                if items:
                    # 记录本轮最高时间戳
                    batch_max = max(it.get("at_time", 0) for it in items)
                    for item in items:
                        # 跳过已处理过的旧消息
                        if item.get("at_time", 0) <= self._last_at_time:
                            continue
                        # 交给消息处理器
                        proc_result = self.processor.process_at_message(item)
                        action = proc_result.get("action", "")
                        if action == "replied":
                            self._stats["total_replied"] += 1
                        elif action == "summarized":
                            self._stats["total_summarized"] += 1
                        elif action == "failed":
                            self._stats["total_failed"] += 1
                        elif action == "ignored":
                            self._stats["total_ignored"] += 1

                    # 更新最后处理时间并持久化
                    if batch_max > self._last_at_time:
                        self._last_at_time = batch_max
                        self.config.set("last_at_time", batch_max)

            elif result.get("code") == -101:
                # -101 = 登录失效
                self.log.add({"m": "B站登录状态已失效",
                             "s": "请重新登录", "type": "fail"})
                self._running = False
                continue

            # 清理缓存
            self.processor.cleanup_cache()

        except Exception as e:
            self.log.add({"m": f"轮询异常: {str(e)}",
                         "s": "自动恢复中", "type": "fail"})

        time.sleep(sleep_time)

8.5 5个关键设计

1. 随机抖动(防风控)

jitter = random.uniform(-2, 2)
sleep_time = max(5, interval + jitter)

间隔不是固定的10秒,而是8-12秒随机,不像机器人在访问。

2. 时间戳去重
程序记住最后处理的消息时间戳,下次只处理更新的。
重启程序也能从 config.json 恢复进度。

3. 登录失效检测
B站API返回code=-101表示Cookie过期,自动停止轮询。

4. 异常捕获
即使网络出错也不会崩溃,记录日志后继续运行。

5. 统计信息收集

@property
def stats(self):
    """获取统计数据"""
    s = dict(self._stats)
    if s["start_time"]:
        s["uptime"] = int(time.time() - s["start_time"])
    return s

Web页面实时显示:轮询次数、已回复、已总结、失败数、运行时长。


9. 数据持久化 (storage层)

9.1 JSON vs 数据库

本项目使用JSON文件存储数据,而不是数据库:

对比JSON文件数据库
优点简单、文件可读、秒级启动性能好、支持复杂查询
缺点数据量大时慢需要安装服务、配置复杂
适用小项目、配置、日志大数据、多人并发

9.2 ConfigManager — 配置管理器

import json, threading
from config import CONFIG_FILE, DEFAULT_CONFIG

class ConfigManager:
    """JSON持久化配置管理器,线程安全"""

    def __init__(self):
        self._lock = threading.Lock()       # 线程安全锁
        self._config = dict(DEFAULT_CONFIG)  # 用默认配置初始化
        self._load()                          # 从文件加载配置

    def _load(self):
        """从config.json加载配置"""
        if CONFIG_FILE.exists():
            try:
                with open(CONFIG_FILE, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    for k in DEFAULT_CONFIG:
                        if k in data:
                            self._config[k] = data[k]
            except (json.JSONDecodeError, IOError):
                pass

    def _save(self):
        """保存配置到文件"""
        with open(CONFIG_FILE, "w", encoding="utf-8") as f:
            json.dump(self._config, f, ensure_ascii=False, indent=2)

    def get(self, key, default=None):
        with self._lock:
            return self._config.get(key, default)

    def set(self, key, value):
        with self._lock:
            self._config[key] = value
            self._save()  # 每次修改立即写入文件

    def set_multi(self, mapping: dict):
        """批量更新配置"""
        with self._lock:
            self._config.update(mapping)
            self._save()

为什么用Lock?
因为Poller线程和Web线程可能同时读写配置(并发操作),Lock保证同一时刻只有一个线程操作数据。

每次set就立即_save — 即使程序崩溃也不会丢失最新配置。

9.3 AccountManager — 账户管理器

class AccountManager:
    """B站登录状态管理"""

    def __init__(self):
        self._lock = threading.Lock()
        self._cookies = None
        self._login_info = {}
        self._load()

    def _load(self):
        """从cookies.json加载"""
        if COOKIES_FILE.exists():
            try:
                with open(COOKIES_FILE, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    self._cookies = data.get("cookies")
                    self._login_info = data.get("login_info", {})
            except:
                pass

    def save_cookies(self, cookies, login_info=None):
        """保存B站登录凭证"""
        with self._lock:
            self._cookies = cookies
            if login_info:
                self._login_info = login_info
            self._save()

    def get_cookies(self):
        with self._lock:
            return dict(self._cookies) if self._cookies else None

    def is_logged_in(self):
        return self._cookies is not None

    def clear(self):
        """退出登录,清除Cookie"""
        with self._lock:
            self._cookies = None
            self._login_info = {}
            if COOKIES_FILE.exists():
                COOKIES_FILE.unlink()  # 删除文件

cookies.json结构:

{
  "cookies": {
    "bili_jct": "xxx",    // CSRF令牌(评论/私信用)
    "SESSDATA": "xxx",    // 会话令牌(登录凭证)
    "DedeUserID": "xxx"   // 用户ID
  },
  "login_info": {
    "uname": "龙哥",
    "uid": 123456,
    "face": "https://..."
  }
}

3个关键的Cookie字段:

  • bili_jct:安全令牌,发评论和私信时必带
  • SESSDATA:登录凭证,证明你已经登录
  • DedeUserID:你的B站用户ID

9.4 LogManager — 日志管理器

class LogManager:
    """日志管理器,最多保留500条"""

    def __init__(self):
        self._lock = threading.Lock()
        self._logs = []
        self._load()

    def add(self, log_entry):
        """添加一条日志"""
        with self._lock:
            self._logs.append(log_entry)
            # 超过500条时丢弃最老的
            if len(self._logs) > MAX_LOG_ENTRIES:
                self._logs = self._logs[-MAX_LOG_ENTRIES:]
            self._save()

    def get_recent(self, count=50):
        """获取最近N条日志"""
        with self._lock:
            return list(self._logs[-count:])

每条日志包含5个字段:

{
  "t": "14:30:25",     // 时间
  "u": "程序员小张",    // 用户名
  "m": "龙哥这代码报错了", // 消息内容
  "s": "回复成功",      // 状态
  "type": "success"    // 类型: success/fail/info
}

环形缓冲区设计: 最多500条,超过自动丢弃最老的。不会无限占用硬盘。


10. Web管理界面 (web/server.py)

10.1 Flask是什么?

Flask是Python最流行的Web框架之一。用Python写网页接口的工具。
你把Python代码写好,Flask把它变成URL接口,浏览器可以访问。

10.2 全局初始化

from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS

# 创建所有管理器实例
config_mgr = ConfigManager()
account_mgr = AccountManager()
log_mgr = LogManager()
persona_mgr = PersonaManager(config_mgr)
processor = MessageProcessor(config_mgr, account_mgr, persona_mgr, log_mgr)
poller = Poller(config_mgr, account_mgr, processor, log_mgr)

# Flask应用
app = Flask(__name__, static_folder=None)
CORS(app)  # 允许跨域请求

程序启动时创建所有管理器,这就是「依赖注入」——每个对象需要的依赖都在创建时传入。

10.3 二维码登录的Web实现

_qrcode_data = {"key": None, "status": "idle", "url": None}

@app.route("/api/login/qrcode", methods=["POST"])
def api_login_qrcode():
    """获取B站登录二维码"""
    global _qrcode_data
    try:
        result = get_qrcode()
        if result.get("code") == 0:
            data = result.get("data", {})
            _qrcode_data = {
                "key": data.get("qrcode_key"),
                "url": data.get("url"),
                "status": "scanning"
            }
            return jsonify({"success": True,
                           "url": data.get("url"),
                           "key": data.get("qrcode_key")})
        return jsonify({"success": False,
                        "message": result.get("message", "获取二维码失败")})
    except Exception as e:
        return jsonify({"success": False, "message": str(e)})


@app.route("/api/login/poll", methods=["POST"])
def api_login_poll():
    """轮询扫码结果,保存cookies"""
    global _qrcode_data
    key = _qrcode_data.get("key")
    if not key:
        return jsonify({"success": False, "status": "idle"})

    result = poll_qrcode(key)
    data = result.get("json", {})
    cookies = result.get("cookies", {})
    code = data.get("code", -1)

    if code == 0:
        status = data.get("data", {}).get("code", 0)
        if status == 0:
            # 扫码成功,保存cookies和用户信息
            if cookies:
                nav = get_nav_info(cookies)
                if nav.get("code") == 0:
                    nd = nav.get("data", {})
                    login_info = {
                        "uname": nd.get("uname", ""),
                        "uid": nd.get("mid", 0),
                        "face": nd.get("face", "")
                    }
                    account_mgr.save_cookies(cookies, login_info)
                _qrcode_data["status"] = "done"
                return jsonify({"success": True, "status": "done",
                                "login_info": login_info})
        elif status == 86038:
            _qrcode_data["status"] = "expired"
            return jsonify({"success": False, "status": "expired"})

    return jsonify({"success": False, "status": "scanning"})

10.4 轮询控制API

@app.route("/api/poller/start", methods=["POST"])
def api_poller_start():
    """启动轮询"""
    if not account_mgr.is_logged_in():
        return jsonify({"success": False,
                        "message": "请先登录B站"})
    ok = poller.start()
    config_mgr.set("poller_running", True)
    return jsonify({"success": ok, "running": poller.is_running})


@app.route("/api/poller/stop", methods=["POST"])
def api_poller_stop():
    """停止轮询"""
    poller.stop()
    config_mgr.set("poller_running", False)
    return jsonify({"success": True, "running": False})

10.5 API路由一览

方法路径功能
GET/api/status运行状态(轮询/登录/人设)
GET/api/stats统计数据
POST/api/login/qrcode获取B站二维码
POST/api/login/poll轮询扫码结果
GET/api/personas获取所有人设
GET/api/persona/current获取当前人设
POST/api/persona/select选择当天人设
POST/api/persona/update更新人设
POST/api/persona/reorder重新排序人设
GET/api/config获取配置
POST/api/config/save保存配置
POST/api/api_test测试API连接
POST/api/poller/start启动轮询
POST/api/poller/stop停止轮询
GET/api/logs获取日志
POST/api/logs/clear清空日志

10.6 启动服务器

def run_server():
    """启动Flask服务器"""
    # 如果之前轮询器在运行且已登录,自动恢复
    if config_mgr.get("poller_running") and account_mgr.is_logged_in():
        try:
            poller.start()
        except Exception:
            pass
    print(f"Web管理页面: http://{WEB_HOST}:{WEB_PORT}")
    app.run(host=WEB_HOST, port=WEB_PORT,
            debug=False, threaded=True)

threaded=True 允许Flask同时处理多个请求,不会卡住。

自动恢复: 如果上次关闭时轮询器在运行,再次启动时自动恢复。

10.7 前端架构

前端文件在 web/static/ 目录下:

文件作用
index.html管理页面HTML,包含仪表盘/设置/人设/日志等页面
api-layer.js前端与后端通信的封装层
personas.json人设数据
logs.json日志数据

前后端通信方式:前端使用浏览器的fetch API调用后端API,后端返回JSON数据。
前端负责展示,后端负责处理业务逻辑。


11. 从启动到运行全流程串联

11.1 完整操作流程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第一步:启动程序

`ash
cd BiliReplyWeb
python app.py


输出:

==============================================
BiliReplyBot - Bilibili Smart Reply Assistant
Web UI: http://127.0.0.1:8005
Press Ctrl+C to stop


**第二步:打开Web管理页面**
浏览器输入 http://127.0.0.1:8005

你会看到仪表盘页面,显示:
- 轮询状态(未启动)
- 登录状态(未登录)
- 统计信息(全为0)
- 当前人设(默认)

**第三步:设置API Key**
1. 点击左侧「设置」
2. 填写你的DeepSeek API Key
3. 点击「测试连接」
4. 显示「连接成功!延迟xxms」即可

**第四步:B站扫码登录**
1. 点击左侧「登录」
2. 点击「获取二维码」
3. 打开手机B站App,扫描二维码
4. 页面自动检测登录状态
5. 登录成功显示你的B站用户名和头像

**第五步:启动轮询**
1. 回到仪表盘
2. 点击「启动轮询」按钮
3. 状态变为「运行中」

**第六步:测试自动回复**
用粉丝账号在你任意视频/动态下评论(记得@你):

> @你的用户名 今天讲得太好了!

等待10-15秒,刷新Web页面查看日志——应该显示「回复成功」。
去B站看你的视频评论区,已有一条自动回复。

**第七步:测试AI总结**
用粉丝账号评论:

> @你的用户名 总结作品,私信我

等待20-30秒(AI生成需要时间),检查B站私信,应该收到AI总结。

### 11.2 程序启动背后发生了什么

运行 python app.py 时:

  1. Python 加载 app.py
  2. app.py import web.server -> 触发 server.py 执行
  3. server.py 创建所有管理器
    • config_mgr (从data/config.json加载配置)
    • account_mgr (从data/cookies.json加载登录凭证)
    • log_mgr (从data/logs.json加载日志)
    • persona_mgr (从web/static/personas.json加载人设)
    • processor, poller
  4. run_server() 调用 Flask 的 app.run()
  5. Flask 开始在 127.0.0.1:8005 监听HTTP请求
  6. 如果之前轮询器在运行且已登录,自动启动轮询

### 11.3 用户使用场景

**场景1:日常运营**
- 早上打开Web页面查看昨晚回复了多少条
- 进入日志页面看有哪些粉丝互动
- 进入人设管理换一种风格

**场景2:处理问题**
- 状态变红时查看日志排查问题
- Cookie过期时重新登录
- 重启程序后自动恢复轮询

---


## 12. 常见问题与排错指南

### 12.1 端口被占用

症状:启动时报错「Address already in use」

解决:
`ash
# 查看谁占用了8005端口
netstat -ano | findstr :8005
# 强制结束进程
taskkill /F /PID 进程ID

或者修改 config.py 中的 WEB_PORT 为其他数字(如8080)。

12.2 API Key无效

检查项:

  1. 是否完整复制(没有漏字符)
  2. 前后有没有多余空格
  3. DeepSeek账户余额是否充足
  4. 网络能否访问 api.deepseek.com
  5. 在Web页面重新填写并测试连接

12.3 扫码登录失败

  • 确保网络正常(能访问 passport.bilibili.com)
  • 重新获取二维码(有效期2分钟)
  • 更新B站App到最新版本
  • 如果一直失败,检查是否被B站风控

12.4 轮询启动后没有回复

逐一排查:

  1. 登录状态:Web页面显示已登录吗?
  2. 轮询状态:显示「运行中」吗?
  3. 自己测试:用粉丝号@自己了吗?
  4. 查看日志:日志里有记录吗?
  5. 粉丝判断:测试号是否关注了你?
  6. 直接看日志文件:data/logs.json

12.5 如何查看日志

两种方式:

  1. Web页面 → 左侧「日志」侧边栏
  2. 直接打开 data/logs.json 文件

日志各字段含义:

t=时间, u=用户名, m=消息内容, s=状态, type=类型
type=success -> 成功
type=fail    -> 失败
type=info    -> 信息提示

12.6 Cookie过期

症状:

  • 轮询停止
  • 日志显示「B站登录状态已失效」
  • code=-101

解决:在Web页面退出登录,重新扫码登录,重新启动轮询。
B站Cookie有效期通常为30天左右。

12.7 常见报错

ModuleNotFoundError:
`ash
pip install flask
pip install flask-cors
pip install requests


**ConnectionError:** 检查网络连接是否正常

**JSONDecodeError:** data/下的json文件可能损坏,删除相应文件重启

### 12.8 安全提醒

1. API Key 不要告诉别人——别人可以用你的key花钱
2. cookies.json 文件不要外传——别人可以登录你的B站账号
3. 只在信任的电脑运行——避免API Key泄漏
4. 不要大量刷评论——遵守B站用户协议
5. 如需分享代码,先删除 data/ 下的敏感文件

---


## 13. 附录

### 13.1 依赖清单

`	xt
flask>=3.0.0
flask-cors>=4.0.0
requests>=2.31.0

13.2 相关链接

链接用途
https://www.python.org/downloads/下载Python
https://platform.deepseek.com/DeepSeek API官网
https://www.bilibili.com/哔哩哔哩
https://pypi.tuna.tsinghua.edu.cn/simple清华pip镜像源

13.3 文件速查

文件备注
config.pyWEB_PORT、POLL_INTERVAL 可修改
web/static/personas.json可编辑人设列表
data/cookies.json自动管理,不要手动改
data/config.json运行时配置,自动管理
core/bilibili_api.pyB站接口变化时才需要动

13.4 常用命令速查

`ash

启动程序

python app.py

安装依赖

pip install -r requirements.txt

换源加速

pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

查看端口占用

netstat -ano | findstr :8005


### 13.5 文件大小参考

| 文件 | 代码行数 | 说明 |
|------|---------|------|
| app.py | 25行 | 程序入口 |
| config.py | 45行 | 配置集中管理 |
| bilibili_api.py | 180行 | B站API封装 |
| ai_service.py | 80行 | AI服务 |
| persona_manager.py | 350行 | 人设管理 |
| poller.py | 150行 | 轮询器 |
| processor.py | 320行 | 消息处理 |
| config_manager.py | 45行 | 配置读写 |
| account_manager.py | 50行 | 账户管理 |
| log_manager.py | 40行 | 日志管理 |
| server.py | 270行 | Web服务器 |
| index.html | 1300行 | 前端页面 |
| api-layer.js | 600行 | 前端通信 |

---

> 🎉 **恭喜你读完全部教程!**
>
> 现在你掌握了:
> - 项目的每个文件是做什么的
> - 每一行核心代码的含义
> - 如何从零开始部署和运行
>
> 下一步:打开命令行,运行 python app.py,亲身体验自动回复的乐趣吧!

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0b8d5f26027c42c7b05c333a25f8d356.png#pic_center)
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9a9fbaad986e46a8a5d5232585fbf997.png#pic_center)
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6d70b18787f24f2e84bb13869fc478a1.png#pic_center)
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/3929b291a48841dcb4e579d35f4a7ada.png#pic_center)
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/d390c70aa6d94c8ebf577bc0c8124208.png#pic_center)
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c693cf5744f24924a58882ae87820a15.png#pic_center)
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b96397191f7c41c0be09feda7cc733b6.png#pic_center)


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐