售后群里常出现这种场面:接口返回 code == 0,气泡上也有个蓝色「@李工」,李工的手机却没震动。把文案改成「@李工-北京」再发,有时蓝字都没了。

这不是发送失败,是 展示名和提醒对象用错了字段。客户群指定 @ 做不到,还有另一条更常见的原因:消息根本不是群成员发的,而是官方群机器人通道。

1. 先确认你走的是哪条通道

通道能不能进客户群@ 能不能产生强提醒发送身份
官方群机器人 / 消息推送不能,客户群没有添加入口内部群里 @userid 也经常无提醒机器人
官方客户群群发能选群不是点名某个人处理要员工确认
已在群内的企微账号 + 消息接口人在群即可接近手工 @企业微信成员

官方机器人走的是群 Webhook 地址,不是群成员身份。客户群没有「添加机器人」,在内部群里即使用了 @userid,对端也常常没有会话强提醒。要在客户群里出现可点击、可提醒的 @,消息必须由 已经在该群里的企业微信账号 发出。

后面所有参数,都建立在「账号级消息接口」上,不再讨论官方机器人。

2. 三种「名字」不要混

客户群里同一个人至少有三套称呼:

你在界面上看到的能不能当接口主键改了会怎样
微信昵称不能随时改,群里其他人看到的不一样
群名片 / 群昵称不能只影响气泡上的字
通讯录 userId退群前稳定,提醒认这个

还有两个会话 ID 会一起错:

字段填什么填错的表现
guid扫码登录后的执行节点掉线、过期,整条发送失败
toId群聊填 roomId填成 userId,消息进私聊,群里没有 @

不要用昵称、备注、群名片做主键。重名、改名、一人多个群名片时,都会 @ 错人。

3. 一次指定 @ 要凑齐的参数

统一入口(Host 由你的接入环境决定):

POST /api/qw/doApi
Content-Type: application/json
X-QIWEI-TOKEN: YOUR_TOKEN
import requests

API = "/api/qw/doApi"
HEADERS = {
    "X-QIWEI-TOKEN": "YOUR_TOKEN",
    "Content-Type": "application/json",
}


def do_api(method, params, timeout=30):
    r = requests.post(
        API,
        headers=HEADERS,
        json={"method": method, "params": params},
        timeout=timeout,
    )
    body = r.json()
    if body.get("code") != 0:
        raise RuntimeError(body)
    return body


def send_group_text(guid, room_id, content):
    return do_api("/msg/sendText", {
        "guid": guid,
        "toId": room_id,
        "content": content,
        "isNoNeedRead": True,
    })


def send_group_at(guid, room_id, content, at_user_ids):
    """
    at 字段名以你实际消息模块为准:
    atlist / mentionList / 混合文本节点里的 mention。
    判断标准只有一个:列表里必须是 userId。
    """
    return do_api("/msg/sendText", {
        "guid": guid,
        "toId": room_id,
        "content": content,
        "atlist": at_user_ids,
        "isNoNeedRead": True,
    })

混合文本接口在部分环境里 @ 更稳,逻辑不变:toId = roomId,被 @ 人走独立列表,content 里的 @群名片 只负责展示。

一次指定 @ 至少凑齐:

  1. guid
  2. roomId(给 toId
  3. 被 @ 人的 userId(给 @ 列表)
  4. content 里与当前群名片一致的 @名字(给人看)

4. 对照实验:建议按这个顺序做

准备两个测试号 A、B,都在同一客户群。A 是执行账号。

实验 1:只发字符串

send_group_text(guid, room_id, "@李工 看下质检单")

群里有「@李工」四个字,点不出去,B 无提醒。这只是普通文本。code == 0 在这里没有意义,它只说明文本发出去了。

实验 2:文案对、ID 错

send_group_at(guid, room_id, "@李工 看下质检单", at_user_ids=["李工-夜班"])

接口可能直接失败,或发出去仍无提醒。@ 列表不接受名片字符串。

实验 3:文案用群名片,列表用 userId

# 群名片此时是「李工-夜班」
send_group_at(
    guid,
    room_id,
    "@李工-夜班 看下质检单",
    at_user_ids=[b_user_id],
)

B 应出现可点蓝字和提醒。把 B 的群名片改成「质检-李工」再发,at 列表不变,提醒仍应打到 B。蓝字文案要跟着新名片改,否则会出现「点进去是对的人,字却对不上」。

实验 4:toId 填成人

send_group_at(guid, b_user_id, "@李工-夜班 看下质检单", at_user_ids=[b_user_id])

消息进私聊。群里没有这条,最像「@ 失败」。

实验 5:@ 两个测试号

atlist 传两个 userIdcontent 里两个 @群名片 顺序与列表一致(若接口有顺序要求)。只被 @ 的人有提醒,第三人无提醒。

5. userId 从哪取

固定角色(值班、群主、质检对接)
拉该 roomId 的成员列表,落地:

guid
roomId
userId
群名片(可过期)
是否还在群(定时刷新)

发送前用 userIdcontent 用缓存的群名片。名片刷新失败时,宁可 @ 成功但蓝字略旧,也不要用名片当 ID。

临时点名(客户刚在群里问「质检单」)
从 Webhook 取发送者 ID 和 roomId,不必全量同步:

def on_group_message(event):
    room_id = event.get("roomId")
    sender_id = event.get("fromUserId")
    nick = event.get("fromName") or "群成员"
    text = event.get("content") or ""
    if "质检单" not in text:
        return
    send_group_at(
        guid=GUID,
        room_id=room_id,
        content=f"@{nick} 质检单已生成,请查看",
        at_user_ids=[sender_id],
    )

回调里的展示名只写进 content,不要写进 @ 列表。字段名以实际回调为准,能确定的是:群走 roomId,人走 userId

成员已退群、执行账号已退群、目标 userId 不属于该 roomId,@ 会无效或接口失败。不要静默当成发送成功。

6. 不要用 @all 代替指定 @

客户群点名是「请这个人处理」。@ 全员容易被屏蔽,也掩盖了「ID 有没有填对」。内部群喊全员、客户群点名值班,不是同一需求。

先把单人 @ 的四个实验做完,再考虑多人列表。不要一上来 @all。

7. 联调验收清单

  1. 不带 @ 列表的群文本:code == 0isSendSuccess == 1,群里能看到
  2. 带单个 userId:气泡有蓝色可点名字
  3. 两个测试号:被 @ 的有提醒,未被 @ 的无提醒
  4. 换未在群内的 userId:失败或 @ 无效,不能静默发进群
  5. toIduserId:确认消息在私聊而不是群
  6. 改群名片后再发:提醒对象不变,只改 content 字符串
  7. 执行账号退群后再发:应失败,而不是发到别的群

日志至少记:guidroomIdat_user_idscontent 摘要、回包 code、会话里是否有蓝字(人工勾)。出问题先对这张表,不要先改文案。

总结

客户群 @ 不到人,先排除官方机器人通道,再排除 toId 填成了人。文案用群名片,提醒用 userId,群用 roomId。名片改了只改字符串,不要改 ID。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