Skip to main content
Glama

qzone-mcp —— QQ 工具箱(空间 + 资料),我们的一个 MCP 插件

把上游两个 AstrBot 插件的能力合成一个插件,改成我们这套栈能直接调的形式:

上游

贡献的能力

Zhalslar/astrbot_plugin_qzone(MIT)

发/看/删/赞/评说说、好友动态、访客、可见范围、表白墙投稿审核、定时说说

Zhalslar/astrbot_plugin_qqprofile(MIT)

改头像 / 昵称 / 签名 / 在线状态、自定义状态、查资料、名片赞、好友分组

为什么不原样装:这两份都是 AstrBot 插件(star.Star + event.bot.*),AstrBot 已经不在我们的栈里了。 所以保留它们的接口语义(端点、参数、可见范围、审核流程、状态码表),换成本机现有认证与调用方式。

从零安装(换台机器 / 换个容器)

三步:放代码 → 配环境变量 → 让 DSH 拉起 MCP。

  1. 代码放到 /opt/qzone-mcp(server.py 是入口,wire.py 负责接线)。依赖:

pip3 install --break-system-packages aiohttp==3.14.5 edge-tts==7.3.1
  1. 环境变量(compose 的 environment: 或 systemd Environment=):

变量

必填

说明

QZONE_ONEBOT_URL

✅

OneBot HTTP,例如 http://snowluma:3000

QZONE_ONEBOT_TOKEN

✅

SnowLuma 的 HTTP token(不是 WS token,两者不同)

QZONE_DATA

建议

数据目录(wall.db 等),例如 /opt/qzone-data

QZONE_SNOWLUMA_SHARE

发图/头像需要

core 侧看到的共用目录,例如 /opt/snowluma-data/stickers

QZONE_SNOWLUMA_PREFIX

发图/头像需要

同一目录在 SnowLuma 容器里的路径,例如 /app/data/stickers

QZONE_IMG_PORT

可选

临时图 HTTP 端口,默认 8799

QZONE_IMG_URL_BASE

可选

临时图对外基址,默认容器 IP http://<容器IP>:8799

QZONE_TTS_VOICE

可选

默认音色,默认 zh-CN-XiaoyiNeural

QZONE_TTS_MAX_CHARS

可选

一次合成最大字数,默认 400

config.example.json 是可选覆盖(放 QZONE_DATA 或 /opt/qzone-mcp/config.json),只管 qzone.timeout / cookieTtl / defaultUgcRight、表白墙、定时说说这些。

  1. 接线(让 DSH 把它当 MCP 服务器拉起来):

python3 /opt/qzone-mcp/wire.py     # 幂等;写 $DSH_HOME/profiles/<profile>/cordis.patch.yml 的 mcp-qzone 段
docker restart core                # profile 层要重启才重读

wire.py 写进去的行长这样(serverName: qzone → 工具名就是 mcp__qzone__*):

- insert:
    - id: mcp-qzone
      name: '@deepseek-ai/dsh-mcp-client'
      config:
        serverName: qzone
        command: /usr/bin/python3
        args: ['/opt/qzone-mcp/server.py']
        toolCallTimeoutMs: 120000
  1. QQ 会话里要能用,还得放行工具前缀:qq-tool-restrict 的 SAFE_PREFIXES 里要有 'mcp__qzone__';mcp__qzone__* 默认只给管理员(按 qq-bridge 的 owner 会话判定)。

自检:

printf '%s\n%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  | python3 /opt/qzone-mcp/server.py | head -c 400

Related MCP server: qzone-mcp

它长在哪

QQ ──► SnowLuma(注入 QQ 的 OneBot 运行时)
          │  HTTP 3000
          │    · 空间:get_cookies → p_skey → 自算 g_tk → 空间网页版 CGI
          │    · 资料:set_qq_avatar / set_qq_profile / set_self_longnick /
          │            set_online_status / set_diy_online_status / get_stranger_info …
          ▼
   core 容器里的 qzone-mcp(stdio MCP server,DSH 拉起)
          │  工具调用
          ▼
      DSH agent(qq-bridge 转来的 QQ 消息 → 挑工具 → 回消息)
  • 运行:/opt/qzone-mcp/server.py,MCP over stdio(JSON-RPC 2.0,按行),只依赖标准库 + aiohttp。

  • 注册:/opt/qzone-mcp/wire.py 往 /opt/dsh-home/profiles/web/cordis.patch.yml 幂等追加一条 mcp-qzone,entrypoint 每次启动跑一遍。

  • 文件:server.py(协议 + 工具注册)、qzone_api.py(空间 CGI)、profile_api.py(资料动作 + 状态码表)、msg_api.py(往 QQ 发图片消息)、img_bridge.py(取图 + 临时 HTTP + core↔SnowLuma 共用目录)、bridge_admin.py(改 qq-bridge 白名单)、wall.py(表白墙 sqlite)、scheduler.py(定时说说)、wire.py(profile 接线)。

