mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-19 12:47:25 +08:00
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:
74
channel/wechat_kf/wechat_kf_cursor_store.py
Normal file
74
channel/wechat_kf/wechat_kf_cursor_store.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- coding=utf-8 -*-
|
||||
"""
|
||||
Local-file based persistence for WeCom customer-service `next_cursor`.
|
||||
|
||||
Why we need this:
|
||||
The WeCom customer-service (微信客服) callback only notifies us that
|
||||
"new messages exist". To actually fetch them we must call the
|
||||
`cgi-bin/kf/sync_msg` endpoint with a `cursor` so that we only get
|
||||
messages newer than the previously processed one. If we lose this
|
||||
cursor (e.g. on process restart) WeCom will replay up to ~14 days of
|
||||
history, which would cause the bot to flood users with duplicate
|
||||
replies.
|
||||
|
||||
This implementation deliberately avoids any external dependency
|
||||
(no Redis / no DB) — a single JSON file under the project's tmp dir is
|
||||
enough for a CoW-style single-process deployment.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
from common.log import logger
|
||||
|
||||
|
||||
class CursorStore:
|
||||
"""Thread-safe per-`open_kfid` cursor store backed by a JSON file."""
|
||||
|
||||
def __init__(self, file_path: str):
|
||||
self._file_path = file_path
|
||||
self._lock = threading.Lock()
|
||||
self._data = self._load()
|
||||
|
||||
def _load(self) -> dict:
|
||||
try:
|
||||
if os.path.exists(self._file_path):
|
||||
with open(self._file_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f) or {}
|
||||
except Exception as e:
|
||||
logger.warning(f"[wechat_kf] failed to load cursor file {self._file_path}: {e}")
|
||||
return {}
|
||||
|
||||
def _flush_locked(self):
|
||||
# Atomic write: write to *.tmp first then rename, avoid corruption on crash.
|
||||
tmp_path = self._file_path + ".tmp"
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self._file_path) or ".", exist_ok=True)
|
||||
with open(tmp_path, "w", encoding="utf-8") as f:
|
||||
json.dump(self._data, f, ensure_ascii=False)
|
||||
os.replace(tmp_path, self._file_path)
|
||||
except Exception as 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)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get(self, open_kfid: str) -> Optional[str]:
|
||||
with self._lock:
|
||||
return self._data.get(open_kfid)
|
||||
|
||||
def set(self, open_kfid: str, cursor: str):
|
||||
if not cursor:
|
||||
return
|
||||
with self._lock:
|
||||
if self._data.get(open_kfid) == cursor:
|
||||
return
|
||||
self._data[open_kfid] = cursor
|
||||
self._flush_locked()
|
||||
|
||||
def has(self, open_kfid: str) -> bool:
|
||||
with self._lock:
|
||||
return open_kfid in self._data
|
||||
Reference in New Issue
Block a user