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

2
app.py
View File

@@ -231,7 +231,7 @@ def _clear_singleton_cache(channel_name: str):
"wechatmp": "channel.wechatmp.wechatmp_channel.WechatMPChannel",
"wechatmp_service": "channel.wechatmp.wechatmp_channel.WechatMPChannel",
"wechatcom_app": "channel.wechatcom.wechatcomapp_channel.WechatComAppChannel",
const.WECHATCOM_KF: "channel.wechatcom_kf.wechatcom_kf_channel.WechatComKfChannel",
const.WECHAT_KF: "channel.wechat_kf.wechat_kf_channel.WechatKfChannel",
const.FEISHU: "channel.feishu.feishu_channel.FeiShuChanel",
const.DINGTALK: "channel.dingtalk.dingtalk_channel.DingTalkChanel",
const.WECOM_BOT: "channel.wecom_bot.wecom_bot_channel.WecomBotChannel",

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

View File

@@ -227,4 +227,4 @@ DINGTALK = "dingtalk"
WECOM_BOT = "wecom_bot"
QQ = "qq"
WEIXIN = "weixin"
WECHATCOM_KF = "wechatcom_kf" # WeCom customer service (微信客服) channel
WECHAT_KF = "wechat_kf" # WeCom customer service (微信客服) channel

View File

