利用QQ平台远程操控freecode_agent

(更新于 2026-06-02)

教程:搭建 QQ × free-code 机器人

项目和本文的绝大部分使用deepseek完成。
本教程中安装的free-code可以脱离QQ使用

##目录结构
/home/user/ # 你的用户主目录
├── .claude/ # free-code 配置和会话(一键安装脚本自动生成)
│ ├── settings.json # DeepSeek API 配置
│ └── sessions/ # 对话上下文文件
│ └── *.jsonl
├────────── qq-bot/ # 机器人项目目录
│ ├── docker-compose.yml
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── bridge.py # 粘合剂代码
│ └── config.yaml # 你的配置文件(可选)

└── free-code/ # (一键安装脚本产生的源码目录,可忽略)

整体架构

整个系统由三部分组成:

QQ 用户 ↔ [Napcat] ↔ [Python 粘合剂 (FastAPI)] ↔ [free-code 子进程]

  • Napcat:把 QQ 协议转成 HTTP,负责接收 QQ 消息并回调给你的服务器,也负责接收你的指令给 QQ 发消息
  • Python 粘合剂:核心逻辑。接收 Napcat 转发的消息,决定是回复指令(如”切对话”)还是启动 free-code 子进程处理
  • free-code:AI 引擎。以子进程方式启动,每次消息启动一个独立进程,用 --resume session_id 保持上下文连续

消息流向:

  1. 你在 QQ 上给机器人发消息
  2. Napcat 收到后,通过 HTTP POST 转发给 Python 粘合剂
  3. Python 粘合剂判断是普通消息 → 启动 free-code 子进程(cli-dev --bare --print --output-format=stream-json
  4. free-code 流式输出每一行 JSON 事件
  5. Python 边解析边调用 Napcat 的 HTTP API 把结果推送到你的 QQ
  6. 你实时看到 AI 的思考、工具调用、执行结果

前置准备

  • 一台 Linux 服务器(安装了 Docker)
  • 一个 QQ 小号(用于机器人登录,不要用主号,有封号风险)
  • 一个 API Key(DeepSeek 的 Anthropic 兼容接口,api.deepseek.com/anthropic

##第一步:安装 free-code【修改】
先跳转到/home/yourname目录
cd /home/yourname
在 /home/yourname/ 目录下使用一键安装脚本执行了安装命令。
curl -fsSL https://raw.githubusercontent.com/paoloanzn/free-code/main/install.sh | bash
安装完成后,验证是否成功:
free-code --version
你应该能看到类似 v2.1.87-dev 的版本号。如果提示 command not found,请重新登录 SSH 或执行 source ~/.bashrc。

第二步:配置 API

free-code 默认连接 Anthropic 官方 API(api.anthropic.com),国内服务器无法访问。但它兼容 Anthropic 格式的第三方 API,所以只需改环境变量指向 DeepSeek 即可:

####方式一:临时环境变量(每次会话有效)

1
2
3
export ANTHROPIC_BASE_URL="https://api.deepseek.com/anthropic"
export ANTHROPIC_API_KEY="sk-你的DeepSeek密钥" # 替换为真实密钥
export ANTHROPIC_MODEL="deepseek-v4-flash"

然后直接运行 free-code 即可。

####方式二:永久写入配置文件(推荐)
编辑 ~/.claude/settings.json(如果目录不存在,先创建):

1
2
mkdir -p ~/.claude
nano ~/.claude/settings.json

填入以下内容(注意 env 对象后的逗号不能少,否则会报 JSON 解析错误):

1
2
3
4
5
6
7
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic",
"ANTHROPIC_API_KEY": "sk-你的DeepSeek密钥",
"ANTHROPIC_MODEL": "deepseek-v4-flash"
}
}

第三步:写 Python 粘合剂(核心)

这是项目核心,下面讲设计思路,完整代码下载放文末。

这个 Python 程序需要完成以下功能:

3.1 会话管理

因为 free-code 每次启动是独立进程,需要通过 --resume <session_id> 来延续上下文。Python 层需要维护一个映射表:每个 QQ 用户 → 他的多个会话 → 每个会话的 session_id 和编号

数据用 JSON 文件存,结构大致:

1
2
3
4
5
6
7
8
9
{
"1234567890": {
"current": "uuid-xxx",
"sessions": [
{"number": 1, "session_id": "uuid-xxx", "first_message": "你好"},
{"number": 2, "session_id": "uuid-yyy", "first_message": "帮忙看下项目"}
]
}
}

