会员身份识别与渠道绑定
触发场景
会员通过微信/飞书/企微等外部渠道联系教练,会话开头需要先确认身份("您好/嗨"之后报名字或直接问事)。
匹配顺序(2026-08-28 微信渠道验证通过)
- 先按渠道ID匹配(不先问名字):
- 微信渠道:c.find_member_by_weixin_id(weixin_id)
- 飞书渠道:c.find_member_by_openid(openid)
- 返回统一结构:{"found": bool, "name": ..., "record_id": ...} - 匹配不上 → 问姓名 →
c.find_member_by_name("姓名")(返回同结构,found: True时拿到 record_id) - 匹配成功 → 回写渠道ID到会员表,下次同渠道联系自动识别,不用再问名字
绑定签名(🔴 两个坑)
import sys; sys.path.insert(0, "/root/.hermes/profiles/coach/knowledge")
import coach_feishu as cf
c = cf.CoachFeishu()
r = c.save_member_weixin_id("recvo7Lrmhofc7", "[email protected]") # (record_id, weixin_id)
# 成功 → {"status": "ok"}
- 🔴
save_member_weixin_id(record_id, weixin_id)需要两个位置参数,record_id 在前。只传 weixin_id 一个参数报TypeError: missing 1 required positional argument: 'weixin_id'。record_id 来自find_member_by_name/find_member_by_weixin_id的返回值 - 🔴 所有查询/绑定方法都是实例方法 — 必须先
c = cf.CoachFeishu()。模块级调用cf.find_member_by_name(...)报AttributeError: module 'coach_feishu' has no attribute 'find_member_by_name'(用 terminal heredoc 跑,勿用 execute_code)
已确认方法清单(2026-08-28 模块内核对)
| 方法 | 参数 | 用途 |
|---|---|---|
find_member_by_name(name) |
name | 按姓名查(兜底) |
find_member_by_openid(openid) |
openid | 飞书渠道识别 |
find_member_by_weixin_id(weixin_id) |
weixin_id | 微信渠道识别 |
save_member_openid(record_id, openid) |
两个位置参数 | 回写飞书ID |
save_member_weixin_id(record_id, weixin_id) |
两个位置参数 | 回写微信ID |
陌生 openid 主动发消息:先定人,再定「会员 or admin」
- 新 chat_id ≠ 新的人:飞书里每个人(含会员)与 bot 各有自己的 DM chat_id,马可的私聊 chat_id 只是其中一个。先
find_member_by_openid(openid)定人,再谈别的。 - admin 名单看 config:
grep -n -A5 "admins:" /root/.hermes/profiles/coach/config.yaml(马可 =ou_e23c26d0b63227efa9ac80a179c602c3)。 - openid 在名单 → admin 模式,可查所有会员
- openid 不在名单 → 会员自助模式:只服务他本人的数据,不要透露他人信息
- 会员档案
飞书OpenID字段有值 = 该会员本人已接入渠道,直接服务,不要再问姓名;匹配成功即cf.bind_member(姓名, "external")后开始干活(绑定后查询方法都是无参的,见下)。
文字说"发了图片"、但这条消息 media=0 → 先别回"没收到图"
飞书图片是独立消息,通常滞后几秒到几十秒到达。正确顺序:
grep "Inbound dm message received" /root/.hermes/profiles/coach/logs/gateway.log | tail -20—— 看同一 chat_id 后面有没有type=photo … media=1的消息;有就等这批到齐再处理。- 落盘路径
/root/.hermes/profiles/coach/cache/images/img_*.jpg。目录里会留历史旧图,必须按 mtime 取最新的几张(ls -lt或find -newermt),不要按文件名猜、也不要把昨天的图当今天的。 - 确实一张都没到,再回会员"图片没收到,麻烦重发"。
绑定之后怎么查:getter 全部无参,返回的是飞书原始结构
cf.bind_member(r["name"], "external") # 先绑定(姓名 + 来源),外部会员传 "external"
c = cf.CoachFeishu() # 再实例化
c.get_member_info() # 无参!传姓名 → TypeError: takes 1 positional argument but 2 were given
get_my_body_tests() / get_my_training_logs() / get_my_diet_records() / get_my_training_plans() / get_my_evaluations()同样无参,取的都是当前绑定的那个会员的数据——换人必须重新bind_member(忘记重绑会把前一个人的数据当成本人的)。- 返回的是飞书原始字段结构:键是中文(
训练日期毫秒时间戳、动作名称是[{"text": …}]富文本数组、关联会员是{"link_record_ids": [...]})。按自己猜的字段名取值只会得到一串{}→ 先print(json.dumps(rec, ensure_ascii=False))看键名再解析。 - 会员自助(
external绑定)只能查本人数据;admin 查询用白名单里的飞书 OpenID。 - 每次答复后留痕:
c.log_qa(question="…", category="饮食营养/动作疑问/…", result="🟢已自动解答", level="🟢常规", ai_answer="…"),成功返回{"status": "ok", "record_id": …}。
判断会员是否已接入渠道
看会员档案(get_member_info())里两个字段的区别:
- 飞书OpenID:会员本人的 open_id → 已接入飞书,方案可直推
- 飞书用户:可能绑的是马可 → 会员未接入飞书,方案需输出给马可转发
回写新渠道ID前先看这两个字段,避免重复绑定或误判。
验证
绑定后同会话内再调 find_member_by_weixin_id(weixin_id) 应返回 found: True。