@@ -151,12 +151,12 @@ available_setting = {
"wechatcomapp_secret": "", # 企业微信app的secret
"wechatcomapp_agent_id": "", # 企业微信app的agent_id
"wechatcomapp_aes_key": "", # 企业微信app的aes_key
# 微信客服(wechatcom_kf)的配置
# 注意: 微信客服与企微自建应用是两套不同的应用,共用 corp_id,但 secret/token/aes_key 各自独立
"wechatcom_kf_token": "", # 微信客服回调token
"wechatcom_kf_port": 9888, # 微信客服回调服务端口
"wechatcom_kf_secret": "", # 微信客服应用的secret
"wechatcom_kf_aes_key": "", # 微信客服回调aes_key
# 微信客服(wechat_kf)的配置
"wechat_kf_corp_id": "", # 微信客服所在企业的corp_id
"wechat_kf_token": "", # 微信客服回调token
"wechat_kf_port": 9888, # 微信客服回调服务端口
"wechat_kf_secret": "", # 微信客服应用的secret
"wechat_kf_aes_key": "", # 微信客服回调aes_key
# 飞书配置
"feishu_port": 80, # 飞书bot监听端口仅webhook模式需要
"feishu_app_id": "", # 飞书机器人应用APP Id
@@ -180,7 +180,7 @@ available_setting = {
# chatgpt指令自定义触发词
"clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头
# channel配置
"channel_type": "", # 通道类型,支持多渠道同时运行。单个: "feishu",多个: "feishu, dingtalk" 或 ["feishu", "dingtalk"]。可选值: web,feishu,dingtalk,wecom_bot,weixin,wechatmp,wechatmp_service,wechatcom_app,wechatcom_kf
"channel_type": "", # 通道类型,支持多渠道同时运行。单个: "feishu",多个: "feishu, dingtalk" 或 ["feishu", "dingtalk"]。可选值: web,feishu,dingtalk,wecom_bot,weixin,wechatmp,wechatmp_service,wechatcom_app,wechat_kf
"web_console": True, # 是否自动启动Web控制台默认启动。设为False可禁用
"subscribe_msg": "", # 订阅消息, 支持: wechatmp, wechatmp_service, wechatcom_app
"debug": False, # 是否开启debug模式开启后会打印更多日志
@@ -417,9 +417,10 @@ def load_config():
"wechatcomapp_agent_id": "WECHATCOMAPP_AGENT_ID",
"wechatcomapp_secret": "WECHATCOMAPP_SECRET",
"wechatcom_corp_id": "WECHATCOM_CORP_ID",
"wechatcom_kf_secret": "WECHATCOM_KF_SECRET",
"wechatcom_kf_token": "WECHATCOM_KF_TOKEN",
"wechatcom_kf_aes_key": "WECHATCOM_KF_AES_KEY",
"wechat_kf_corp_id": "WECHAT_KF_CORP_ID",
"wechat_kf_secret": "WECHAT_KF_SECRET",
"wechat_kf_token": "WECHAT_KF_TOKEN",
"wechat_kf_aes_key": "WECHAT_KF_AES_KEY",
"qq_app_id": "QQ_APP_ID",
"qq_app_secret": "QQ_APP_SECRET",
"weixin_token": "WEIXIN_TOKEN",

View File

@@ -27,7 +27,7 @@ description: 将 CowAgent 接入微信客服WeCom Customer Service
<img src="https://cdn.link-ai.tech/doc/20260228103156.png" width="480"/>
2. 点击 **我的企业**,在最下方获取 **企业ID**(后续填写到 `wechatcom_corp_id`
2. 点击 **我的企业**,在最下方获取 **企业ID**(后续填写到 `wechat_kf_corp_id`
<img src="https://cdn.link-ai.tech/doc/wechatcom-hosting-companyid.png" width="600"/>
@@ -58,22 +58,22 @@ description: 将 CowAgent 接入微信客服WeCom Customer Service
```json
{
"channel_type": "wechatcom_kf",
"wechatcom_corp_id": "YOUR_CORP_ID",
"wechatcom_kf_secret": "YOUR_SECRET",
"wechatcom_kf_token": "YOUR_TOKEN",
"wechatcom_kf_aes_key": "YOUR_AES_KEY",
"wechatcom_kf_port": 9888
"channel_type": "wechat_kf",
"wechat_kf_corp_id": "YOUR_CORP_ID",
"wechat_kf_secret": "YOUR_SECRET",
"wechat_kf_token": "YOUR_TOKEN",
"wechat_kf_aes_key": "YOUR_AES_KEY",
"wechat_kf_port": 9888
}
```
| 参数 | 说明 |
| --- | --- |
| `wechatcom_corp_id` | 企业 ID |
| `wechatcom_kf_secret` | 绑定到微信客服的那个企微自建应用的 Secret |
| `wechatcom_kf_token` | API 接收配置中的 Token |
| `wechatcom_kf_aes_key` | API 接收配置中的 EncodingAESKey |
| `wechatcom_kf_port` | 监听端口,默认 9888 |
| `wechat_kf_corp_id` | 企业 ID |
| `wechat_kf_secret` | 绑定到微信客服的那个企微自建应用的 Secret |
| `wechat_kf_token` | API 接收配置中的 Token |
| `wechat_kf_aes_key` | API 接收配置中的 EncodingAESKey |
| `wechat_kf_port` | 监听端口,默认 9888 |
</Tab>
</Tabs>

View File

@@ -188,7 +188,7 @@
"channels/wecom-bot",
"channels/qq",
"channels/wecom",
"channels/wecom-kf",
"channels/wechat-kf",
"channels/wechatmp"
]
}
@@ -381,7 +381,7 @@
"en/channels/wecom-bot",
"en/channels/qq",
"en/channels/wecom",
"en/channels/wecom-kf",
"en/channels/wechat-kf",
"en/channels/wechatmp"
]
}
@@ -576,7 +576,7 @@
"ja/channels/wecom-bot",
"ja/channels/qq",
"ja/channels/wecom",
"ja/channels/wecom-kf",
"ja/channels/wechat-kf",
"ja/channels/wechatmp"
]
}

View File

