
1. 从一条消息说起这个方案到底在解决什么问题微信里的消息本质上就是一条条结构化数据在客户端、服务器、网关之间来回跑。你想让 Claude Code 这类 AI 编程助手自动处理微信消息核心难点从来不是AI 会不会写代码而是消息怎么进来、身份怎么确认、回复怎么出去这三件事能不能串成一条稳定的链路。我最早动这个念头是因为团队内部有个需求把一些重复性的技术问答、代码片段检索、日志初步分析放到微信里做让同事在群里 一下就能拿到结果而不是每次都去翻文档或者找人。市面上的方案要么是纯 SaaS 的黑盒要么是只支持固定问答的机器人想接 Claude Code 这种能直接执行终端命令、能读写文件的工具几乎没有现成的路子。这个方案要解决的核心问题有三个。第一是消息链路微信本身不开放个人号的消息接口你得找到一个合法的、稳定的消息入口把用户发的消息拿到手再把 AI 的回复送回去。第二是身份白名单AI 能执行终端命令这件事本身就是双刃剑如果谁都能触发等于把服务器钥匙挂在门口所以必须有一套基于四元组的白名单机制精确控制谁、在哪个场景、能触发什么级别的操作。第三是Claude Code 的接入方式它不是一个简单的 HTTP API而是一个带会话状态、能操作本地环境的工具怎么把它嵌进消息处理流程里需要想清楚。适合看这篇的人是那些已经有一定后端基础、玩过微信生态小程序、公众号、企业微信都算、并且想认真把 AI 助手落到实际工作流里的开发者。如果你只是想找个现成的机器人用这篇可能偏重了但如果你想自己掌控整条链路知道每一环为什么这么设计那接下来的内容应该对你有用。2. 整体架构设计为什么这么拆2.1 消息入口的选型逻辑微信生态里能拿到消息的入口掰着手指头数就那么几个公众号、企业微信、小程序客服消息、以及个人号的非官方方案。个人号方案这里不展开合规风险太高而且稳定性极差今天能用的明天可能就封了不适合做正经项目。公众号的问题在于它更适合用户主动发消息、你被动回复这种模式而且有 48 小时客服消息窗口的限制超过窗口就得用模板消息交互体验很割裂。企业微信相对好一些它有真正的应用消息接口可以主动推送也有回调机制适合做内部工具。小程序客服消息则介于两者之间适合 C 端场景但对开发者要求更高。我最终选的是企业微信应用 回调服务的组合。原因很直接企业微信的消息回调是官方支持的有签名校验机制消息格式清晰而且天然带组织架构和用户身份白名单可以直接复用企业微信的 userid不用自己再建一套账号体系。对于内部工具场景这是最省事也最稳的路子。提示如果你做的是面向外部用户的产品企业微信可能不合适得走公众号或小程序。但白名单和消息链路的思路是通用的换汤不换药。2.2 Claude Code 的接入姿势Claude Code 和普通的聊天 API 最大的区别在于它是有状态的、能操作本地环境的。你让它看一下这个目录下的日志文件它是真的会去读文件你让它跑一下测试它是真的会执行命令。这意味着两件事一是它必须跑在一个受控的环境里二是它的调用不能是简单的发一条消息等一个回复而要考虑会话上下文和执行超时。接入方式上我试过两种。一种是把它当成一个子进程通过标准输入输出交互这种方式最直接但进程管理麻烦会话状态不好保持。另一种是把它封装成一个本地服务消息进来后转发给这个服务服务内部维护会话再把结果返回。第二种更可控也方便做超时和并发控制我最终用的是这个思路。这里有个关键点Claude Code 的执行是有副作用的它可能改文件、可能跑命令。所以我在服务层加了一层执行沙箱的概念——不是真的容器隔离而是通过工作目录限制和命令白名单把它的操作范围框住。比如只允许它在某个特定目录下读写只允许执行预先登记过的命令前缀。2.3 白名单为什么要用四元组白名单需要四元组这个说法是我在实际踩坑之后总结出来的。一开始我只做了用户维度的白名单就是哪些 userid 可以用。结果发现不够因为同一个用户在不同场景下的权限应该不一样。比如在私聊里可以让他触发代码执行但在群里就不行因为群里消息可能被转发、被截图风险不可控。四元组我定义成用户身份 消息场景 操作类型 时间窗口。用户身份就是企业微信的 userid消息场景区分私聊、群聊、特定群操作类型区分只读查询、代码执行、文件写入这几个级别时间窗口则是限制某些高危操作只能在工作时间触发避免半夜被误触发。这四个维度组合起来才能精确表达张三在工作时间、私聊场景下、可以触发代码执行这样的规则。少了任何一个维度要么权限过宽要么误伤正常使用。这个设计不是拍脑袋来的是被几次误触发事件逼出来的。3. 消息链路的核心细节与实操要点3.1 回调服务的搭建与签名校验企业微信的回调服务第一步是配置 URL、Token 和 EncodingAESKey。这三个东西在企业微信管理后台的应用设置里生成Token 是你自己定的EncodingAESKey 是系统给的。配置的时候企业微信会发一个 GET 请求过来做验证你需要把收到的 echostr 解密后原样返回。签名校验的逻辑是这样的把 Token、Timestamp、Nonce 三个值按字典序排序拼接成一个字符串做 SHA1 哈希得到的值跟企业微信传来的 msg_signature 比对。一致才说明请求是合法的。这一步千万别省否则任何人都能伪造请求打你的服务。import hashlib def check_signature(token, timestamp, nonce, signature): items [token, timestamp, nonce] items.sort() joined .join(items) computed hashlib.sha1(joined.encode()).hexdigest() return computed signature消息体的解密用的是 AES-256-CBCEncodingAESKey 是 Base64 编码的 43 位字符串补一个等号变成标准 Base64 后解码得到 32 字节的密钥。IV 取密钥的前 16 字节。解密后的明文结构是16 字节随机数 4 字节消息长度 消息内容 CorpID。解析的时候要按这个结构切。注意解密后的消息长度字段是大端序的 4 字节整数很多人在这里踩坑直接用 struct.unpack 的时候忘了指定字节序导致解析出来的长度是错的。3.2 消息的接收、解析与路由企业微信推过来的消息是 XML 格式的外层是加密的解密后里面还有一层 XML。解析的时候要提取 MsgType、FromUserName、Content、AgentID 这些字段。MsgType 可能是 text、image、event 等我们主要处理 text。路由的逻辑是先根据 FromUserName 查白名单看这个用户有没有权限再根据消息内容判断意图是普通问答还是需要触发代码执行然后决定走哪条处理链路。这里有个细节企业微信的消息有重试机制如果 5 秒内没收到你的响应它会重发最多重试三次。所以你的处理必须是幂等的或者至少要在 5 秒内先返回一个已收到的响应再异步处理。我的做法是收到消息后立刻返回空字符串企业微信规定返回空串表示不回复然后把消息丢进一个队列由后台 worker 处理。worker 处理完再通过企业微信的主动消息接口把结果推回去。这样既避免了超时重试也让处理逻辑可以慢慢跑不受 5 秒限制。3.3 回复消息的推送与格式处理主动推送消息用的是企业微信的 message/send 接口需要 access_token。access_token 有 2 小时有效期得做缓存和自动刷新。刷新的逻辑很简单但要注意并发场景下别同时刷新多次用一个锁或者单例来保证。消息格式上企业微信支持 text、markdown、news 等。Claude Code 的输出经常带代码块用 markdown 格式展示效果最好。但企业微信的 markdown 支持有限不支持表格和部分语法所以我在推送前会做一层转换把不支持的语法降级成纯文本或者图片。def format_for_wecom(content): # 企业微信 markdown 不支持表格转成列表 lines content.split(\n) result [] for line in lines: if line.strip().startswith(|): # 表格行转成普通文本 cells [c.strip() for c in line.split(|) if c.strip()] result.append( / .join(cells)) else: result.append(line) return \n.join(result)这里有个实操心得Claude Code 的输出有时候很长超过企业微信单条消息的限制2048 字节需要分片发送。分片的时候别按字节硬切要按行或者按段落切否则代码块会被切得乱七八糟。我一般按 1500 字节左右切留点余量。4. 白名单机制的设计与实现4.1 四元组的具体定义与存储前面说了四元组是用户、场景、操作类型、时间窗口。具体实现上我用一张表来存规则每条规则就是一组四元组加上一个是否允许的标记。查询的时候把当前请求的四个维度都拿出来去表里匹配只要有一条匹配且允许就放行。存储用 SQLite 就够了这种量级的数据不需要上 MySQL。表结构大概是id、user_id、scene、action、time_window、allow、created_at。scene 和 action 用枚举值time_window 存成 09:00-18:00 这样的字符串查询的时候解析。CREATE TABLE whitelist ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, scene TEXT NOT NULL, action TEXT NOT NULL, time_window TEXT, allow INTEGER DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );匹配逻辑要注意优先级更具体的规则应该覆盖更宽泛的规则。比如有一条张三、私聊、代码执行、允许和一条张三、所有场景、代码执行、拒绝应该以更具体的那条为准。我的做法是给规则加一个 specificity 分数匹配时取分数最高的那条。4.2 权限分级与操作类型操作类型我分了四级L1 只读查询查文档、查日志、L2 代码生成生成代码但不执行、L3 代码执行在沙箱里跑命令、L4 文件写入修改文件。级别越高能匹配的白名单规则越少。默认情况下所有用户只有 L1 权限。L2 需要申请L3 和 L4 需要管理员审批。这个分级不是形式主义是因为 L3 和 L4 真的出过事。有一次一个同事让 AI 清理一下临时文件结果 AI 把工作目录下的一个还没提交的代码文件删了。虽然能恢复但那次之后我就把文件写入单独拎出来做了审批。提示权限分级的关键不是技术实现而是让用户知道我现在这个操作是什么级别。我在回复消息里会带上当前操作的级别标记比如 [L1] 或 [L3]让用户心里有数。4.3 动态白名单与临时授权固定白名单有个问题临时需要给某个人开权限改表太麻烦。所以我加了一个临时授权机制管理员可以在微信里发一条特定格式的消息比如 grant 张三 L3 2h系统就临时给张三开 2 小时的 L3 权限到期自动失效。这个机制的实现是在内存里维护一个临时授权表定期清理过期的。临时授权的优先级高于固定白名单但低于显式的拒绝规则。这样既灵活又不会失控。5. Claude Code 的集成与执行沙箱5.1 会话管理与上下文保持Claude Code 的会话是有状态的同一个会话里它能记住之前聊了什么、改了什么文件。所以我在服务层维护了一个 session 映射企业微信的 userid 对应一个 Claude Code 会话 ID。用户第一次发消息时创建会话后续消息复用这个会话。会话的过期时间设的是 30 分钟超过 30 分钟没交互就销毁下次重新创建。这个时间是根据实际使用习惯定的太短了上下文老是断太长了会话堆积占资源。30 分钟是个比较平衡的值。会话销毁的时候要注意清理工作目录把临时文件删掉。我有一次忘了清理结果磁盘被一堆临时文件占满了服务直接挂了。后来加了个定时任务每小时扫一遍工作目录把超过 1 小时没动过的会话目录删掉。5.2 执行沙箱的边界控制沙箱这块我没用容器因为容器启动太慢而且企业微信的消息处理对延迟敏感。我用的是工作目录 命令白名单的软隔离。工作目录方面每个会话分配一个独立的目录Claude Code 只能在这个目录及其子目录下操作。通过设置进程的工作目录和限制文件访问路径来实现。命令白名单方面我维护了一个允许执行的命令前缀列表比如ls、cat、grep、python、node这些。执行前先检查命令是否在白名单里不在就拒绝。ALLOWED_COMMANDS [ls, cat, grep, head, tail, python, node, npm] def is_command_allowed(cmd): first_word cmd.strip().split()[0] if cmd.strip() else return first_word in ALLOWED_COMMANDS这个白名单肯定不是绝对安全的比如python可以执行任意代码。所以 L3 权限的审批才那么严格。软隔离的目的是防误操作不是防恶意攻击。真要防恶意得上容器或者虚拟机那是另一个量级的工程。5.3 超时控制与资源限制Claude Code 执行命令可能卡住比如跑了一个死循环或者等一个永远不来的输入。所以每个执行都要设超时我设的是 60 秒。超时后强制杀掉进程返回执行超时的提示。资源限制方面主要是 CPU 和内存。我用的是resource模块在子进程启动前设置限制比如最多用 50% 的 CPU 时间、最多 512MB 内存。超过就杀。这个在 Linux 下用setrlimit实现Windows 下支持有限所以服务是跑在 Linux 上的。import resource def limit_resources(): resource.setrlimit(resource.RLIMIT_CPU, (60, 60)) resource.setrlimit(resource.RLIMIT_AS, (512 * 1024 * 1024, 512 * 1024 * 1024))注意setrlimit 要在 fork 之后、exec 之前调用否则会影响父进程。用 subprocess 的 preexec_fn 参数来传这个函数。6. 常见问题与排查技巧实录6.1 消息收不到或延迟高最常见的问题是回调服务没响应或者响应太慢。排查顺序是先看企业微信后台的回调日志那里会显示每次回调的状态码和耗时。如果是 500说明你的服务报错了去看服务日志。如果是超时说明处理太慢检查是不是在回调里做了耗时操作。我遇到过一次回调服务本身没问题但服务器的时间不对导致签名校验一直失败。企业微信的签名校验依赖 Timestamp如果服务器时间偏差超过几分钟校验就会失败。所以服务器一定要开 NTP 同步。6.2 Claude Code 执行失败执行失败的原因很多常见的有命令不在白名单、工作目录权限不对、超时、内存超限。排查的时候先看错误信息Claude Code 会把 stderr 返回回来。如果是权限问题检查工作目录的 owner 和权限位。如果是超时看看是不是命令本身就需要很久考虑调大超时或者优化命令。有一次一个用户让 AI 统计一下所有日志文件的行数AI 执行了wc -l *.log但那个目录下有几千个日志文件命令跑了很久超时了。后来我加了个提示让 AI 在执行这类命令前先确认文件数量。6.3 白名单误判白名单误判有两种该放行的没放行不该放行的放行了。前者通常是规则写得太窄比如只写了私聊场景用户在群里用就被拒了。后者通常是规则冲突比如有一条允许和一条拒绝同时匹配优先级没处理好。排查的时候我会把匹配过程打日志记录每个请求匹配到了哪些规则、最终用了哪条。这样出问题的时候一看日志就知道是哪条规则的问题。这个日志我建议一直开着虽然有点占空间但排查问题时能省很多时间。问题现象可能原因排查方法解决方式消息收不到回调服务未启动检查服务进程和端口重启服务签名校验失败服务器时间偏差对比服务器和标准时间开启 NTP 同步回复延迟高处理逻辑阻塞查看回调耗时日志改为异步处理执行超时命令耗时过长查看执行日志调大超时或优化命令白名单误拒规则太窄查看匹配日志补充规则白名单误放规则冲突查看匹配日志调整优先级6.4 会话状态丢失会话状态丢失的表现是用户明明刚聊过下一条消息 AI 就失忆了。原因通常是会话过期或者服务重启。会话过期是设计如此但服务重启导致的丢失是可以避免的。我的做法是把会话状态持久化到 SQLite服务重启后能恢复。不过 Claude Code 本身的会话状态没法持久化所以恢复的只是映射关系上下文还是得重新建立。这个问题的终极解法是让 Claude Code 支持会话导出和导入但目前它没这个功能。所以我的建议是重要对话别依赖会话记忆该说清楚的背景每次都说清楚。7. 一些实操心得和后续可以折腾的方向这套方案跑了大半年整体是稳的但有几个心得值得单独说。第一是日志一定要全消息进来、白名单匹配、Claude Code 执行、结果推送每个环节都要打日志而且日志要带 trace id方便串联。我一开始日志打得少出问题的时候全靠猜后来补全了日志排查效率提升了好几倍。第二是别把 AI 的输出直接透传给用户。Claude Code 有时候会输出一些内部思考过程或者调试信息直接发给用户会很奇怪。我在中间加了一层过滤把明显的内部信息去掉只保留最终结果。这个过滤规则是逐步积累的遇到一次加一条。第三是白名单要定期 review。临时授权多了之后很容易忘记清理导致权限膨胀。我现在的做法是每月 review 一次把不再需要的规则删掉。这个习惯是被一次安全审计逼出来的审计的时候发现有一堆半年前的临时授权还挂着。后续可以折腾的方向一个是把消息链路扩展到更多入口比如公众号和小程序让同一套白名单和 Claude Code 服务复用。另一个是给 Claude Code 加一个操作预览功能在执行高危操作前先把要做什么告诉用户用户确认后再执行。这个在技术上不难难的是怎么设计交互让确认不显得烦人。最后分享一个小技巧Claude Code 的执行结果里经常带 ANSI 颜色码直接推到微信里会显示成一堆乱码。在推送前用正则把 ANSI 码去掉\x1b\[[0-9;]*m这个模式能匹配大部分情况。这个坑我踩过当时排查了半天才发现是颜色码的问题。