工具(38 个)

空间

工具

作用

qzone_publish

发说说(ugc_right 1 公开 / 4 好友 / 64 仅自己,images 支持 URL 或本地路径)

qzone_list

看说说列表,target_uin 看别人的,序号 0 = 最新

qzone_detail

单条说说完整内容(评论、图片、转发)

qzone_delete

删自己的说说(走 SnowLuma 原生 delete_qzone_msg,CGI 那个端点会返回 HTML 跳转页)

qzone_like / qzone_unlike

点赞 / 取消赞

qzone_comment / qzone_reply

评论 / 楼中楼回复

qzone_feeds

好友动态流

qzone_visitors

最近访客

qzone_profile

空间名 / 简介

qzone_set_right

改已发说说的可见范围

wall_submit / wall_list / wall_show

表白墙投稿、看稿、看详情

wall_approve / wall_reject / wall_withdraw

过稿(发到空间)、拒稿、撤稿

qzone_schedule_get / qzone_schedule_set

定时说说(时间点 + ±随机浮动 + 候选文案)

资料

工具

作用

profile_get

机器人自己的资料:QQ、昵称、连接状态、性别、年龄、等级

profile_set_nickname

改昵称(可选 personal_note)

profile_set_signature

改个性签名(set_self_longnick)

profile_set_avatar

改头像(图片 URL 或容器内文件路径)

profile_set_status

改在线状态,认官方名也认白话(见下)

profile_set_diy_status

自定义状态(face_id + 文字)

profile_list_statuses

列出全部支持的官方状态名

profile_stranger

查任意 QQ 的资料(昵称/性别/年龄/等级/签名)

profile_likes

名片赞:谁赞过、总次数、今日新增

profile_friends

好友分组列表(含备注)

状态白话归一:profile_set_status 先精确匹配 status.py 的官方名(43 个),再走别名表, 最后做包含匹配。举例:忙→忙碌、挂机→离开、睡了→睡觉中、在摸鱼→摸鱼中、 coding→搬砖中、好累→被掏空、开心→元气满满、emo→emo中、没电→我的电量。 也接受纯数字(1300 → 摸鱼中)。

没搬的东西:上游的「切换人格 / 人格列表」依赖 AstrBot 的 conversation_manager + persona_manager,我们这套栈里没有对应概念(人设由 DSH 的 agent preset + feeling 插件承担),所以不做。

发图

工具

作用

qq_send_image

把一张图当图片消息发到 QQ(私聊或群)。image 支持公网 URL、base64://、data:image/…、本容器内文件路径;target 填 QQ 号或群号;kind 默认 private

这条是 v1.5.0 加的,解决的是「本地生成的图发不进 QQ」:收藏表情只能走 qq_send_sticker (发的是表情包收藏里的东西),而 meme_render / 自己用 PIL 画的图原先只落在容器里、 控制台能看、QQ 发不出去。

// 发 meme_render 刚搓出来的图
{"name": "qq_send_image", "arguments": {
  "image": "/opt/dsh-home/nekowhale-stickers/renders/a0985f7f45247229.gif",
  "target": "123456789", "kind": "private", "text": "喏,本鱼自己搓的"
}}
// → 已把图片发到私聊 123456789:发送成功(797.1 KB image/gif,走共用目录,message_id=2105297674,前面带了文字,回读确认有图片段)

实现要点:

  1. img_bridge.load(image) 统一把 URL / base64 / data URL / 容器内路径读成字节;

  2. 优先 img_bridge.to_snowluma() 写到 core↔SnowLuma 共用目录(宿主 /opt/snowluma/data/stickers, core 里 /opt/snowluma-data/stickers,snowluma 里 /app/data/stickers),把 snowluma 侧路径 交给 send_private_msg / send_group_msg 的 image 段;失败退 base64://;

  3. 发完用 get_msg 回读确认图片段真的在(返回里那句「回读确认有图片段」就是它)。

  4. v1.5.1 加的重试自愈:call_action 遇到 unauthorized / retcode 1401 会从 /opt/snowluma-data/config/onebot_*.json 重挑 HTTP token 再试一次;send_image 每条通道 各试 2 次(鉴权失败换 token 后 0.6s 重试,其它失败退避 0.8s)。失败信息会把三条尝试的 原话拼出来,不再是光秃秃一句 unauthorized。

