1. 项目缘起与整体架构思路微信接入 Claude Code 这件事最早是从一个很具体的需求开始的团队内部有个知识库机器人平时丢链接、丢文档进去它能自动总结、归档、打标签。但用着用着发现很多问题其实不需要“总结”而是需要“动手”——比如让它读一下某个目录下的日志、跑一段脚本、改一个配置文件、生成一份报告。这些事 Claude Code 能干但它是个命令行工具总不能要求每个同事都去开终端。于是就有了这个方案把 Claude Code 挂到一个微信消息入口后面让微信里的消息变成 Claude Code 的输入Claude Code 的输出再回到微信里。听起来简单但真正拆开来看中间涉及消息链路、身份校验、白名单控制、会话隔离、执行环境隔离这几块每一块都有坑。这篇文章就把整个方案从头到尾拆一遍。适合谁看如果你手上有 Claude Code想把它接到某个 IM 入口上做自动回复或者任务执行或者你单纯想搞清楚“微信消息怎么变成 AI 的输入”这条链路那这篇应该能给你省不少时间。我会尽量把每一步的“为什么”讲清楚而不是只丢一段代码让你抄。先说整体架构。整个方案分四层接入层微信消息的收发。这里不碰个人微信的私有协议走的是企业微信或者公众号的消息回调合规且稳定。路由层判断这条消息该不该处理、由谁处理、处理完回给谁。白名单就在这一层。执行层Claude Code 的调用。包括会话管理、工作目录隔离、超时控制。回传层把执行结果切分、格式化再通过微信的消息接口发回去。这四层里接入层和回传层是“微信侧”的事路由层和执行层是“Claude Code 侧”的事。中间用消息队列或者直接函数调用串起来都行看你的并发量。我这边因为量不大直接用了同步调用加超时简单直接。提示不要一上来就想着支持个人微信。个人微信的自动化方案在合规和稳定性上都有很大风险账号被封是小事消息链路断了排查起来非常痛苦。企业微信或者公众号的消息回调是官方支持的路径虽然配置麻烦一点但后面省心。为什么选 Claude Code 而不是直接调 API因为 Claude Code 的核心价值在于它能“动手”——读写文件、执行命令、多轮迭代。直接调 API 你只能得到一段文本而 Claude Code 能给你一个改好的文件、一份跑通的脚本。这个差别在“自动回复”场景里是决定性的前者只能聊天后者能干活。2. 消息链路拆解从微信到 Claude Code 的完整路径2.1 微信侧的消息入口选择微信生态里能拿到消息回调的入口主要有三个公众号、企业微信应用、企业微信机器人。这三个的差异直接决定了你后面能做什么。公众号的消息回调是最常见的但限制也多用户发消息后你需要在 5 秒内响应否则会重试被动回复的消息有长度限制主动推送需要用户 48 小时内有过互动。这些限制对于“让 Claude Code 跑一个任务再回复”来说5 秒根本不够。企业微信应用的消息回调就宽松很多。企业微信允许你先回一个空响应然后用应用消息接口主动推送结果。也就是说你可以收到消息后立刻确认等 Claude Code 跑完再推回去。这个“先确认后推送”的模式是整个方案能成立的关键。企业微信机器人群机器人更简单但它只能被动接收 webhook 推送不能接收用户消息。所以它适合做“结果通知”不适合做“消息入口”。我最终选的是企业微信自建应用。配置路径是企业微信管理后台 → 应用管理 → 自建 → 创建应用 → 接收消息 → 设置 API 接收。这里需要填一个 URL、Token 和 EncodingAESKey。URL 就是你服务器的回调地址Token 和 EncodingAESKey 用来做消息的签名校验和解密。注意企业微信的消息回调默认是加密的。你在后台设置 EncodingAESKey 后收到的消息体是加密的需要先解密才能拿到明文。很多人卡在这一步以为收到的乱码是编码问题其实是没解密。2.2 消息的接收与解密企业微信回调过来的消息是一个 XML 结构加密后的形式大概是这样xml ToUserName![CDATA[corpid]]/ToUserName Encrypt![CDATA[加密后的消息体]]/Encrypt AgentID![CDATA[1000002]]/AgentID /xml解密需要用到企业微信提供的加解密库。官方有 Python 和 Java 的版本我用的是 Python 版。解密的核心逻辑是用 EncodingAESKey 做 AES-256-CBC 解密然后去掉 padding再取前 16 字节作为随机串接着 4 字节是消息长度后面才是真正的消息内容。这里有个细节解密后的消息内容里msg_type 字段决定了这条消息是什么类型。文本消息是text图片是image事件是event。我们只处理text类型其他类型直接忽略或者回一个“暂不支持”。from wechatpy.enterprise.crypto import WeChatCrypto from wechatpy.enterprise import parse_message crypto WeChatCrypto(token, encoding_aes_key, corp_id) decrypted_xml crypto.decrypt_message(encrypted_xml, signature, timestamp, nonce) msg parse_message(decrypted_xml) if msg.type text: user_input msg.content from_user msg.sourcewechatpy这个库把加解密和消息解析都封装好了省了很多事。如果你不想引入额外依赖也可以自己实现但加解密那块容易出错建议直接用现成的。2.3 消息的异步处理与回传收到消息后不能直接在里面跑 Claude Code因为企业微信要求 5 秒内响应。我的做法是收到消息后立刻返回一个空字符串表示“收到稍后回复”然后把消息丢到一个后台任务队列里。后台任务的处理流程是从队列里取出消息解析出用户 ID 和内容。检查用户是否在白名单里。不在就直接丢弃不回任何消息。检查当前是否有正在执行的任务。如果有回一条“上一个任务还在跑请稍等”。调用 Claude Code传入用户消息设置超时时间。拿到结果后通过企业微信的应用消息接口推送给用户。推送消息的接口是message/send需要传touser、msgtype、agentid和content。文本消息有长度限制大概 2048 字节超过需要切分。切分的时候要注意不要把一个代码块从中间切断否则用户看到的格式会乱。def send_text_message(user_id, content): # 按 2000 字符切分尽量在换行处切 chunks split_content(content, 2000) for chunk in chunks: client.message.send_text(agent_id, user_id, chunk) time.sleep(0.5) # 避免触发频率限制提示企业微信的应用消息接口有频率限制大概每分钟 600 次。如果你切分了很多段记得加个 sleep否则后面的消息会发送失败。3. 白名单机制为什么需要四元组3.1 白名单的基本逻辑白名单这件事看起来简单——不就是维护一个用户列表吗但实际做起来你会发现光有用户 ID 不够。最早我的白名单就是一个用户 ID 列表谁在列表里谁就能用。但很快问题就来了同一个用户可能在私聊里发消息也可能在群里 应用发消息可能在企业微信里发也可能在微信插件里发。这些场景下消息里的from_user字段格式是不一样的。企业微信的消息里用户标识有几个维度FromUserName发送者的 UserID在企业内唯一。AgentID接收消息的应用 ID。ToUserName企业 CorpID。MsgType消息类型。如果你只校验FromUserName那么任何知道这个 UserID 的人只要构造一个请求打到你的回调地址就能触发 Claude Code。虽然企业微信有签名校验但签名校验只能证明消息来自企业微信不能证明发送者是你允许的人。所以白名单需要更严格的校验。我最终用的是四元组CorpID AgentID FromUserName MsgType。这四个字段组合起来才能唯一确定“谁在哪个应用里发了什么类型的消息”。3.2 四元组的校验实现四元组的校验逻辑是这样的WHITELIST { (corp_id_1, agent_id_1, user_a, text), (corp_id_1, agent_id_1, user_b, text), # ... } def is_allowed(msg): key (msg.corp_id, msg.agent_id, msg.source, msg.type) return key in WHITELIST为什么不用数据库因为白名单通常很小几十个条目用内存里的 set 就够了查询是 O(1)。如果白名单很大或者需要动态管理再考虑用 Redis 或者数据库。这里有个容易忽略的点MsgType也要校验。因为企业微信的消息类型有很多种文本、图片、语音、事件等。如果你只校验用户不校验类型那么用户发一张图片也可能触发 Claude Code而 Claude Code 拿到的是图片的 media_id不是文本处理起来会出错。注意白名单的校验一定要在解密之后、处理之前做。不要在解密之前做因为解密之前你拿不到FromUserName也不要在处理之后做因为那时候 Claude Code 可能已经开始跑了。3.3 白名单的维护与动态更新白名单的维护有两种方式硬编码和动态配置。硬编码就是写在代码里改的时候要重新部署。适合白名单很稳定的场景。我一开始就是硬编码后来发现要加人就得改代码、重启服务太麻烦。动态配置是把白名单放在一个外部文件或者配置中心里服务定期读取。我用的是一个简单的 JSON 文件放在服务器上服务每次处理消息前读一下文件。文件格式{ whitelist: [ {corp_id: xxx, agent_id: 1000002, user_id: user_a, msg_type: text}, {corp_id: xxx, agent_id: 1000002, user_id: user_b, msg_type: text} ] }读文件的开销很小而且可以随时改不用重启。如果你用 Redis可以做得更实时但我觉得没必要JSON 文件足够了。还有一个细节白名单里要不要区分“能执行命令”和“只能聊天”我后来加了一个权限字段因为有些用户只需要问答不需要让 Claude Code 去执行命令。执行命令的权限更大风险也更高应该单独控制。{user_id: user_a, permission: execute} {user_id: user_b, permission: chat}这样在调用 Claude Code 的时候可以根据权限决定是否允许它执行终端命令。Claude Code 本身有权限控制但你在外面再包一层更安全。4. Claude Code 的调用与执行环境隔离4.1 Claude Code 的安装与配置Claude Code 的安装方式取决于你的操作系统。在 Ubuntu 上我用的方式是直接下载二进制包然后放到/usr/local/bin下。官方文档里有详细的安装步骤这里不展开。安装完之后需要配置 API Key 或者登录账号。# 下载并安装 curl -fsSL https://claude.ai/install.sh | sh # 配置 API Key export ANTHROPIC_API_KEYyour_key_here如果你在服务器上跑建议把 API Key 放在环境变量或者配置文件里不要硬编码在代码里。Claude Code 支持从环境变量读取也支持从配置文件读取。注意Claude Code 在某些地区可能不可用安装前先确认你的网络环境是否支持。如果服务器在国内可能需要配置代理但代理的配置方式这里不展开自行解决。安装完之后可以用claude --version验证是否安装成功。然后跑一个简单的测试echo print(hello) | claude -p 解释这段代码如果能看到输出说明 Claude Code 已经可以正常工作了。4.2 会话管理与工作目录隔离Claude Code 默认会在当前工作目录下读写文件。如果你直接在一个共享目录里跑多个用户的任务会互相干扰。所以每个用户或者每个会话应该有一个独立的工作目录。我的做法是为每个用户创建一个目录路径是/data/claude-workspaces/{user_id}。每次处理消息时把工作目录切换到该用户的目录下。import os import subprocess def run_claude(user_id, prompt): workspace f/data/claude-workspaces/{user_id} os.makedirs(workspace, exist_okTrue) result subprocess.run( [claude, -p, prompt], cwdworkspace, capture_outputTrue, textTrue, timeout300 ) return result.stdout这样每个用户只能看到自己的文件不会互相干扰。如果用户需要访问公共资源可以在工作目录里建一个软链接指向公共目录。会话管理还有一个问题Claude Code 默认是无状态的每次调用都是新的会话。如果你希望多轮对话能记住上下文需要用--session参数指定一个会话 ID。我的做法是用user_id作为会话 ID这样同一个用户的多轮消息会共享上下文。claude -p 继续上一个问题 --session user_a但这里有个坑会话文件会越来越大如果一直不清理磁盘会满。我加了一个定时任务每天清理超过 7 天的会话文件。4.3 超时控制与资源限制Claude Code 执行任务的时间不确定简单的问答几秒钟复杂的任务可能几分钟。如果不加超时控制一个卡住的任务会占着资源不放。我用的是subprocess.run的timeout参数设置 300 秒。超过 300 秒就杀掉进程返回一个超时提示。try: result subprocess.run(..., timeout300) except subprocess.TimeoutExpired: return 任务执行超时请简化你的请求后重试。除了超时还要限制资源。Claude Code 执行命令时可能会跑一些吃 CPU 或内存的操作。我用的是resource模块来限制子进程的资源import resource def limit_resources(): resource.setrlimit(resource.RLIMIT_CPU, (300, 300)) # CPU 时间 300 秒 resource.setrlimit(resource.RLIMIT_AS, (2 * 1024**3, 2 * 1024**3)) # 内存 2GB subprocess.run(..., preexec_fnlimit_resources)这样即使 Claude Code 跑飞了也不会把整个服务器拖垮。提示preexec_fn在 Python 3.11 之后有一些限制如果你用的是新版本可以考虑用subprocess.Popen加resource的方式或者用容器来隔离。5. 常见问题与排查技巧实录5.1 消息收不到回调地址与签名校验最常见的问题是消息收不到。排查顺序是检查回调 URL 是否可以从外网访问。企业微信的服务器需要能访问到你的 URL。检查 Token 和 EncodingAESKey 是否配置正确。这两个值在后台设置后代码里也要对应。检查签名校验逻辑。企业微信的签名是sha1(sort(token, timestamp, nonce, encrypt))排序后拼接再哈希。如果签名校验失败企业微信会返回一个错误但不会告诉你具体哪里错了。我的做法是在代码里把签名校验的中间值打日志对比一下就能发现是哪个字段不对。还有一个坑企业微信在配置回调 URL 的时候会先发一个 GET 请求做验证。你需要正确处理这个 GET 请求返回echostr的解密结果。很多人只处理了 POST忘了 GET导致 URL 验证不通过。if request.method GET: echo_str request.args.get(echostr) decrypted crypto.decrypt_message(echo_str, signature, timestamp, nonce) return decrypted5.2 Claude Code 无响应环境与权限问题Claude Code 跑不起来通常是这几个原因API Key 没配置检查环境变量是否设置或者配置文件是否存在。网络不通Claude Code 需要访问外部 API如果服务器网络受限会卡住。工作目录权限不对如果工作目录不可写Claude Code 无法创建文件会报错。命令不存在如果claude不在 PATH 里subprocess.run会抛FileNotFoundError。排查的时候先在服务器上手动跑一遍claude -p test看能不能正常输出。如果手动能跑代码里跑不了那就是环境变量或者工作目录的问题。5.3 消息发送失败频率限制与内容格式消息发送失败常见的原因是频率限制和内容格式。频率限制前面提过企业微信的应用消息接口有每分钟 600 次的限制。如果你切分了很多段或者短时间内给多个用户发消息可能会触发限制。解决方法是加 sleep或者用队列慢慢发。内容格式的问题主要是特殊字符。企业微信的文本消息支持换行和部分 Markdown但不支持所有格式。如果你发的消息里有或可能会被解析成 XML 标签导致消息发送失败。解决方法是做转义import html content html.escape(content)还有一个坑消息长度。企业微信的文本消息限制是 2048 字节不是字符。中文一个字占 3 个字节所以实际能发的字符数更少。切分的时候要按字节算不是按字符算。def split_by_bytes(text, max_bytes2000): chunks [] current for char in text: if len((current char).encode(utf-8)) max_bytes: chunks.append(current) current char else: current char if current: chunks.append(current) return chunks5.4 常见问题速查表问题现象可能原因排查方法解决方案消息收不到回调 URL 不可达用 curl 从外网访问 URL检查防火墙和域名解析消息收不到签名校验失败打印签名中间值对比检查 Token 和 AESKey消息收不到GET 验证未处理检查代码是否处理 GET补充 GET 处理逻辑Claude Code 无响应API Key 未配置手动跑 claude 命令配置环境变量Claude Code 无响应网络不通curl 测试外部 API检查网络配置Claude Code 无响应工作目录不可写ls -la 检查权限修改目录权限消息发送失败频率限制查看返回错误码加 sleep 或队列消息发送失败内容含特殊字符检查消息内容HTML 转义消息发送失败内容超长计算字节数按字节切分会话上下文丢失未指定 session检查命令参数加 --session 参数5.5 几个踩过的坑第一个坑是消息重复处理。企业微信在 5 秒内没收到响应会重试。如果你处理得慢同一条消息可能会被处理两次。解决方法是在收到消息后立刻返回然后在后台处理。另外可以用消息 ID 做去重处理过的消息 ID 记下来重复的直接忽略。第二个坑是工作目录累积。每个用户一个目录时间长了目录会很多而且里面的文件会越积越多。我加了一个定时清理任务每天凌晨清理超过 30 天没有活动的目录。第三个坑是Claude Code 的输出格式。Claude Code 的输出有时候会包含 ANSI 转义码比如颜色代码直接发到微信里会显示成乱码。解决方法是在发送前过滤掉 ANSI 转义码import re ansi_escape re.compile(r\x1B(?:[-Z\\-_]|\[[0-?]*[ -/]*[-~])) clean_output ansi_escape.sub(, output)第四个坑是并发控制。如果多个用户同时发消息Claude Code 会同时跑多个实例服务器可能扛不住。我加了一个简单的信号量限制同时执行的任务数不超过 3 个。超过的排队等待。import threading semaphore threading.Semaphore(3) def run_claude_with_limit(user_id, prompt): with semaphore: return run_claude(user_id, prompt)这个方案从最早的一个简单脚本到现在稳定跑了几个月中间踩的坑基本都在这了。如果你也想把 Claude Code 接到微信上建议先从企业微信自建应用开始把消息链路跑通再加白名单和会话管理。不要一上来就追求功能全先把“收到消息→跑 Claude Code→回消息”这个最小闭环跑通后面的都是在这个基础上加。