@@ -27,7 +27,7 @@ Required resources:
<img src="https://cdn.link-ai.tech/doc/20260228103156.png" width="480"/>
2. Click **My Enterprise** and find the **Corp ID** at the bottom of the page (it goes into `wechatcom_corp_id`):
2. Click **My Enterprise** and find the **Corp ID** at the bottom of the page (it goes into `wechat_kf_corp_id`):
<img src="https://cdn.link-ai.tech/doc/wechatcom-hosting-companyid.png" width="600"/>
@@ -58,22 +58,22 @@ Fill in the 4 fields collected from the previous step (Corp ID / Secret / Token
```json
{
"channel_type": "wechatcom_kf",
"wechatcom_corp_id": "YOUR_CORP_ID",
"wechatcom_kf_secret": "YOUR_SECRET",
"wechatcom_kf_token": "YOUR_TOKEN",
"wechatcom_kf_aes_key": "YOUR_AES_KEY",
"wechatcom_kf_port": 9888
"channel_type": "wechat_kf",
"wechat_kf_corp_id": "YOUR_CORP_ID",
"wechat_kf_secret": "YOUR_SECRET",
"wechat_kf_token": "YOUR_TOKEN",
"wechat_kf_aes_key": "YOUR_AES_KEY",
"wechat_kf_port": 9888
}
```
| Parameter | Description |
| --- | --- |
| `wechatcom_corp_id` | Corp ID |
| `wechatcom_kf_secret` | Secret of the WeCom custom app bound to Customer Service |
| `wechatcom_kf_token` | Token from the API reception config |
| `wechatcom_kf_aes_key` | EncodingAESKey from the API reception config |
| `wechatcom_kf_port` | Listening port, default 9888 |
| `wechat_kf_corp_id` | Corp ID |
| `wechat_kf_secret` | Secret of the WeCom custom app bound to Customer Service |
| `wechat_kf_token` | Token from the API reception config |
| `wechat_kf_aes_key` | EncodingAESKey from the API reception config |
| `wechat_kf_port` | Listening port, default 9888 |
</Tab>
</Tabs>

View File

@@ -27,7 +27,7 @@ WeCom の自建アプリを「微信客服WeCom Customer Service」アカ
<img src="https://cdn.link-ai.tech/doc/20260228103156.png" width="480"/>
2. **自社情報** をクリックし、ページ下部で **企業IDCorp ID** を確認します(`wechatcom_corp_id` に設定します):
2. **自社情報** をクリックし、ページ下部で **企業IDCorp ID** を確認します(`wechat_kf_corp_id` に設定します):
<img src="https://cdn.link-ai.tech/doc/wechatcom-hosting-companyid.png" width="600"/>
@@ -58,22 +58,22 @@ WeCom の自建アプリを「微信客服WeCom Customer Service」アカ
```json
{
"channel_type": "wechatcom_kf",
"wechatcom_corp_id": "YOUR_CORP_ID",
"wechatcom_kf_secret": "YOUR_SECRET",
"wechatcom_kf_token": "YOUR_TOKEN",
"wechatcom_kf_aes_key": "YOUR_AES_KEY",
"wechatcom_kf_port": 9888
"channel_type": "wechat_kf",
"wechat_kf_corp_id": "YOUR_CORP_ID",
"wechat_kf_secret": "YOUR_SECRET",
"wechat_kf_token": "YOUR_TOKEN",
"wechat_kf_aes_key": "YOUR_AES_KEY",
"wechat_kf_port": 9888
}
```
| パラメータ | 説明 |
| --- | --- |
| `wechatcom_corp_id` | 企業 ID |
| `wechatcom_kf_secret` | カスタマーサービスにバインドした企業微信自建アプリの Secret |
| `wechatcom_kf_token` | API 受信設定の Token |
| `wechatcom_kf_aes_key` | API 受信設定の EncodingAESKey |
| `wechatcom_kf_port` | リスンポート、デフォルトは 9888 |
| `wechat_kf_corp_id` | 企業 ID |
| `wechat_kf_secret` | カスタマーサービスにバインドした企業微信自建アプリの Secret |
| `wechat_kf_token` | API 受信設定の Token |
| `wechat_kf_aes_key` | API 受信設定の EncodingAESKey |
| `wechat_kf_port` | リスンポート、デフォルトは 9888 |
</Tab>
</Tabs>