入参可以是 dsh web 的路由地址:meme_render / sticker_send 返回的是 ![…](http://127.0.0.1:8080/x-nekowhale-stickers/r/<id>.gif) 这种 markdown(web profile 下 origin() 有值,插件就不给文件路径了)。这种地址可以原样填进 image —— img_bridge.load 会自己去 127.0.0.1:8080 把图取下来(实测 /r/ 814 KB、/s/ 1.90 MB 都能发)。

为什么不用别的路子:set_qq_avatar 对 base64:// 直接报 avatar file is empty;QQ 消息里自带的 图片地址带 rkey,SnowLuma 自己去拉会 401。发消息这条对三种写法都放行(实测 A 容器内路径、B base64://、 C file:///… 全部 retcode 0,回读都能看到 multimedia.nt.qq.com.cn 的下载地址)。

meme_render 的产物落在 /opt/dsh-home/nekowhale-stickers/renders/<hash>.gif,它返回的 Markdown 行里就带这个路径——直接把同一个路径喂给 qq_send_image 即可。

语音(让机器人开口说话,v1.6.0 起)

工具

作用

qq_speak

把文字念成 QQ 语音条发出去。text 要念的话(≤400 字)、target QQ 号/群号、kind private/group、voice 音色名、rate/volume 形如 +10%

qq_voice_list

列出常用中文音色名,不发消息

{"name": "qq_speak", "arguments": {
  "text": "我是大肥鱼,这条语音是本鱼自己说的,听到没。",
  "target": "123456789", "kind": "private"
}}
// → 已经用语音说了(28.3 KB,音色 zh-CN-XiaoyiNeural,message_id=1771999584,回读确认是语音条)

链路(和发图完全同构):

text → edge-tts(微软 Edge 朗读接口,免密钥,装 pip 包即可)合成 mp3
     → 写进 core↔SnowLuma 共用目录(img_bridge.to_snowluma(..., "audio/mpeg", ext=".mp3"))
     → OneBot send_private_msg / send_group_msg 的 {"type":"record","data":{"file":"/app/data/stickers/xxx.mp3"}}
     → SnowLuma 自己把 mp3 转 SILK 发出去 → get_msg 回读确认 record 段

为什么不直接用 qq-bridge 的 qq_send_voice:它只认 /opt/qq-bridge/audio/ 语音库里的现成文件 (allowAbsolutePath:false),而且 cfg.socialV2.tools.sendVoice 默认 false → 工具根本不注册, 还要 core 里有 ffmpeg/ffprobe 才能过音量归一化。我们要的是「现说现发」,所以绕开它。 snowluma 容器里有 ffmpeg 5.1.9,转码那一步由它做,core 不必装 ffmpeg(省 ~250 MB)。

音色默认 zh-CN-XiaoyiNeural(少女感女声,最像「大肥鱼」);东北话 zh-CN-liaoning-XiaobeiNeural、 陕西话 zh-CN-shaanxi-XiaoniNeural、粤语 zh-HK-HiuMaanNeural、台腔 zh-TW-HsiaoChenNeural 都有。 换默认音色:compose 里给 core 加 QZONE_TTS_VOICE=zh-CN-XiaoxiaoNeural。

已知边界:edge-tts 要出网访问 speech.platform.bing.com(本机已验证通,4.8 秒 mp3 用了约 1 秒); 合成失败会明确区分「出网不通/音色名不对」;单条上限 400 字(QZONE_TTS_MAX_CHARS 可调)。

配置

/opt/qzone-data/config.json(容器内),宿主上是 /opt/core/qzone-data/config.json:

{
  "onebot": { "httpUrl": "http://snowluma:3000", "accessToken": "<SnowLuma HTTP token>" },
  "qzone": { "timeout": 30, "cookieTtl": 1800, "defaultUgcRight": 1 },
  "wall": { "enabled": true, "name": "表白墙", "template": "【表白墙】\n{text}",
            "anonymousSuffix": "\n—— 匿名", "maxLength": 800 },
  "schedule": { "enabled": false, "times": ["09:30","14:30","21:00"],
                "offsetMinutes": 20, "ugcRight": 1, "texts": [] }
}
  • QZONE_ONEBOT_URL / QZONE_ONEBOT_TOKEN 覆盖 onebot 段。不要用 ONEBOT_TOKEN(那是 qqd 的 WS token)。

  • SnowLuma 的 HTTP(3000) 与 WS(3001) token 是两个不同的值,这里要 HTTP 那个。

  • 表白墙数据:/opt/qzone-data/wall.db(sqlite)。

  • 定时说说只在 MCP 进程活着时生效;qzone_schedule_set 会即时重载,不用重启。

运维

# 改了插件代码必须重建(COPY 进镜像的,compose up -d 不重建)
cd /opt/core && docker compose up -d --build

# 看 MCP 是否被 DSH 拉起来
docker logs core 2>&1 | grep -i qzone

# 协议自检
printf '%s\n' \
 '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
 '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
 | docker exec -i core python3 /opt/qzone-mcp/server.py

# 直接调一个工具
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
 '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"profile_get","arguments":{}}}' \
 | docker exec -i core python3 /opt/qzone-mcp/server.py

已知边界

  • get_status 只回 {online, good, time},拿不到当前 status/ext_status,所以 profile_get 只报「在线/离线」。 改完状态要确认效果,看 QQ 客户端上的机器人名片。

  • get_unidirectional_friend_list 在 SnowLuma 这版会抛 protobuf invalid wire type 6(SnowLuma 侧 bug), 所以这个工具没做,别去加。

  • profile_likes 里 QQ 常常不下发昵称(nick 为空),只能显示 QQ 号与次数。

  • profile_set_avatar 已实现但未实机验证(换头像对用户可见,等你自己点头再动)。

  • 点赞接口上游注明「接口显示成功但实测点赞无效」;这里沿用同一端点(http:// 的 unikey 才记赞)。

  • 图片上传(up.qzone.qq.com)上游标为「较脆弱」,失败会报错而不是静默。

  • 访客接口只返回 items,字段随 QQ 版本变化时可能少名字;动态流深翻页需时间游标,这里只取首页。

白名单管理(admin_* 五个工具,v1.2.0 起)

为什么要这组工具:qq-bridge 的白名单闸门在「消息进 agent 之前」,所以以前想加人只能登录服务器改 config.json 再重启容器 —— 你在 QQ 里跟机器人说「把谁加白」是没人接的。

现在怎么做:直接说人话,agent 会调工具。

你说

agent 调

效果

「看下白名单」

admin_whitelist_show

列出模式 / 管理员 / 私聊白名单 / 群白名单

「把 123456 加进白名单」

admin_whitelist_add {kind:private, ids:["123456"]}

那个人能私聊机器人

「把群 987654321 加白」

admin_whitelist_add {kind:group, ids:["987654321"]}

那个群能 @ 机器人

「把 123456 删掉」

admin_whitelist_remove

立即不再响应

「白名单只留 123456789」

admin_whitelist_set

整份替换

「管理员改成 xxx」

admin_set_owner

换 ownerQQ

实现路径(为什么不用重启):

qzone-mcp (MCP server)
  └─ POST http://127.0.0.1:3100/api/whitelist   ← qq-bridge 自带控制台 API
       header: x-console-token: $(cat /opt/qq-bridge/state/console-token)
  └─ qq-bridge: atomicWriteJson(config.json) + cfg.allow 热更新 + reconcileSessionPolicies()

踩过的坑(重要):/opt/qq-bridge/config.json 是单文件 bind mount(./qbridge/config.json:/opt/qq-bridge/config.json), 容器内对它 rename(2) 恒返回 EBUSY → qq-bridge 的 atomicWriteJson 抛错 → 控制台所有「改配置」接口 500, 工具报 OSError(16, 'Device or resource busy')。修法:构建期用 core/patch-qqbridge.js 给 renameWithRetry 打一条退化路径(rename 反复 EBUSY 就原地覆盖写),core/Dockerfile 里 RUN node /tmp/patch-qqbridge.js。 本插件自己的 patch_file() 也有同样的 EBUSY 退化,作为控制台彻底不通时的保底(那种情况要 docker restart core 才生效,返回值里会写明)。

验证过:加群 → config.json 里 groups: [987654321] 落盘 → qq-bridge 日志 控制台:白名单已更新(群: 987654321,私聊: 123456789,管理员: 123456789) → 移除后回到 groups: [],全程没重启容器。

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    An MCP server that enables AI clients to send and receive QQ messages through NapCatQQ (OneBot v11) for both private and group chats. It supports message context management, real-time WebSocket listening, and human-like typing simulation.
    7
    26
    MIT
  • A
    license
    B
    quality
    C
    maintenance
    MCP server for QZone (QQ space) integration, enabling publishing, listing, liking, and commenting on posts through the QzoneUltra daemon and NapCat.
    12
    1
    MIT
  • A
    license
    B
    quality
    A
    maintenance
    Enables AI agents to control and monitor QQ accounts through the NapCatQQ OneBot 11 protocol, covering private/group messaging, forwards, group administration, file transfer, OCR, friend and profile management, and real-time message listening. It also handles QR-code login, hot-switching between multiple QQ accounts, and one-click self-check, deployment, and updates.
    74
    3
    MIT