企业微信 × AI 对接第八步:客户联系——客户加员工那一刻,AI 现场生成欢迎语(welcome_code 20 秒窗口实战)
W1-W7 都在企业"内"转。企微的另一半价值在外:客户的微信里躺着你销售的好友位,这就是"客户联系"体系。运营的第一战场在加好友后的第一句话——官方机制把这个战场设计成了一场限时赛。
一、接口速览表
| 项 | 官方事实(2026-11-05 核对) |
|---|---|
| 触发事件 | change_external_contact,ChangeType=add_external_contact(成员加客户)/ add_half_external_contact(客户免验证加成员) |
| 回调字段 | UserID(成员)、ExternalUserID(客户,不是企业成员账号)、State(「联系我」配置的渠道参数)、WelcomeCode(发欢迎语的凭证) |
| 欢迎语接口 | POST externalcontact/send_welcome_msg,参数 welcome_code + text + attachments(新版支持 9 个附件含视频) |
| 20 秒窗口 | 官方原文:“企业仅可在收到相关事件后 20 秒内调用,且只可调用一次” |
| 竞态码 | 41051=欢迎语已下发(客户已开始聊天等,勿重试);41096=其他应用正在发(可重试——两码处理方向相反) |
| 不返回 code 的情形 | 管理端已配默认欢迎语 / 客户已聊天 / 商务伙伴自动递名片——代码必须容忍"没有 welcome_code" |
| 客户详情 | get_externalcontact/get 带 external_userid;跟进人 >500 需 cursor 分页;follow_user.state 可回溯渠道 |
| 权限 | 自建应用要配到「客户联系-可调用接口的应用」;unionid 需绑定微信开发者 ID 才有 |
| 数据红线 | remark_mobiles、avatar、gender、unionid 等字段第三方/代开发应用不可获取(官方硬隔离) |
二、20 秒窗口的工程含义:AI 必须"预备答题"
大模型现场生成一段欢迎语,快则 3-5 秒、慢则 10 秒+,再算上网络往返,裸调模型大概率超窗。正确架构是"两段式":
- 预生成:按渠道(
State参数标识:获客链接/展会二维码/视频号…)提前用 AI 批量写好欢迎语模板池,缓存 - 临场:事件到达 → 查渠道取模板 + 变量填充(本地毫秒级)→ 20 秒倒计时内发出
- 降级:临场生成超时或无 welcome_code → 放弃个性化,什么都不发(管理端默认欢迎语兜底)
把 LLM 放在倒计时前面而不是里面,这是全系列里最典型的"延迟预算倒推架构"。
三、完整代码(事件解析 + 欢迎语装配 + 超时降级,实测跑通)
"""客户联系欢迎语链路:事件解析→渠道匹配→20秒窗口守卫→竞态码处理(本地模拟,dry-run)"""
import json, re, time
WELCOME_WINDOW = 20.0 # 官方:收到事件后20秒内、仅一次
ERR_ALREADY = 41051 # 已下发:勿重试
ERR_RACING = 41096 # 他应用正在发:可重试
def parse_event(xml_text):
g = lambda tag: (re.search(r'<%s><!\[CDATA\[(.*?)\]\]></%s>' % (tag, tag), xml_text)
or re.search(r'<%s>(.*?)</%s>' % (tag, xml_text) , xml_text))
d = {t: (g(t).group(1) if g(t) else '') for t in
('Event', 'ChangeType', 'UserID', 'ExternalUserID', 'State', 'WelcomeCode')}
if d['Event'] != 'change_external_contact':
raise ValueError('非客户联系事件')
return d
# 渠道→预生成欢迎语池(上线前由 AI 按渠道批量写好,事件到达只做选择与变量填充)
POOL = {
'expo_qr': '王工您好,我是展会上聊过产线的{{name}}团队。附件是方案与案例,约个演示随时说。',
'link_ad': '您好,从介绍页过来的话,{{name}}这边今天可安排产品顾问,先问您三个场景问题。',
'': None, # 无渠道标记→不发个性化,交给默认欢迎语
}
def build_welcome(ev, now, gen_deadline=None):
"""返回发送动作或降级说明。gen_deadline=AI临场生成路径的最晚可用时刻。"""
wc = ev['WelcomeCode']
if not wc:
return {'skip': '无 welcome_code(默认欢迎语已配置/客户已聊天/名片自动递)'}
age = now - ev['_arrived_at']
if age > WELCOME_WINDOW:
return {'skip': '超20秒窗口(官方硬限),放弃个性化,默认话术兜底'}
tpl = POOL.get(ev['State'])
if tpl is None:
return {'skip': '未知渠道 State=%r,不发个性化避免说错话' % ev['State']}
text = tpl.replace('{{name}}', '李') # 变量填充本地毫秒级,不等LLM
return {'send': {'welcome_code': wc, 'text': {'content': text},
'attachments': []},
'deadline_left': '%.1fs' % (WELCOME_WINDOW - age)}
def handle_resp(r, retry_left=1):
"""发送结果处理:41051/41096 方向相反,别写反。"""
code = r.get('errcode')
if code == 0:
return '已送达'
if code == ERR_ALREADY:
return '41051 客户已开始聊天,欢迎语已由他方下发——勿重试'
if code == ERR_RACING and retry_left > 0:
return '41096 他应用抢占中——可重试(剩%d次)' % retry_left
return 'errcode=%s 记录待查' % code
if __name__ == '__main__':
now = time.time()
cb = ('<xml><Event><![CDATA[change_external_contact]]></Event>'
'<ChangeType><![CDATA[add_external_contact]]></ChangeType>'
'<UserID><![CDATA[sales_wang]]></UserID>'
'<ExternalUserID><![CDATA[wmAbc123]]></ExternalUserID>'
'<State><![CDATA[expo_qr]]></State>'
'<WelcomeCode><![CDATA[wc_20261105_001]]></WelcomeCode></xml>')
ev = parse_event(cb); ev['_arrived_at'] = now
act = build_welcome(ev, now)
print('① 正常链路:', json.dumps(act, ensure_ascii=False)[:150])
# ② 超窗事件
late = dict(ev); late['_arrived_at'] = now - 25
print('② 迟到25秒:', build_welcome(late, now)['skip'])
# ③ 无 welcome_code
nc = dict(ev); nc['WelcomeCode'] = ''
print('③ 无code:', build_welcome(nc, now)['skip'])
# ④ 未知渠道
uk = dict(ev); uk['State'] = 'weird_channel'
print('④ 未知渠道:', build_welcome(uk, now)['skip'])
# ⑤ 竞态码处理
print('⑤', handle_resp({'errcode': 41051}), '/', handle_resp({'errcode': 41096}))
实测输出:
① 正常链路: {"send": {"welcome_code": "wc_20261105_001", "text": {"content": "王工您好,我是展会上聊过产线的李团队。附件是方案与案例,约个演示随时说。"}, "attachments": []}, "deadline_left": "20.0s"}
② 迟到25秒: 超20秒窗口(官方硬限),放弃个性化,默认话术兜底
③ 无code: 无 welcome_code(默认欢迎语已配置/客户已聊天/名片自动递)
④ 未知渠道: 未知渠道 State='weird_channel',不发个性化避免说错话
⑤ 41051 客户已开始聊天,欢迎语已由他方下发——勿重试 / 41096 他应用抢占中——可重试(剩1次)
五条路径各有出口,核心原则一条:任何不确定,宁可让默认欢迎语兜底,也不发错话——欢迎语直达客户,是 AI 应用里少数"错了会被客户看见"的链路。
四、预生成池的可复制提示词(AI 批量写渠道欢迎语)
你是企微客户欢迎语写手。为渠道「{{channel}}」写3条备选欢迎语,存进预生成池:
硬约束:
1. 每条 ≤120 汉字(welcome text 会展示在聊天里,长了没人看)
2. 必须包含一个开放式提问或低门槛动作("约演示"/"回个1"),结尾不追问
3. 只允许用输入里出现的事实:渠道场景、我方业务、客户可能身份;
没有的信息用{{变量}}占位,不许编"贵司作为行业领军企业"式套话
4. 禁用词表:您好打扰了/亲/福利/优惠/限时/免费领
渠道场景描述:{{desc}} 我方业务一句话:{{biz}}
输出 JSON:[{"text":"...","use_when":"什么信号出现用这条"}]
这条是"离线批量生成",不受 20 秒限制——把创意压力从倒计时里挪出来,就是第二节的架构思想落到提示词层。
五、错误码表
| errcode | 含义 | 动作 |
|---|---|---|
| 41051 | 欢迎语已下发(客户已聊天) | 勿重试 |
| 41096 | 多应用抢占,他方正在发 | 可重试(与上条方向相反,写反即事故) |
| 93000 系 | external_userid 不合法/不在范围 | 核对客户与成员关系,重试无意义 |
| 事件无 WelcomeCode | 默认欢迎语已配/已聊天/自动递名片 | 正常分支,静默跳过 |
六、系列收官:企业微信 × AI 的完整地图
| 步骤 | 能力 | 一句话 |
|---|---|---|
| W1 | 消息→审批单 | 双输入+控件序列化,AI 填单不编造 |
| W2 | 审批→群播报 | webhook 模板卡片,状态自动回群 |
| W3 | access_token | 缓存+提前刷新+失效重试,全体系的底座 |
| W4 | 应用消息 | 定向到人+配额预算,机器人管不到的地方 |
| W5 | 接收消息 | 验签+AES 解密+排重,对话入口 |
| W6 | 卡片交互 | EventKey/ResponseCode 状态机,一键审批 |
| W7 | 智能机器人 | stream 全量刷新,大模型原生流式 |
| W8 | 客户联系 | welcome_code 20 秒窗口,对客第一句话 |
八步连起来:对内(审批/消息/机器人)+ 对外(客户欢迎语)的双向通路,每一步的接口事实都对齐了官方文档 2026-11-05 版。
七、参考与说明
本文系列涉及的提示词模板与配套教程合集,收录于提示岛(prompts.ssjintaitong.com)。
接口原文在企业微信开发者中心官方文档(管理后台侧栏可进入,或搜"发送新客户欢迎语"“客户联系事件格式”“获取客户详情”),本文 20 秒窗口、41051/41096 语义、不返回 welcome_code 的三种情形、State/unionid/字段隔离规则均以 2026-11-05 官方页面为准;第三节代码实测跑通(输出见正文)。
change_external_contact事件字段/WelcomeCode一次性20秒窗口/41051勿重试与41096可重试/无code三种情形/get详情cursor分页500人/第三方应用字段不可获取清单——逐条摘自企业微信开发者中心官方文档 2026-11-05 核对原文;代码本地真跑;示例客户/欢迎语池均为虚构占位。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)