mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-19 21:07:28 +08:00
feat: support weixin channel
This commit is contained in:
@@ -353,13 +353,15 @@ class WebChannel(ChatChannel):
|
||||
# 打印可用渠道类型提示
|
||||
logger.info(
|
||||
"[WebChannel] 全部可用通道如下,可修改 config.json 配置文件中的 channel_type 字段进行切换,多个通道用逗号分隔:")
|
||||
logger.info("[WebChannel] 1. web - 网页")
|
||||
logger.info("[WebChannel] 2. terminal - 终端")
|
||||
logger.info("[WebChannel] 3. feishu - 飞书")
|
||||
logger.info("[WebChannel] 4. dingtalk - 钉钉")
|
||||
logger.info("[WebChannel] 5. wechatcom_app - 企微自建应用")
|
||||
logger.info("[WebChannel] 6. wechatmp - 个人公众号")
|
||||
logger.info("[WebChannel] 7. wechatmp_service - 企业公众号")
|
||||
logger.info("[WebChannel] 1. weixin - 微信")
|
||||
logger.info("[WebChannel] 2. web - 网页")
|
||||
logger.info("[WebChannel] 3. terminal - 终端")
|
||||
logger.info("[WebChannel] 4. feishu - 飞书")
|
||||
logger.info("[WebChannel] 5. dingtalk - 钉钉")
|
||||
logger.info("[WebChannel] 6. wecom_bot - 企微智能机器人")
|
||||
logger.info("[WebChannel] 7. wechatcom_app - 企微自建应用")
|
||||
logger.info("[WebChannel] 8. wechatmp - 个人公众号")
|
||||
logger.info("[WebChannel] 9. wechatmp_service - 企业公众号")
|
||||
logger.info("[WebChannel] ✅ Web控制台已运行")
|
||||
logger.info(f"[WebChannel] 🌐 本地访问: http://localhost:{port}")
|
||||
logger.info(f"[WebChannel] 🌍 服务器访问: http://YOUR_IP:{port} (请将YOUR_IP替换为服务器IP)")
|
||||
@@ -380,6 +382,7 @@ class WebChannel(ChatChannel):
|
||||
'/chat', 'ChatHandler',
|
||||
'/config', 'ConfigHandler',
|
||||
'/api/channels', 'ChannelsHandler',
|
||||
'/api/weixin/qrlogin', 'WeixinQrHandler',
|
||||
'/api/tools', 'ToolsHandler',
|
||||
'/api/skills', 'SkillsHandler',
|
||||
'/api/memory', 'MemoryHandler',
|
||||
@@ -685,6 +688,12 @@ class ChannelsHandler:
|
||||
"""API for managing external channel configurations (feishu, dingtalk, etc)."""
|
||||
|
||||
CHANNEL_DEFS = OrderedDict([
|
||||
("weixin", {
|
||||
"label": {"zh": "微信", "en": "WeChat"},
|
||||
"icon": "fa-comment",
|
||||
"color": "emerald",
|
||||
"fields": [],
|
||||
}),
|
||||
("feishu", {
|
||||
"label": {"zh": "飞书", "en": "Feishu"},
|
||||
"icon": "fa-paper-plane",
|
||||
@@ -750,6 +759,20 @@ class ChannelsHandler:
|
||||
}),
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def _get_weixin_login_status() -> str:
|
||||
try:
|
||||
import sys
|
||||
app_module = sys.modules.get('__main__') or sys.modules.get('app')
|
||||
mgr = getattr(app_module, '_channel_mgr', None) if app_module else None
|
||||
if mgr:
|
||||
ch = mgr.get_channel("weixin")
|
||||
if ch and hasattr(ch, 'login_status'):
|
||||
return ch.login_status
|
||||
except Exception:
|
||||
pass
|
||||
return "unknown"
|
||||
|
||||
@staticmethod
|
||||
def _mask_secret(value: str) -> str:
|
||||
if not value or len(value) <= 8:
|
||||
@@ -789,14 +812,17 @@ class ChannelsHandler:
|
||||
"value": display_val,
|
||||
"default": f.get("default", ""),
|
||||
})
|
||||
channels.append({
|
||||
ch_info = {
|
||||
"name": ch_name,
|
||||
"label": ch_def["label"],
|
||||
"icon": ch_def["icon"],
|
||||
"color": ch_def["color"],
|
||||
"active": ch_name in active_channels,
|
||||
"fields": fields_out,
|
||||
})
|
||||
}
|
||||
if ch_name == "weixin" and ch_name in active_channels:
|
||||
ch_info["login_status"] = self._get_weixin_login_status()
|
||||
channels.append(ch_info)
|
||||
return json.dumps({"status": "success", "channels": channels}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] Channels API error: {e}")
|
||||
@@ -1016,6 +1042,157 @@ class ChannelsHandler:
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
class WeixinQrHandler:
|
||||
"""Handle WeChat QR code login from the web console.
|
||||
|
||||
GET /api/weixin/qrlogin → fetch a new QR code
|
||||
POST /api/weixin/qrlogin → poll QR status or start channel after login
|
||||
"""
|
||||
|
||||
_qr_state = {}
|
||||
|
||||
@staticmethod
|
||||
def _qr_to_data_uri(data: str) -> str:
|
||||
"""Generate a QR code as a PNG data URI."""
|
||||
try:
|
||||
import qrcode as qr_lib
|
||||
import io
|
||||
import base64
|
||||
qr = qr_lib.QRCode(error_correction=qr_lib.constants.ERROR_CORRECT_L, box_size=6, border=2)
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
|
||||
return f"data:image/png;base64,{b64}"
|
||||
except ImportError:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _get_running_channel():
|
||||
try:
|
||||
import sys
|
||||
app_module = sys.modules.get('__main__') or sys.modules.get('app')
|
||||
mgr = getattr(app_module, '_channel_mgr', None) if app_module else None
|
||||
if mgr:
|
||||
return mgr.get_channel("weixin")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
running_ch = self._get_running_channel()
|
||||
if running_ch and hasattr(running_ch, '_current_qr_url') and running_ch._current_qr_url:
|
||||
qr_image = self._qr_to_data_uri(running_ch._current_qr_url)
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
"qrcode_url": running_ch._current_qr_url,
|
||||
"qr_image": qr_image,
|
||||
"source": "channel",
|
||||
})
|
||||
|
||||
from channel.weixin.weixin_api import WeixinApi, DEFAULT_BASE_URL
|
||||
base_url = conf().get("weixin_base_url", DEFAULT_BASE_URL)
|
||||
api = WeixinApi(base_url=base_url)
|
||||
qr_resp = api.fetch_qr_code()
|
||||
qrcode = qr_resp.get("qrcode", "")
|
||||
qrcode_url = qr_resp.get("qrcode_img_content", "")
|
||||
if not qrcode:
|
||||
return json.dumps({"status": "error", "message": "No QR code returned"})
|
||||
qr_image = self._qr_to_data_uri(qrcode_url)
|
||||
WeixinQrHandler._qr_state = {
|
||||
"qrcode": qrcode,
|
||||
"qrcode_url": qrcode_url,
|
||||
"base_url": base_url,
|
||||
}
|
||||
return json.dumps({"status": "success", "qrcode_url": qrcode_url, "qr_image": qr_image})
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] WeixinQr GET error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def POST(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
body = json.loads(web.data())
|
||||
action = body.get("action", "poll")
|
||||
|
||||
if action == "poll":
|
||||
return self._poll_status()
|
||||
elif action == "refresh":
|
||||
return self.GET()
|
||||
else:
|
||||
return json.dumps({"status": "error", "message": f"unknown action: {action}"})
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] WeixinQr POST error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def _poll_status(self):
|
||||
state = WeixinQrHandler._qr_state
|
||||
qrcode = state.get("qrcode", "")
|
||||
base_url = state.get("base_url", "")
|
||||
if not qrcode:
|
||||
return json.dumps({"status": "error", "message": "No active QR session"})
|
||||
|
||||
from channel.weixin.weixin_api import WeixinApi, DEFAULT_BASE_URL
|
||||
api = WeixinApi(base_url=base_url or DEFAULT_BASE_URL)
|
||||
try:
|
||||
status_resp = api.poll_qr_status(qrcode, timeout=10)
|
||||
except Exception as e:
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
qr_status = status_resp.get("status", "wait")
|
||||
|
||||
if qr_status == "confirmed":
|
||||
bot_token = status_resp.get("bot_token", "")
|
||||
bot_id = status_resp.get("ilink_bot_id", "")
|
||||
result_base_url = status_resp.get("baseurl", base_url)
|
||||
user_id = status_resp.get("ilink_user_id", "")
|
||||
|
||||
if not bot_token or not bot_id:
|
||||
return json.dumps({"status": "error", "message": "Login confirmed but missing token"})
|
||||
|
||||
cred_path = os.path.expanduser(
|
||||
conf().get("weixin_credentials_path", "~/.weixin_cow_credentials.json")
|
||||
)
|
||||
from channel.weixin.weixin_channel import _save_credentials
|
||||
_save_credentials(cred_path, {
|
||||
"token": bot_token,
|
||||
"base_url": result_base_url,
|
||||
"bot_id": bot_id,
|
||||
"user_id": user_id,
|
||||
})
|
||||
conf()["weixin_token"] = bot_token
|
||||
conf()["weixin_base_url"] = result_base_url
|
||||
|
||||
WeixinQrHandler._qr_state = {}
|
||||
logger.info(f"[WebChannel] WeChat QR login confirmed: bot_id={bot_id}")
|
||||
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
"qr_status": "confirmed",
|
||||
"bot_id": bot_id,
|
||||
})
|
||||
|
||||
if qr_status == "expired":
|
||||
new_resp = api.fetch_qr_code()
|
||||
new_qrcode = new_resp.get("qrcode", "")
|
||||
new_qrcode_url = new_resp.get("qrcode_img_content", "")
|
||||
new_qr_image = self._qr_to_data_uri(new_qrcode_url)
|
||||
WeixinQrHandler._qr_state["qrcode"] = new_qrcode
|
||||
WeixinQrHandler._qr_state["qrcode_url"] = new_qrcode_url
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
"qr_status": "expired",
|
||||
"qrcode_url": new_qrcode_url,
|
||||
"qr_image": new_qr_image,
|
||||
})
|
||||
|
||||
return json.dumps({"status": "success", "qr_status": qr_status})
|
||||
|
||||
|
||||
def _get_workspace_root():
|
||||
"""Resolve the agent workspace directory."""
|
||||
from common.utils import expand_path
|
||||
|
||||
Reference in New Issue
Block a user