关键设计点:

  • 占位符机制:用户说”新对话”时,先创建一个带 __new__{编号} 占位 session id 的记录。等他发了第一条真实消息,调用 free-code 拿到真实 session_id 后再替换。这避免了在创建阶段就调用 free-code 做无意义的初始化。
  • 编号复用:如果删除了中间的某个会话(比如删了 #2),新的会话应该复用 #2 而不是跳到 #4。用 while number in used 找最小可用编号即可。
  • 并发锁:同一个用户同时发两条消息,如果不加锁会导致 session_id 错乱。用 asyncio.Lock 让每个用户串行处理。

3.2 调用 free-code

这是技术难点。free-code 的 --print 模式支持 --output-format=stream-json,输出是 JSONL(每行一个 JSON 对象):

1
2
3
4
{"type":"assistant","message":{"content":[{"type":"text","text":"让我看看..."},{"type":"tool_use","id":"toolu_xxx","name":"Bash","input":{"command":"ls"}}]}}
{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_xxx","content":"file1.txt\nfile2.txt"}]}}
{"type":"assistant","message":{"content":[{"type":"text","text":"服务器上有这些文件..."}]}}
{"type":"result","session_id":"uuid-xxx","is_error":false,"result":"最终回答"}

处理流程:

  1. asyncio.create_subprocess_exec 启动 free-code 子进程,启动参数包含 --permission-mode acceptEdits,自动批准所有操作(安全性由底层的 Docker volume :ro/:rw 保证)
  2. 将要问的消息通过 stdin 写入,然后关闭 stdin
  3. 循环 readline() 逐行读取 stdout
  4. 解析 JSON,type == "assistant" 时提取 text 内容和 tool_use 调用
  5. type == "user" 时提取 tool_result(命令执行结果)
  6. type == "result" 时拿到最终回答和 session_id
  7. 每解析出一个 step,立即通过 Napcat API 推送给 QQ,实现流式推送

注意 stderr 也要异步读取,否则 pipe 堵满会导致进程卡死。

3.3 指令系统

需要拦截的 QQ 消息:

用户发 行为
新对话 创建新会话,回复编号
切对话 列出当前用户所有会话的编号和第一句话
删对话 清空所有会话
回复数字 切换到对应编号的会话
其他消息 发给 free-code 处理

3.4 长消息分块

free-code 的回答可能会很长(比如执行结果几百行)。QQ 消息有长度限制,不能一次性发送。解决方案:按换行符分块,每块 1500 字符以内,优先在换行处断开,拆成多条消息逐条发送。

1
2
3
4
5
6
7
def split_long_text(text, limit=1500):
if len(text) <= limit:
return [text]
cut = text.rfind("\n", 0, limit) # 优先找换行符
if cut == -1:
cut = limit # 没换行符才硬切
return [text[:cut]] + split_long_text(text[cut:], limit)

第四步:配置 Napcat

Napcat 用 Docker 启动,通过环境变量配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
services:
napcat:
image: mlikiowa/napcat-docker:latest
container_name: qq-bot-napcat
restart: unless-stopped
ports:
- "3000:3000" # HTTP API 端口
environment:
- ACCOUNT=你的QQ号
- WS_ENABLE=false
- HTTP_ENABLE=true
- HTTP_PORT=3000
- HTTP_POST_ENABLE=true
- HTTP_POST_URLS=http://bridge:5000/napcat/callback
volumes:
- napcat-data:/app/.config/QQ
- napcat-config:/napcat/config

关键点:

  • HTTP_POST_URLS 是 Napcat 收到消息时回调的地址。bridge 容器也在同一个 docker compose 网络里,所以用 http://bridge:5000/...(容器名作为 hostname)
  • 首次启动需要扫码登录,看日志 docker compose logs -f napcat

第五步:整体编排

完整的 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
services:
napcat:
image: mlikiowa/napcat-docker:latest
container_name: qq-bot-napcat
restart: unless-stopped
ports:
- "3000:3000" # HTTP API 端口
environment:
- ACCOUNT=你的QQ号
- WS_ENABLE=false
- HTTP_ENABLE=true
- HTTP_PORT=3000
- HTTP_POST_ENABLE=true
- HTTP_POST_URLS=http://bridge:5000/napcat/callback
#请确保 bridge 服务名与 docker-compose.yml 中的服务名一致,Napcat 通过服务名访问”。
volumes:
- napcat-data:/app/.config/QQ
- napcat-config:/napcat/config
bridge:
build: .
container_name: qq-bot-bridge
restart: unless-stopped
ports:
- "5000:5000" # 回调端口
volumes:
- bridge-data:/app/data # session 数据持久化
- bridge-claude:/root/.claude # free-code session 持久化
- /home/yourname/free-code:/app/free-code:ro # free-code 只读挂载
- /home/yourname:/home/yourname:ro # 只读访问(安全)这里可以自己修改
- /home/yourname/workspace:/home/yourname/workspace:rw # 工作区可写 这里可以自己修改
environment:
- ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
- ANTHROPIC_API_KEY=sk-你的密钥
- NO_COLOR=1
depends_on:
- napcat
mem_limit: 2g
cpus: "1.0" #使用资源限制

volumes:
napcat-data:
napcat-config:
bridge-data:
bridge-claude:

为什么要做只读/可写拆分? 通过 Docker 卷的 :ro:rw 挂载权限来控制 AI 能修改的目录——所有目录只读,只有指定工作区可写。这比 free-code 内置的权限配置(settings.json 的 permissions 字段)稳定得多,因为 free-code 的权限系统只支持简单通配符(如 Read*Edit(path/*)),不支持复杂正则,写错了会导致整个配置文件被跳过。

为什么 free-code 的 session 要用 volume 持久化? free-code 把 session 存在 ~/.claude/sessions/ 下。如果不挂载 volume,每次容器重建后这些文件就丢了,旧会话无法继续,会报 No conversation found

第六步:启动这DS写的啥啊,怎么少这么多

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM python:3.12-slim

WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*

# 安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制代码
COPY bridge.py config.yaml ./

# 启动
CMD ["python", "bridge.py"]

requirements.txt

1
2
3
4
5
6
fastapi>=0.115.0
uvicorn>=0.32.0
httpx>=0.28.0
pyyaml>=6.0
pydantic>=2.0

config.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
allowed_qq:
- "123456" # 替换为你的 QQ 号
freecode:
env:
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic"
ANTHROPIC_API_KEY: "sk-你的DeepSeek密钥"
NO_COLOR: "1"
model: "deepseek-v4-flash"
path: "/app/free-code/cli-dev"
work_dir: "/home/yourname"
napcat:
callback_port: 5000
host: "napcat"
port: 3000
session:
data_file: "/app/data/sessions.json"
max_per_user: 10

###启动前请注意,这些文件是需要修改的,请确保你进行了修改。

第七步:启动

1
2
3
4
docker compose up -d

# 查看 Napcat 日志,扫码登录
docker compose logs -f napcat

看到二维码后扫码登录。之后你的 QQ 小号就会保持在线。

验证:

1
curl http://localhost:5000/health

然后用授权 QQ 号给机器人发消息测试。

使用效果

发一条消息,你会看到类似这样的流式推送:

1
2
3
4
5
6
7
8
9
● 我来看看服务器上有哪些项目...
● Bash(ls -la /home/user)
⎿ total 48
drwxr-xr-x 12 user user 4096 ...
drwxrwxr-x 3 user user 4096 ...
...
● 服务器上有以下项目:
1. agent-workplace/ - AI 工作区
2. free-code/ - free-code 源码

踩坑记录

  1. Pydantic 422 错误 — Napcat 发来的 message 字段有时是字符串有时是数组 [{"type":"text","data":{"text":"你好"}}]。需要写 @field_validator 做兼容转换,并且设置 model_config = {"extra": "ignore"} 忽略 Napcat 的额外字段。

  2. 默认 60 秒超时 — free-code 处理复杂问题可能超过 60 秒(比如分析整个项目)。Python 的 asyncio.wait_for 默认超时需要改成 300 秒。

  3. Session 丢失 — 容器重建后 free-code 的 session 文件没了,所有旧会话报 No conversation found。解决方案是在 docker compose 里加一个独立 volume 挂载到 /root/.claude

  4. 权限弹窗卡死 — free-code 遇到危险操作时会弹出交互式确认 ? 允许执行此命令?(Y/n),但纯 QQ 对话环境无法回应,导致进程挂起。解决方案:启动参数加 --permission-mode acceptEdits 自动批准(安全性由 Docker volume :ro/:rw 保证)。

  5. 并发消息错乱 — 用户连续发两条消息,第二个子进程可能读到第一个的 session_id 导致覆盖。每个用户的 asyncio.Lock 解决。

  6. 会话编号重复 — 用了 max(s.number) + 1 取编号,但中间删除会话后会产生重复。改成 while number in used: number += 1

  7. 流式输出 — 用 --output-format=stream-jsonreadline() 逐行读取,边解析边推送。

  8. 长消息 — 按换行符分块发送,避免超出 QQ 消息长度限制。

  9. free-code 权限配置 regex 不支持 — free-code 的 settings.json 里的 permissions 字段只支持简单的通配符(Read*Edit(path/*)),不支持负向前瞻这类复杂正则。用正则会导致整个配置文件被跳过,API 配置也不生效。正确的做法是通过 Docker volume 的 :ro/:rw 来控制文件系统访问,而不是依赖 free-code 的权限系统。

设计取舍说明

为什么用 HTTP 回调而不是 WebSocket? Napcat 同时支持 HTTP 和 WebSocket。HTTP 更简单——Napcat 把消息 POST 过来,处理完就返回,不需要维护长连接。Python 要主动发消息时,调用 Napcat 的 HTTP API 即可。

为什么每次消息启动独立进程? free-code 的 --print 模式就是为这种一次性调用设计的。每次启动新进程,用完就退,不用担心常驻进程的内存泄漏、进程挂掉、重启恢复等问题。上下文通过 --resume session_id 延续。

为什么用 DeepSeek 的 API? DeepSeek 兼容 Anthropic 的 API 格式(api.deepseek.com/anthropic),国内服务器可以直接访问,成本也比 Anthropic 官方低很多。只需改 ANTHROPIC_BASE_URL 就能切换,不需要改代码。

为什么用 Docker volume 控制权限而不是 free-code 的 settings.json? free-code 的权限配置语法受限(仅支持简单通配符),且写错了会导致整个配置文件被跳过。Docker 的 :ro/:rw 是操作系统级别的文件系统权限,稳定可靠,且与 AI 引擎无关——换任何 AI 引擎都能复用同样的权限策略。

下载

bridge.py