refactor(wechat_kf): rename channel from wechatcom_kf and split corp_id

Rename the WeCom customer-service channel and give it its own corp_id
field so users no longer have to share `wechatcom_corp_id` with the
self-built WeCom app channel.

Renames (channel-side):
- channel type / const: wechatcom_kf -> wechat_kf
- package dir: channel/wechatcom_kf/ -> channel/wechat_kf/
- python files / classes: WechatComKf* -> WechatKf*
- config keys: wechatcom_kf_{secret,token,aes_key,port} ->
  wechat_kf_{secret,token,aes_key,port}; new wechat_kf_corp_id
- env vars: WECHATCOM_KF_* -> WECHAT_KF_*; new WECHAT_KF_CORP_ID
- log prefix / cursor file: [wechatcom_kf] -> [wechat_kf]
- web console CHANNEL_DEFS key + startup log line

Renames (docs):
- docs/channels/wecom-kf.mdx -> docs/channels/wechat-kf.mdx (zh/en/ja)
- update docs.json sidebar entries and all field names inside the docs

In addition, the Web Console "微信客服" entry now exposes its own
Corp ID field instead of reusing the wechatcom_app one, and includes
the screenshot of the visual config in the channel guide.

Web Console onboarding section is added (Tabs: Web Console / config
file) and the local URL `http://127.0.0.1:9899/` parenthetical is
dropped for consistency with other channel docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
6vision
2026-05-28 12:12:44 +08:00
parent 6721dbdbcc
commit 6e04ea8240
13 changed files with 139 additions and 139 deletions

View File

@@ -27,9 +27,9 @@ def create_channel(channel_type) -> Channel:
elif channel_type == "wechatcom_app":
from channel.wechatcom.wechatcomapp_channel import WechatComAppChannel
ch = WechatComAppChannel()
elif channel_type == const.WECHATCOM_KF:
from channel.wechatcom_kf.wechatcom_kf_channel import WechatComKfChannel
ch = WechatComKfChannel()
elif channel_type == const.WECHAT_KF:
from channel.wechat_kf.wechat_kf_channel import WechatKfChannel
ch = WechatKfChannel()
elif channel_type == const.FEISHU:
from channel.feishu.feishu_channel import FeiShuChanel
ch = FeiShuChanel()

View File

