mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-01 10:10:15 +08:00
Compare commits
2 Commits
v4.26.0
...
soulter/fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbd39e3efe | ||
|
|
a12a9b40d7 |
@@ -44,6 +44,7 @@ WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
"qq_official_webhook",
|
||||
"weixin_official_account",
|
||||
"wecom",
|
||||
"wecom_kf",
|
||||
"wecom_ai_bot",
|
||||
"slack",
|
||||
"lark",
|
||||
@@ -381,6 +382,20 @@ CONFIG_METADATA_2 = {
|
||||
"callback_server_host": "0.0.0.0",
|
||||
"port": 6195,
|
||||
},
|
||||
"微信客服": {
|
||||
"id": "wecom_kf",
|
||||
"type": "wecom_kf",
|
||||
"enable": False,
|
||||
"corpid": "",
|
||||
"secret": "",
|
||||
"token": "",
|
||||
"encoding_aes_key": "",
|
||||
"api_base_url": "https://qyapi.weixin.qq.com/cgi-bin/",
|
||||
"unified_webhook_mode": True,
|
||||
"webhook_uuid": "",
|
||||
"callback_server_host": "0.0.0.0",
|
||||
"port": 6195,
|
||||
},
|
||||
"企业微信智能机器人": {
|
||||
"id": "wecom_ai_bot",
|
||||
"type": "wecom_ai_bot",
|
||||
|
||||
@@ -154,6 +154,10 @@ class PlatformManager:
|
||||
from .sources.wecom.wecom_adapter import (
|
||||
WecomPlatformAdapter, # noqa: F401
|
||||
)
|
||||
case "wecom_kf":
|
||||
from .sources.wecom.wecom_kf_adapter import (
|
||||
WecomKFPlatformAdapter, # noqa: F401
|
||||
)
|
||||
case "wecom_ai_bot":
|
||||
from .sources.wecom_ai_bot.wecomai_adapter import (
|
||||
WecomAIBotAdapter, # noqa: F401
|
||||
|
||||
@@ -160,6 +160,42 @@ class WeChatKF(BaseWeChatAPI):
|
||||
"""
|
||||
return self._get("kf/account/list")
|
||||
|
||||
def add_account(self, name, media_id=""):
|
||||
"""添加客服帐号
|
||||
|
||||
:param name: 客服帐号名称
|
||||
:param media_id: 客服头像临时素材 media_id
|
||||
:return: 接口调用结果
|
||||
"""
|
||||
data = {"name": name}
|
||||
if media_id:
|
||||
data["media_id"] = media_id
|
||||
return self._post("kf/account/add", data=data)
|
||||
|
||||
def update_account(self, open_kfid, name="", media_id=""):
|
||||
"""修改客服帐号
|
||||
|
||||
:param open_kfid: 客服帐号ID
|
||||
:param name: 客服帐号名称
|
||||
:param media_id: 客服头像临时素材 media_id
|
||||
:return: 接口调用结果
|
||||
"""
|
||||
data = {"open_kfid": open_kfid}
|
||||
if name:
|
||||
data["name"] = name
|
||||
if media_id:
|
||||
data["media_id"] = media_id
|
||||
return self._post("kf/account/update", data=data)
|
||||
|
||||
def del_account(self, open_kfid):
|
||||
"""删除客服帐号
|
||||
|
||||
:param open_kfid: 客服帐号ID
|
||||
:return: 接口调用结果
|
||||
"""
|
||||
data = {"open_kfid": open_kfid}
|
||||
return self._post("kf/account/del", data=data)
|
||||
|
||||
def add_contact_way(self, open_kfid, scene):
|
||||
"""获取客服帐号链接
|
||||
|
||||
|
||||
468
astrbot/core/platform/sources/wecom/wecom_kf_adapter.py
Normal file
468
astrbot/core/platform/sources/wecom/wecom_kf_adapter.py
Normal file
@@ -0,0 +1,468 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
import quart
|
||||
from requests import Response
|
||||
from wechatpy.enterprise import WeChatClient, parse_message
|
||||
from wechatpy.enterprise.crypto import WeChatCrypto
|
||||
from wechatpy.exceptions import InvalidSignatureException
|
||||
from wechatpy.messages import BaseMessage
|
||||
|
||||
from astrbot.api.event import MessageChain
|
||||
from astrbot.api.message_components import Image, Plain, Record
|
||||
from astrbot.api.platform import (
|
||||
AstrBotMessage,
|
||||
MessageMember,
|
||||
MessageType,
|
||||
Platform,
|
||||
PlatformMetadata,
|
||||
register_platform_adapter,
|
||||
)
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.media_utils import convert_audio_to_wav
|
||||
from astrbot.core.utils.webhook_utils import log_webhook_info
|
||||
|
||||
from .wecom_event import WecomPlatformEvent
|
||||
from .wecom_kf import WeChatKF
|
||||
from .wecom_kf_message import WeChatKFMessage
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class WecomKFServer:
|
||||
def __init__(self, config: dict) -> None:
|
||||
self.server = quart.Quart(__name__)
|
||||
self.port = int(cast(str, config.get("port")))
|
||||
self.callback_server_host = config.get("callback_server_host", "0.0.0.0")
|
||||
self.server.add_url_rule(
|
||||
"/callback/command",
|
||||
view_func=self.verify,
|
||||
methods=["GET"],
|
||||
)
|
||||
self.server.add_url_rule(
|
||||
"/callback/command",
|
||||
view_func=self.callback_command,
|
||||
methods=["POST"],
|
||||
)
|
||||
|
||||
self.crypto = WeChatCrypto(
|
||||
config["token"].strip(),
|
||||
config["encoding_aes_key"].strip(),
|
||||
config["corpid"].strip(),
|
||||
)
|
||||
|
||||
self.callback: Callable[[BaseMessage], Awaitable[None]] | None = None
|
||||
self.shutdown_event = asyncio.Event()
|
||||
|
||||
async def verify(self):
|
||||
return await self.handle_verify(quart.request)
|
||||
|
||||
async def handle_verify(self, request) -> str:
|
||||
logger.info(f"验证微信客服请求有效性: {request.args}")
|
||||
args = request.args
|
||||
try:
|
||||
echo_str = self.crypto.check_signature(
|
||||
args.get("msg_signature"),
|
||||
args.get("timestamp"),
|
||||
args.get("nonce"),
|
||||
args.get("echostr"),
|
||||
)
|
||||
logger.info("验证微信客服请求有效性成功。")
|
||||
return echo_str
|
||||
except InvalidSignatureException:
|
||||
logger.error("验证微信客服请求有效性失败,签名异常,请检查配置。")
|
||||
raise
|
||||
|
||||
async def callback_command(self):
|
||||
return await self.handle_callback(quart.request)
|
||||
|
||||
async def handle_callback(self, request) -> str:
|
||||
data = await request.get_data()
|
||||
msg_signature = request.args.get("msg_signature")
|
||||
timestamp = request.args.get("timestamp")
|
||||
nonce = request.args.get("nonce")
|
||||
try:
|
||||
xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce)
|
||||
except InvalidSignatureException:
|
||||
logger.error("解密微信客服回调失败,签名异常,请检查配置。")
|
||||
raise
|
||||
|
||||
msg = cast(BaseMessage, parse_message(xml))
|
||||
logger.info(f"解析微信客服回调成功: {msg}")
|
||||
if self.callback:
|
||||
await self.callback(msg)
|
||||
return "success"
|
||||
|
||||
async def start_polling(self) -> None:
|
||||
logger.info(
|
||||
f"将在 {self.callback_server_host}:{self.port} 端口启动 微信客服 适配器。",
|
||||
)
|
||||
await self.server.run_task(
|
||||
host=self.callback_server_host,
|
||||
port=self.port,
|
||||
shutdown_trigger=self.shutdown_trigger,
|
||||
)
|
||||
|
||||
async def shutdown_trigger(self) -> None:
|
||||
await self.shutdown_event.wait()
|
||||
|
||||
|
||||
@register_platform_adapter(
|
||||
"wecom_kf", "微信客服适配器", support_streaming_message=False
|
||||
)
|
||||
class WecomKFPlatformAdapter(Platform):
|
||||
MSGID_DEDUP_TTL_SECONDS = 300
|
||||
TEXT_CONTENT_DEDUP_TTL_SECONDS = 15
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
platform_config: dict,
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(platform_config, event_queue)
|
||||
self.settings = platform_settings
|
||||
self.api_base_url = platform_config.get(
|
||||
"api_base_url",
|
||||
"https://qyapi.weixin.qq.com/cgi-bin/",
|
||||
)
|
||||
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
||||
|
||||
if not self.api_base_url:
|
||||
self.api_base_url = "https://qyapi.weixin.qq.com/cgi-bin/"
|
||||
self.api_base_url = self.api_base_url.removesuffix("/")
|
||||
if not self.api_base_url.endswith("/cgi-bin"):
|
||||
self.api_base_url += "/cgi-bin"
|
||||
if not self.api_base_url.endswith("/"):
|
||||
self.api_base_url += "/"
|
||||
|
||||
self.server = WecomKFServer(self.config)
|
||||
self.client = WeChatClient(
|
||||
self.config["corpid"].strip(),
|
||||
self.config["secret"].strip(),
|
||||
)
|
||||
self.wechat_kf_api = WeChatKF(client=self.client)
|
||||
self.wechat_kf_message_api = WeChatKFMessage(self.client)
|
||||
self.client.__setattr__("kf", self.wechat_kf_api)
|
||||
self.client.__setattr__("kf_message", self.wechat_kf_message_api)
|
||||
self.client.__setattr__("API_BASE_URL", self.api_base_url)
|
||||
|
||||
self._seen_msgids: dict[str, float] = {}
|
||||
self._seen_text_messages: dict[str, float] = {}
|
||||
self._kf_accounts: list[dict[str, Any]] = []
|
||||
self._kf_contact_links: list[dict[str, str]] = []
|
||||
self._kf_link_error = ""
|
||||
|
||||
async def callback(msg: BaseMessage) -> None:
|
||||
if msg.type == "unknown" and msg._data.get("Event") == "kf_msg_or_event":
|
||||
await self._handle_kf_msg_or_event(msg)
|
||||
return
|
||||
logger.debug("微信客服适配器忽略非客服回调: %s", msg)
|
||||
|
||||
self.server.callback = callback
|
||||
|
||||
@override
|
||||
def meta(self) -> PlatformMetadata:
|
||||
return PlatformMetadata(
|
||||
"wecom_kf",
|
||||
"微信客服适配器",
|
||||
id=self.config.get("id", "wecom_kf"),
|
||||
support_streaming_message=False,
|
||||
support_proactive_message=False,
|
||||
)
|
||||
|
||||
@override
|
||||
async def send_by_session(
|
||||
self,
|
||||
session: MessageSesion,
|
||||
message_chain: MessageChain,
|
||||
) -> None:
|
||||
logger.warning("微信客服适配器不支持 send_by_session 主动发送。")
|
||||
await super().send_by_session(session, message_chain)
|
||||
|
||||
async def run(self) -> None:
|
||||
await self._refresh_kf_contact_links()
|
||||
|
||||
webhook_uuid = self.config.get("webhook_uuid")
|
||||
if self.unified_webhook_mode and webhook_uuid:
|
||||
log_webhook_info(f"{self.meta().id}(微信客服)", webhook_uuid)
|
||||
await self.server.shutdown_event.wait()
|
||||
else:
|
||||
await self.server.start_polling()
|
||||
|
||||
async def webhook_callback(self, request: Any) -> Any:
|
||||
if request.method == "GET":
|
||||
return await self.server.handle_verify(request)
|
||||
return await self.server.handle_callback(request)
|
||||
|
||||
async def _refresh_kf_contact_links(self) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
account_payload = await loop.run_in_executor(
|
||||
None,
|
||||
self.wechat_kf_api.get_account_list,
|
||||
)
|
||||
accounts = account_payload.get("account_list", [])
|
||||
self._kf_accounts = accounts if isinstance(accounts, list) else []
|
||||
|
||||
contact_links: list[dict[str, str]] = []
|
||||
for index, account in enumerate(self._kf_accounts):
|
||||
if not isinstance(account, dict):
|
||||
continue
|
||||
open_kfid = str(account.get("open_kfid", "")).strip()
|
||||
if not open_kfid:
|
||||
continue
|
||||
scene = f"astrbot_{index}"
|
||||
contact_payload = await loop.run_in_executor(
|
||||
None,
|
||||
self.wechat_kf_api.add_contact_way,
|
||||
open_kfid,
|
||||
scene,
|
||||
)
|
||||
url = str(contact_payload.get("url", "")).strip()
|
||||
if not url:
|
||||
continue
|
||||
contact_links.append(
|
||||
{
|
||||
"open_kfid": open_kfid,
|
||||
"name": str(account.get("name", "")).strip() or open_kfid,
|
||||
"qrcode": url,
|
||||
"qrcode_img_content": url,
|
||||
}
|
||||
)
|
||||
|
||||
self._kf_contact_links = contact_links
|
||||
self._kf_link_error = ""
|
||||
logger.info(
|
||||
"微信客服适配器获取到 %s 个客服账号,生成 %s 个客服链接。",
|
||||
len(self._kf_accounts),
|
||||
len(self._kf_contact_links),
|
||||
)
|
||||
except Exception as e:
|
||||
self._kf_link_error = str(e)
|
||||
logger.error("获取微信客服列表或客服链接失败: %s", e, exc_info=True)
|
||||
|
||||
async def get_kf_accounts_payload(self) -> dict[str, Any]:
|
||||
await self._refresh_kf_contact_links()
|
||||
return {
|
||||
"accounts": self._kf_accounts,
|
||||
"contact_links": self._kf_contact_links,
|
||||
"link_error": self._kf_link_error,
|
||||
}
|
||||
|
||||
async def add_kf_account(self, name: str, media_id: str = "") -> dict[str, Any]:
|
||||
payload = await asyncio.get_running_loop().run_in_executor(
|
||||
None,
|
||||
self.wechat_kf_api.add_account,
|
||||
name,
|
||||
media_id,
|
||||
)
|
||||
await self._refresh_kf_contact_links()
|
||||
return cast(dict[str, Any], payload)
|
||||
|
||||
async def update_kf_account(
|
||||
self, open_kfid: str, name: str = "", media_id: str = ""
|
||||
) -> dict[str, Any]:
|
||||
payload = await asyncio.get_running_loop().run_in_executor(
|
||||
None,
|
||||
self.wechat_kf_api.update_account,
|
||||
open_kfid,
|
||||
name,
|
||||
media_id,
|
||||
)
|
||||
await self._refresh_kf_contact_links()
|
||||
return cast(dict[str, Any], payload)
|
||||
|
||||
async def upload_kf_avatar(self, file_path: Path) -> dict[str, Any]:
|
||||
def upload() -> dict[str, Any]:
|
||||
with file_path.open("rb") as f:
|
||||
return cast(dict[str, Any], self.client.media.upload("image", f))
|
||||
|
||||
return await asyncio.get_running_loop().run_in_executor(None, upload)
|
||||
|
||||
async def delete_kf_account(self, open_kfid: str) -> dict[str, Any]:
|
||||
payload = await asyncio.get_running_loop().run_in_executor(
|
||||
None,
|
||||
self.wechat_kf_api.del_account,
|
||||
open_kfid,
|
||||
)
|
||||
await self._refresh_kf_contact_links()
|
||||
return cast(dict[str, Any], payload)
|
||||
|
||||
async def _handle_kf_msg_or_event(self, msg: BaseMessage) -> None:
|
||||
token = msg._data.get("Token", "")
|
||||
open_kfid = msg._data.get("OpenKfId", "")
|
||||
messages = await asyncio.get_running_loop().run_in_executor(
|
||||
None,
|
||||
self._sync_all_messages,
|
||||
token,
|
||||
open_kfid,
|
||||
)
|
||||
for item in messages:
|
||||
await self.convert_wechat_kf_message(item)
|
||||
|
||||
def _sync_all_messages(self, token: str, open_kfid: str) -> list[dict[str, Any]]:
|
||||
cursor = ""
|
||||
has_more = 1
|
||||
messages: list[dict[str, Any]] = []
|
||||
while has_more:
|
||||
payload = self.wechat_kf_api.sync_msg(token, open_kfid, cursor=cursor)
|
||||
msg_list = payload.get("msg_list", [])
|
||||
if isinstance(msg_list, list):
|
||||
messages.extend(item for item in msg_list if isinstance(item, dict))
|
||||
has_more = int(payload.get("has_more", 0) or 0)
|
||||
next_cursor = str(payload.get("next_cursor", "")).strip()
|
||||
if not has_more or not next_cursor or next_cursor == cursor:
|
||||
break
|
||||
cursor = next_cursor
|
||||
return messages
|
||||
|
||||
def _is_duplicate_msgid(self, msgid: str) -> bool:
|
||||
now = time.monotonic()
|
||||
expired_msgids = [
|
||||
cached_msgid
|
||||
for cached_msgid, expires_at in self._seen_msgids.items()
|
||||
if expires_at <= now
|
||||
]
|
||||
for cached_msgid in expired_msgids:
|
||||
self._seen_msgids.pop(cached_msgid, None)
|
||||
|
||||
if msgid in self._seen_msgids:
|
||||
return True
|
||||
self._seen_msgids[msgid] = now + self.MSGID_DEDUP_TTL_SECONDS
|
||||
return False
|
||||
|
||||
def _is_duplicate_text_message(self, session_id: str, text: str) -> bool:
|
||||
normalized_text = text.strip()
|
||||
if not normalized_text:
|
||||
return False
|
||||
|
||||
now = time.monotonic()
|
||||
expired_keys = [
|
||||
key
|
||||
for key, expires_at in self._seen_text_messages.items()
|
||||
if expires_at <= now
|
||||
]
|
||||
for key in expired_keys:
|
||||
self._seen_text_messages.pop(key, None)
|
||||
|
||||
dedup_key = f"{session_id}:{normalized_text}"
|
||||
if dedup_key in self._seen_text_messages:
|
||||
return True
|
||||
self._seen_text_messages[dedup_key] = now + self.TEXT_CONTENT_DEDUP_TTL_SECONDS
|
||||
return False
|
||||
|
||||
async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None:
|
||||
logger.info(f"收到微信客服消息: {msg}")
|
||||
|
||||
msgid = str(msg.get("msgid", "")).strip()
|
||||
if msgid and self._is_duplicate_msgid(msgid):
|
||||
logger.debug("忽略重复微信客服消息 msgid=%s", msgid)
|
||||
return None
|
||||
|
||||
msgtype = msg.get("msgtype")
|
||||
open_kfid = str(msg.get("open_kfid", "")).strip()
|
||||
external_userid = str(msg.get("external_userid", "")).strip()
|
||||
if not open_kfid or not external_userid:
|
||||
logger.debug(
|
||||
"忽略缺少 open_kfid 或 external_userid 的微信客服消息: %s", msg
|
||||
)
|
||||
return None
|
||||
|
||||
abm = AstrBotMessage()
|
||||
abm.raw_message = dict(msg)
|
||||
abm.raw_message["_wechat_kf_flag"] = None
|
||||
abm.self_id = open_kfid
|
||||
abm.sender = MessageMember(external_userid, external_userid)
|
||||
abm.session_id = f"{open_kfid}_{external_userid}"
|
||||
abm.type = MessageType.FRIEND_MESSAGE
|
||||
abm.message_id = msgid or uuid.uuid4().hex[:8]
|
||||
abm.timestamp = int(msg.get("send_time", time.time()))
|
||||
abm.message_str = ""
|
||||
|
||||
if msgtype == "text":
|
||||
text = msg.get("text", {}).get("content", "").strip()
|
||||
if self._is_duplicate_text_message(abm.session_id, text):
|
||||
logger.debug(
|
||||
"忽略 15 秒内重复微信客服文本消息 session_id=%s text=%s",
|
||||
abm.session_id,
|
||||
text,
|
||||
)
|
||||
return None
|
||||
abm.message = [Plain(text=text)]
|
||||
abm.message_str = text
|
||||
elif msgtype == "image":
|
||||
media_id = msg.get("image", {}).get("media_id", "")
|
||||
path = await self._download_media(media_id, "wecom_kf_img", ".jpg")
|
||||
abm.message = [Image(file=str(path), url=str(path))]
|
||||
abm.message_str = "[图片]"
|
||||
elif msgtype == "voice":
|
||||
media_id = msg.get("voice", {}).get("media_id", "")
|
||||
path = await self._download_media(media_id, "wecom_kf_voice", ".amr")
|
||||
try:
|
||||
wav_path = path.with_suffix(".wav")
|
||||
converted_path = await convert_audio_to_wav(str(path), str(wav_path))
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"转换微信客服音频失败: {e}。如果没有安装 ffmpeg 请先安装。"
|
||||
)
|
||||
return None
|
||||
abm.message = [Record(file=converted_path, url=converted_path)]
|
||||
else:
|
||||
logger.warning(f"未实现的微信客服消息事件: {msg}")
|
||||
return None
|
||||
|
||||
await self.handle_msg(abm)
|
||||
return abm
|
||||
|
||||
async def _download_media(self, media_id: str, prefix: str, suffix: str) -> Path:
|
||||
resp: Response = await asyncio.get_running_loop().run_in_executor(
|
||||
None,
|
||||
self.client.media.download,
|
||||
media_id,
|
||||
)
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = temp_dir / f"{prefix}_{media_id or uuid.uuid4().hex}{suffix}"
|
||||
path.write_bytes(resp.content)
|
||||
return path
|
||||
|
||||
async def handle_msg(self, message: AstrBotMessage) -> None:
|
||||
message_event = WecomPlatformEvent(
|
||||
message_str=message.message_str,
|
||||
message_obj=message,
|
||||
platform_meta=self.meta(),
|
||||
session_id=message.session_id,
|
||||
client=self.client,
|
||||
)
|
||||
self.commit_event(message_event)
|
||||
|
||||
def get_client(self) -> WeChatClient:
|
||||
return self.client
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
stat = super().get_stats()
|
||||
stat["wecom_kf"] = {
|
||||
"accounts": self._kf_accounts,
|
||||
"contact_links": self._kf_contact_links,
|
||||
"link_error": self._kf_link_error,
|
||||
}
|
||||
return stat
|
||||
|
||||
async def terminate(self) -> None:
|
||||
self.server.shutdown_event.set()
|
||||
try:
|
||||
await self.server.server.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("微信客服适配器已被关闭")
|
||||
@@ -12,6 +12,7 @@ class PlatformAdapterType(enum.Flag):
|
||||
QQOFFICIAL_WEBHOOK = enum.auto()
|
||||
TELEGRAM = enum.auto()
|
||||
WECOM = enum.auto()
|
||||
WECOM_KF = enum.auto()
|
||||
WECOM_AI_BOT = enum.auto()
|
||||
LARK = enum.auto()
|
||||
DINGTALK = enum.auto()
|
||||
@@ -36,6 +37,7 @@ ADAPTER_NAME_2_TYPE = {
|
||||
"qq_official_webhook": PlatformAdapterType.QQOFFICIAL_WEBHOOK,
|
||||
"telegram": PlatformAdapterType.TELEGRAM,
|
||||
"wecom": PlatformAdapterType.WECOM,
|
||||
"wecom_kf": PlatformAdapterType.WECOM_KF,
|
||||
"wecom_ai_bot": PlatformAdapterType.WECOM_AI_BOT,
|
||||
"lark": PlatformAdapterType.LARK,
|
||||
"dingtalk": PlatformAdapterType.DINGTALK,
|
||||
|
||||
@@ -140,7 +140,7 @@ def _evaluate_send_message_tool(config: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
if not platform_type:
|
||||
continue
|
||||
|
||||
if platform_type in {"wecom", "weixin_official_account"}:
|
||||
if platform_type in {"wecom", "wecom_kf", "weixin_official_account"}:
|
||||
continue
|
||||
|
||||
if platform_type == "wecom_ai_bot":
|
||||
|
||||
@@ -8,6 +8,7 @@ from quart import request
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.platform import Platform
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
|
||||
from .route import Response, Route, RouteContext
|
||||
|
||||
@@ -41,6 +42,31 @@ class PlatformRoute(Route):
|
||||
view_func=self.get_platform_stats,
|
||||
methods=["GET"],
|
||||
)
|
||||
self.app.add_url_rule(
|
||||
"/api/platform/wecom-kf/<platform_id>/accounts",
|
||||
view_func=self.get_wecom_kf_accounts,
|
||||
methods=["GET"],
|
||||
)
|
||||
self.app.add_url_rule(
|
||||
"/api/platform/wecom-kf/<platform_id>/accounts",
|
||||
view_func=self.add_wecom_kf_account,
|
||||
methods=["POST"],
|
||||
)
|
||||
self.app.add_url_rule(
|
||||
"/api/platform/wecom-kf/<platform_id>/accounts/<open_kfid>",
|
||||
view_func=self.update_wecom_kf_account,
|
||||
methods=["PUT"],
|
||||
)
|
||||
self.app.add_url_rule(
|
||||
"/api/platform/wecom-kf/<platform_id>/accounts/<open_kfid>",
|
||||
view_func=self.delete_wecom_kf_account,
|
||||
methods=["DELETE"],
|
||||
)
|
||||
self.app.add_url_rule(
|
||||
"/api/platform/wecom-kf/<platform_id>/avatar",
|
||||
view_func=self.upload_wecom_kf_avatar,
|
||||
methods=["POST"],
|
||||
)
|
||||
|
||||
async def unified_webhook_callback(self, webhook_uuid: str):
|
||||
"""统一 webhook 回调入口
|
||||
@@ -98,3 +124,105 @@ class PlatformRoute(Route):
|
||||
except Exception as e:
|
||||
logger.error(f"获取平台统计信息失败: {e}", exc_info=True)
|
||||
return Response().error(f"获取统计信息失败: {e}").__dict__, 500
|
||||
|
||||
def _find_wecom_kf_platform(self, platform_id: str) -> Platform | None:
|
||||
for platform in self.platform_manager.platform_insts:
|
||||
if platform.meta().id == platform_id and platform.meta().name == "wecom_kf":
|
||||
return platform
|
||||
return None
|
||||
|
||||
async def get_wecom_kf_accounts(self, platform_id: str):
|
||||
platform = self._find_wecom_kf_platform(platform_id)
|
||||
if not platform or not hasattr(platform, "get_kf_accounts_payload"):
|
||||
return Response().error("未找到微信客服平台实例").__dict__, 404
|
||||
try:
|
||||
payload = await platform.get_kf_accounts_payload()
|
||||
return Response().ok(payload).__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"获取微信客服账号列表失败: {e}", exc_info=True)
|
||||
return Response().error(f"获取微信客服账号列表失败: {e}").__dict__, 500
|
||||
|
||||
async def add_wecom_kf_account(self, platform_id: str):
|
||||
platform = self._find_wecom_kf_platform(platform_id)
|
||||
if not platform or not hasattr(platform, "add_kf_account"):
|
||||
return Response().error("未找到微信客服平台实例").__dict__, 404
|
||||
|
||||
body = await request.get_json(silent=True) or {}
|
||||
name = str(body.get("name", "")).strip()
|
||||
media_id = str(body.get("media_id", "")).strip()
|
||||
if not name:
|
||||
return Response().error("客服名称不能为空").__dict__, 400
|
||||
|
||||
try:
|
||||
payload = await platform.add_kf_account(name, media_id)
|
||||
return Response().ok(payload, "微信客服账号已创建").__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"创建微信客服账号失败: {e}", exc_info=True)
|
||||
return Response().error(f"创建微信客服账号失败: {e}").__dict__, 500
|
||||
|
||||
async def update_wecom_kf_account(self, platform_id: str, open_kfid: str):
|
||||
platform = self._find_wecom_kf_platform(platform_id)
|
||||
if not platform or not hasattr(platform, "update_kf_account"):
|
||||
return Response().error("未找到微信客服平台实例").__dict__, 404
|
||||
|
||||
body = await request.get_json(silent=True) or {}
|
||||
name = str(body.get("name", "")).strip()
|
||||
media_id = str(body.get("media_id", "")).strip()
|
||||
if not name and not media_id:
|
||||
return Response().error(
|
||||
"客服名称和头像 media_id 不能同时为空"
|
||||
).__dict__, 400
|
||||
|
||||
try:
|
||||
payload = await platform.update_kf_account(open_kfid, name, media_id)
|
||||
return Response().ok(payload, "微信客服账号已更新").__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"更新微信客服账号失败: {e}", exc_info=True)
|
||||
return Response().error(f"更新微信客服账号失败: {e}").__dict__, 500
|
||||
|
||||
async def delete_wecom_kf_account(self, platform_id: str, open_kfid: str):
|
||||
platform = self._find_wecom_kf_platform(platform_id)
|
||||
if not platform or not hasattr(platform, "delete_kf_account"):
|
||||
return Response().error("未找到微信客服平台实例").__dict__, 404
|
||||
|
||||
try:
|
||||
payload = await platform.delete_kf_account(open_kfid)
|
||||
return Response().ok(payload, "微信客服账号已删除").__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"删除微信客服账号失败: {e}", exc_info=True)
|
||||
return Response().error(f"删除微信客服账号失败: {e}").__dict__, 500
|
||||
|
||||
async def upload_wecom_kf_avatar(self, platform_id: str):
|
||||
platform = self._find_wecom_kf_platform(platform_id)
|
||||
if not platform or not hasattr(platform, "upload_kf_avatar"):
|
||||
return Response().error("未找到微信客服平台实例").__dict__, 404
|
||||
|
||||
files = await request.files
|
||||
file = files.get("file")
|
||||
if not file:
|
||||
return Response().error("未上传头像文件").__dict__, 400
|
||||
if file.mimetype and not file.mimetype.startswith("image/"):
|
||||
return Response().error("头像文件必须是图片").__dict__, 400
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
temp_dir = Path(get_astrbot_temp_path())
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
suffix = Path(file.filename or "").suffix or ".jpg"
|
||||
target = temp_dir / f"wecom_kf_avatar_{platform_id}{suffix}"
|
||||
|
||||
try:
|
||||
await file.save(target)
|
||||
payload = await platform.upload_kf_avatar(target)
|
||||
media_id = str(payload.get("media_id", "")).strip()
|
||||
if not media_id:
|
||||
return Response().error("微信客服头像上传未返回 media_id").__dict__, 500
|
||||
return Response().ok({"media_id": media_id, "raw": payload}).__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"上传微信客服头像失败: {e}", exc_info=True)
|
||||
return Response().error(f"上传微信客服头像失败: {e}").__dict__, 500
|
||||
finally:
|
||||
try:
|
||||
target.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@@ -130,6 +130,31 @@
|
||||
"waiting": "Waiting for QR",
|
||||
"close": "Close"
|
||||
},
|
||||
"wecomKf": {
|
||||
"title": "WeCom Customer Service",
|
||||
"manage": "Manage CS",
|
||||
"name": "Account name",
|
||||
"mediaId": "Avatar media_id (optional)",
|
||||
"avatarFile": "Upload avatar",
|
||||
"actions": "Actions",
|
||||
"create": "Create",
|
||||
"update": "Save",
|
||||
"cancelEdit": "Cancel",
|
||||
"close": "Close",
|
||||
"loading": "Loading accounts",
|
||||
"empty": "No customer service accounts",
|
||||
"loadFailed": "Failed to load customer service accounts",
|
||||
"saveFailed": "Failed to save customer service account",
|
||||
"deleteFailed": "Failed to delete customer service account",
|
||||
"nameRequired": "Account name is required",
|
||||
"updateRequired": "Account name and avatar media_id cannot both be empty",
|
||||
"createSuccess": "Customer service account created",
|
||||
"updateSuccess": "Customer service account updated",
|
||||
"deleteSuccess": "Customer service account deleted",
|
||||
"avatarUploadSuccess": "Avatar uploaded and media_id filled",
|
||||
"avatarUploadFailed": "Failed to upload avatar",
|
||||
"deleteConfirm": "Delete customer service account {name}?"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "Error Details",
|
||||
"platformId": "Platform ID",
|
||||
|
||||
@@ -130,6 +130,31 @@
|
||||
"waiting": "Ожидание QR",
|
||||
"close": "Закрыть"
|
||||
},
|
||||
"wecomKf": {
|
||||
"title": "WeCom Customer Service",
|
||||
"manage": "Управлять",
|
||||
"name": "Имя аккаунта",
|
||||
"mediaId": "Avatar media_id (optional)",
|
||||
"avatarFile": "Загрузить аватар",
|
||||
"actions": "Действия",
|
||||
"create": "Создать",
|
||||
"update": "Сохранить",
|
||||
"cancelEdit": "Отмена",
|
||||
"close": "Закрыть",
|
||||
"loading": "Загрузка аккаунтов",
|
||||
"empty": "Нет аккаунтов",
|
||||
"loadFailed": "Не удалось загрузить аккаунты",
|
||||
"saveFailed": "Не удалось сохранить аккаунт",
|
||||
"deleteFailed": "Не удалось удалить аккаунт",
|
||||
"nameRequired": "Имя аккаунта обязательно",
|
||||
"updateRequired": "Имя аккаунта и avatar media_id не могут быть пустыми одновременно",
|
||||
"createSuccess": "Аккаунт создан",
|
||||
"updateSuccess": "Аккаунт обновлен",
|
||||
"deleteSuccess": "Аккаунт удален",
|
||||
"avatarUploadSuccess": "Аватар загружен, media_id заполнен",
|
||||
"avatarUploadFailed": "Не удалось загрузить аватар",
|
||||
"deleteConfirm": "Удалить аккаунт {name}?"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "Детали ошибки",
|
||||
"platformId": "ID платформы",
|
||||
|
||||
@@ -130,6 +130,31 @@
|
||||
"waiting": "等待二维码",
|
||||
"close": "关闭"
|
||||
},
|
||||
"wecomKf": {
|
||||
"title": "微信客服管理",
|
||||
"manage": "管理客服",
|
||||
"name": "客服名称",
|
||||
"mediaId": "头像 media_id(可选)",
|
||||
"avatarFile": "上传头像",
|
||||
"actions": "操作",
|
||||
"create": "新建",
|
||||
"update": "保存修改",
|
||||
"cancelEdit": "取消编辑",
|
||||
"close": "关闭",
|
||||
"loading": "正在加载客服列表",
|
||||
"empty": "暂无客服账号",
|
||||
"loadFailed": "加载微信客服账号失败",
|
||||
"saveFailed": "保存微信客服账号失败",
|
||||
"deleteFailed": "删除微信客服账号失败",
|
||||
"nameRequired": "客服名称不能为空",
|
||||
"updateRequired": "客服名称和头像 media_id 不能同时为空",
|
||||
"createSuccess": "微信客服账号已创建",
|
||||
"updateSuccess": "微信客服账号已更新",
|
||||
"deleteSuccess": "微信客服账号已删除",
|
||||
"avatarUploadSuccess": "头像已上传,media_id 已填入",
|
||||
"avatarUploadFailed": "上传头像失败",
|
||||
"deleteConfirm": "确定要删除微信客服账号 {name} 吗?"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "错误详情",
|
||||
"platformId": "平台 ID",
|
||||
|
||||
@@ -14,7 +14,7 @@ export function getPlatformIcon(name) {
|
||||
return new URL('@/assets/images/platform_logos/qq.png', import.meta.url).href
|
||||
} else if (name === 'weixin_oc' || name === 'weixin_oc') {
|
||||
return new URL('@/assets/images/platform_logos/wechat.png', import.meta.url).href
|
||||
} else if (name === 'wecom' || name === 'wecom_ai_bot') {
|
||||
} else if (name === 'wecom' || name === 'wecom_kf' || name === 'wecom_ai_bot') {
|
||||
return new URL('@/assets/images/platform_logos/wecom.png', import.meta.url).href
|
||||
} else if (name === 'weixin_official_account') {
|
||||
return new URL('@/assets/images/platform_logos/wechat.png', import.meta.url).href
|
||||
@@ -56,6 +56,7 @@ export function getTutorialLink(platformType) {
|
||||
"qq_official": "https://docs.astrbot.app/platform/qqofficial/websockets.html",
|
||||
"aiocqhttp": "https://docs.astrbot.app/platform/aiocqhttp.html",
|
||||
"wecom": "https://docs.astrbot.app/platform/wecom.html",
|
||||
"wecom_kf": "https://docs.astrbot.app/platform/wecom.html",
|
||||
"weixin_oc": "https://docs.astrbot.app/platform/weixin_oc.html",
|
||||
"wecom_ai_bot": "https://docs.astrbot.app/platform/wecom_ai_bot.html",
|
||||
"lark": "https://docs.astrbot.app/platform/lark.html",
|
||||
@@ -101,6 +102,7 @@ export function getPlatformDisplayName(platformId) {
|
||||
qq_official: 'qq_official (QQ 官方机器人平台)',
|
||||
weixin_official_account: 'weixin_official_account (微信公众号)',
|
||||
wecom: 'wecom (企业微信应用)',
|
||||
wecom_kf: 'wecom_kf (微信客服)',
|
||||
wecom_ai_bot: 'wecom_ai_bot (企业微信智能机器人)',
|
||||
lark: 'lark (飞书)',
|
||||
dingtalk: 'dingtalk (钉钉)',
|
||||
|
||||
@@ -72,6 +72,21 @@
|
||||
{{ tm('platformQr.show') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div
|
||||
class="platform-qr-chip"
|
||||
v-if="item.type === 'wecom_kf' && getPlatformStat(item.id)"
|
||||
>
|
||||
<v-chip
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="platform-qr-chip-item"
|
||||
@click.stop="openWecomKfDialog(item.id)"
|
||||
>
|
||||
<v-icon size="small" start>mdi-robot</v-icon>
|
||||
{{ tm('wecomKf.manage') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div v-if="getPlatformStat(item.id)?.unified_webhook && item.webhook_uuid" class="webhook-info">
|
||||
<v-chip
|
||||
size="small"
|
||||
@@ -160,11 +175,26 @@
|
||||
{{ tm('platformQr.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text class="px-4 pb-4">
|
||||
<div class="platform-qr-status">
|
||||
<div v-if="getPlatformQrLoginStat(currentQrPlatformId)?.qr_status" class="platform-qr-status">
|
||||
{{ tm('platformQr.status') }}: {{ getPlatformQrLoginStat(currentQrPlatformId)?.qr_status || tm('platformQr.waiting') }}
|
||||
</div>
|
||||
<div v-if="getPlatformQrLoginStat(currentQrPlatformId)?.contact_links" class="platform-qr-list">
|
||||
<div
|
||||
v-for="item in getPlatformQrItems(currentQrPlatformId)"
|
||||
:key="item.open_kfid || item.qrcode_img_content || item.qrcode"
|
||||
class="platform-qr-list-item"
|
||||
>
|
||||
<div class="platform-qr-name">{{ item.name || item.open_kfid || tm('platformQr.title') }}</div>
|
||||
<QrCodeViewer
|
||||
:value="(item.qrcode_img_content || item.qrcode || '')"
|
||||
:alt="item.name || tm('platformQr.title')"
|
||||
:size="180"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<QrCodeViewer
|
||||
:value="(getPlatformQrLoginStat(currentQrPlatformId)?.qrcode_img_content || getPlatformQrLoginStat(currentQrPlatformId)?.qrcode || '')"
|
||||
v-else
|
||||
:value="(getPlatformQrItems(currentQrPlatformId)[0]?.qrcode_img_content || getPlatformQrItems(currentQrPlatformId)[0]?.qrcode || '')"
|
||||
:alt="tm('platformQr.title')"
|
||||
/>
|
||||
</v-card-text>
|
||||
@@ -177,6 +207,95 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="showWecomKfDialog" max-width="760">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center pa-4">
|
||||
<v-icon class="me-2">mdi-robot</v-icon>
|
||||
{{ tm('wecomKf.title') }}
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon variant="text" :loading="wecomKfLoading" @click="loadWecomKfAccounts">
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text class="px-4 pb-4">
|
||||
<div class="wecom-kf-form">
|
||||
<v-text-field
|
||||
v-model="wecomKfForm.name"
|
||||
:label="tm('wecomKf.name')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
<v-file-input
|
||||
v-model="wecomKfAvatarFile"
|
||||
:label="tm('wecomKf.avatarFile')"
|
||||
accept="image/*"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
prepend-icon=""
|
||||
prepend-inner-icon="mdi-robot"
|
||||
:loading="wecomKfAvatarUploading"
|
||||
@update:model-value="uploadWecomKfAvatar"
|
||||
/>
|
||||
<div class="wecom-kf-actions">
|
||||
<v-btn variant="tonal" @click="resetWecomKfForm" :disabled="wecomKfSaving">
|
||||
{{ tm('wecomKf.cancelEdit') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="saveWecomKfAccount" :loading="wecomKfSaving">
|
||||
{{ wecomKfForm.open_kfid ? tm('wecomKf.update') : tm('wecomKf.create') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="wecomKfError"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mt-4"
|
||||
>
|
||||
{{ wecomKfError }}
|
||||
</v-alert>
|
||||
|
||||
<v-table class="mt-4" density="comfortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ tm('wecomKf.name') }}</th>
|
||||
<th>open_kfid</th>
|
||||
<th class="text-right">{{ tm('wecomKf.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="wecomKfAccounts.length === 0">
|
||||
<td colspan="3" class="text-center text-medium-emphasis py-6">
|
||||
{{ wecomKfLoading ? tm('wecomKf.loading') : tm('wecomKf.empty') }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="account in wecomKfAccounts" :key="account.open_kfid">
|
||||
<td>{{ account.name || '-' }}</td>
|
||||
<td class="wecom-kf-openid">{{ account.open_kfid }}</td>
|
||||
<td class="text-right">
|
||||
<v-btn icon variant="text" size="small" @click="editWecomKfAccount(account)">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon variant="text" size="small" color="error" @click="deleteWecomKfAccount(account)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4 pt-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="tonal" color="primary" @click="showWecomKfDialog = false">
|
||||
{{ tm('wecomKf.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 错误详情对话框 -->
|
||||
<v-dialog v-model="showErrorDialog" max-width="700">
|
||||
<v-card>
|
||||
@@ -292,6 +411,19 @@ export default {
|
||||
currentErrorPlatform: null,
|
||||
showQrDialog: false,
|
||||
currentQrPlatformId: "",
|
||||
showWecomKfDialog: false,
|
||||
currentWecomKfPlatformId: "",
|
||||
wecomKfAccounts: [],
|
||||
wecomKfLoading: false,
|
||||
wecomKfSaving: false,
|
||||
wecomKfAvatarUploading: false,
|
||||
wecomKfAvatarFile: null,
|
||||
wecomKfError: "",
|
||||
wecomKfForm: {
|
||||
open_kfid: "",
|
||||
name: "",
|
||||
media_id: "",
|
||||
},
|
||||
|
||||
store: useCommonStore()
|
||||
}
|
||||
@@ -390,8 +522,21 @@ export default {
|
||||
},
|
||||
|
||||
hasQrPayload(platformId) {
|
||||
return this.getPlatformQrItems(platformId).length > 0;
|
||||
},
|
||||
|
||||
getPlatformQrItems(platformId) {
|
||||
const stat = this.getPlatformQrLoginStat(platformId);
|
||||
return Boolean(stat?.qrcode_img_content || stat?.qrcode);
|
||||
if (!stat) {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(stat.contact_links)) {
|
||||
return stat.contact_links.filter((item) => item?.qrcode_img_content || item?.qrcode);
|
||||
}
|
||||
if (stat.qrcode_img_content || stat.qrcode) {
|
||||
return [stat];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
getPlatformQrLoginStat(platformId) {
|
||||
@@ -399,9 +544,12 @@ export default {
|
||||
if (stat?.weixin_oc) {
|
||||
return stat.weixin_oc;
|
||||
}
|
||||
if (stat?.wecom_kf) {
|
||||
return stat.wecom_kf;
|
||||
}
|
||||
if (stat && typeof stat === "object") {
|
||||
for (const value of Object.values(stat)) {
|
||||
if (value && typeof value === "object" && ("qrcode_img_content" in value || "qrcode" in value)) {
|
||||
if (value && typeof value === "object" && ("qrcode_img_content" in value || "qrcode" in value || "contact_links" in value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -414,6 +562,144 @@ export default {
|
||||
this.showQrDialog = true;
|
||||
},
|
||||
|
||||
openWecomKfDialog(platformId) {
|
||||
this.currentWecomKfPlatformId = platformId;
|
||||
this.showWecomKfDialog = true;
|
||||
this.resetWecomKfForm();
|
||||
this.loadWecomKfAccounts();
|
||||
},
|
||||
|
||||
resetWecomKfForm() {
|
||||
this.wecomKfForm = {
|
||||
open_kfid: "",
|
||||
name: "",
|
||||
media_id: "",
|
||||
};
|
||||
this.wecomKfAvatarFile = null;
|
||||
this.wecomKfError = "";
|
||||
},
|
||||
|
||||
async loadWecomKfAccounts() {
|
||||
if (!this.currentWecomKfPlatformId) {
|
||||
return;
|
||||
}
|
||||
this.wecomKfLoading = true;
|
||||
this.wecomKfError = "";
|
||||
try {
|
||||
const platformId = encodeURIComponent(this.currentWecomKfPlatformId);
|
||||
const res = await axios.get(`/api/platform/wecom-kf/${platformId}/accounts`);
|
||||
if (res.data.status === 'ok') {
|
||||
this.wecomKfAccounts = res.data.data.accounts || [];
|
||||
await this.getPlatformStats();
|
||||
} else {
|
||||
this.wecomKfError = res.data.message || this.tm('wecomKf.loadFailed');
|
||||
}
|
||||
} catch (err) {
|
||||
this.wecomKfError = err.response?.data?.message || err.message || this.tm('wecomKf.loadFailed');
|
||||
} finally {
|
||||
this.wecomKfLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
editWecomKfAccount(account) {
|
||||
this.wecomKfForm = {
|
||||
open_kfid: account.open_kfid || "",
|
||||
name: account.name || "",
|
||||
media_id: "",
|
||||
};
|
||||
this.wecomKfError = "";
|
||||
},
|
||||
|
||||
async uploadWecomKfAvatar(fileValue) {
|
||||
const file = Array.isArray(fileValue) ? fileValue[0] : fileValue;
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
this.wecomKfAvatarUploading = true;
|
||||
this.wecomKfError = "";
|
||||
try {
|
||||
const platformId = encodeURIComponent(this.currentWecomKfPlatformId);
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await axios.post(`/api/platform/wecom-kf/${platformId}/avatar`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
const mediaId = res.data?.data?.media_id;
|
||||
if (mediaId) {
|
||||
this.wecomKfForm.media_id = mediaId;
|
||||
this.showSuccess(this.tm('wecomKf.avatarUploadSuccess'));
|
||||
} else {
|
||||
this.wecomKfError = this.tm('wecomKf.avatarUploadFailed');
|
||||
}
|
||||
} catch (err) {
|
||||
this.wecomKfError = err.response?.data?.message || err.message || this.tm('wecomKf.avatarUploadFailed');
|
||||
} finally {
|
||||
this.wecomKfAvatarUploading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async saveWecomKfAccount() {
|
||||
const name = String(this.wecomKfForm.name || "").trim();
|
||||
const mediaId = String(this.wecomKfForm.media_id || "").trim();
|
||||
if (!this.wecomKfForm.open_kfid && !name) {
|
||||
this.wecomKfError = this.tm('wecomKf.nameRequired');
|
||||
return;
|
||||
}
|
||||
if (this.wecomKfForm.open_kfid && !name && !mediaId) {
|
||||
this.wecomKfError = this.tm('wecomKf.updateRequired');
|
||||
return;
|
||||
}
|
||||
|
||||
this.wecomKfSaving = true;
|
||||
this.wecomKfError = "";
|
||||
try {
|
||||
const platformId = encodeURIComponent(this.currentWecomKfPlatformId);
|
||||
const payload = { name, media_id: mediaId };
|
||||
if (this.wecomKfForm.open_kfid) {
|
||||
const openKfid = encodeURIComponent(this.wecomKfForm.open_kfid);
|
||||
await axios.put(
|
||||
`/api/platform/wecom-kf/${platformId}/accounts/${openKfid}`,
|
||||
payload
|
||||
);
|
||||
this.showSuccess(this.tm('wecomKf.updateSuccess'));
|
||||
} else {
|
||||
await axios.post(`/api/platform/wecom-kf/${platformId}/accounts`, payload);
|
||||
this.showSuccess(this.tm('wecomKf.createSuccess'));
|
||||
}
|
||||
this.resetWecomKfForm();
|
||||
await this.loadWecomKfAccounts();
|
||||
} catch (err) {
|
||||
this.wecomKfError = err.response?.data?.message || err.message || this.tm('wecomKf.saveFailed');
|
||||
} finally {
|
||||
this.wecomKfSaving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteWecomKfAccount(account) {
|
||||
const name = account.name || account.open_kfid;
|
||||
const message = this.tm('wecomKf.deleteConfirm', { name });
|
||||
if (!(await askForConfirmationDialog(message, this.confirmDialog))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.wecomKfSaving = true;
|
||||
this.wecomKfError = "";
|
||||
try {
|
||||
const platformId = encodeURIComponent(this.currentWecomKfPlatformId);
|
||||
const openKfid = encodeURIComponent(account.open_kfid);
|
||||
await axios.delete(`/api/platform/wecom-kf/${platformId}/accounts/${openKfid}`);
|
||||
this.showSuccess(this.tm('wecomKf.deleteSuccess'));
|
||||
if (this.wecomKfForm.open_kfid === account.open_kfid) {
|
||||
this.resetWecomKfForm();
|
||||
}
|
||||
await this.loadWecomKfAccounts();
|
||||
} catch (err) {
|
||||
this.wecomKfError = err.response?.data?.message || err.message || this.tm('wecomKf.deleteFailed');
|
||||
} finally {
|
||||
this.wecomKfSaving = false;
|
||||
}
|
||||
},
|
||||
|
||||
getStatusColor(status) {
|
||||
switch (status) {
|
||||
case 'running': return 'success';
|
||||
@@ -697,4 +983,55 @@ export default {
|
||||
margin-bottom: 10px;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.platform-qr-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.platform-qr-list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.platform-qr-name {
|
||||
max-width: 100%;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wecom-kf-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 1fr) minmax(180px, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.wecom-kf-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wecom-kf-openid {
|
||||
max-width: 260px;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.wecom-kf-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wecom-kf-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user