@@ -713,7 +713,7 @@ class WebChannel(ChatChannel):
logger.info("[WebChannel] 5. dingtalk - 钉钉")
logger.info("[WebChannel] 6. wecom_bot - 企微智能机器人")
logger.info("[WebChannel] 7. wechatcom_app - 企微自建应用")
logger.info("[WebChannel] 8. wechatcom_kf - 微信客服")
logger.info("[WebChannel] 8. wechat_kf - 微信客服")
logger.info("[WebChannel] 9. wechatmp - 个人公众号")
logger.info("[WebChannel] 10. wechatmp_service - 企业公众号")
logger.info("[WebChannel] ✅ Web控制台已运行")
@@ -1272,17 +1272,16 @@ class ChannelsHandler:
{"key": "wechatcomapp_port", "label": "Port", "type": "number", "default": 9898},
],
}),
("wechatcom_kf", {
("wechat_kf", {
"label": {"zh": "微信客服", "en": "WeCom Customer Service"},
"icon": "fa-headset",
"color": "emerald",
"fields": [
# wechatcom_corp_id is shared with wechatcom_app — same key, same value.
{"key": "wechatcom_corp_id", "label": "Corp ID", "type": "text"},
{"key": "wechatcom_kf_secret", "label": "Secret", "type": "secret"},
{"key": "wechatcom_kf_token", "label": "Token", "type": "secret"},
{"key": "wechatcom_kf_aes_key", "label": "AES Key", "type": "secret"},
{"key": "wechatcom_kf_port", "label": "Port", "type": "number", "default": 9888},
{"key": "wechat_kf_corp_id", "label": "Corp ID", "type": "text"},
{"key": "wechat_kf_secret", "label": "Secret", "type": "secret"},
{"key": "wechat_kf_token", "label": "Token", "type": "secret"},
{"key": "wechat_kf_aes_key", "label": "AES Key", "type": "secret"},
{"key": "wechat_kf_port", "label": "Port", "type": "number", "default": 9888},
],
}),
("wechatmp", {

View File

@@ -32,10 +32,10 @@
| 字段 | 来源 | 对应 CoW 配置项 |
|---|---|---|
| 企业IDCorpId | 「我的企业」最下方 | `wechatcom_corp_id` |
| Secret | 进入应用详情 → 点击「查看」(会推送到管理员手机端,在手机上查看) | `wechatcom_kf_secret` |
| Token | 应用「接收消息 → 设置API接收」 | `wechatcom_kf_token` |
| EncodingAESKey | 应用「接收消息 → 设置API接收」 | `wechatcom_kf_aes_key` |
| 企业IDCorpId | 「我的企业」最下方 | `wechat_kf_corp_id` |
| Secret | 进入应用详情 → 点击「查看」(会推送到管理员手机端,在手机上查看) | `wechat_kf_secret` |
| Token | 应用「接收消息 → 设置API接收」 | `wechat_kf_token` |
| EncodingAESKey | 应用「接收消息 → 设置API接收」 | `wechat_kf_aes_key` |
> AgentId 在本通道**不需要**(消息发送走的是 `cgi-bin/kf/send_msg`,不依赖 agent_id
@@ -63,25 +63,25 @@
```json
{
"channel_type": "wechatcom_kf",
"channel_type": "wechat_kf",
"wechatcom_corp_id": "ww1234567890abcdef",
"wechatcom_kf_secret": "<企微应用的 Secret>",
"wechatcom_kf_token": "<接收消息 Token>",
"wechatcom_kf_aes_key": "<EncodingAESKey>",
"wechatcom_kf_port": 9888
"wechat_kf_corp_id": "ww1234567890abcdef",
"wechat_kf_secret": "<企微应用的 Secret>",
"wechat_kf_token": "<接收消息 Token>",
"wechat_kf_aes_key": "<EncodingAESKey>",
"wechat_kf_port": 9888
}
```
| 字段 | 说明 |
|---|---|
| `wechatcom_corp_id` | 企业 ID,可与 `wechatcom_app` 共用 |
| `wechatcom_kf_secret` | **绑定到微信客服**的那个企微自建应用的 Secret |
| `wechatcom_kf_token` | 该应用「接收消息」配置的 Token |
| `wechatcom_kf_aes_key` | 该应用「接收消息」配置的 EncodingAESKey |
| `wechatcom_kf_port` | 监听端口,默认 `9888` |
| `wechat_kf_corp_id` | 企业 ID |
| `wechat_kf_secret` | **绑定到微信客服**的那个企微自建应用的 Secret |
| `wechat_kf_token` | 该应用「接收消息」配置的 Token |
| `wechat_kf_aes_key` | 该应用「接收消息」配置的 EncodingAESKey |
| `wechat_kf_port` | 监听端口,默认 `9888` |
也支持环境变量:`WECHATCOM_CORP_ID` / `WECHATCOM_KF_SECRET` / `WECHATCOM_KF_TOKEN` / `WECHATCOM_KF_AES_KEY`
也支持环境变量:`WECHAT_KF_CORP_ID` / `WECHAT_KF_SECRET` / `WECHAT_KF_TOKEN` / `WECHAT_KF_AES_KEY`
## 四、运行
@@ -92,8 +92,8 @@ python app.py
启动后日志里会看到:
```
[wechatcom_kf] WeCom customer-service channel started
[wechatcom_kf] Listening on http://0.0.0.0:9888/wxkf/
[wechat_kf] WeCom customer-service channel started
[wechat_kf] Listening on http://0.0.0.0:9888/wxkf/
```
回到企微后台「设置API接收」点击保存——会触发 `GET /wxkf/?...&echostr=...`CoW 通过 `crypto.check_signature` 校验后返回明文 `echostr`,验证成功。

View File

@@ -8,7 +8,7 @@ Differences from `channel/wechatcom/` (企微自建应用):
member `userid`.
3. Inbound flow: callback only delivers an event token, the actual
message bodies must be pulled via `cgi-bin/kf/sync_msg` with a
persistent cursor. See `wechatcom_kf_cursor_store.py`.
persistent cursor. See `wechat_kf_cursor_store.py`.
4. Outbound flow: messages are sent via `cgi-bin/kf/send_msg` (each
request must specify both `touser` and `open_kfid`); wechatpy has
no native helper, so we call the HTTP endpoint directly.
@@ -30,8 +30,8 @@ from wechatpy.exceptions import InvalidSignatureException, WeChatClientException
from bridge.context import Context
from bridge.reply import Reply, ReplyType
from channel.chat_channel import ChatChannel
from channel.wechatcom_kf.wechatcom_kf_cursor_store import CursorStore
from channel.wechatcom_kf.wechatcom_kf_message import WechatComKfMessage
from channel.wechat_kf.wechat_kf_cursor_store import CursorStore
from channel.wechat_kf.wechat_kf_message import WechatKfMessage
from common.log import logger
from common.singleton import singleton
from common.utils import (
@@ -46,7 +46,7 @@ try:
from voice.audio_convert import any_to_amr, split_audio
except ImportError as e: # voice features optional
logger.debug(
"[wechatcom_kf] import voice.audio_convert failed, voice will be disabled: {}".format(e)
"[wechat_kf] import voice.audio_convert failed, voice will be disabled: {}".format(e)
)
MAX_UTF8_LEN = 2048
@@ -55,18 +55,18 @@ SYNC_MSG_LIMIT = 1000
@singleton
class WechatComKfChannel(ChatChannel):
class WechatKfChannel(ChatChannel):
NOT_SUPPORT_REPLYTYPE = []
def __init__(self):
super().__init__()
self.corp_id = conf().get("wechatcom_corp_id")
self.secret = conf().get("wechatcom_kf_secret")
self.token = conf().get("wechatcom_kf_token")
self.aes_key = conf().get("wechatcom_kf_aes_key")
self.corp_id = conf().get("wechat_kf_corp_id")
self.secret = conf().get("wechat_kf_secret")
self.token = conf().get("wechat_kf_token")
self.aes_key = conf().get("wechat_kf_aes_key")
self._http_server = None
logger.info(
"[wechatcom_kf] Initializing WeCom customer-service channel, corp_id: {}".format(
"[wechat_kf] Initializing WeCom customer-service channel, corp_id: {}".format(
self.corp_id
)
)
@@ -81,18 +81,18 @@ class WechatComKfChannel(ChatChannel):
# Cursor file is an internal implementation detail — fixed under
# the project's `tmp/` dir, not exposed as a user-facing config.
cursor_path = os.path.join("tmp", "wechatcom_kf_cursors.json")
cursor_path = os.path.join("tmp", "wechat_kf_cursors.json")
self.cursor_store = CursorStore(cursor_path)
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
def startup(self):
urls = ("/wxkf/?", "channel.wechatcom_kf.wechatcom_kf_channel.Query")
urls = ("/wxkf/?", "channel.wechat_kf.wechat_kf_channel.Query")
app = web.application(urls, globals(), autoreload=False)
port = conf().get("wechatcom_kf_port", 9888)
logger.info("[wechatcom_kf] WeCom customer-service channel started")
logger.info("[wechatcom_kf] Listening on http://0.0.0.0:{}/wxkf/".format(port))
port = conf().get("wechat_kf_port", 9888)
logger.info("[wechat_kf] WeCom customer-service channel started")
logger.info("[wechat_kf] Listening on http://0.0.0.0:{}/wxkf/".format(port))
func = web.httpserver.StaticMiddleware(app.wsgifunc())
func = web.httpserver.LogMiddleware(func)
server = web.httpserver.WSGIServer(("0.0.0.0", port), func)
@@ -106,9 +106,9 @@ class WechatComKfChannel(ChatChannel):
if self._http_server:
try:
self._http_server.stop()
logger.info("[wechatcom_kf] HTTP server stopped")
logger.info("[wechat_kf] HTTP server stopped")
except Exception as e:
logger.warning(f"[wechatcom_kf] Error stopping HTTP server: {e}")
logger.warning(f"[wechat_kf] Error stopping HTTP server: {e}")
self._http_server = None
# ------------------------------------------------------------------
@@ -122,7 +122,7 @@ class WechatComKfChannel(ChatChannel):
if not external_userid or not open_kfid:
logger.error(
"[wechatcom_kf] missing external_userid or open_kfid, cannot send: "
"[wechat_kf] missing external_userid or open_kfid, cannot send: "
f"external_userid={external_userid}, open_kfid={open_kfid}"
)
return
@@ -132,13 +132,13 @@ class WechatComKfChannel(ChatChannel):
texts = split_string_by_utf8_length(reply_text, MAX_UTF8_LEN)
if len(texts) > 1:
logger.info(
"[wechatcom_kf] text too long, split into {} parts".format(len(texts))
"[wechat_kf] text too long, split into {} parts".format(len(texts))
)
for i, text in enumerate(texts):
self._send_text(external_userid, open_kfid, text)
if i != len(texts) - 1:
time.sleep(0.5)
logger.info("[wechatcom_kf] Do send text to {}: {}".format(receiver, reply_text))
logger.info("[wechat_kf] Do send text to {}: {}".format(receiver, reply_text))
elif reply.type == ReplyType.VOICE:
file_path = reply.content
@@ -148,7 +148,7 @@ class WechatComKfChannel(ChatChannel):
duration, files = split_audio(amr_file, 60 * 1000)
if len(files) > 1:
logger.info(
"[wechatcom_kf] voice too long {}s > 60s, split into {} parts".format(
"[wechat_kf] voice too long {}s > 60s, split into {} parts".format(
duration / 1000.0, len(files)
)
)
@@ -156,14 +156,14 @@ class WechatComKfChannel(ChatChannel):
for path in files:
with open(path, "rb") as f:
response = self.client.media.upload("voice", f)
logger.debug("[wechatcom_kf] upload voice response: {}".format(response))
logger.debug("[wechat_kf] upload voice response: {}".format(response))
media_ids.append(response["media_id"])
except ImportError as e:
logger.error("[wechatcom_kf] voice conversion failed: {}".format(e))
logger.error("[wechatcom_kf] please install pydub: pip install pydub")
logger.error("[wechat_kf] voice conversion failed: {}".format(e))
logger.error("[wechat_kf] please install pydub: pip install pydub")
return
except WeChatClientException as e:
logger.error("[wechatcom_kf] upload voice failed: {}".format(e))
logger.error("[wechat_kf] upload voice failed: {}".format(e))
return
try:
@@ -176,7 +176,7 @@ class WechatComKfChannel(ChatChannel):
for media_id in media_ids:
self._send_voice(external_userid, open_kfid, media_id)
time.sleep(1)
logger.info("[wechatcom_kf] sendVoice={}, receiver={}".format(reply.content, receiver))
logger.info("[wechat_kf] sendVoice={}, receiver={}".format(reply.content, receiver))
elif reply.type == ReplyType.IMAGE_URL:
img_url = reply.content
@@ -186,31 +186,31 @@ class WechatComKfChannel(ChatChannel):
image_storage.write(block)
sz = fsize(image_storage)
if sz >= 10 * 1024 * 1024:
logger.info("[wechatcom_kf] image too large, compressing, sz={}".format(sz))
logger.info("[wechat_kf] image too large, compressing, sz={}".format(sz))
image_storage = compress_imgfile(image_storage, 10 * 1024 * 1024 - 1)
image_storage.seek(0)
try:
response = self.client.media.upload("image", image_storage)
except WeChatClientException as e:
logger.error("[wechatcom_kf] upload image failed: {}".format(e))
logger.error("[wechat_kf] upload image failed: {}".format(e))
return
self._send_image(external_userid, open_kfid, response["media_id"])
logger.info("[wechatcom_kf] sendImage url={}, receiver={}".format(img_url, receiver))
logger.info("[wechat_kf] sendImage url={}, receiver={}".format(img_url, receiver))
elif reply.type == ReplyType.IMAGE:
image_storage = reply.content
sz = fsize(image_storage)
if sz >= 10 * 1024 * 1024:
logger.info("[wechatcom_kf] image too large, compressing, sz={}".format(sz))
logger.info("[wechat_kf] image too large, compressing, sz={}".format(sz))
image_storage = compress_imgfile(image_storage, 10 * 1024 * 1024 - 1)
image_storage.seek(0)
try:
response = self.client.media.upload("image", image_storage)
except WeChatClientException as e:
logger.error("[wechatcom_kf] upload image failed: {}".format(e))
logger.error("[wechat_kf] upload image failed: {}".format(e))
return
self._send_image(external_userid, open_kfid, response["media_id"])
logger.info("[wechatcom_kf] sendImage, receiver={}".format(receiver))
logger.info("[wechat_kf] sendImage, receiver={}".format(receiver))
elif reply.type == ReplyType.VIDEO_URL:
video_url = reply.content
@@ -219,10 +219,10 @@ class WechatComKfChannel(ChatChannel):
"video", requests.get(video_url, stream=True).content
)
except WeChatClientException as e:
logger.error("[wechatcom_kf] upload video failed: {}".format(e))
logger.error("[wechat_kf] upload video failed: {}".format(e))
return
self._send_video(external_userid, open_kfid, response["media_id"])
logger.info("[wechatcom_kf] sendVideo url={}, receiver={}".format(video_url, receiver))
logger.info("[wechat_kf] sendVideo url={}, receiver={}".format(video_url, receiver))
elif reply.type == ReplyType.FILE:
file_path = reply.content
@@ -232,13 +232,13 @@ class WechatComKfChannel(ChatChannel):
"file", (os.path.basename(file_path), f.read())
)
except WeChatClientException as e:
logger.error("[wechatcom_kf] upload file failed: {}".format(e))
logger.error("[wechat_kf] upload file failed: {}".format(e))
return
self._send_file(external_userid, open_kfid, response["media_id"])
logger.info("[wechatcom_kf] sendFile={}, receiver={}".format(file_path, receiver))
logger.info("[wechat_kf] sendFile={}, receiver={}".format(file_path, receiver))
else:
logger.warning("[wechatcom_kf] unsupported reply type: {}".format(reply.type))
logger.warning("[wechat_kf] unsupported reply type: {}".format(reply.type))
# ------------------------------------------------------------------
# Inbound — pull messages by cursor
@@ -263,9 +263,9 @@ class WechatComKfChannel(ChatChannel):
return
for raw in msgs:
try:
kf_msg = WechatComKfMessage(msg=raw, client=self.client)
kf_msg = WechatKfMessage(msg=raw, client=self.client)
except NotImplementedError as e:
logger.debug("[wechatcom_kf] {}".format(e))
logger.debug("[wechat_kf] {}".format(e))
continue
context = self._compose_context(
kf_msg.ctype,
@@ -300,7 +300,7 @@ class WechatComKfChannel(ChatChannel):
break
next_cursor = cursor_after
logger.info(
"[wechatcom_kf] first-start bootstrap finished for open_kfid={}, "
"[wechat_kf] first-start bootstrap finished for open_kfid={}, "
"skipped {} historical messages".format(open_kfid, total_skipped)
)
@@ -332,7 +332,7 @@ class WechatComKfChannel(ChatChannel):
if collected:
collected = _dedup_image_text_pair(collected)
logger.info(
"[wechatcom_kf] pulled {} messages for open_kfid={}".format(len(collected), open_kfid)
"[wechat_kf] pulled {} messages for open_kfid={}".format(len(collected), open_kfid)
)
return collected
@@ -351,12 +351,12 @@ class WechatComKfChannel(ChatChannel):
try:
resp = requests.post(url, json=payload, timeout=10).json()
except Exception as e:
logger.error(f"[wechatcom_kf] sync_msg request failed: {e}")
logger.error(f"[wechat_kf] sync_msg request failed: {e}")
return None
if resp.get("errcode") != 0:
logger.error(
f"[wechatcom_kf] sync_msg errcode={resp.get('errcode')}, "
f"[wechat_kf] sync_msg errcode={resp.get('errcode')}, "
f"errmsg={resp.get('errmsg')}, open_kfid={open_kfid}"
)
return None
@@ -370,10 +370,10 @@ class WechatComKfChannel(ChatChannel):
try:
resp = requests.post(url, json=payload, timeout=10).json()
except Exception as e:
logger.error(f"[wechatcom_kf] send_msg request failed: {e}")
logger.error(f"[wechat_kf] send_msg request failed: {e}")
return {"errcode": -1, "errmsg": str(e)}
if resp.get("errcode") != 0:
logger.error(f"[wechatcom_kf] send_msg failed, payload={payload}, resp={resp}")
logger.error(f"[wechat_kf] send_msg failed, payload={payload}, resp={resp}")
return resp
def _send_text(self, external_userid: str, open_kfid: str, content: str) -> dict:
@@ -451,9 +451,9 @@ def _dedup_image_text_pair(messages: list) -> list:
# ----------------------------------------------------------------------
class Query:
def GET(self):
channel = WechatComKfChannel()
channel = WechatKfChannel()
params = web.input()
logger.info("[wechatcom_kf] verify params: {}".format(params))
logger.info("[wechat_kf] verify params: {}".format(params))
try:
signature = params.msg_signature
timestamp = params.timestamp
@@ -465,7 +465,7 @@ class Query:
return echostr
def POST(self):
channel = WechatComKfChannel()
channel = WechatKfChannel()
params = web.input()
try:
signature = params.msg_signature
@@ -474,7 +474,7 @@ class Query:
raw_body = web.data()
decrypted = channel.crypto.decrypt_message(raw_body, signature, timestamp, nonce)
except (InvalidSignatureException, InvalidCorpIdException) as e:
logger.warning(f"[wechatcom_kf] invalid signature: {e}")
logger.warning(f"[wechat_kf] invalid signature: {e}")
raise web.Forbidden()
# We need the Token + OpenKfId fields from the inner XML to call
@@ -483,14 +483,14 @@ class Query:
try:
root = ET.fromstring(decrypted)
except ET.ParseError as e:
logger.error(f"[wechatcom_kf] xml parse error: {e}")
logger.error(f"[wechat_kf] xml parse error: {e}")
return "success"
msg_type = (root.findtext("MsgType") or "").strip()
event = (root.findtext("Event") or "").strip()
if msg_type != "event" or event != "kf_msg_or_event":
logger.debug(
f"[wechatcom_kf] ignored callback msg_type={msg_type}, event={event}"
f"[wechat_kf] ignored callback msg_type={msg_type}, event={event}"
)
return "success"
@@ -498,12 +498,12 @@ class Query:
open_kfid = root.findtext("OpenKfId") or ""
if not token or not open_kfid:
logger.warning(
f"[wechatcom_kf] callback missing token or open_kfid: {decrypted}"
f"[wechat_kf] callback missing token or open_kfid: {decrypted}"
)
return "success"
try:
channel.consume_callback(token, open_kfid)
except Exception as e:
logger.exception(f"[wechatcom_kf] consume_callback error: {e}")
logger.exception(f"[wechat_kf] consume_callback error: {e}")
return "success"

View File

@@ -37,7 +37,7 @@ class CursorStore:
with open(self._file_path, "r", encoding="utf-8") as f:
return json.load(f) or {}
except Exception as e:
logger.warning(f"[wechatcom_kf] failed to load cursor file {self._file_path}: {e}")
logger.warning(f"[wechat_kf] failed to load cursor file {self._file_path}: {e}")
return {}
def _flush_locked(self):
@@ -49,7 +49,7 @@ class CursorStore:
json.dump(self._data, f, ensure_ascii=False)
os.replace(tmp_path, self._file_path)
except Exception as e:
logger.warning(f"[wechatcom_kf] failed to flush cursor file {self._file_path}: {e}")
logger.warning(f"[wechat_kf] failed to flush cursor file {self._file_path}: {e}")
try:
if os.path.exists(tmp_path):
os.remove(tmp_path)

View File

@@ -11,7 +11,7 @@ from common.log import logger
from common.tmp_dir import TmpDir
class WechatComKfMessage(ChatMessage):
class WechatKfMessage(ChatMessage):
"""
msg structure (from cgi-bin/kf/sync_msg):
{
@@ -54,7 +54,7 @@ class WechatComKfMessage(ChatMessage):
with open(self.content, "wb") as f:
f.write(response.content)
else:
logger.info(f"[wechatcom_kf] Failed to download image, {response.content}")
logger.info(f"[wechat_kf] Failed to download image, {response.content}")
self._prepare_fn = download_image
elif self.msgtype == "voice":
@@ -69,12 +69,12 @@ class WechatComKfMessage(ChatMessage):
with open(self.content, "wb") as f:
f.write(response.content)
else:
logger.info(f"[wechatcom_kf] Failed to download voice, {response.content}")
logger.info(f"[wechat_kf] Failed to download voice, {response.content}")
self._prepare_fn = download_voice
else:
raise NotImplementedError(
f"[wechatcom_kf] Unsupported message type: {self.msgtype}"
f"[wechat_kf] Unsupported message type: {self.msgtype}"
)
self.from_user_id = self.external_userid